diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000..3bc3844 Binary files /dev/null and b/.DS_Store differ diff --git a/coverage/lcov-report/base.css b/coverage/lcov-report/base.css new file mode 100644 index 0000000..f418035 --- /dev/null +++ b/coverage/lcov-report/base.css @@ -0,0 +1,224 @@ +body, html { + margin:0; padding: 0; + height: 100%; +} +body { + font-family: Helvetica Neue, Helvetica, Arial; + font-size: 14px; + color:#333; +} +.small { font-size: 12px; } +*, *:after, *:before { + -webkit-box-sizing:border-box; + -moz-box-sizing:border-box; + box-sizing:border-box; + } +h1 { font-size: 20px; margin: 0;} +h2 { font-size: 14px; } +pre { + font: 12px/1.4 Consolas, "Liberation Mono", Menlo, Courier, monospace; + margin: 0; + padding: 0; + -moz-tab-size: 2; + -o-tab-size: 2; + tab-size: 2; +} +a { color:#0074D9; text-decoration:none; } +a:hover { text-decoration:underline; } +.strong { font-weight: bold; } +.space-top1 { padding: 10px 0 0 0; } +.pad2y { padding: 20px 0; } +.pad1y { padding: 10px 0; } +.pad2x { padding: 0 20px; } +.pad2 { padding: 20px; } +.pad1 { padding: 10px; } +.space-left2 { padding-left:55px; } +.space-right2 { padding-right:20px; } +.center { text-align:center; } +.clearfix { display:block; } +.clearfix:after { + content:''; + display:block; + height:0; + clear:both; + visibility:hidden; + } +.fl { float: left; } +@media only screen and (max-width:640px) { + .col3 { width:100%; max-width:100%; } + .hide-mobile { display:none!important; } +} + +.quiet { + color: #7f7f7f; + color: rgba(0,0,0,0.5); +} +.quiet a { opacity: 0.7; } + +.fraction { + font-family: Consolas, 'Liberation Mono', Menlo, Courier, monospace; + font-size: 10px; + color: #555; + background: #E8E8E8; + padding: 4px 5px; + border-radius: 3px; + vertical-align: middle; +} + +div.path a:link, div.path a:visited { color: #333; } +table.coverage { + border-collapse: collapse; + margin: 10px 0 0 0; + padding: 0; +} + +table.coverage td { + margin: 0; + padding: 0; + vertical-align: top; +} +table.coverage td.line-count { + text-align: right; + padding: 0 5px 0 20px; +} +table.coverage td.line-coverage { + text-align: right; + padding-right: 10px; + min-width:20px; +} + +table.coverage td span.cline-any { + display: inline-block; + padding: 0 5px; + width: 100%; +} +.missing-if-branch { + display: inline-block; + margin-right: 5px; + border-radius: 3px; + position: relative; + padding: 0 4px; + background: #333; + color: yellow; +} + +.skip-if-branch { + display: none; + margin-right: 10px; + position: relative; + padding: 0 4px; + background: #ccc; + color: white; +} +.missing-if-branch .typ, .skip-if-branch .typ { + color: inherit !important; +} +.coverage-summary { + border-collapse: collapse; + width: 100%; +} +.coverage-summary tr { border-bottom: 1px solid #bbb; } +.keyline-all { border: 1px solid #ddd; } +.coverage-summary td, .coverage-summary th { padding: 10px; } +.coverage-summary tbody { border: 1px solid #bbb; } +.coverage-summary td { border-right: 1px solid #bbb; } +.coverage-summary td:last-child { border-right: none; } +.coverage-summary th { + text-align: left; + font-weight: normal; + white-space: nowrap; +} +.coverage-summary th.file { border-right: none !important; } +.coverage-summary th.pct { } +.coverage-summary th.pic, +.coverage-summary th.abs, +.coverage-summary td.pct, +.coverage-summary td.abs { text-align: right; } +.coverage-summary td.file { white-space: nowrap; } +.coverage-summary td.pic { min-width: 120px !important; } +.coverage-summary tfoot td { } + +.coverage-summary .sorter { + height: 10px; + width: 7px; + display: inline-block; + margin-left: 0.5em; + background: url(sort-arrow-sprite.png) no-repeat scroll 0 0 transparent; +} +.coverage-summary .sorted .sorter { + background-position: 0 -20px; +} +.coverage-summary .sorted-desc .sorter { + background-position: 0 -10px; +} +.status-line { height: 10px; } +/* yellow */ +.cbranch-no { background: yellow !important; color: #111; } +/* dark red */ +.red.solid, .status-line.low, .low .cover-fill { background:#C21F39 } +.low .chart { border:1px solid #C21F39 } +.highlighted, +.highlighted .cstat-no, .highlighted .fstat-no, .highlighted .cbranch-no{ + background: #C21F39 !important; +} +/* medium red */ +.cstat-no, .fstat-no, .cbranch-no, .cbranch-no { background:#F6C6CE } +/* light red */ +.low, .cline-no { background:#FCE1E5 } +/* light green */ +.high, .cline-yes { background:rgb(230,245,208) } +/* medium green */ +.cstat-yes { background:rgb(161,215,106) } +/* dark green */ +.status-line.high, .high .cover-fill { background:rgb(77,146,33) } +.high .chart { border:1px solid rgb(77,146,33) } +/* dark yellow (gold) */ +.status-line.medium, .medium .cover-fill { background: #f9cd0b; } +.medium .chart { border:1px solid #f9cd0b; } +/* light yellow */ +.medium { background: #fff4c2; } + +.cstat-skip { background: #ddd; color: #111; } +.fstat-skip { background: #ddd; color: #111 !important; } +.cbranch-skip { background: #ddd !important; color: #111; } + +span.cline-neutral { background: #eaeaea; } + +.coverage-summary td.empty { + opacity: .5; + padding-top: 4px; + padding-bottom: 4px; + line-height: 1; + color: #888; +} + +.cover-fill, .cover-empty { + display:inline-block; + height: 12px; +} +.chart { + line-height: 0; +} +.cover-empty { + background: white; +} +.cover-full { + border-right: none !important; +} +pre.prettyprint { + border: none !important; + padding: 0 !important; + margin: 0 !important; +} +.com { color: #999 !important; } +.ignore-none { color: #999; font-weight: normal; } + +.wrapper { + min-height: 100%; + height: auto !important; + height: 100%; + margin: 0 auto -48px; +} +.footer, .push { + height: 48px; +} diff --git a/coverage/lcov-report/block-navigation.js b/coverage/lcov-report/block-navigation.js new file mode 100644 index 0000000..530d1ed --- /dev/null +++ b/coverage/lcov-report/block-navigation.js @@ -0,0 +1,87 @@ +/* eslint-disable */ +var jumpToCode = (function init() { + // Classes of code we would like to highlight in the file view + var missingCoverageClasses = ['.cbranch-no', '.cstat-no', '.fstat-no']; + + // Elements to highlight in the file listing view + var fileListingElements = ['td.pct.low']; + + // We don't want to select elements that are direct descendants of another match + var notSelector = ':not(' + missingCoverageClasses.join('):not(') + ') > '; // becomes `:not(a):not(b) > ` + + // Selector that finds elements on the page to which we can jump + var selector = + fileListingElements.join(', ') + + ', ' + + notSelector + + missingCoverageClasses.join(', ' + notSelector); // becomes `:not(a):not(b) > a, :not(a):not(b) > b` + + // The NodeList of matching elements + var missingCoverageElements = document.querySelectorAll(selector); + + var currentIndex; + + function toggleClass(index) { + missingCoverageElements + .item(currentIndex) + .classList.remove('highlighted'); + missingCoverageElements.item(index).classList.add('highlighted'); + } + + function makeCurrent(index) { + toggleClass(index); + currentIndex = index; + missingCoverageElements.item(index).scrollIntoView({ + behavior: 'smooth', + block: 'center', + inline: 'center' + }); + } + + function goToPrevious() { + var nextIndex = 0; + if (typeof currentIndex !== 'number' || currentIndex === 0) { + nextIndex = missingCoverageElements.length - 1; + } else if (missingCoverageElements.length > 1) { + nextIndex = currentIndex - 1; + } + + makeCurrent(nextIndex); + } + + function goToNext() { + var nextIndex = 0; + + if ( + typeof currentIndex === 'number' && + currentIndex < missingCoverageElements.length - 1 + ) { + nextIndex = currentIndex + 1; + } + + makeCurrent(nextIndex); + } + + return function jump(event) { + if ( + document.getElementById('fileSearch') === document.activeElement && + document.activeElement != null + ) { + // if we're currently focused on the search input, we don't want to navigate + return; + } + + switch (event.which) { + case 78: // n + case 74: // j + goToNext(); + break; + case 66: // b + case 75: // k + case 80: // p + goToPrevious(); + break; + } + }; +})(); +window.addEventListener('keydown', jumpToCode); diff --git a/coverage/lcov-report/favicon.png b/coverage/lcov-report/favicon.png new file mode 100644 index 0000000..c1525b8 Binary files /dev/null and b/coverage/lcov-report/favicon.png differ diff --git a/coverage/lcov-report/index.html b/coverage/lcov-report/index.html new file mode 100644 index 0000000..21f03f9 --- /dev/null +++ b/coverage/lcov-report/index.html @@ -0,0 +1,131 @@ + + + + + + Code coverage report for All files + + + + + + + + + +
+
+

All files

+
+ +
+ 46.93% + Statements + 322/686 +
+ + +
+ 44.58% + Branches + 280/628 +
+ + +
+ 37.97% + Functions + 30/79 +
+ + +
+ 46.32% + Lines + 284/613 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FileStatementsBranchesFunctionsLines
src +
+
92.94%79/8574.07%80/108100%5/595.94%71/74
src/server +
+
40.43%243/60138.46%200/52033.78%25/7439.51%213/539
+
+
+
+ + + + + + + + \ No newline at end of file diff --git a/coverage/lcov-report/prettify.css b/coverage/lcov-report/prettify.css new file mode 100644 index 0000000..b317a7c --- /dev/null +++ b/coverage/lcov-report/prettify.css @@ -0,0 +1 @@ +.pln{color:#000}@media screen{.str{color:#080}.kwd{color:#008}.com{color:#800}.typ{color:#606}.lit{color:#066}.pun,.opn,.clo{color:#660}.tag{color:#008}.atn{color:#606}.atv{color:#080}.dec,.var{color:#606}.fun{color:red}}@media print,projection{.str{color:#060}.kwd{color:#006;font-weight:bold}.com{color:#600;font-style:italic}.typ{color:#404;font-weight:bold}.lit{color:#044}.pun,.opn,.clo{color:#440}.tag{color:#006;font-weight:bold}.atn{color:#404}.atv{color:#060}}pre.prettyprint{padding:2px;border:1px solid #888}ol.linenums{margin-top:0;margin-bottom:0}li.L0,li.L1,li.L2,li.L3,li.L5,li.L6,li.L7,li.L8{list-style-type:none}li.L1,li.L3,li.L5,li.L7,li.L9{background:#eee} diff --git a/coverage/lcov-report/prettify.js b/coverage/lcov-report/prettify.js new file mode 100644 index 0000000..b322523 --- /dev/null +++ b/coverage/lcov-report/prettify.js @@ -0,0 +1,2 @@ +/* eslint-disable */ +window.PR_SHOULD_USE_CONTINUATION=true;(function(){var h=["break,continue,do,else,for,if,return,while"];var u=[h,"auto,case,char,const,default,double,enum,extern,float,goto,int,long,register,short,signed,sizeof,static,struct,switch,typedef,union,unsigned,void,volatile"];var p=[u,"catch,class,delete,false,import,new,operator,private,protected,public,this,throw,true,try,typeof"];var l=[p,"alignof,align_union,asm,axiom,bool,concept,concept_map,const_cast,constexpr,decltype,dynamic_cast,explicit,export,friend,inline,late_check,mutable,namespace,nullptr,reinterpret_cast,static_assert,static_cast,template,typeid,typename,using,virtual,where"];var x=[p,"abstract,boolean,byte,extends,final,finally,implements,import,instanceof,null,native,package,strictfp,super,synchronized,throws,transient"];var R=[x,"as,base,by,checked,decimal,delegate,descending,dynamic,event,fixed,foreach,from,group,implicit,in,interface,internal,into,is,lock,object,out,override,orderby,params,partial,readonly,ref,sbyte,sealed,stackalloc,string,select,uint,ulong,unchecked,unsafe,ushort,var"];var r="all,and,by,catch,class,else,extends,false,finally,for,if,in,is,isnt,loop,new,no,not,null,of,off,on,or,return,super,then,true,try,unless,until,when,while,yes";var w=[p,"debugger,eval,export,function,get,null,set,undefined,var,with,Infinity,NaN"];var s="caller,delete,die,do,dump,elsif,eval,exit,foreach,for,goto,if,import,last,local,my,next,no,our,print,package,redo,require,sub,undef,unless,until,use,wantarray,while,BEGIN,END";var I=[h,"and,as,assert,class,def,del,elif,except,exec,finally,from,global,import,in,is,lambda,nonlocal,not,or,pass,print,raise,try,with,yield,False,True,None"];var f=[h,"alias,and,begin,case,class,def,defined,elsif,end,ensure,false,in,module,next,nil,not,or,redo,rescue,retry,self,super,then,true,undef,unless,until,when,yield,BEGIN,END"];var H=[h,"case,done,elif,esac,eval,fi,function,in,local,set,then,until"];var A=[l,R,w,s+I,f,H];var e=/^(DIR|FILE|vector|(de|priority_)?queue|list|stack|(const_)?iterator|(multi)?(set|map)|bitset|u?(int|float)\d*)/;var C="str";var z="kwd";var j="com";var O="typ";var G="lit";var L="pun";var F="pln";var m="tag";var E="dec";var J="src";var P="atn";var n="atv";var N="nocode";var M="(?:^^\\.?|[+-]|\\!|\\!=|\\!==|\\#|\\%|\\%=|&|&&|&&=|&=|\\(|\\*|\\*=|\\+=|\\,|\\-=|\\->|\\/|\\/=|:|::|\\;|<|<<|<<=|<=|=|==|===|>|>=|>>|>>=|>>>|>>>=|\\?|\\@|\\[|\\^|\\^=|\\^\\^|\\^\\^=|\\{|\\||\\|=|\\|\\||\\|\\|=|\\~|break|case|continue|delete|do|else|finally|instanceof|return|throw|try|typeof)\\s*";function k(Z){var ad=0;var S=false;var ac=false;for(var V=0,U=Z.length;V122)){if(!(al<65||ag>90)){af.push([Math.max(65,ag)|32,Math.min(al,90)|32])}if(!(al<97||ag>122)){af.push([Math.max(97,ag)&~32,Math.min(al,122)&~32])}}}}af.sort(function(av,au){return(av[0]-au[0])||(au[1]-av[1])});var ai=[];var ap=[NaN,NaN];for(var ar=0;arat[0]){if(at[1]+1>at[0]){an.push("-")}an.push(T(at[1]))}}an.push("]");return an.join("")}function W(al){var aj=al.source.match(new RegExp("(?:\\[(?:[^\\x5C\\x5D]|\\\\[\\s\\S])*\\]|\\\\u[A-Fa-f0-9]{4}|\\\\x[A-Fa-f0-9]{2}|\\\\[0-9]+|\\\\[^ux0-9]|\\(\\?[:!=]|[\\(\\)\\^]|[^\\x5B\\x5C\\(\\)\\^]+)","g"));var ah=aj.length;var an=[];for(var ak=0,am=0;ak=2&&ai==="["){aj[ak]=X(ag)}else{if(ai!=="\\"){aj[ak]=ag.replace(/[a-zA-Z]/g,function(ao){var ap=ao.charCodeAt(0);return"["+String.fromCharCode(ap&~32,ap|32)+"]"})}}}}return aj.join("")}var aa=[];for(var V=0,U=Z.length;V=0;){S[ac.charAt(ae)]=Y}}var af=Y[1];var aa=""+af;if(!ag.hasOwnProperty(aa)){ah.push(af);ag[aa]=null}}ah.push(/[\0-\uffff]/);V=k(ah)})();var X=T.length;var W=function(ah){var Z=ah.sourceCode,Y=ah.basePos;var ad=[Y,F];var af=0;var an=Z.match(V)||[];var aj={};for(var ae=0,aq=an.length;ae=5&&"lang-"===ap.substring(0,5);if(am&&!(ai&&typeof ai[1]==="string")){am=false;ap=J}if(!am){aj[ag]=ap}}var ab=af;af+=ag.length;if(!am){ad.push(Y+ab,ap)}else{var al=ai[1];var ak=ag.indexOf(al);var ac=ak+al.length;if(ai[2]){ac=ag.length-ai[2].length;ak=ac-al.length}var ar=ap.substring(5);B(Y+ab,ag.substring(0,ak),W,ad);B(Y+ab+ak,al,q(ar,al),ad);B(Y+ab+ac,ag.substring(ac),W,ad)}}ah.decorations=ad};return W}function i(T){var W=[],S=[];if(T.tripleQuotedStrings){W.push([C,/^(?:\'\'\'(?:[^\'\\]|\\[\s\S]|\'{1,2}(?=[^\']))*(?:\'\'\'|$)|\"\"\"(?:[^\"\\]|\\[\s\S]|\"{1,2}(?=[^\"]))*(?:\"\"\"|$)|\'(?:[^\\\']|\\[\s\S])*(?:\'|$)|\"(?:[^\\\"]|\\[\s\S])*(?:\"|$))/,null,"'\""])}else{if(T.multiLineStrings){W.push([C,/^(?:\'(?:[^\\\']|\\[\s\S])*(?:\'|$)|\"(?:[^\\\"]|\\[\s\S])*(?:\"|$)|\`(?:[^\\\`]|\\[\s\S])*(?:\`|$))/,null,"'\"`"])}else{W.push([C,/^(?:\'(?:[^\\\'\r\n]|\\.)*(?:\'|$)|\"(?:[^\\\"\r\n]|\\.)*(?:\"|$))/,null,"\"'"])}}if(T.verbatimStrings){S.push([C,/^@\"(?:[^\"]|\"\")*(?:\"|$)/,null])}var Y=T.hashComments;if(Y){if(T.cStyleComments){if(Y>1){W.push([j,/^#(?:##(?:[^#]|#(?!##))*(?:###|$)|.*)/,null,"#"])}else{W.push([j,/^#(?:(?:define|elif|else|endif|error|ifdef|include|ifndef|line|pragma|undef|warning)\b|[^\r\n]*)/,null,"#"])}S.push([C,/^<(?:(?:(?:\.\.\/)*|\/?)(?:[\w-]+(?:\/[\w-]+)+)?[\w-]+\.h|[a-z]\w*)>/,null])}else{W.push([j,/^#[^\r\n]*/,null,"#"])}}if(T.cStyleComments){S.push([j,/^\/\/[^\r\n]*/,null]);S.push([j,/^\/\*[\s\S]*?(?:\*\/|$)/,null])}if(T.regexLiterals){var X=("/(?=[^/*])(?:[^/\\x5B\\x5C]|\\x5C[\\s\\S]|\\x5B(?:[^\\x5C\\x5D]|\\x5C[\\s\\S])*(?:\\x5D|$))+/");S.push(["lang-regex",new RegExp("^"+M+"("+X+")")])}var V=T.types;if(V){S.push([O,V])}var U=(""+T.keywords).replace(/^ | $/g,"");if(U.length){S.push([z,new RegExp("^(?:"+U.replace(/[\s,]+/g,"|")+")\\b"),null])}W.push([F,/^\s+/,null," \r\n\t\xA0"]);S.push([G,/^@[a-z_$][a-z_$@0-9]*/i,null],[O,/^(?:[@_]?[A-Z]+[a-z][A-Za-z_$@0-9]*|\w+_t\b)/,null],[F,/^[a-z_$][a-z_$@0-9]*/i,null],[G,new RegExp("^(?:0x[a-f0-9]+|(?:\\d(?:_\\d+)*\\d*(?:\\.\\d*)?|\\.\\d\\+)(?:e[+\\-]?\\d+)?)[a-z]*","i"),null,"0123456789"],[F,/^\\[\s\S]?/,null],[L,/^.[^\s\w\.$@\'\"\`\/\#\\]*/,null]);return g(W,S)}var K=i({keywords:A,hashComments:true,cStyleComments:true,multiLineStrings:true,regexLiterals:true});function Q(V,ag){var U=/(?:^|\s)nocode(?:\s|$)/;var ab=/\r\n?|\n/;var ac=V.ownerDocument;var S;if(V.currentStyle){S=V.currentStyle.whiteSpace}else{if(window.getComputedStyle){S=ac.defaultView.getComputedStyle(V,null).getPropertyValue("white-space")}}var Z=S&&"pre"===S.substring(0,3);var af=ac.createElement("LI");while(V.firstChild){af.appendChild(V.firstChild)}var W=[af];function ae(al){switch(al.nodeType){case 1:if(U.test(al.className)){break}if("BR"===al.nodeName){ad(al);if(al.parentNode){al.parentNode.removeChild(al)}}else{for(var an=al.firstChild;an;an=an.nextSibling){ae(an)}}break;case 3:case 4:if(Z){var am=al.nodeValue;var aj=am.match(ab);if(aj){var ai=am.substring(0,aj.index);al.nodeValue=ai;var ah=am.substring(aj.index+aj[0].length);if(ah){var ak=al.parentNode;ak.insertBefore(ac.createTextNode(ah),al.nextSibling)}ad(al);if(!ai){al.parentNode.removeChild(al)}}}break}}function ad(ak){while(!ak.nextSibling){ak=ak.parentNode;if(!ak){return}}function ai(al,ar){var aq=ar?al.cloneNode(false):al;var ao=al.parentNode;if(ao){var ap=ai(ao,1);var an=al.nextSibling;ap.appendChild(aq);for(var am=an;am;am=an){an=am.nextSibling;ap.appendChild(am)}}return aq}var ah=ai(ak.nextSibling,0);for(var aj;(aj=ah.parentNode)&&aj.nodeType===1;){ah=aj}W.push(ah)}for(var Y=0;Y=S){ah+=2}if(V>=ap){Z+=2}}}var t={};function c(U,V){for(var S=V.length;--S>=0;){var T=V[S];if(!t.hasOwnProperty(T)){t[T]=U}else{if(window.console){console.warn("cannot override language handler %s",T)}}}}function q(T,S){if(!(T&&t.hasOwnProperty(T))){T=/^\s*]*(?:>|$)/],[j,/^<\!--[\s\S]*?(?:-\->|$)/],["lang-",/^<\?([\s\S]+?)(?:\?>|$)/],["lang-",/^<%([\s\S]+?)(?:%>|$)/],[L,/^(?:<[%?]|[%?]>)/],["lang-",/^]*>([\s\S]+?)<\/xmp\b[^>]*>/i],["lang-js",/^]*>([\s\S]*?)(<\/script\b[^>]*>)/i],["lang-css",/^]*>([\s\S]*?)(<\/style\b[^>]*>)/i],["lang-in.tag",/^(<\/?[a-z][^<>]*>)/i]]),["default-markup","htm","html","mxml","xhtml","xml","xsl"]);c(g([[F,/^[\s]+/,null," \t\r\n"],[n,/^(?:\"[^\"]*\"?|\'[^\']*\'?)/,null,"\"'"]],[[m,/^^<\/?[a-z](?:[\w.:-]*\w)?|\/?>$/i],[P,/^(?!style[\s=]|on)[a-z](?:[\w:-]*\w)?/i],["lang-uq.val",/^=\s*([^>\'\"\s]*(?:[^>\'\"\s\/]|\/(?=\s)))/],[L,/^[=<>\/]+/],["lang-js",/^on\w+\s*=\s*\"([^\"]+)\"/i],["lang-js",/^on\w+\s*=\s*\'([^\']+)\'/i],["lang-js",/^on\w+\s*=\s*([^\"\'>\s]+)/i],["lang-css",/^style\s*=\s*\"([^\"]+)\"/i],["lang-css",/^style\s*=\s*\'([^\']+)\'/i],["lang-css",/^style\s*=\s*([^\"\'>\s]+)/i]]),["in.tag"]);c(g([],[[n,/^[\s\S]+/]]),["uq.val"]);c(i({keywords:l,hashComments:true,cStyleComments:true,types:e}),["c","cc","cpp","cxx","cyc","m"]);c(i({keywords:"null,true,false"}),["json"]);c(i({keywords:R,hashComments:true,cStyleComments:true,verbatimStrings:true,types:e}),["cs"]);c(i({keywords:x,cStyleComments:true}),["java"]);c(i({keywords:H,hashComments:true,multiLineStrings:true}),["bsh","csh","sh"]);c(i({keywords:I,hashComments:true,multiLineStrings:true,tripleQuotedStrings:true}),["cv","py"]);c(i({keywords:s,hashComments:true,multiLineStrings:true,regexLiterals:true}),["perl","pl","pm"]);c(i({keywords:f,hashComments:true,multiLineStrings:true,regexLiterals:true}),["rb"]);c(i({keywords:w,cStyleComments:true,regexLiterals:true}),["js"]);c(i({keywords:r,hashComments:3,cStyleComments:true,multilineStrings:true,tripleQuotedStrings:true,regexLiterals:true}),["coffee"]);c(g([],[[C,/^[\s\S]+/]]),["regex"]);function d(V){var U=V.langExtension;try{var S=a(V.sourceNode);var T=S.sourceCode;V.sourceCode=T;V.spans=S.spans;V.basePos=0;q(U,T)(V);D(V)}catch(W){if("console" in window){console.log(W&&W.stack?W.stack:W)}}}function y(W,V,U){var S=document.createElement("PRE");S.innerHTML=W;if(U){Q(S,U)}var T={langExtension:V,numberLines:U,sourceNode:S};d(T);return S.innerHTML}function b(ad){function Y(af){return document.getElementsByTagName(af)}var ac=[Y("pre"),Y("code"),Y("xmp")];var T=[];for(var aa=0;aa=0){var ah=ai.match(ab);var am;if(!ah&&(am=o(aj))&&"CODE"===am.tagName){ah=am.className.match(ab)}if(ah){ah=ah[1]}var al=false;for(var ak=aj.parentNode;ak;ak=ak.parentNode){if((ak.tagName==="pre"||ak.tagName==="code"||ak.tagName==="xmp")&&ak.className&&ak.className.indexOf("prettyprint")>=0){al=true;break}}if(!al){var af=aj.className.match(/\blinenums\b(?::(\d+))?/);af=af?af[1]&&af[1].length?+af[1]:true:false;if(af){Q(aj,af)}S={langExtension:ah,sourceNode:aj,numberLines:af};d(S)}}}if(X]*(?:>|$)/],[PR.PR_COMMENT,/^<\!--[\s\S]*?(?:-\->|$)/],[PR.PR_PUNCTUATION,/^(?:<[%?]|[%?]>)/],["lang-",/^<\?([\s\S]+?)(?:\?>|$)/],["lang-",/^<%([\s\S]+?)(?:%>|$)/],["lang-",/^]*>([\s\S]+?)<\/xmp\b[^>]*>/i],["lang-handlebars",/^]*type\s*=\s*['"]?text\/x-handlebars-template['"]?\b[^>]*>([\s\S]*?)(<\/script\b[^>]*>)/i],["lang-js",/^]*>([\s\S]*?)(<\/script\b[^>]*>)/i],["lang-css",/^]*>([\s\S]*?)(<\/style\b[^>]*>)/i],["lang-in.tag",/^(<\/?[a-z][^<>]*>)/i],[PR.PR_DECLARATION,/^{{[#^>/]?\s*[\w.][^}]*}}/],[PR.PR_DECLARATION,/^{{&?\s*[\w.][^}]*}}/],[PR.PR_DECLARATION,/^{{{>?\s*[\w.][^}]*}}}/],[PR.PR_COMMENT,/^{{![^}]*}}/]]),["handlebars","hbs"]);PR.registerLangHandler(PR.createSimpleLexer([[PR.PR_PLAIN,/^[ \t\r\n\f]+/,null," \t\r\n\f"]],[[PR.PR_STRING,/^\"(?:[^\n\r\f\\\"]|\\(?:\r\n?|\n|\f)|\\[\s\S])*\"/,null],[PR.PR_STRING,/^\'(?:[^\n\r\f\\\']|\\(?:\r\n?|\n|\f)|\\[\s\S])*\'/,null],["lang-css-str",/^url\(([^\)\"\']*)\)/i],[PR.PR_KEYWORD,/^(?:url|rgb|\!important|@import|@page|@media|@charset|inherit)(?=[^\-\w]|$)/i,null],["lang-css-kw",/^(-?(?:[_a-z]|(?:\\[0-9a-f]+ ?))(?:[_a-z0-9\-]|\\(?:\\[0-9a-f]+ ?))*)\s*:/i],[PR.PR_COMMENT,/^\/\*[^*]*\*+(?:[^\/*][^*]*\*+)*\//],[PR.PR_COMMENT,/^(?:)/],[PR.PR_LITERAL,/^(?:\d+|\d*\.\d+)(?:%|[a-z]+)?/i],[PR.PR_LITERAL,/^#(?:[0-9a-f]{3}){1,2}/i],[PR.PR_PLAIN,/^-?(?:[_a-z]|(?:\\[\da-f]+ ?))(?:[_a-z\d\-]|\\(?:\\[\da-f]+ ?))*/i],[PR.PR_PUNCTUATION,/^[^\s\w\'\"]+/]]),["css"]);PR.registerLangHandler(PR.createSimpleLexer([],[[PR.PR_KEYWORD,/^-?(?:[_a-z]|(?:\\[\da-f]+ ?))(?:[_a-z\d\-]|\\(?:\\[\da-f]+ ?))*/i]]),["css-kw"]);PR.registerLangHandler(PR.createSimpleLexer([],[[PR.PR_STRING,/^[^\)\"\']+/]]),["css-str"]); diff --git a/coverage/lcov-report/sort-arrow-sprite.png b/coverage/lcov-report/sort-arrow-sprite.png new file mode 100644 index 0000000..6ed6831 Binary files /dev/null and b/coverage/lcov-report/sort-arrow-sprite.png differ diff --git a/coverage/lcov-report/sorter.js b/coverage/lcov-report/sorter.js new file mode 100644 index 0000000..4ed70ae --- /dev/null +++ b/coverage/lcov-report/sorter.js @@ -0,0 +1,210 @@ +/* eslint-disable */ +var addSorting = (function() { + 'use strict'; + var cols, + currentSort = { + index: 0, + desc: false + }; + + // returns the summary table element + function getTable() { + return document.querySelector('.coverage-summary'); + } + // returns the thead element of the summary table + function getTableHeader() { + return getTable().querySelector('thead tr'); + } + // returns the tbody element of the summary table + function getTableBody() { + return getTable().querySelector('tbody'); + } + // returns the th element for nth column + function getNthColumn(n) { + return getTableHeader().querySelectorAll('th')[n]; + } + + function onFilterInput() { + const searchValue = document.getElementById('fileSearch').value; + const rows = document.getElementsByTagName('tbody')[0].children; + + // Try to create a RegExp from the searchValue. If it fails (invalid regex), + // it will be treated as a plain text search + let searchRegex; + try { + searchRegex = new RegExp(searchValue, 'i'); // 'i' for case-insensitive + } catch (error) { + searchRegex = null; + } + + for (let i = 0; i < rows.length; i++) { + const row = rows[i]; + let isMatch = false; + + if (searchRegex) { + // If a valid regex was created, use it for matching + isMatch = searchRegex.test(row.textContent); + } else { + // Otherwise, fall back to the original plain text search + isMatch = row.textContent + .toLowerCase() + .includes(searchValue.toLowerCase()); + } + + row.style.display = isMatch ? '' : 'none'; + } + } + + // loads the search box + function addSearchBox() { + var template = document.getElementById('filterTemplate'); + var templateClone = template.content.cloneNode(true); + templateClone.getElementById('fileSearch').oninput = onFilterInput; + template.parentElement.appendChild(templateClone); + } + + // loads all columns + function loadColumns() { + var colNodes = getTableHeader().querySelectorAll('th'), + colNode, + cols = [], + col, + i; + + for (i = 0; i < colNodes.length; i += 1) { + colNode = colNodes[i]; + col = { + key: colNode.getAttribute('data-col'), + sortable: !colNode.getAttribute('data-nosort'), + type: colNode.getAttribute('data-type') || 'string' + }; + cols.push(col); + if (col.sortable) { + col.defaultDescSort = col.type === 'number'; + colNode.innerHTML = + colNode.innerHTML + ''; + } + } + return cols; + } + // attaches a data attribute to every tr element with an object + // of data values keyed by column name + function loadRowData(tableRow) { + var tableCols = tableRow.querySelectorAll('td'), + colNode, + col, + data = {}, + i, + val; + for (i = 0; i < tableCols.length; i += 1) { + colNode = tableCols[i]; + col = cols[i]; + val = colNode.getAttribute('data-value'); + if (col.type === 'number') { + val = Number(val); + } + data[col.key] = val; + } + return data; + } + // loads all row data + function loadData() { + var rows = getTableBody().querySelectorAll('tr'), + i; + + for (i = 0; i < rows.length; i += 1) { + rows[i].data = loadRowData(rows[i]); + } + } + // sorts the table using the data for the ith column + function sortByIndex(index, desc) { + var key = cols[index].key, + sorter = function(a, b) { + a = a.data[key]; + b = b.data[key]; + return a < b ? -1 : a > b ? 1 : 0; + }, + finalSorter = sorter, + tableBody = document.querySelector('.coverage-summary tbody'), + rowNodes = tableBody.querySelectorAll('tr'), + rows = [], + i; + + if (desc) { + finalSorter = function(a, b) { + return -1 * sorter(a, b); + }; + } + + for (i = 0; i < rowNodes.length; i += 1) { + rows.push(rowNodes[i]); + tableBody.removeChild(rowNodes[i]); + } + + rows.sort(finalSorter); + + for (i = 0; i < rows.length; i += 1) { + tableBody.appendChild(rows[i]); + } + } + // removes sort indicators for current column being sorted + function removeSortIndicators() { + var col = getNthColumn(currentSort.index), + cls = col.className; + + cls = cls.replace(/ sorted$/, '').replace(/ sorted-desc$/, ''); + col.className = cls; + } + // adds sort indicators for current column being sorted + function addSortIndicators() { + getNthColumn(currentSort.index).className += currentSort.desc + ? ' sorted-desc' + : ' sorted'; + } + // adds event listeners for all sorter widgets + function enableUI() { + var i, + el, + ithSorter = function ithSorter(i) { + var col = cols[i]; + + return function() { + var desc = col.defaultDescSort; + + if (currentSort.index === i) { + desc = !currentSort.desc; + } + sortByIndex(i, desc); + removeSortIndicators(); + currentSort.index = i; + currentSort.desc = desc; + addSortIndicators(); + }; + }; + for (i = 0; i < cols.length; i += 1) { + if (cols[i].sortable) { + // add the click event handler on the th so users + // dont have to click on those tiny arrows + el = getNthColumn(i).querySelector('.sorter').parentElement; + if (el.addEventListener) { + el.addEventListener('click', ithSorter(i)); + } else { + el.attachEvent('onclick', ithSorter(i)); + } + } + } + } + // adds sorting functionality to the UI + return function() { + if (!getTable()) { + return; + } + cols = loadColumns(); + loadData(); + addSearchBox(); + addSortIndicators(); + enableUI(); + }; +})(); + +window.addEventListener('load', addSorting); diff --git a/coverage/lcov-report/src/index.html b/coverage/lcov-report/src/index.html new file mode 100644 index 0000000..e1cd7d6 --- /dev/null +++ b/coverage/lcov-report/src/index.html @@ -0,0 +1,116 @@ + + + + + + Code coverage report for src + + + + + + + + + +
+
+

All files src

+
+ +
+ 92.94% + Statements + 79/85 +
+ + +
+ 74.07% + Branches + 80/108 +
+ + +
+ 100% + Functions + 5/5 +
+ + +
+ 95.94% + Lines + 71/74 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FileStatementsBranchesFunctionsLines
ui-parser.ts +
+
92.94%79/8574.07%80/108100%5/595.94%71/74
+
+
+
+ + + + + + + + \ No newline at end of file diff --git a/coverage/lcov-report/src/server/execute.ts.html b/coverage/lcov-report/src/server/execute.ts.html new file mode 100644 index 0000000..7da230e --- /dev/null +++ b/coverage/lcov-report/src/server/execute.ts.html @@ -0,0 +1,1552 @@ + + + + + + Code coverage report for src/server/execute.ts + + + + + + + + + +
+
+

All files / src/server execute.ts

+
+ +
+ 0% + Statements + 0/198 +
+ + +
+ 0% + Branches + 0/201 +
+ + +
+ 0% + Functions + 0/25 +
+ + +
+ 0% + Lines + 0/178 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +80 +81 +82 +83 +84 +85 +86 +87 +88 +89 +90 +91 +92 +93 +94 +95 +96 +97 +98 +99 +100 +101 +102 +103 +104 +105 +106 +107 +108 +109 +110 +111 +112 +113 +114 +115 +116 +117 +118 +119 +120 +121 +122 +123 +124 +125 +126 +127 +128 +129 +130 +131 +132 +133 +134 +135 +136 +137 +138 +139 +140 +141 +142 +143 +144 +145 +146 +147 +148 +149 +150 +151 +152 +153 +154 +155 +156 +157 +158 +159 +160 +161 +162 +163 +164 +165 +166 +167 +168 +169 +170 +171 +172 +173 +174 +175 +176 +177 +178 +179 +180 +181 +182 +183 +184 +185 +186 +187 +188 +189 +190 +191 +192 +193 +194 +195 +196 +197 +198 +199 +200 +201 +202 +203 +204 +205 +206 +207 +208 +209 +210 +211 +212 +213 +214 +215 +216 +217 +218 +219 +220 +221 +222 +223 +224 +225 +226 +227 +228 +229 +230 +231 +232 +233 +234 +235 +236 +237 +238 +239 +240 +241 +242 +243 +244 +245 +246 +247 +248 +249 +250 +251 +252 +253 +254 +255 +256 +257 +258 +259 +260 +261 +262 +263 +264 +265 +266 +267 +268 +269 +270 +271 +272 +273 +274 +275 +276 +277 +278 +279 +280 +281 +282 +283 +284 +285 +286 +287 +288 +289 +290 +291 +292 +293 +294 +295 +296 +297 +298 +299 +300 +301 +302 +303 +304 +305 +306 +307 +308 +309 +310 +311 +312 +313 +314 +315 +316 +317 +318 +319 +320 +321 +322 +323 +324 +325 +326 +327 +328 +329 +330 +331 +332 +333 +334 +335 +336 +337 +338 +339 +340 +341 +342 +343 +344 +345 +346 +347 +348 +349 +350 +351 +352 +353 +354 +355 +356 +357 +358 +359 +360 +361 +362 +363 +364 +365 +366 +367 +368 +369 +370 +371 +372 +373 +374 +375 +376 +377 +378 +379 +380 +381 +382 +383 +384 +385 +386 +387 +388 +389 +390 +391 +392 +393 +394 +395 +396 +397 +398 +399 +400 +401 +402 +403 +404 +405 +406 +407 +408 +409 +410 +411 +412 +413 +414 +415 +416 +417 +418 +419 +420 +421 +422 +423 +424 +425 +426 +427 +428 +429 +430 +431 +432 +433 +434 +435 +436 +437 +438 +439 +440 +441 +442 +443 +444 +445 +446 +447 +448 +449 +450 +451 +452 +453 +454 +455 +456 +457 +458 +459 +460 +461 +462 +463 +464 +465 +466 +467 +468 +469 +470 +471 +472 +473 +474 +475 +476 +477 +478 +479 +480 +481 +482 +483 +484 +485 +486 +487 +488 +489 +490  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  + 
import type { AdapterExecutionContext, AdapterExecutionResult } from "@paperclipai/adapter-utils";
+import { asString, asNumber, asBoolean, parseObject } from "@paperclipai/adapter-utils/server-utils";
+import {
+  parseClaudeStreamJson,
+  describeClaudeFailure,
+  isClaudeMaxTurnsResult,
+  isClaudeUnknownSessionError,
+} from "./parse.js";
+import { getSelfPodInfo, getBatchApi, getCoreApi, getLogApi } from "./k8s-client.js";
+import { buildJobManifest } from "./job-manifest.js";
+import type * as k8s from "@kubernetes/client-node";
+import { Writable } from "node:stream";
+ 
+const POLL_INTERVAL_MS = 2000;
+ 
+/**
+ * Wait for the Job's pod to reach a terminal or running state.
+ * Returns the pod name once logs can be streamed, or throws on failure.
+ */
+async function waitForPod(
+  namespace: string,
+  jobName: string,
+  timeoutMs: number,
+  onLog: AdapterExecutionContext["onLog"],
+  kubeconfigPath?: string,
+): Promise<string> {
+  const coreApi = getCoreApi(kubeconfigPath);
+  const deadline = Date.now() + timeoutMs;
+  const labelSelector = `job-name=${jobName}`;
+ 
+  await onLog("stdout", `[paperclip] Waiting for pod to be scheduled (job: ${jobName})...\n`);
+ 
+  let lastStatus = "";
+  while (Date.now() < deadline) {
+    const podList = await coreApi.listNamespacedPod({
+      namespace,
+      labelSelector,
+    });
+    const pod = podList.items[0];
+ 
+    if (!pod) {
+      if (lastStatus !== "no-pod") {
+        await onLog("stdout", `[paperclip] Waiting for Job controller to create pod...\n`);
+        lastStatus = "no-pod";
+      }
+      await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL_MS));
+      continue;
+    }
+ 
+    const podName = pod.metadata?.name ?? "unknown";
+    const phase = pod.status?.phase ?? "Unknown";
+    const initStatuses = pod.status?.initContainerStatuses ?? [];
+    const containerStatuses = pod.status?.containerStatuses ?? [];
+ 
+    // Log phase transitions
+    const statusKey = `${phase}:${initStatuses.map((s) => s.state?.waiting?.reason ?? s.state?.terminated?.reason ?? "ok").join(",")}:${containerStatuses.map((s) => s.state?.waiting?.reason ?? s.state?.running ? "running" : "waiting").join(",")}`;
+    if (statusKey !== lastStatus) {
+      const details: string[] = [`phase=${phase}`];
+      for (const init of initStatuses) {
+        if (init.state?.waiting) details.push(`init/${init.name}: waiting (${init.state.waiting.reason ?? "unknown"})`);
+        else if (init.state?.running) details.push(`init/${init.name}: running`);
+        else if (init.state?.terminated) details.push(`init/${init.name}: done (exit ${init.state.terminated.exitCode})`);
+      }
+      for (const cs of containerStatuses) {
+        if (cs.state?.waiting) details.push(`${cs.name}: waiting (${cs.state.waiting.reason ?? "unknown"})`);
+        else if (cs.state?.running) details.push(`${cs.name}: running`);
+      }
+      await onLog("stdout", `[paperclip] Pod ${podName}: ${details.join(", ")}\n`);
+      lastStatus = statusKey;
+    }
+ 
+    // Ready to stream logs
+    if (phase === "Running" || phase === "Succeeded" || phase === "Failed") {
+      return podName;
+    }
+ 
+    // Init containers done + main running (phase may still say Pending briefly)
+    const allInitsDone = initStatuses.length > 0 && initStatuses.every(
+      (s) => s.state?.terminated?.exitCode === 0,
+    );
+    const mainRunning = containerStatuses.some((s) => s.state?.running);
+    if (allInitsDone && mainRunning) {
+      return podName;
+    }
+ 
+    // Check for init container failures
+    for (const init of initStatuses) {
+      const terminated = init.state?.terminated;
+      if (terminated && (terminated.exitCode ?? 0) !== 0) {
+        throw new Error(`Init container "${init.name}" failed with exit code ${terminated.exitCode}: ${terminated.reason ?? terminated.message ?? "unknown"}`);
+      }
+      const waiting = init.state?.waiting;
+      if (waiting?.reason === "ErrImagePull" || waiting?.reason === "ImagePullBackOff") {
+        throw new Error(`Init container "${init.name}" image pull failed: ${waiting.message ?? waiting.reason}`);
+      }
+      if (waiting?.reason === "CrashLoopBackOff") {
+        throw new Error(`Init container "${init.name}" crash loop: ${waiting.message ?? waiting.reason}`);
+      }
+    }
+ 
+    // Check for unrecoverable scheduling failures
+    const conditions = pod.status?.conditions ?? [];
+    const unschedulable = conditions.find(
+      (c) => c.type === "PodScheduled" && c.status === "False" && c.reason === "Unschedulable",
+    );
+    if (unschedulable) {
+      throw new Error(`Pod unschedulable: ${unschedulable.message ?? "insufficient resources"}`);
+    }
+ 
+    // Check for main container image pull errors
+    for (const cs of containerStatuses) {
+      const waiting = cs.state?.waiting;
+      if (waiting?.reason === "ErrImagePull" || waiting?.reason === "ImagePullBackOff") {
+        throw new Error(`Image pull failed for "${cs.name}": ${waiting.message ?? waiting.reason}`);
+      }
+      if (waiting?.reason === "CrashLoopBackOff") {
+        throw new Error(`Container "${cs.name}" crash loop: ${waiting.message ?? waiting.reason}`);
+      }
+    }
+ 
+    await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL_MS));
+  }
+ 
+  throw new Error(`Timed out waiting for pod to be scheduled (${Math.round(timeoutMs / 1000)}s)`);
+}
+ 
+/**
+ * Stream pod logs and accumulate stdout for result parsing.
+ * Returns accumulated stdout when the stream ends.
+ */
+async function streamPodLogs(
+  namespace: string,
+  podName: string,
+  onLog: AdapterExecutionContext["onLog"],
+  kubeconfigPath?: string,
+): Promise<string> {
+  const logApi = getLogApi(kubeconfigPath);
+  const chunks: string[] = [];
+ 
+  const writable = new Writable({
+    write(chunk: Buffer, _encoding, callback) {
+      const text = chunk.toString("utf-8");
+      chunks.push(text);
+      void onLog("stdout", text).then(() => callback(), callback);
+    },
+  });
+ 
+  try {
+    await logApi.log(namespace, podName, "claude", writable, {
+      follow: true,
+      pretty: false,
+    });
+  } catch {
+    // follow may fail if the container already exited — not fatal,
+    // we'll try a one-shot read below
+  }
+ 
+  return chunks.join("");
+}
+ 
+/**
+ * One-shot read of pod logs (no follow). Used as fallback when the
+ * follow stream missed output because the container exited quickly.
+ */
+async function readPodLogs(
+  namespace: string,
+  podName: string,
+  kubeconfigPath?: string,
+): Promise<string> {
+  const coreApi = getCoreApi(kubeconfigPath);
+  try {
+    const log = await coreApi.readNamespacedPodLog({
+      name: podName,
+      namespace,
+      container: "claude",
+    });
+    return typeof log === "string" ? log : "";
+  } catch {
+    return "";
+  }
+}
+ 
+/**
+ * Wait for the Job to reach a terminal state (Complete or Failed).
+ * Returns the Job's final status.
+ */
+async function waitForJobCompletion(
+  namespace: string,
+  jobName: string,
+  timeoutMs: number,
+  kubeconfigPath?: string,
+): Promise<{ succeeded: boolean; timedOut: boolean }> {
+  const batchApi = getBatchApi(kubeconfigPath);
+  const deadline = timeoutMs > 0 ? Date.now() + timeoutMs : 0;
+ 
+  while (deadline === 0 || Date.now() < deadline) {
+    const job = await batchApi.readNamespacedJob({ name: jobName, namespace });
+    const conditions = job.status?.conditions ?? [];
+ 
+    const complete = conditions.find((c) => c.type === "Complete" && c.status === "True");
+    if (complete) return { succeeded: true, timedOut: false };
+ 
+    const failed = conditions.find((c) => c.type === "Failed" && c.status === "True");
+    if (failed) {
+      const isDeadlineExceeded = failed.reason === "DeadlineExceeded";
+      return { succeeded: false, timedOut: isDeadlineExceeded };
+    }
+ 
+    await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL_MS));
+  }
+ 
+  return { succeeded: false, timedOut: true };
+}
+ 
+/**
+ * Get the exit code from the Job's pod.
+ */
+async function getPodExitCode(namespace: string, jobName: string, kubeconfigPath?: string): Promise<number | null> {
+  const coreApi = getCoreApi(kubeconfigPath);
+  const podList = await coreApi.listNamespacedPod({
+    namespace,
+    labelSelector: `job-name=${jobName}`,
+  });
+  const pod = podList.items[0];
+  if (!pod) return null;
+ 
+  const containerStatus = pod.status?.containerStatuses?.find((s) => s.name === "claude");
+  return containerStatus?.state?.terminated?.exitCode ?? null;
+}
+ 
+/**
+ * Delete Job and its pods. Best-effort — failures are logged but not thrown.
+ */
+async function cleanupJob(
+  namespace: string,
+  jobName: string,
+  onLog: AdapterExecutionContext["onLog"],
+  kubeconfigPath?: string,
+): Promise<void> {
+  try {
+    const batchApi = getBatchApi(kubeconfigPath);
+    await batchApi.deleteNamespacedJob({
+      name: jobName,
+      namespace,
+      body: { propagationPolicy: "Background" },
+    });
+  } catch (err) {
+    const msg = err instanceof Error ? err.message : String(err);
+    await onLog("stderr", `[paperclip] Warning: failed to cleanup job ${jobName}: ${msg}\n`);
+  }
+}
+ 
+export async function execute(ctx: AdapterExecutionContext): Promise<AdapterExecutionResult> {
+  const { runId, runtime, config: rawConfig, onLog, onMeta } = ctx;
+  const config = parseObject(rawConfig);
+  const timeoutSec = asNumber(config.timeoutSec, 0);
+  const graceSec = asNumber(config.graceSec, 60);
+  const retainJobs = asBoolean(config.retainJobs, false);
+  const kubeconfigPath = asString(config.kubeconfig, "") || undefined;
+ 
+  // Guard: claude_k8s must not run concurrently for the same agent (shared PVC/session)
+  const agentId = ctx.agent.id;
+  const selfPod = await getSelfPodInfo(kubeconfigPath);
+  const guardNamespace = asString(config.namespace, "") || selfPod.namespace;
+  try {
+    const batchApi = getBatchApi(kubeconfigPath);
+    const existing = await batchApi.listNamespacedJob({
+      namespace: guardNamespace,
+      labelSelector: `paperclip.io/agent-id=${agentId},paperclip.io/adapter-type=claude_k8s`,
+    });
+    const running = existing.items.filter(
+      (j) => !j.status?.conditions?.some((c) => (c.type === "Complete" || c.type === "Failed") && c.status === "True"),
+    );
+    if (running.length > 0) {
+      const names = running.map((j) => j.metadata?.name).join(", ");
+      await onLog("stderr", `[paperclip] Concurrent run blocked: existing Job(s) still running for this agent: ${names}\n`);
+      return {
+        exitCode: null,
+        signal: null,
+        timedOut: false,
+        errorMessage: `Concurrent run blocked: Job ${names} is still running for this agent`,
+        errorCode: "k8s_concurrent_run_blocked",
+      };
+    }
+  } catch {
+    // If we can't check, proceed — the heartbeat service enforces concurrency too
+  }
+ 
+  // Build Job manifest
+  const { job, jobName, namespace, prompt, claudeArgs, promptMetrics } = buildJobManifest({
+    ctx,
+    selfPod,
+  });
+ 
+  // Report invocation metadata
+  if (onMeta) {
+    await onMeta({
+      adapterType: "claude_k8s",
+      command: `kubectl job/${jobName}`,
+      cwd: namespace,
+      commandArgs: claudeArgs,
+      commandNotes: [
+        `Image: ${job.spec?.template.spec?.containers[0]?.image ?? "unknown"}`,
+        `Namespace: ${namespace}`,
+        `Timeout: ${timeoutSec}s`,
+      ],
+      prompt,
+      ...(promptMetrics ? { promptMetrics } : {}),
+      context: ctx.context,
+    } as Parameters<typeof onMeta>[0]);
+  }
+ 
+  // Create the Job
+  const batchApi = getBatchApi(kubeconfigPath);
+  try {
+    await batchApi.createNamespacedJob({ namespace, body: job });
+  } catch (err) {
+    const msg = err instanceof Error ? err.message : String(err);
+    await onLog("stderr", `[paperclip] Failed to create K8s Job: ${msg}\n`);
+    return {
+      exitCode: null,
+      signal: null,
+      timedOut: false,
+      errorMessage: `Failed to create Kubernetes Job: ${msg}`,
+      errorCode: "k8s_job_create_failed",
+    };
+  }
+ 
+  await onLog("stdout", `[paperclip] Created K8s Job: ${jobName} in namespace ${namespace} (deadline: ${timeoutSec > 0 ? `${timeoutSec}s` : "none"})\n`);
+ 
+  let stdout = "";
+  let exitCode: number | null = null;
+  let jobTimedOut = false;
+ 
+  try {
+    // Wait for pod to be ready for log streaming
+    const scheduleTimeoutMs = 120_000; // 2 minutes for scheduling
+    let podName: string;
+    try {
+      podName = await waitForPod(namespace, jobName, scheduleTimeoutMs, onLog, kubeconfigPath);
+      await onLog("stdout", `[paperclip] Pod running: ${podName}\n`);
+    } catch (err) {
+      const msg = err instanceof Error ? err.message : String(err);
+      await onLog("stderr", `[paperclip] Pod scheduling failed: ${msg}\n`);
+      return {
+        exitCode: null,
+        signal: null,
+        timedOut: false,
+        errorMessage: `Pod scheduling failed: ${msg}`,
+        errorCode: "k8s_pod_schedule_failed",
+      };
+    }
+ 
+    // Stream logs and wait for completion concurrently.
+    // The log stream will end when the container exits.
+    // We also poll the Job status to detect deadline exceeded.
+    // 0 = no timeout (run indefinitely, matching claude_local behavior)
+    const completionTimeoutMs = timeoutSec > 0 ? (timeoutSec + graceSec) * 1000 : 0;
+ 
+    const [logResult, completionResult] = await Promise.allSettled([
+      streamPodLogs(namespace, podName, onLog, kubeconfigPath),
+      waitForJobCompletion(namespace, jobName, completionTimeoutMs, kubeconfigPath),
+    ]);
+ 
+    if (logResult.status === "fulfilled") {
+      stdout = logResult.value;
+    }
+ 
+    // If the follow stream missed output (container exited quickly), do a
+    // one-shot log read as fallback before the pod is cleaned up.
+    if (!stdout.trim()) {
+      await onLog("stdout", `[paperclip] Log stream returned empty — reading pod logs directly...\n`);
+      stdout = await readPodLogs(namespace, podName, kubeconfigPath);
+      if (stdout.trim()) {
+        await onLog("stdout", stdout);
+      }
+    }
+ 
+    if (completionResult.status === "fulfilled") {
+      jobTimedOut = completionResult.value.timedOut;
+    } else {
+      jobTimedOut = true;
+    }
+ 
+    exitCode = await getPodExitCode(namespace, jobName, kubeconfigPath);
+  } finally {
+    if (!retainJobs) {
+      await cleanupJob(namespace, jobName, onLog, kubeconfigPath);
+    } else {
+      await onLog("stdout", `[paperclip] Retaining job ${jobName} for debugging (retainJobs=true)\n`);
+    }
+  }
+ 
+  // Parse Claude output (reuse claude_local parsing)
+  if (jobTimedOut) {
+    return {
+      exitCode,
+      signal: null,
+      timedOut: true,
+      errorMessage: `Timed out after ${timeoutSec}s`,
+      errorCode: "timeout",
+    };
+  }
+ 
+  const parsedStream = parseClaudeStreamJson(stdout);
+  const parsed = parsedStream.resultJson;
+ 
+  // If the session was stale, clear it so the next heartbeat starts fresh
+  if (parsed && (exitCode ?? 0) !== 0 && isClaudeUnknownSessionError(parsed)) {
+    await onLog("stdout", `[paperclip] Claude session is unavailable; clearing for next run.\n`);
+    return {
+      exitCode,
+      signal: null,
+      timedOut: false,
+      errorMessage: describeClaudeFailure(parsed) ?? "Session unavailable",
+      errorCode: "session_unavailable",
+      clearSession: true,
+      resultJson: parsed,
+    };
+  }
+ 
+  if (!parsed) {
+    const stderrLine = stdout.split(/\r?\n/).map((l) => l.trim()).find(Boolean) ?? "";
+    return {
+      exitCode,
+      signal: null,
+      timedOut: false,
+      errorMessage: exitCode === 0
+        ? "Failed to parse Claude JSON output"
+        : stderrLine
+          ? `Claude exited with code ${exitCode ?? -1}: ${stderrLine}`
+          : `Claude exited with code ${exitCode ?? -1}`,
+      resultJson: { stdout },
+    };
+  }
+ 
+  const usage = parsedStream.usage ?? (() => {
+    const usageObj = parseObject(parsed.usage as Record<string, unknown>);
+    return {
+      inputTokens: asNumber(usageObj.input_tokens, 0),
+      cachedInputTokens: asNumber(usageObj.cache_read_input_tokens, 0),
+      outputTokens: asNumber(usageObj.output_tokens, 0),
+    };
+  })();
+ 
+  const runtimeSessionParams = parseObject(runtime.sessionParams);
+  const fallbackSessionId = asString(runtimeSessionParams.sessionId, runtime.sessionId ?? "");
+  const resolvedSessionId = parsedStream.sessionId
+    ?? (asString(parsed.session_id as string, fallbackSessionId) || fallbackSessionId);
+  const model = asString(config.model, "");
+  const workspaceContext = parseObject(ctx.context.paperclipWorkspace);
+  const workspaceId = asString(workspaceContext.workspaceId, "") || null;
+  const workspaceRepoUrl = asString(workspaceContext.repoUrl, "") || null;
+  const workspaceRepoRef = asString(workspaceContext.repoRef, "") || null;
+  const cwd = asString(workspaceContext.cwd, "");
+ 
+  const resolvedSessionParams = resolvedSessionId
+    ? {
+        sessionId: resolvedSessionId,
+        ...(cwd ? { cwd } : {}),
+        ...(workspaceId ? { workspaceId } : {}),
+        ...(workspaceRepoUrl ? { repoUrl: workspaceRepoUrl } : {}),
+        ...(workspaceRepoRef ? { repoRef: workspaceRepoRef } : {}),
+      } as Record<string, unknown>
+    : null;
+ 
+  const clearSessionForMaxTurns = isClaudeMaxTurnsResult(parsed);
+ 
+  return {
+    exitCode,
+    signal: null,
+    timedOut: false,
+    errorMessage:
+      (exitCode ?? 0) === 0
+        ? null
+        : describeClaudeFailure(parsed) ?? `Claude exited with code ${exitCode ?? -1}`,
+    usage,
+    sessionId: resolvedSessionId || null,
+    sessionParams: resolvedSessionParams,
+    sessionDisplayId: resolvedSessionId || null,
+    provider: "anthropic",
+    model: parsedStream.model || asString(parsed.model as string, model),
+    billingType: "api",
+    costUsd: parsedStream.costUsd ?? asNumber(parsed.total_cost_usd, 0),
+    resultJson: parsed,
+    summary: parsedStream.summary || asString(parsed.result as string, ""),
+    clearSession: clearSessionForMaxTurns,
+  } as AdapterExecutionResult;
+}
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/coverage/lcov-report/src/server/index.html b/coverage/lcov-report/src/server/index.html new file mode 100644 index 0000000..2b93e86 --- /dev/null +++ b/coverage/lcov-report/src/server/index.html @@ -0,0 +1,206 @@ + + + + + + Code coverage report for src/server + + + + + + + + + +
+
+

All files src/server

+
+ +
+ 40.43% + Statements + 243/601 +
+ + +
+ 38.46% + Branches + 200/520 +
+ + +
+ 33.78% + Functions + 25/74 +
+ + +
+ 39.51% + Lines + 213/539 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FileStatementsBranchesFunctionsLines
execute.ts +
+
0%0/1980%0/2010%0/250%0/178
index.ts +
+
0%0/1100%0/00%0/10%0/1
job-manifest.ts +
+
84.45%125/14866.07%74/11290.9%10/1184.73%111/131
k8s-client.ts +
+
0%0/600%0/320%0/130%0/56
parse.ts +
+
91.08%92/10178.4%69/88100%11/1191.01%81/89
session.ts +
+
100%26/26100%57/57100%4/4100%21/21
test.ts +
+
0%0/670%0/300%0/90%0/63
+
+
+
+ + + + + + + + \ No newline at end of file diff --git a/coverage/lcov-report/src/server/index.ts.html b/coverage/lcov-report/src/server/index.ts.html new file mode 100644 index 0000000..8affa6e --- /dev/null +++ b/coverage/lcov-report/src/server/index.ts.html @@ -0,0 +1,142 @@ + + + + + + Code coverage report for src/server/index.ts + + + + + + + + + +
+
+

All files / src/server index.ts

+
+ +
+ 0% + Statements + 0/1 +
+ + +
+ 100% + Branches + 0/0 +
+ + +
+ 0% + Functions + 0/1 +
+ + +
+ 0% + Lines + 0/1 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  + 
import type { ServerAdapterModule } from "@paperclipai/adapter-utils";
+import { type, models, agentConfigurationDoc } from "../index.js";
+import { execute } from "./execute.js";
+import { testEnvironment } from "./test.js";
+import { sessionCodec } from "./session.js";
+ 
+export function createServerAdapter(): ServerAdapterModule {
+  return {
+    type,
+    execute,
+    testEnvironment,
+    sessionCodec,
+    models,
+    supportsLocalAgentJwt: true,
+    agentConfigurationDoc,
+  };
+}
+ 
+export { execute, testEnvironment, sessionCodec };
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/coverage/lcov-report/src/server/job-manifest.ts.html b/coverage/lcov-report/src/server/job-manifest.ts.html new file mode 100644 index 0000000..a0b1aef --- /dev/null +++ b/coverage/lcov-report/src/server/job-manifest.ts.html @@ -0,0 +1,1261 @@ + + + + + + Code coverage report for src/server/job-manifest.ts + + + + + + + + + +
+
+

All files / src/server job-manifest.ts

+
+ +
+ 84.45% + Statements + 125/148 +
+ + +
+ 66.07% + Branches + 74/112 +
+ + +
+ 90.9% + Functions + 10/11 +
+ + +
+ 84.73% + Lines + 111/131 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +80 +81 +82 +83 +84 +85 +86 +87 +88 +89 +90 +91 +92 +93 +94 +95 +96 +97 +98 +99 +100 +101 +102 +103 +104 +105 +106 +107 +108 +109 +110 +111 +112 +113 +114 +115 +116 +117 +118 +119 +120 +121 +122 +123 +124 +125 +126 +127 +128 +129 +130 +131 +132 +133 +134 +135 +136 +137 +138 +139 +140 +141 +142 +143 +144 +145 +146 +147 +148 +149 +150 +151 +152 +153 +154 +155 +156 +157 +158 +159 +160 +161 +162 +163 +164 +165 +166 +167 +168 +169 +170 +171 +172 +173 +174 +175 +176 +177 +178 +179 +180 +181 +182 +183 +184 +185 +186 +187 +188 +189 +190 +191 +192 +193 +194 +195 +196 +197 +198 +199 +200 +201 +202 +203 +204 +205 +206 +207 +208 +209 +210 +211 +212 +213 +214 +215 +216 +217 +218 +219 +220 +221 +222 +223 +224 +225 +226 +227 +228 +229 +230 +231 +232 +233 +234 +235 +236 +237 +238 +239 +240 +241 +242 +243 +244 +245 +246 +247 +248 +249 +250 +251 +252 +253 +254 +255 +256 +257 +258 +259 +260 +261 +262 +263 +264 +265 +266 +267 +268 +269 +270 +271 +272 +273 +274 +275 +276 +277 +278 +279 +280 +281 +282 +283 +284 +285 +286 +287 +288 +289 +290 +291 +292 +293 +294 +295 +296 +297 +298 +299 +300 +301 +302 +303 +304 +305 +306 +307 +308 +309 +310 +311 +312 +313 +314 +315 +316 +317 +318 +319 +320 +321 +322 +323 +324 +325 +326 +327 +328 +329 +330 +331 +332 +333 +334 +335 +336 +337 +338 +339 +340 +341 +342 +343 +344 +345 +346 +347 +348 +349 +350 +351 +352 +353 +354 +355 +356 +357 +358 +359 +360 +361 +362 +363 +364 +365 +366 +367 +368 +369 +370 +371 +372 +373 +374 +375 +376 +377 +378 +379 +380 +381 +382 +383 +384 +385 +386 +387 +388 +389 +390 +391 +392 +393  +  +  +  +  +  +  +  +  +  +  +  +  +  +256x +  +  +  +64x +  +  +  +  +  +  +  +  +  +64x +  +  +64x +64x +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +128x +  +  +  +  +  +  +  +64x +64x +  +  +64x +  +  +64x +  +64x +960x +7x +  +  +  +64x +64x +64x +64x +64x +  +64x +64x +  +  +  +64x +64x +64x +64x +64x +64x +64x +64x +64x +64x +  +64x +  +  +64x +  +  +  +64x +  +  +64x +  +  +64x +  +  +64x +  +  +64x +2x +  +  +  +  +  +64x +1x +  +  +  +64x +  +  +  +  +  +64x +1x +  +  +  +64x +  +  +332x +  +  +  +  +64x +  +  +  +64x +64x +64x +  +  +64x +64x +64x +64x +64x +  +64x +64x +64x +64x +64x +64x +64x +64x +  +  +64x +64x +64x +64x +  +64x +64x +64x +  +  +64x +  +  +  +64x +64x +64x +64x +  +  +  +  +  +  +  +  +  +64x +  +  +64x +64x +64x +64x +64x +  +  +  +  +  +64x +  +  +  +  +  +  +  +  +64x +64x +64x +64x +64x +64x +64x +64x +64x +  +  +64x +  +  +64x +64x +64x +  +  +  +  +  +  +  +  +  +  +  +64x +  +  +  +  +  +  +  +64x +3x +  +  +  +64x +  +  +  +  +  +64x +  +  +  +  +  +  +  +64x +63x +  +  +  +63x +  +  +  +  +  +  +64x +1x +  +  +  +1x +  +  +  +  +  +  +  +64x +  +  +  +  +  +  +  +64x +  +  +  +  +  +  +  +  +396x +64x +  +64x +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +64x +  + 
import type * as k8s from "@kubernetes/client-node";
+import type { AdapterExecutionContext } from "@paperclipai/adapter-utils";
+import {
+  asString,
+  asNumber,
+  asBoolean,
+  asStringArray,
+  parseObject,
+  buildPaperclipEnv,
+  renderTemplate,
+} from "@paperclipai/adapter-utils/server-utils";
+ 
+// Inline prompt assembly — these functions are not yet in the published adapter-utils
+function joinPromptSections(sections: string[], separator = "\n\n"): string {
+  return sections.filter((s) => s.trim().length > 0).join(separator);
+}
+ 
+function stringifyPaperclipWakePayload(wake: unknown): string | null {
+  Eif (!wake || typeof wake !== "object") return null;
+  try {
+    const json = JSON.stringify(wake);
+    return json === "{}" ? null : json;
+  } catch {
+    return null;
+  }
+}
+ 
+function renderPaperclipWakePrompt(wake: unknown, _opts?: { resumedSession?: boolean }): string {
+  Eif (!wake || typeof wake !== "object") return "";
+  const w = wake as Record<string, unknown>;
+  const reason = typeof w.reason === "string" ? w.reason.trim() : "";
+  const comments = Array.isArray(w.comments) ? w.comments : [];
+  Iif (!reason && comments.length === 0) return "";
+  const parts: string[] = [];
+  if (reason) parts.push(`Wake reason: ${reason}`);
+  for (const c of comments) {
+    if (typeof c === "object" && c !== null) {
+      const comment = c as Record<string, unknown>;
+      const body = typeof comment.body === "string" ? comment.body.trim() : "";
+      if (body) parts.push(`Comment: ${body}`);
+    }
+  }
+  return parts.join("\n\n");
+}
+import type { SelfPodInfo } from "./k8s-client.js";
+ 
+export interface JobBuildInput {
+  ctx: AdapterExecutionContext;
+  selfPod: SelfPodInfo;
+}
+ 
+export interface JobBuildResult {
+  job: k8s.V1Job;
+  jobName: string;
+  namespace: string;
+  prompt: string;
+  claudeArgs: string[];
+  promptMetrics: Record<string, number>;
+}
+ 
+function sanitizeForK8sName(value: string): string {
+  return value.toLowerCase().replace(/[^a-z0-9-]/g, "").slice(0, 8);
+}
+ 
+function buildEnvVars(
+  ctx: AdapterExecutionContext,
+  selfPod: SelfPodInfo,
+  config: Record<string, unknown>,
+): k8s.V1EnvVar[] {
+  const { runId, agent, context } = ctx;
+  const envConfig = parseObject(config.env);
+ 
+  // Layer 1: PAPERCLIP_* base vars
+  const paperclipEnv = buildPaperclipEnv(agent);
+ 
+  // Layer 2: Context vars (run, wake, workspace — same as claude_local)
+  paperclipEnv.PAPERCLIP_RUN_ID = runId;
+ 
+  const setIfPresent = (envKey: string, value: unknown) => {
+    if (typeof value === "string" && value.trim().length > 0) {
+      paperclipEnv[envKey] = value.trim();
+    }
+  };
+ 
+  setIfPresent("PAPERCLIP_TASK_ID", context.taskId ?? context.issueId);
+  setIfPresent("PAPERCLIP_WAKE_REASON", context.wakeReason);
+  setIfPresent("PAPERCLIP_WAKE_COMMENT_ID", context.wakeCommentId ?? context.commentId);
+  setIfPresent("PAPERCLIP_APPROVAL_ID", context.approvalId);
+  setIfPresent("PAPERCLIP_APPROVAL_STATUS", context.approvalStatus);
+ 
+  const wakePayloadJson = stringifyPaperclipWakePayload(context.paperclipWake);
+  Iif (wakePayloadJson) {
+    paperclipEnv.PAPERCLIP_WAKE_PAYLOAD_JSON = wakePayloadJson;
+  }
+ 
+  const workspaceContext = parseObject(context.paperclipWorkspace);
+  setIfPresent("PAPERCLIP_WORKSPACE_CWD", workspaceContext.cwd);
+  setIfPresent("PAPERCLIP_WORKSPACE_SOURCE", workspaceContext.source);
+  setIfPresent("PAPERCLIP_WORKSPACE_STRATEGY", workspaceContext.strategy);
+  setIfPresent("PAPERCLIP_WORKSPACE_ID", workspaceContext.workspaceId);
+  setIfPresent("PAPERCLIP_WORKSPACE_REPO_URL", workspaceContext.repoUrl);
+  setIfPresent("PAPERCLIP_WORKSPACE_REPO_REF", workspaceContext.repoRef);
+  setIfPresent("PAPERCLIP_WORKSPACE_BRANCH", workspaceContext.branchName);
+  setIfPresent("PAPERCLIP_WORKSPACE_WORKTREE_PATH", workspaceContext.worktreePath);
+  setIfPresent("AGENT_HOME", workspaceContext.agentHome);
+ 
+  const linkedIssueIds = Array.isArray(context.issueIds)
+    ? context.issueIds.filter((v): v is string => typeof v === "string" && v.trim().length > 0)
+    : [];
+  Iif (linkedIssueIds.length > 0) {
+    paperclipEnv.PAPERCLIP_LINKED_ISSUE_IDS = linkedIssueIds.join(",");
+  }
+ 
+  Iif (Array.isArray(context.paperclipWorkspaces) && context.paperclipWorkspaces.length > 0) {
+    paperclipEnv.PAPERCLIP_WORKSPACES_JSON = JSON.stringify(context.paperclipWorkspaces);
+  }
+  Iif (Array.isArray(context.paperclipRuntimeServiceIntents) && context.paperclipRuntimeServiceIntents.length > 0) {
+    paperclipEnv.PAPERCLIP_RUNTIME_SERVICE_INTENTS_JSON = JSON.stringify(context.paperclipRuntimeServiceIntents);
+  }
+  Iif (Array.isArray(context.paperclipRuntimeServices) && context.paperclipRuntimeServices.length > 0) {
+    paperclipEnv.PAPERCLIP_RUNTIME_SERVICES_JSON = JSON.stringify(context.paperclipRuntimeServices);
+  }
+  setIfPresent("PAPERCLIP_RUNTIME_PRIMARY_URL", context.paperclipRuntimePrimaryUrl);
+ 
+  // Auth token for agent callback to Paperclip API
+  if (ctx.authToken) {
+    paperclipEnv.PAPERCLIP_API_KEY = ctx.authToken;
+  }
+ 
+  // PAPERCLIP_API_URL is inherited from the Deployment env via selfPod.inheritedEnv.
+  // buildPaperclipEnv() sets a localhost value which is wrong for Job pods —
+  // the inherited value (set in the infra repo) points to the in-cluster service.
+  if (selfPod.inheritedEnv.PAPERCLIP_API_URL) {
+    paperclipEnv.PAPERCLIP_API_URL = selfPod.inheritedEnv.PAPERCLIP_API_URL;
+  }
+ 
+  // Layer 3: Inherited from Deployment (Bedrock, API keys, etc.)
+  const merged: Record<string, string> = {
+    ...selfPod.inheritedEnv,
+    ...paperclipEnv,
+  };
+ 
+  // Layer 4: User-defined overrides from adapterConfig.env (wins over everything)
+  for (const [key, value] of Object.entries(envConfig)) {
+    Eif (typeof value === "string") merged[key] = value;
+  }
+ 
+  // HOME must be /paperclip to match PVC mount and enable session resume
+  merged.HOME = "/paperclip";
+ 
+  // Convert to V1EnvVar array
+  const envVars: k8s.V1EnvVar[] = Object.entries(merged).map(([name, value]) => ({
+    name,
+    value,
+  }));
+ 
+  return envVars;
+}
+ 
+export function buildJobManifest(input: JobBuildInput): JobBuildResult {
+  const { ctx, selfPod } = input;
+  const { runId, agent, runtime, config: rawConfig, context } = ctx;
+  const config = parseObject(rawConfig);
+ 
+  // Resolve config values
+  const namespace = asString(config.namespace, "") || selfPod.namespace;
+  const image = asString(config.image, "") || selfPod.image;
+  const model = asString(config.model, "");
+  const effort = asString(config.effort, "");
+  const maxTurns = asNumber(config.maxTurnsPerRun, 0);
+  // K8s Job pods are always unattended — no one to approve permission prompts
+  const dangerouslySkipPermissions = asBoolean(config.dangerouslySkipPermissions, true);
+  const extraArgs = asStringArray(config.extraArgs);
+  const timeoutSec = asNumber(config.timeoutSec, 0);
+  const ttlSeconds = asNumber(config.ttlSecondsAfterFinished, 300);
+  const resources = parseObject(config.resources);
+  const nodeSelector = parseObject(config.nodeSelector);
+  const tolerations = Array.isArray(config.tolerations) ? config.tolerations : [];
+  const extraLabels = parseObject(config.labels);
+ 
+  // Resolve working directory — use workspace cwd, fall back to /paperclip
+  const workspaceContext = parseObject(context.paperclipWorkspace);
+  const workspaceCwd = asString(workspaceContext.cwd, "");
+  const configuredCwd = asString(config.cwd, "");
+  const workingDir = workspaceCwd || configuredCwd || "/paperclip";
+ 
+  const agentSlug = sanitizeForK8sName(agent.id);
+  const runSlug = sanitizeForK8sName(runId);
+  const jobName = `agent-claude-${agentSlug}-${runSlug}`;
+ 
+  // Build prompt (same logic as claude_local)
+  const promptTemplate = asString(
+    config.promptTemplate,
+    "You are agent {{agent.id}} ({{agent.name}}). Continue your Paperclip work.",
+  );
+  const bootstrapPromptTemplate = asString(config.bootstrapPromptTemplate, "");
+  const runtimeSessionParams = parseObject(runtime.sessionParams);
+  const runtimeSessionId = asString(runtimeSessionParams.sessionId, runtime.sessionId ?? "");
+  const templateData = {
+    agentId: agent.id,
+    companyId: agent.companyId,
+    runId,
+    company: { id: agent.companyId },
+    agent,
+    run: { id: runId, source: "on_demand" },
+    context,
+  };
+  const renderedBootstrapPrompt =
+    !runtimeSessionId && bootstrapPromptTemplate.trim().length > 0
+      ? renderTemplate(bootstrapPromptTemplate, templateData).trim()
+      : "";
+  const wakePrompt = renderPaperclipWakePrompt(context.paperclipWake, { resumedSession: Boolean(runtimeSessionId) });
+  const shouldUseResumeDeltaPrompt = Boolean(runtimeSessionId) && wakePrompt.length > 0;
+  const renderedPrompt = shouldUseResumeDeltaPrompt ? "" : renderTemplate(promptTemplate, templateData);
+  const sessionHandoffNote = asString(context.paperclipSessionHandoffMarkdown, "").trim();
+  const prompt = joinPromptSections([
+    renderedBootstrapPrompt,
+    wakePrompt,
+    sessionHandoffNote,
+    renderedPrompt,
+  ]);
+  const promptMetrics = {
+    promptChars: prompt.length,
+    bootstrapPromptChars: renderedBootstrapPrompt.length,
+    wakePromptChars: wakePrompt.length,
+    sessionHandoffChars: sessionHandoffNote.length,
+    heartbeatPromptChars: renderedPrompt.length,
+  };
+ 
+  // Build Claude CLI args
+  const instructionsFilePath = asString(config.instructionsFilePath, "").trim();
+  const claudeArgs = ["--print", "-", "--output-format", "stream-json", "--verbose"];
+  if (runtimeSessionId) claudeArgs.push("--resume", runtimeSessionId);
+  Eif (dangerouslySkipPermissions) claudeArgs.push("--dangerously-skip-permissions");
+  if (model) claudeArgs.push("--model", model);
+  if (effort) claudeArgs.push("--effort", effort);
+  if (maxTurns > 0) claudeArgs.push("--max-turns", String(maxTurns));
+  if (instructionsFilePath) claudeArgs.push("--append-system-prompt-file", instructionsFilePath);
+  if (extraArgs.length > 0) claudeArgs.push(...extraArgs);
+ 
+  // Build env vars
+  const envVars = buildEnvVars(ctx, selfPod, config);
+ 
+  // Resource defaults
+  const resourceRequests = parseObject(resources.requests);
+  const resourceLimits = parseObject(resources.limits);
+  const containerResources: k8s.V1ResourceRequirements = {
+    requests: {
+      cpu: asString(resourceRequests.cpu, "1000m"),
+      memory: asString(resourceRequests.memory, "2Gi"),
+    },
+    limits: {
+      cpu: asString(resourceLimits.cpu, "4000m"),
+      memory: asString(resourceLimits.memory, "8Gi"),
+    },
+  };
+ 
+  // Labels
+  const labels: Record<string, string> = {
+    "app.kubernetes.io/managed-by": "paperclip",
+    "app.kubernetes.io/component": "agent-job",
+    "paperclip.io/agent-id": agent.id,
+    "paperclip.io/run-id": runId,
+    "paperclip.io/company-id": agent.companyId,
+    "paperclip.io/adapter-type": "claude_k8s",
+  };
+  for (const [key, value] of Object.entries(extraLabels)) {
+    Eif (typeof value === "string") labels[key] = value;
+  }
+ 
+  // Volumes
+  const volumes: k8s.V1Volume[] = [
+    {
+      name: "prompt",
+      emptyDir: {},
+    },
+  ];
+  const volumeMounts: k8s.V1VolumeMount[] = [
+    {
+      name: "prompt",
+      mountPath: "/tmp/prompt",
+    },
+  ];
+ 
+  // Mount shared PVC for /paperclip (session state, workspaces, data)
+  if (selfPod.pvcClaimName) {
+    volumes.push({
+      name: "data",
+      persistentVolumeClaim: { claimName: selfPod.pvcClaimName },
+    });
+    volumeMounts.push({
+      name: "data",
+      mountPath: "/paperclip",
+    });
+  }
+ 
+  // Mount secret volumes inherited from the Deployment pod
+  for (const sv of selfPod.secretVolumes) {
+    volumes.push({
+      name: sv.volumeName,
+      secret: { secretName: sv.secretName, defaultMode: sv.defaultMode, optional: true },
+    });
+    volumeMounts.push({
+      name: sv.volumeName,
+      mountPath: sv.mountPath,
+      readOnly: true,
+    });
+  }
+ 
+  // Security context matching the main Deployment
+  const securityContext: k8s.V1SecurityContext = {
+    capabilities: { drop: ["ALL"] },
+    readOnlyRootFilesystem: false,
+    runAsNonRoot: true,
+    runAsUser: 1000,
+    allowPrivilegeEscalation: false,
+  };
+ 
+  const podSecurityContext: k8s.V1PodSecurityContext = {
+    runAsNonRoot: true,
+    runAsUser: 1000,
+    runAsGroup: 1000,
+    fsGroup: 1000,
+    fsGroupChangePolicy: "OnRootMismatch",
+  };
+ 
+  // Build the claude command string for the main container
+  const claudeArgsEscaped = claudeArgs.map((a) => `'${a.replace(/'/g, "'\\''")}'`).join(" ");
+  const mainCommand = `cat /tmp/prompt/prompt.txt | claude ${claudeArgsEscaped}`;
+ 
+  const job: k8s.V1Job = {
+    apiVersion: "batch/v1",
+    kind: "Job",
+    metadata: {
+      name: jobName,
+      namespace,
+      labels,
+      annotations: {
+        "paperclip.io/adapter-type": "claude_k8s",
+        "paperclip.io/agent-name": agent.name,
+      },
+    },
+    spec: {
+      backoffLimit: 0,
+      ...(timeoutSec > 0 ? { activeDeadlineSeconds: timeoutSec } : {}),
+      ttlSecondsAfterFinished: ttlSeconds,
+      template: {
+        metadata: { labels },
+        spec: {
+          restartPolicy: "Never",
+          serviceAccountName: asString(config.serviceAccountName, "") || undefined,
+          securityContext: podSecurityContext,
+          ...(selfPod.imagePullSecrets.length > 0 ? { imagePullSecrets: selfPod.imagePullSecrets } : {}),
+          ...(selfPod.dnsConfig ? { dnsConfig: selfPod.dnsConfig } : {}),
+          ...(Object.keys(nodeSelector).length > 0 ? { nodeSelector: nodeSelector as Record<string, string> } : {}),
+          ...(tolerations.length > 0 ? { tolerations: tolerations as k8s.V1Toleration[] } : {}),
+          initContainers: [
+            {
+              name: "write-prompt",
+              image: "busybox:1.36",
+              imagePullPolicy: "IfNotPresent",
+              command: ["sh", "-c", "echo \"$PROMPT_CONTENT\" > /tmp/prompt/prompt.txt"],
+              env: [{ name: "PROMPT_CONTENT", value: prompt }],
+              volumeMounts: [{ name: "prompt", mountPath: "/tmp/prompt" }],
+              securityContext,
+              resources: {
+                requests: { cpu: "10m", memory: "16Mi" },
+                limits: { cpu: "100m", memory: "64Mi" },
+              },
+            },
+          ],
+          containers: [
+            {
+              name: "claude",
+              image,
+              imagePullPolicy: asString(config.imagePullPolicy, "IfNotPresent"),
+              workingDir,
+              command: ["sh", "-c", mainCommand],
+              env: envVars,
+              volumeMounts,
+              securityContext,
+              resources: containerResources,
+            },
+          ],
+          volumes,
+        },
+      },
+    },
+  };
+ 
+  return { job, jobName, namespace, prompt, claudeArgs, promptMetrics };
+}
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/coverage/lcov-report/src/server/k8s-client.ts.html b/coverage/lcov-report/src/server/k8s-client.ts.html new file mode 100644 index 0000000..039a863 --- /dev/null +++ b/coverage/lcov-report/src/server/k8s-client.ts.html @@ -0,0 +1,601 @@ + + + + + + Code coverage report for src/server/k8s-client.ts + + + + + + + + + +
+
+

All files / src/server k8s-client.ts

+
+ +
+ 0% + Statements + 0/60 +
+ + +
+ 0% + Branches + 0/32 +
+ + +
+ 0% + Functions + 0/13 +
+ + +
+ 0% + Lines + 0/56 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +80 +81 +82 +83 +84 +85 +86 +87 +88 +89 +90 +91 +92 +93 +94 +95 +96 +97 +98 +99 +100 +101 +102 +103 +104 +105 +106 +107 +108 +109 +110 +111 +112 +113 +114 +115 +116 +117 +118 +119 +120 +121 +122 +123 +124 +125 +126 +127 +128 +129 +130 +131 +132 +133 +134 +135 +136 +137 +138 +139 +140 +141 +142 +143 +144 +145 +146 +147 +148 +149 +150 +151 +152 +153 +154 +155 +156 +157 +158 +159 +160 +161 +162 +163 +164 +165 +166 +167 +168 +169 +170 +171 +172 +173  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  + 
import * as k8s from "@kubernetes/client-node";
+import { readFileSync } from "node:fs";
+ 
+/**
+ * Cached self-pod introspection result. Queried once on first execute(),
+ * then reused for all subsequent Job builds so every Job inherits the
+ * Deployment's image, imagePullSecrets, DNS config, and PVC claim.
+ */
+export interface SelfPodSecretVolume {
+  volumeName: string;
+  secretName: string;
+  mountPath: string;
+  defaultMode: number | undefined;
+}
+ 
+export interface SelfPodInfo {
+  namespace: string;
+  image: string;
+  imagePullSecrets: Array<{ name: string }>;
+  dnsConfig: k8s.V1PodDNSConfig | undefined;
+  pvcClaimName: string | null;
+  secretVolumes: SelfPodSecretVolume[];
+  /** Env vars inherited from the Deployment container. */
+  inheritedEnv: Record<string, string>;
+}
+ 
+/** Keys forwarded from the Deployment container env into Job pods. */
+const INHERITED_ENV_KEYS = [
+  "CLAUDE_CODE_USE_BEDROCK",
+  "AWS_REGION",
+  "AWS_BEARER_TOKEN_BEDROCK",
+  "ANTHROPIC_API_KEY",
+  "OPENAI_API_KEY",
+  "PAPERCLIP_API_URL",
+];
+ 
+let cachedSelfPod: SelfPodInfo | null = null;
+ 
+/**
+ * Cache keyed by kubeconfig path (empty string = in-cluster).
+ * Supports multiple agents with different kubeconfigs.
+ */
+const kcCache = new Map<string, k8s.KubeConfig>();
+ 
+function getKubeConfig(kubeconfigPath?: string): k8s.KubeConfig {
+  const key = kubeconfigPath ?? "";
+  let kc = kcCache.get(key);
+  if (!kc) {
+    kc = new k8s.KubeConfig();
+    if (kubeconfigPath) {
+      kc.loadFromFile(kubeconfigPath);
+    } else {
+      kc.loadFromCluster();
+    }
+    kcCache.set(key, kc);
+  }
+  return kc;
+}
+ 
+export function getBatchApi(kubeconfigPath?: string): k8s.BatchV1Api {
+  return getKubeConfig(kubeconfigPath).makeApiClient(k8s.BatchV1Api);
+}
+ 
+export function getCoreApi(kubeconfigPath?: string): k8s.CoreV1Api {
+  return getKubeConfig(kubeconfigPath).makeApiClient(k8s.CoreV1Api);
+}
+ 
+export function getAuthzApi(kubeconfigPath?: string): k8s.AuthorizationV1Api {
+  return getKubeConfig(kubeconfigPath).makeApiClient(k8s.AuthorizationV1Api);
+}
+ 
+export function getLogApi(kubeconfigPath?: string): k8s.Log {
+  return new k8s.Log(getKubeConfig(kubeconfigPath));
+}
+ 
+/**
+ * Read the current pod's namespace. Checks (in order):
+ * 1. PAPERCLIP_NAMESPACE env var (set explicitly in Deployment)
+ * 2. Service account namespace file (standard in-cluster path)
+ * 3. POD_NAMESPACE env var (Downward API convention)
+ * Falls back to "default" only if none of the above are available.
+ */
+function readInClusterNamespace(): string {
+  const fromEnv = process.env.PAPERCLIP_NAMESPACE ?? process.env.POD_NAMESPACE;
+  if (fromEnv?.trim()) return fromEnv.trim();
+  try {
+    return readFileSync("/var/run/secrets/kubernetes.io/serviceaccount/namespace", "utf-8").trim();
+  } catch {
+    return "default";
+  }
+}
+ 
+/**
+ * Query the K8s API for our own pod spec and cache the result.
+ * Extracts image, imagePullSecrets, dnsConfig, PVC claim name,
+ * and environment variables to forward to Job pods.
+ */
+export async function getSelfPodInfo(kubeconfigPath?: string): Promise<SelfPodInfo> {
+  if (cachedSelfPod) return cachedSelfPod;
+ 
+  const hostname = process.env.HOSTNAME;
+  if (!hostname) {
+    throw new Error("claude_k8s: HOSTNAME env var not set — cannot introspect running pod");
+  }
+ 
+  const namespace = readInClusterNamespace();
+  const coreApi = getCoreApi(kubeconfigPath);
+  const pod = await coreApi.readNamespacedPod({ name: hostname, namespace });
+ 
+  const spec = pod.spec;
+  if (!spec) {
+    throw new Error(`claude_k8s: pod ${hostname} has no spec`);
+  }
+ 
+  const mainContainer = spec.containers[0];
+  if (!mainContainer?.image) {
+    throw new Error(`claude_k8s: pod ${hostname} has no container image`);
+  }
+ 
+  // Find PVC claim name from volumes mounted at /paperclip
+  let pvcClaimName: string | null = null;
+  const dataMount = mainContainer.volumeMounts?.find(
+    (vm) => vm.mountPath === "/paperclip",
+  );
+  if (dataMount) {
+    const volume = spec.volumes?.find((v) => v.name === dataMount.name);
+    pvcClaimName = volume?.persistentVolumeClaim?.claimName ?? null;
+  }
+ 
+  // Discover secret volumes mounted on the main container
+  const secretVolumes: SelfPodSecretVolume[] = [];
+  for (const vm of mainContainer.volumeMounts ?? []) {
+    const vol = spec.volumes?.find((v) => v.name === vm.name);
+    if (vol?.secret?.secretName) {
+      secretVolumes.push({
+        volumeName: vm.name,
+        secretName: vol.secret.secretName,
+        mountPath: vm.mountPath,
+        defaultMode: vol.secret.defaultMode,
+      });
+    }
+  }
+ 
+  // Collect inherited env vars from process.env (these came from the Deployment spec)
+  const inheritedEnv: Record<string, string> = {};
+  for (const key of INHERITED_ENV_KEYS) {
+    const value = process.env[key];
+    if (value !== undefined) {
+      inheritedEnv[key] = value;
+    }
+  }
+ 
+  cachedSelfPod = {
+    namespace,
+    image: mainContainer.image,
+    imagePullSecrets: (spec.imagePullSecrets ?? []).map((s) => ({
+      name: s.name ?? "",
+    })).filter((s) => s.name.length > 0),
+    dnsConfig: spec.dnsConfig,
+    pvcClaimName,
+    secretVolumes,
+    inheritedEnv,
+  };
+ 
+  return cachedSelfPod;
+}
+ 
+/** Reset cached state — useful for tests. */
+export function resetCache(): void {
+  kcCache.clear();
+  cachedSelfPod = null;
+}
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/coverage/lcov-report/src/server/parse.ts.html b/coverage/lcov-report/src/server/parse.ts.html new file mode 100644 index 0000000..426fa4a --- /dev/null +++ b/coverage/lcov-report/src/server/parse.ts.html @@ -0,0 +1,622 @@ + + + + + + Code coverage report for src/server/parse.ts + + + + + + + + + +
+
+

All files / src/server parse.ts

+
+ +
+ 91.08% + Statements + 92/101 +
+ + +
+ 78.4% + Branches + 69/88 +
+ + +
+ 100% + Functions + 11/11 +
+ + +
+ 91.01% + Lines + 81/89 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +80 +81 +82 +83 +84 +85 +86 +87 +88 +89 +90 +91 +92 +93 +94 +95 +96 +97 +98 +99 +100 +101 +102 +103 +104 +105 +106 +107 +108 +109 +110 +111 +112 +113 +114 +115 +116 +117 +118 +119 +120 +121 +122 +123 +124 +125 +126 +127 +128 +129 +130 +131 +132 +133 +134 +135 +136 +137 +138 +139 +140 +141 +142 +143 +144 +145 +146 +147 +148 +149 +150 +151 +152 +153 +154 +155 +156 +157 +158 +159 +160 +161 +162 +163 +164 +165 +166 +167 +168 +169 +170 +171 +172 +173 +174 +175 +176 +177 +178 +179 +180  +  +  +1x +1x +  +  +10x +10x +10x +10x +  +10x +15x +15x +13x +13x +  +9x +9x +1x +1x +1x +  +  +8x +6x +6x +6x +6x +6x +6x +6x +4x +4x +  +  +6x +  +  +2x +2x +2x +  +  +  +10x +8x +  +  +  +  +  +  +  +  +  +2x +2x +  +  +  +  +2x +2x +10x +  +10x +  +  +  +  +  +  +  +  +  +  +15x +15x +  +15x +4x +4x +4x +4x +  +  +  +  +  +  +  +  +4x +  +  +  +  +  +  +  +  +  +  +  +15x +  +  +  +11x +11x +7x +8x +8x +5x +  +  +2x +  +  +  +  +  +  +  +6x +6x +  +  +20x +  +  +6x +6x +  +  +  +  +  +  +4x +4x +4x +  +4x +4x +1x +  +  +4x +4x +4x +4x +  +  +  +9x +  +7x +7x +  +5x +5x +  +4x +4x +  +  +  +5x +5x +6x +  +  +5x +5x +  +  + 
import type { UsageSummary } from "@paperclipai/adapter-utils";
+import { asString, asNumber, parseObject, parseJson } from "@paperclipai/adapter-utils/server-utils";
+ 
+const CLAUDE_AUTH_REQUIRED_RE = /(?:not\s+logged\s+in|please\s+log\s+in|please\s+run\s+`?claude\s+login`?|login\s+required|requires\s+login|unauthorized|authentication\s+required)/i;
+const URL_RE = /(https?:\/\/[^\s'"`<>()[\]{};,!?]+[^\s'"`<>()[\]{};,!.?:]+)/gi;
+ 
+export function parseClaudeStreamJson(stdout: string) {
+  let sessionId: string | null = null;
+  let model = "";
+  let finalResult: Record<string, unknown> | null = null;
+  const assistantTexts: string[] = [];
+ 
+  for (const rawLine of stdout.split(/\r?\n/)) {
+    const line = rawLine.trim();
+    if (!line) continue;
+    const event = parseJson(line);
+    if (!event) continue;
+ 
+    const type = asString(event.type, "");
+    if (type === "system" && asString(event.subtype, "") === "init") {
+      sessionId = asString(event.session_id, sessionId ?? "") || sessionId;
+      model = asString(event.model, model);
+      continue;
+    }
+ 
+    if (type === "assistant") {
+      sessionId = asString(event.session_id, sessionId ?? "") || sessionId;
+      const message = parseObject(event.message);
+      const content = Array.isArray(message.content) ? message.content : [];
+      for (const entry of content) {
+        Iif (typeof entry !== "object" || entry === null || Array.isArray(entry)) continue;
+        const block = entry as Record<string, unknown>;
+        if (asString(block.type, "") === "text") {
+          const text = asString(block.text, "");
+          Eif (text) assistantTexts.push(text);
+        }
+      }
+      continue;
+    }
+ 
+    Eif (type === "result") {
+      finalResult = event;
+      sessionId = asString(event.session_id, sessionId ?? "") || sessionId;
+    }
+  }
+ 
+  if (!finalResult) {
+    return {
+      sessionId,
+      model,
+      costUsd: null as number | null,
+      usage: null as UsageSummary | null,
+      summary: assistantTexts.join("\n\n").trim(),
+      resultJson: null as Record<string, unknown> | null,
+    };
+  }
+ 
+  const usageObj = parseObject(finalResult.usage);
+  const usage: UsageSummary = {
+    inputTokens: asNumber(usageObj.input_tokens, 0),
+    cachedInputTokens: asNumber(usageObj.cache_read_input_tokens, 0),
+    outputTokens: asNumber(usageObj.output_tokens, 0),
+  };
+  const costRaw = finalResult.total_cost_usd;
+  const costUsd = typeof costRaw === "number" && Number.isFinite(costRaw) ? costRaw : null;
+  const summary = asString(finalResult.result, assistantTexts.join("\n\n")).trim();
+ 
+  return {
+    sessionId,
+    model,
+    costUsd,
+    usage,
+    summary,
+    resultJson: finalResult,
+  };
+}
+ 
+function extractClaudeErrorMessages(parsed: Record<string, unknown>): string[] {
+  const raw = Array.isArray(parsed.errors) ? parsed.errors : [];
+  const messages: string[] = [];
+ 
+  for (const entry of raw) {
+    Eif (typeof entry === "string") {
+      const msg = entry.trim();
+      Eif (msg) messages.push(msg);
+      continue;
+    }
+ 
+    if (typeof entry !== "object" || entry === null || Array.isArray(entry)) {
+      continue;
+    }
+ 
+    const obj = entry as Record<string, unknown>;
+    const msg = asString(obj.message, "") || asString(obj.error, "") || asString(obj.code, "");
+    Iif (msg) {
+      messages.push(msg);
+      continue;
+    }
+ 
+    try {
+      messages.push(JSON.stringify(obj));
+    } catch {
+      // skip non-serializable entry
+    }
+  }
+ 
+  return messages;
+}
+ 
+export function extractClaudeLoginUrl(text: string): string | null {
+  const match = text.match(URL_RE);
+  if (!match || match.length === 0) return null;
+  for (const rawUrl of match) {
+    const cleaned = rawUrl.replace(/[\])}.!,?;:'\"]+$/g, "");
+    if (cleaned.includes("claude") || cleaned.includes("anthropic") || cleaned.includes("auth")) {
+      return cleaned;
+    }
+  }
+  return match[0]?.replace(/[\])}.!,?;:'\"]+$/g, "") ?? null;
+}
+ 
+export function detectClaudeLoginRequired(input: {
+  parsed: Record<string, unknown> | null;
+  stdout: string;
+  stderr: string;
+}): { requiresLogin: boolean; loginUrl: string | null } {
+  const resultText = asString(input.parsed?.result, "").trim();
+  const messages = [resultText, ...extractClaudeErrorMessages(input.parsed ?? {}), input.stdout, input.stderr]
+    .join("\n")
+    .split(/\r?\n/)
+    .map((line) => line.trim())
+    .filter(Boolean);
+ 
+  const requiresLogin = messages.some((line) => CLAUDE_AUTH_REQUIRED_RE.test(line));
+  return {
+    requiresLogin,
+    loginUrl: extractClaudeLoginUrl([input.stdout, input.stderr].join("\n")),
+  };
+}
+ 
+export function describeClaudeFailure(parsed: Record<string, unknown>): string | null {
+  const subtype = asString(parsed.subtype, "");
+  const resultText = asString(parsed.result, "").trim();
+  const errors = extractClaudeErrorMessages(parsed);
+ 
+  let detail = resultText;
+  if (!detail && errors.length > 0) {
+    detail = errors[0] ?? "";
+  }
+ 
+  const parts = ["Claude run failed"];
+  if (subtype) parts.push(`subtype=${subtype}`);
+  if (detail) parts.push(detail);
+  return parts.length > 1 ? parts.join(": ") : null;
+}
+ 
+export function isClaudeMaxTurnsResult(parsed: Record<string, unknown> | null | undefined): boolean {
+  if (!parsed) return false;
+ 
+  const subtype = asString(parsed.subtype, "").trim().toLowerCase();
+  if (subtype === "error_max_turns") return true;
+ 
+  const stopReason = asString(parsed.stop_reason, "").trim().toLowerCase();
+  if (stopReason === "max_turns") return true;
+ 
+  const resultText = asString(parsed.result, "").trim();
+  return /max(?:imum)?\s+turns?/i.test(resultText);
+}
+ 
+export function isClaudeUnknownSessionError(parsed: Record<string, unknown>): boolean {
+  const resultText = asString(parsed.result, "").trim();
+  const allMessages = [resultText, ...extractClaudeErrorMessages(parsed)]
+    .map((msg) => msg.trim())
+    .filter(Boolean);
+ 
+  return allMessages.some((msg) =>
+    /no conversation found with session id|unknown session|session .* not found/i.test(msg),
+  );
+}
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/coverage/lcov-report/src/server/session.ts.html b/coverage/lcov-report/src/server/session.ts.html new file mode 100644 index 0000000..f484d2d --- /dev/null +++ b/coverage/lcov-report/src/server/session.ts.html @@ -0,0 +1,238 @@ + + + + + + Code coverage report for src/server/session.ts + + + + + + + + + +
+
+

All files / src/server session.ts

+
+ +
+ 100% + Statements + 26/26 +
+ + +
+ 100% + Branches + 57/57 +
+ + +
+ 100% + Functions + 4/4 +
+ + +
+ 100% + Lines + 21/21 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52  +  +  +203x +  +  +1x +  +25x +20x +20x +25x +  +16x +  +  +25x +25x +25x +25x +  +  +  +  +  +  +  +  +8x +6x +8x +  +4x +  +  +8x +8x +8x +8x +  +  +  +  +  +  +  +  +7x +5x +  +  + 
import type { AdapterSessionCodec } from "@paperclipai/adapter-utils";
+ 
+function readNonEmptyString(value: unknown): string | null {
+  return typeof value === "string" && value.trim().length > 0 ? value.trim() : null;
+}
+ 
+export const sessionCodec: AdapterSessionCodec = {
+  deserialize(raw: unknown) {
+    if (typeof raw !== "object" || raw === null || Array.isArray(raw)) return null;
+    const record = raw as Record<string, unknown>;
+    const sessionId = readNonEmptyString(record.sessionId) ?? readNonEmptyString(record.session_id);
+    if (!sessionId) return null;
+    const cwd =
+      readNonEmptyString(record.cwd) ??
+      readNonEmptyString(record.workdir) ??
+      readNonEmptyString(record.folder);
+    const workspaceId = readNonEmptyString(record.workspaceId) ?? readNonEmptyString(record.workspace_id);
+    const repoUrl = readNonEmptyString(record.repoUrl) ?? readNonEmptyString(record.repo_url);
+    const repoRef = readNonEmptyString(record.repoRef) ?? readNonEmptyString(record.repo_ref);
+    return {
+      sessionId,
+      ...(cwd ? { cwd } : {}),
+      ...(workspaceId ? { workspaceId } : {}),
+      ...(repoUrl ? { repoUrl } : {}),
+      ...(repoRef ? { repoRef } : {}),
+    };
+  },
+  serialize(params: Record<string, unknown> | null) {
+    if (!params) return null;
+    const sessionId = readNonEmptyString(params.sessionId) ?? readNonEmptyString(params.session_id);
+    if (!sessionId) return null;
+    const cwd =
+      readNonEmptyString(params.cwd) ??
+      readNonEmptyString(params.workdir) ??
+      readNonEmptyString(params.folder);
+    const workspaceId = readNonEmptyString(params.workspaceId) ?? readNonEmptyString(params.workspace_id);
+    const repoUrl = readNonEmptyString(params.repoUrl) ?? readNonEmptyString(params.repo_url);
+    const repoRef = readNonEmptyString(params.repoRef) ?? readNonEmptyString(params.repo_ref);
+    return {
+      sessionId,
+      ...(cwd ? { cwd } : {}),
+      ...(workspaceId ? { workspaceId } : {}),
+      ...(repoUrl ? { repoUrl } : {}),
+      ...(repoRef ? { repoRef } : {}),
+    };
+  },
+  getDisplayId(params: Record<string, unknown> | null) {
+    if (!params) return null;
+    return readNonEmptyString(params.sessionId) ?? readNonEmptyString(params.session_id);
+  },
+};
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/coverage/lcov-report/src/server/test.ts.html b/coverage/lcov-report/src/server/test.ts.html new file mode 100644 index 0000000..6887129 --- /dev/null +++ b/coverage/lcov-report/src/server/test.ts.html @@ -0,0 +1,808 @@ + + + + + + Code coverage report for src/server/test.ts + + + + + + + + + +
+
+

All files / src/server test.ts

+
+ +
+ 0% + Statements + 0/67 +
+ + +
+ 0% + Branches + 0/30 +
+ + +
+ 0% + Functions + 0/9 +
+ + +
+ 0% + Lines + 0/63 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +80 +81 +82 +83 +84 +85 +86 +87 +88 +89 +90 +91 +92 +93 +94 +95 +96 +97 +98 +99 +100 +101 +102 +103 +104 +105 +106 +107 +108 +109 +110 +111 +112 +113 +114 +115 +116 +117 +118 +119 +120 +121 +122 +123 +124 +125 +126 +127 +128 +129 +130 +131 +132 +133 +134 +135 +136 +137 +138 +139 +140 +141 +142 +143 +144 +145 +146 +147 +148 +149 +150 +151 +152 +153 +154 +155 +156 +157 +158 +159 +160 +161 +162 +163 +164 +165 +166 +167 +168 +169 +170 +171 +172 +173 +174 +175 +176 +177 +178 +179 +180 +181 +182 +183 +184 +185 +186 +187 +188 +189 +190 +191 +192 +193 +194 +195 +196 +197 +198 +199 +200 +201 +202 +203 +204 +205 +206 +207 +208 +209 +210 +211 +212 +213 +214 +215 +216 +217 +218 +219 +220 +221 +222 +223 +224 +225 +226 +227 +228 +229 +230 +231 +232 +233 +234 +235 +236 +237 +238 +239 +240 +241 +242  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  + 
import type {
+  AdapterEnvironmentCheck,
+  AdapterEnvironmentTestContext,
+  AdapterEnvironmentTestResult,
+} from "@paperclipai/adapter-utils";
+import { asString, parseObject } from "@paperclipai/adapter-utils/server-utils";
+import { getSelfPodInfo, getCoreApi, getAuthzApi } from "./k8s-client.js";
+ 
+function summarizeStatus(checks: AdapterEnvironmentCheck[]): AdapterEnvironmentTestResult["status"] {
+  if (checks.some((c) => c.level === "error")) return "fail";
+  if (checks.some((c) => c.level === "warn")) return "warn";
+  return "pass";
+}
+ 
+async function checkApiReachable(checks: AdapterEnvironmentCheck[], kubeconfigPath?: string): Promise<boolean> {
+  try {
+    const selfPod = await getSelfPodInfo(kubeconfigPath);
+    checks.push({
+      code: "k8s_api_reachable",
+      level: "info",
+      message: `Kubernetes API reachable; running in namespace ${selfPod.namespace}`,
+      detail: `Image: ${selfPod.image}`,
+    });
+    return true;
+  } catch (err) {
+    const msg = err instanceof Error ? err.message : String(err);
+    checks.push({
+      code: "k8s_api_unreachable",
+      level: "error",
+      message: `Cannot reach Kubernetes API: ${msg}`,
+      hint: "Ensure the pod has a valid service account token mounted and the API server is accessible.",
+    });
+    return false;
+  }
+}
+ 
+async function checkNamespace(
+  namespace: string,
+  selfPodNamespace: string,
+  checks: AdapterEnvironmentCheck[],
+  kubeconfigPath?: string,
+): Promise<boolean> {
+  // If targeting the same namespace we're running in, skip the cluster-scoped
+  // readNamespace call — we know it exists, and the SA may lack cluster-level
+  // namespace get permissions.
+  if (namespace === selfPodNamespace) {
+    checks.push({
+      code: "k8s_namespace_exists",
+      level: "info",
+      message: `Target namespace is the pod namespace: ${namespace}`,
+    });
+    return true;
+  }
+ 
+  try {
+    const coreApi = getCoreApi(kubeconfigPath);
+    await coreApi.readNamespace({ name: namespace });
+    checks.push({
+      code: "k8s_namespace_exists",
+      level: "info",
+      message: `Target namespace exists: ${namespace}`,
+    });
+    return true;
+  } catch (err) {
+    const msg = err instanceof Error ? err.message : String(err);
+    checks.push({
+      code: "k8s_namespace_check_failed",
+      level: "warn",
+      message: `Cannot verify namespace "${namespace}": ${msg}`,
+      hint: "The service account may lack cluster-level namespace read permissions. The namespace may still be usable — verify RBAC checks below.",
+    });
+    // Don't block on this — RBAC checks below will catch actual permission issues
+    return true;
+  }
+}
+ 
+async function checkRbac(
+  namespace: string,
+  checks: AdapterEnvironmentCheck[],
+  kubeconfigPath?: string,
+): Promise<void> {
+  const authzApi = getAuthzApi(kubeconfigPath);
+ 
+  const rbacChecks = [
+    { resource: "jobs", group: "batch", verb: "create", code: "k8s_rbac_job_create", label: "create Jobs" },
+    { resource: "jobs", group: "batch", verb: "delete", code: "k8s_rbac_job_delete", label: "delete Jobs" },
+    { resource: "jobs", group: "batch", verb: "get", code: "k8s_rbac_job_get", label: "get Jobs" },
+    { resource: "pods", group: "", verb: "list", code: "k8s_rbac_pod_list", label: "list Pods" },
+    { resource: "pods/log", group: "", verb: "get", code: "k8s_rbac_pod_log", label: "get Pod logs" },
+  ];
+ 
+  for (const check of rbacChecks) {
+    try {
+      const review = await authzApi.createSelfSubjectAccessReview({
+        body: {
+          apiVersion: "authorization.k8s.io/v1",
+          kind: "SelfSubjectAccessReview",
+          spec: {
+            resourceAttributes: {
+              namespace,
+              verb: check.verb,
+              resource: check.resource,
+              group: check.group,
+            },
+          },
+        },
+      });
+      if (review.status?.allowed) {
+        checks.push({
+          code: check.code,
+          level: "info",
+          message: `RBAC: allowed to ${check.label} in ${namespace}`,
+        });
+      } else {
+        checks.push({
+          code: check.code,
+          level: "error",
+          message: `RBAC: not allowed to ${check.label} in ${namespace}`,
+          hint: `Grant the service account permission to ${check.verb} ${check.resource} in namespace ${namespace}.`,
+        });
+      }
+    } catch (err) {
+      const msg = err instanceof Error ? err.message : String(err);
+      checks.push({
+        code: check.code,
+        level: "warn",
+        message: `RBAC check failed for ${check.label}: ${msg}`,
+        hint: "SelfSubjectAccessReview may not be available; verify permissions manually.",
+      });
+    }
+  }
+}
+ 
+async function checkSecret(
+  namespace: string,
+  secretName: string,
+  checks: AdapterEnvironmentCheck[],
+  kubeconfigPath?: string,
+): Promise<void> {
+  try {
+    const coreApi = getCoreApi(kubeconfigPath);
+    await coreApi.readNamespacedSecret({ name: secretName, namespace });
+    checks.push({
+      code: "k8s_secret_exists",
+      level: "info",
+      message: `Secret "${secretName}" exists in namespace ${namespace}`,
+    });
+  } catch {
+    checks.push({
+      code: "k8s_secret_missing",
+      level: "warn",
+      message: `Secret "${secretName}" not found in namespace ${namespace}`,
+      hint: `Ensure the paperclip-secrets Secret exists with keys for ANTHROPIC_API_KEY and/or AWS_BEARER_TOKEN_BEDROCK.`,
+    });
+  }
+}
+ 
+async function checkPvc(
+  selfPod: { pvcClaimName: string | null; namespace: string },
+  checks: AdapterEnvironmentCheck[],
+  kubeconfigPath?: string,
+): Promise<void> {
+  if (!selfPod.pvcClaimName) {
+    checks.push({
+      code: "k8s_pvc_not_detected",
+      level: "warn",
+      message: "No PVC detected on /paperclip mount — session resume and workspace sharing will not work.",
+      hint: "Ensure the Paperclip Deployment has a PVC mounted at /paperclip with ReadWriteMany access mode.",
+    });
+    return;
+  }
+ 
+  try {
+    const coreApi = getCoreApi(kubeconfigPath);
+    const pvc = await coreApi.readNamespacedPersistentVolumeClaim({
+      name: selfPod.pvcClaimName,
+      namespace: selfPod.namespace,
+    });
+    const accessModes = pvc.spec?.accessModes ?? [];
+    const isRwx = accessModes.includes("ReadWriteMany");
+    if (isRwx) {
+      checks.push({
+        code: "k8s_pvc_rwx",
+        level: "info",
+        message: `PVC "${selfPod.pvcClaimName}" has ReadWriteMany access — Job pods can mount it.`,
+      });
+    } else {
+      checks.push({
+        code: "k8s_pvc_not_rwx",
+        level: "warn",
+        message: `PVC "${selfPod.pvcClaimName}" access modes: ${accessModes.join(", ")}. ReadWriteMany is required for Job pods to share the volume.`,
+        hint: "Change the PVC accessMode to ReadWriteMany in Helm values.",
+      });
+    }
+  } catch (err) {
+    const msg = err instanceof Error ? err.message : String(err);
+    checks.push({
+      code: "k8s_pvc_check_failed",
+      level: "warn",
+      message: `Could not read PVC "${selfPod.pvcClaimName}": ${msg}`,
+    });
+  }
+}
+ 
+export async function testEnvironment(
+  ctx: AdapterEnvironmentTestContext,
+): Promise<AdapterEnvironmentTestResult> {
+  const checks: AdapterEnvironmentCheck[] = [];
+  const config = parseObject(ctx.config);
+  const secretRef = asString(config.secretRef, "paperclip-secrets");
+  const kubeconfigPath = asString(config.kubeconfig, "") || undefined;
+ 
+  // 1. K8s API reachable + self-pod introspection
+  const apiOk = await checkApiReachable(checks, kubeconfigPath);
+  if (!apiOk) {
+    return { adapterType: ctx.adapterType, status: summarizeStatus(checks), checks, testedAt: new Date().toISOString() };
+  }
+ 
+  const selfPod = await getSelfPodInfo(kubeconfigPath);
+  const namespace = asString(config.namespace, "") || selfPod.namespace;
+ 
+  // 2. Target namespace exists
+  const nsOk = await checkNamespace(namespace, selfPod.namespace, checks, kubeconfigPath);
+  if (!nsOk) {
+    return { adapterType: ctx.adapterType, status: summarizeStatus(checks), checks, testedAt: new Date().toISOString() };
+  }
+ 
+  // 3-5. Run remaining checks in parallel
+  await Promise.all([
+    checkRbac(namespace, checks, kubeconfigPath),
+    checkSecret(namespace, secretRef, checks, kubeconfigPath),
+    checkPvc(selfPod, checks, kubeconfigPath),
+  ]);
+ 
+  return {
+    adapterType: ctx.adapterType,
+    status: summarizeStatus(checks),
+    checks,
+    testedAt: new Date().toISOString(),
+  };
+}
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/coverage/lcov-report/src/ui-parser.ts.html b/coverage/lcov-report/src/ui-parser.ts.html new file mode 100644 index 0000000..c5f0325 --- /dev/null +++ b/coverage/lcov-report/src/ui-parser.ts.html @@ -0,0 +1,559 @@ + + + + + + Code coverage report for src/ui-parser.ts + + + + + + + + + +
+
+

All files / src ui-parser.ts

+
+ +
+ 92.94% + Statements + 79/85 +
+ + +
+ 74.07% + Branches + 80/108 +
+ + +
+ 100% + Functions + 5/5 +
+ + +
+ 95.94% + Lines + 71/74 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +80 +81 +82 +83 +84 +85 +86 +87 +88 +89 +90 +91 +92 +93 +94 +95 +96 +97 +98 +99 +100 +101 +102 +103 +104 +105 +106 +107 +108 +109 +110 +111 +112 +113 +114 +115 +116 +117 +118 +119 +120 +121 +122 +123 +124 +125 +126 +127 +128 +129 +130 +131 +132 +133 +134 +135 +136 +137 +138 +139 +140 +141 +142 +143 +144 +145 +146 +147 +148 +149 +150 +151 +152 +153 +154 +155 +156 +157 +158 +159  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +48x +43x +  +  +  +16x +  +  +  +4x +1x +1x +  +1x +  +  +  +4x +  +  +  +  +  +  +  +  +20x +20x +  +2x +  +  +  +  +20x +20x +2x +  +  +18x +20x +2x +  +  +  +  +  +  +  +  +  +16x +7x +7x +7x +7x +6x +6x +6x +6x +2x +2x +4x +2x +2x +2x +2x +  +  +  +  +  +  +  +  +  +  +  +  +  +7x +  +  +9x +4x +4x +4x +4x +4x +4x +4x +4x +1x +1x +3x +3x +3x +3x +3x +2x +1x +1x +1x +2x +2x +  +1x +  +3x +  +  +4x +  +  +5x +4x +4x +4x +4x +4x +4x +4x +4x +4x +4x +  +  +  +  +  +  +  +  +  +  +  +  +  +1x +  + 
/**
+ * Self-contained stdout parser for Claude stream-json output.
+ * Zero external imports — required by the Paperclip adapter plugin UI parser contract.
+ */
+ 
+type TranscriptEntry =
+  | { kind: "assistant"; ts: string; text: string }
+  | { kind: "thinking"; ts: string; text: string }
+  | { kind: "user"; ts: string; text: string }
+  | { kind: "tool_call"; ts: string; name: string; input: unknown; toolUseId?: string }
+  | { kind: "tool_result"; ts: string; toolUseId: string; content: string; isError: boolean }
+  | { kind: "init"; ts: string; model: string; sessionId: string }
+  | { kind: "result"; ts: string; text: string; inputTokens: number; outputTokens: number; cachedTokens: number; costUsd: number; subtype: string; isError: boolean; errors: string[] }
+  | { kind: "stderr"; ts: string; text: string }
+  | { kind: "system"; ts: string; text: string }
+  | { kind: "stdout"; ts: string; text: string };
+ 
+function asRecord(value: unknown): Record<string, unknown> | null {
+  if (typeof value !== "object" || value === null || Array.isArray(value)) return null;
+  return value as Record<string, unknown>;
+}
+ 
+function asNumber(value: unknown): number {
+  return typeof value === "number" && Number.isFinite(value) ? value : 0;
+}
+ 
+function errorText(value: unknown): string {
+  if (typeof value === "string") return value;
+  const rec = asRecord(value);
+  Iif (!rec) return "";
+  const msg =
+    (typeof rec.message === "string" && rec.message) ||
+    (typeof rec.error === "string" && rec.error) ||
+    (typeof rec.code === "string" && rec.code) ||
+    "";
+  if (msg) return msg;
+  try {
+    return JSON.stringify(rec);
+  } catch {
+    return "";
+  }
+}
+ 
+function safeJsonParse(text: string): unknown {
+  try {
+    return JSON.parse(text);
+  } catch {
+    return null;
+  }
+}
+ 
+export function parseStdoutLine(line: string, ts: string): TranscriptEntry[] {
+  const parsed = asRecord(safeJsonParse(line));
+  if (!parsed) {
+    return [{ kind: "stdout", ts, text: line }];
+  }
+ 
+  const type = typeof parsed.type === "string" ? parsed.type : "";
+  if (type === "system" && parsed.subtype === "init") {
+    return [
+      {
+        kind: "init",
+        ts,
+        model: typeof parsed.model === "string" ? parsed.model : "unknown",
+        sessionId: typeof parsed.session_id === "string" ? parsed.session_id : "",
+      },
+    ];
+  }
+ 
+  if (type === "assistant") {
+    const message = asRecord(parsed.message) ?? {};
+    const content = Array.isArray(message.content) ? message.content : [];
+    const entries: TranscriptEntry[] = [];
+    for (const blockRaw of content) {
+      const block = asRecord(blockRaw);
+      Iif (!block) continue;
+      const blockType = typeof block.type === "string" ? block.type : "";
+      if (blockType === "text") {
+        const text = typeof block.text === "string" ? block.text : "";
+        if (text) entries.push({ kind: "assistant", ts, text });
+      } else if (blockType === "thinking") {
+        const text = typeof block.thinking === "string" ? block.thinking : "";
+        if (text) entries.push({ kind: "thinking", ts, text });
+      } else if (EblockType === "tool_use") {
+        entries.push({
+          kind: "tool_call",
+          ts,
+          name: typeof block.name === "string" ? block.name : "unknown",
+          toolUseId:
+            typeof block.id === "string"
+              ? block.id
+              : typeof block.tool_use_id === "string"
+                ? block.tool_use_id
+                : undefined,
+          input: block.input ?? {},
+        });
+      }
+    }
+    return entries.length > 0 ? entries : [{ kind: "stdout", ts, text: line }];
+  }
+ 
+  if (type === "user") {
+    const message = asRecord(parsed.message) ?? {};
+    const content = Array.isArray(message.content) ? message.content : [];
+    const entries: TranscriptEntry[] = [];
+    for (const blockRaw of content) {
+      const block = asRecord(blockRaw);
+      Iif (!block) continue;
+      const blockType = typeof block.type === "string" ? block.type : "";
+      if (blockType === "text") {
+        const text = typeof block.text === "string" ? block.text : "";
+        Eif (text) entries.push({ kind: "user", ts, text });
+      } else if (EblockType === "tool_result") {
+        const toolUseId = typeof block.tool_use_id === "string" ? block.tool_use_id : "";
+        const isError = block.is_error === true;
+        let text = "";
+        if (typeof block.content === "string") {
+          text = block.content;
+        } else if (EArray.isArray(block.content)) {
+          const parts: string[] = [];
+          for (const part of block.content) {
+            const p = asRecord(part);
+            Eif (p && typeof p.text === "string") parts.push(p.text);
+          }
+          text = parts.join("\n");
+        }
+        entries.push({ kind: "tool_result", ts, toolUseId, content: text, isError });
+      }
+    }
+    Eif (entries.length > 0) return entries;
+  }
+ 
+  if (type === "result") {
+    const usage = asRecord(parsed.usage) ?? {};
+    const inputTokens = asNumber(usage.input_tokens);
+    const outputTokens = asNumber(usage.output_tokens);
+    const cachedTokens = asNumber(usage.cache_read_input_tokens);
+    const costUsd = asNumber(parsed.total_cost_usd);
+    const subtype = typeof parsed.subtype === "string" ? parsed.subtype : "";
+    const isError = parsed.is_error === true;
+    const errors = Array.isArray(parsed.errors) ? parsed.errors.map(errorText).filter(Boolean) : [];
+    const text = typeof parsed.result === "string" ? parsed.result : "";
+    return [{
+      kind: "result",
+      ts,
+      text,
+      inputTokens,
+      outputTokens,
+      cachedTokens,
+      costUsd,
+      subtype,
+      isError,
+      errors,
+    }];
+  }
+ 
+  return [{ kind: "stdout", ts, text: line }];
+}
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/coverage/lcov.info b/coverage/lcov.info new file mode 100644 index 0000000..c297091 --- /dev/null +++ b/coverage/lcov.info @@ -0,0 +1,1471 @@ +TN: +SF:src/ui-parser.ts +FN:18,asRecord +FN:23,asNumber +FN:27,errorText +FN:44,safeJsonParse +FN:52,parseStdoutLine +FNF:5 +FNH:5 +FNDA:48,asRecord +FNDA:16,asNumber +FNDA:4,errorText +FNDA:20,safeJsonParse +FNDA:20,parseStdoutLine +DA:19,48 +DA:20,43 +DA:24,16 +DA:28,4 +DA:29,1 +DA:30,1 +DA:32,1 +DA:36,4 +DA:37,0 +DA:38,0 +DA:40,0 +DA:45,20 +DA:46,20 +DA:48,2 +DA:53,20 +DA:54,20 +DA:55,2 +DA:58,18 +DA:59,20 +DA:60,2 +DA:70,16 +DA:71,7 +DA:72,7 +DA:73,7 +DA:74,7 +DA:75,6 +DA:76,6 +DA:77,6 +DA:78,6 +DA:79,2 +DA:80,2 +DA:81,4 +DA:82,2 +DA:83,2 +DA:84,2 +DA:85,2 +DA:99,7 +DA:102,9 +DA:103,4 +DA:104,4 +DA:105,4 +DA:106,4 +DA:107,4 +DA:108,4 +DA:109,4 +DA:110,4 +DA:111,1 +DA:112,1 +DA:113,3 +DA:114,3 +DA:115,3 +DA:116,3 +DA:117,3 +DA:118,2 +DA:119,1 +DA:120,1 +DA:121,1 +DA:122,2 +DA:123,2 +DA:125,1 +DA:127,3 +DA:130,4 +DA:133,5 +DA:134,4 +DA:135,4 +DA:136,4 +DA:137,4 +DA:138,4 +DA:139,4 +DA:140,4 +DA:141,4 +DA:142,4 +DA:143,4 +DA:157,1 +LF:74 +LH:71 +BRDA:19,0,0,5 +BRDA:19,0,1,43 +BRDA:19,1,0,48 +BRDA:19,1,1,45 +BRDA:19,1,2,43 +BRDA:24,2,0,4 +BRDA:24,2,1,12 +BRDA:24,3,0,16 +BRDA:24,3,1,4 +BRDA:28,4,0,3 +BRDA:28,4,1,1 +BRDA:30,5,0,0 +BRDA:30,5,1,1 +BRDA:32,6,0,1 +BRDA:32,6,1,1 +BRDA:32,6,2,0 +BRDA:32,6,3,0 +BRDA:32,6,4,0 +BRDA:32,6,5,0 +BRDA:32,6,6,0 +BRDA:36,7,0,1 +BRDA:36,7,1,3 +BRDA:54,8,0,2 +BRDA:54,8,1,18 +BRDA:58,9,0,18 +BRDA:58,9,1,0 +BRDA:59,10,0,2 +BRDA:59,10,1,18 +BRDA:59,11,0,20 +BRDA:59,11,1,2 +BRDA:64,12,0,1 +BRDA:64,12,1,1 +BRDA:65,13,0,1 +BRDA:65,13,1,1 +BRDA:70,14,0,7 +BRDA:70,14,1,9 +BRDA:71,15,0,7 +BRDA:71,15,1,0 +BRDA:72,16,0,7 +BRDA:72,16,1,0 +BRDA:76,17,0,0 +BRDA:76,17,1,6 +BRDA:77,18,0,6 +BRDA:77,18,1,0 +BRDA:78,19,0,2 +BRDA:78,19,1,4 +BRDA:79,20,0,2 +BRDA:79,20,1,0 +BRDA:80,21,0,1 +BRDA:80,21,1,1 +BRDA:81,22,0,2 +BRDA:81,22,1,2 +BRDA:82,23,0,2 +BRDA:82,23,1,0 +BRDA:83,24,0,1 +BRDA:83,24,1,1 +BRDA:84,25,0,2 +BRDA:84,25,1,0 +BRDA:88,26,0,2 +BRDA:88,26,1,0 +BRDA:90,27,0,1 +BRDA:90,27,1,1 +BRDA:92,28,0,1 +BRDA:92,28,1,0 +BRDA:95,29,0,2 +BRDA:95,29,1,0 +BRDA:99,30,0,4 +BRDA:99,30,1,3 +BRDA:102,31,0,4 +BRDA:102,31,1,5 +BRDA:103,32,0,4 +BRDA:103,32,1,0 +BRDA:104,33,0,4 +BRDA:104,33,1,0 +BRDA:108,34,0,0 +BRDA:108,34,1,4 +BRDA:109,35,0,4 +BRDA:109,35,1,0 +BRDA:110,36,0,1 +BRDA:110,36,1,3 +BRDA:111,37,0,1 +BRDA:111,37,1,0 +BRDA:112,38,0,1 +BRDA:112,38,1,0 +BRDA:113,39,0,3 +BRDA:113,39,1,0 +BRDA:114,40,0,3 +BRDA:114,40,1,0 +BRDA:117,41,0,2 +BRDA:117,41,1,1 +BRDA:119,42,0,1 +BRDA:119,42,1,0 +BRDA:123,43,0,2 +BRDA:123,43,1,0 +BRDA:123,44,0,2 +BRDA:123,44,1,2 +BRDA:130,45,0,4 +BRDA:130,45,1,0 +BRDA:133,46,0,4 +BRDA:133,46,1,1 +BRDA:134,47,0,4 +BRDA:134,47,1,3 +BRDA:139,48,0,1 +BRDA:139,48,1,3 +BRDA:141,49,0,3 +BRDA:141,49,1,1 +BRDA:142,50,0,1 +BRDA:142,50,1,3 +BRF:108 +BRH:80 +end_of_record +TN: +SF:src/server/execute.ts +FN:20,waitForPod +FN:46,(anonymous_1) +FN:56,(anonymous_2) +FN:56,(anonymous_3) +FN:78,(anonymous_4) +FN:81,(anonymous_5) +FN:103,(anonymous_6) +FN:121,(anonymous_7) +FN:131,streamPodLogs +FN:141,(anonymous_9) +FN:144,(anonymous_10) +FN:165,readPodLogs +FN:187,waitForJobCompletion +FN:200,(anonymous_13) +FN:203,(anonymous_14) +FN:209,(anonymous_15) +FN:218,getPodExitCode +FN:227,(anonymous_17) +FN:234,cleanupJob +FN:253,execute +FN:271,(anonymous_20) +FN:272,(anonymous_21) +FN:275,(anonymous_22) +FN:423,(anonymous_23) +FN:437,(anonymous_24) +FNF:25 +FNH:0 +FNDA:0,waitForPod +FNDA:0,(anonymous_1) +FNDA:0,(anonymous_2) +FNDA:0,(anonymous_3) +FNDA:0,(anonymous_4) +FNDA:0,(anonymous_5) +FNDA:0,(anonymous_6) +FNDA:0,(anonymous_7) +FNDA:0,streamPodLogs +FNDA:0,(anonymous_9) +FNDA:0,(anonymous_10) +FNDA:0,readPodLogs +FNDA:0,waitForJobCompletion +FNDA:0,(anonymous_13) +FNDA:0,(anonymous_14) +FNDA:0,(anonymous_15) +FNDA:0,getPodExitCode +FNDA:0,(anonymous_17) +FNDA:0,cleanupJob +FNDA:0,execute +FNDA:0,(anonymous_20) +FNDA:0,(anonymous_21) +FNDA:0,(anonymous_22) +FNDA:0,(anonymous_23) +FNDA:0,(anonymous_24) +DA:14,0 +DA:27,0 +DA:28,0 +DA:29,0 +DA:31,0 +DA:33,0 +DA:34,0 +DA:35,0 +DA:39,0 +DA:41,0 +DA:42,0 +DA:43,0 +DA:44,0 +DA:46,0 +DA:47,0 +DA:50,0 +DA:51,0 +DA:52,0 +DA:53,0 +DA:56,0 +DA:57,0 +DA:58,0 +DA:59,0 +DA:60,0 +DA:61,0 +DA:62,0 +DA:64,0 +DA:65,0 +DA:66,0 +DA:68,0 +DA:69,0 +DA:73,0 +DA:74,0 +DA:78,0 +DA:79,0 +DA:81,0 +DA:82,0 +DA:83,0 +DA:87,0 +DA:88,0 +DA:89,0 +DA:90,0 +DA:92,0 +DA:93,0 +DA:94,0 +DA:96,0 +DA:97,0 +DA:102,0 +DA:103,0 +DA:104,0 +DA:106,0 +DA:107,0 +DA:111,0 +DA:112,0 +DA:113,0 +DA:114,0 +DA:116,0 +DA:117,0 +DA:121,0 +DA:124,0 +DA:137,0 +DA:138,0 +DA:140,0 +DA:142,0 +DA:143,0 +DA:144,0 +DA:148,0 +DA:149,0 +DA:158,0 +DA:170,0 +DA:171,0 +DA:172,0 +DA:177,0 +DA:179,0 +DA:193,0 +DA:194,0 +DA:196,0 +DA:197,0 +DA:198,0 +DA:200,0 +DA:201,0 +DA:203,0 +DA:204,0 +DA:205,0 +DA:206,0 +DA:209,0 +DA:212,0 +DA:219,0 +DA:220,0 +DA:224,0 +DA:225,0 +DA:227,0 +DA:228,0 +DA:240,0 +DA:241,0 +DA:242,0 +DA:248,0 +DA:249,0 +DA:254,0 +DA:255,0 +DA:256,0 +DA:257,0 +DA:258,0 +DA:259,0 +DA:262,0 +DA:263,0 +DA:264,0 +DA:265,0 +DA:266,0 +DA:267,0 +DA:271,0 +DA:272,0 +DA:274,0 +DA:275,0 +DA:276,0 +DA:277,0 +DA:290,0 +DA:296,0 +DA:297,0 +DA:314,0 +DA:315,0 +DA:316,0 +DA:318,0 +DA:319,0 +DA:320,0 +DA:329,0 +DA:331,0 +DA:332,0 +DA:333,0 +DA:335,0 +DA:337,0 +DA:339,0 +DA:340,0 +DA:341,0 +DA:343,0 +DA:344,0 +DA:345,0 +DA:358,0 +DA:360,0 +DA:365,0 +DA:366,0 +DA:371,0 +DA:372,0 +DA:373,0 +DA:374,0 +DA:375,0 +DA:379,0 +DA:380,0 +DA:382,0 +DA:385,0 +DA:387,0 +DA:388,0 +DA:390,0 +DA:395,0 +DA:396,0 +DA:405,0 +DA:406,0 +DA:409,0 +DA:410,0 +DA:411,0 +DA:422,0 +DA:423,0 +DA:424,0 +DA:437,0 +DA:438,0 +DA:439,0 +DA:446,0 +DA:447,0 +DA:448,0 +DA:450,0 +DA:451,0 +DA:452,0 +DA:453,0 +DA:454,0 +DA:455,0 +DA:457,0 +DA:467,0 +DA:469,0 +LF:178 +LH:0 +BRDA:41,0,0,0 +BRDA:41,0,1,0 +BRDA:42,1,0,0 +BRDA:42,1,1,0 +BRDA:50,2,0,0 +BRDA:50,2,1,0 +BRDA:51,3,0,0 +BRDA:51,3,1,0 +BRDA:52,4,0,0 +BRDA:52,4,1,0 +BRDA:53,5,0,0 +BRDA:53,5,1,0 +BRDA:56,6,0,0 +BRDA:56,6,1,0 +BRDA:56,6,2,0 +BRDA:56,7,0,0 +BRDA:56,7,1,0 +BRDA:56,8,0,0 +BRDA:56,8,1,0 +BRDA:57,9,0,0 +BRDA:57,9,1,0 +BRDA:60,10,0,0 +BRDA:60,10,1,0 +BRDA:60,11,0,0 +BRDA:60,11,1,0 +BRDA:61,12,0,0 +BRDA:61,12,1,0 +BRDA:62,13,0,0 +BRDA:62,13,1,0 +BRDA:65,14,0,0 +BRDA:65,14,1,0 +BRDA:65,15,0,0 +BRDA:65,15,1,0 +BRDA:66,16,0,0 +BRDA:66,16,1,0 +BRDA:73,17,0,0 +BRDA:73,17,1,0 +BRDA:73,18,0,0 +BRDA:73,18,1,0 +BRDA:73,18,2,0 +BRDA:78,19,0,0 +BRDA:78,19,1,0 +BRDA:82,20,0,0 +BRDA:82,20,1,0 +BRDA:82,21,0,0 +BRDA:82,21,1,0 +BRDA:89,22,0,0 +BRDA:89,22,1,0 +BRDA:89,23,0,0 +BRDA:89,23,1,0 +BRDA:89,24,0,0 +BRDA:89,24,1,0 +BRDA:90,25,0,0 +BRDA:90,25,1,0 +BRDA:90,25,2,0 +BRDA:93,26,0,0 +BRDA:93,26,1,0 +BRDA:93,27,0,0 +BRDA:93,27,1,0 +BRDA:94,28,0,0 +BRDA:94,28,1,0 +BRDA:96,29,0,0 +BRDA:96,29,1,0 +BRDA:97,30,0,0 +BRDA:97,30,1,0 +BRDA:102,31,0,0 +BRDA:102,31,1,0 +BRDA:104,32,0,0 +BRDA:104,32,1,0 +BRDA:104,32,2,0 +BRDA:106,33,0,0 +BRDA:106,33,1,0 +BRDA:107,34,0,0 +BRDA:107,34,1,0 +BRDA:113,35,0,0 +BRDA:113,35,1,0 +BRDA:113,36,0,0 +BRDA:113,36,1,0 +BRDA:114,37,0,0 +BRDA:114,37,1,0 +BRDA:116,38,0,0 +BRDA:116,38,1,0 +BRDA:117,39,0,0 +BRDA:117,39,1,0 +BRDA:177,40,0,0 +BRDA:177,40,1,0 +BRDA:194,41,0,0 +BRDA:194,41,1,0 +BRDA:196,42,0,0 +BRDA:196,42,1,0 +BRDA:198,43,0,0 +BRDA:198,43,1,0 +BRDA:200,44,0,0 +BRDA:200,44,1,0 +BRDA:201,45,0,0 +BRDA:201,45,1,0 +BRDA:203,46,0,0 +BRDA:203,46,1,0 +BRDA:204,47,0,0 +BRDA:204,47,1,0 +BRDA:225,48,0,0 +BRDA:225,48,1,0 +BRDA:228,49,0,0 +BRDA:228,49,1,0 +BRDA:248,50,0,0 +BRDA:248,50,1,0 +BRDA:259,51,0,0 +BRDA:259,51,1,0 +BRDA:264,52,0,0 +BRDA:264,52,1,0 +BRDA:272,53,0,0 +BRDA:272,53,1,0 +BRDA:272,53,2,0 +BRDA:274,54,0,0 +BRDA:274,54,1,0 +BRDA:296,55,0,0 +BRDA:296,55,1,0 +BRDA:303,56,0,0 +BRDA:303,56,1,0 +BRDA:308,57,0,0 +BRDA:308,57,1,0 +BRDA:318,58,0,0 +BRDA:318,58,1,0 +BRDA:329,59,0,0 +BRDA:329,59,1,0 +BRDA:343,60,0,0 +BRDA:343,60,1,0 +BRDA:358,61,0,0 +BRDA:358,61,1,0 +BRDA:365,62,0,0 +BRDA:365,62,1,0 +BRDA:371,63,0,0 +BRDA:371,63,1,0 +BRDA:374,64,0,0 +BRDA:374,64,1,0 +BRDA:379,65,0,0 +BRDA:379,65,1,0 +BRDA:387,66,0,0 +BRDA:387,66,1,0 +BRDA:395,67,0,0 +BRDA:395,67,1,0 +BRDA:409,68,0,0 +BRDA:409,68,1,0 +BRDA:409,69,0,0 +BRDA:409,69,1,0 +BRDA:409,69,2,0 +BRDA:415,70,0,0 +BRDA:415,70,1,0 +BRDA:422,71,0,0 +BRDA:422,71,1,0 +BRDA:423,72,0,0 +BRDA:423,72,1,0 +BRDA:428,73,0,0 +BRDA:428,73,1,0 +BRDA:430,74,0,0 +BRDA:430,74,1,0 +BRDA:431,75,0,0 +BRDA:431,75,1,0 +BRDA:432,76,0,0 +BRDA:432,76,1,0 +BRDA:437,77,0,0 +BRDA:437,77,1,0 +BRDA:447,78,0,0 +BRDA:447,78,1,0 +BRDA:448,79,0,0 +BRDA:448,79,1,0 +BRDA:448,79,2,0 +BRDA:452,80,0,0 +BRDA:452,80,1,0 +BRDA:453,81,0,0 +BRDA:453,81,1,0 +BRDA:454,82,0,0 +BRDA:454,82,1,0 +BRDA:457,83,0,0 +BRDA:457,83,1,0 +BRDA:460,84,0,0 +BRDA:460,84,1,0 +BRDA:461,85,0,0 +BRDA:461,85,1,0 +BRDA:462,86,0,0 +BRDA:462,86,1,0 +BRDA:463,87,0,0 +BRDA:463,87,1,0 +BRDA:473,88,0,0 +BRDA:473,88,1,0 +BRDA:474,89,0,0 +BRDA:474,89,1,0 +BRDA:475,90,0,0 +BRDA:475,90,1,0 +BRDA:476,91,0,0 +BRDA:476,91,1,0 +BRDA:478,92,0,0 +BRDA:478,92,1,0 +BRDA:480,93,0,0 +BRDA:480,93,1,0 +BRDA:482,94,0,0 +BRDA:482,94,1,0 +BRDA:484,95,0,0 +BRDA:484,95,1,0 +BRDA:486,96,0,0 +BRDA:486,96,1,0 +BRF:201 +BRH:0 +end_of_record +TN: +SF:src/server/index.ts +FN:7,createServerAdapter +FNF:1 +FNH:0 +FNDA:0,createServerAdapter +DA:8,0 +LF:1 +LH:0 +BRF:0 +BRH:0 +end_of_record +TN: +SF:src/server/job-manifest.ts +FN:14,joinPromptSections +FN:15,(anonymous_1) +FN:18,stringifyPaperclipWakePayload +FN:28,renderPaperclipWakePrompt +FN:61,sanitizeForK8sName +FN:65,buildEnvVars +FN:79,(anonymous_6) +FN:108,(anonymous_7) +FN:152,(anonymous_8) +FN:160,buildJobManifest +FN:328,(anonymous_10) +FNF:11 +FNH:10 +FNDA:64,joinPromptSections +FNDA:256,(anonymous_1) +FNDA:64,stringifyPaperclipWakePayload +FNDA:64,renderPaperclipWakePrompt +FNDA:128,sanitizeForK8sName +FNDA:64,buildEnvVars +FNDA:960,(anonymous_6) +FNDA:0,(anonymous_7) +FNDA:332,(anonymous_8) +FNDA:64,buildJobManifest +FNDA:396,(anonymous_10) +DA:15,256 +DA:19,64 +DA:20,0 +DA:21,0 +DA:22,0 +DA:24,0 +DA:29,64 +DA:30,0 +DA:31,0 +DA:32,64 +DA:33,64 +DA:34,0 +DA:35,0 +DA:36,0 +DA:37,0 +DA:38,0 +DA:39,0 +DA:40,0 +DA:43,0 +DA:62,128 +DA:70,64 +DA:71,64 +DA:74,64 +DA:77,64 +DA:79,64 +DA:80,960 +DA:81,7 +DA:85,64 +DA:86,64 +DA:87,64 +DA:88,64 +DA:89,64 +DA:91,64 +DA:92,64 +DA:93,0 +DA:96,64 +DA:97,64 +DA:98,64 +DA:99,64 +DA:100,64 +DA:101,64 +DA:102,64 +DA:103,64 +DA:104,64 +DA:105,64 +DA:107,64 +DA:108,0 +DA:110,64 +DA:111,0 +DA:114,64 +DA:115,0 +DA:117,64 +DA:118,0 +DA:120,64 +DA:121,0 +DA:123,64 +DA:126,64 +DA:127,2 +DA:133,64 +DA:134,1 +DA:138,64 +DA:144,64 +DA:145,1 +DA:149,64 +DA:152,332 +DA:157,64 +DA:161,64 +DA:162,64 +DA:163,64 +DA:166,64 +DA:167,64 +DA:168,64 +DA:169,64 +DA:170,64 +DA:172,64 +DA:173,64 +DA:174,64 +DA:175,64 +DA:176,64 +DA:177,64 +DA:178,64 +DA:179,64 +DA:182,64 +DA:183,64 +DA:184,64 +DA:185,64 +DA:187,64 +DA:188,64 +DA:189,64 +DA:192,64 +DA:196,64 +DA:197,64 +DA:198,64 +DA:199,64 +DA:209,64 +DA:212,64 +DA:213,64 +DA:214,64 +DA:215,64 +DA:216,64 +DA:222,64 +DA:231,64 +DA:232,64 +DA:233,64 +DA:234,64 +DA:235,64 +DA:236,64 +DA:237,64 +DA:238,64 +DA:239,64 +DA:242,64 +DA:245,64 +DA:246,64 +DA:247,64 +DA:259,64 +DA:267,64 +DA:268,3 +DA:272,64 +DA:278,64 +DA:286,64 +DA:287,63 +DA:291,63 +DA:298,64 +DA:299,1 +DA:303,1 +DA:311,64 +DA:319,64 +DA:328,396 +DA:329,64 +DA:331,64 +DA:391,64 +LF:131 +LH:111 +BRDA:14,0,0,64 +BRDA:19,1,0,64 +BRDA:19,1,1,0 +BRDA:19,2,0,64 +BRDA:19,2,1,0 +BRDA:22,3,0,0 +BRDA:22,3,1,0 +BRDA:29,4,0,64 +BRDA:29,4,1,0 +BRDA:29,5,0,64 +BRDA:29,5,1,0 +BRDA:31,6,0,0 +BRDA:31,6,1,0 +BRDA:32,7,0,0 +BRDA:32,7,1,0 +BRDA:33,8,0,0 +BRDA:33,8,1,64 +BRDA:33,9,0,64 +BRDA:33,9,1,0 +BRDA:35,10,0,0 +BRDA:35,10,1,0 +BRDA:37,11,0,0 +BRDA:37,11,1,0 +BRDA:37,12,0,0 +BRDA:37,12,1,0 +BRDA:39,13,0,0 +BRDA:39,13,1,0 +BRDA:40,14,0,0 +BRDA:40,14,1,0 +BRDA:80,15,0,7 +BRDA:80,15,1,953 +BRDA:80,16,0,960 +BRDA:80,16,1,7 +BRDA:85,17,0,64 +BRDA:85,17,1,64 +BRDA:87,18,0,64 +BRDA:87,18,1,64 +BRDA:92,19,0,0 +BRDA:92,19,1,64 +BRDA:107,20,0,0 +BRDA:107,20,1,64 +BRDA:108,21,0,0 +BRDA:108,21,1,0 +BRDA:110,22,0,0 +BRDA:110,22,1,64 +BRDA:114,23,0,0 +BRDA:114,23,1,64 +BRDA:114,24,0,64 +BRDA:114,24,1,0 +BRDA:117,25,0,0 +BRDA:117,25,1,64 +BRDA:117,26,0,64 +BRDA:117,26,1,0 +BRDA:120,27,0,0 +BRDA:120,27,1,64 +BRDA:120,28,0,64 +BRDA:120,28,1,0 +BRDA:126,29,0,2 +BRDA:126,29,1,62 +BRDA:133,30,0,1 +BRDA:133,30,1,63 +BRDA:145,31,0,1 +BRDA:145,31,1,0 +BRDA:166,32,0,64 +BRDA:166,32,1,63 +BRDA:167,33,0,64 +BRDA:167,33,1,63 +BRDA:178,34,0,1 +BRDA:178,34,1,63 +BRDA:185,35,0,64 +BRDA:185,35,1,61 +BRDA:185,35,2,61 +BRDA:198,36,0,64 +BRDA:198,36,1,63 +BRDA:209,37,0,0 +BRDA:209,37,1,64 +BRDA:209,38,0,64 +BRDA:209,38,1,63 +BRDA:213,39,0,64 +BRDA:213,39,1,1 +BRDA:214,40,0,0 +BRDA:214,40,1,64 +BRDA:233,41,0,1 +BRDA:233,41,1,63 +BRDA:234,42,0,64 +BRDA:234,42,1,0 +BRDA:235,43,0,1 +BRDA:235,43,1,63 +BRDA:236,44,0,1 +BRDA:236,44,1,63 +BRDA:237,45,0,1 +BRDA:237,45,1,63 +BRDA:238,46,0,1 +BRDA:238,46,1,63 +BRDA:239,47,0,1 +BRDA:239,47,1,63 +BRDA:268,48,0,3 +BRDA:268,48,1,0 +BRDA:286,49,0,63 +BRDA:286,49,1,1 +BRDA:345,50,0,1 +BRDA:345,50,1,63 +BRDA:351,51,0,64 +BRDA:351,51,1,63 +BRDA:353,52,0,63 +BRDA:353,52,1,1 +BRDA:354,53,0,1 +BRDA:354,53,1,63 +BRDA:355,54,0,1 +BRDA:355,54,1,63 +BRDA:356,55,0,1 +BRDA:356,55,1,63 +BRF:112 +BRH:74 +end_of_record +TN: +SF:src/server/k8s-client.ts +FN:45,getKubeConfig +FN:60,getBatchApi +FN:64,getCoreApi +FN:68,getAuthzApi +FN:72,getLogApi +FN:83,readInClusterNamespace +FN:98,getSelfPodInfo +FN:122,(anonymous_7) +FN:126,(anonymous_8) +FN:133,(anonymous_9) +FN:156,(anonymous_10) +FN:158,(anonymous_11) +FN:169,resetCache +FNF:13 +FNH:0 +FNDA:0,getKubeConfig +FNDA:0,getBatchApi +FNDA:0,getCoreApi +FNDA:0,getAuthzApi +FNDA:0,getLogApi +FNDA:0,readInClusterNamespace +FNDA:0,getSelfPodInfo +FNDA:0,(anonymous_7) +FNDA:0,(anonymous_8) +FNDA:0,(anonymous_9) +FNDA:0,(anonymous_10) +FNDA:0,(anonymous_11) +FNDA:0,resetCache +DA:28,0 +DA:37,0 +DA:43,0 +DA:46,0 +DA:47,0 +DA:48,0 +DA:49,0 +DA:50,0 +DA:51,0 +DA:53,0 +DA:55,0 +DA:57,0 +DA:61,0 +DA:65,0 +DA:69,0 +DA:73,0 +DA:84,0 +DA:85,0 +DA:86,0 +DA:87,0 +DA:89,0 +DA:99,0 +DA:101,0 +DA:102,0 +DA:103,0 +DA:106,0 +DA:107,0 +DA:108,0 +DA:110,0 +DA:111,0 +DA:112,0 +DA:115,0 +DA:116,0 +DA:117,0 +DA:121,0 +DA:122,0 +DA:123,0 +DA:125,0 +DA:126,0 +DA:127,0 +DA:131,0 +DA:132,0 +DA:133,0 +DA:134,0 +DA:135,0 +DA:145,0 +DA:146,0 +DA:147,0 +DA:148,0 +DA:149,0 +DA:153,0 +DA:156,0 +DA:158,0 +DA:165,0 +DA:170,0 +DA:171,0 +LF:56 +LH:0 +BRDA:46,0,0,0 +BRDA:46,0,1,0 +BRDA:48,1,0,0 +BRDA:48,1,1,0 +BRDA:50,2,0,0 +BRDA:50,2,1,0 +BRDA:84,3,0,0 +BRDA:84,3,1,0 +BRDA:85,4,0,0 +BRDA:85,4,1,0 +BRDA:99,5,0,0 +BRDA:99,5,1,0 +BRDA:102,6,0,0 +BRDA:102,6,1,0 +BRDA:111,7,0,0 +BRDA:111,7,1,0 +BRDA:116,8,0,0 +BRDA:116,8,1,0 +BRDA:125,9,0,0 +BRDA:125,9,1,0 +BRDA:127,10,0,0 +BRDA:127,10,1,0 +BRDA:132,11,0,0 +BRDA:132,11,1,0 +BRDA:134,12,0,0 +BRDA:134,12,1,0 +BRDA:148,13,0,0 +BRDA:148,13,1,0 +BRDA:156,14,0,0 +BRDA:156,14,1,0 +BRDA:157,15,0,0 +BRDA:157,15,1,0 +BRF:32 +BRH:0 +end_of_record +TN: +SF:src/server/parse.ts +FN:7,parseClaudeStreamJson +FN:78,extractClaudeErrorMessages +FN:110,extractClaudeLoginUrl +FN:122,detectClaudeLoginRequired +FN:131,(anonymous_4) +FN:134,(anonymous_5) +FN:141,describeClaudeFailure +FN:157,isClaudeMaxTurnsResult +FN:170,isClaudeUnknownSessionError +FN:173,(anonymous_9) +FN:176,(anonymous_10) +FNF:11 +FNH:11 +FNDA:10,parseClaudeStreamJson +FNDA:15,extractClaudeErrorMessages +FNDA:11,extractClaudeLoginUrl +FNDA:6,detectClaudeLoginRequired +FNDA:20,(anonymous_4) +FNDA:6,(anonymous_5) +FNDA:4,describeClaudeFailure +FNDA:9,isClaudeMaxTurnsResult +FNDA:5,isClaudeUnknownSessionError +FNDA:6,(anonymous_9) +FNDA:5,(anonymous_10) +DA:4,1 +DA:5,1 +DA:8,10 +DA:9,10 +DA:10,10 +DA:11,10 +DA:13,10 +DA:14,15 +DA:15,15 +DA:16,13 +DA:17,13 +DA:19,9 +DA:20,9 +DA:21,1 +DA:22,1 +DA:23,1 +DA:26,8 +DA:27,6 +DA:28,6 +DA:29,6 +DA:30,6 +DA:31,6 +DA:32,6 +DA:33,6 +DA:34,4 +DA:35,4 +DA:38,6 +DA:41,2 +DA:42,2 +DA:43,2 +DA:47,10 +DA:48,8 +DA:58,2 +DA:59,2 +DA:64,2 +DA:65,2 +DA:66,10 +DA:68,10 +DA:79,15 +DA:80,15 +DA:82,15 +DA:83,4 +DA:84,4 +DA:85,4 +DA:86,4 +DA:89,0 +DA:90,0 +DA:93,0 +DA:94,0 +DA:95,4 +DA:96,0 +DA:97,0 +DA:100,0 +DA:101,0 +DA:107,15 +DA:111,11 +DA:112,11 +DA:113,7 +DA:114,8 +DA:115,8 +DA:116,5 +DA:119,2 +DA:127,6 +DA:128,6 +DA:131,20 +DA:134,6 +DA:135,6 +DA:142,4 +DA:143,4 +DA:144,4 +DA:146,4 +DA:147,4 +DA:148,1 +DA:151,4 +DA:152,4 +DA:153,4 +DA:154,4 +DA:158,9 +DA:160,7 +DA:161,7 +DA:163,5 +DA:164,5 +DA:166,4 +DA:167,4 +DA:171,5 +DA:172,5 +DA:173,6 +DA:176,5 +DA:177,5 +LF:89 +LH:81 +BRDA:15,0,0,2 +BRDA:15,0,1,13 +BRDA:17,1,0,4 +BRDA:17,1,1,9 +BRDA:20,2,0,1 +BRDA:20,2,1,8 +BRDA:20,3,0,9 +BRDA:20,3,1,1 +BRDA:21,4,0,1 +BRDA:21,4,1,0 +BRDA:21,5,0,1 +BRDA:21,5,1,1 +BRDA:26,6,0,6 +BRDA:26,6,1,2 +BRDA:27,7,0,6 +BRDA:27,7,1,4 +BRDA:27,8,0,6 +BRDA:27,8,1,5 +BRDA:29,9,0,6 +BRDA:29,9,1,0 +BRDA:31,10,0,0 +BRDA:31,10,1,6 +BRDA:31,11,0,6 +BRDA:31,11,1,6 +BRDA:31,11,2,6 +BRDA:33,12,0,4 +BRDA:33,12,1,2 +BRDA:35,13,0,4 +BRDA:35,13,1,0 +BRDA:41,14,0,2 +BRDA:41,14,1,0 +BRDA:43,15,0,2 +BRDA:43,15,1,1 +BRDA:43,16,0,2 +BRDA:43,16,1,2 +BRDA:47,17,0,8 +BRDA:47,17,1,2 +BRDA:65,18,0,1 +BRDA:65,18,1,1 +BRDA:65,19,0,2 +BRDA:65,19,1,1 +BRDA:79,20,0,3 +BRDA:79,20,1,12 +BRDA:83,21,0,4 +BRDA:83,21,1,0 +BRDA:85,22,0,4 +BRDA:85,22,1,0 +BRDA:89,23,0,0 +BRDA:89,23,1,0 +BRDA:89,24,0,0 +BRDA:89,24,1,0 +BRDA:89,24,2,0 +BRDA:94,25,0,0 +BRDA:94,25,1,0 +BRDA:94,25,2,0 +BRDA:95,26,0,0 +BRDA:95,26,1,4 +BRDA:112,27,0,4 +BRDA:112,27,1,7 +BRDA:112,28,0,11 +BRDA:112,28,1,7 +BRDA:115,29,0,5 +BRDA:115,29,1,3 +BRDA:115,30,0,8 +BRDA:115,30,1,8 +BRDA:115,30,2,3 +BRDA:119,31,0,2 +BRDA:119,31,1,0 +BRDA:128,32,0,6 +BRDA:128,32,1,0 +BRDA:147,33,0,1 +BRDA:147,33,1,3 +BRDA:147,34,0,4 +BRDA:147,34,1,3 +BRDA:148,35,0,1 +BRDA:148,35,1,0 +BRDA:152,36,0,1 +BRDA:152,36,1,3 +BRDA:153,37,0,2 +BRDA:153,37,1,2 +BRDA:154,38,0,2 +BRDA:154,38,1,2 +BRDA:158,39,0,2 +BRDA:158,39,1,7 +BRDA:161,40,0,2 +BRDA:161,40,1,5 +BRDA:164,41,0,1 +BRDA:164,41,1,4 +BRF:88 +BRH:69 +end_of_record +TN: +SF:src/server/session.ts +FN:3,readNonEmptyString +FN:8,(anonymous_1) +FN:28,(anonymous_2) +FN:47,(anonymous_3) +FNF:4 +FNH:4 +FNDA:203,readNonEmptyString +FNDA:25,(anonymous_1) +FNDA:8,(anonymous_2) +FNDA:7,(anonymous_3) +DA:4,203 +DA:7,1 +DA:9,25 +DA:10,20 +DA:11,20 +DA:12,25 +DA:14,16 +DA:17,25 +DA:18,25 +DA:19,25 +DA:20,25 +DA:29,8 +DA:30,6 +DA:31,8 +DA:33,4 +DA:36,8 +DA:37,8 +DA:38,8 +DA:39,8 +DA:48,7 +DA:49,5 +LF:21 +LH:21 +BRDA:4,0,0,42 +BRDA:4,0,1,161 +BRDA:4,1,0,203 +BRDA:4,1,1,44 +BRDA:9,2,0,5 +BRDA:9,2,1,20 +BRDA:9,3,0,25 +BRDA:9,3,1,22 +BRDA:9,3,2,21 +BRDA:11,4,0,20 +BRDA:11,4,1,5 +BRDA:12,5,0,4 +BRDA:12,5,1,21 +BRDA:14,6,0,16 +BRDA:14,6,1,13 +BRDA:14,6,2,12 +BRDA:17,7,0,25 +BRDA:17,7,1,14 +BRDA:18,8,0,25 +BRDA:18,8,1,14 +BRDA:19,9,0,25 +BRDA:19,9,1,14 +BRDA:22,10,0,5 +BRDA:22,10,1,11 +BRDA:23,11,0,3 +BRDA:23,11,1,13 +BRDA:24,12,0,3 +BRDA:24,12,1,13 +BRDA:25,13,0,3 +BRDA:25,13,1,13 +BRDA:29,14,0,2 +BRDA:29,14,1,6 +BRDA:30,15,0,6 +BRDA:30,15,1,3 +BRDA:31,16,0,2 +BRDA:31,16,1,6 +BRDA:33,17,0,4 +BRDA:33,17,1,3 +BRDA:33,17,2,3 +BRDA:36,18,0,8 +BRDA:36,18,1,3 +BRDA:37,19,0,8 +BRDA:37,19,1,3 +BRDA:38,20,0,8 +BRDA:38,20,1,3 +BRDA:41,21,0,1 +BRDA:41,21,1,3 +BRDA:42,22,0,1 +BRDA:42,22,1,3 +BRDA:43,23,0,1 +BRDA:43,23,1,3 +BRDA:44,24,0,1 +BRDA:44,24,1,3 +BRDA:48,25,0,2 +BRDA:48,25,1,5 +BRDA:49,26,0,5 +BRDA:49,26,1,2 +BRF:57 +BRH:57 +end_of_record +TN: +SF:src/server/test.ts +FN:9,summarizeStatus +FN:10,(anonymous_1) +FN:11,(anonymous_2) +FN:15,checkApiReachable +FN:37,checkNamespace +FN:77,checkRbac +FN:134,checkSecret +FN:158,checkPvc +FN:205,testEnvironment +FNF:9 +FNH:0 +FNDA:0,summarizeStatus +FNDA:0,(anonymous_1) +FNDA:0,(anonymous_2) +FNDA:0,checkApiReachable +FNDA:0,checkNamespace +FNDA:0,checkRbac +FNDA:0,checkSecret +FNDA:0,checkPvc +FNDA:0,testEnvironment +DA:10,0 +DA:11,0 +DA:12,0 +DA:16,0 +DA:17,0 +DA:18,0 +DA:24,0 +DA:26,0 +DA:27,0 +DA:33,0 +DA:46,0 +DA:47,0 +DA:52,0 +DA:55,0 +DA:56,0 +DA:57,0 +DA:58,0 +DA:63,0 +DA:65,0 +DA:66,0 +DA:73,0 +DA:82,0 +DA:84,0 +DA:92,0 +DA:93,0 +DA:94,0 +DA:108,0 +DA:109,0 +DA:115,0 +DA:123,0 +DA:124,0 +DA:140,0 +DA:141,0 +DA:142,0 +DA:143,0 +DA:149,0 +DA:163,0 +DA:164,0 +DA:170,0 +DA:173,0 +DA:174,0 +DA:175,0 +DA:179,0 +DA:180,0 +DA:181,0 +DA:182,0 +DA:188,0 +DA:196,0 +DA:197,0 +DA:208,0 +DA:209,0 +DA:210,0 +DA:211,0 +DA:214,0 +DA:215,0 +DA:216,0 +DA:219,0 +DA:220,0 +DA:223,0 +DA:224,0 +DA:225,0 +DA:229,0 +DA:235,0 +LF:63 +LH:0 +BRDA:10,0,0,0 +BRDA:10,0,1,0 +BRDA:11,1,0,0 +BRDA:11,1,1,0 +BRDA:26,2,0,0 +BRDA:26,2,1,0 +BRDA:46,3,0,0 +BRDA:46,3,1,0 +BRDA:65,4,0,0 +BRDA:65,4,1,0 +BRDA:108,5,0,0 +BRDA:108,5,1,0 +BRDA:123,6,0,0 +BRDA:123,6,1,0 +BRDA:163,7,0,0 +BRDA:163,7,1,0 +BRDA:179,8,0,0 +BRDA:179,8,1,0 +BRDA:181,9,0,0 +BRDA:181,9,1,0 +BRDA:196,10,0,0 +BRDA:196,10,1,0 +BRDA:211,11,0,0 +BRDA:211,11,1,0 +BRDA:215,12,0,0 +BRDA:215,12,1,0 +BRDA:220,13,0,0 +BRDA:220,13,1,0 +BRDA:224,14,0,0 +BRDA:224,14,1,0 +BRF:30 +BRH:0 +end_of_record diff --git a/package-lock.json b/package-lock.json index 50cb8ba..d7e15a0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@farhoodliquor/paperclip-adapter-claude-k8s", - "version": "0.1.12", + "version": "0.1.14", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@farhoodliquor/paperclip-adapter-claude-k8s", - "version": "0.1.12", + "version": "0.1.14", "license": "MIT", "dependencies": { "@kubernetes/client-node": "^1.0.0", diff --git a/package.json b/package.json index 0424013..75fbb3f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@farhoodliquor/paperclip-adapter-claude-k8s", - "version": "0.1.13", + "version": "0.1.14", "description": "Paperclip adapter plugin that runs Claude Code agents as Kubernetes Jobs", "license": "MIT", "repository": {