hankweave 0.3.3 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +76 -16
- package/dist/index.js +272 -266
- package/dist/index.js.map +32 -32
- package/dist/shims/codex/index.js +99 -71
- package/dist/shims/gemini/index.js +6 -5
- package/package.json +4 -7
- package/schemas/hank.schema.json +18 -0
- package/schemas/hankweave.schema.json +6 -0
- package/shims/codex/index.js +99 -71
- package/shims/gemini/index.js +6 -5
|
@@ -1,5 +1,64 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
+
// ../common/src/args.ts
|
|
4
|
+
function parseArgs(argv, aliases) {
|
|
5
|
+
const args = {
|
|
6
|
+
model: "",
|
|
7
|
+
verbose: false,
|
|
8
|
+
idleTimeout: 120,
|
|
9
|
+
selfTest: false,
|
|
10
|
+
version: false,
|
|
11
|
+
help: false
|
|
12
|
+
};
|
|
13
|
+
for (let i = 0; i < argv.length; i++) {
|
|
14
|
+
let arg = argv[i];
|
|
15
|
+
if (arg.includes("=")) {
|
|
16
|
+
const [key, value] = arg.split("=", 2);
|
|
17
|
+
argv.splice(i, 1, key, value);
|
|
18
|
+
arg = key;
|
|
19
|
+
}
|
|
20
|
+
if (aliases && arg in aliases) {
|
|
21
|
+
arg = aliases[arg];
|
|
22
|
+
}
|
|
23
|
+
switch (arg) {
|
|
24
|
+
case "--model":
|
|
25
|
+
args.model = argv[++i];
|
|
26
|
+
break;
|
|
27
|
+
case "--resume":
|
|
28
|
+
args.resume = argv[++i];
|
|
29
|
+
break;
|
|
30
|
+
case "--verbose":
|
|
31
|
+
args.verbose = true;
|
|
32
|
+
break;
|
|
33
|
+
case "--append-system-prompt":
|
|
34
|
+
args.appendSystemPrompt = argv[++i];
|
|
35
|
+
break;
|
|
36
|
+
case "--debug-dir":
|
|
37
|
+
args.debugDir = argv[++i];
|
|
38
|
+
break;
|
|
39
|
+
case "--idle-timeout": {
|
|
40
|
+
const val = Number(argv[++i]);
|
|
41
|
+
if (!Number.isFinite(val) || val <= 0) {
|
|
42
|
+
console.error("Invalid --idle-timeout value: must be a positive number");
|
|
43
|
+
process.exit(1);
|
|
44
|
+
}
|
|
45
|
+
args.idleTimeout = val;
|
|
46
|
+
break;
|
|
47
|
+
}
|
|
48
|
+
case "--self-test":
|
|
49
|
+
args.selfTest = true;
|
|
50
|
+
break;
|
|
51
|
+
case "--version":
|
|
52
|
+
args.version = true;
|
|
53
|
+
break;
|
|
54
|
+
case "--help":
|
|
55
|
+
args.help = true;
|
|
56
|
+
break;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return args;
|
|
60
|
+
}
|
|
61
|
+
|
|
3
62
|
// src/selftest.ts
|
|
4
63
|
import { spawn } from "child_process";
|
|
5
64
|
import { existsSync as existsSync2 } from "fs";
|
|
@@ -666,6 +725,40 @@ var SessionManager = class {
|
|
|
666
725
|
}
|
|
667
726
|
};
|
|
668
727
|
|
|
728
|
+
// ../common/src/timeout.ts
|
|
729
|
+
var IdleTimeoutError = class extends Error {
|
|
730
|
+
timeoutMs;
|
|
731
|
+
constructor(timeoutMs) {
|
|
732
|
+
super(`Idle timeout: no events received for ${timeoutMs}ms`);
|
|
733
|
+
this.name = "IdleTimeoutError";
|
|
734
|
+
this.timeoutMs = timeoutMs;
|
|
735
|
+
}
|
|
736
|
+
};
|
|
737
|
+
async function* withIdleTimeout(events, timeoutMs) {
|
|
738
|
+
const iterator = events[Symbol.asyncIterator]();
|
|
739
|
+
try {
|
|
740
|
+
while (true) {
|
|
741
|
+
let timeoutId;
|
|
742
|
+
try {
|
|
743
|
+
const result = await Promise.race([
|
|
744
|
+
iterator.next(),
|
|
745
|
+
new Promise((_, reject) => {
|
|
746
|
+
timeoutId = setTimeout(() => {
|
|
747
|
+
reject(new IdleTimeoutError(timeoutMs));
|
|
748
|
+
}, timeoutMs);
|
|
749
|
+
})
|
|
750
|
+
]);
|
|
751
|
+
if (result.done) break;
|
|
752
|
+
yield result.value;
|
|
753
|
+
} finally {
|
|
754
|
+
clearTimeout(timeoutId);
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
} finally {
|
|
758
|
+
void iterator.return?.();
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
|
|
669
762
|
// src/utils/ids.ts
|
|
670
763
|
function generateMessageId() {
|
|
671
764
|
return `msg_${Date.now().toString(36)}${Math.random().toString(36).substring(2, 10)}`;
|
|
@@ -721,27 +814,6 @@ function getCodexModelId(spec) {
|
|
|
721
814
|
return spec.modelID;
|
|
722
815
|
}
|
|
723
816
|
|
|
724
|
-
// Model resolution assertions - verify reasoning effort parsing at load time
|
|
725
|
-
{
|
|
726
|
-
const testCases = [
|
|
727
|
-
{ input: "gpt-5.3-codex-high", expectedModelID: "gpt-5.3-codex", expectedEffort: "high" },
|
|
728
|
-
{ input: "gpt-5.3-codex-xhigh", expectedModelID: "gpt-5.3-codex", expectedEffort: "xhigh" },
|
|
729
|
-
{ input: "gpt-5.2-codex-high", expectedModelID: "gpt-5.2-codex", expectedEffort: "high" },
|
|
730
|
-
{ input: "gpt-5.2-xhigh", expectedModelID: "gpt-5.2", expectedEffort: "xhigh" },
|
|
731
|
-
{ input: "gpt-5.3-codex", expectedModelID: "gpt-5.3-codex", expectedEffort: undefined },
|
|
732
|
-
];
|
|
733
|
-
for (const tc of testCases) {
|
|
734
|
-
const result = resolveModel(tc.input);
|
|
735
|
-
if (result.modelID !== tc.expectedModelID || result.reasoningEffort !== tc.expectedEffort) {
|
|
736
|
-
throw new Error(
|
|
737
|
-
`Model resolution assertion failed for "${tc.input}": ` +
|
|
738
|
-
`expected modelID="${tc.expectedModelID}" effort="${tc.expectedEffort}", ` +
|
|
739
|
-
`got modelID="${result.modelID}" effort="${result.reasoningEffort}"`
|
|
740
|
-
);
|
|
741
|
-
}
|
|
742
|
-
}
|
|
743
|
-
}
|
|
744
|
-
|
|
745
817
|
// src/utils/output.ts
|
|
746
818
|
function emit(message) {
|
|
747
819
|
console.log(JSON.stringify(message));
|
|
@@ -975,11 +1047,12 @@ ${this.args.appendSystemPrompt}`;
|
|
|
975
1047
|
state.apiStartTime = Date.now();
|
|
976
1048
|
this.apiStartTime = state.apiStartTime;
|
|
977
1049
|
const { events } = await state.thread.runStreamed(finalPrompt);
|
|
1050
|
+
const timedEvents = withIdleTimeout(events, this.args.idleTimeout * 1e3);
|
|
978
1051
|
const currentMessageContent = [];
|
|
979
1052
|
let currentMessageId = generateMessageId();
|
|
980
1053
|
const pendingToolResults = /* @__PURE__ */ new Map();
|
|
981
1054
|
let systemInitEmitted = false;
|
|
982
|
-
for await (const event of
|
|
1055
|
+
for await (const event of timedEvents) {
|
|
983
1056
|
if (this.interrupted) {
|
|
984
1057
|
verboseLog(this.args.verbose, "Interrupted, stopping event processing");
|
|
985
1058
|
break;
|
|
@@ -1391,55 +1464,6 @@ ${errorStack}
|
|
|
1391
1464
|
};
|
|
1392
1465
|
|
|
1393
1466
|
// src/utils/args.ts
|
|
1394
|
-
function parseArgs(argv) {
|
|
1395
|
-
const args = {
|
|
1396
|
-
model: "",
|
|
1397
|
-
verbose: false,
|
|
1398
|
-
selfTest: false,
|
|
1399
|
-
version: false,
|
|
1400
|
-
help: false
|
|
1401
|
-
};
|
|
1402
|
-
for (let i = 0; i < argv.length; i++) {
|
|
1403
|
-
const arg = argv[i];
|
|
1404
|
-
if (arg.includes("=")) {
|
|
1405
|
-
const [key, value] = arg.split("=", 2);
|
|
1406
|
-
argv.splice(i, 1, key, value);
|
|
1407
|
-
}
|
|
1408
|
-
switch (arg) {
|
|
1409
|
-
case "--model":
|
|
1410
|
-
args.model = argv[++i];
|
|
1411
|
-
break;
|
|
1412
|
-
case "-p":
|
|
1413
|
-
args.prompt = "";
|
|
1414
|
-
break;
|
|
1415
|
-
case "--resume":
|
|
1416
|
-
args.resume = argv[++i];
|
|
1417
|
-
break;
|
|
1418
|
-
case "--verbose":
|
|
1419
|
-
args.verbose = true;
|
|
1420
|
-
break;
|
|
1421
|
-
case "--append-system-prompt":
|
|
1422
|
-
args.appendSystemPrompt = argv[++i];
|
|
1423
|
-
break;
|
|
1424
|
-
case "--debug-dir":
|
|
1425
|
-
args.debugDir = argv[++i];
|
|
1426
|
-
break;
|
|
1427
|
-
case "--self-test":
|
|
1428
|
-
args.selfTest = true;
|
|
1429
|
-
break;
|
|
1430
|
-
case "--version":
|
|
1431
|
-
args.version = true;
|
|
1432
|
-
break;
|
|
1433
|
-
case "--help":
|
|
1434
|
-
args.help = true;
|
|
1435
|
-
break;
|
|
1436
|
-
}
|
|
1437
|
-
}
|
|
1438
|
-
if (!args.model && !args.selfTest && !args.version && !args.help) {
|
|
1439
|
-
args.model = process.env.MODEL || "codex";
|
|
1440
|
-
}
|
|
1441
|
-
return args;
|
|
1442
|
-
}
|
|
1443
1467
|
async function readStdin() {
|
|
1444
1468
|
return new Promise((resolve, reject) => {
|
|
1445
1469
|
let data = "";
|
|
@@ -1475,6 +1499,7 @@ OPTIONS:
|
|
|
1475
1499
|
--resume <session_id> Continue existing session
|
|
1476
1500
|
--verbose Enable verbose logging to stderr
|
|
1477
1501
|
--append-system-prompt Additional system prompt to append
|
|
1502
|
+
--idle-timeout <seconds> Max seconds between agent events before aborting (default: 120)
|
|
1478
1503
|
--debug-dir <path> Directory for debug logs and session data
|
|
1479
1504
|
--self-test Run environment verification
|
|
1480
1505
|
--version Print version and exit
|
|
@@ -1497,6 +1522,9 @@ EXAMPLES:
|
|
|
1497
1522
|
async function main() {
|
|
1498
1523
|
try {
|
|
1499
1524
|
const args = parseArgs(process.argv.slice(2));
|
|
1525
|
+
if (!args.model && !args.selfTest && !args.version && !args.help) {
|
|
1526
|
+
args.model = process.env.MODEL || "codex";
|
|
1527
|
+
}
|
|
1500
1528
|
if (args.version) {
|
|
1501
1529
|
printVersion();
|
|
1502
1530
|
process.exit(0);
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import{readFileSync as
|
|
3
|
-
`)}writeStderr(e){this.options.debugDir&&(this.stderrStream?this.stderrStream.write(e):this.stderrBuffer+=e)}async closeDebugStreams(){let e=[];if(this.stderrBuffer&&!this.stderrStream&&this.options.debugDir)try{let t=
|
|
2
|
+
import{readFileSync as de}from"fs";import{dirname as pe,join as ge}from"path";import{fileURLToPath as fe}from"url";function Y(s,e){let t={model:"",verbose:!1,idleTimeout:120,selfTest:!1,version:!1,help:!1};for(let i=0;i<s.length;i++){let r=s[i];if(r.includes("=")){let[a,l]=r.split("=",2);s.splice(i,1,a,l),r=a}switch(e&&r in e&&(r=e[r]),r){case"--model":t.model=s[++i];break;case"--resume":t.resume=s[++i];break;case"--verbose":t.verbose=!0;break;case"--append-system-prompt":t.appendSystemPrompt=s[++i];break;case"--debug-dir":t.debugDir=s[++i];break;case"--idle-timeout":{let a=Number(s[++i]);(!Number.isFinite(a)||a<=0)&&(console.error("Invalid --idle-timeout value: must be a positive number"),process.exit(1)),t.idleTimeout=a;break}case"--self-test":t.selfTest=!0;break;case"--version":t.version=!0;break;case"--help":t.help=!0;break}}return t}import{spawn as A}from"child_process";import{createWriteStream as O}from"fs";import{mkdir as J}from"fs/promises";import{isAbsolute as G,resolve as h}from"path";import{createInterface as ae}from"readline";var _=class{constructor(e){this.options=e;this.verbose=e.verbose}process=null;verbose;rawEventStream=null;stderrStream=null;stderrBuffer="";currentSessionId="unknown";static async isInstalled(){return new Promise(e=>{let t=process.platform==="win32",r=A(t?"where":"which",["gemini"],{shell:t}),a="";r.stdout?.on("data",l=>{a+=l.toString()}),r.on("close",l=>{if(l===0&&a.trim()){let c=A("gemini",["--version"],{shell:t}),o="";c.stdout?.on("data",m=>{o+=m.toString()}),c.on("close",()=>{e({found:!0,path:a.trim(),version:o.trim()})})}else e({found:!1})})})}async initializeDebugStreams(e){if(this.options.debugDir)try{let t=G(this.options.debugDir)?this.options.debugDir:h(this.options.cwd,this.options.debugDir);await J(t,{recursive:!0});let i=h(t,`session-${e}.raw.jsonl`);this.rawEventStream=O(i,{flags:"a",encoding:"utf8"});let r=h(t,`session-${e}.raw.log`);this.stderrStream=O(r,{flags:"a",encoding:"utf8"}),this.stderrBuffer&&(this.stderrStream.write(this.stderrBuffer),this.stderrBuffer=""),this.currentSessionId=e}catch(t){throw new Error(`Failed to create debug log files in ${this.options.debugDir}: ${t instanceof Error?t.message:String(t)}`)}}async renameDebugStreams(e){if(!(!this.options.debugDir||this.currentSessionId===e))try{let t=G(this.options.debugDir)?this.options.debugDir:h(this.options.cwd,this.options.debugDir);await this.closeDebugStreams();let{rename:i}=await import("fs/promises"),r=h(t,`session-${this.currentSessionId}.raw.jsonl`),a=h(t,`session-${e}.raw.jsonl`),l=h(t,`session-${this.currentSessionId}.raw.log`),c=h(t,`session-${e}.raw.log`);try{await i(r,a)}catch(o){this.verbose&&console.error(`[gemini-cli-shim] Could not rename ${r}:`,o)}try{await i(l,c)}catch(o){this.verbose&&console.error(`[gemini-cli-shim] Could not rename ${l}:`,o)}await this.initializeDebugStreams(e)}catch(t){throw new Error(`Failed to rename debug log files: ${t instanceof Error?t.message:String(t)}`)}}writeRawEvent(e){this.rawEventStream&&this.rawEventStream.write(`${JSON.stringify(e)}
|
|
3
|
+
`)}writeStderr(e){this.options.debugDir&&(this.stderrStream?this.stderrStream.write(e):this.stderrBuffer+=e)}async closeDebugStreams(){let e=[];if(this.stderrBuffer&&!this.stderrStream&&this.options.debugDir)try{let t=G(this.options.debugDir)?this.options.debugDir:h(this.options.cwd,this.options.debugDir);await J(t,{recursive:!0});let i=h(t,`session-${this.currentSessionId}.raw.log`),r=O(i,{flags:"a",encoding:"utf8"});await new Promise((a,l)=>{r.write(this.stderrBuffer,c=>{c?l(c):r.end(o=>{o?l(o):a()})})}),this.stderrBuffer=""}catch(t){console.error("[gemini-cli-shim] Error flushing buffered stderr:",t)}this.rawEventStream&&(e.push(new Promise((t,i)=>{this.rawEventStream?.end(r=>{r?i(r):t()})})),this.rawEventStream=null),this.stderrStream&&(e.push(new Promise((t,i)=>{this.stderrStream?.end(r=>{r?i(r):t()})})),this.stderrStream=null),await Promise.all(e)}async spawn(e){let t=["--model",this.options.model,"--output-format","stream-json","--yolo"];this.options.resume&&t.push("--resume",this.options.resume),this.verbose&&console.error("[gemini-cli-shim] Spawning gemini:",t.join(" "));let i=process.platform==="win32";this.process=A("gemini",t,{cwd:this.options.cwd,stdio:["pipe","pipe","pipe"],env:{...process.env},shell:i}),this.options.debugDir&&await this.initializeDebugStreams("unknown");let r=this.options.appendSystemPrompt?`${e}
|
|
4
4
|
|
|
5
|
-
Additional instructions: ${this.options.appendSystemPrompt}`:e;return this.process.stdin?.write(
|
|
6
|
-
${e.appendSystemPrompt}`:
|
|
5
|
+
Additional instructions: ${this.options.appendSystemPrompt}`:e;return this.process.stdin?.write(r),this.process.stdin?.end(),this.createEventStream()}async*createEventStream(){if(!this.process||!this.process.stdout||!this.process.stderr)throw new Error("Process not spawned");let e="",t=!1,i=!1,r="",a=null;this.process.stderr.on("data",c=>{let o=c.toString();e+=o,this.writeStderr(o),!i&&(o.includes("Error resuming session:")||o.includes("Invalid session identifier"))&&(t=!0,i=!0,this.verbose&&console.error("[gemini-cli-shim] Session error detected, killing process"),this.process&&this.process.kill("SIGKILL")),this.verbose&&console.error("[gemini stderr]",o)});let l=ae({input:this.process.stdout,crlfDelay:1/0});if(t)throw new Error("Invalid session ID");try{for await(let c of l){let o=c.trim();if(t)throw new Error("Invalid session ID");if(o)try{let m=JSON.parse(o);m.type==="init"&&m.session_id&&(r=m.session_id,this.options.debugDir&&this.currentSessionId==="unknown"&&await this.renameDebugStreams(r)),this.writeRawEvent(m),this.verbose&&console.error("[gemini event]",JSON.stringify(m)),yield m}catch(m){this.verbose&&(console.error("[gemini-cli-shim] Failed to parse line:",o),console.error("[gemini-cli-shim] Error:",m))}}}catch(c){throw t?new Error("Invalid session ID"):c}if(t)throw new Error("Invalid session ID");await new Promise((c,o)=>{if(!this.process){t?o(new Error("Invalid session ID")):c();return}let m=setTimeout(()=>{this.verbose&&console.error("[gemini-cli-shim] Process exit timeout, forcing kill"),this.kill(),o(t?new Error("Invalid session ID"):new Error("Process did not exit within timeout"))},2e3);this.process.on("close",async d=>{clearTimeout(m),a=d;try{await this.closeDebugStreams()}catch(I){console.error("[gemini-cli-shim] Error closing debug streams:",I)}t?o(new Error("Invalid session ID")):d===0||d===null?c():o(new Error(`Gemini CLI exited with code ${d}`))}),this.process.on("error",async d=>{clearTimeout(m);try{await this.closeDebugStreams()}catch(I){console.error("[gemini-cli-shim] Error closing debug streams:",I)}o(t?new Error("Invalid session ID"):d)})})}kill(){this.process&&(this.process.kill("SIGTERM"),this.process=null)}};import{readFileSync as me}from"fs";import{resolve as ue}from"path";import{randomUUID as Ge}from"crypto";import Ne from"fs";import Ce from"path";var R=class extends Error{timeoutMs;constructor(e){super(`Idle timeout: no events received for ${e}ms`),this.name="IdleTimeoutError",this.timeoutMs=e}};async function*z(s,e){let t=s[Symbol.asyncIterator]();try{for(;;){let i;try{let r=await Promise.race([t.next(),new Promise((a,l)=>{i=setTimeout(()=>{l(new R(e))},e)})]);if(r.done)break;yield r.value}finally{clearTimeout(i)}}}finally{t.return?.()}}var q=["Read","Write","Edit","Bash","Glob","Grep","LS"];var H={sonnet:"anthropic/claude-sonnet-4-20250514",haiku:"anthropic/claude-3-haiku",opus:"anthropic/claude-opus-4-5-20251101",flash:"google/gemini-2.0-flash",pro:"google/gemini-2.0-pro"};function V(s){if(!s){let e=process.env.MODEL;e?s=e:s="flash"}if(H[s]){let e=H[s];return e.startsWith("google/")?e.replace("google/",""):e}if(s.includes("/")){let[e,t]=s.split("/",2);return e==="google"?t:s}return s}function E(s){return s.startsWith("anthropic/")?s.replace("anthropic/",""):!s.includes("/")&&(s.startsWith("gemini-")||s==="flash"||s==="pro")?`google/${s}`:s}import{randomBytes as Z}from"crypto";function N(){let s=Date.now().toString(36),e=Z(5).toString("hex");return`msg_${s}${e}`}function $(){let s=Date.now().toString(36),e=Z(6).toString("hex");return`toolu_${s}${e}`}var x="00000000-0000-0000-0000-000000000000";var le={read_file:"Read",readFile:"Read",file_read:"Read",write_file:"Write",writeFile:"Write",file_write:"Write",edit_file:"Edit",editFile:"Edit",str_replace_editor:"Edit",run_shell_command:"Bash",bash:"Bash",shell:"Bash",execute_bash:"Bash",list_directory:"LS",ls:"LS",list:"LS",list_dir:"LS",glob:"Glob",find_files:"Glob",grep:"Grep",search_files:"Grep",search:"Grep"};function Q(s){return le[s]||s}function ce(s){return s.replace(/[A-Z]/g,e=>`_${e.toLowerCase()}`)}function X(s){if(!s)return s;let e={};for(let[t,i]of Object.entries(s))e[ce(t)]=i;return e}function k(){return[...q]}function y(s){console.log(JSON.stringify(s))}function C(){return process.env.GOOGLE_API_KEY?"GOOGLE_API_KEY":process.env.GEMINI_API_KEY?"GEMINI_API_KEY":"none"}async function ee(s,e){let t=Date.now(),i=0,r=0,a="",l=0,c={input_tokens:0,output_tokens:0},o=!1,m="",d=!1;if(e.debugDir)try{let{mkdirSync:p}=await import("fs");p(e.debugDir,{recursive:!0})}catch{}let I="Always repeat the results of your tool calls (like file contents or command output) in your text response. This is critical for verification.",re=e.appendSystemPrompt?`${I}
|
|
6
|
+
${e.appendSystemPrompt}`:I,ie={...e,appendSystemPrompt:re},L=new Set,U=new Map,B=new Map,w=[],T=N(),D=!1,v="";try{let P=await new _(ie).spawn(s),K=z(P,e.idleTimeout*1e3);for await(let n of K)switch(n.type){case"init":{a=n.session_id,d=!0;let u={type:"system",subtype:"init",cwd:e.cwd,session_id:a,tools:k(),model:E(e.model),permissionMode:"bypassPermissions",apiKeySource:C(),mcp_servers:[]};y(u),i=Date.now();break}case"message":{if(n.role==="assistant"){if(n.delta)D=!0,v+=n.content,w.push({type:"text",text:n.content});else if(!D)w.push({type:"text",text:n.content}),v=n.content;else if(n.content.length>v.length&&n.content.startsWith(v)){let u=n.content.slice(v.length);u.trim()&&(w.push({type:"text",text:u}),v=n.content)}}break}case"tool_use":{if(!L.has(n.tool_id)){L.add(n.tool_id);let u=$();U.set(n.tool_id,u);let S=Q(n.tool_name),b=X(n.parameters);B.set(n.tool_id,{name:S,params:n.parameters}),w.push({type:"tool_use",id:u,name:S,input:b});let j={type:"assistant",message:{id:T,type:"message",role:"assistant",model:E(e.model),content:w,stop_reason:"tool_use"}};y(j),w=[],T=N(),D=!1,v="",l++}break}case"tool_result":{let u=U.get(n.tool_id)||$(),S=B.get(n.tool_id),b="";if(n.status==="error")b={is_error:!0,error:n.error||"Unknown error"};else{let F=n;if(b=n.output!==void 0?n.output:F.result||F.content||"",S?.name==="Read"&&b===""){let M=S.params?.file_path||S.params?.filePath||S.params?.path;if(M&&typeof M=="string")try{let W=ue(e.cwd,M);b=me(W,"utf-8")}catch{}}}y({type:"user",message:{role:"user",content:[{type:"tool_result",tool_use_id:u,content:b}]}}),D=!1,v="";break}case"result":{if(r=Date.now(),w.length>0){let u={type:"assistant",message:{id:T,type:"message",role:"assistant",model:E(e.model),content:w,stop_reason:"end_turn"}};y(u),l++}c.input_tokens=(c.input_tokens||0)+(n.stats.input_tokens||0),c.output_tokens=(c.output_tokens||0)+(n.stats.output_tokens||0),o=n.status==="error",m=o?"Error completing request":"Request completed successfully";break}}}catch(p){let P=p instanceof Error?p.message:String(p);if(P.includes("Invalid session ID"))throw p;if(o=!0,m=`Agent Error: ${P}`,!d){let n={type:"system",subtype:"init",cwd:e.cwd,session_id:a||x,tools:k(),model:E(e.model),permissionMode:"bypassPermissions",apiKeySource:C(),mcp_servers:[]};y(n),d=!0}y({type:"assistant",message:{id:x,type:"message",role:"assistant",model:"<synthetic>",content:[{type:"text",text:m}]}})}let oe=Date.now(),ne={type:"result",subtype:o?"error":"success",is_error:o,duration_ms:oe-t,duration_api_ms:r>0?r-i:0,num_turns:l,result:m,session_id:a,usage:c};return y(ne),await new Promise(p=>{process.stdout.write("",()=>p())}),{exitCode:o?1:0,sessionId:a}}async function se(s,e){let t={type:"system",subtype:"init",cwd:process.cwd(),session_id:e||x,tools:k(),model:"<unknown>",permissionMode:"bypassPermissions",apiKeySource:C(),mcp_servers:[]};y(t);let i={type:"assistant",message:{id:x,type:"message",role:"assistant",model:"<synthetic>",content:[{type:"text",text:`API Error: ${s}`}]}};y(i),y({type:"result",subtype:"error",is_error:!0,duration_ms:0,duration_api_ms:0,num_turns:0,result:s,session_id:e}),await new Promise(a=>{process.stdout.write("",()=>a())})}var he=fe(import.meta.url),ye=pe(he);async function we(){let s=[];for await(let e of process.stdin)s.push(Buffer.from(e));return Buffer.concat(s).toString("utf-8").trim()}function te(){console.error(`
|
|
7
7
|
Gemini CLI Shim - Translate Gemini CLI to standardized JSONL output
|
|
8
8
|
|
|
9
9
|
Usage: gemini-cli-shim [options]
|
|
@@ -16,6 +16,7 @@ Optional Arguments:
|
|
|
16
16
|
--resume <session_id> Session ID to continue
|
|
17
17
|
--verbose Enable verbose logging to stderr
|
|
18
18
|
--append-system-prompt <txt> Additional system prompt to append
|
|
19
|
+
--idle-timeout <seconds> Max seconds between agent events before aborting (default: 120)
|
|
19
20
|
--debug-dir <path> Directory for debug logs and session data
|
|
20
21
|
--self-test Run environment verification
|
|
21
22
|
--version Print version and exit
|
|
@@ -26,4 +27,4 @@ Examples:
|
|
|
26
27
|
echo "Continue" | gemini-cli-shim --model pro --resume <session_id>
|
|
27
28
|
echo "Debug" | gemini-cli-shim --model flash --debug-dir ./debug
|
|
28
29
|
gemini-cli-shim --self-test
|
|
29
|
-
`)}function
|
|
30
|
+
`)}function ve(){try{let s=ge(ye,"..","package.json"),e=JSON.parse(de(s,"utf-8"));console.error(`gemini-cli-shim v${e.version}`)}catch{console.error("gemini-cli-shim (version unknown)")}}async function Se(){let s=[],e=await _.isInstalled();s.push({name:"gemini_cli_found",passed:e.found,message:e.found?`Gemini CLI found at ${e.path} (${e.version})`:"Gemini CLI not found in PATH"});let t=!!(process.env.GOOGLE_API_KEY||process.env.GEMINI_API_KEY);s.push({name:"api_key",passed:t,message:t?"API key found in environment":"No GOOGLE_API_KEY or GEMINI_API_KEY found"});let i=s.every(a=>a.passed),r={shim:{name:"gemini-cli-shim",version:"1.0.0"},agent:{name:"gemini-cli",version:e.version||"unknown",found:e.found},checks:s,overall:{passed:i,message:i?"All checks passed":"Some checks failed"}};console.log(JSON.stringify(r,null,2)),process.exit(i?0:1)}async function be(){let s=Y(process.argv.slice(2),{"-m":"--model","-r":"--resume","-v":"--verbose","-h":"--help"});if(s.help&&(te(),process.exit(0)),s.version&&(ve(),process.exit(0)),s.selfTest){await Se();return}s.model||(console.error("Error: --model is required"),te(),process.exit(1));let e=await we();e||process.exit(0);let t=V(s.model);(await _.isInstalled()).found||(console.error("Error: Gemini CLI not found in PATH"),console.error("Please install Gemini CLI: https://geminicli.com/docs/"),process.exit(1)),!process.env.GOOGLE_API_KEY&&!process.env.GEMINI_API_KEY&&(console.error("Error: No API key found"),console.error("Set GOOGLE_API_KEY or GEMINI_API_KEY environment variable"),process.exit(1));let r=!1,a=()=>{r||(r=!0,s.verbose&&console.error("[gemini-cli-shim] Received interrupt signal, exiting gracefully"),process.exit(0))};process.on("SIGINT",a),process.on("SIGTERM",a);try{let l=await ee(e,{model:t,resume:s.resume,verbose:s.verbose,cwd:process.cwd(),appendSystemPrompt:s.appendSystemPrompt,debugDir:s.debugDir,idleTimeout:s.idleTimeout});process.exit(l.exitCode)}catch(l){let c=l instanceof Error?l.message:String(l);c.includes("Invalid session ID")&&(console.error(`Error: Session not found: ${s.resume}`),console.error("Use a valid session ID to resume"),process.exit(1)),await se(c),process.exit(1)}}be().catch(s=>{console.error("Fatal error:",s),process.exit(1)});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "hankweave",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "Orchestration runtime for antibrittle agentic workflows",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Southbridge AI",
|
|
@@ -35,9 +35,6 @@
|
|
|
35
35
|
"lint": "biome check .",
|
|
36
36
|
"lint:fix": "biome check --write .",
|
|
37
37
|
"format": "biome format --write .",
|
|
38
|
-
"release:patch": "bun scripts/release.ts patch",
|
|
39
|
-
"release:minor": "bun scripts/release.ts minor",
|
|
40
|
-
"release:major": "bun scripts/release.ts major",
|
|
41
38
|
"fetch-models": "bun scripts/fetch-models-dev.ts",
|
|
42
39
|
"server": "bun server/index.ts",
|
|
43
40
|
"server:headless": "bun server/index.ts --headless",
|
|
@@ -57,9 +54,9 @@
|
|
|
57
54
|
"cleanup": "echo 'Error: --cleanup requires --config' && exit 1",
|
|
58
55
|
"cleanup:example": "bun server/index.ts --cleanup --config=hank.json",
|
|
59
56
|
"cleanup:force": "bun server/index.ts --cleanup --config=hank.json -y",
|
|
60
|
-
"test:cross-runtime:bun": "bun
|
|
61
|
-
"test:cross-runtime:node": "
|
|
62
|
-
"test:cross-runtime:deno": "
|
|
57
|
+
"test:cross-runtime:bun": "bun scripts/run-cross-runtime-tests.ts bun",
|
|
58
|
+
"test:cross-runtime:node": "bun scripts/run-cross-runtime-tests.ts node",
|
|
59
|
+
"test:cross-runtime:deno": "bun scripts/run-cross-runtime-tests.ts deno",
|
|
63
60
|
"test:cross-runtime:all": "bun run test:cross-runtime:bun && bun run test:cross-runtime:node && bun run test:cross-runtime:deno"
|
|
64
61
|
},
|
|
65
62
|
"devDependencies": {
|
package/schemas/hank.schema.json
CHANGED
|
@@ -72,6 +72,12 @@
|
|
|
72
72
|
},
|
|
73
73
|
"additionalProperties": false,
|
|
74
74
|
"description": "Override sentinel system settings"
|
|
75
|
+
},
|
|
76
|
+
"shimIdleTimeout": {
|
|
77
|
+
"type": "integer",
|
|
78
|
+
"exclusiveMinimum": 0,
|
|
79
|
+
"maximum": 600,
|
|
80
|
+
"description": "Default shim idle timeout for all codons in this hank (seconds)"
|
|
75
81
|
}
|
|
76
82
|
},
|
|
77
83
|
"additionalProperties": false,
|
|
@@ -1086,6 +1092,12 @@
|
|
|
1086
1092
|
"exclusiveMinimum": 0,
|
|
1087
1093
|
"default": 100,
|
|
1088
1094
|
"description": "Maximum number of extensions before forcing completion. Default: 100. Safety valve to prevent infinite extension loops."
|
|
1095
|
+
},
|
|
1096
|
+
"shimIdleTimeout": {
|
|
1097
|
+
"type": "integer",
|
|
1098
|
+
"exclusiveMinimum": 0,
|
|
1099
|
+
"maximum": 600,
|
|
1100
|
+
"description": "Max seconds between agent events before the shim aborts (idle timeout). Overrides hank-level and runtime defaults. If unset, falls back to hank override, runtime config, or shim default (120s)."
|
|
1089
1101
|
}
|
|
1090
1102
|
},
|
|
1091
1103
|
"required": [
|
|
@@ -2161,6 +2173,12 @@
|
|
|
2161
2173
|
"exclusiveMinimum": 0,
|
|
2162
2174
|
"default": 100,
|
|
2163
2175
|
"description": "Maximum number of extensions before forcing completion. Default: 100. Safety valve to prevent infinite extension loops."
|
|
2176
|
+
},
|
|
2177
|
+
"shimIdleTimeout": {
|
|
2178
|
+
"type": "integer",
|
|
2179
|
+
"exclusiveMinimum": 0,
|
|
2180
|
+
"maximum": 600,
|
|
2181
|
+
"description": "Max seconds between agent events before the shim aborts (idle timeout). Overrides hank-level and runtime defaults. If unset, falls back to hank override, runtime config, or shim default (120s)."
|
|
2164
2182
|
}
|
|
2165
2183
|
},
|
|
2166
2184
|
"required": [
|
|
@@ -55,6 +55,12 @@
|
|
|
55
55
|
"maximum": 255,
|
|
56
56
|
"description": "Idle timeout for WebSocket and proxy servers in seconds (0-255). This is the maximum amount of time a connection is allowed to be idle before the server closes it. A connection is idling if there is no data sent or received."
|
|
57
57
|
},
|
|
58
|
+
"shimIdleTimeout": {
|
|
59
|
+
"type": "integer",
|
|
60
|
+
"exclusiveMinimum": 0,
|
|
61
|
+
"maximum": 600,
|
|
62
|
+
"description": "Default shim idle timeout in seconds. Max time between agent events before the shim aborts. Per-codon and hank override settings take precedence."
|
|
63
|
+
},
|
|
58
64
|
"ignoreRigFailures": {
|
|
59
65
|
"type": "boolean",
|
|
60
66
|
"description": "If true, ignore all rig setup failures (useful for resume workflows)"
|
package/shims/codex/index.js
CHANGED
|
@@ -1,5 +1,64 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
+
// ../common/src/args.ts
|
|
4
|
+
function parseArgs(argv, aliases) {
|
|
5
|
+
const args = {
|
|
6
|
+
model: "",
|
|
7
|
+
verbose: false,
|
|
8
|
+
idleTimeout: 120,
|
|
9
|
+
selfTest: false,
|
|
10
|
+
version: false,
|
|
11
|
+
help: false
|
|
12
|
+
};
|
|
13
|
+
for (let i = 0; i < argv.length; i++) {
|
|
14
|
+
let arg = argv[i];
|
|
15
|
+
if (arg.includes("=")) {
|
|
16
|
+
const [key, value] = arg.split("=", 2);
|
|
17
|
+
argv.splice(i, 1, key, value);
|
|
18
|
+
arg = key;
|
|
19
|
+
}
|
|
20
|
+
if (aliases && arg in aliases) {
|
|
21
|
+
arg = aliases[arg];
|
|
22
|
+
}
|
|
23
|
+
switch (arg) {
|
|
24
|
+
case "--model":
|
|
25
|
+
args.model = argv[++i];
|
|
26
|
+
break;
|
|
27
|
+
case "--resume":
|
|
28
|
+
args.resume = argv[++i];
|
|
29
|
+
break;
|
|
30
|
+
case "--verbose":
|
|
31
|
+
args.verbose = true;
|
|
32
|
+
break;
|
|
33
|
+
case "--append-system-prompt":
|
|
34
|
+
args.appendSystemPrompt = argv[++i];
|
|
35
|
+
break;
|
|
36
|
+
case "--debug-dir":
|
|
37
|
+
args.debugDir = argv[++i];
|
|
38
|
+
break;
|
|
39
|
+
case "--idle-timeout": {
|
|
40
|
+
const val = Number(argv[++i]);
|
|
41
|
+
if (!Number.isFinite(val) || val <= 0) {
|
|
42
|
+
console.error("Invalid --idle-timeout value: must be a positive number");
|
|
43
|
+
process.exit(1);
|
|
44
|
+
}
|
|
45
|
+
args.idleTimeout = val;
|
|
46
|
+
break;
|
|
47
|
+
}
|
|
48
|
+
case "--self-test":
|
|
49
|
+
args.selfTest = true;
|
|
50
|
+
break;
|
|
51
|
+
case "--version":
|
|
52
|
+
args.version = true;
|
|
53
|
+
break;
|
|
54
|
+
case "--help":
|
|
55
|
+
args.help = true;
|
|
56
|
+
break;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return args;
|
|
60
|
+
}
|
|
61
|
+
|
|
3
62
|
// src/selftest.ts
|
|
4
63
|
import { spawn } from "child_process";
|
|
5
64
|
import { existsSync as existsSync2 } from "fs";
|
|
@@ -666,6 +725,40 @@ var SessionManager = class {
|
|
|
666
725
|
}
|
|
667
726
|
};
|
|
668
727
|
|
|
728
|
+
// ../common/src/timeout.ts
|
|
729
|
+
var IdleTimeoutError = class extends Error {
|
|
730
|
+
timeoutMs;
|
|
731
|
+
constructor(timeoutMs) {
|
|
732
|
+
super(`Idle timeout: no events received for ${timeoutMs}ms`);
|
|
733
|
+
this.name = "IdleTimeoutError";
|
|
734
|
+
this.timeoutMs = timeoutMs;
|
|
735
|
+
}
|
|
736
|
+
};
|
|
737
|
+
async function* withIdleTimeout(events, timeoutMs) {
|
|
738
|
+
const iterator = events[Symbol.asyncIterator]();
|
|
739
|
+
try {
|
|
740
|
+
while (true) {
|
|
741
|
+
let timeoutId;
|
|
742
|
+
try {
|
|
743
|
+
const result = await Promise.race([
|
|
744
|
+
iterator.next(),
|
|
745
|
+
new Promise((_, reject) => {
|
|
746
|
+
timeoutId = setTimeout(() => {
|
|
747
|
+
reject(new IdleTimeoutError(timeoutMs));
|
|
748
|
+
}, timeoutMs);
|
|
749
|
+
})
|
|
750
|
+
]);
|
|
751
|
+
if (result.done) break;
|
|
752
|
+
yield result.value;
|
|
753
|
+
} finally {
|
|
754
|
+
clearTimeout(timeoutId);
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
} finally {
|
|
758
|
+
void iterator.return?.();
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
|
|
669
762
|
// src/utils/ids.ts
|
|
670
763
|
function generateMessageId() {
|
|
671
764
|
return `msg_${Date.now().toString(36)}${Math.random().toString(36).substring(2, 10)}`;
|
|
@@ -721,27 +814,6 @@ function getCodexModelId(spec) {
|
|
|
721
814
|
return spec.modelID;
|
|
722
815
|
}
|
|
723
816
|
|
|
724
|
-
// Model resolution assertions - verify reasoning effort parsing at load time
|
|
725
|
-
{
|
|
726
|
-
const testCases = [
|
|
727
|
-
{ input: "gpt-5.3-codex-high", expectedModelID: "gpt-5.3-codex", expectedEffort: "high" },
|
|
728
|
-
{ input: "gpt-5.3-codex-xhigh", expectedModelID: "gpt-5.3-codex", expectedEffort: "xhigh" },
|
|
729
|
-
{ input: "gpt-5.2-codex-high", expectedModelID: "gpt-5.2-codex", expectedEffort: "high" },
|
|
730
|
-
{ input: "gpt-5.2-xhigh", expectedModelID: "gpt-5.2", expectedEffort: "xhigh" },
|
|
731
|
-
{ input: "gpt-5.3-codex", expectedModelID: "gpt-5.3-codex", expectedEffort: undefined },
|
|
732
|
-
];
|
|
733
|
-
for (const tc of testCases) {
|
|
734
|
-
const result = resolveModel(tc.input);
|
|
735
|
-
if (result.modelID !== tc.expectedModelID || result.reasoningEffort !== tc.expectedEffort) {
|
|
736
|
-
throw new Error(
|
|
737
|
-
`Model resolution assertion failed for "${tc.input}": ` +
|
|
738
|
-
`expected modelID="${tc.expectedModelID}" effort="${tc.expectedEffort}", ` +
|
|
739
|
-
`got modelID="${result.modelID}" effort="${result.reasoningEffort}"`
|
|
740
|
-
);
|
|
741
|
-
}
|
|
742
|
-
}
|
|
743
|
-
}
|
|
744
|
-
|
|
745
817
|
// src/utils/output.ts
|
|
746
818
|
function emit(message) {
|
|
747
819
|
console.log(JSON.stringify(message));
|
|
@@ -975,11 +1047,12 @@ ${this.args.appendSystemPrompt}`;
|
|
|
975
1047
|
state.apiStartTime = Date.now();
|
|
976
1048
|
this.apiStartTime = state.apiStartTime;
|
|
977
1049
|
const { events } = await state.thread.runStreamed(finalPrompt);
|
|
1050
|
+
const timedEvents = withIdleTimeout(events, this.args.idleTimeout * 1e3);
|
|
978
1051
|
const currentMessageContent = [];
|
|
979
1052
|
let currentMessageId = generateMessageId();
|
|
980
1053
|
const pendingToolResults = /* @__PURE__ */ new Map();
|
|
981
1054
|
let systemInitEmitted = false;
|
|
982
|
-
for await (const event of
|
|
1055
|
+
for await (const event of timedEvents) {
|
|
983
1056
|
if (this.interrupted) {
|
|
984
1057
|
verboseLog(this.args.verbose, "Interrupted, stopping event processing");
|
|
985
1058
|
break;
|
|
@@ -1391,55 +1464,6 @@ ${errorStack}
|
|
|
1391
1464
|
};
|
|
1392
1465
|
|
|
1393
1466
|
// src/utils/args.ts
|
|
1394
|
-
function parseArgs(argv) {
|
|
1395
|
-
const args = {
|
|
1396
|
-
model: "",
|
|
1397
|
-
verbose: false,
|
|
1398
|
-
selfTest: false,
|
|
1399
|
-
version: false,
|
|
1400
|
-
help: false
|
|
1401
|
-
};
|
|
1402
|
-
for (let i = 0; i < argv.length; i++) {
|
|
1403
|
-
const arg = argv[i];
|
|
1404
|
-
if (arg.includes("=")) {
|
|
1405
|
-
const [key, value] = arg.split("=", 2);
|
|
1406
|
-
argv.splice(i, 1, key, value);
|
|
1407
|
-
}
|
|
1408
|
-
switch (arg) {
|
|
1409
|
-
case "--model":
|
|
1410
|
-
args.model = argv[++i];
|
|
1411
|
-
break;
|
|
1412
|
-
case "-p":
|
|
1413
|
-
args.prompt = "";
|
|
1414
|
-
break;
|
|
1415
|
-
case "--resume":
|
|
1416
|
-
args.resume = argv[++i];
|
|
1417
|
-
break;
|
|
1418
|
-
case "--verbose":
|
|
1419
|
-
args.verbose = true;
|
|
1420
|
-
break;
|
|
1421
|
-
case "--append-system-prompt":
|
|
1422
|
-
args.appendSystemPrompt = argv[++i];
|
|
1423
|
-
break;
|
|
1424
|
-
case "--debug-dir":
|
|
1425
|
-
args.debugDir = argv[++i];
|
|
1426
|
-
break;
|
|
1427
|
-
case "--self-test":
|
|
1428
|
-
args.selfTest = true;
|
|
1429
|
-
break;
|
|
1430
|
-
case "--version":
|
|
1431
|
-
args.version = true;
|
|
1432
|
-
break;
|
|
1433
|
-
case "--help":
|
|
1434
|
-
args.help = true;
|
|
1435
|
-
break;
|
|
1436
|
-
}
|
|
1437
|
-
}
|
|
1438
|
-
if (!args.model && !args.selfTest && !args.version && !args.help) {
|
|
1439
|
-
args.model = process.env.MODEL || "codex";
|
|
1440
|
-
}
|
|
1441
|
-
return args;
|
|
1442
|
-
}
|
|
1443
1467
|
async function readStdin() {
|
|
1444
1468
|
return new Promise((resolve, reject) => {
|
|
1445
1469
|
let data = "";
|
|
@@ -1475,6 +1499,7 @@ OPTIONS:
|
|
|
1475
1499
|
--resume <session_id> Continue existing session
|
|
1476
1500
|
--verbose Enable verbose logging to stderr
|
|
1477
1501
|
--append-system-prompt Additional system prompt to append
|
|
1502
|
+
--idle-timeout <seconds> Max seconds between agent events before aborting (default: 120)
|
|
1478
1503
|
--debug-dir <path> Directory for debug logs and session data
|
|
1479
1504
|
--self-test Run environment verification
|
|
1480
1505
|
--version Print version and exit
|
|
@@ -1497,6 +1522,9 @@ EXAMPLES:
|
|
|
1497
1522
|
async function main() {
|
|
1498
1523
|
try {
|
|
1499
1524
|
const args = parseArgs(process.argv.slice(2));
|
|
1525
|
+
if (!args.model && !args.selfTest && !args.version && !args.help) {
|
|
1526
|
+
args.model = process.env.MODEL || "codex";
|
|
1527
|
+
}
|
|
1500
1528
|
if (args.version) {
|
|
1501
1529
|
printVersion();
|
|
1502
1530
|
process.exit(0);
|