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.
@@ -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 events) {
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 le}from"fs";import{dirname as ce,join as me}from"path";import{fileURLToPath as ue}from"url";import{spawn as G}from"child_process";import{createWriteStream as T}from"fs";import{mkdir as Y}from"fs/promises";import{isAbsolute as O,resolve as g}from"path";import{createInterface as re}from"readline";var S=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",i=G(t?"where":"which",["gemini"],{shell:t}),a="";i.stdout?.on("data",c=>{a+=c.toString()}),i.on("close",c=>{if(c===0&&a.trim()){let l=G("gemini",["--version"],{shell:t}),r="";l.stdout?.on("data",m=>{r+=m.toString()}),l.on("close",()=>{e({found:!0,path:a.trim(),version:r.trim()})})}else e({found:!1})})})}async initializeDebugStreams(e){if(this.options.debugDir)try{let t=O(this.options.debugDir)?this.options.debugDir:g(this.options.cwd,this.options.debugDir);await Y(t,{recursive:!0});let o=g(t,`session-${e}.raw.jsonl`);this.rawEventStream=T(o,{flags:"a",encoding:"utf8"});let i=g(t,`session-${e}.raw.log`);this.stderrStream=T(i,{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=O(this.options.debugDir)?this.options.debugDir:g(this.options.cwd,this.options.debugDir);await this.closeDebugStreams();let{rename:o}=await import("fs/promises"),i=g(t,`session-${this.currentSessionId}.raw.jsonl`),a=g(t,`session-${e}.raw.jsonl`),c=g(t,`session-${this.currentSessionId}.raw.log`),l=g(t,`session-${e}.raw.log`);try{await o(i,a)}catch(r){this.verbose&&console.error(`[gemini-cli-shim] Could not rename ${i}:`,r)}try{await o(c,l)}catch(r){this.verbose&&console.error(`[gemini-cli-shim] Could not rename ${c}:`,r)}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=O(this.options.debugDir)?this.options.debugDir:g(this.options.cwd,this.options.debugDir);await Y(t,{recursive:!0});let o=g(t,`session-${this.currentSessionId}.raw.log`),i=T(o,{flags:"a",encoding:"utf8"});await new Promise((a,c)=>{i.write(this.stderrBuffer,l=>{l?c(l):i.end(r=>{r?c(r):a()})})}),this.stderrBuffer=""}catch(t){console.error("[gemini-cli-shim] Error flushing buffered stderr:",t)}this.rawEventStream&&(e.push(new Promise((t,o)=>{this.rawEventStream?.end(i=>{i?o(i):t()})})),this.rawEventStream=null),this.stderrStream&&(e.push(new Promise((t,o)=>{this.stderrStream?.end(i=>{i?o(i):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 o=process.platform==="win32";this.process=G("gemini",t,{cwd:this.options.cwd,stdio:["pipe","pipe","pipe"],env:{...process.env},shell:o}),this.options.debugDir&&await this.initializeDebugStreams("unknown");let i=this.options.appendSystemPrompt?`${e}
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(i),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,o=!1,i="",a=null;this.process.stderr.on("data",l=>{let r=l.toString();e+=r,this.writeStderr(r),!o&&(r.includes("Error resuming session:")||r.includes("Invalid session identifier"))&&(t=!0,o=!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]",r)});let c=re({input:this.process.stdout,crlfDelay:1/0});if(t)throw new Error("Invalid session ID");try{for await(let l of c){let r=l.trim();if(t)throw new Error("Invalid session ID");if(r)try{let m=JSON.parse(r);m.type==="init"&&m.session_id&&(i=m.session_id,this.options.debugDir&&this.currentSessionId==="unknown"&&await this.renameDebugStreams(i)),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:",r),console.error("[gemini-cli-shim] Error:",m))}}}catch(l){throw t?new Error("Invalid session ID"):l}if(t)throw new Error("Invalid session ID");await new Promise((l,r)=>{if(!this.process){t?r(new Error("Invalid session ID")):l();return}let m=setTimeout(()=>{this.verbose&&console.error("[gemini-cli-shim] Process exit timeout, forcing kill"),this.kill(),r(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(b){console.error("[gemini-cli-shim] Error closing debug streams:",b)}t?r(new Error("Invalid session ID")):d===0||d===null?l():r(new Error(`Gemini CLI exited with code ${d}`))}),this.process.on("error",async d=>{clearTimeout(m);try{await this.closeDebugStreams()}catch(b){console.error("[gemini-cli-shim] Error closing debug streams:",b)}r(t?new Error("Invalid session ID"):d)})})}kill(){this.process&&(this.process.kill("SIGTERM"),this.process=null)}};import{readFileSync as ne}from"fs";import{resolve as ae}from"path";function z(s){let e={model:"",verbose:!1,selfTest:!1,version:!1,help:!1};for(let t=2;t<s.length;t++){let o=s[t];if(o.includes("=")){let[i,a]=o.split("=",2);switch(i){case"--model":e.model=a;break;case"--resume":e.resume=a;break;case"--append-system-prompt":e.appendSystemPrompt=a;break;case"--debug-dir":e.debugDir=a;break}continue}switch(o){case"--model":case"-m":t+1<s.length&&(e.model=s[++t]);break;case"--resume":case"-r":t+1<s.length&&(e.resume=s[++t]);break;case"--append-system-prompt":t+1<s.length&&(e.appendSystemPrompt=s[++t]);break;case"--debug-dir":t+1<s.length&&(e.debugDir=s[++t]);break;case"--verbose":case"-v":e.verbose=!0;break;case"--self-test":e.selfTest=!0;break;case"--version":e.version=!0;break;case"--help":case"-h":e.help=!0;break;case"-p":break;default:break}}return e}var F={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 J(s){if(!s){let e=process.env.MODEL;e?s=e:s="flash"}if(F[s]){let e=F[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 _(s){return s.startsWith("anthropic/")?s.replace("anthropic/",""):!s.includes("/")&&(s.startsWith("gemini-")||s==="flash"||s==="pro")?`google/${s}`:s}import{randomBytes as j}from"crypto";function L(){let s=Date.now().toString(36),e=j(5).toString("hex");return`msg_${s}${e}`}function $(){let s=Date.now().toString(36),e=j(6).toString("hex");return`toolu_${s}${e}`}var I="00000000-0000-0000-0000-000000000000";var ie={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 H(s){return ie[s]||s}function oe(s){return s.replace(/[A-Z]/g,e=>`_${e.toLowerCase()}`)}function q(s){if(!s)return s;let e={};for(let[t,o]of Object.entries(s))e[oe(t)]=o;return e}function D(){return["Read","Write","Edit","Bash","Glob","Grep","LS"]}function f(s){console.log(JSON.stringify(s))}function R(){return process.env.GOOGLE_API_KEY?"GOOGLE_API_KEY":process.env.GEMINI_API_KEY?"GEMINI_API_KEY":"none"}async function V(s,e){let t=Date.now(),o=0,i=0,a="",c=0,l={input_tokens:0,output_tokens:0},r=!1,m="",d=!1;if(e.debugDir)try{let{mkdirSync:p}=await import("fs");p(e.debugDir,{recursive:!0})}catch{}let b="Always repeat the results of your tool calls (like file contents or command output) in your text response. This is critical for verification.",X=e.appendSystemPrompt?`${b}
6
- ${e.appendSystemPrompt}`:b,ee={...e,appendSystemPrompt:X},C=new Set,N=new Map,U=new Map,h=[],M=L(),E=!1,w="";try{let P=await new S(ee).spawn(s);for await(let n of P)switch(n.type){case"init":{a=n.session_id,d=!0;let u={type:"system",subtype:"init",cwd:e.cwd,session_id:a,tools:D(),model:_(e.model),permissionMode:"bypassPermissions",apiKeySource:R(),mcp_servers:[]};f(u),o=Date.now();break}case"message":{if(n.role==="assistant"){if(n.delta)E=!0,w+=n.content,h.push({type:"text",text:n.content});else if(!E)h.push({type:"text",text:n.content}),w=n.content;else if(n.content.length>w.length&&n.content.startsWith(w)){let u=n.content.slice(w.length);u.trim()&&(h.push({type:"text",text:u}),w=n.content)}}break}case"tool_use":{if(!C.has(n.tool_id)){C.add(n.tool_id);let u=$();N.set(n.tool_id,u);let v=H(n.tool_name),y=q(n.parameters);U.set(n.tool_id,{name:v,params:n.parameters}),h.push({type:"tool_use",id:u,name:v,input:y});let K={type:"assistant",message:{id:M,type:"message",role:"assistant",model:_(e.model),content:h,stop_reason:"tool_use"}};f(K),h=[],M=L(),E=!1,w="",c++}break}case"tool_result":{let u=N.get(n.tool_id)||$(),v=U.get(n.tool_id),y="";if(n.status==="error")y={is_error:!0,error:n.error||"Unknown error"};else{let B=n;if(y=n.output!==void 0?n.output:B.result||B.content||"",v?.name==="Read"&&y===""){let A=v.params?.file_path||v.params?.filePath||v.params?.path;if(A&&typeof A=="string")try{let W=ae(e.cwd,A);y=ne(W,"utf-8")}catch{}}}f({type:"user",message:{role:"user",content:[{type:"tool_result",tool_use_id:u,content:y}]}}),E=!1,w="";break}case"result":{if(i=Date.now(),h.length>0){let u={type:"assistant",message:{id:M,type:"message",role:"assistant",model:_(e.model),content:h,stop_reason:"end_turn"}};f(u),c++}l.input_tokens=(l.input_tokens||0)+(n.stats.input_tokens||0),l.output_tokens=(l.output_tokens||0)+(n.stats.output_tokens||0),r=n.status==="error",m=r?"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(r=!0,m=`Agent Error: ${P}`,!d){let u={type:"system",subtype:"init",cwd:e.cwd,session_id:a||I,tools:D(),model:_(e.model),permissionMode:"bypassPermissions",apiKeySource:R(),mcp_servers:[]};f(u),d=!0}f({type:"assistant",message:{id:I,type:"message",role:"assistant",model:"<synthetic>",content:[{type:"text",text:m}]}})}let se=Date.now(),te={type:"result",subtype:r?"error":"success",is_error:r,duration_ms:se-t,duration_api_ms:i>0?i-o:0,num_turns:c,result:m,session_id:a,usage:l};return f(te),await new Promise(p=>{process.stdout.write("",()=>p())}),{exitCode:r?1:0,sessionId:a}}async function Z(s,e){let t={type:"system",subtype:"init",cwd:process.cwd(),session_id:e||I,tools:D(),model:"<unknown>",permissionMode:"bypassPermissions",apiKeySource:R(),mcp_servers:[]};f(t);let o={type:"assistant",message:{id:I,type:"message",role:"assistant",model:"<synthetic>",content:[{type:"text",text:`API Error: ${s}`}]}};f(o),f({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 de=ue(import.meta.url),pe=ce(de);async function ge(){let s=[];for await(let e of process.stdin)s.push(Buffer.from(e));return Buffer.concat(s).toString("utf-8").trim()}function Q(){console.error(`
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 fe(){try{let s=me(pe,"..","package.json"),e=JSON.parse(le(s,"utf-8"));console.error(`gemini-cli-shim v${e.version}`)}catch{console.error("gemini-cli-shim (version unknown)")}}async function he(){let s=[],e=await S.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 o=s.every(a=>a.passed),i={shim:{name:"gemini-cli-shim",version:"1.0.0"},agent:{name:"gemini-cli",version:e.version||"unknown",found:e.found},checks:s,overall:{passed:o,message:o?"All checks passed":"Some checks failed"}};console.log(JSON.stringify(i,null,2)),process.exit(o?0:1)}async function we(){let s=z(process.argv);if(s.help&&(Q(),process.exit(0)),s.version&&(fe(),process.exit(0)),s.selfTest){await he();return}s.model||(console.error("Error: --model is required"),Q(),process.exit(1));let e=await ge();e||process.exit(0);let t=J(s.model);(await S.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 i=!1,a=()=>{i||(i=!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 c=await V(e,{model:t,resume:s.resume,verbose:s.verbose,cwd:process.cwd(),appendSystemPrompt:s.appendSystemPrompt,debugDir:s.debugDir});process.exit(c.exitCode)}catch(c){let l=c instanceof Error?c.message:String(c);l.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 Z(l),process.exit(1)}}we().catch(s=>{console.error("Fatal error:",s),process.exit(1)});
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.3",
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 tests/cross-runtime/dynamic-port.test.ts",
61
- "test:cross-runtime:node": "npx tsx tests/cross-runtime/dynamic-port.test.ts",
62
- "test:cross-runtime:deno": "deno run -A --node-modules-dir --sloppy-imports tests/cross-runtime/dynamic-port.test.ts",
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": {
@@ -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)"
@@ -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 events) {
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);