@oh-my-pi/pi-coding-agent 16.3.2 → 16.3.3

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.
Files changed (93) hide show
  1. package/CHANGELOG.md +38 -0
  2. package/dist/cli.js +3221 -3208
  3. package/dist/types/cli/update-cli.d.ts +1 -0
  4. package/dist/types/cli/update-cli.test.d.ts +1 -0
  5. package/dist/types/commands/update.d.ts +5 -0
  6. package/dist/types/config/config-file.d.ts +1 -1
  7. package/dist/types/config/settings-schema.d.ts +10 -0
  8. package/dist/types/dap/client.d.ts +9 -1
  9. package/dist/types/edit/file-snapshot-store.d.ts +9 -1
  10. package/dist/types/edit/hashline/execute.d.ts +1 -1
  11. package/dist/types/edit/hashline/params.d.ts +3 -6
  12. package/dist/types/edit/renderer.d.ts +1 -0
  13. package/dist/types/extensibility/plugins/parser.d.ts +2 -1
  14. package/dist/types/hindsight/client.d.ts +15 -11
  15. package/dist/types/hindsight/client.test.d.ts +1 -0
  16. package/dist/types/mcp/smithery-registry.d.ts +1 -0
  17. package/dist/types/mcp/smithery-registry.test.d.ts +1 -0
  18. package/dist/types/modes/components/advisor-message.d.ts +4 -2
  19. package/dist/types/modes/components/tool-execution.d.ts +9 -6
  20. package/dist/types/modes/components/transcript-container.d.ts +1 -0
  21. package/dist/types/modes/theme/theme.d.ts +1 -1
  22. package/dist/types/session/indexed-session-storage.d.ts +2 -2
  23. package/dist/types/session/session-storage.d.ts +31 -3
  24. package/dist/types/ssh/__tests__/connection-manager-timeout.test.d.ts +1 -0
  25. package/dist/types/ssh/__tests__/sshfs-mount.test.d.ts +1 -0
  26. package/dist/types/ssh/connection-manager.d.ts +19 -0
  27. package/dist/types/ssh/sshfs-mount.d.ts +10 -1
  28. package/dist/types/subprocess/worker-client.d.ts +20 -6
  29. package/dist/types/tools/bash.d.ts +27 -0
  30. package/dist/types/tools/renderers.d.ts +13 -0
  31. package/dist/types/tools/ssh.d.ts +2 -0
  32. package/dist/types/utils/fetch-timeout.d.ts +4 -0
  33. package/dist/types/web/search/providers/base.d.ts +1 -0
  34. package/dist/types/web/search/providers/gemini.d.ts +1 -0
  35. package/package.json +12 -12
  36. package/scripts/generate-legacy-pi-bundled-registry.ts +8 -2
  37. package/src/cli/models-cli.ts +19 -0
  38. package/src/cli/update-cli.test.ts +28 -0
  39. package/src/cli/update-cli.ts +35 -8
  40. package/src/commands/update.ts +8 -2
  41. package/src/config/config-file.ts +6 -6
  42. package/src/config/settings-schema.ts +10 -0
  43. package/src/dap/client.ts +134 -36
  44. package/src/edit/file-snapshot-store.ts +12 -1
  45. package/src/edit/hashline/diff.ts +4 -13
  46. package/src/edit/hashline/execute.ts +1 -1
  47. package/src/edit/hashline/params.ts +5 -12
  48. package/src/edit/renderer.ts +4 -2
  49. package/src/edit/streaming.ts +15 -5
  50. package/src/export/html/tool-views.generated.js +2 -2
  51. package/src/extensibility/plugins/installer.ts +12 -3
  52. package/src/extensibility/plugins/manager.ts +32 -8
  53. package/src/extensibility/plugins/parser.ts +7 -5
  54. package/src/extensibility/tool-event-input.ts +1 -1
  55. package/src/hindsight/client.test.ts +33 -0
  56. package/src/hindsight/client.ts +42 -22
  57. package/src/internal-urls/docs-index.generated.txt +1 -1
  58. package/src/main.ts +7 -1
  59. package/src/mcp/oauth-flow.ts +93 -4
  60. package/src/mcp/smithery-auth.ts +3 -0
  61. package/src/mcp/smithery-connect.ts +9 -0
  62. package/src/mcp/smithery-registry.test.ts +51 -0
  63. package/src/mcp/smithery-registry.ts +27 -4
  64. package/src/modes/components/advisor-message.ts +13 -10
  65. package/src/modes/components/status-line/component.ts +11 -4
  66. package/src/modes/components/tool-execution.ts +74 -8
  67. package/src/modes/components/transcript-container.ts +26 -0
  68. package/src/modes/controllers/event-controller.ts +7 -3
  69. package/src/modes/controllers/tool-args-reveal.ts +1 -1
  70. package/src/modes/interactive-mode.ts +1 -0
  71. package/src/modes/rpc/rpc-client.ts +29 -16
  72. package/src/modes/theme/shimmer.ts +49 -15
  73. package/src/modes/theme/theme.ts +7 -0
  74. package/src/session/agent-session.ts +166 -30
  75. package/src/session/indexed-session-storage.ts +40 -3
  76. package/src/session/session-manager.ts +83 -14
  77. package/src/session/session-storage.ts +112 -26
  78. package/src/slash-commands/helpers/usage-report.ts +6 -1
  79. package/src/ssh/__tests__/connection-manager-timeout.test.ts +61 -0
  80. package/src/ssh/__tests__/sshfs-mount.test.ts +13 -0
  81. package/src/ssh/connection-manager.ts +44 -11
  82. package/src/ssh/sshfs-mount.ts +27 -4
  83. package/src/subprocess/worker-client.ts +161 -10
  84. package/src/tools/bash.ts +30 -1
  85. package/src/tools/grep.ts +21 -1
  86. package/src/tools/read.ts +14 -4
  87. package/src/tools/renderers.ts +13 -0
  88. package/src/tools/ssh.ts +8 -0
  89. package/src/utils/clipboard.ts +49 -12
  90. package/src/utils/fetch-timeout.ts +10 -0
  91. package/src/web/search/index.ts +8 -0
  92. package/src/web/search/providers/base.ts +1 -0
  93. package/src/web/search/providers/gemini.ts +23 -6
@@ -114,6 +114,7 @@ interface EditRenderArgs {
114
114
  newText?: string;
115
115
  patch?: string;
116
116
  input?: string;
117
+ _input?: string;
117
118
  all?: boolean;
118
119
  // Patch mode fields
119
120
  op?: Operation;
@@ -600,10 +601,11 @@ function getHashlineInputRenderSummary(
600
601
  args: EditRenderArgs,
601
602
  editMode: EditMode | undefined,
602
603
  ): HashlineInputRenderSummary | undefined {
603
- if (editMode !== "hashline" || typeof args.input !== "string") {
604
+ const input = args.input ?? args._input;
605
+ if (editMode !== "hashline" || typeof input !== "string") {
604
606
  return undefined;
605
607
  }
606
- return { entries: getHashlineInputSections(args.input) };
608
+ return { entries: getHashlineInputSections(input) };
607
609
  }
608
610
 
609
611
  function getApplyPatchRenderSummary(
@@ -429,9 +429,19 @@ const patchStrategy: EditStreamingStrategy<PatchArgs> = {
429
429
 
430
430
  interface HashlineArgs {
431
431
  input?: string;
432
+ _input?: string;
432
433
  __partialJson?: string;
433
434
  }
434
435
 
436
+ /**
437
+ * Text payload of a hashline edit call. The public schema declares `input`, but
438
+ * streaming sees the raw model output before validation coerces aliases, so a
439
+ * provider that emits the legacy `_input` key still previews correctly.
440
+ */
441
+ function hashlineEditText(args: HashlineArgs | undefined): string | undefined {
442
+ return args?.input ?? args?._input;
443
+ }
444
+
435
445
  /**
436
446
  * While streaming a free-form text payload (apply_patch envelope, hashline
437
447
  * input), trim the trailing partial line so per-character growth of an
@@ -513,7 +523,8 @@ const hashlineStrategy: EditStreamingStrategy<HashlineArgs> = {
513
523
  return args;
514
524
  },
515
525
  async computeDiffPreview(args, ctx) {
516
- if (typeof args.input !== "string" || args.input.length === 0) return null;
526
+ const input = hashlineEditText(args);
527
+ if (typeof input !== "string" || input.length === 0) return null;
517
528
  // Unlike apply_patch, hashline previews flow through `applyPartialTo`,
518
529
  // whose streaming-tolerant parser (`parsePatchStreaming` → `endStreaming`)
519
530
  // drops a payload-less trailing op and projects a partially-typed payload
@@ -521,7 +532,6 @@ const hashlineStrategy: EditStreamingStrategy<HashlineArgs> = {
521
532
  // would instead strip the sole payload of a single-op `replace`/`insert`
522
533
  // for almost the entire stream, collapsing the preview to "No changes" and
523
534
  // rendering a blank box. Feed the raw in-flight text straight through.
524
- const input = args.input;
525
535
  ctx.signal.throwIfAborted();
526
536
 
527
537
  let sections: readonly HashlineInputSection[];
@@ -579,19 +589,19 @@ const hashlineStrategy: EditStreamingStrategy<HashlineArgs> = {
579
589
  return "";
580
590
  },
581
591
  matcherDigest(args) {
582
- const input = args?.input;
592
+ const input = hashlineEditText(args);
583
593
  if (typeof input !== "string") return undefined;
584
594
  // Body rows are `+TEXT`; headers and op lines are grammar, never content.
585
595
  return extractAddedLines(input, false);
586
596
  },
587
597
  matcherPaths(args) {
588
- const input = args?.input;
598
+ const input = hashlineEditText(args);
589
599
  if (typeof input !== "string" || input.length === 0) return undefined;
590
600
  const paths = extractHashlineHeaderPaths(input);
591
601
  return paths.length > 0 ? paths : undefined;
592
602
  },
593
603
  matcherEntries(args) {
594
- const input = args?.input;
604
+ const input = hashlineEditText(args);
595
605
  if (typeof input !== "string" || input.length === 0) return undefined;
596
606
  const entries = splitHashlinePerFile(input);
597
607
  return entries.length > 0 ? entries : undefined;
@@ -17,9 +17,9 @@ Error generating stack: `+X.message+`
17
17
  `),maxLines:6,title:N>D.length?`parse issues (${N} total)`:"parse issues"}),W0.jsx(S,{result:Y,maxLines:12})]})}var HH={Summary:jq,Body:xq};var K0=K($(),1),yq=/^[\w@%+=:,./-]+$/,gq=/\[raw output: artifact:\/\/([\w-]+)\]/;function mq(f){let Y=[];for(let Z in f){let X=$Z(f[Z]);Y.push(`${Z}=${yq.test(X)?X:JSON.stringify(X)}`)}return Y.join(" ")}function uq(f){if(!f||!T(f.async))return null;let Y=A(f.async.state);return Y?{state:Y,jobId:A(f.async.jobId)}:null}function cq({args:f,result:Y}){let Z=f.command===void 0?"…":A(f.command);if(Z===null)return K0.jsx(v,{what:"command"});let X=I(b(Z)||"…",80);return Y?.isError?K0.jsx("span",{className:"tv-err-text",children:X}):K0.jsx("span",{children:X})}function dq({args:f,result:Y}){let Z=f.command===void 0?"…":A(f.command),X=T(f.env)?mq(f.env):"",G=A(f.cwd),z=O(f.head),J=O(f.tail),U=P(Y),H=O(U?.exitCode),W=O(U?.wallTimeMs),Q=O(f.timeout)??O(U?.timeoutSeconds),_=O(U?.requestedTimeoutSeconds),q=uq(U),M=gq.exec(o0(Y))?.[1]??null,D=[];if(W!==null)D.push(W<1000?`wall ${Math.round(W)}ms`:`wall ${(W/1000).toFixed(1)}s`);if(_!==null&&_!==Q)D.push(`requested timeout ${_}s clamped`);if(q?.jobId)D.push(`job ${q.jobId}`);if(M)D.push(`artifact ${M}`);return K0.jsxs(K0.Fragment,{children:[K0.jsxs("div",{className:"tv-cmd",children:[K0.jsx("span",{className:"tv-cmd-prompt",children:"$"}),K0.jsxs("span",{className:"tv-cmd-text",children:[X&&K0.jsx("span",{className:"tv-cmd-env",children:`${X} `}),Z??K0.jsx(v,{what:"command"})]})]}),K0.jsx(o,{items:[G&&K0.jsxs(w,{children:["cwd=",V0(G)]}),Q!==null&&K0.jsxs(w,{children:["timeout=",Q,"s"]}),f.pty===!0&&K0.jsx(w,{tone:"accent",children:"pty"}),!q&&f.async===!0&&K0.jsx(w,{tone:"accent",children:"async"}),z!==null&&K0.jsxs(w,{children:["head=",z]}),J!==null&&K0.jsxs(w,{children:["tail=",J]}),H!==null&&K0.jsxs(w,{tone:"err",children:["exit ",H]}),q&&K0.jsxs(w,{tone:q.state==="failed"?"err":q.state==="running"?"accent":"ok",children:["async ",q.state]})]}),K0.jsx(q7,{result:Y}),K0.jsx(S,{result:Y,maxLines:12}),D.length>0&&K0.jsx(m,{children:D.join(" · ")})]})}var AH={Summary:cq,Body:dq};var v0=K($(),1);function WH(f){let Y=P(f);return{action:Y?A(Y.action):null,name:Y?A(Y.name):null,url:Y?A(Y.url):null,browser:Y?A(Y.browser):null}}function lq(f){if(!T(f.app))return null;return{path:A(f.app.path),cdpUrl:A(f.app.cdp_url),target:A(f.app.target)}}function pq(f){switch(f){case"open":return"ok";case"run":return"accent";case"close":return"warn";default:return}}function iq(f,Y){if(f?.cdpUrl)return`connected ${f.cdpUrl}`;if(f?.path)return`spawned ${V0(f.path)}`;return Y.browser}function oq({args:f,result:Y}){let Z=WH(Y),X=A(f.action)??Z.action??"?",G=X==="close"&&(f.all===!0||A(f.name)===null&&Z.name===null),z=Z.name??A(f.name)??"main",J=Z.url??A(f.url);return v0.jsxs(v0.Fragment,{children:[v0.jsx(w,{tone:pq(X),children:X}),v0.jsx("span",{children:G?"all tabs":z}),f.kill===!0&&v0.jsx(w,{tone:"err",children:"kill"}),J&&v0.jsx("span",{className:"tv-faint",children:I(V0(J),72)})]})}function nq({args:f,result:Y}){let Z=WH(Y),X=A(f.action)??Z.action,G=lq(f),z=Z.name??A(f.name),J=Z.url??A(f.url),U=iq(G,Z),H=T(f.viewport)?f.viewport:null,W=H?O(H.width):null,Q=H?O(H.height):null,_=H?O(H.scale):null,q=A(f.code);return v0.jsxs(v0.Fragment,{children:[v0.jsxs("span",{className:"tv-badges",children:[z!==null&&v0.jsxs(w,{children:["tab ",z]}),J&&v0.jsx(w,{tone:"accent",children:I(V0(J),120)}),U&&v0.jsx(w,{children:U}),G?.target&&v0.jsxs(w,{children:["target ",G.target]}),f.all===!0&&v0.jsx(w,{tone:"warn",children:"all"}),f.kill===!0&&v0.jsx(w,{tone:"err",children:"kill"}),W!==null&&Q!==null&&v0.jsxs(w,{children:[W,"×",Q,_!==null?`@${_}x`:""]})]}),X==="run"&&q!==null&&v0.jsx(O7,{code:q.replace(/\s+$/,""),lang:"javascript"}),v0.jsx(q7,{result:Y}),v0.jsx(S,{result:Y,maxLines:10})]})}var f8={Summary:oq,Body:nq};var G0=K($(),1);function rq(f){let Y=P(f);if(!Y||!T(Y.snapshot))return null;let Z=Y.snapshot,X=T(Z.source)?Z.source:null;return{id:A(Z.id)??(O(Z.id)!==null?String(Z.id):null),adapter:A(Z.adapter),status:A(Z.status),program:A(Z.program),stopReason:A(Z.stopReason),frameName:A(Z.frameName),sourcePath:X?A(X.path):null,line:O(Z.line),column:O(Z.column),exitCode:O(Z.exitCode),needsConfigurationDone:Z.needsConfigurationDone===!0}}function aq(f){let Y=A(f.args.action)??A(P(f.result)?.action);return Y?Y.replace(/_/g," "):"request"}function sq(f){return A(f.function)??A(f.expression)??A(f.command)??A(f.memory_reference)??A(f.instruction_reference)??A(f.data_id)??A(f.name)}function tq(f){let{args:Y}=f,Z=A(Y.program),X=A(Y.file),G=O(Y.line),z=sq(Y);return G0.jsxs(G0.Fragment,{children:[G0.jsx(w,{tone:"accent",children:aq(f)}),Z!==null?G0.jsx(Y0,{path:Z}):X!==null?G0.jsx(Y0,{path:X,from:G??void 0}):z!==null?G0.jsx("span",{children:I(b(z),80)}):null]})}var eq=[["adapter","adapter"],["cwd","cwd"],["function","function"],["name","name"],["condition","condition"],["hit_condition","hit condition"],["context","context"],["frame_id","frame id"],["scope_id","scope id"],["variable_ref","variable ref"],["pid","pid"],["host","host"],["port","port"],["levels","levels"],["memory_reference","memory ref"],["instruction_reference","instruction ref"],["instruction_count","instruction count"],["instruction_offset","instruction offset"],["offset","offset"],["count","count"],["data","data"],["data_id","data id"],["access_type","access"],["command","command"],["resolve_symbols","resolve symbols"],["allow_partial","allow partial"],["start_module","start module"],["module_count","module count"],["timeout","timeout"]];function fQ(f){let{args:Y,result:Z}=f,X=A(Y.program),G=A(Y.file),z=O(Y.line),J=A(Y.expression),U=Array.isArray(Y.args)?Y.args.filter((_)=>typeof _==="string"):[],H=[];if(X!==null)H.push(G0.jsx(E,{k:"program",children:G0.jsx(Y0,{path:X})},"program"));if(U.length>0)H.push(G0.jsx(E,{k:"args",children:I(U.join(" "),160)},"args"));if(G!==null)H.push(G0.jsx(E,{k:"file",children:G0.jsx(Y0,{path:G,from:z??void 0})},"file"));else if(z!==null)H.push(G0.jsx(E,{k:"line",children:z},"line"));for(let[_,q]of eq){let M=Y[_];if(typeof M!=="string"&&typeof M!=="number"&&typeof M!=="boolean")continue;H.push(G0.jsx(E,{k:q,children:I($Z(M),120)},_))}let W="";if(T(Y.arguments))try{W=JSON.stringify(Y.arguments,null,2)??""}catch{W=""}let Q=rq(Z);return G0.jsxs(G0.Fragment,{children:[H.length>0&&G0.jsx(O0,{children:H}),J!==null&&G0.jsx(O7,{code:J,title:"expression",maxLines:8}),W&&G0.jsx(O7,{code:W,lang:"json",title:"arguments",maxLines:10}),Q&&G0.jsxs(O0,{children:[Q.id!==null&&G0.jsx(E,{k:"session",children:Q.id}),Q.adapter!==null&&G0.jsx(E,{k:"adapter",children:Q.adapter}),Q.status!==null&&G0.jsx(E,{k:"status",children:G0.jsx(w,{tone:Q.status==="exited"?"warn":"ok",children:Q.status})}),Q.program!==null&&G0.jsx(E,{k:"program",children:G0.jsx(Y0,{path:Q.program})}),Q.stopReason!==null&&G0.jsx(E,{k:"stop reason",children:Q.stopReason}),Q.frameName!==null&&G0.jsx(E,{k:"frame",children:Q.frameName}),Q.sourcePath!==null&&Q.line!==null&&G0.jsx(E,{k:"location",children:G0.jsx(Y0,{path:Q.sourcePath,sel:Q.column!==null?`${Q.line}:${Q.column}`:String(Q.line)})}),Q.exitCode!==null&&G0.jsx(E,{k:"exit code",children:Q.exitCode}),Q.needsConfigurationDone&&G0.jsx(E,{k:"configuration",children:"pending configurationDone — set breakpoints, then continue"})]}),G0.jsx(S,{result:Z,maxLines:10})]})}var qH={Summary:tq,Body:fQ};var y=K($(),1);function YQ(f){let Y=f.trimEnd();if(!Y.startsWith("[")||!Y.endsWith("]"))return null;let Z=Y.slice(1,-1).trim(),X=/#[0-9a-fA-F]{4}$/.exec(Z);if(X)Z=Z.slice(0,X.index);if(Z.length>=2){let G=Z[0];if((G==='"'||G==="'")&&G===Z[Z.length-1])Z=Z.slice(1,-1)}return Z.length>0?Z:null}var ZQ=/^\*{3} (?:Update|Add|Delete) File:\s*(.+)$/;function RH(f){let Y=f.startsWith("\uFEFF")?f.slice(1):f,Z=[];for(let X of Y.split(`
18
18
  `)){let G=X.replace(/\r$/,""),z=YQ(G);if(z){Z.push(z);continue}let J=ZQ.exec(G.trim());if(J)Z.push(J[1].trim())}return Z}var XQ=/^(?:replace|insert|delete)\b/;function GQ(f){let Y=0;for(let Z of f.split(`
19
19
  `))if(XQ.test(Z))Y++;return Y}function zQ(f){let Y=0,Z=0;for(let X of f.split(`
20
- `))if(X.startsWith("+"))Y++;else if(X.startsWith("-"))Z++;return{added:Y,removed:Z}}function QH(f){let Y=T(f.diagnostics)?f.diagnostics:null,Z=[];if(Y&&Array.isArray(Y.messages)){for(let X of Y.messages)if(typeof X==="string")Z.push(X)}return{path:A(f.path),diff:A(f.diff),firstChangedLine:O(f.firstChangedLine),op:A(f.op),move:A(f.move),isError:f.isError===!0,errorText:A(f.displayErrorText)??A(f.errorText),diagnostics:Y?{summary:A(Y.summary),messages:Z,errored:Y.errored===!0}:null}}function JQ({args:f,result:Y}){let Z=A(f.input),X=Z?RH(Z):[],G=A(f.file_path)??A(f.path);if(X.length===0&&G)X.push(G);if(X.length===0&&Array.isArray(f.edits))for(let Q of f.edits){let _=T(Q)?A(Q.path):null;if(_&&!X.includes(_))X.push(_)}let z=Z?GQ(Z):0,J=P(Y),U=J&&Y?.isError!==!0?A(J.diff):null,H=U?zQ(U):null,W=Z?I(b(Z.split(`
20
+ `))if(X.startsWith("+"))Y++;else if(X.startsWith("-"))Z++;return{added:Y,removed:Z}}function QH(f){let Y=T(f.diagnostics)?f.diagnostics:null,Z=[];if(Y&&Array.isArray(Y.messages)){for(let X of Y.messages)if(typeof X==="string")Z.push(X)}return{path:A(f.path),diff:A(f.diff),firstChangedLine:O(f.firstChangedLine),op:A(f.op),move:A(f.move),isError:f.isError===!0,errorText:A(f.displayErrorText)??A(f.errorText),diagnostics:Y?{summary:A(Y.summary),messages:Z,errored:Y.errored===!0}:null}}function JQ({args:f,result:Y}){let Z=A(f.input)??A(f._input),X=Z?RH(Z):[],G=A(f.file_path)??A(f.path);if(X.length===0&&G)X.push(G);if(X.length===0&&Array.isArray(f.edits))for(let Q of f.edits){let _=T(Q)?A(Q.path):null;if(_&&!X.includes(_))X.push(_)}let z=Z?GQ(Z):0,J=P(Y),U=J&&Y?.isError!==!0?A(J.diff):null,H=U?zQ(U):null,W=Z?I(b(Z.split(`
21
21
  `,1)[0]??""),80):"";return y.jsxs(y.Fragment,{children:[X.length>0?y.jsx(Y0,{path:X[0]}):y.jsx("span",{children:W}),X.length>1&&y.jsxs(y.Fragment,{children:[" ",y.jsxs(w,{children:["+",X.length-1," more"]})]}),z>0&&y.jsxs(y.Fragment,{children:[" ",y.jsxs(w,{children:[z," op",z===1?"":"s"]})]}),H!==null&&H.added>0&&y.jsxs(y.Fragment,{children:[" ",y.jsxs(w,{tone:"ok",children:["+",H.added]})]}),H!==null&&H.removed>0&&y.jsxs(y.Fragment,{children:[" ",y.jsxs(w,{tone:"err",children:["−",H.removed]})]}),Y?.isError===!0&&y.jsxs(y.Fragment,{children:[" ",y.jsx(w,{tone:"err",children:"failed"})]})]})}function MH({entry:f,fallbackPath:Y}){let Z=f.path??Y??null,X=f.op==="create"||f.op==="delete"?f.op:null,G=f.diagnostics;return y.jsxs("div",{children:[(Z!==null||X!==null||f.move!==null)&&y.jsx("div",{className:"tv-row",children:y.jsxs("span",{className:"tv-row-val",children:[Z!==null&&y.jsx(Y0,{path:Z,from:f.isError?null:f.firstChangedLine}),f.move!==null&&y.jsxs(y.Fragment,{children:[" → ",y.jsx(Y0,{path:f.move})]}),X!==null&&y.jsxs(y.Fragment,{children:[" ",y.jsx(w,{tone:X==="delete"?"err":"ok",children:X})]}),f.isError&&y.jsxs(y.Fragment,{children:[" ",y.jsx(w,{tone:"err",children:"failed"})]})]})}),f.isError?f.errorText!==null&&y.jsx(n,{text:f.errorText,error:!0,maxLines:10}):f.diff!==null&&f.diff.length>0&&y.jsx(AX,{diff:f.diff,maxLines:40}),G?.summary&&y.jsx(p,{tone:G.errored?"err":"warn",children:G.summary}),G!==null&&G.messages.length>0&&y.jsx(n,{text:G.messages.join(`
22
- `),maxLines:6})]})}function UQ({args:f,result:Y}){let Z=A(f.input),X=P(Y),G=[];if(X&&Array.isArray(X.perFileResults)){for(let U of X.perFileResults)if(T(U))G.push(QH(U))}let z=A(f.file_path)??A(f.path)??(Z?RH(Z)[0]??null:null),J;if(G.length>0)J=G.map((U,H)=>y.jsx(MH,{entry:U},`${U.path??""}:${H}`));else if(Y?.isError===!0)J=y.jsx(S,{result:Y,maxLines:15});else if(X){let U=QH(X);J=U.diff!==null||U.diagnostics!==null||U.move!==null?y.jsx(MH,{entry:U,fallbackPath:z}):y.jsx(S,{result:Y,maxLines:8})}else J=y.jsx(S,{result:Y,maxLines:8});return y.jsxs(y.Fragment,{children:[J,Array.isArray(f.edits)&&f.edits.length>0&&y.jsx(O0,{children:f.edits.map((U,H)=>T(U)?y.jsx(E,{k:A(U.op)??"edit",children:A(U.sel)??A(U.path)??A(U.rename)??A(U.move)??"?"},`${H}`):y.jsx(E,{k:"edit",children:y.jsx(v,{what:"edit"})},`${H}`))}),Z!==null&&Z.length>0&&y.jsx(n,{text:Z,variant:"code",maxLines:10,title:"input"}),Z===null&&f.input!==void 0&&y.jsx(v,{what:"input"})]})}var Y8={Summary:JQ,Body:UQ};var C0=K($(),1),HQ={py:"python",js:"javascript",ts:"typescript",rb:"ruby",jl:"julia"};function VY(f){let Y=(f??"").toUpperCase();if(Y==="PY"||Y==="PYTHON"||Y==="IPY"||Y==="IPYTHON")return"py";if(Y==="JS"||Y==="JAVASCRIPT")return"js";if(Y==="TS"||Y==="TYPESCRIPT")return"ts";if(Y==="RB"||Y==="RUBY")return"rb";if(Y==="JL"||Y==="JULIA")return"jl";return null}function AQ(f){let Y=[],Z=0;while(Z<f.length){while(Z<f.length&&/\s/.test(f[Z]))Z++;if(Z>=f.length)break;let X="";while(Z<f.length&&!/\s/.test(f[Z])){let G=f[Z];if(G==='"'||G==="'"){X+=G,Z++;while(Z<f.length&&f[Z]!==G)X+=f[Z],Z++;if(Z<f.length)X+=f[Z],Z++}else X+=G,Z++}Y.push(X)}return Y}function WQ(f){let Y=/^\*{2,}\s*Cell\b\s*(.*)$/i,Z=/^\*{2,}\s*End\b.*$/i,X=/^([a-zA-Z][\w-]*)(?::(?:"([^"]*)"|'([^']*)'|(.*)))?$/,G=/^\d+(?:ms|s|m)?$/,z=["id","title","name","cell","file","label"],J=["t","timeout","duration","time"],U=["rst","reset"],H=f.split(`
22
+ `),maxLines:6})]})}function UQ({args:f,result:Y}){let Z=A(f.input)??A(f._input),X=P(Y),G=[];if(X&&Array.isArray(X.perFileResults)){for(let U of X.perFileResults)if(T(U))G.push(QH(U))}let z=A(f.file_path)??A(f.path)??(Z?RH(Z)[0]??null:null),J;if(G.length>0)J=G.map((U,H)=>y.jsx(MH,{entry:U},`${U.path??""}:${H}`));else if(Y?.isError===!0)J=y.jsx(S,{result:Y,maxLines:15});else if(X){let U=QH(X);J=U.diff!==null||U.diagnostics!==null||U.move!==null?y.jsx(MH,{entry:U,fallbackPath:z}):y.jsx(S,{result:Y,maxLines:8})}else J=y.jsx(S,{result:Y,maxLines:8});return y.jsxs(y.Fragment,{children:[J,Array.isArray(f.edits)&&f.edits.length>0&&y.jsx(O0,{children:f.edits.map((U,H)=>T(U)?y.jsx(E,{k:A(U.op)??"edit",children:A(U.sel)??A(U.path)??A(U.rename)??A(U.move)??"?"},`${H}`):y.jsx(E,{k:"edit",children:y.jsx(v,{what:"edit"})},`${H}`))}),Z!==null&&Z.length>0&&y.jsx(n,{text:Z,variant:"code",maxLines:10,title:"input"}),Z===null&&(f.input!==void 0||f._input!==void 0)&&y.jsx(v,{what:"input"})]})}var Y8={Summary:JQ,Body:UQ};var C0=K($(),1),HQ={py:"python",js:"javascript",ts:"typescript",rb:"ruby",jl:"julia"};function VY(f){let Y=(f??"").toUpperCase();if(Y==="PY"||Y==="PYTHON"||Y==="IPY"||Y==="IPYTHON")return"py";if(Y==="JS"||Y==="JAVASCRIPT")return"js";if(Y==="TS"||Y==="TYPESCRIPT")return"ts";if(Y==="RB"||Y==="RUBY")return"rb";if(Y==="JL"||Y==="JULIA")return"jl";return null}function AQ(f){let Y=[],Z=0;while(Z<f.length){while(Z<f.length&&/\s/.test(f[Z]))Z++;if(Z>=f.length)break;let X="";while(Z<f.length&&!/\s/.test(f[Z])){let G=f[Z];if(G==='"'||G==="'"){X+=G,Z++;while(Z<f.length&&f[Z]!==G)X+=f[Z],Z++;if(Z<f.length)X+=f[Z],Z++}else X+=G,Z++}Y.push(X)}return Y}function WQ(f){let Y=/^\*{2,}\s*Cell\b\s*(.*)$/i,Z=/^\*{2,}\s*End\b.*$/i,X=/^([a-zA-Z][\w-]*)(?::(?:"([^"]*)"|'([^']*)'|(.*)))?$/,G=/^\d+(?:ms|s|m)?$/,z=["id","title","name","cell","file","label"],J=["t","timeout","duration","time"],U=["rst","reset"],H=f.split(`
23
23
  `);if(H.length>0&&H[H.length-1]==="")H.pop();let W=[],Q=0;while(Q<H.length&&H[Q].trim()==="")Q++;while(Q<H.length){let _=Y.exec(H[Q]);if(!_){Q++;continue}let q=AQ(_[1]??""),M=null,D="",N=[],u=!1,B=[];for(let L of q){if(U.includes(L.toLowerCase())){u=!0;continue}let V=X.exec(L);if(V&&L.includes(":")){let r=V[1].toLowerCase(),C=V[2]??V[3]??V[4]??"",d=VY(r);if(d){if(!M)M=d;if(!D&&C)D=C;continue}if(z.includes(r)){if(!D)D=C;continue}if(J.includes(r)){N.push(`t=${C}`);continue}if(U.includes(r))N.push("rst");continue}let h=VY(L);if(h&&!M){M=h;continue}if(G.test(L)){N.push(`t=${L}`);continue}B.push(L)}if(!D&&B.length>0)D=B.join(" ");if(u)N.push("rst");Q++;let R=[];while(Q<H.length){if(Z.test(H[Q])){Q++;break}if(Y.test(H[Q]))break;R.push(H[Q]),Q++}while(R.length>0&&R[R.length-1].trim()==="")R.pop();W.push({lang:M??"py",title:D,attrs:N,code:R.join(`
24
24
  `)});while(Q<H.length&&H[Q].trim()==="")Q++}return W}function qQ(f){let Y=/^\*{2,}\s*Begin\b\s*(\S+)?\s*$/i,Z=/^\*{2,}\s*End\b.*$/i,X=/^\*{2,}\s*Title\s*:\s*(.+?)\s*$/i,G=/^\*{2,}\s*Timeout\s*:\s*(\S+)\s*$/i,z=/^\*{2,}\s*Reset\s*$/i,J=f.split(`
25
25
  `);if(J.length>0&&J[J.length-1]==="")J.pop();let U=[],H=0;while(H<J.length&&J[H].trim()==="")H++;while(H<J.length){let W=Y.exec(J[H]);if(!W){H++;continue}let Q=VY(W[1])??"py";H++;let _="",q=[];while(H<J.length){let D=X.exec(J[H]);if(D){if(!_)_=D[1];H++;continue}let N=G.exec(J[H]);if(N){q.push(`t=${N[1]}`),H++;continue}if(z.test(J[H])){q.push("rst"),H++;continue}break}let M=[];while(H<J.length){if(Z.test(J[H])){H++;break}if(Y.test(J[H]))break;M.push(J[H]),H++}while(M.length>0&&M[M.length-1].trim()==="")M.pop();U.push({lang:Q,title:_,attrs:q,code:M.join(`
@@ -53,9 +53,14 @@ export async function installPlugin(packageName: string): Promise<InstalledPlugi
53
53
  windowsHide: true,
54
54
  });
55
55
 
56
- const exitCode = await proc.exited;
56
+ // Drain both pipes concurrently with proc.exited to avoid a pipe-buffer
57
+ // deadlock if bun install floods stdout/stderr.
58
+ const [exitCode, , stderr] = await Promise.all([
59
+ proc.exited,
60
+ new Response(proc.stdout).text(),
61
+ new Response(proc.stderr).text(),
62
+ ]);
57
63
  if (exitCode !== 0) {
58
- const stderr = await new Response(proc.stderr).text();
59
64
  throw new Error(`Failed to install ${packageName}: ${stderr}`);
60
65
  }
61
66
 
@@ -95,7 +100,11 @@ export async function uninstallPlugin(name: string): Promise<void> {
95
100
  windowsHide: true,
96
101
  });
97
102
 
98
- const exitCode = await proc.exited;
103
+ const [exitCode] = await Promise.all([
104
+ proc.exited,
105
+ new Response(proc.stdout).text(),
106
+ new Response(proc.stderr).text(),
107
+ ]);
99
108
  if (exitCode !== 0) {
100
109
  throw new Error(`Failed to uninstall ${name}`);
101
110
  }
@@ -458,10 +458,17 @@ export class PluginManager {
458
458
  stderr: "pipe",
459
459
  windowsHide: true,
460
460
  });
461
- const installExit = await installProc.exited;
461
+ // Drain stdout+stderr concurrently with proc.exited. Awaiting exited
462
+ // before reading either pipe risks a >64 KiB OS-pipe-buffer deadlock
463
+ // once bun install prints enough progress; even where Bun currently
464
+ // buffers eagerly, doing this leaks unbounded memory.
465
+ const [installExit, , installStderr] = await Promise.all([
466
+ installProc.exited,
467
+ new Response(installProc.stdout).text(),
468
+ new Response(installProc.stderr).text(),
469
+ ]);
462
470
  if (installExit !== 0) {
463
- const stderr = await new Response(installProc.stderr).text();
464
- throw new Error(`bun install failed: ${stderr}`);
471
+ throw new Error(`bun install failed: ${installStderr}`);
465
472
  }
466
473
  // Resolve actual package name. npm specs encode the name (strip version);
467
474
  // git specs do not, so diff plugins/package.json deps to find the new entry.
@@ -508,10 +515,14 @@ export class PluginManager {
508
515
  stderr: "pipe",
509
516
  windowsHide: true,
510
517
  });
511
- const updateExit = await updateProc.exited;
518
+ // Same drain-concurrent-with-exit pattern as the bun install above.
519
+ const [updateExit, , updateStderr] = await Promise.all([
520
+ updateProc.exited,
521
+ new Response(updateProc.stdout).text(),
522
+ new Response(updateProc.stderr).text(),
523
+ ]);
512
524
  if (updateExit !== 0) {
513
- const stderr = await new Response(updateProc.stderr).text();
514
- throw new Error(`bun update ${actualName} failed: ${stderr}`);
525
+ throw new Error(`bun update ${actualName} failed: ${updateStderr}`);
515
526
  }
516
527
  }
517
528
 
@@ -608,7 +619,13 @@ export class PluginManager {
608
619
  windowsHide: true,
609
620
  });
610
621
 
611
- const exitCode = await proc.exited;
622
+ // Drain both pipes concurrently with proc.exited to avoid a pipe-buffer
623
+ // deadlock if bun uninstall floods stdout/stderr.
624
+ const [exitCode] = await Promise.all([
625
+ proc.exited,
626
+ new Response(proc.stdout).text(),
627
+ new Response(proc.stderr).text(),
628
+ ]);
612
629
  if (exitCode !== 0) {
613
630
  throw new Error(`npm uninstall failed for ${name}`);
614
631
  }
@@ -1007,7 +1024,14 @@ export class PluginManager {
1007
1024
  stderr: "pipe",
1008
1025
  windowsHide: true,
1009
1026
  });
1010
- return (await proc.exited) === 0;
1027
+ // Drain pipes concurrently with proc.exited; otherwise a chatty
1028
+ // bun install can block on a full OS pipe buffer.
1029
+ const [exit] = await Promise.all([
1030
+ proc.exited,
1031
+ new Response(proc.stdout).text(),
1032
+ new Response(proc.stderr).text(),
1033
+ ]);
1034
+ return exit === 0;
1011
1035
  } catch {
1012
1036
  return false;
1013
1037
  }
@@ -86,20 +86,22 @@ export function formatPluginSpec(spec: ParsedPluginSpec): string {
86
86
  }
87
87
 
88
88
  /**
89
- * Extract the base package name without version specifier.
89
+ * Extract the dependency key from an npm package specifier.
90
90
  * Used for path lookups after npm install.
91
91
  *
92
92
  * @example
93
93
  * extractPackageName("lodash@4.17.21") // "lodash"
94
94
  * extractPackageName("@scope/pkg@1.0.0") // "@scope/pkg"
95
95
  * extractPackageName("@scope/pkg") // "@scope/pkg"
96
+ * extractPackageName("npm:lodash") // "lodash"
96
97
  */
97
98
  export function extractPackageName(specifier: string): string {
99
+ const npmSpecifier = specifier.replace(/^npm:/i, "");
98
100
  // Handle scoped packages: @scope/name@version -> @scope/name
99
- if (specifier.startsWith("@")) {
100
- const match = specifier.match(/^(@[^/]+\/[^@]+)/);
101
- return match ? match[1] : specifier;
101
+ if (npmSpecifier.startsWith("@")) {
102
+ const match = npmSpecifier.match(/^(@[^/]+\/[^@]+)/);
103
+ return match ? match[1] : npmSpecifier;
102
104
  }
103
105
  // Unscoped: name@version -> name
104
- return specifier.replace(/@[^@]+$/, "");
106
+ return npmSpecifier.replace(/@[^@]+$/, "");
105
107
  }
@@ -60,7 +60,7 @@ export function normalizeToolEventInput(toolName: string, input: Record<string,
60
60
  if (toolName !== "edit" || stringField(input, "path")) return input;
61
61
 
62
62
  // Hashline edit mode: the only authoritative target list is the parsed
63
- // `¶PATH#TAG` headers inside `input`/`_input`. Trusting a passthrough
63
+ // `¶PATH#TAG` headers inside the patch. Trusting a passthrough
64
64
  // `_path` here would let a model-supplied field override the real edit
65
65
  // target and bypass extension gates that allowlist by path.
66
66
  const rawInput = stringField(input, "input") ?? stringField(input, "_input");
@@ -0,0 +1,33 @@
1
+ import { afterEach, describe, expect, it, vi } from "bun:test";
2
+ import { HindsightApi } from "./client";
3
+
4
+ type FetchInput = string | URL | Request;
5
+ type FetchInit = RequestInit | BunFetchRequestInit;
6
+
7
+ describe("HindsightApi fetch cancellation", () => {
8
+ afterEach(() => {
9
+ vi.restoreAllMocks();
10
+ });
11
+
12
+ it("combines caller cancellation with the request timeout", async () => {
13
+ let requestSignal: AbortSignal | undefined;
14
+ const fetchStub = Object.assign(
15
+ async (_input: FetchInput, init?: FetchInit) => {
16
+ requestSignal = init?.signal ?? undefined;
17
+ return Response.json({ results: [] });
18
+ },
19
+ { preconnect: globalThis.fetch.preconnect },
20
+ );
21
+ vi.spyOn(globalThis, "fetch").mockImplementation(fetchStub);
22
+
23
+ const caller = new AbortController();
24
+ const client = new HindsightApi({ baseUrl: "https://hindsight.example" });
25
+ await client.recall("bank", "query", { signal: caller.signal });
26
+
27
+ expect(requestSignal).toBeInstanceOf(AbortSignal);
28
+ expect(requestSignal).not.toBe(caller.signal);
29
+ caller.abort(new Error("caller aborted"));
30
+ expect(requestSignal?.aborted).toBe(true);
31
+ expect(requestSignal?.reason).toBe(caller.signal.reason);
32
+ });
33
+ });
@@ -8,10 +8,12 @@
8
8
  * tests to spy on.
9
9
  */
10
10
 
11
+ import { isTimeoutError, withTimeoutSignal } from "../utils/fetch-timeout";
11
12
  import type { HindsightConfig } from "./config";
12
13
 
13
14
  const USER_AGENT = "oh-my-pi-coding-agent";
14
15
  const DEFAULT_USER_AGENT = USER_AGENT;
16
+ const HINDSIGHT_REQUEST_TIMEOUT_MS = 30_000;
15
17
 
16
18
  export type Budget = "low" | "mid" | "high" | string;
17
19
  export type TagsMatch = "any" | "all" | "any_strict" | "all_strict";
@@ -24,6 +26,11 @@ export interface HindsightApiOptions {
24
26
  userAgent?: string;
25
27
  }
26
28
 
29
+ /** Caller cancellation shared by Hindsight request option bags. */
30
+ export interface HindsightRequestOptions {
31
+ signal?: AbortSignal;
32
+ }
33
+
27
34
  export interface RecallResult {
28
35
  id?: string;
29
36
  text: string;
@@ -77,7 +84,7 @@ export interface MemoryItemInput {
77
84
  updateMode?: UpdateMode;
78
85
  }
79
86
 
80
- export interface RetainOptions {
87
+ export interface RetainOptions extends HindsightRequestOptions {
81
88
  timestamp?: Date | string;
82
89
  context?: string;
83
90
  metadata?: Record<string, string>;
@@ -87,7 +94,7 @@ export interface RetainOptions {
87
94
  updateMode?: UpdateMode;
88
95
  }
89
96
 
90
- export interface RetainBatchOptions {
97
+ export interface RetainBatchOptions extends HindsightRequestOptions {
91
98
  /** Document id applied to every item that doesn't carry its own. */
92
99
  documentId?: string;
93
100
  /** Tags attached to the resulting document(s), not individual items. */
@@ -95,7 +102,7 @@ export interface RetainBatchOptions {
95
102
  async?: boolean;
96
103
  }
97
104
 
98
- export interface RecallOptions {
105
+ export interface RecallOptions extends HindsightRequestOptions {
99
106
  types?: string[];
100
107
  maxTokens?: number;
101
108
  budget?: Budget;
@@ -103,19 +110,19 @@ export interface RecallOptions {
103
110
  tagsMatch?: TagsMatch;
104
111
  }
105
112
 
106
- export interface ReflectOptions {
113
+ export interface ReflectOptions extends HindsightRequestOptions {
107
114
  context?: string;
108
115
  budget?: Budget;
109
116
  tags?: string[];
110
117
  tagsMatch?: TagsMatch;
111
118
  }
112
119
 
113
- export interface CreateBankOptions {
120
+ export interface CreateBankOptions extends HindsightRequestOptions {
114
121
  reflectMission?: string;
115
122
  retainMission?: string;
116
123
  }
117
124
 
118
- export interface ListMemoriesOptions {
125
+ export interface ListMemoriesOptions extends HindsightRequestOptions {
119
126
  limit?: number;
120
127
  offset?: number;
121
128
  type?: string;
@@ -123,12 +130,12 @@ export interface ListMemoriesOptions {
123
130
  consolidationState?: ConsolidationState;
124
131
  }
125
132
 
126
- export interface ListDocumentsOptions {
133
+ export interface ListDocumentsOptions extends HindsightRequestOptions {
127
134
  limit?: number;
128
135
  offset?: number;
129
136
  }
130
137
 
131
- export interface UpdateDocumentOptions {
138
+ export interface UpdateDocumentOptions extends HindsightRequestOptions {
132
139
  tags?: string[];
133
140
  }
134
141
 
@@ -166,7 +173,7 @@ export interface MentalModelHistoryEntry {
166
173
  [key: string]: unknown;
167
174
  }
168
175
 
169
- export interface CreateMentalModelOptions {
176
+ export interface CreateMentalModelOptions extends HindsightRequestOptions {
170
177
  id?: string;
171
178
  tags?: string[];
172
179
  maxTokens?: number;
@@ -183,11 +190,11 @@ export interface RefreshMentalModelResponse {
183
190
  [key: string]: unknown;
184
191
  }
185
192
 
186
- export interface ListMentalModelsOptions {
193
+ export interface ListMentalModelsOptions extends HindsightRequestOptions {
187
194
  detail?: MentalModelDetail;
188
195
  }
189
196
 
190
- export interface GetMentalModelOptions {
197
+ export interface GetMentalModelOptions extends HindsightRequestOptions {
191
198
  detail?: MentalModelDetail;
192
199
  }
193
200
 
@@ -208,6 +215,7 @@ interface RequestOptions {
208
215
  query?: Record<string, unknown>;
209
216
  /** Return null instead of throwing on a 404 response. */
210
217
  allow404?: boolean;
218
+ signal?: AbortSignal;
211
219
  }
212
220
 
213
221
  export class HindsightApi {
@@ -240,7 +248,10 @@ export class HindsightApi {
240
248
  "POST",
241
249
  `/v1/default/banks/${encodeURIComponent(bankId)}/memories`,
242
250
  "retain",
243
- { body: { items: [item], async: options?.async } },
251
+ {
252
+ body: { items: [item], async: options?.async },
253
+ signal: options?.signal,
254
+ },
244
255
  );
245
256
  }
246
257
 
@@ -270,6 +281,7 @@ export class HindsightApi {
270
281
  document_tags: options?.documentTags,
271
282
  async: options?.async,
272
283
  },
284
+ signal: options?.signal,
273
285
  },
274
286
  );
275
287
  }
@@ -288,6 +300,7 @@ export class HindsightApi {
288
300
  tags: options?.tags,
289
301
  tags_match: options?.tagsMatch,
290
302
  },
303
+ signal: options?.signal,
291
304
  },
292
305
  );
293
306
  }
@@ -305,6 +318,7 @@ export class HindsightApi {
305
318
  tags: options?.tags,
306
319
  tags_match: options?.tagsMatch,
307
320
  },
321
+ signal: options?.signal,
308
322
  },
309
323
  );
310
324
  }
@@ -319,6 +333,7 @@ export class HindsightApi {
319
333
  reflect_mission: options.reflectMission,
320
334
  retain_mission: options.retainMission,
321
335
  },
336
+ signal: options.signal,
322
337
  },
323
338
  );
324
339
  }
@@ -340,6 +355,7 @@ export class HindsightApi {
340
355
  limit: options?.limit,
341
356
  offset: options?.offset,
342
357
  },
358
+ signal: options?.signal,
343
359
  },
344
360
  );
345
361
  }
@@ -350,7 +366,7 @@ export class HindsightApi {
350
366
  "GET",
351
367
  `/v1/default/banks/${encodeURIComponent(bankId)}/documents`,
352
368
  "listDocuments",
353
- { query: { limit: options?.limit, offset: options?.offset } },
369
+ { query: { limit: options?.limit, offset: options?.offset }, signal: options?.signal },
354
370
  );
355
371
  }
356
372
 
@@ -370,7 +386,7 @@ export class HindsightApi {
370
386
  "PATCH",
371
387
  `/v1/default/banks/${encodeURIComponent(bankId)}/documents/${encodeURIComponent(documentId)}`,
372
388
  "updateDocument",
373
- { body: { tags: options.tags } },
389
+ { body: { tags: options.tags }, signal: options.signal },
374
390
  );
375
391
  }
376
392
 
@@ -399,7 +415,7 @@ export class HindsightApi {
399
415
  "GET",
400
416
  `/v1/default/banks/${encodeURIComponent(bankId)}/mental-models`,
401
417
  "listMentalModels",
402
- { query: { detail: options?.detail ?? "content" } },
418
+ { query: { detail: options?.detail ?? "content" }, signal: options?.signal },
403
419
  );
404
420
  }
405
421
 
@@ -413,7 +429,7 @@ export class HindsightApi {
413
429
  "GET",
414
430
  `/v1/default/banks/${encodeURIComponent(bankId)}/mental-models/${encodeURIComponent(mentalModelId)}`,
415
431
  "getMentalModel",
416
- { query: { detail: options?.detail ?? "content" }, allow404: true },
432
+ { query: { detail: options?.detail ?? "content" }, allow404: true, signal: options?.signal },
417
433
  );
418
434
  }
419
435
 
@@ -441,6 +457,7 @@ export class HindsightApi {
441
457
  max_tokens: options?.maxTokens,
442
458
  trigger: options?.trigger,
443
459
  },
460
+ signal: options?.signal,
444
461
  },
445
462
  );
446
463
  }
@@ -489,7 +506,11 @@ export class HindsightApi {
489
506
  if (qs) url += `?${qs}`;
490
507
  }
491
508
 
492
- const init: RequestInit = { method, headers: this.#headers };
509
+ const init: RequestInit = {
510
+ method,
511
+ headers: this.#headers,
512
+ signal: withTimeoutSignal(HINDSIGHT_REQUEST_TIMEOUT_MS, opts?.signal),
513
+ };
493
514
  if (opts?.body !== undefined) {
494
515
  init.body = JSON.stringify(pruneUndefined(opts.body));
495
516
  }
@@ -498,11 +519,10 @@ export class HindsightApi {
498
519
  try {
499
520
  response = await fetch(url, init);
500
521
  } catch (err) {
501
- throw new HindsightError(
502
- `${operation} request failed: ${err instanceof Error ? err.message : String(err)}`,
503
- undefined,
504
- err,
505
- );
522
+ const message = isTimeoutError(err)
523
+ ? `${operation} request timed out after 30s`
524
+ : `${operation} request failed: ${err instanceof Error ? err.message : String(err)}`;
525
+ throw new HindsightError(message, undefined, err);
506
526
  }
507
527
 
508
528
  if (opts?.allow404 && response.status === 404) {