howtofixcli 0.7.23 → 0.7.25

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 (167) hide show
  1. package/dist/account-credential.js +136 -0
  2. package/dist/acp.js +234 -0
  3. package/dist/activity.js +124 -0
  4. package/dist/agent-defs.js +237 -0
  5. package/dist/agent-limiter.js +68 -0
  6. package/dist/agents.js +554 -0
  7. package/dist/anim.js +361 -0
  8. package/dist/args.js +250 -0
  9. package/dist/at-file.js +28 -0
  10. package/dist/brand-extension.js +87 -0
  11. package/dist/brand.js +76 -0
  12. package/dist/browser.js +58 -0
  13. package/dist/cache-report.js +139 -0
  14. package/dist/checkpoint.js +437 -0
  15. package/dist/claims.js +167 -0
  16. package/dist/cli-auth.js +142 -0
  17. package/dist/cli-checkin.js +121 -0
  18. package/dist/cli-credentials.js +254 -0
  19. package/dist/clipboard.js +149 -0
  20. package/dist/collapse.js +129 -0
  21. package/dist/commands.js +381 -0
  22. package/dist/compaction-guard.js +81 -0
  23. package/dist/connect-prompt.js +22 -0
  24. package/dist/context-extension.js +17 -0
  25. package/dist/dcp/commands.js +172 -0
  26. package/dist/dcp/compress-state.js +139 -0
  27. package/dist/dcp/compress.js +329 -0
  28. package/dist/dcp/dcp-extension.js +295 -0
  29. package/dist/dcp/dedup.js +93 -0
  30. package/dist/dcp/ids.js +189 -0
  31. package/dist/dcp/index.js +25 -0
  32. package/dist/dcp/middle-prune.js +76 -0
  33. package/dist/dcp/prompts.js +68 -0
  34. package/dist/dcp/protected-content.js +221 -0
  35. package/dist/dcp/protected.js +161 -0
  36. package/dist/dcp/prune.js +418 -0
  37. package/dist/dcp/state.js +176 -0
  38. package/dist/errors.js +63 -0
  39. package/dist/extensions.js +48 -0
  40. package/dist/fatal.js +35 -0
  41. package/dist/feed.js +498 -0
  42. package/dist/fs-atomic.js +54 -0
  43. package/dist/gateway-errors.js +107 -0
  44. package/dist/git.js +230 -0
  45. package/dist/goal-extension.js +88 -0
  46. package/dist/goal.js +222 -0
  47. package/dist/hooks-extension.js +176 -0
  48. package/dist/hooks.js +443 -0
  49. package/dist/howtofix-account.js +834 -0
  50. package/dist/hud-line.js +158 -0
  51. package/dist/hwid.js +130 -0
  52. package/dist/interrupt.js +60 -0
  53. package/dist/json-events.js +50 -0
  54. package/dist/level-style.js +112 -0
  55. package/dist/loop-guard.js +95 -0
  56. package/dist/lsp-tools.js +32 -0
  57. package/dist/lsp.js +429 -0
  58. package/dist/main.js +573 -98
  59. package/dist/mcp-config.js +85 -0
  60. package/dist/mcp-lifecycle.js +147 -0
  61. package/dist/model-catalog.js +217 -0
  62. package/dist/model-facts.js +35 -0
  63. package/dist/model-resolve.js +65 -0
  64. package/dist/notes.js +78 -0
  65. package/dist/oauth-login.js +568 -0
  66. package/dist/paths.js +61 -0
  67. package/dist/permissions.js +298 -0
  68. package/dist/plan-mode.js +363 -0
  69. package/dist/preset-command.js +227 -0
  70. package/dist/preset-extension.js +35 -0
  71. package/dist/preset-mcp.js +292 -0
  72. package/dist/presets/mcp/rea/preset.json +28 -0
  73. package/dist/presets/mcp/rea/prompt.md +5 -0
  74. package/dist/presets/mcp/rea/skills/mcp-rea/SKILL.md +6 -0
  75. package/dist/presets/mcp/remotion/preset.json +27 -0
  76. package/dist/presets/mcp/remotion/prompt.md +7 -0
  77. package/dist/presets/mcp/remotion/skills/mcp-remotion/SKILL.md +6 -0
  78. package/dist/presets.js +727 -0
  79. package/dist/prompt-files.js +103 -0
  80. package/dist/prompt-text.js +41 -0
  81. package/dist/proxy.js +137 -0
  82. package/dist/quality/ast.js +196 -0
  83. package/dist/quality/cognitive.js +83 -0
  84. package/dist/quality/cyclomatic.js +30 -0
  85. package/dist/quality/halstead.js +83 -0
  86. package/dist/quality/index.js +18 -0
  87. package/dist/quality/length.js +3 -0
  88. package/dist/quality/maintainability.js +15 -0
  89. package/dist/quality/nesting.js +57 -0
  90. package/dist/quality/report.js +803 -0
  91. package/dist/quality/smells.js +67 -0
  92. package/dist/quality-tool.js +63 -0
  93. package/dist/read-only-commands.js +597 -0
  94. package/dist/repo-map.js +292 -0
  95. package/dist/roblox-connect.js +125 -0
  96. package/dist/rpc.js +91 -0
  97. package/dist/rtk.js +193 -0
  98. package/dist/sandbox.js +105 -0
  99. package/dist/schema.js +214 -0
  100. package/dist/serve.js +50 -0
  101. package/dist/session-lifecycle.js +19 -0
  102. package/dist/session-retention.js +111 -0
  103. package/dist/session.js +460 -0
  104. package/dist/sessions.js +100 -0
  105. package/dist/settings.js +148 -0
  106. package/dist/skills.js +35 -0
  107. package/dist/slop-tool.js +96 -0
  108. package/dist/spend-cap.js +43 -0
  109. package/dist/subagent.js +171 -0
  110. package/dist/tar.js +146 -0
  111. package/dist/theme.js +79 -0
  112. package/dist/thinking.js +45 -0
  113. package/dist/todo.js +114 -0
  114. package/dist/tool-loadout.js +102 -0
  115. package/dist/tool-render.js +769 -0
  116. package/dist/tools.js +176 -0
  117. package/dist/ui.js +4930 -0
  118. package/dist/update.js +551 -0
  119. package/dist/usage-segment.js +74 -0
  120. package/dist/web-tools.js +46 -0
  121. package/dist/web.js +265 -0
  122. package/dist/welcome.js +179 -0
  123. package/dist/workflow-engine.js +246 -0
  124. package/dist/workflow-progress.js +446 -0
  125. package/dist/workflow-view.js +195 -0
  126. package/dist/workflow.js +349 -0
  127. package/package.json +62 -15
  128. package/dist/chunk-27TRKD65.js +0 -2
  129. package/dist/chunk-3FYCFLNK.js +0 -1
  130. package/dist/chunk-3LZR7OEU.js +0 -1
  131. package/dist/chunk-3XL2557N.js +0 -6
  132. package/dist/chunk-56SEAGAQ.js +0 -1
  133. package/dist/chunk-7CHDNTH6.js +0 -1
  134. package/dist/chunk-7QU425F4.js +0 -1
  135. package/dist/chunk-AGGIYXN7.js +0 -2
  136. package/dist/chunk-AJJCVTO4.js +0 -2
  137. package/dist/chunk-AUDMRGWF.js +0 -189
  138. package/dist/chunk-DAPXGSGD.js +0 -12
  139. package/dist/chunk-DL5PTYAO.js +0 -5
  140. package/dist/chunk-DOD7MUGX.js +0 -1
  141. package/dist/chunk-FPXVDP32.js +0 -8
  142. package/dist/chunk-GO2QIQ5P.js +0 -1
  143. package/dist/chunk-GQHBNOPS.js +0 -2
  144. package/dist/chunk-GXB7RZHH.js +0 -1
  145. package/dist/chunk-I6CAUN2V.js +0 -1
  146. package/dist/chunk-ILDDDLRC.js +0 -1
  147. package/dist/chunk-JCBRLX44.js +0 -2
  148. package/dist/chunk-JFYVRWJI.js +0 -2
  149. package/dist/chunk-JX4PCVYT.js +0 -34
  150. package/dist/chunk-LAHVHRCS.js +0 -2
  151. package/dist/chunk-LSJC73DG.js +0 -1
  152. package/dist/chunk-MQVP243W.js +0 -117
  153. package/dist/chunk-N345KWWK.js +0 -1
  154. package/dist/chunk-ONZLNFQ2.js +0 -3
  155. package/dist/chunk-ORJ2QZBG.js +0 -3
  156. package/dist/chunk-P6WSWRXK.js +0 -5
  157. package/dist/chunk-PCALBZOO.js +0 -2
  158. package/dist/chunk-TSEKPGK4.js +0 -1
  159. package/dist/chunk-UTYFLLSM.js +0 -1
  160. package/dist/chunk-V3R4FYTJ.js +0 -1
  161. package/dist/chunk-V656XVXV.js +0 -8
  162. package/dist/chunk-WKUJ7CFO.js +0 -1
  163. package/dist/chunk-WOT6VMZA.js +0 -1
  164. package/dist/chunk-WUPU6LTD.js +0 -4
  165. package/dist/chunk-WX3V5P7U.js +0 -5
  166. package/dist/chunk-YJSES7RK.js +0 -1
  167. package/dist/chunk-YW7NORH3.js +0 -2
package/dist/main.js CHANGED
@@ -1,99 +1,574 @@
1
1
  #!/usr/bin/env node
2
- import{b as I,d as W,e as B}from"./chunk-3LZR7OEU.js";import{b as U}from"./chunk-YW7NORH3.js";import{a as F,b as R}from"./chunk-AGGIYXN7.js";import{D as M,K as N}from"./chunk-V656XVXV.js";import"./chunk-UTYFLLSM.js";import{a as n,b as y}from"./chunk-P6WSWRXK.js";import"./chunk-WOT6VMZA.js";var L={rpc:!0,acp:!0},D={off:!0,minimal:!0,low:!0,medium:!0,high:!0,xhigh:!0,max:!0},G={suggest:!0,"auto-edit":!0,"full-auto":!0},H={text:!0,json:!0,"stream-json":!0};function fe(c){let e=Number(c);if(!Number.isFinite(e)||e<=0)throw new Error(`--max-budget-usd needs a positive number of dollars, got '${c}'`);return e}function $(c,e,p){let d=c[e];if(d?.includes("="))return d.slice(d.indexOf("=")+1);let l=c[e+1];if(!l||l.startsWith("-"))throw new Error(`${p} needs a value`);return c.splice(e+1,1),l}function X(c){let e={print:!1,continueSession:!1,noSession:!1,help:!1,version:!1,update:!1,checkUpdate:!1,outputFormat:"text",dangerouslySkipPermissions:!1,sandbox:!1,prompt:""},p=[],d=[...c];for(let l=0;l<d.length;l++){let s=d[l]??"";if(s==="--"){p.push(...d.slice(l+1));break}if(s==="-h"||s==="--help")e.help=!0;else if(s==="-v"||s==="--version")e.version=!0;else if(s==="-p"||s==="--print")e.print=!0;else if(s==="-c"||s==="--continue")e.continueSession=!0;else if(s==="--no-session")e.noSession=!0;else if(s==="--sandbox")e.sandbox=!0;else if(s==="--update")e.update=!0;else if(s==="--check-update")e.checkUpdate=!0;else if(s==="-r"||s==="--resume")e.continueSession=!0;else if(s==="--session"||s.startsWith("--session="))e.sessionPath=$(d,l,"--session");else if(s==="-m"||s==="--model"||s.startsWith("--model="))e.model=$(d,l,"--model");else if(s==="--thinking"||s.startsWith("--thinking="))e.thinking=$(d,l,"--thinking");else if(s==="--approval"||s.startsWith("--approval="))e.approval=$(d,l,"--approval");else if(s==="--output-format"||s.startsWith("--output-format="))e.outputFormat=$(d,l,"--output-format");else if(s==="--max-budget-usd"||s.startsWith("--max-budget-usd="))e.maxBudgetUsd=fe($(d,l,"--max-budget-usd"));else if(s==="--mode"||s.startsWith("--mode=")){let u=$(d,l,"--mode");if(!L[u])throw new Error(`--mode must be one of: ${Object.keys(L).join(", ")}`);e.mode=u}else if(s==="--acp")e.mode="acp";else if(s==="--dangerously-skip-permissions")e.dangerouslySkipPermissions=!0;else{if(s.startsWith("-"))throw new Error(`unknown option ${s}`);p.push(s)}}if(e.thinking&&!D[e.thinking])throw new Error(`thinking must be one of: ${Object.keys(D).join(", ")}`);if(e.approval&&!G[e.approval])throw new Error(`approval must be one of: ${Object.keys(G).join(", ")}`);if(!H[e.outputFormat])throw new Error(`output-format must be one of: ${Object.keys(H).join(", ")}`);if(e.outputFormat!=="text"&&!e.print)throw new Error(`--output-format ${e.outputFormat} is a machine contract; it needs --print`);if(e.dangerouslySkipPermissions&&!e.print)throw new Error("--dangerously-skip-permissions is for unattended runs; it needs --print. interactively, pick an approval mode instead (/approval, or --approval)");if(e.prompt=p.join(" ").trim(),e.update&&e.checkUpdate)throw new Error("--update installs; --check-update only looks. pick one");if((e.update||e.checkUpdate)&&(e.print||e.continueSession||e.sessionPath||e.prompt))throw new Error("--update and --check-update act on the installation, not a session; they take no prompt");if(e.continueSession&&e.noSession)throw new Error("--continue and --no-session cannot be combined");if(e.sessionPath&&e.noSession)throw new Error("--session and --no-session cannot be combined");if(e.sessionPath&&e.continueSession)throw new Error("--session and --continue cannot be combined");if(e.print&&(e.continueSession||e.sessionPath))throw new Error("--print runs one prompt in a throwaway session; it cannot be combined with --continue or --session");if(e.mode&&(e.print||e.prompt))throw new Error(`--mode ${e.mode} is a protocol server: prompts arrive over the protocol, not the command line`);if(e.mode&&(e.continueSession||e.sessionPath))throw new Error(`--mode ${e.mode} opens its own sessions; it cannot be combined with --continue or --session`);if(e.sessionPath&&!e.sessionPath.endsWith(".jsonl"))throw new Error(`--session expects a session file, got '${e.sessionPath}'`);return e}function C(){return`${n} ${y}
3
- coding agent. layout is a workbench, not a chat log.
4
-
5
- usage:
6
- howtofix [options] [prompt]
7
- howtofix -p "explain this repo"
8
- howtofix acp serve the Agent Client Protocol on stdio
9
- howtofix --mode rpc serve JSON-line commands on stdio
10
- howtofix preset new|check|pack author a preset
11
- howtofix doctor|models|sessions inspect the installation
12
- howtofix sessions --search <text> find a session by what was said in it
13
- howtofix sessions --export <n> print session n from the list as markdown
14
-
15
- options:
16
- -p, --print run one prompt and print the answer
17
- -c, --continue resume the latest session in this folder
18
- -r, --resume alias for --continue
19
- --session <file> run inside a specific saved session file
20
- --no-session do not write a session file
21
- -m, --model <id> provider/model, or a fuzzy model id
22
- --thinking <lv> off | minimal | low | medium | high | xhigh | max
23
- --approval <m> suggest | auto-edit | full-auto
24
- --output-format <f> text | json | stream-json
25
- text for a person; json writes the final result only;
26
- stream-json writes every event as it happens
27
- --max-budget-usd <n> stop the run once it has spent n dollars
28
- --mode <m> rpc | acp: serve a protocol on stdin/stdout, no terminal
29
- --acp same as --mode acp
30
- --sandbox run the whole session inside a filesystem sandbox:
31
- the project is writable, the rest is read-only.
32
- needs bubblewrap (Linux) or sandbox-exec (macOS)
33
- --dangerously-skip-permissions
34
- run everything, including commands otherwise refused.
35
- unattended runs only; pairs with --print
36
- --update install the newest release and exit
37
- --check-update say whether a newer release exists
38
- -h, --help
39
- -v, --version
40
-
41
- keys:
42
- enter send
43
- shift+enter new line
44
- ctrl+c stop the run, or quit when idle
45
- ctrl+d quit
46
- ctrl+l jump to the latest line
47
- ctrl+o clear the screen (history is kept)
48
- ctrl+t raise the thinking level (wraps to off)
49
- ctrl+shift+t lower the thinking level
50
- esc clear the draft
51
- ctrl+v paste from the clipboard
52
- pgup / pgdn scroll the transcript a page
53
- home / end jump to the top / back to the latest line
54
- wheel scroll the transcript
55
-
56
- commands:
57
- /init /model /providers /thinking /approval /login /connect /logout /new
58
- /resume /compact /name /stats /rtk /diff /commit /undo /rewind /review /tools /export
59
- /copy /agents /goal /plan /workflow /session /help /quit /exit
60
- /dcp /context /decompress /sweep
61
- /update /usage /sync
62
- /skills /preset /settings /trust /proxy /roblox
63
- /roblox login|logout|status|tunnel manages the Roblox connection: sign in with Roblox
64
- (no API key), see what is connected, and link this machine to the hosted workspace.
65
-
66
- /stats shows session tokens, cost, and the prompt-cache hit rate; a high read
67
- total alone hides re-billed context, so the rate is what tells you the truth.
68
- /rtk compresses long tool output (logs, diffs, greps) before the model reads it.
69
- Off by default: /rtk on to save tokens when skimming, /rtk off to keep every line.
70
- Nothing is invented by it \u2014 it drops redundant lines and says how many it dropped,
71
- and never touches a failed command or one already truncated.
72
-
73
- /preset lists skills bundled with this build and skills from the registry, by
74
- category; picking one downloads it into the project and loads it as a skill.
75
- An integration preset installs disabled: its skills and MCP server stay dormant
76
- until /preset enable <name>, and /preset disable <name> takes the server back
77
- out of the live session. /roblox reports that state for RoToolbox in one line.
78
- /skills lists what is loaded now.
79
-
80
- /proxy <url> routes requests through a proxy, /proxy off stops, /proxy env uses
81
- HTTPS_PROXY / HTTP_PROXY / ALL_PROXY; /proxy alone shows what is set.
82
-
83
- /agents <id> <message> messages a parked agent; /agents <id> shows one.
84
-
85
- a line starting with ! runs in the project shell.
86
-
87
- an npm install checks for a newer release at startup and can install it itself;
88
- /update does it on demand. config and sessions live in ~/.howtofix/agent.
89
- /connect signs in to howtofix.id in your browser, and falls back to a pasted
90
- token; /login takes another provider's key.
91
- `}function ge(c,e=!1){let{message:p}=F(c);if(!e)return p;let d=c instanceof Error?c.stack:void 0;return d&&d.includes(`
92
- `)?`${p}
93
- ${d}`:p}function V(c,e){let p=e?.debug??process.env.HOWTOFIX_DEBUG==="1",d=e?.exit??(s=>process.exit(s)),l=s=>{console.error(`${c}: ${ge(s,p)}`),d(1)};process.on("uncaughtException",l),process.on("unhandledRejection",l)}function we(){try{return{cwd:process.cwd()}}catch{return{error:"the current directory no longer exists (it was deleted while the shell stayed in it). cd to a directory that exists, or pass one on the command line."}}}function be(){process.stdout.on("error",c=>{throw c.code==="EPIPE"&&process.exit(0),c})}async function ke(){be(),N();let c=performance.now(),e=we();if("error"in e){console.error(`${n}: ${e.error}`),process.exitCode=1;return}let p=e.cwd,d=process.env.HXF_FAKE_NODE??process.versions.node,[l,s]=d.split(".").map(o=>Number.parseInt(o,10));if(!Number.isFinite(l)||l<22||l===22&&Number.isFinite(s)&&s<19){console.error(`${n}: Node ${d} is too old. Node 22.19 or newer is required (run "node --version" to check yours), because undici needs zstd, which Node added in 22.19. Upgrade Node and run this again.`),process.exitCode=1;let o=process.argv.slice(2);if(o.some(a=>a==="-h"||a==="--help")){console.log(C());return}if(o.some(a=>a==="-v"||a==="--version")){console.log(`${n} ${y}`);return}return}let u=process.argv.slice(2);if(u[0]==="doctor"||u[0]==="models"||u[0]==="sessions"){let o=u[0];try{let a=await import("./chunk-DL5PTYAO.js");o==="doctor"?process.exitCode=await a.runDoctor():o==="models"?process.exitCode=a.runModels():process.exitCode=a.runSessions(u.slice(1))}catch(a){R(n,o,a),process.exitCode=1}return}let q=u[0]==="acp"?["--acp",...u.slice(1)]:u,_;try{_=X(q)}catch(o){let a=o instanceof Error?o.message:String(o);console.error(`${n}: ${a}
94
- ${C()}`),process.exitCode=2;return}if(_.print&&!_.prompt){console.error(`${n}: --print needs a prompt`),process.exitCode=2;return}let{repairGatewayModels:J,ensureGatewaySettings:K,repairModelFacts:Y,syncGatewayModelsOnStart:z,checkAccount:Q,fetchGatewayPrices:Z}=await import("./chunk-YJSES7RK.js"),{readCliCredential:ee,renewCliCredentialIfNeeded:se}=await import("./chunk-7QU425F4.js"),{AGENT_DIR:b}=await import("./chunk-GXB7RZHH.js"),j=J(b);j>0&&console.error(`${n}: repaired ${j} howtofix model(s) that the gateway would have rejected`);let P,O;try{let o=ee(b);if(o){let x=(await se(b,o)).credential.accessToken,[m,h]=await Promise.all([Z(x),Q(x)]);m.size>0&&(P=m),h.ok&&(O=h.models)}}catch{}let E=await Y(b,P?{prices:P}:void 0);if(E.changed>0?console.error(`${n}: filled in the context window, reasoning flag and price for ${E.changed} model(s)`):E.message&&console.error(`${n}: model catalogue unavailable (${E.message}); context windows stay at their conservative defaults`),K(b)&&console.error(`${n}: set httpIdleTimeoutMs in settings.json so a stalled provider recovers instead of hanging`),O)try{let o=await z(b,O,P);o.added>0&&console.error(`${n}: added ${o.added} model(s) this account can now reach`)}catch{}let t=_;if(t.help){console.log(C());return}if(t.version){console.log(`${n} ${y}`);let{cliSigningKeyMatchesPanel:o,cliSigningKeyFingerprint:a}=await import("./chunk-I6CAUN2V.js");o()||(console.error(`${n}: this build's CLI signing key (${a()}) does not match the key the panel verifies against, so sign-in will be refused.`),process.exitCode=1);return}if(t.sandbox&&!process.env.HOWTOFIX_SANDBOXED){let{detectSandbox:o,enterSandbox:a,describeSandbox:x}=await import("./chunk-N345KWWK.js"),m=o();if(!m){console.error(`${n}: --sandbox needs a working bubblewrap (Linux) or sandbox-exec (macOS). Neither could be used here \u2014 on Linux this usually means unprivileged user namespaces are disabled.`),process.exitCode=1;return}console.error(x(m)),process.exitCode=a(m,{cwd:p,agentDir:M,argv:[process.execPath,...process.argv.slice(1)]});return}let{installProxy:te}=await import("./chunk-WKUJ7CFO.js");if(te(M),t.update||t.checkUpdate){let{PACKAGE_NAME:o,checkForUpdate:a,installUpdate:x,isInstalledCopy:m}=await import("./chunk-7CHDNTH6.js"),h=await a(y);if(!h.latest){console.error(`${n}: ${h.message}`),process.exitCode=t.update?1:0;return}if(console.log(h.message),t.checkUpdate||!h.available)return;if(!m()){console.error(`${n}: this copy was not installed by npm, so npm cannot replace it. install it with: npm install -g ${o}`),process.exitCode=1;return}let v=await x({version:h.latest});console.log(v.message),v.ok||(process.exitCode=1);return}let A=process.argv.slice(2);if(A[0]==="preset"){let{runPresetCommand:o}=await import("./chunk-FPXVDP32.js");process.exitCode=o(A.slice(1));return}let{expandFileReferences:oe}=await import("./chunk-TSEKPGK4.js"),{isApprovalMode:ne,setApprovalMode:re,setDangerousBypass:ie,isDangerousBypass:ae}=await import("./chunk-56SEAGAQ.js"),{openSession:de}=await import("./chunk-JX4PCVYT.js");re(t.approval&&ne(t.approval)?t.approval:"full-auto");let{readWorkbenchSettings:ce}=await import("./chunk-V3R4FYTJ.js");if((t.dangerouslySkipPermissions||ce(b).skipPermissions)&&ie(!0),ae()&&console.error(`${n}: permission checks are OFF \u2014 critical commands (filesystem wipes, fork bombs, host shutdown) and the repeated-call guard are NOT enforced for this run. Turn it off with /settings, or by removing skipPermissions from settings.json.`),t.mode){let{serveProtocol:o}=await import("./chunk-GO2QIQ5P.js");await o(t.mode,{cwd:p,model:t.model,thinking:t.thinking});return}let T=o=>de({cwd:p,model:t.model,thinking:t.thinking,continueSession:o.continueSession??!1,noSession:o.noSession??!1,sessionPath:o.sessionPath,interactive:!t.print}),r=await T({continueSession:t.continueSession,sessionPath:t.sessionPath,noSession:t.noSession||t.print});if(t.print&&r.note&&console.error(r.note),t.maxBudgetUsd!==void 0&&!r.priced){console.error(`${n}: ${B(r.modelLabel,b)}`),await r.dispose(),process.exitCode=2;return}if(t.print){let o=t.prompt.trimStart();if(o.startsWith("/")){let f=o.slice(1).split(/\s+/)[0]??"",{skillInvocation:S}=await import("./chunk-ILDDDLRC.js"),g=r.session.resourceLoader.getSkills().skills.map(i=>i.name);if(!(S(o,g)!==void 0)){let{slashCommands:i}=await import("./chunk-MQVP243W.js");if(i(r.session).some(w=>w.name===f)){console.error(`${n}: /${f} runs in the TUI only \u2014 --print sends prompts straight to the model. Run \`howtofix\` without --print for interactive commands.`),await r.dispose(),process.exitCode=2;return}}}let a=t.outputFormat==="stream-json",x=t.outputFormat==="json",m=a||x;if(a){let f={type:"system",subtype:"init",cwd:p,session_id:r.session.sessionId,model:r.modelLabel,output_format:t.outputFormat,max_budget_usd:t.maxBudgetUsd??null,dangerously_skip_permissions:t.dangerouslySkipPermissions,cli_version:y};process.stdout.write(`${JSON.stringify(f)}
95
- `)}let h,v,he=()=>{if(t.maxBudgetUsd===void 0||v!==void 0)return;let f=r.session.getSessionStats().cost;f>t.maxBudgetUsd&&(v={spent:f},r.session.abort())};try{let f;h=r.session.subscribe(i=>{if(a){let w=U(i);w!==void 0&&process.stdout.write(w)}if(i.type==="turn_end"&&i.message?.role==="assistant"){let w=i.message;w.stopReason==="error"||w.stopReason==="aborted"||w.errorMessage?f={stopReason:w.stopReason??"error",errorMessage:w.errorMessage}:f=void 0}he()}),await r.session.prompt(oe(t.prompt,p));let S=r.session.getLastAssistantText()??"";if(v){if(console.error(`${n}: ${W(v.spent,t.maxBudgetUsd)}`),m){let i=r.session.getSessionStats();process.stdout.write(`${JSON.stringify({type:"result",subtype:"error_max_budget_usd",is_error:!0,result:S,session_id:r.session.sessionId,duration_ms:Math.round(performance.now()-c),num_turns:i.assistantMessages,total_cost_usd:i.cost,usage:{input_tokens:i.tokens.input,output_tokens:i.tokens.output,cache_read_input_tokens:i.tokens.cacheRead,cache_creation_input_tokens:i.tokens.cacheWrite}})}
96
- `)}process.exitCode=1;return}let g=r.session.state.messages.at(-1),k=f??(g?.role==="assistant"&&(g.stopReason==="error"||g.stopReason==="aborted"||g.errorMessage)?{stopReason:g.stopReason??"error",errorMessage:g.errorMessage}:void 0);if(m){let i=r.session.getSessionStats();process.stdout.write(`${JSON.stringify({type:"result",subtype:k?k.stopReason==="aborted"?"error_aborted":"error_during_execution":"success",is_error:k!==void 0,result:S,session_id:r.session.sessionId,duration_ms:Math.round(performance.now()-c),num_turns:i.assistantMessages,total_cost_usd:i.cost,usage:{input_tokens:i.tokens.input,output_tokens:i.tokens.output,cache_read_input_tokens:i.tokens.cacheRead,cache_creation_input_tokens:i.tokens.cacheWrite},stop_reason:k?.stopReason??(g?.role==="assistant"?g.stopReason:void 0)})}
97
- `)}else process.stdout.write(S.endsWith(`
98
- `)?S:`${S}
99
- `);if(k&&(process.exitCode=1,k.errorMessage)){console.error(`${n}: ${k.errorMessage}`);let i=I(k.errorMessage);i!==void 0&&console.error(`${n}: ${i}`)}}finally{h?.(),await r.dispose()}return}if(!process.stdin.isTTY||!process.stdout.isTTY){console.error(`${n}: interactive mode needs a terminal. use --print for pipes.`),await r.dispose(),process.exitCode=2;return}let{runWorkbench:le}=await import("./chunk-MQVP243W.js"),{autoUpdateEnabled:pe,isInstalledCopy:ue}=await import("./chunk-7CHDNTH6.js"),me=ue()&&pe(`${b}/settings.json`);await le({cwd:p,modelLabel:r.modelLabel,session:r.session,setCompressToolOutput:r.setCompressToolOutput,initialPrompt:t.prompt||void 0,note:r.note,maxBudgetUsd:t.maxBudgetUsd,update:me?{current:y,install:!0}:void 0,reopen:T})}V(n);ke().catch(c=>{R(n,void 0,c),process.exitCode=1});
2
+ import { helpText, parseArgs } from "./args.js";
3
+ import { APP_TITLE, APP_VERSION } from "./brand.js";
4
+ import { installCrashHandlers } from "./fatal.js";
5
+ import { reportError } from "./errors.js";
6
+ import { AGENT_DIR, pinAgentDir } from "./paths.js";
7
+ import { jsonLine } from "./json-events.js";
8
+ import { diagnoseGatewayFailure } from "./gateway-errors.js";
9
+ import { spendCapMessage, unpricedBudgetMessage } from "./spend-cap.js";
10
+ /**
11
+ * The working directory to run in, or a reason the process cannot start.
12
+ *
13
+ * `process.cwd()` throws `ENOENT` when the directory it was started in has been
14
+ * deleted — a real case (the scratch dir is cleaned up mid-session) and an
15
+ * uncaught crash with a stack trace, since the throw happens while building the
16
+ * session options. Report it as a sentence instead.
17
+ */
18
+ function workingDirectory() {
19
+ try {
20
+ return { cwd: process.cwd() };
21
+ }
22
+ catch {
23
+ return {
24
+ error: "the current directory no longer exists (it was deleted while the shell stayed in it). " +
25
+ "cd to a directory that exists, or pass one on the command line.",
26
+ };
27
+ }
28
+ }
29
+ /**
30
+ * A closed stdout (`howtofix --version | head -1`) makes every later write throw
31
+ * `EPIPE`, which the top-level handler used to report as a crash with a stack
32
+ * trace. A pipe that went away is not an error the user needs to see.
33
+ */
34
+ function exitQuietlyOnEpipe() {
35
+ process.stdout.on("error", (error) => {
36
+ if (error.code === "EPIPE")
37
+ process.exit(0);
38
+ throw error;
39
+ });
40
+ }
41
+ async function main() {
42
+ exitQuietlyOnEpipe();
43
+ pinAgentDir();
44
+ const startedAt = performance.now();
45
+ const here = workingDirectory();
46
+ if ("error" in here) {
47
+ console.error(`${APP_TITLE}: ${here.error}`);
48
+ process.exitCode = 1;
49
+ return;
50
+ }
51
+ const cwd = here.cwd;
52
+ // A Node too old to be supported fails deep inside a dependency instead of here.
53
+ // `undici` 8 reaches `zlib.createZstdDecompress` at module load, and that function
54
+ // only exists from Node 22.19 — so on Node 20 the user gets
55
+ // `TypeError: Cannot read properties of undefined (reading 'bind')` with a stack
56
+ // trace pointing at `undici/lib/interceptor/decompress.js`, which explains nothing
57
+ // and names no fix. `engines` in `package.json` is not enough on its own: npm's
58
+ // `engine-strict` is false by default, so the install succeeds with a warning the
59
+ // user never sees.
60
+ //
61
+ // `--help` and `--version` are deliberately still served. They exit before `undici`
62
+ // is ever loaded, so they cannot hit the bug, and the user who most needs to read the
63
+ // help is the one on the runtime the CLI cannot run — refusing to answer leaves them
64
+ // with a command that looks broken and no way to learn why. Everything else is
65
+ // refused, because a session that cannot start the agent is not a usable CLI and
66
+ // failing here is what turns a stack trace inside a dependency into a sentence.
67
+ //
68
+ // Gated behind an env var so the check itself is what breaks under test, not the
69
+ // runtime: `HXF_FAKE_NODE` lets a check drive this branch on a modern Node. The
70
+ // same value feeds the message, so a test sees the version it asked about rather
71
+ // than the one it happens to be running on.
72
+ //
73
+ // The exit code says whether the ENVIRONMENT is supported, not whether output was
74
+ // produced: an old Node exits non-zero even for `--help`, so a script reading the
75
+ // code is not told the runtime is fine.
76
+ const reportedVersion = process.env.HXF_FAKE_NODE ?? process.versions.node;
77
+ const [major, minor] = reportedVersion.split(".").map((part) => Number.parseInt(part, 10));
78
+ if (!Number.isFinite(major) || major < 22 || (major === 22 && Number.isFinite(minor) && minor < 19)) {
79
+ // Same sentence on both streams, and stderr carries it in the informational case
80
+ // too: `--version` is often parsed by a script, and a warning on stdout would
81
+ // corrupt the value it is reading.
82
+ console.error(`${APP_TITLE}: Node ${reportedVersion} is too old. Node 22.19 or newer is required ` +
83
+ `(run "node --version" to check yours), because undici needs zstd, which Node added in 22.19. ` +
84
+ `Upgrade Node and run this again.`);
85
+ process.exitCode = 1;
86
+ const argv = process.argv.slice(2);
87
+ // The two flags that work on any runtime, served from here so the account repair
88
+ // below — which makes an HTTP call, and therefore loads undici — is never reached
89
+ // on a runtime that cannot load it.
90
+ if (argv.some((token) => token === "-h" || token === "--help")) {
91
+ console.log(helpText());
92
+ return;
93
+ }
94
+ if (argv.some((token) => token === "-v" || token === "--version")) {
95
+ console.log(`${APP_TITLE} ${APP_VERSION}`);
96
+ return;
97
+ }
98
+ return;
99
+ }
100
+ // Questions about the installation, answered without a session, a model, or
101
+ // any of the repairs below. They run first on purpose: `howtofix models` used
102
+ // to rewrite settings.json and print two notices about it before listing a
103
+ // single model, which reads like the command did something it did not.
104
+ const subArgv = process.argv.slice(2);
105
+ if (subArgv[0] === "doctor" || subArgv[0] === "models" || subArgv[0] === "sessions") {
106
+ const context = subArgv[0];
107
+ try {
108
+ const commands = await import("./commands.js");
109
+ if (context === "doctor")
110
+ process.exitCode = await commands.runDoctor();
111
+ else if (context === "models")
112
+ process.exitCode = commands.runModels();
113
+ else
114
+ process.exitCode = commands.runSessions(subArgv.slice(1));
115
+ }
116
+ catch (error) {
117
+ reportError(APP_TITLE, context, error);
118
+ process.exitCode = 1;
119
+ }
120
+ return;
121
+ }
122
+ // Arguments are read and checked before the start-up repairs. A typo must not
123
+ // rewrite settings.json, print a hint about a file the user never mentioned,
124
+ // or reach the network: the answer to "you forgot the prompt" is one line.
125
+ // `howtofix acp` is the subcommand spelling of `--acp`, dispatched here rather
126
+ // than in `parseArgs` so the word is not mistaken for the start of a prompt.
127
+ const argvForParse = subArgv[0] === "acp" ? ["--acp", ...subArgv.slice(1)] : subArgv;
128
+ let earlyArgs;
129
+ try {
130
+ earlyArgs = parseArgs(argvForParse);
131
+ }
132
+ catch (error) {
133
+ const message = error instanceof Error ? error.message : String(error);
134
+ console.error(`${APP_TITLE}: ${message}\n${helpText()}`);
135
+ process.exitCode = 2;
136
+ return;
137
+ }
138
+ if (earlyArgs.print && !earlyArgs.prompt) {
139
+ console.error(`${APP_TITLE}: --print needs a prompt`);
140
+ process.exitCode = 2;
141
+ return;
142
+ }
143
+ // Repair a howtofix gateway entry that predates `compat.supportsDeveloperRole`.
144
+ // The gateway refuses the `developer` role that the SDK selects for every
145
+ // reasoning model, so such an entry fails every prompt silently — the spinner
146
+ // turns and nothing is ever reported. `/connect` writes the flag, but only for
147
+ // models it adds, so an account connected earlier stays broken until something
148
+ // repairs the file. The repair is silent: it runs on every start and only says
149
+ // anything when it actually changed something. Imported lazily to keep
150
+ // `--version` and `--help` free of the model stack.
151
+ const { repairGatewayModels, ensureGatewaySettings, repairModelFacts, syncGatewayModelsOnStart, checkAccount, fetchGatewayPrices } = await import("./howtofix-account.js");
152
+ const { readCliCredential, renewCliCredentialIfNeeded } = await import("./cli-credentials.js");
153
+ const { AGENT_DIR: stateDir } = await import("./paths.js");
154
+ const repaired = repairGatewayModels(stateDir);
155
+ if (repaired > 0) {
156
+ console.error(`${APP_TITLE}: repaired ${repaired} howtofix model(s) that the gateway would have rejected`);
157
+ }
158
+ // The gateway's own price list, read before the repair below: the repair writes
159
+ // the numbers the picker shows, and a picker that calls a $2/M model free is
160
+ // worse than one that shows no price at all. One request, once per start, and
161
+ // only when there is a credential to make it with. The account's model list
162
+ // rides along on the same token — the re-sync further down needs it, and the
163
+ // two calls are independent.
164
+ let gatewayPrices;
165
+ let accountModels;
166
+ try {
167
+ const stored = readCliCredential(stateDir);
168
+ if (stored) {
169
+ const renewed = await renewCliCredentialIfNeeded(stateDir, stored);
170
+ const token = renewed.credential.accessToken;
171
+ const [prices, account] = await Promise.all([fetchGatewayPrices(token), checkAccount(token)]);
172
+ if (prices.size > 0)
173
+ gatewayPrices = prices;
174
+ if (account.ok)
175
+ accountModels = account.models;
176
+ }
177
+ }
178
+ catch {
179
+ // No price list is a picker without prices, not a failed start.
180
+ }
181
+ // Repair the numbers a synced model never got: a real context window and the
182
+ // reasoning flag that enables thinking. Both used to be guesses — 128k for
183
+ // every model, and no capability at all — so a model with a 1M window read as
184
+ // almost full and a reasoning model showed no thinking levels. The catalogue is
185
+ // cached next to the agent state, so this is a file read on a normal start and
186
+ // a fetch only when the cache is missing or stale.
187
+ const facts = await repairModelFacts(stateDir, gatewayPrices ? { prices: gatewayPrices } : undefined);
188
+ if (facts.changed > 0) {
189
+ console.error(`${APP_TITLE}: filled in the context window, reasoning flag and price for ${facts.changed} model(s)`);
190
+ }
191
+ else if (facts.message) {
192
+ // A catalogue that could not be read is not a failure — the models keep the
193
+ // conservative defaults — but it is worth one line, because otherwise the
194
+ // wrong numbers look like the model's own.
195
+ console.error(`${APP_TITLE}: model catalogue unavailable (${facts.message}); context windows stay at their conservative defaults`);
196
+ }
197
+ // The SDK's HTTP idle timeout defaults to 5 minutes, so a provider that goes
198
+ // quiet mid-request leaves a spinner and nothing else for that long. 90s lets
199
+ // the SDK's own retry recover while the user is still watching. Written only
200
+ // when the key is missing, so a value the user set is kept.
201
+ if (ensureGatewaySettings(stateDir)) {
202
+ console.error(`${APP_TITLE}: set httpIdleTimeoutMs in settings.json so a stalled provider recovers instead of hanging`);
203
+ }
204
+ // The account's model list follows its tier, and `models.json` is a snapshot:
205
+ // after a tier change the file still offered what the account could reach on
206
+ // the day it was written, and the only refresh was a manual `/sync`. Pull the
207
+ // list once per start when a sign-in exists and add whatever is new — the
208
+ // gateway filters by the account's own group, so a free account can never
209
+ // collect a VIP model this way. Silent unless something was added, and never
210
+ // fatal: an unreachable gateway must not stop the CLI from starting.
211
+ if (accountModels) {
212
+ try {
213
+ const synced = await syncGatewayModelsOnStart(stateDir, accountModels, gatewayPrices);
214
+ if (synced.added > 0) {
215
+ console.error(`${APP_TITLE}: added ${synced.added} model(s) this account can now reach`);
216
+ }
217
+ }
218
+ catch {
219
+ // A start-time re-sync that fails costs the previous list, which still
220
+ // works; `/sync` remains the manual way to try again.
221
+ }
222
+ }
223
+ const args = earlyArgs;
224
+ // These two exit before a session exists, so they must not load the SDK.
225
+ if (args.help) {
226
+ console.log(helpText());
227
+ return;
228
+ }
229
+ if (args.version) {
230
+ console.log(`${APP_TITLE} ${APP_VERSION}`);
231
+ // A build whose signing key does not match the panel's would fail every
232
+ // sign-in with a 401 that reads like a server fault. Say it here, where it
233
+ // costs nothing, rather than leaving it to be discovered in the login flow.
234
+ const { cliSigningKeyMatchesPanel, cliSigningKeyFingerprint } = await import("./cli-auth.js");
235
+ if (!cliSigningKeyMatchesPanel()) {
236
+ console.error(`${APP_TITLE}: this build's CLI signing key (${cliSigningKeyFingerprint()}) does not match ` +
237
+ `the key the panel verifies against, so sign-in will be refused.`);
238
+ process.exitCode = 1;
239
+ }
240
+ return;
241
+ }
242
+ // The sandbox re-executes this process once, before any session exists. The
243
+ // guard in the environment is what stops the child from wrapping itself again.
244
+ if (args.sandbox && !process.env.HOWTOFIX_SANDBOXED) {
245
+ const { detectSandbox, enterSandbox, describeSandbox } = await import("./sandbox.js");
246
+ const kind = detectSandbox();
247
+ if (!kind) {
248
+ console.error(`${APP_TITLE}: --sandbox needs a working bubblewrap (Linux) or sandbox-exec (macOS). ` +
249
+ `Neither could be used here — on Linux this usually means unprivileged user namespaces are disabled.`);
250
+ process.exitCode = 1;
251
+ return;
252
+ }
253
+ console.error(describeSandbox(kind));
254
+ process.exitCode = enterSandbox(kind, { cwd, agentDir: AGENT_DIR, argv: [process.execPath, ...process.argv.slice(1)] });
255
+ return;
256
+ }
257
+ // Route through a proxy before the first request goes out. Here rather than at
258
+ // the top of `main` because this is the first line that cannot exit early:
259
+ // `--version` and `--help` have already returned above, and loading the proxy
260
+ // agent for a command that makes no request is a cost with no purpose. The
261
+ // update check and the sign-in exchange both go through it, and they run
262
+ // before any session exists — a proxy installed after them would be a setting
263
+ // that works only sometimes.
264
+ const { installProxy } = await import("./proxy.js");
265
+ installProxy(AGENT_DIR);
266
+ // `--update` installs the newest release; `--check-update` only looks. Both
267
+ // exit without a session, so neither loads the model stack.
268
+ if (args.update || args.checkUpdate) {
269
+ const { PACKAGE_NAME, checkForUpdate, installUpdate, isInstalledCopy } = await import("./update.js");
270
+ const check = await checkForUpdate(APP_VERSION);
271
+ if (!check.latest) {
272
+ // Not reaching the registry is a failure only when an install was asked
273
+ // for; a plain check that could not run has nothing to report.
274
+ console.error(`${APP_TITLE}: ${check.message}`);
275
+ process.exitCode = args.update ? 1 : 0;
276
+ return;
277
+ }
278
+ console.log(check.message);
279
+ if (args.checkUpdate || !check.available)
280
+ return;
281
+ if (!isInstalledCopy()) {
282
+ console.error(`${APP_TITLE}: this copy was not installed by npm, so npm cannot replace it. ` +
283
+ `install it with: npm install -g ${PACKAGE_NAME}`);
284
+ process.exitCode = 1;
285
+ return;
286
+ }
287
+ const result = await installUpdate({ version: check.latest });
288
+ console.log(result.message);
289
+ if (!result.ok)
290
+ process.exitCode = 1;
291
+ return;
292
+ }
293
+ // `howtofix preset <new|check|pack>` authors a preset and exits. It runs before
294
+ // anything loads the SDK, because authoring has nothing to do with a model and
295
+ // a preset author on a machine without credentials should still be able to
296
+ // validate what they wrote.
297
+ const presetArgv = process.argv.slice(2);
298
+ if (presetArgv[0] === "preset") {
299
+ const { runPresetCommand } = await import("./preset-command.js");
300
+ process.exitCode = runPresetCommand(presetArgv.slice(1));
301
+ return;
302
+ }
303
+ const { expandFileReferences } = await import("./at-file.js");
304
+ const { isApprovalMode, setApprovalMode, setDangerousBypass, isDangerousBypass } = await import("./permissions.js");
305
+ const { openSession } = await import("./session.js");
306
+ // Print mode has no dialogs, so anything that would ask must not block. The
307
+ // interactive default is `full-auto` too: an approval prompt that fires on
308
+ // every write is friction the user has to configure away, and a critical
309
+ // command is denied in every mode regardless.
310
+ setApprovalMode(args.approval && isApprovalMode(args.approval) ? args.approval : "full-auto");
311
+ // `--dangerously-skip-permissions` lifts the last two refusals print mode can
312
+ // still hit: a critical command and the loop guard. A stored preference does
313
+ // the same for someone who never passes flags — the flag wins when it is
314
+ // given, so a one-off run cannot be overridden by a file.
315
+ const { readWorkbenchSettings } = await import("./settings.js");
316
+ if (args.dangerouslySkipPermissions || readWorkbenchSettings(stateDir).skipPermissions)
317
+ setDangerousBypass(true);
318
+ // A bypass nobody can see is the dangerous kind: it removes the refusals a
319
+ // user relies on and leaves no trace of why. Say it is on, every run.
320
+ if (isDangerousBypass()) {
321
+ console.error(`${APP_TITLE}: permission checks are OFF — critical commands (filesystem wipes, ` +
322
+ `fork bombs, host shutdown) and the repeated-call guard are NOT enforced for this run. ` +
323
+ `Turn it off with /settings, or by removing skipPermissions from settings.json.`);
324
+ }
325
+ // A protocol server owns stdin and stdout from here on. It opens its own
326
+ // sessions (ACP opens one per `session/new`), so it returns before the one
327
+ // below is created.
328
+ if (args.mode) {
329
+ const { serveProtocol } = await import("./serve.js");
330
+ await serveProtocol(args.mode, { cwd, model: args.model, thinking: args.thinking });
331
+ return;
332
+ }
333
+ const open = (options) => openSession({
334
+ cwd,
335
+ model: args.model,
336
+ thinking: args.thinking,
337
+ continueSession: options.continueSession ?? false,
338
+ noSession: options.noSession ?? false,
339
+ sessionPath: options.sessionPath,
340
+ // The caller chooses the session, not the prompt. `--print` is the only
341
+ // route that runs without a terminal, and the prompt must say so or a
342
+ // skill that asks a question becomes a dead end that still exits 0.
343
+ interactive: !args.print,
344
+ });
345
+ const opened = await open({
346
+ continueSession: args.continueSession,
347
+ sessionPath: args.sessionPath,
348
+ noSession: args.noSession || args.print,
349
+ });
350
+ // The note reports how the session was opened, so a refusal below must not
351
+ // swallow it: print writes it here, ahead of any guard; the TUI carries it
352
+ // into the feed as `options.note`.
353
+ if (args.print && opened.note)
354
+ console.error(opened.note);
355
+ // A budget on an unpriced model can never fire, so refuse instead of
356
+ // pretending to guard — in *both* modes, now that the workbench enforces the
357
+ // cap too: a TUI drawing a cap over a model that reports $0 a turn would be
358
+ // the exact lie this refusal exists to prevent. See `priced` on
359
+ // `OpenedSession`.
360
+ if (args.maxBudgetUsd !== undefined && !opened.priced) {
361
+ console.error(`${APP_TITLE}: ${unpricedBudgetMessage(opened.modelLabel, stateDir)}`);
362
+ await opened.dispose();
363
+ process.exitCode = 2;
364
+ return;
365
+ }
366
+ if (args.print) {
367
+ // `--print` has no slash router, but the TUI routes every "/" prompt to one
368
+ // (submit → runSlash) and never lets those reach the model. Without this
369
+ // guard the same prompt is forwarded verbatim — `/connect sk-...` pasted
370
+ // into a script sends the credential to the provider and records it in the
371
+ // transcript, and the command silently does not run. Skills are exempt:
372
+ // the TUI forwards those as prompts too (runSlash → skillInvocation).
373
+ const trimmedPrompt = args.prompt.trimStart();
374
+ if (trimmedPrompt.startsWith("/")) {
375
+ const name = trimmedPrompt.slice(1).split(/\s+/)[0] ?? "";
376
+ const { skillInvocation } = await import("./skills.js");
377
+ const skillNames = opened.session.resourceLoader.getSkills().skills.map((skill) => skill.name);
378
+ const isSkill = skillInvocation(trimmedPrompt, skillNames) !== undefined;
379
+ if (!isSkill) {
380
+ const { slashCommands } = await import("./ui.js");
381
+ if (slashCommands(opened.session).some((command) => command.name === name)) {
382
+ console.error(`${APP_TITLE}: /${name} runs in the TUI only — --print sends prompts straight to the model. ` +
383
+ `Run \`howtofix\` without --print for interactive commands.`);
384
+ await opened.dispose();
385
+ process.exitCode = 2;
386
+ return;
387
+ }
388
+ }
389
+ }
390
+ const stream = args.outputFormat === "stream-json";
391
+ const json = args.outputFormat === "json";
392
+ // Both machine formats end with a result record; only `stream-json` writes
393
+ // the events along the way.
394
+ const machine = stream || json;
395
+ // A `system`-shaped header first, so a consumer learns the model and the
396
+ // session id before any event that refers to them — the same courtesy the
397
+ // SDK's own JSON mode pays. Only the streaming format has events to head.
398
+ if (stream) {
399
+ const header = {
400
+ type: "system",
401
+ subtype: "init",
402
+ cwd,
403
+ session_id: opened.session.sessionId,
404
+ model: opened.modelLabel,
405
+ output_format: args.outputFormat,
406
+ max_budget_usd: args.maxBudgetUsd ?? null,
407
+ dangerously_skip_permissions: args.dangerouslySkipPermissions,
408
+ cli_version: APP_VERSION,
409
+ };
410
+ process.stdout.write(`${JSON.stringify(header)}\n`);
411
+ }
412
+ let unsubscribe;
413
+ let budgetHit;
414
+ const checkBudget = () => {
415
+ if (args.maxBudgetUsd === undefined || budgetHit !== undefined)
416
+ return;
417
+ const spent = opened.session.getSessionStats().cost;
418
+ if (spent > args.maxBudgetUsd) {
419
+ budgetHit = { spent };
420
+ void opened.session.abort();
421
+ }
422
+ };
423
+ try {
424
+ // Both machine formats and text need the same budget signal, and the only
425
+ // reliable one is the event stream: usage is updated as events arrive, so
426
+ // a timer that samples the total can miss the update entirely and let the
427
+ // run overspend. Subscribe in every mode; only `stream-json` writes the
428
+ // lines out.
429
+ // The failed turn is captured here, not read back from the session at the
430
+ // end. When the SDK's overflow recovery omits the attempt it could not
431
+ // recover from, `state.messages.at(-1)` is no longer the error — it is the
432
+ // user's prompt, or nothing — so `--print` reported success with an empty
433
+ // answer and exit code 0 for a run that failed. Measured against a gateway
434
+ // answering 400 "prompt is too long": exit 0, no output on either stream.
435
+ let failedTurn;
436
+ unsubscribe = opened.session.subscribe((event) => {
437
+ if (stream) {
438
+ const line = jsonLine(event);
439
+ if (line !== undefined)
440
+ process.stdout.write(line);
441
+ }
442
+ if (event.type === "turn_end" && event.message?.role === "assistant") {
443
+ const message = event.message;
444
+ if (message.stopReason === "error" || message.stopReason === "aborted" || message.errorMessage) {
445
+ failedTurn = { stopReason: message.stopReason ?? "error", errorMessage: message.errorMessage };
446
+ }
447
+ else {
448
+ failedTurn = undefined;
449
+ }
450
+ }
451
+ checkBudget();
452
+ });
453
+ await opened.session.prompt(expandFileReferences(args.prompt, cwd));
454
+ const text = opened.session.getLastAssistantText() ?? "";
455
+ if (budgetHit) {
456
+ console.error(`${APP_TITLE}: ${spendCapMessage(budgetHit.spent, args.maxBudgetUsd)}`);
457
+ // A consumer reading the stream needs a terminal record on this path
458
+ // too: without it the run just stops mid-stream and looks truncated.
459
+ if (machine) {
460
+ const stats = opened.session.getSessionStats();
461
+ process.stdout.write(`${JSON.stringify({
462
+ type: "result",
463
+ subtype: "error_max_budget_usd",
464
+ is_error: true,
465
+ result: text,
466
+ session_id: opened.session.sessionId,
467
+ duration_ms: Math.round(performance.now() - startedAt),
468
+ num_turns: stats.assistantMessages,
469
+ total_cost_usd: stats.cost,
470
+ usage: {
471
+ input_tokens: stats.tokens.input,
472
+ output_tokens: stats.tokens.output,
473
+ cache_read_input_tokens: stats.tokens.cacheRead,
474
+ cache_creation_input_tokens: stats.tokens.cacheWrite,
475
+ },
476
+ })}\n`);
477
+ }
478
+ process.exitCode = 1;
479
+ return;
480
+ }
481
+ // The SDK contract: a final assistant response that ended in `error` or
482
+ // `aborted` produces a nonzero exit status, so scripts can branch on it.
483
+ //
484
+ // `failedTurn` is the turn the event stream saw fail; `messages.at(-1)` is
485
+ // the fallback for a run that failed without ever emitting one. Reading
486
+ // only the tail is what let an unrecovered overflow report success.
487
+ const last = opened.session.state.messages.at(-1);
488
+ const failure = failedTurn ??
489
+ (last?.role === "assistant" && (last.stopReason === "error" || last.stopReason === "aborted" || last.errorMessage)
490
+ ? { stopReason: last.stopReason ?? "error", errorMessage: last.errorMessage }
491
+ : undefined);
492
+ // One result record per run, written after the outcome is known. The
493
+ // success record used to be written first and the exit status decided
494
+ // afterwards, so a failed turn emitted "success" and then "error" — and a
495
+ // consumer reading the first record believed a failed run had worked.
496
+ if (machine) {
497
+ const stats = opened.session.getSessionStats();
498
+ process.stdout.write(`${JSON.stringify({
499
+ type: "result",
500
+ subtype: failure
501
+ ? failure.stopReason === "aborted"
502
+ ? "error_aborted"
503
+ : "error_during_execution"
504
+ : "success",
505
+ is_error: failure !== undefined,
506
+ result: text,
507
+ session_id: opened.session.sessionId,
508
+ duration_ms: Math.round(performance.now() - startedAt),
509
+ num_turns: stats.assistantMessages,
510
+ total_cost_usd: stats.cost,
511
+ usage: {
512
+ input_tokens: stats.tokens.input,
513
+ output_tokens: stats.tokens.output,
514
+ cache_read_input_tokens: stats.tokens.cacheRead,
515
+ cache_creation_input_tokens: stats.tokens.cacheWrite,
516
+ },
517
+ stop_reason: failure?.stopReason ?? (last?.role === "assistant" ? last.stopReason : undefined),
518
+ })}\n`);
519
+ }
520
+ else {
521
+ process.stdout.write(text.endsWith("\n") ? text : `${text}\n`);
522
+ }
523
+ if (failure) {
524
+ process.exitCode = 1;
525
+ // A failed turn used to exit 1 with no output at all, which is
526
+ // indistinguishable from a crash. Say what the provider said.
527
+ if (failure.errorMessage) {
528
+ console.error(`${APP_TITLE}: ${failure.errorMessage}`);
529
+ // Same gap as the TUI: the gateway's own sentence, and the fix for it.
530
+ const fix = diagnoseGatewayFailure(failure.errorMessage);
531
+ if (fix !== undefined)
532
+ console.error(`${APP_TITLE}: ${fix}`);
533
+ }
534
+ }
535
+ }
536
+ finally {
537
+ unsubscribe?.();
538
+ await opened.dispose();
539
+ }
540
+ return;
541
+ }
542
+ if (!process.stdin.isTTY || !process.stdout.isTTY) {
543
+ console.error(`${APP_TITLE}: interactive mode needs a terminal. use --print for pipes.`);
544
+ await opened.dispose();
545
+ process.exitCode = 2;
546
+ return;
547
+ }
548
+ const { runWorkbench } = await import("./ui.js");
549
+ // Only a copy npm owns can be replaced by npm, and only when the user has not
550
+ // turned it off. The check itself is handed to the workbench so it runs after
551
+ // the UI is up rather than before it.
552
+ const { autoUpdateEnabled, isInstalledCopy } = await import("./update.js");
553
+ const canSelfUpdate = isInstalledCopy() && autoUpdateEnabled(`${stateDir}/settings.json`);
554
+ await runWorkbench({
555
+ cwd,
556
+ modelLabel: opened.modelLabel,
557
+ session: opened.session,
558
+ // The `/rtk` command toggles the flag the tool-result extension reads; the setter
559
+ // lives beside that flag so the two cannot drift apart.
560
+ setCompressToolOutput: opened.setCompressToolOutput,
561
+ initialPrompt: args.prompt || undefined,
562
+ note: opened.note,
563
+ // The cap the print path enforces over one run, handed to a UI that has to
564
+ // enforce it over many: same flag, same dollars, different lifetime.
565
+ maxBudgetUsd: args.maxBudgetUsd,
566
+ update: canSelfUpdate ? { current: APP_VERSION, install: true } : undefined,
567
+ reopen: open,
568
+ });
569
+ }
570
+ installCrashHandlers(APP_TITLE);
571
+ main().catch((error) => {
572
+ reportError(APP_TITLE, undefined, error);
573
+ process.exitCode = 1;
574
+ });