@warlock.js/ai-cli 0.0.0-stage → 5.32.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (139) hide show
  1. package/CHANGELOG.md +37 -0
  2. package/LICENSE +21 -0
  3. package/README.md +69 -2
  4. package/cjs/index.cjs +4568 -0
  5. package/cjs/index.cjs.map +1 -0
  6. package/esm/adapters/aider.d.mts +41 -0
  7. package/esm/adapters/aider.d.mts.map +1 -0
  8. package/esm/adapters/aider.mjs +294 -0
  9. package/esm/adapters/aider.mjs.map +1 -0
  10. package/esm/adapters/amp.d.mts +33 -0
  11. package/esm/adapters/amp.d.mts.map +1 -0
  12. package/esm/adapters/amp.mjs +119 -0
  13. package/esm/adapters/amp.mjs.map +1 -0
  14. package/esm/adapters/antigravity.d.mts +37 -0
  15. package/esm/adapters/antigravity.d.mts.map +1 -0
  16. package/esm/adapters/antigravity.mjs +235 -0
  17. package/esm/adapters/antigravity.mjs.map +1 -0
  18. package/esm/adapters/claude-code.d.mts +30 -0
  19. package/esm/adapters/claude-code.d.mts.map +1 -0
  20. package/esm/adapters/claude-code.mjs +121 -0
  21. package/esm/adapters/claude-code.mjs.map +1 -0
  22. package/esm/adapters/claude-stream.d.mts +32 -0
  23. package/esm/adapters/claude-stream.d.mts.map +1 -0
  24. package/esm/adapters/claude-stream.mjs +140 -0
  25. package/esm/adapters/claude-stream.mjs.map +1 -0
  26. package/esm/adapters/codex.d.mts +37 -0
  27. package/esm/adapters/codex.d.mts.map +1 -0
  28. package/esm/adapters/codex.mjs +322 -0
  29. package/esm/adapters/codex.mjs.map +1 -0
  30. package/esm/adapters/copilot.d.mts +35 -0
  31. package/esm/adapters/copilot.d.mts.map +1 -0
  32. package/esm/adapters/copilot.mjs +242 -0
  33. package/esm/adapters/copilot.mjs.map +1 -0
  34. package/esm/adapters/cursor.d.mts +35 -0
  35. package/esm/adapters/cursor.d.mts.map +1 -0
  36. package/esm/adapters/cursor.mjs +203 -0
  37. package/esm/adapters/cursor.mjs.map +1 -0
  38. package/esm/adapters/droid.d.mts +32 -0
  39. package/esm/adapters/droid.d.mts.map +1 -0
  40. package/esm/adapters/droid.mjs +183 -0
  41. package/esm/adapters/droid.mjs.map +1 -0
  42. package/esm/adapters/grok.d.mts +32 -0
  43. package/esm/adapters/grok.d.mts.map +1 -0
  44. package/esm/adapters/grok.mjs +197 -0
  45. package/esm/adapters/grok.mjs.map +1 -0
  46. package/esm/adapters/index.d.mts +17 -0
  47. package/esm/adapters/index.mjs +18 -0
  48. package/esm/adapters/kimi.d.mts +34 -0
  49. package/esm/adapters/kimi.d.mts.map +1 -0
  50. package/esm/adapters/kimi.mjs +162 -0
  51. package/esm/adapters/kimi.mjs.map +1 -0
  52. package/esm/adapters/mistral-vibe.d.mts +32 -0
  53. package/esm/adapters/mistral-vibe.d.mts.map +1 -0
  54. package/esm/adapters/mistral-vibe.mjs +250 -0
  55. package/esm/adapters/mistral-vibe.mjs.map +1 -0
  56. package/esm/adapters/opencode.d.mts +36 -0
  57. package/esm/adapters/opencode.d.mts.map +1 -0
  58. package/esm/adapters/opencode.mjs +219 -0
  59. package/esm/adapters/opencode.mjs.map +1 -0
  60. package/esm/adapters/qwen-code.d.mts +30 -0
  61. package/esm/adapters/qwen-code.d.mts.map +1 -0
  62. package/esm/adapters/qwen-code.mjs +94 -0
  63. package/esm/adapters/qwen-code.mjs.map +1 -0
  64. package/esm/adapters/reasonix.d.mts +31 -0
  65. package/esm/adapters/reasonix.d.mts.map +1 -0
  66. package/esm/adapters/reasonix.mjs +248 -0
  67. package/esm/adapters/reasonix.mjs.map +1 -0
  68. package/esm/adapters/shared.d.mts +81 -0
  69. package/esm/adapters/shared.d.mts.map +1 -0
  70. package/esm/adapters/shared.mjs +161 -0
  71. package/esm/adapters/shared.mjs.map +1 -0
  72. package/esm/cli-agent.d.mts +41 -0
  73. package/esm/cli-agent.d.mts.map +1 -0
  74. package/esm/cli-agent.mjs +207 -0
  75. package/esm/cli-agent.mjs.map +1 -0
  76. package/esm/cli.d.mts +56 -0
  77. package/esm/cli.d.mts.map +1 -0
  78. package/esm/cli.mjs +61 -0
  79. package/esm/cli.mjs.map +1 -0
  80. package/esm/contracts/adapter.contract.d.mts +175 -0
  81. package/esm/contracts/adapter.contract.d.mts.map +1 -0
  82. package/esm/contracts/events.type.d.mts +123 -0
  83. package/esm/contracts/events.type.d.mts.map +1 -0
  84. package/esm/contracts/index.d.mts +5 -0
  85. package/esm/contracts/mcp.type.d.mts +28 -0
  86. package/esm/contracts/mcp.type.d.mts.map +1 -0
  87. package/esm/contracts/options.type.d.mts +112 -0
  88. package/esm/contracts/options.type.d.mts.map +1 -0
  89. package/esm/errors.d.mts +48 -0
  90. package/esm/errors.d.mts.map +1 -0
  91. package/esm/errors.mjs +32 -0
  92. package/esm/errors.mjs.map +1 -0
  93. package/esm/index.d.mts +33 -0
  94. package/esm/index.mjs +28 -0
  95. package/esm/model/cli-model.d.mts +49 -0
  96. package/esm/model/cli-model.d.mts.map +1 -0
  97. package/esm/model/cli-model.mjs +133 -0
  98. package/esm/model/cli-model.mjs.map +1 -0
  99. package/esm/probe/probe.d.mts +18 -0
  100. package/esm/probe/probe.d.mts.map +1 -0
  101. package/esm/probe/probe.mjs +148 -0
  102. package/esm/probe/probe.mjs.map +1 -0
  103. package/esm/session/aggregate.mjs +84 -0
  104. package/esm/session/aggregate.mjs.map +1 -0
  105. package/esm/session/environment.mjs +71 -0
  106. package/esm/session/environment.mjs.map +1 -0
  107. package/esm/session/run-directory.mjs +74 -0
  108. package/esm/session/run-directory.mjs.map +1 -0
  109. package/esm/session/session.mjs +188 -0
  110. package/esm/session/session.mjs.map +1 -0
  111. package/esm/spawn/invocation.d.mts +34 -0
  112. package/esm/spawn/invocation.d.mts.map +1 -0
  113. package/esm/spawn/invocation.mjs +76 -0
  114. package/esm/spawn/invocation.mjs.map +1 -0
  115. package/esm/spawn/process-tree.mjs +57 -0
  116. package/esm/spawn/process-tree.mjs.map +1 -0
  117. package/esm/spawn/resolve-command.d.mts +75 -0
  118. package/esm/spawn/resolve-command.d.mts.map +1 -0
  119. package/esm/spawn/resolve-command.mjs +109 -0
  120. package/esm/spawn/resolve-command.mjs.map +1 -0
  121. package/esm/stream/event-log.mjs +52 -0
  122. package/esm/stream/event-log.mjs.map +1 -0
  123. package/esm/stream/json-lines.d.mts +33 -0
  124. package/esm/stream/json-lines.d.mts.map +1 -0
  125. package/esm/stream/json-lines.mjs +65 -0
  126. package/esm/stream/json-lines.mjs.map +1 -0
  127. package/esm/stream/line-splitter.mjs +53 -0
  128. package/esm/stream/line-splitter.mjs.map +1 -0
  129. package/esm/utils/toml.d.mts +21 -0
  130. package/esm/utils/toml.d.mts.map +1 -0
  131. package/esm/utils/toml.mjs +53 -0
  132. package/esm/utils/toml.mjs.map +1 -0
  133. package/llms-full.txt +373 -0
  134. package/llms.txt +10 -0
  135. package/package.json +39 -4
  136. package/skills/README.md +13 -0
  137. package/skills/index.md +22 -0
  138. package/skills/run-cli-agents/SKILL.md +186 -0
  139. package/skills/write-a-cli-adapter/SKILL.md +173 -0
package/cjs/index.cjs ADDED
@@ -0,0 +1,4568 @@
1
+ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
2
+ //#region \0rolldown/runtime.js
3
+ var __create = Object.create;
4
+ var __defProp = Object.defineProperty;
5
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
6
+ var __getOwnPropNames = Object.getOwnPropertyNames;
7
+ var __getProtoOf = Object.getPrototypeOf;
8
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
9
+ var __copyProps = (to, from, except, desc) => {
10
+ if (from && typeof from === "object" || typeof from === "function") {
11
+ for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
12
+ key = keys[i];
13
+ if (!__hasOwnProp.call(to, key) && key !== except) {
14
+ __defProp(to, key, {
15
+ get: ((k) => from[k]).bind(null, key),
16
+ enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
17
+ });
18
+ }
19
+ }
20
+ }
21
+ return to;
22
+ };
23
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule || !__hasOwnProp.call(mod, "default") ? __defProp(target, "default", {
24
+ value: mod,
25
+ enumerable: true
26
+ }) : target, mod));
27
+
28
+ //#endregion
29
+ let _warlock_js_ai = require("@warlock.js/ai");
30
+ let node_fs = require("node:fs");
31
+ let node_path = require("node:path");
32
+ node_path = __toESM(node_path, 1);
33
+ let node_child_process = require("node:child_process");
34
+ let node_fs_promises = require("node:fs/promises");
35
+ let node_os = require("node:os");
36
+ let node_string_decoder = require("node:string_decoder");
37
+
38
+ //#region ../../packages/ai-cli/src/errors.ts
39
+ /**
40
+ * A CLI agent error. Extends the framework `ProviderError`, so agents,
41
+ * fallbacks and observers treat it like any other provider failure; branch
42
+ * on `error.type` for the specific reason.
43
+ *
44
+ * @example
45
+ * try {
46
+ * await claude.run("hi");
47
+ * } catch (error) {
48
+ * if (error instanceof CliError && error.type === "capability") {
49
+ * console.error(`Upgrade ${error.adapter}: missing ${error.flag} in ${error.version}`);
50
+ * }
51
+ * }
52
+ */
53
+ var CliError = class extends _warlock_js_ai.ProviderError {
54
+ constructor(message, options) {
55
+ super(message, options, options.type === "unsupported-option" || options.type === "prompt-transport" ? "PROVIDER_INVALID_REQUEST" : "PROVIDER_ERROR");
56
+ this.name = "CliError";
57
+ this.type = options.type;
58
+ this.adapter = options.adapter;
59
+ this.flag = options.flag;
60
+ this.version = options.version;
61
+ this.executable = options.executable;
62
+ }
63
+ };
64
+
65
+ //#endregion
66
+ //#region ../../packages/ai-cli/src/model/cli-model.ts
67
+ /** The text of one message; CLI models accept text only. */
68
+ function messageText(message, adapter) {
69
+ if (typeof message.content === "string") return message.content;
70
+ return message.content.map((part) => {
71
+ if (part.type !== "text") throw new CliError(`${adapter}: CLI models accept text only; a "${part.type}" attachment cannot be passed to the CLI.`, {
72
+ type: "unsupported-option",
73
+ adapter
74
+ });
75
+ return part.text;
76
+ }).join("\n");
77
+ }
78
+ /**
79
+ * Render an agent conversation into one CLI prompt. System messages become
80
+ * a leading instruction block; a single user turn is passed as-is; a longer
81
+ * history is rendered as a labelled transcript ending with the latest turn.
82
+ *
83
+ * @example
84
+ * renderPrompt([{ role: "system", content: "Be brief." }, { role: "user", content: "Hi" }], "codex");
85
+ * // "Be brief.\n\nHi"
86
+ */
87
+ function renderPrompt(messages, adapter) {
88
+ const system = messages.filter((message) => message.role === "system").map((message) => messageText(message, adapter).trim()).filter((text) => text.length > 0);
89
+ const turns = messages.filter((message) => message.role !== "system");
90
+ const only = turns.length === 1 ? turns[0] : void 0;
91
+ const body = only !== void 0 && only.role === "user" ? messageText(only, adapter) : turns.map((message) => {
92
+ return `${message.role === "user" ? "User" : message.role === "assistant" ? "Assistant" : `Tool result${message.toolCallId ? ` (${message.toolCallId})` : ""}`}:\n${messageText(message, adapter)}`;
93
+ }).join("\n\n");
94
+ return [...system, body].filter((part) => part.length > 0).join("\n\n");
95
+ }
96
+ /** Map CLI usage to the framework `Usage` (zeros when the CLI reported none). */
97
+ function toUsage(usage) {
98
+ if (usage === void 0) return {
99
+ input: 0,
100
+ output: 0,
101
+ total: 0
102
+ };
103
+ const mapped = {
104
+ input: usage.input,
105
+ output: usage.output,
106
+ total: usage.total
107
+ };
108
+ if (usage.cachedTokens !== void 0) mapped.cachedTokens = usage.cachedTokens;
109
+ if (usage.cacheWriteTokens !== void 0) mapped.cacheWriteTokens = usage.cacheWriteTokens;
110
+ if (usage.reasoningTokens !== void 0) mapped.reasoningTokens = usage.reasoningTokens;
111
+ return mapped;
112
+ }
113
+ function abortError(message) {
114
+ const error = new Error(message);
115
+ error.name = "AbortError";
116
+ return error;
117
+ }
118
+ /**
119
+ * A `ModelContract` backed by a CLI agent, so `ai.agent({ model })` — and
120
+ * its budget, guardrail, observer and fallback middleware — can drive a
121
+ * coding CLI.
122
+ *
123
+ * Each call is one fresh CLI session: the conversation is rendered into a
124
+ * single prompt (see {@link renderPrompt}) and the session's final answer
125
+ * becomes the response. The CLI runs its own tools; framework tools cannot
126
+ * be handed to it this way (give the CLI MCP servers with `mcp` instead),
127
+ * so a call carrying `tools` is refused. `stream()` yields the final answer
128
+ * once the session ends — read `session.events` from `agent.run()` for live
129
+ * progress.
130
+ *
131
+ * @example
132
+ * const coder = cli.codex({ cwd: "/srv/repo" });
133
+ * const reviewer = ai.agent({
134
+ * model: cliModel(coder, { name: "codex" }),
135
+ * middleware: [budget({ maxTokens: 200_000 })],
136
+ * });
137
+ * const { text } = await reviewer.execute("Review the last commit.");
138
+ */
139
+ function cliModel(agent, config = {}) {
140
+ const { adapter } = agent;
141
+ const capabilities = { reasoning: adapter.effort === true };
142
+ const execute = async (messages, options = {}) => {
143
+ if (options.tools !== void 0 && options.tools.length > 0) throw new CliError(`${adapter.title}: a CLI model runs the CLI's own tools; pass framework tools to the CLI as MCP servers (\`mcp\`) instead of agent \`tools\`.`, {
144
+ type: "unsupported-option",
145
+ adapter: adapter.name
146
+ });
147
+ const { name: _name, pricing: _pricing, ...runDefaults } = config;
148
+ const effort = options.reasoning?.effort;
149
+ const result = await (await agent.run(renderPrompt(messages, adapter.name), {
150
+ ...runDefaults,
151
+ signal: options.signal ?? runDefaults.signal,
152
+ effort: adapter.effort === true && effort !== void 0 ? effort : runDefaults.effort
153
+ })).done();
154
+ if (result.status === "cancelled") throw abortError(result.error ?? `${adapter.name} run was cancelled`);
155
+ if (result.status !== "completed") throw new CliError(`${adapter.title}: ${result.error ?? `run ${result.status}`}`, {
156
+ type: "run-failed",
157
+ adapter: adapter.name,
158
+ context: {
159
+ status: result.status,
160
+ exitCode: result.exitCode,
161
+ sessionRef: result.sessionRef
162
+ }
163
+ });
164
+ return result;
165
+ };
166
+ return {
167
+ name: config.name ?? config.model ?? agent.options.model ?? adapter.name,
168
+ provider: adapter.name,
169
+ capabilities,
170
+ pricing: config.pricing,
171
+ async complete(messages, options) {
172
+ const result = await execute(messages, options);
173
+ return {
174
+ content: result.text,
175
+ finishReason: "stop",
176
+ usage: toUsage(result.usage)
177
+ };
178
+ },
179
+ async *stream(messages, options) {
180
+ const result = await execute(messages, options);
181
+ if (result.text.length > 0) yield {
182
+ type: "delta",
183
+ content: result.text
184
+ };
185
+ yield {
186
+ type: "done",
187
+ finishReason: "stop",
188
+ usage: toUsage(result.usage)
189
+ };
190
+ }
191
+ };
192
+ }
193
+
194
+ //#endregion
195
+ //#region ../../packages/ai-cli/src/spawn/process-tree.ts
196
+ /**
197
+ * Terminate a CLI and everything it started.
198
+ *
199
+ * Coding CLIs spawn shells, language servers and MCP servers, so signalling
200
+ * the direct child alone would leave grandchildren running.
201
+ *
202
+ * - **POSIX** — the child was spawned `detached` (its own process group), so
203
+ * the whole group gets `SIGTERM`, then `SIGKILL` after `graceMs`.
204
+ * - **Windows** — `taskkill /T /F` walks and kills the PID's tree (console
205
+ * programs ignore a polite close, so there is no grace step).
206
+ *
207
+ * Only the tree this package spawned is ever signalled: the child's own PID
208
+ * (Windows) or its own process group (POSIX) — never a name, port or
209
+ * pattern. The SIGKILL follows even when the leader exits on SIGTERM, so a
210
+ * grandchild that ignores SIGTERM (and keeps the pipes open) is still
211
+ * reaped; a process group id cannot be reused while any member is alive.
212
+ * Call the returned function once the session's pipes have closed (the
213
+ * whole tree is gone) to cancel that pending SIGKILL.
214
+ */
215
+ function killProcessTree(child, platform, graceMs) {
216
+ const pid = child.pid;
217
+ if (pid === void 0) {
218
+ child.kill("SIGKILL");
219
+ return () => void 0;
220
+ }
221
+ if (platform === "win32") {
222
+ (0, node_child_process.spawn)("taskkill", [
223
+ "/pid",
224
+ String(pid),
225
+ "/T",
226
+ "/F"
227
+ ], { windowsHide: true }).on("error", () => {
228
+ child.kill("SIGKILL");
229
+ });
230
+ return () => void 0;
231
+ }
232
+ const signalGroup = (signal) => {
233
+ try {
234
+ process.kill(-pid, signal);
235
+ } catch {
236
+ try {
237
+ child.kill(signal);
238
+ } catch {}
239
+ }
240
+ };
241
+ signalGroup("SIGTERM");
242
+ const timer = setTimeout(() => signalGroup("SIGKILL"), Math.max(0, graceMs));
243
+ timer.unref();
244
+ return () => clearTimeout(timer);
245
+ }
246
+
247
+ //#endregion
248
+ //#region ../../packages/ai-cli/src/spawn/invocation.ts
249
+ /**
250
+ * Bytes `cmd.exe` cannot carry inside a double-quoted argument: a quote ends
251
+ * the quoted span, `%` expands variables even inside quotes, and a newline
252
+ * ends the command (the BatBadBut / CVE-2024-24576 class). An argument
253
+ * holding one of them is refused rather than escaped.
254
+ */
255
+ const CMD_UNSAFE = /["%\r\n\0]/;
256
+ /** `cmd.exe` command-line limit (8191 characters), with headroom for the wrapper. */
257
+ const CMD_LINE_LIMIT = 8e3;
258
+ /** `CreateProcess` command-line limit (32767 characters), with headroom. */
259
+ const WINDOWS_LINE_LIMIT = 32e3;
260
+ /** Linux `MAX_ARG_STRLEN` (128 KiB per argument), with headroom. */
261
+ const POSIX_ARGUMENT_LIMIT = 122880;
262
+ /** Conservative total argv budget on POSIX (ARG_MAX is shared with the environment). */
263
+ const POSIX_TOTAL_LIMIT = 524288;
264
+ /**
265
+ * Quote one argument for a `cmd.exe /s /c "..."` line, or return `null` when
266
+ * it holds a byte cmd cannot quote safely. Inside double quotes `& | < > ^`
267
+ * are literal; trailing backslashes are doubled so the target program's
268
+ * argv parser does not read the closing quote as escaped.
269
+ */
270
+ function quoteForCmd(argument) {
271
+ if (CMD_UNSAFE.test(argument)) return null;
272
+ return `"${argument}${/\\+$/.exec(argument)?.[0] ?? ""}"`;
273
+ }
274
+ /**
275
+ * Build the invocation for a target and argv. A `cmd` target becomes
276
+ * `cmd.exe /d /v:off /s /c ""batch" "arg" …"` — AutoRun disabled, delayed
277
+ * expansion off (so `!` stays literal), each element quoted. Returns a
278
+ * `refused` reason when an argument cannot be quoted safely.
279
+ */
280
+ function buildInvocation(target, args, comspec) {
281
+ if (target.type === "direct") return {
282
+ file: target.file,
283
+ args: [...target.prefixArgs, ...args],
284
+ windowsVerbatimArguments: false
285
+ };
286
+ const quoted = [];
287
+ for (const element of [target.file, ...args]) {
288
+ const safe = quoteForCmd(element);
289
+ if (safe === null) return { refused: "an argument contains \", %, a newline or a NUL byte, which cmd.exe cannot carry safely" };
290
+ quoted.push(safe);
291
+ }
292
+ return {
293
+ file: comspec,
294
+ args: [
295
+ "/d",
296
+ "/v:off",
297
+ "/s",
298
+ "/c",
299
+ `"${quoted.join(" ")}"`
300
+ ],
301
+ windowsVerbatimArguments: true
302
+ };
303
+ }
304
+ /**
305
+ * Whether an invocation's argv fits the platform's limits. Windows counts
306
+ * the whole command line (8191 for `cmd.exe`, 32767 for `CreateProcess`,
307
+ * each argument quoted); POSIX limits a single argument to 128 KiB on Linux
308
+ * and shares `ARG_MAX` with the environment.
309
+ */
310
+ function fitsCommandLine(invocation, platform) {
311
+ if (platform === "win32") return [invocation.file, ...invocation.args].reduce((total, element) => total + element.length + 3, 0) <= (invocation.windowsVerbatimArguments ? CMD_LINE_LIMIT : WINDOWS_LINE_LIMIT);
312
+ let total = 0;
313
+ for (const element of [invocation.file, ...invocation.args]) {
314
+ const bytes = Buffer.byteLength(element, "utf8") + 1;
315
+ if (bytes > 122880) return false;
316
+ total += bytes;
317
+ }
318
+ return total <= POSIX_TOTAL_LIMIT;
319
+ }
320
+
321
+ //#endregion
322
+ //#region ../../packages/ai-cli/src/probe/probe.ts
323
+ /** How long a single `--help`/`--version` call may take. */
324
+ const PROBE_TIMEOUT_MS = 3e4;
325
+ /** Output cap per probe call; help screens are a few KiB. */
326
+ const PROBE_OUTPUT_LIMIT = 1048576;
327
+ function escapeRegExp(text) {
328
+ return text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
329
+ }
330
+ /**
331
+ * Whether a help screen lists a flag or subcommand. Flags (`--json`, `-p`)
332
+ * must appear as a whole token — `--json` does not match `--json-schema`;
333
+ * a bare word (`exec`) is matched as a whole word.
334
+ *
335
+ * @example
336
+ * helpLists(" -p, --print Print response", "--print"); // true
337
+ * helpLists(" --output-format <format>", "--output"); // false
338
+ */
339
+ function helpLists(help, flag) {
340
+ const escaped = escapeRegExp(flag);
341
+ if (flag.startsWith("-")) return new RegExp(`(^|[\\s,\\[(|/"'\`])${escaped}(?=$|[\\s,=\\])|<\\[.:;"'\`])`, "m").test(help);
342
+ return new RegExp(`(^|[^\\w-])${escaped}(?=$|[^\\w-])`, "m").test(help);
343
+ }
344
+ /** The first version-looking token of `--version` output, else its first line. */
345
+ function parseVersion(output) {
346
+ const match = /\d+\.\d+(?:\.\d+)?(?:[-+][0-9A-Za-z.-]+)?/.exec(output);
347
+ if (match) return match[0];
348
+ const firstLine = output.trim().split(/\r?\n/)[0]?.trim();
349
+ return firstLine && firstLine.length > 0 ? firstLine : "unknown";
350
+ }
351
+ /** Flags a plan requires that the help screen does not list. */
352
+ function missingFlags(help, required) {
353
+ return required.filter((flag) => !helpLists(help, flag));
354
+ }
355
+ /** Run a probe invocation and capture stdout + stderr (help is printed to either). */
356
+ function capture(input) {
357
+ return new Promise((resolve) => {
358
+ const invocation = buildInvocation(input.target, input.args, input.comspec);
359
+ if ("refused" in invocation) {
360
+ resolve({
361
+ output: "",
362
+ exitCode: null,
363
+ error: new Error(invocation.refused)
364
+ });
365
+ return;
366
+ }
367
+ const child = (0, node_child_process.spawn)(invocation.file, invocation.args, {
368
+ cwd: input.cwd,
369
+ env: input.env,
370
+ shell: false,
371
+ windowsHide: true,
372
+ windowsVerbatimArguments: invocation.windowsVerbatimArguments,
373
+ detached: input.platform !== "win32",
374
+ stdio: [
375
+ "ignore",
376
+ "pipe",
377
+ "pipe"
378
+ ]
379
+ });
380
+ const chunks = [];
381
+ let size = 0;
382
+ let settled = false;
383
+ const collect = (chunk) => {
384
+ if (size < PROBE_OUTPUT_LIMIT) {
385
+ chunks.push(chunk);
386
+ size += chunk.length;
387
+ }
388
+ };
389
+ const timer = setTimeout(() => {
390
+ killProcessTree(child, input.platform, 0);
391
+ }, PROBE_TIMEOUT_MS);
392
+ const finish = (exitCode, error) => {
393
+ if (settled) return;
394
+ settled = true;
395
+ clearTimeout(timer);
396
+ resolve({
397
+ output: Buffer.concat(chunks).toString("utf8"),
398
+ exitCode,
399
+ error
400
+ });
401
+ };
402
+ child.stdout?.on("data", collect);
403
+ child.stderr?.on("data", collect);
404
+ child.on("error", (error) => finish(null, error));
405
+ child.on("close", (code) => finish(code));
406
+ });
407
+ }
408
+ /**
409
+ * Read the CLI's version and help screens. A help invocation that cannot
410
+ * run at all (spawn error, no output) fails the probe; a non-zero exit with
411
+ * output is accepted, because several CLIs exit 1 after printing help.
412
+ */
413
+ async function probeCli(input) {
414
+ const { adapter } = input;
415
+ const versionRun = await capture({
416
+ ...input,
417
+ args: adapter.probe.version
418
+ });
419
+ if (versionRun.error !== void 0) throw new CliError(`${adapter.title}: could not run "${input.target.executable} ${adapter.probe.version.join(" ")}": ${versionRun.error.message}`, {
420
+ type: "probe-failed",
421
+ adapter: adapter.name,
422
+ executable: input.target.executable,
423
+ cause: versionRun.error
424
+ });
425
+ const version = parseVersion(versionRun.output);
426
+ const helps = [];
427
+ for (const args of adapter.probe.help) {
428
+ const helpRun = await capture({
429
+ ...input,
430
+ args
431
+ });
432
+ if (helpRun.error !== void 0 || helpRun.output.trim().length === 0) throw new CliError(`${adapter.title} ${version}: "${input.target.executable} ${args.join(" ")}" printed no help` + (helpRun.error ? ` (${helpRun.error.message})` : "") + "; the capability probe cannot verify the flags this adapter needs.", {
433
+ type: "probe-failed",
434
+ adapter: adapter.name,
435
+ version,
436
+ executable: input.target.executable,
437
+ cause: helpRun.error
438
+ });
439
+ helps.push(helpRun.output);
440
+ }
441
+ return {
442
+ executable: input.target.executable,
443
+ version,
444
+ help: helps.join("\n")
445
+ };
446
+ }
447
+ /** Throw a `capability` error naming the first required flag the help does not list. */
448
+ function assertCapabilities(adapter, probe, required) {
449
+ const missing = missingFlags(probe.help, required);
450
+ const flag = missing[0];
451
+ if (flag === void 0) return;
452
+ const helpCommands = adapter.probe.help.map((args) => `${adapter.command} ${args.join(" ")}`).join(", ");
453
+ throw new CliError(`${adapter.title} ${probe.version} (${probe.executable}) does not support ${missing.join(", ")}: not listed in "${helpCommands}". This adapter was verified against ${adapter.title} ${adapter.verification.version}; install a compatible version or drop the option that needs ${flag}.`, {
454
+ type: "capability",
455
+ adapter: adapter.name,
456
+ flag,
457
+ version: probe.version,
458
+ executable: probe.executable
459
+ });
460
+ }
461
+
462
+ //#endregion
463
+ //#region ../../packages/ai-cli/src/session/environment.ts
464
+ /**
465
+ * Variables a CLI needs to start at all: executable lookup, home and temp
466
+ * directories, locale, TLS/proxy settings and the Windows system set.
467
+ * Passed through even with `inheritEnv: false`.
468
+ */
469
+ const MINIMAL_ENV = [
470
+ "PATH",
471
+ "PATHEXT",
472
+ "HOME",
473
+ "USERPROFILE",
474
+ "HOMEDRIVE",
475
+ "HOMEPATH",
476
+ "APPDATA",
477
+ "LOCALAPPDATA",
478
+ "SYSTEMROOT",
479
+ "SYSTEMDRIVE",
480
+ "WINDIR",
481
+ "COMSPEC",
482
+ "TEMP",
483
+ "TMP",
484
+ "TMPDIR",
485
+ "LANG",
486
+ "LC_ALL",
487
+ "LC_CTYPE",
488
+ "TERM",
489
+ "SHELL",
490
+ "USER",
491
+ "LOGNAME",
492
+ "XDG_RUNTIME_DIR",
493
+ "HTTP_PROXY",
494
+ "HTTPS_PROXY",
495
+ "NO_PROXY",
496
+ "http_proxy",
497
+ "https_proxy",
498
+ "no_proxy",
499
+ "NODE_EXTRA_CA_CERTS",
500
+ "SSL_CERT_FILE",
501
+ "SSL_CERT_DIR"
502
+ ];
503
+ /**
504
+ * Build a CLI's environment. Precedence, lowest first: inherited variables
505
+ * (minus the adapter's managed home variables), the adapter plan, the API
506
+ * key, then the caller's `env`. Windows variable names compare
507
+ * case-insensitively.
508
+ */
509
+ function buildEnvironment(input) {
510
+ const normalize = (name) => input.platform === "win32" ? name.toUpperCase() : name;
511
+ const managed = new Set(input.managed.map(normalize));
512
+ const allowed = input.inherit === true ? void 0 : new Set([...MINIMAL_ENV, ...input.inherit === false ? [] : input.inherit].map(normalize));
513
+ const environment = {};
514
+ for (const [name, value] of Object.entries(input.source)) {
515
+ if (value === void 0 || managed.has(normalize(name))) continue;
516
+ if (allowed !== void 0 && !allowed.has(normalize(name))) continue;
517
+ environment[name] = value;
518
+ }
519
+ const assign = (name, value) => {
520
+ if (input.platform === "win32") {
521
+ for (const existing of Object.keys(environment)) if (existing.toUpperCase() === name.toUpperCase()) delete environment[existing];
522
+ }
523
+ environment[name] = value;
524
+ };
525
+ for (const [name, value] of Object.entries(input.plan ?? {})) assign(name, value);
526
+ if (input.apiKey !== void 0) assign(input.apiKey.name, input.apiKey.value);
527
+ for (const [name, value] of Object.entries(input.user ?? {})) assign(name, value);
528
+ return environment;
529
+ }
530
+
531
+ //#endregion
532
+ //#region ../../packages/ai-cli/src/session/run-directory.ts
533
+ /**
534
+ * Create the run's private directory and resolve its config home.
535
+ *
536
+ * The run directory is `mkdtemp`'d under the OS temp dir with mode 0700, so
537
+ * other local users cannot read the MCP config (and the tokens in it)
538
+ * written there. A `"private"` home is a `home/` folder inside it.
539
+ */
540
+ async function createRunDirectories(home, cwd) {
541
+ const runDir = await (0, node_fs_promises.mkdtemp)(node_path.default.join((0, node_os.tmpdir)(), "warlock-cli-"));
542
+ await (0, node_fs_promises.chmod)(runDir, 448);
543
+ const dispose = async () => {
544
+ await (0, node_fs_promises.rm)(runDir, {
545
+ recursive: true,
546
+ force: true
547
+ });
548
+ };
549
+ if (home === "inherit") return {
550
+ runDir,
551
+ homeIsPrivate: false,
552
+ dispose
553
+ };
554
+ if (home === "private") {
555
+ const privateHome = node_path.default.join(runDir, "home");
556
+ await (0, node_fs_promises.mkdir)(privateHome, { mode: 448 });
557
+ return {
558
+ runDir,
559
+ home: privateHome,
560
+ homeIsPrivate: true,
561
+ dispose
562
+ };
563
+ }
564
+ const ownedHome = node_path.default.resolve(cwd, home);
565
+ await (0, node_fs_promises.mkdir)(ownedHome, {
566
+ recursive: true,
567
+ mode: 448
568
+ });
569
+ return {
570
+ runDir,
571
+ home: ownedHome,
572
+ homeIsPrivate: false,
573
+ dispose
574
+ };
575
+ }
576
+ /**
577
+ * Write launch files with owner-only permissions (0600; parent directories
578
+ * 0700). Only paths inside the run directory or a private home are
579
+ * accepted, so an adapter can never overwrite a file in a caller-owned
580
+ * home or elsewhere on disk.
581
+ */
582
+ async function writeLaunchFiles(files, directories) {
583
+ const roots = [directories.runDir];
584
+ for (const file of files) {
585
+ const target = node_path.default.resolve(file.path);
586
+ if (!roots.some((root) => {
587
+ const relative = node_path.default.relative(root, target);
588
+ return relative.length > 0 && !relative.startsWith("..") && !node_path.default.isAbsolute(relative);
589
+ })) throw new Error(`Refusing to write ${target}: launch files must live inside the run directory.`);
590
+ await (0, node_fs_promises.mkdir)(node_path.default.dirname(target), {
591
+ recursive: true,
592
+ mode: 448
593
+ });
594
+ await (0, node_fs_promises.writeFile)(target, file.content, { mode: 384 });
595
+ await (0, node_fs_promises.chmod)(target, 384);
596
+ }
597
+ }
598
+
599
+ //#endregion
600
+ //#region ../../packages/ai-cli/src/stream/event-log.ts
601
+ /**
602
+ * An append-only event log that any number of async iterators can read.
603
+ * Each iterator starts at the first event and ends once the log is closed
604
+ * and drained, so a late consumer still sees the whole session.
605
+ */
606
+ var EventLog = class {
607
+ constructor() {
608
+ this.items = [];
609
+ this.closed = false;
610
+ this.waiters = [];
611
+ }
612
+ push(item) {
613
+ if (this.closed) return;
614
+ this.items.push(item);
615
+ this.wake();
616
+ }
617
+ close() {
618
+ this.closed = true;
619
+ this.wake();
620
+ }
621
+ get size() {
622
+ return this.items.length;
623
+ }
624
+ [Symbol.asyncIterator]() {
625
+ let index = 0;
626
+ return { next: async () => {
627
+ while (index >= this.items.length && !this.closed) await new Promise((resolve) => this.waiters.push(resolve));
628
+ if (index < this.items.length) {
629
+ const value = this.items[index];
630
+ index += 1;
631
+ return {
632
+ value,
633
+ done: false
634
+ };
635
+ }
636
+ return {
637
+ value: void 0,
638
+ done: true
639
+ };
640
+ } };
641
+ }
642
+ wake() {
643
+ const waiters = this.waiters;
644
+ this.waiters = [];
645
+ for (const waiter of waiters) waiter();
646
+ }
647
+ };
648
+
649
+ //#endregion
650
+ //#region ../../packages/ai-cli/src/stream/line-splitter.ts
651
+ /** Longest line kept in memory; a longer one is dropped (reported via `onOverflow`). */
652
+ const MAX_LINE_BYTES = 33554432;
653
+ /**
654
+ * Split a byte stream into lines. Multi-byte UTF-8 characters split across
655
+ * chunks are decoded correctly, `\r\n` and `\n` both end a line, and a final
656
+ * line without a newline is flushed by `end()`.
657
+ *
658
+ * @example
659
+ * const splitter = createLineSplitter((line) => console.log(line));
660
+ * splitter.write(Buffer.from('{"a":1}\n{"b"'));
661
+ * splitter.write(Buffer.from(":2}\n"));
662
+ * splitter.end();
663
+ */
664
+ function createLineSplitter(onLine, onOverflow = () => void 0) {
665
+ const decoder = new node_string_decoder.StringDecoder("utf8");
666
+ let pending = "";
667
+ let overflowed = false;
668
+ const consume = (text) => {
669
+ pending += text;
670
+ let newline = pending.indexOf("\n");
671
+ while (newline !== -1) {
672
+ const line = pending.slice(0, newline).replace(/\r$/, "");
673
+ pending = pending.slice(newline + 1);
674
+ if (overflowed) overflowed = false;
675
+ else onLine(line);
676
+ newline = pending.indexOf("\n");
677
+ }
678
+ if (pending.length > 33554432) {
679
+ pending = "";
680
+ if (!overflowed) {
681
+ overflowed = true;
682
+ onOverflow();
683
+ }
684
+ }
685
+ };
686
+ return {
687
+ write(chunk) {
688
+ consume(decoder.write(chunk));
689
+ },
690
+ end() {
691
+ consume(decoder.end());
692
+ if (pending.length > 0 && !overflowed) onLine(pending.replace(/\r$/, ""));
693
+ pending = "";
694
+ }
695
+ };
696
+ }
697
+
698
+ //#endregion
699
+ //#region ../../packages/ai-cli/src/session/aggregate.ts
700
+ function sum(a, b) {
701
+ return a === void 0 && b === void 0 ? void 0 : (a ?? 0) + (b ?? 0);
702
+ }
703
+ /** Add two usages field by field; optional fields stay absent unless either side has them. */
704
+ function addUsage(left, right) {
705
+ if (left === void 0) return { ...right };
706
+ const merged = {
707
+ input: left.input + right.input,
708
+ output: left.output + right.output,
709
+ total: left.total + right.total
710
+ };
711
+ const cachedTokens = sum(left.cachedTokens, right.cachedTokens);
712
+ const cacheWriteTokens = sum(left.cacheWriteTokens, right.cacheWriteTokens);
713
+ const reasoningTokens = sum(left.reasoningTokens, right.reasoningTokens);
714
+ const costUSD = sum(left.costUSD, right.costUSD);
715
+ if (cachedTokens !== void 0) merged.cachedTokens = cachedTokens;
716
+ if (cacheWriteTokens !== void 0) merged.cacheWriteTokens = cacheWriteTokens;
717
+ if (reasoningTokens !== void 0) merged.reasoningTokens = reasoningTokens;
718
+ if (costUSD !== void 0) merged.costUSD = costUSD;
719
+ return merged;
720
+ }
721
+ /**
722
+ * Folds parsed events into the pieces of a `CliResult`: the session ref,
723
+ * usage, every complete message, and the final answer.
724
+ *
725
+ * Final-answer rule: the last `final: true` message (the vendor's own
726
+ * result record) wins; otherwise the newest assistant text — a complete
727
+ * message, or the deltas streamed since the last message or tool activity.
728
+ */
729
+ var ResultAggregator = class {
730
+ constructor(resume) {
731
+ this.messages = [];
732
+ this.deltaBuffer = "";
733
+ this.sessionRef = resume;
734
+ }
735
+ add(event) {
736
+ switch (event.type) {
737
+ case "session.started":
738
+ if (event.sessionRef) this.sessionRef = event.sessionRef;
739
+ break;
740
+ case "message":
741
+ if (event.delta) {
742
+ this.deltaBuffer += event.text;
743
+ break;
744
+ }
745
+ if (event.final) {
746
+ this.finalText = event.text;
747
+ break;
748
+ }
749
+ this.messages.push(event.text);
750
+ this.lastCompleteText = event.text;
751
+ this.deltaBuffer = "";
752
+ break;
753
+ case "tool.call":
754
+ case "tool.result":
755
+ this.flushDeltas();
756
+ break;
757
+ case "usage":
758
+ this.usage = event.cumulative ? { ...event.usage } : addUsage(this.usage, event.usage);
759
+ break;
760
+ case "error": if (event.fatal && this.fatalError === void 0) this.fatalError = event.message;
761
+ }
762
+ }
763
+ /** Close the stream: deltas never followed by a complete message count as one. */
764
+ finish() {
765
+ this.flushDeltas();
766
+ }
767
+ get text() {
768
+ if (this.finalText !== void 0) return this.finalText;
769
+ return this.deltaBuffer.length > 0 ? this.deltaBuffer : this.lastCompleteText ?? "";
770
+ }
771
+ flushDeltas() {
772
+ if (this.deltaBuffer.trim().length > 0) {
773
+ this.messages.push(this.deltaBuffer);
774
+ this.lastCompleteText = this.deltaBuffer;
775
+ }
776
+ this.deltaBuffer = "";
777
+ }
778
+ };
779
+
780
+ //#endregion
781
+ //#region ../../packages/ai-cli/src/session/session.ts
782
+ /**
783
+ * Spawn the CLI and expose it as a {@link CliSession}: stdout lines go
784
+ * through the adapter's parser into a replayable event log, and the
785
+ * process's end becomes the `session.ended` event and the `done()` result.
786
+ */
787
+ function startSession(input) {
788
+ const startedAt = Date.now();
789
+ const log = new EventLog();
790
+ const aggregator = new ResultAggregator(input.resume);
791
+ const controller = new AbortController();
792
+ let stderr = "";
793
+ let timedOut = false;
794
+ let cancelled = false;
795
+ let closed = false;
796
+ let spawnError;
797
+ const emit = (events) => {
798
+ for (const event of events) {
799
+ aggregator.add(event);
800
+ log.push(event);
801
+ }
802
+ };
803
+ const parse = (step) => {
804
+ try {
805
+ emit(step());
806
+ } catch (error) {
807
+ emit([{
808
+ type: "error",
809
+ message: `stream parser failed: ${error instanceof Error ? error.message : String(error)}`
810
+ }]);
811
+ }
812
+ };
813
+ const child = (0, node_child_process.spawn)(input.invocation.file, input.invocation.args, {
814
+ cwd: input.cwd,
815
+ env: input.env,
816
+ shell: false,
817
+ windowsHide: true,
818
+ windowsVerbatimArguments: input.invocation.windowsVerbatimArguments,
819
+ detached: input.platform !== "win32",
820
+ stdio: [
821
+ "pipe",
822
+ "pipe",
823
+ "pipe"
824
+ ]
825
+ });
826
+ let cancelPendingKill;
827
+ const kill = () => {
828
+ cancelPendingKill = killProcessTree(child, input.platform, input.killGraceMs);
829
+ };
830
+ const onAbort = () => {
831
+ if (closed || timedOut || cancelled) return;
832
+ cancelled = true;
833
+ kill();
834
+ };
835
+ input.signal?.addEventListener("abort", onAbort, { once: true });
836
+ controller.signal.addEventListener("abort", onAbort, { once: true });
837
+ if (input.signal?.aborted) onAbort();
838
+ const timer = input.timeoutMs !== void 0 && input.timeoutMs > 0 ? setTimeout(() => {
839
+ if (cancelled) return;
840
+ timedOut = true;
841
+ kill();
842
+ }, input.timeoutMs) : void 0;
843
+ const splitter = createLineSplitter((line) => parse(() => input.parser.line(line)), () => emit([{
844
+ type: "error",
845
+ message: "dropped an output line longer than 32 MiB"
846
+ }]));
847
+ child.stdout?.on("data", (chunk) => splitter.write(chunk));
848
+ child.stderr?.on("data", (chunk) => {
849
+ stderr = (stderr + chunk.toString("utf8")).slice(-65536);
850
+ });
851
+ child.stdin?.on("error", () => void 0);
852
+ child.stdin?.end(input.stdin ?? "");
853
+ const done = new Promise((resolve) => {
854
+ let settled = false;
855
+ const settle = async (exitCode, signal) => {
856
+ if (settled) return;
857
+ settled = true;
858
+ if (timer !== void 0) clearTimeout(timer);
859
+ input.signal?.removeEventListener("abort", onAbort);
860
+ controller.signal.removeEventListener("abort", onAbort);
861
+ splitter.end();
862
+ parse(() => input.parser.end());
863
+ aggregator.finish();
864
+ const lateRef = input.parser.sessionRef?.();
865
+ if (lateRef !== void 0 && lateRef.length > 0) aggregator.sessionRef = lateRef;
866
+ const status = cancelled ? "cancelled" : timedOut ? "timeout" : spawnError === void 0 && exitCode === 0 && aggregator.fatalError === void 0 ? "completed" : "failed";
867
+ let error;
868
+ if (status === "failed") {
869
+ error = aggregator.fatalError ?? (spawnError ? `could not start ${input.invocation.file}: ${spawnError.message}` : `${input.adapter} exited with ${signal ?? `code ${exitCode}`}` + (stderr.trim() ? `: ${stderr.trim().slice(-2e3)}` : ""));
870
+ if (aggregator.fatalError === void 0) emit([{
871
+ type: "error",
872
+ message: error,
873
+ fatal: true
874
+ }]);
875
+ } else if (status === "timeout") error = `${input.adapter} timed out after ${input.timeoutMs} ms`;
876
+ else if (status === "cancelled") error = `${input.adapter} run was cancelled`;
877
+ const durationMs = Date.now() - startedAt;
878
+ log.push({
879
+ type: "session.ended",
880
+ status,
881
+ exitCode,
882
+ signal,
883
+ sessionRef: aggregator.sessionRef,
884
+ durationMs
885
+ });
886
+ log.close();
887
+ try {
888
+ await input.cleanup();
889
+ } catch {}
890
+ resolve({
891
+ status,
892
+ text: aggregator.text,
893
+ messages: [...aggregator.messages],
894
+ usage: aggregator.usage,
895
+ sessionRef: aggregator.sessionRef,
896
+ exitCode,
897
+ signal,
898
+ error,
899
+ stderr,
900
+ durationMs
901
+ });
902
+ };
903
+ child.on("error", (error) => {
904
+ if (child.pid === void 0) {
905
+ closed = true;
906
+ spawnError = error;
907
+ settle(null, null);
908
+ }
909
+ });
910
+ child.on("close", (code, signal) => {
911
+ closed = true;
912
+ cancelPendingKill?.();
913
+ settle(code, signal);
914
+ });
915
+ });
916
+ return {
917
+ adapter: input.adapter,
918
+ pid: child.pid,
919
+ events: log,
920
+ done: () => done,
921
+ cancel: () => controller.abort()
922
+ };
923
+ }
924
+ /**
925
+ * A session for a run whose signal was already aborted: nothing is spawned,
926
+ * the stream is a single `session.ended`, and `done()` resolves `cancelled`.
927
+ */
928
+ function cancelledSession(adapter, resume) {
929
+ const log = new EventLog();
930
+ const error = `${adapter} run was cancelled before it started`;
931
+ log.push({
932
+ type: "session.ended",
933
+ status: "cancelled",
934
+ exitCode: null,
935
+ signal: null,
936
+ sessionRef: resume,
937
+ durationMs: 0
938
+ });
939
+ log.close();
940
+ const result = {
941
+ status: "cancelled",
942
+ text: "",
943
+ messages: [],
944
+ sessionRef: resume,
945
+ exitCode: null,
946
+ signal: null,
947
+ error,
948
+ stderr: "",
949
+ durationMs: 0
950
+ };
951
+ return {
952
+ adapter,
953
+ pid: void 0,
954
+ events: log,
955
+ done: () => Promise.resolve(result),
956
+ cancel: () => void 0
957
+ };
958
+ }
959
+
960
+ //#endregion
961
+ //#region ../../packages/ai-cli/src/spawn/resolve-command.ts
962
+ /** Windows' default `PATHEXT` when the variable is unset. */
963
+ const DEFAULT_PATHEXT = ".COM;.EXE;.BAT;.CMD";
964
+ /** Case-insensitive read on Windows (`Path` vs `PATH`), exact elsewhere. */
965
+ function readEnv(env, name, platform) {
966
+ if (platform !== "win32") return env[name];
967
+ const wanted = name.toUpperCase();
968
+ for (const [key, value] of Object.entries(env)) if (key.toUpperCase() === wanted && value !== void 0) return value;
969
+ }
970
+ function pathApi(platform) {
971
+ return platform === "win32" ? node_path.default.win32 : node_path.default.posix;
972
+ }
973
+ /**
974
+ * Find an executable the way the platform's shell would.
975
+ *
976
+ * POSIX walks `PATH`. Windows walks `PATH` trying each `PATHEXT` extension
977
+ * (`claude` → `claude.cmd`), and also accepts a name that already carries
978
+ * one of those extensions. A command containing a path separator is
979
+ * checked directly instead of searched.
980
+ *
981
+ * @example
982
+ * findExecutable("claude", { platform: "win32", env: { PATH: "C:\\npm", PATHEXT: ".EXE;.CMD" }, ... });
983
+ * // → "C:\\npm\\claude.cmd"
984
+ */
985
+ function findExecutable(command, lookup) {
986
+ const api = pathApi(lookup.platform);
987
+ const isWindows = lookup.platform === "win32";
988
+ const extensions = isWindows ? (readEnv(lookup.env, "PATHEXT", "win32") ?? DEFAULT_PATHEXT).split(";").map((extension) => extension.trim()).filter((extension) => extension.length > 0) : [];
989
+ const candidatesFor = (base) => {
990
+ if (!isWindows) return [base];
991
+ const hasKnownExtension = extensions.some((extension) => base.toUpperCase().endsWith(extension.toUpperCase()));
992
+ const withExtensions = extensions.map((extension) => base + extension.toLowerCase());
993
+ return hasKnownExtension ? [base, ...withExtensions] : withExtensions;
994
+ };
995
+ if ((isWindows ? /[\\/]/.test(command) : command.includes("/")) || api.isAbsolute(command)) return candidatesFor(api.resolve(command)).find((candidate) => lookup.isFile(candidate));
996
+ const directories = (readEnv(lookup.env, "PATH", lookup.platform) ?? "").split(isWindows ? ";" : ":").map((directory) => directory.trim().replace(/^"(.*)"$/, "$1")).filter((directory) => directory.length > 0);
997
+ for (const directory of directories) for (const candidate of candidatesFor(api.join(directory, command))) if (lookup.isFile(candidate)) return candidate;
998
+ }
999
+ /**
1000
+ * Read the target of an npm (`cmd-shim`) or pnpm `.cmd` shim.
1001
+ *
1002
+ * Both generators end the shim with a line that runs the target with `%*`;
1003
+ * the target is a quoted path relative to the shim's own directory
1004
+ * (`"%dp0%\node_modules\pkg\cli.js"` for npm, `"%~dp0\..\pkg\cli.js"` for
1005
+ * pnpm). A JavaScript target is run by Node; an `.exe`/`.com` target is run
1006
+ * directly. Anything else returns `undefined` and the caller falls back to
1007
+ * `cmd.exe`.
1008
+ */
1009
+ function parseCmdShim(content, shimPath) {
1010
+ const directory = node_path.default.win32.dirname(shimPath);
1011
+ const line = content.split(/\r?\n/).filter((line) => line.includes("%*")).at(-1);
1012
+ if (line === void 0) return;
1013
+ const raw = [...line.matchAll(/"(%~?dp0%?[\\/][^"]+)"/gi)].map((match) => match[1] ?? "").filter((target) => !/[\\/]node(\.exe)?$/i.test(target)).at(-1);
1014
+ if (raw === void 0) return;
1015
+ const relative = raw.replace(/^%~?dp0%?[\\/]/i, "");
1016
+ const target = node_path.default.win32.resolve(directory, relative);
1017
+ if (/\.(c|m)?js$/i.test(target)) return {
1018
+ type: "node",
1019
+ script: target
1020
+ };
1021
+ if (/\.(exe|com)$/i.test(target)) return {
1022
+ type: "native",
1023
+ file: target
1024
+ };
1025
+ }
1026
+ /**
1027
+ * Resolve a command to something `child_process.spawn` can start without a
1028
+ * shell. On Windows a `.cmd`/`.bat` npm shim is unwrapped to
1029
+ * `node <script>` (or its native `.exe`), so argv reaches the CLI intact
1030
+ * with no `cmd.exe` parsing in between; any other batch file falls back
1031
+ * to `cmd.exe`.
1032
+ */
1033
+ function resolveSpawnTarget(command, lookup) {
1034
+ const executable = findExecutable(command, lookup);
1035
+ if (executable === void 0) return;
1036
+ if (lookup.platform !== "win32" || !/\.(cmd|bat)$/i.test(executable)) return {
1037
+ type: "direct",
1038
+ file: executable,
1039
+ prefixArgs: [],
1040
+ executable
1041
+ };
1042
+ const shim = parseCmdShim(lookup.readFile(executable) ?? "", executable);
1043
+ if (shim?.type === "node") {
1044
+ const bundledNode = node_path.default.win32.join(node_path.default.win32.dirname(executable), "node.exe");
1045
+ return {
1046
+ type: "direct",
1047
+ file: lookup.isFile(bundledNode) ? bundledNode : lookup.execPath,
1048
+ prefixArgs: [shim.script],
1049
+ executable
1050
+ };
1051
+ }
1052
+ if (shim?.type === "native" && lookup.isFile(shim.file)) return {
1053
+ type: "direct",
1054
+ file: shim.file,
1055
+ prefixArgs: [],
1056
+ executable
1057
+ };
1058
+ return {
1059
+ type: "cmd",
1060
+ file: executable,
1061
+ executable
1062
+ };
1063
+ }
1064
+
1065
+ //#endregion
1066
+ //#region ../../packages/ai-cli/src/cli-agent.ts
1067
+ function isRunnableFile(filePath, platform) {
1068
+ try {
1069
+ if (!(0, node_fs.statSync)(filePath).isFile()) return false;
1070
+ if (platform !== "win32") (0, node_fs.accessSync)(filePath, node_fs.constants.X_OK);
1071
+ return true;
1072
+ } catch {
1073
+ return false;
1074
+ }
1075
+ }
1076
+ /** The current machine as a {@link CliHost}. */
1077
+ function currentHost() {
1078
+ const platform = process.platform;
1079
+ return {
1080
+ platform,
1081
+ env: process.env,
1082
+ isFile: (filePath) => isRunnableFile(filePath, platform),
1083
+ readFile: (filePath) => {
1084
+ try {
1085
+ return (0, node_fs.readFileSync)(filePath, "utf8");
1086
+ } catch {
1087
+ return;
1088
+ }
1089
+ },
1090
+ execPath: process.execPath,
1091
+ comspec: readEnv(process.env, "ComSpec", platform) ?? "cmd.exe"
1092
+ };
1093
+ }
1094
+ function unsupported(adapter, message) {
1095
+ return new CliError(`${adapter.title}: ${message}`, {
1096
+ type: "unsupported-option",
1097
+ adapter: adapter.name
1098
+ });
1099
+ }
1100
+ /**
1101
+ * Create a CLI agent from an adapter. The vendor factories
1102
+ * (`cli.claudeCode(...)`, `cli.codex(...)`, …) call this with their
1103
+ * built-in adapter; call it directly with your own {@link CliAdapter}.
1104
+ *
1105
+ * @example
1106
+ * const agent = createCliAgent(myAdapter, { model: "fast", permission: "standard" });
1107
+ * const session = await agent.run("Summarize README.md", { cwd: "/srv/repo" });
1108
+ * console.log((await session.done()).text);
1109
+ */
1110
+ function createCliAgent(adapter, options = {}, host = currentHost()) {
1111
+ const probes = /* @__PURE__ */ new Map();
1112
+ const command = options.command ?? adapter.command;
1113
+ const resolveTarget = () => {
1114
+ const target = resolveSpawnTarget(command, host);
1115
+ if (target === void 0) throw new CliError(`${adapter.title}: could not find "${command}" on PATH. Install the CLI or pass its absolute path as \`command\`.`, {
1116
+ type: "not-found",
1117
+ adapter: adapter.name
1118
+ });
1119
+ return target;
1120
+ };
1121
+ const probeEnvironment = () => buildEnvironment({
1122
+ source: host.env,
1123
+ inherit: options.inheritEnv ?? true,
1124
+ managed: [],
1125
+ user: options.env,
1126
+ platform: host.platform
1127
+ });
1128
+ const probeTarget = (target) => {
1129
+ const cached = probes.get(target.executable);
1130
+ if (cached !== void 0) return cached;
1131
+ const pending = probeCli({
1132
+ adapter,
1133
+ target,
1134
+ env: probeEnvironment(),
1135
+ cwd: node_path.default.resolve(options.cwd ?? process.cwd()),
1136
+ comspec: host.comspec,
1137
+ platform: host.platform
1138
+ });
1139
+ probes.set(target.executable, pending);
1140
+ pending.catch(() => probes.delete(target.executable));
1141
+ return pending;
1142
+ };
1143
+ const run = async (prompt, runOptions = {}) => {
1144
+ const merged = {
1145
+ ...options,
1146
+ ...runOptions,
1147
+ env: {
1148
+ ...options.env,
1149
+ ...runOptions.env
1150
+ },
1151
+ args: [...options.args ?? [], ...runOptions.args ?? []],
1152
+ mcp: runOptions.mcp ?? options.mcp
1153
+ };
1154
+ const permission = merged.permission ?? "standard";
1155
+ if (permission !== "standard" && permission !== "bypass") throw unsupported(adapter, `unknown permission "${String(permission)}" (use "standard" or "bypass").`);
1156
+ if (merged.effort !== void 0 && adapter.effort !== true) throw unsupported(adapter, "this CLI has no reasoning-effort control; drop `effort`.");
1157
+ const mcp = merged.mcp !== void 0 && Object.keys(merged.mcp).length > 0 ? merged.mcp : void 0;
1158
+ if (mcp !== void 0 && adapter.mcp !== true) throw unsupported(adapter, "this CLI cannot attach per-run MCP servers; drop `mcp`.");
1159
+ if (merged.apiKey !== void 0 && adapter.apiKeyEnv.length === 0) throw unsupported(adapter, "this CLI reads no API key from the environment; drop `apiKey`.");
1160
+ const home = options.home ?? "private";
1161
+ if (home !== "inherit" && adapter.homeEnv.length === 0) throw unsupported(adapter, "this CLI cannot relocate its config home; use `home: \"inherit\"`.");
1162
+ const cwd = node_path.default.resolve(merged.cwd ?? process.cwd());
1163
+ const target = resolveTarget();
1164
+ const probe = options.probe === false ? void 0 : await probeTarget(target);
1165
+ if (merged.signal?.aborted) return cancelledSession(adapter.name, merged.resume);
1166
+ const directories = await createRunDirectories(home, cwd);
1167
+ try {
1168
+ let chosen;
1169
+ const reasons = [];
1170
+ for (const transport of adapter.transports) {
1171
+ const context = {
1172
+ prompt,
1173
+ transport,
1174
+ cwd,
1175
+ model: merged.model,
1176
+ effort: merged.effort,
1177
+ permission,
1178
+ resume: merged.resume,
1179
+ mcp,
1180
+ apiKey: merged.apiKey,
1181
+ home: directories.home,
1182
+ homeIsPrivate: directories.homeIsPrivate,
1183
+ runDir: directories.runDir,
1184
+ platform: host.platform
1185
+ };
1186
+ const plan = adapter.plan(context);
1187
+ if (probe !== void 0) assertCapabilities(adapter, probe, plan.requires);
1188
+ const invocation = buildInvocation(target, [
1189
+ ...plan.args,
1190
+ ...merged.args ?? [],
1191
+ ...plan.tail ?? []
1192
+ ], host.comspec);
1193
+ if ("refused" in invocation) {
1194
+ reasons.push(`${transport}: ${invocation.refused}`);
1195
+ continue;
1196
+ }
1197
+ if (!fitsCommandLine(invocation, host.platform)) {
1198
+ reasons.push(`${transport}: the command line exceeds the platform limit`);
1199
+ continue;
1200
+ }
1201
+ chosen = {
1202
+ plan,
1203
+ invocation,
1204
+ context
1205
+ };
1206
+ break;
1207
+ }
1208
+ if (chosen === void 0) throw new CliError(`${adapter.title}: the prompt cannot be passed to "${target.executable}" (${reasons.join("; ")}).`, {
1209
+ type: "prompt-transport",
1210
+ adapter: adapter.name,
1211
+ executable: target.executable
1212
+ });
1213
+ await writeLaunchFiles(chosen.plan.files ?? [], directories);
1214
+ const apiKeyEnv = adapter.apiKeyEnv[0];
1215
+ const env = buildEnvironment({
1216
+ source: host.env,
1217
+ inherit: options.inheritEnv ?? true,
1218
+ managed: home === "inherit" ? [] : adapter.homeEnv,
1219
+ plan: host.platform === "win32" ? chosen.plan.env : {
1220
+ PWD: cwd,
1221
+ ...chosen.plan.env
1222
+ },
1223
+ user: merged.env,
1224
+ apiKey: merged.apiKey !== void 0 && apiKeyEnv !== void 0 ? {
1225
+ name: apiKeyEnv,
1226
+ value: merged.apiKey
1227
+ } : void 0,
1228
+ platform: host.platform
1229
+ });
1230
+ return startSession({
1231
+ adapter: adapter.name,
1232
+ invocation: chosen.invocation,
1233
+ cwd,
1234
+ env,
1235
+ stdin: chosen.plan.stdin,
1236
+ parser: adapter.parser(chosen.context),
1237
+ signal: merged.signal,
1238
+ timeoutMs: merged.timeoutMs,
1239
+ killGraceMs: options.killGraceMs ?? 2e3,
1240
+ platform: host.platform,
1241
+ resume: merged.resume,
1242
+ cleanup: directories.dispose
1243
+ });
1244
+ } catch (error) {
1245
+ await directories.dispose();
1246
+ throw error;
1247
+ }
1248
+ };
1249
+ const agent = {
1250
+ adapter,
1251
+ options,
1252
+ run,
1253
+ probe: () => probeTarget(resolveTarget()),
1254
+ model: (config) => cliModel(agent, config)
1255
+ };
1256
+ return agent;
1257
+ }
1258
+
1259
+ //#endregion
1260
+ //#region ../../packages/ai-cli/src/stream/json-lines.ts
1261
+ /** `value` as a JSON object, or `undefined`. */
1262
+ function asRecord(value) {
1263
+ return typeof value === "object" && value !== null && !Array.isArray(value) ? value : void 0;
1264
+ }
1265
+ /** `value` as an array, or an empty array. */
1266
+ function asArray(value) {
1267
+ return Array.isArray(value) ? value : [];
1268
+ }
1269
+ /** A string field, or `undefined`. */
1270
+ function stringField(record, key) {
1271
+ const value = record?.[key];
1272
+ return typeof value === "string" ? value : void 0;
1273
+ }
1274
+ /** A finite number field, or `undefined`. */
1275
+ function numberField(record, key) {
1276
+ const value = record?.[key];
1277
+ return typeof value === "number" && Number.isFinite(value) ? value : void 0;
1278
+ }
1279
+ /** A nested object field, or `undefined`. */
1280
+ function recordField(record, key) {
1281
+ return asRecord(record?.[key]);
1282
+ }
1283
+ /** Tool output as text: strings verbatim, text-block arrays joined, anything else as JSON. */
1284
+ function outputText(value) {
1285
+ if (typeof value === "string") return value;
1286
+ if (value === void 0 || value === null) return "";
1287
+ if (Array.isArray(value)) return value.map((item) => {
1288
+ return stringField(asRecord(item), "text") ?? (typeof item === "string" ? item : JSON.stringify(item));
1289
+ }).join("\n");
1290
+ return JSON.stringify(value);
1291
+ }
1292
+ /** Parse one line as a JSON object; anything else (banners, blank lines) is `undefined`. */
1293
+ function parseJsonLine(line) {
1294
+ const trimmed = line.trim();
1295
+ if (!trimmed.startsWith("{")) return;
1296
+ try {
1297
+ return asRecord(JSON.parse(trimmed));
1298
+ } catch {
1299
+ return;
1300
+ }
1301
+ }
1302
+ /**
1303
+ * Build a parser for a JSONL/NDJSON stream. Non-JSON lines (progress
1304
+ * banners, warnings) are skipped; each JSON object goes to `onRecord`.
1305
+ * `onEnd` may flush buffered state when stdout closes.
1306
+ *
1307
+ * @example
1308
+ * const parser = jsonLinesParser((record) =>
1309
+ * record.type === "text" ? [{ type: "message", text: String(record.text), delta: false }] : [],
1310
+ * );
1311
+ */
1312
+ function jsonLinesParser(onRecord, onEnd = () => []) {
1313
+ return {
1314
+ line(line) {
1315
+ const record = parseJsonLine(line);
1316
+ return record === void 0 ? [] : onRecord(record);
1317
+ },
1318
+ end: onEnd
1319
+ };
1320
+ }
1321
+
1322
+ //#endregion
1323
+ //#region ../../packages/ai-cli/src/utils/toml.ts
1324
+ function isTable(value) {
1325
+ return typeof value === "object" && !Array.isArray(value);
1326
+ }
1327
+ /** A bare key when possible, else a quoted one. */
1328
+ function tomlKey(key) {
1329
+ return /^[A-Za-z0-9_-]+$/.test(key) ? key : JSON.stringify(key);
1330
+ }
1331
+ /** A TOML scalar or inline value. JSON string escapes are valid TOML basic-string escapes. */
1332
+ function inlineValue(value) {
1333
+ if (typeof value === "string") return JSON.stringify(value);
1334
+ if (typeof value === "number") {
1335
+ if (!Number.isFinite(value)) throw new Error("TOML cannot encode a non-finite number");
1336
+ return String(value);
1337
+ }
1338
+ if (typeof value === "boolean") return value ? "true" : "false";
1339
+ if (Array.isArray(value)) return `[${value.map(inlineValue).join(", ")}]`;
1340
+ return `{ ${Object.entries(value).filter((entry) => entry[1] !== void 0).map(([key, item]) => `${tomlKey(key)} = ${inlineValue(item)}`).join(", ")} }`;
1341
+ }
1342
+ /**
1343
+ * Serialize a table to TOML: scalars and arrays first, then each nested
1344
+ * table as a `[dotted.header]` section. Enough for the CLI config files
1345
+ * this package writes (Codex and Mistral Vibe MCP sections).
1346
+ *
1347
+ * @example
1348
+ * toToml({ mcp_servers: { docs: { command: "npx", args: ["-y", "docs-mcp"] } } });
1349
+ * // [mcp_servers.docs]
1350
+ * // command = "npx"
1351
+ * // args = ["-y", "docs-mcp"]
1352
+ */
1353
+ function toToml(table, prefix = []) {
1354
+ const lines = [];
1355
+ const nested = [];
1356
+ for (const [key, value] of Object.entries(table)) {
1357
+ if (value === void 0) continue;
1358
+ if (isTable(value)) {
1359
+ nested.push([key, value]);
1360
+ continue;
1361
+ }
1362
+ lines.push(`${tomlKey(key)} = ${inlineValue(value)}`);
1363
+ }
1364
+ const sections = [];
1365
+ if (lines.length > 0) sections.push((prefix.length > 0 ? `[${prefix.map(tomlKey).join(".")}]\n` : "") + lines.join("\n"));
1366
+ for (const [key, value] of nested) {
1367
+ const section = toToml(value, [...prefix, key]);
1368
+ if (section.length > 0) sections.push(section.trimEnd());
1369
+ }
1370
+ return sections.length > 0 ? `${sections.join("\n\n")}\n` : "";
1371
+ }
1372
+
1373
+ //#endregion
1374
+ //#region ../../packages/ai-cli/src/adapters/shared.ts
1375
+ /**
1376
+ * The prompt as an argv value. A prompt starting with `-` would be read as a
1377
+ * flag by the CLI's argument parser, so it gets one leading space (harmless
1378
+ * to the model, and no parser treats it as an option).
1379
+ */
1380
+ function argvPrompt(prompt) {
1381
+ return prompt.startsWith("-") ? ` ${prompt}` : prompt;
1382
+ }
1383
+ /** The prompt written to `prompt.txt` in the run directory, for `transport: "file"`. */
1384
+ function promptFile(context) {
1385
+ return {
1386
+ path: node_path.default.join(context.runDir, "prompt.txt"),
1387
+ content: context.prompt
1388
+ };
1389
+ }
1390
+ /**
1391
+ * Fail closed when an option needs a file inside the CLI's config home but
1392
+ * the home is not private to this run (writing it into a caller-owned or
1393
+ * inherited home would leak tokens into it and race concurrent runs).
1394
+ */
1395
+ function requirePrivateHome(context, adapter, option) {
1396
+ if (context.home === void 0 || !context.homeIsPrivate) throw new CliError(`${adapter.title} reads ${option} only from its config home; use \`home: "private"\` (the default) for runs with ${option}.`, {
1397
+ type: "unsupported-option",
1398
+ adapter: adapter.name
1399
+ });
1400
+ return context.home;
1401
+ }
1402
+ /** A `.env` file body. Values are written verbatim; newlines are refused. */
1403
+ function dotenvFile(values) {
1404
+ return Object.entries(values).map(([name, value]) => {
1405
+ if (/[\r\n]/.test(value)) throw new Error(`${name} contains a newline and cannot be written to a .env file`);
1406
+ return `${name}=${value}`;
1407
+ }).join("\n").concat("\n");
1408
+ }
1409
+ /** The framework effort scale, lowest first. */
1410
+ const EFFORT_ORDER = [
1411
+ "none",
1412
+ "minimal",
1413
+ "low",
1414
+ "medium",
1415
+ "high",
1416
+ "xhigh",
1417
+ "max"
1418
+ ];
1419
+ /**
1420
+ * Map a framework effort to a vendor's levels, following the framework
1421
+ * rule: an exact level wins; otherwise the nearest higher level the vendor
1422
+ * has (so `minimal` becomes `low`); a level above the vendor's highest is
1423
+ * clamped to it.
1424
+ *
1425
+ * @example
1426
+ * mapEffort("xhigh", { low: "low", medium: "medium", high: "high" }); // "high"
1427
+ * mapEffort("minimal", { low: "low", medium: "medium", high: "high" }); // "low"
1428
+ */
1429
+ function mapEffort(effort, levels) {
1430
+ const exact = levels[effort];
1431
+ if (exact !== void 0) return exact;
1432
+ const requested = EFFORT_ORDER.indexOf(effort);
1433
+ const supported = EFFORT_ORDER.filter((level) => levels[level] !== void 0);
1434
+ const chosen = supported.find((level) => EFFORT_ORDER.indexOf(level) > requested) ?? supported.at(-1);
1435
+ const value = chosen === void 0 ? void 0 : levels[chosen];
1436
+ if (value === void 0) throw new Error("mapEffort needs at least one vendor level");
1437
+ return value;
1438
+ }
1439
+ /**
1440
+ * The common `mcpServers` JSON shape (Claude Code, Cursor, Kimi, Droid,
1441
+ * Copilot, Amp, Qwen Code …): stdio servers as `command`/`args`/`env`,
1442
+ * remote servers as `url`/`headers`. `typeField` adds the `type` key the
1443
+ * vendor expects (`"stdio"`, `"http"`, `"sse"`), or a renamed remote
1444
+ * transport (`"streamable-http"`).
1445
+ */
1446
+ function mcpServersJson(servers, options = {}) {
1447
+ const rendered = {};
1448
+ for (const [name, server] of Object.entries(servers)) {
1449
+ if (server.type === "stdio") {
1450
+ rendered[name] = {
1451
+ ...options.typeField ? { type: "stdio" } : {},
1452
+ command: server.command,
1453
+ args: server.args ?? [],
1454
+ ...server.env !== void 0 ? { env: server.env } : {},
1455
+ ...options.extra
1456
+ };
1457
+ continue;
1458
+ }
1459
+ const urlKey = server.type === "http" ? options.httpUrlKey ?? "url" : "url";
1460
+ rendered[name] = {
1461
+ ...options.typeField ? { type: server.type === "http" ? options.httpType ?? "http" : "sse" } : {},
1462
+ [urlKey]: server.url,
1463
+ ...server.headers !== void 0 ? { headers: server.headers } : {},
1464
+ ...options.extra
1465
+ };
1466
+ }
1467
+ return rendered;
1468
+ }
1469
+ /** JSON for a config file, pretty-printed with a trailing newline. */
1470
+ function jsonFile(value) {
1471
+ return `${JSON.stringify(value, null, 2)}\n`;
1472
+ }
1473
+ /** Build a `CliUsage`, computing `total` when the vendor omits it. */
1474
+ function usageOf(fields) {
1475
+ const input = fields.input ?? 0;
1476
+ const output = fields.output ?? 0;
1477
+ const usage = {
1478
+ input,
1479
+ output,
1480
+ total: fields.total ?? input + output
1481
+ };
1482
+ if (fields.cachedTokens !== void 0) usage.cachedTokens = fields.cachedTokens;
1483
+ if (fields.cacheWriteTokens !== void 0) usage.cacheWriteTokens = fields.cacheWriteTokens;
1484
+ if (fields.reasoningTokens !== void 0) usage.reasoningTokens = fields.reasoningTokens;
1485
+ if (fields.costUSD !== void 0) usage.costUSD = fields.costUSD;
1486
+ return usage;
1487
+ }
1488
+ /** An `unsupported-option` error for an adapter. */
1489
+ function unsupportedOption(adapter, message) {
1490
+ return new CliError(`${adapter.title}: ${message}`, {
1491
+ type: "unsupported-option",
1492
+ adapter: adapter.name
1493
+ });
1494
+ }
1495
+ /** Parse a JSON-encoded tool-arguments string; keeps the raw string when it is not JSON. */
1496
+ function parseArguments(value) {
1497
+ if (typeof value !== "string") return value;
1498
+ try {
1499
+ return JSON.parse(value);
1500
+ } catch {
1501
+ return value;
1502
+ }
1503
+ }
1504
+ /**
1505
+ * Usage in the Anthropic Messages shape (`input_tokens` excludes cache reads
1506
+ * and writes, which are reported separately). `input` is the sum of all
1507
+ * three, matching the framework rule that `input` includes cached tokens.
1508
+ */
1509
+ function anthropicUsage(usage, costUSD) {
1510
+ if (usage === void 0) return;
1511
+ const read = (key) => {
1512
+ const value = usage[key];
1513
+ return typeof value === "number" && Number.isFinite(value) ? value : void 0;
1514
+ };
1515
+ const uncached = read("input_tokens") ?? 0;
1516
+ const cacheRead = read("cache_read_input_tokens");
1517
+ const cacheWrite = read("cache_creation_input_tokens");
1518
+ const output = read("output_tokens") ?? 0;
1519
+ return usageOf({
1520
+ input: uncached + (cacheRead ?? 0) + (cacheWrite ?? 0),
1521
+ output,
1522
+ cachedTokens: cacheRead !== void 0 && cacheRead > 0 ? cacheRead : void 0,
1523
+ cacheWriteTokens: cacheWrite !== void 0 && cacheWrite > 0 ? cacheWrite : void 0,
1524
+ reasoningTokens: read("reasoning_tokens") || void 0,
1525
+ costUSD
1526
+ });
1527
+ }
1528
+
1529
+ //#endregion
1530
+ //#region ../../packages/ai-cli/src/adapters/aider.ts
1531
+ /** Local analytics log the adapter asks Aider to write (exact token counts). */
1532
+ const ANALYTICS_LOG = "aider-analytics.jsonl";
1533
+ /**
1534
+ * Aider (`aider`, PyPI `aider-chat`).
1535
+ *
1536
+ * Verified against the official docs (`aider.chat/docs/scripting.html`,
1537
+ * `/docs/config/options.html`, `/docs/config/reasoning.html`) and the
1538
+ * source at tag v0.86.2 (`aider/main.py`, `aider/args.py`,
1539
+ * `aider/coders/base_coder.py`, `aider/analytics.py`); Aider needs Python
1540
+ * 3.10+, so no real `--help` was run, 2026-10-17. The capability probe
1541
+ * checks every flag against the installed `--help`.
1542
+ *
1543
+ * - Headless: `aider --message <text>` (or `--message-file`). Aider has **no
1544
+ * machine-readable output**: the parser reads the plain-text stdout —
1545
+ * startup banner (`Main model: …`), the reply, the usage line
1546
+ * (`Tokens: 2.4k sent, 61 received. Cost: $0.0081 message, …`),
1547
+ * `Applied edit to <path>` (reported as an `edit` tool call) and
1548
+ * `Commit <hash> <message>`. Exact token counts and cost come from the
1549
+ * local `--analytics-log` file (`message_send` events), read when the
1550
+ * stream ends.
1551
+ * - Aider exits 0 even when the model call fails, so `litellm.*Error` lines
1552
+ * and the "not able to authenticate you" notice are reported as fatal.
1553
+ * - Permissions: Aider applies edits without approval and, headless with
1554
+ * `--yes-always`, never runs the shell commands it suggests; there is no
1555
+ * stricter or looser mode, so `standard` and `bypass` behave the same.
1556
+ * Auto-commits are turned off (`--no-auto-commits`); pass
1557
+ * `args: ["--auto-commits"]` to keep them.
1558
+ * - No session ids (`resume` is refused) and no MCP. Chat and input
1559
+ * history are written to the run directory, not the repository.
1560
+ * - Home: Aider uses `HOME`; provider keys use their own variables
1561
+ * (`OPENAI_API_KEY`, `ANTHROPIC_API_KEY`, …) via `env`. Always pass
1562
+ * `model` — without one Aider may start an interactive OAuth flow.
1563
+ */
1564
+ const aiderAdapter = {
1565
+ name: "aider",
1566
+ title: "Aider",
1567
+ command: "aider",
1568
+ apiKeyEnv: [],
1569
+ homeEnv: ["HOME", "USERPROFILE"],
1570
+ transports: ["argv", "file"],
1571
+ effort: true,
1572
+ mcp: false,
1573
+ probe: {
1574
+ help: [["--help"]],
1575
+ version: ["--version"]
1576
+ },
1577
+ verification: {
1578
+ version: "0.86.2",
1579
+ verifiedBy: "docs",
1580
+ sources: [
1581
+ "https://aider.chat/docs/scripting.html",
1582
+ "https://aider.chat/docs/config/options.html",
1583
+ "https://aider.chat/docs/config/reasoning.html",
1584
+ "https://github.com/Aider-AI/aider/tree/v0.86.2"
1585
+ ],
1586
+ checkedAt: "2026-10-17"
1587
+ },
1588
+ plan(context) {
1589
+ if (context.resume !== void 0) throw unsupportedOption(aiderAdapter, "Aider has no session ids, so `resume` is not supported.");
1590
+ const args = [
1591
+ "--yes-always",
1592
+ "--no-pretty",
1593
+ "--no-stream",
1594
+ "--no-fancy-input",
1595
+ "--no-auto-commits",
1596
+ "--no-check-update",
1597
+ "--no-show-release-notes",
1598
+ "--no-analytics",
1599
+ "--no-show-model-warnings",
1600
+ "--no-gitignore",
1601
+ "--analytics-log",
1602
+ node_path.default.join(context.runDir, ANALYTICS_LOG),
1603
+ "--chat-history-file",
1604
+ node_path.default.join(context.runDir, "chat-history.md"),
1605
+ "--input-history-file",
1606
+ node_path.default.join(context.runDir, "input-history")
1607
+ ];
1608
+ const requires = [
1609
+ "--yes-always",
1610
+ "--no-pretty",
1611
+ "--no-stream",
1612
+ "--no-auto-commits",
1613
+ "--analytics-log",
1614
+ "--chat-history-file",
1615
+ "--input-history-file"
1616
+ ];
1617
+ const files = [];
1618
+ let tail;
1619
+ if (context.model !== void 0) {
1620
+ args.push("--model", context.model);
1621
+ requires.push("--model");
1622
+ }
1623
+ if (context.effort !== void 0) {
1624
+ args.push("--reasoning-effort", mapEffort(context.effort, {
1625
+ low: "low",
1626
+ medium: "medium",
1627
+ high: "high"
1628
+ }));
1629
+ requires.push("--reasoning-effort");
1630
+ }
1631
+ if (context.transport === "file") {
1632
+ const prompt = promptFile(context);
1633
+ files.push(prompt);
1634
+ tail = ["--message-file", prompt.path];
1635
+ requires.push("--message-file");
1636
+ } else {
1637
+ tail = ["--message", argvPrompt(context.prompt)];
1638
+ requires.push("--message");
1639
+ }
1640
+ return {
1641
+ args,
1642
+ tail,
1643
+ env: {
1644
+ ...context.home !== void 0 ? {
1645
+ HOME: context.home,
1646
+ USERPROFILE: context.home
1647
+ } : {},
1648
+ TERM: "dumb"
1649
+ },
1650
+ files,
1651
+ requires
1652
+ };
1653
+ },
1654
+ parser: createAiderParser
1655
+ };
1656
+ /** `2.4k` → 2400, `12k` → 12000, `61` → 61. */
1657
+ function parseAiderCount(value) {
1658
+ if (value === void 0) return;
1659
+ const match = /^([\d.]+)(k|M)?$/.exec(value.trim());
1660
+ if (!match) return;
1661
+ const number = Number(match[1]);
1662
+ const scale = match[2] === "k" ? 1e3 : match[2] === "M" ? 1e6 : 1;
1663
+ return Number.isFinite(number) ? Math.round(number * scale) : void 0;
1664
+ }
1665
+ const TOKENS_LINE = /^Tokens: ([\d.]+[kM]?) sent(?:, ([\d.]+[kM]?) cache write)?(?:, ([\d.]+[kM]?) cache hit)?, ([\d.]+[kM]?) received\.(?:\s+Cost: \$([\d.]+) message, \$([\d.]+) session\.)?/;
1666
+ const BANNER = [
1667
+ /^Aider v\d/,
1668
+ /^Main model: /,
1669
+ /^Weak model: /,
1670
+ /^Editor model: /,
1671
+ /^Model: /,
1672
+ /^Git repo: /,
1673
+ /^Repo-map: /,
1674
+ /^Added .+ to the chat\.$/,
1675
+ /^Cost: \$[\d.]+ message, \$[\d.]+ session\.$/,
1676
+ /^Use \/help /
1677
+ ];
1678
+ /** Usage from the analytics log (`message_send` events), summed. */
1679
+ function analyticsUsage(context) {
1680
+ if (context === void 0) return;
1681
+ let content;
1682
+ try {
1683
+ content = (0, node_fs.readFileSync)(node_path.default.join(context.runDir, ANALYTICS_LOG), "utf8");
1684
+ } catch {
1685
+ return;
1686
+ }
1687
+ let input = 0;
1688
+ let output = 0;
1689
+ let cost;
1690
+ let seen = false;
1691
+ for (const line of content.split(/\r?\n/)) {
1692
+ const record = parseJsonLine(line);
1693
+ if (stringField(record, "event") !== "message_send") continue;
1694
+ const properties = recordField(record, "properties");
1695
+ seen = true;
1696
+ input += numberField(properties, "prompt_tokens") ?? 0;
1697
+ output += numberField(properties, "completion_tokens") ?? 0;
1698
+ const messageCost = numberField(properties, "cost");
1699
+ if (messageCost !== void 0) cost = (cost ?? 0) + messageCost;
1700
+ }
1701
+ return seen ? {
1702
+ type: "usage",
1703
+ cumulative: true,
1704
+ usage: usageOf({
1705
+ input,
1706
+ output,
1707
+ costUSD: cost
1708
+ })
1709
+ } : void 0;
1710
+ }
1711
+ /** Parser for Aider's plain-text stdout (plus its analytics log). */
1712
+ function createAiderParser(context) {
1713
+ let started = false;
1714
+ let body = [];
1715
+ const flush = () => {
1716
+ const text = body.join("\n").trim();
1717
+ body = [];
1718
+ return text.length > 0 ? [{
1719
+ type: "message",
1720
+ text,
1721
+ delta: false
1722
+ }] : [];
1723
+ };
1724
+ return {
1725
+ line(line) {
1726
+ const events = [];
1727
+ const model = /^Main model: (\S+)/.exec(line);
1728
+ if (!started && (model || line.trim().length > 0 && !BANNER.some((pattern) => pattern.test(line)))) {
1729
+ started = true;
1730
+ events.push({
1731
+ type: "session.started",
1732
+ model: model?.[1]
1733
+ });
1734
+ }
1735
+ if (BANNER.some((pattern) => pattern.test(line))) return events;
1736
+ const tokens = TOKENS_LINE.exec(line);
1737
+ if (tokens) {
1738
+ const cacheWrite = parseAiderCount(tokens[2]);
1739
+ const cacheHit = parseAiderCount(tokens[3]);
1740
+ const sent = parseAiderCount(tokens[1]) ?? 0;
1741
+ events.push(...flush(), {
1742
+ type: "usage",
1743
+ cumulative: false,
1744
+ usage: usageOf({
1745
+ input: sent,
1746
+ output: parseAiderCount(tokens[4]),
1747
+ cachedTokens: cacheHit,
1748
+ cacheWriteTokens: cacheWrite,
1749
+ costUSD: tokens[5] !== void 0 ? Number(tokens[5]) : void 0
1750
+ })
1751
+ });
1752
+ return events;
1753
+ }
1754
+ const applied = /^Applied edit to (.+)$/.exec(line);
1755
+ if (applied) {
1756
+ const file = applied[1] ?? "";
1757
+ const id = `edit-${file}`;
1758
+ events.push(...flush(), {
1759
+ type: "tool.call",
1760
+ id,
1761
+ name: "edit",
1762
+ input: { path: file }
1763
+ }, {
1764
+ type: "tool.result",
1765
+ id,
1766
+ output: line,
1767
+ isError: false
1768
+ });
1769
+ return events;
1770
+ }
1771
+ const commit = /^Commit ([0-9a-f]{7,40}) (.*)$/.exec(line);
1772
+ if (commit) {
1773
+ const id = `commit-${commit[1] ?? ""}`;
1774
+ events.push({
1775
+ type: "tool.call",
1776
+ id,
1777
+ name: "commit",
1778
+ input: {
1779
+ hash: commit[1],
1780
+ message: commit[2]
1781
+ }
1782
+ }, {
1783
+ type: "tool.result",
1784
+ id,
1785
+ output: line,
1786
+ isError: false
1787
+ });
1788
+ return events;
1789
+ }
1790
+ if (/^litellm\.\w+Error:/.test(line) || line.startsWith("The API provider is not able to authenticate you")) {
1791
+ events.push({
1792
+ type: "error",
1793
+ message: line,
1794
+ fatal: true
1795
+ });
1796
+ return events;
1797
+ }
1798
+ if (/^Retrying in [\d.]+ seconds/.test(line)) {
1799
+ events.push({
1800
+ type: "error",
1801
+ message: line
1802
+ });
1803
+ return events;
1804
+ }
1805
+ body.push(line);
1806
+ return events;
1807
+ },
1808
+ end() {
1809
+ const events = flush();
1810
+ const usage = analyticsUsage(context);
1811
+ return usage !== void 0 ? [...events, usage] : events;
1812
+ }
1813
+ };
1814
+ }
1815
+
1816
+ //#endregion
1817
+ //#region ../../packages/ai-cli/src/adapters/claude-stream.ts
1818
+ function inclusiveUsage(usage, costUSD) {
1819
+ if (usage === void 0) return;
1820
+ const cached = numberField(usage, "cache_read_input_tokens");
1821
+ const written = numberField(usage, "cache_creation_input_tokens");
1822
+ return usageOf({
1823
+ input: numberField(usage, "input_tokens"),
1824
+ output: numberField(usage, "output_tokens"),
1825
+ total: numberField(usage, "total_tokens"),
1826
+ cachedTokens: cached !== void 0 && cached > 0 ? cached : void 0,
1827
+ cacheWriteTokens: written !== void 0 && written > 0 ? written : void 0,
1828
+ costUSD
1829
+ });
1830
+ }
1831
+ /**
1832
+ * Parser for the Claude Code `stream-json` format, which several CLIs
1833
+ * reproduce (Qwen Code, Grok Build's `streaming-messages-json`):
1834
+ *
1835
+ * - `{"type":"system","subtype":"init","session_id","model",…}`
1836
+ * - `{"type":"assistant","message":{"content":[text | tool_use | thinking]}}`
1837
+ * - `{"type":"user","message":{"content":[{"type":"tool_result","tool_use_id","content","is_error"}]}}`
1838
+ * - `{"type":"stream_event","event":{"type":"content_block_delta","delta":{"type":"text_delta","text"}}}`
1839
+ * (only with partial messages enabled)
1840
+ * - `{"type":"result","subtype","is_error","result","session_id","usage","total_cost_usd"}`
1841
+ *
1842
+ * Per-message `usage` is ignored (Claude Code repeats it on every content
1843
+ * block of a message); the `result` totals are reported as cumulative usage.
1844
+ * Sub-agent traffic (`parent_tool_use_id` set) is reported like the main
1845
+ * thread's, so its tool calls stay visible.
1846
+ */
1847
+ function createClaudeStreamParser(options = {}) {
1848
+ const style = options.usage ?? "anthropic";
1849
+ const vendor = options.vendor ?? "The CLI";
1850
+ return jsonLinesParser((record) => {
1851
+ switch (stringField(record, "type")) {
1852
+ case "system": {
1853
+ const subtype = stringField(record, "subtype");
1854
+ if (subtype?.startsWith("error") && stringField(record, "error") !== void 0) return [{
1855
+ type: "error",
1856
+ message: stringField(record, "error") ?? subtype,
1857
+ raw: record
1858
+ }];
1859
+ return subtype === "init" || subtype === "session_start" ? [{
1860
+ type: "session.started",
1861
+ sessionRef: stringField(record, "session_id"),
1862
+ model: stringField(record, "model"),
1863
+ raw: record
1864
+ }] : [];
1865
+ }
1866
+ case "assistant": {
1867
+ const events = [];
1868
+ for (const item of asArray(recordField(record, "message")?.content)) {
1869
+ const block = asRecord(item);
1870
+ const blockType = stringField(block, "type");
1871
+ if (blockType === "text") {
1872
+ const text = stringField(block, "text") ?? "";
1873
+ if (text.length > 0) events.push({
1874
+ type: "message",
1875
+ text,
1876
+ delta: false,
1877
+ raw: record
1878
+ });
1879
+ } else if (blockType === "tool_use" || blockType === "server_tool_use") events.push({
1880
+ type: "tool.call",
1881
+ id: stringField(block, "id") ?? "",
1882
+ name: stringField(block, "name") ?? "",
1883
+ input: block?.input,
1884
+ raw: record
1885
+ });
1886
+ }
1887
+ return events;
1888
+ }
1889
+ case "user": {
1890
+ const events = [];
1891
+ for (const item of asArray(recordField(record, "message")?.content)) {
1892
+ const block = asRecord(item);
1893
+ if (stringField(block, "type") === "tool_result") events.push({
1894
+ type: "tool.result",
1895
+ id: stringField(block, "tool_use_id") ?? "",
1896
+ output: outputText(block?.content),
1897
+ isError: block?.is_error === true,
1898
+ raw: record
1899
+ });
1900
+ }
1901
+ return events;
1902
+ }
1903
+ case "stream_event": {
1904
+ const event = recordField(record, "event");
1905
+ const delta = recordField(event, "delta");
1906
+ if (stringField(event, "type") === "content_block_delta" && stringField(delta, "type") === "text_delta") {
1907
+ const text = stringField(delta, "text") ?? "";
1908
+ return text.length > 0 ? [{
1909
+ type: "message",
1910
+ text,
1911
+ delta: true,
1912
+ raw: record
1913
+ }] : [];
1914
+ }
1915
+ return [];
1916
+ }
1917
+ case "result": {
1918
+ const events = [];
1919
+ const cost = numberField(record, "total_cost_usd");
1920
+ const usage = style === "anthropic" ? anthropicUsage(recordField(record, "usage"), cost) : inclusiveUsage(recordField(record, "usage"), cost);
1921
+ if (usage !== void 0) events.push({
1922
+ type: "usage",
1923
+ usage,
1924
+ cumulative: true,
1925
+ raw: record
1926
+ });
1927
+ const result = stringField(record, "result");
1928
+ if (record.is_error === true) {
1929
+ const errors = asArray(record.errors).map((item) => typeof item === "string" ? item : stringField(asRecord(item), "message") ?? "").filter((item) => item.length > 0).join("; ");
1930
+ const message = stringField(recordField(record, "error"), "message") || stringField(record, "error") || errors || result || `${vendor} run ended with ${stringField(record, "subtype") ?? "an error"}`;
1931
+ events.push({
1932
+ type: "error",
1933
+ message,
1934
+ fatal: true,
1935
+ raw: record
1936
+ });
1937
+ } else if (result !== void 0 && result.length > 0) events.push({
1938
+ type: "message",
1939
+ text: result,
1940
+ delta: false,
1941
+ final: true,
1942
+ raw: record
1943
+ });
1944
+ return events;
1945
+ }
1946
+ default: return [];
1947
+ }
1948
+ });
1949
+ }
1950
+
1951
+ //#endregion
1952
+ //#region ../../packages/ai-cli/src/adapters/amp.ts
1953
+ /**
1954
+ * Amp (`amp`, npm `@ampcode/cli`; `@sourcegraph/amp` is a shim to it).
1955
+ *
1956
+ * Verified against the real `amp --help` of 0.0.1792254886-g14d018 and the
1957
+ * docs (`ampcode.com/docs/cli/{streaming-json,execute-mode,settings}`,
1958
+ * `/docs/tools`), 2026-10-17.
1959
+ *
1960
+ * - Headless: `amp -x <prompt> --stream-json` (with `-x` and no message,
1961
+ * stdin is the prompt); resume is `amp threads continue <T-id> -x …`.
1962
+ * Threads are kept unarchived (`--no-archive-after-execute`) so they can
1963
+ * be continued.
1964
+ * - Stream: Claude Code-compatible (`system/init` with `session_id`
1965
+ * `T-…`, `assistant` text/`tool_use`, `user` `tool_result`, `result`),
1966
+ * no deltas and no cost.
1967
+ * - Model: Amp has no model flag; the mode (`-m low|medium|high|ultra`)
1968
+ * picks model and effort, so `effort` maps to the mode and `model` is
1969
+ * refused.
1970
+ * - Permissions: Amp's own rules (tools run without approval by default;
1971
+ * anything that needs approval is rejected in execute mode). Bypass passes
1972
+ * `--dangerously-allow-all`, which the CLI accepts but no longer lists in
1973
+ * `--help`, so it is not part of the capability probe — a CLI that drops
1974
+ * it rejects the run as an unknown option.
1975
+ * - MCP: `--mcp-config <file>` holding the server map itself (no
1976
+ * `mcpServers` wrapper), written to the run directory.
1977
+ * - Home: Amp follows XDG (`$XDG_CONFIG_HOME/amp/settings.json`); auth
1978
+ * `AMP_API_KEY`; update checks off with `AMP_SKIP_UPDATE_CHECK=1`.
1979
+ */
1980
+ const ampAdapter = {
1981
+ name: "amp",
1982
+ title: "Amp",
1983
+ command: "amp",
1984
+ apiKeyEnv: ["AMP_API_KEY"],
1985
+ homeEnv: [
1986
+ "XDG_CONFIG_HOME",
1987
+ "XDG_DATA_HOME",
1988
+ "XDG_STATE_HOME",
1989
+ "XDG_CACHE_HOME"
1990
+ ],
1991
+ transports: ["argv", "stdin"],
1992
+ effort: true,
1993
+ mcp: true,
1994
+ probe: {
1995
+ help: [["--help"]],
1996
+ version: ["--version"]
1997
+ },
1998
+ verification: {
1999
+ version: "0.0.1792254886-g14d018",
2000
+ verifiedBy: "help",
2001
+ sources: [
2002
+ "https://ampcode.com/docs/cli/streaming-json",
2003
+ "https://ampcode.com/docs/cli/execute-mode",
2004
+ "https://ampcode.com/docs/cli/settings"
2005
+ ],
2006
+ checkedAt: "2026-10-17"
2007
+ },
2008
+ plan(context) {
2009
+ if (context.model !== void 0) throw unsupportedOption(ampAdapter, "Amp has no model flag; its mode picks the model — use `effort` instead of `model`.");
2010
+ const args = context.resume !== void 0 ? [
2011
+ "threads",
2012
+ "continue",
2013
+ context.resume
2014
+ ] : [];
2015
+ const requires = [
2016
+ "--execute",
2017
+ "--stream-json",
2018
+ "--no-archive-after-execute"
2019
+ ];
2020
+ const files = [];
2021
+ if (context.resume !== void 0) requires.push("threads");
2022
+ args.push("--stream-json", "--no-archive-after-execute");
2023
+ if (context.effort !== void 0) {
2024
+ args.push("--mode", mapEffort(context.effort, {
2025
+ low: "low",
2026
+ medium: "medium",
2027
+ high: "high",
2028
+ max: "ultra"
2029
+ }));
2030
+ requires.push("--mode");
2031
+ }
2032
+ if (context.permission === "bypass") args.push("--dangerously-allow-all");
2033
+ if (context.mcp !== void 0) {
2034
+ const file = node_path.default.join(context.runDir, "amp-mcp.json");
2035
+ files.push({
2036
+ path: file,
2037
+ content: jsonFile(mcpServersJson(context.mcp))
2038
+ });
2039
+ args.push("--mcp-config", file);
2040
+ requires.push("--mcp-config");
2041
+ }
2042
+ const env = { AMP_SKIP_UPDATE_CHECK: "1" };
2043
+ if (context.home !== void 0) {
2044
+ env.XDG_CONFIG_HOME = node_path.default.join(context.home, "config");
2045
+ env.XDG_DATA_HOME = node_path.default.join(context.home, "data");
2046
+ env.XDG_STATE_HOME = node_path.default.join(context.home, "state");
2047
+ env.XDG_CACHE_HOME = node_path.default.join(context.home, "cache");
2048
+ }
2049
+ return {
2050
+ args,
2051
+ tail: context.transport === "argv" ? ["--execute", argvPrompt(context.prompt)] : ["--execute"],
2052
+ stdin: context.transport === "stdin" ? context.prompt : void 0,
2053
+ env,
2054
+ files,
2055
+ requires
2056
+ };
2057
+ },
2058
+ parser: () => createClaudeStreamParser({
2059
+ usage: "anthropic",
2060
+ vendor: "Amp"
2061
+ })
2062
+ };
2063
+
2064
+ //#endregion
2065
+ //#region ../../packages/ai-cli/src/adapters/antigravity.ts
2066
+ /**
2067
+ * Google Antigravity CLI (`agy`), installed by the
2068
+ * `antigravity.google/cli/install.sh` script (no npm package). Not Gemini
2069
+ * CLI, which it succeeds.
2070
+ *
2071
+ * Verified against the official docs only (the binary is distributed by a
2072
+ * curl installer, which was not run): `antigravity.google/docs/cli/headless`,
2073
+ * `/docs/cli/reference`, `/docs/mcp`, `/docs/permissions`, `/docs/settings`,
2074
+ * `/docs/install`, and the 1.4.0 release notes, 2026-10-17. The capability
2075
+ * probe checks every flag against the installed binary's `--help`.
2076
+ *
2077
+ * - Headless: `agy -p <prompt> --output-format stream-json`. For long
2078
+ * prompts the adapter uses the documented stdin protocol instead:
2079
+ * `--input-format stream-json` with one `{"event":"user","message":
2080
+ * {"content":…}}` line, then stdin is closed.
2081
+ * - Stream: `{"event":X, X:{…}}` lines — `init` (with `conversation_id`),
2082
+ * `step_update` (`agent_response` steps carry `text_delta` fragments;
2083
+ * `tool` steps carry `tool_info{name,parameters,output,error}` with call
2084
+ * and result together, correlated by `step_index`) and a final `result`
2085
+ * (`status`, `response`, `error`, cumulative `usage`). No cost field.
2086
+ * - Permissions: headless soft-denies tools that need approval (the run
2087
+ * continues and exits 0); bypass is `--dangerously-skip-permissions`.
2088
+ * - Home: no variable relocates `~/.gemini`, so a private/owned home sets
2089
+ * `HOME` (and `USERPROFILE`). MCP goes to
2090
+ * `$HOME/.gemini/config/mcp_config.json` (remote servers use `serverUrl`).
2091
+ * - Auth: `GEMINI_API_KEY` only works with `"modelProvider": "gemini"` in
2092
+ * `$HOME/.gemini/antigravity-cli/settings.json`; the adapter writes that
2093
+ * file when `apiKey` is set on a private home.
2094
+ */
2095
+ const antigravityAdapter = {
2096
+ name: "antigravity",
2097
+ title: "Antigravity CLI",
2098
+ command: "agy",
2099
+ apiKeyEnv: ["GEMINI_API_KEY"],
2100
+ homeEnv: ["HOME", "USERPROFILE"],
2101
+ transports: ["argv", "stdin"],
2102
+ effort: true,
2103
+ mcp: true,
2104
+ probe: {
2105
+ help: [["--help"]],
2106
+ version: ["--version"]
2107
+ },
2108
+ verification: {
2109
+ version: "1.4.0",
2110
+ verifiedBy: "docs",
2111
+ sources: [
2112
+ "https://antigravity.google/docs/cli/headless",
2113
+ "https://antigravity.google/docs/cli/reference",
2114
+ "https://antigravity.google/docs/mcp",
2115
+ "https://antigravity.google/docs/permissions",
2116
+ "https://github.com/google-antigravity/antigravity-cli"
2117
+ ],
2118
+ checkedAt: "2026-10-17"
2119
+ },
2120
+ plan(context) {
2121
+ const args = ["--output-format", "stream-json"];
2122
+ const requires = ["--output-format", "stream-json"];
2123
+ const files = [];
2124
+ if (context.model !== void 0) {
2125
+ args.push("--model", context.model);
2126
+ requires.push("--model");
2127
+ }
2128
+ if (context.effort !== void 0) {
2129
+ args.push("--effort", mapEffort(context.effort, {
2130
+ low: "low",
2131
+ medium: "medium",
2132
+ high: "high"
2133
+ }));
2134
+ requires.push("--effort");
2135
+ }
2136
+ if (context.resume !== void 0) {
2137
+ args.push("--conversation", context.resume);
2138
+ requires.push("--conversation");
2139
+ }
2140
+ if (context.permission === "bypass") {
2141
+ args.push("--dangerously-skip-permissions");
2142
+ requires.push("--dangerously-skip-permissions");
2143
+ }
2144
+ if (context.mcp !== void 0) {
2145
+ const home = requirePrivateHome(context, antigravityAdapter, "MCP servers");
2146
+ files.push({
2147
+ path: node_path.default.join(home, ".gemini", "config", "mcp_config.json"),
2148
+ content: jsonFile({ mcpServers: antigravityMcp(context.mcp) })
2149
+ });
2150
+ }
2151
+ if (context.apiKey !== void 0 && context.homeIsPrivate && context.home !== void 0) files.push({
2152
+ path: node_path.default.join(context.home, ".gemini", "antigravity-cli", "settings.json"),
2153
+ content: jsonFile({ modelProvider: "gemini" })
2154
+ });
2155
+ const env = context.home !== void 0 ? {
2156
+ HOME: context.home,
2157
+ USERPROFILE: context.home
2158
+ } : {};
2159
+ if (context.transport === "stdin") {
2160
+ args.push("--input-format", "stream-json");
2161
+ requires.push("--input-format");
2162
+ return {
2163
+ args,
2164
+ stdin: `${JSON.stringify({
2165
+ event: "user",
2166
+ message: { content: context.prompt }
2167
+ })}\n`,
2168
+ env,
2169
+ files,
2170
+ requires
2171
+ };
2172
+ }
2173
+ requires.push("-p");
2174
+ return {
2175
+ args,
2176
+ tail: ["-p", argvPrompt(context.prompt)],
2177
+ env,
2178
+ files,
2179
+ requires
2180
+ };
2181
+ },
2182
+ parser: createAntigravityParser
2183
+ };
2184
+ function antigravityMcp(servers) {
2185
+ const rendered = {};
2186
+ for (const [name, server] of Object.entries(servers)) rendered[name] = server.type === "stdio" ? {
2187
+ command: server.command,
2188
+ args: server.args ?? [],
2189
+ ...server.env ? { env: server.env } : {}
2190
+ } : {
2191
+ serverUrl: server.url,
2192
+ ...server.headers ? { headers: server.headers } : {}
2193
+ };
2194
+ return rendered;
2195
+ }
2196
+ function stepUsage(usage) {
2197
+ if (usage === void 0) return;
2198
+ const cached = numberField(usage, "cache_read_tokens");
2199
+ return usageOf({
2200
+ input: numberField(usage, "input_tokens"),
2201
+ output: numberField(usage, "output_tokens"),
2202
+ total: numberField(usage, "total_tokens"),
2203
+ cachedTokens: cached !== void 0 && cached > 0 ? cached : void 0,
2204
+ reasoningTokens: numberField(usage, "thinking_tokens") || void 0
2205
+ });
2206
+ }
2207
+ /** Parser for `agy -p … --output-format stream-json`. */
2208
+ function createAntigravityParser() {
2209
+ return jsonLinesParser((record) => {
2210
+ const event = stringField(record, "event");
2211
+ if (event === "init") {
2212
+ const init = recordField(record, "init");
2213
+ return [{
2214
+ type: "session.started",
2215
+ sessionRef: stringField(record, "conversation_id"),
2216
+ model: stringField(init, "model"),
2217
+ raw: record
2218
+ }];
2219
+ }
2220
+ if (event === "step_update") {
2221
+ const step = recordField(record, "step_update");
2222
+ const stepType = stringField(step, "step_type");
2223
+ const events = [];
2224
+ if (stepType === "agent_response") {
2225
+ const text = stringField(step, "text_delta") ?? "";
2226
+ if (text.length > 0) events.push({
2227
+ type: "message",
2228
+ text,
2229
+ delta: true,
2230
+ raw: record
2231
+ });
2232
+ }
2233
+ if (stepType === "tool" && stringField(step, "state") === "DONE") {
2234
+ const info = recordField(step, "tool_info");
2235
+ const error = recordField(info, "error");
2236
+ const id = `step-${numberField(step, "step_index") ?? 0}`;
2237
+ events.push({
2238
+ type: "tool.call",
2239
+ id,
2240
+ name: stringField(info, "name") ?? stringField(step, "tool_name") ?? "",
2241
+ input: info?.parameters,
2242
+ raw: record
2243
+ }, {
2244
+ type: "tool.result",
2245
+ id,
2246
+ output: error !== void 0 ? stringField(error, "message") ?? stringField(error, "type") ?? "tool failed" : stringField(info, "output") ?? "",
2247
+ isError: error !== void 0,
2248
+ raw: record
2249
+ });
2250
+ }
2251
+ const usage = stepUsage(recordField(step, "usage"));
2252
+ if (usage !== void 0) events.push({
2253
+ type: "usage",
2254
+ usage,
2255
+ cumulative: false,
2256
+ raw: record
2257
+ });
2258
+ return events;
2259
+ }
2260
+ if (event === "result") {
2261
+ const result = recordField(record, "result");
2262
+ const events = [];
2263
+ const usage = stepUsage(recordField(result, "usage"));
2264
+ if (usage !== void 0) events.push({
2265
+ type: "usage",
2266
+ usage,
2267
+ cumulative: true,
2268
+ raw: record
2269
+ });
2270
+ const status = stringField(result, "status");
2271
+ if (status !== void 0 && status !== "SUCCESS") events.push({
2272
+ type: "error",
2273
+ message: stringField(result, "error") || `Antigravity run ended ${status}`,
2274
+ fatal: true,
2275
+ raw: record
2276
+ });
2277
+ else {
2278
+ const response = stringField(result, "response") ?? "";
2279
+ if (response.length > 0) events.push({
2280
+ type: "message",
2281
+ text: response,
2282
+ delta: false,
2283
+ final: true,
2284
+ raw: record
2285
+ });
2286
+ }
2287
+ return events;
2288
+ }
2289
+ return [];
2290
+ });
2291
+ }
2292
+
2293
+ //#endregion
2294
+ //#region ../../packages/ai-cli/src/adapters/claude-code.ts
2295
+ /**
2296
+ * Claude Code (`claude`, npm `@anthropic-ai/claude-code`).
2297
+ *
2298
+ * Verified against the real `claude --help` of 2.1.302, the event types of
2299
+ * `@anthropic-ai/claude-agent-sdk` 0.3.302 and the docs
2300
+ * (`code.claude.com/docs/en/{headless,cli-reference,mcp,env-vars,
2301
+ * permission-modes}`), 2026-10-17.
2302
+ *
2303
+ * - Headless: `claude -p --output-format stream-json --verbose
2304
+ * --include-partial-messages <prompt>` (`--verbose` is required with
2305
+ * stream-json in print mode); piped stdin is the prompt when no positional
2306
+ * prompt is given.
2307
+ * - Stream: `system/init` (`session_id`), `stream_event` text deltas,
2308
+ * `assistant` messages (text / `tool_use`), `user` messages with
2309
+ * `tool_result`, and a final `result` (`result`, `usage`,
2310
+ * `total_cost_usd`, `is_error`, `errors[]`).
2311
+ * - Permissions: print mode auto-denies anything that would prompt; bypass
2312
+ * is `--dangerously-skip-permissions` (Claude Code refuses it as root
2313
+ * outside a recognised sandbox).
2314
+ * - MCP: `--mcp-config <file>` with `{"mcpServers":…}`, written to the run
2315
+ * directory.
2316
+ * - Home: `CLAUDE_CONFIG_DIR`; auth `ANTHROPIC_API_KEY` (or a home you
2317
+ * logged in to); auto-update off with `DISABLE_AUTOUPDATER=1`.
2318
+ */
2319
+ const claudeCodeAdapter = {
2320
+ name: "claude-code",
2321
+ title: "Claude Code",
2322
+ command: "claude",
2323
+ apiKeyEnv: ["ANTHROPIC_API_KEY"],
2324
+ homeEnv: ["CLAUDE_CONFIG_DIR"],
2325
+ transports: ["argv", "stdin"],
2326
+ effort: true,
2327
+ mcp: true,
2328
+ probe: {
2329
+ help: [["--help"]],
2330
+ version: ["--version"]
2331
+ },
2332
+ verification: {
2333
+ version: "2.1.302",
2334
+ verifiedBy: "help",
2335
+ sources: [
2336
+ "https://code.claude.com/docs/en/headless",
2337
+ "https://code.claude.com/docs/en/cli-reference",
2338
+ "https://code.claude.com/docs/en/mcp",
2339
+ "https://code.claude.com/docs/en/env-vars"
2340
+ ],
2341
+ checkedAt: "2026-10-17"
2342
+ },
2343
+ plan(context) {
2344
+ const args = [
2345
+ "--print",
2346
+ "--output-format",
2347
+ "stream-json",
2348
+ "--verbose",
2349
+ "--include-partial-messages"
2350
+ ];
2351
+ const requires = [
2352
+ "--print",
2353
+ "--output-format",
2354
+ "stream-json",
2355
+ "--verbose",
2356
+ "--include-partial-messages"
2357
+ ];
2358
+ const files = [];
2359
+ if (context.model !== void 0) {
2360
+ args.push("--model", context.model);
2361
+ requires.push("--model");
2362
+ }
2363
+ if (context.effort !== void 0) {
2364
+ args.push("--effort", mapEffort(context.effort, {
2365
+ low: "low",
2366
+ medium: "medium",
2367
+ high: "high",
2368
+ xhigh: "xhigh",
2369
+ max: "max"
2370
+ }));
2371
+ requires.push("--effort");
2372
+ }
2373
+ if (context.resume !== void 0) {
2374
+ args.push("--resume", context.resume);
2375
+ requires.push("--resume");
2376
+ }
2377
+ if (context.permission === "bypass") {
2378
+ args.push("--dangerously-skip-permissions");
2379
+ requires.push("--dangerously-skip-permissions");
2380
+ }
2381
+ if (context.mcp !== void 0) {
2382
+ const file = node_path.default.join(context.runDir, "mcp.json");
2383
+ files.push({
2384
+ path: file,
2385
+ content: jsonFile({ mcpServers: mcpServersJson(context.mcp, { typeField: true }) })
2386
+ });
2387
+ args.push("--mcp-config", file);
2388
+ requires.push("--mcp-config");
2389
+ }
2390
+ return {
2391
+ args,
2392
+ tail: context.transport === "argv" ? [argvPrompt(context.prompt)] : [],
2393
+ stdin: context.transport === "stdin" ? context.prompt : void 0,
2394
+ env: {
2395
+ ...context.home !== void 0 ? { CLAUDE_CONFIG_DIR: context.home } : {},
2396
+ DISABLE_AUTOUPDATER: "1"
2397
+ },
2398
+ files,
2399
+ requires
2400
+ };
2401
+ },
2402
+ parser: () => createClaudeStreamParser({
2403
+ usage: "anthropic",
2404
+ vendor: "Claude Code"
2405
+ })
2406
+ };
2407
+
2408
+ //#endregion
2409
+ //#region ../../packages/ai-cli/src/adapters/codex.ts
2410
+ /**
2411
+ * OpenAI Codex CLI (`codex`, npm `@openai/codex`).
2412
+ *
2413
+ * Verified against the real `codex exec --help` / `codex exec resume
2414
+ * --help` of 0.163.0, the source at tag `rust-v0.163.0` (`exec_events.rs`,
2415
+ * the JSONL event processor, `cli.rs`) and the docs (non-interactive mode,
2416
+ * config reference), 2026-10-17.
2417
+ *
2418
+ * - Headless: `codex exec --json --skip-git-repo-check <prompt>` (`-` reads
2419
+ * the prompt from stdin); resume is `codex exec resume … <thread_id>
2420
+ * <prompt>`.
2421
+ * - Stream: `thread.started` (`thread_id`), `item.started`/`item.completed`
2422
+ * with `item.type` `agent_message`, `reasoning`, `command_execution`,
2423
+ * `file_change`, `mcp_tool_call`, `web_search`, `error`; `turn.completed`
2424
+ * with the thread's cumulative `usage` (`input_tokens` includes
2425
+ * `cached_input_tokens`); `turn.failed` and `error` on failure. No text
2426
+ * deltas and no cost field.
2427
+ * - Permissions: exec never asks (approval policy "never"); `standard` keeps
2428
+ * Codex's default **read-only** sandbox — allow edits with
2429
+ * `args: ["-c", "sandbox_mode=workspace-write"]`. Bypass is
2430
+ * `--dangerously-bypass-approvals-and-sandbox`.
2431
+ * - Effort: `-c model_reasoning_effort=<level>`.
2432
+ * - MCP: no flag. With a private home the servers go into
2433
+ * `$CODEX_HOME/config.toml` (0600). With a caller-owned or inherited home
2434
+ * they are passed as `-c mcp_servers.<name>.…` overrides that carry only
2435
+ * the command, args and URL; secret `env` values and headers travel in the
2436
+ * process environment, named through `env_vars` / `env_http_headers`.
2437
+ * - Home `CODEX_HOME`; auth `CODEX_API_KEY`.
2438
+ */
2439
+ const codexAdapter = {
2440
+ name: "codex",
2441
+ title: "Codex CLI",
2442
+ command: "codex",
2443
+ apiKeyEnv: ["CODEX_API_KEY"],
2444
+ homeEnv: ["CODEX_HOME"],
2445
+ transports: ["argv", "stdin"],
2446
+ effort: true,
2447
+ mcp: true,
2448
+ probe: {
2449
+ help: [["exec", "--help"], [
2450
+ "exec",
2451
+ "resume",
2452
+ "--help"
2453
+ ]],
2454
+ version: ["--version"]
2455
+ },
2456
+ verification: {
2457
+ version: "0.163.0",
2458
+ verifiedBy: "help",
2459
+ sources: [
2460
+ "https://developers.openai.com/codex/noninteractive",
2461
+ "https://developers.openai.com/codex/config-reference",
2462
+ "https://github.com/openai/codex/blob/rust-v0.163.0/codex-rs/exec/src/exec_events.rs"
2463
+ ],
2464
+ checkedAt: "2026-10-17"
2465
+ },
2466
+ plan(context) {
2467
+ const args = ["exec"];
2468
+ const requires = [
2469
+ "exec",
2470
+ "--json",
2471
+ "--skip-git-repo-check"
2472
+ ];
2473
+ const files = [];
2474
+ const env = {};
2475
+ if (context.resume !== void 0) {
2476
+ args.push("resume");
2477
+ requires.push("resume");
2478
+ }
2479
+ args.push("--json", "--skip-git-repo-check", "-c", "check_for_update_on_startup=false");
2480
+ if (context.model !== void 0) {
2481
+ args.push("--model", context.model);
2482
+ requires.push("--model");
2483
+ }
2484
+ if (context.effort !== void 0) {
2485
+ const effort = mapEffort(context.effort, {
2486
+ low: "low",
2487
+ medium: "medium",
2488
+ high: "high",
2489
+ xhigh: "xhigh",
2490
+ max: "max"
2491
+ });
2492
+ args.push("-c", `model_reasoning_effort=${effort}`);
2493
+ requires.push("--config");
2494
+ }
2495
+ if (context.permission === "bypass") {
2496
+ args.push("--dangerously-bypass-approvals-and-sandbox");
2497
+ requires.push("--dangerously-bypass-approvals-and-sandbox");
2498
+ }
2499
+ if (context.mcp !== void 0) {
2500
+ if (context.home !== void 0 && context.homeIsPrivate) files.push({
2501
+ path: node_path.default.join(context.home, "config.toml"),
2502
+ content: toToml({ mcp_servers: codexMcpTable(context.mcp) })
2503
+ });
2504
+ else {
2505
+ args.push(...codexMcpOverrides(context.mcp, env));
2506
+ requires.push("--config");
2507
+ }
2508
+ }
2509
+ if (context.home !== void 0) env.CODEX_HOME = context.home;
2510
+ const prompt = context.transport === "argv" ? argvPrompt(context.prompt) : "-";
2511
+ return {
2512
+ args,
2513
+ tail: context.resume !== void 0 ? [context.resume, prompt] : [prompt],
2514
+ stdin: context.transport === "stdin" ? context.prompt : void 0,
2515
+ env,
2516
+ files,
2517
+ requires
2518
+ };
2519
+ },
2520
+ parser: createCodexParser
2521
+ };
2522
+ /** `[mcp_servers.<name>]` tables for a private `config.toml`. */
2523
+ function codexMcpTable(servers) {
2524
+ const table = {};
2525
+ for (const [name, server] of Object.entries(servers)) {
2526
+ if (server.type === "sse") throw unsupportedOption(codexAdapter, `MCP server "${name}" uses SSE; Codex supports stdio and streamable HTTP.`);
2527
+ table[name] = server.type === "stdio" ? {
2528
+ command: server.command,
2529
+ args: server.args ?? [],
2530
+ env: server.env
2531
+ } : {
2532
+ url: server.url,
2533
+ http_headers: server.headers
2534
+ };
2535
+ }
2536
+ return table;
2537
+ }
2538
+ /** A TOML string for a `-c key=value` override. */
2539
+ function tomlString(value) {
2540
+ return JSON.stringify(value);
2541
+ }
2542
+ /**
2543
+ * `-c` overrides that carry no secret: env values and header values are put
2544
+ * in `env` (the Codex process environment) and only their names go to argv.
2545
+ */
2546
+ function codexMcpOverrides(servers, env) {
2547
+ const overrides = [];
2548
+ const claim = (name, value) => {
2549
+ if (env[name] !== void 0 && env[name] !== value) throw unsupportedOption(codexAdapter, `two MCP servers set ${name} to different values; with a shared home they share one environment.`);
2550
+ env[name] = value;
2551
+ };
2552
+ for (const [name, server] of Object.entries(servers)) {
2553
+ const prefix = `mcp_servers.${tomlKey(name)}`;
2554
+ if (server.type === "sse") throw unsupportedOption(codexAdapter, `MCP server "${name}" uses SSE; Codex supports stdio and streamable HTTP.`);
2555
+ if (server.type === "stdio") {
2556
+ overrides.push("-c", `${prefix}.command=${tomlString(server.command)}`);
2557
+ overrides.push("-c", `${prefix}.args=[${(server.args ?? []).map(tomlString).join(",")}]`);
2558
+ const names = Object.keys(server.env ?? {});
2559
+ for (const [key, value] of Object.entries(server.env ?? {})) claim(key, value);
2560
+ if (names.length > 0) overrides.push("-c", `${prefix}.env_vars=[${names.map(tomlString).join(",")}]`);
2561
+ continue;
2562
+ }
2563
+ overrides.push("-c", `${prefix}.url=${tomlString(server.url)}`);
2564
+ const headers = Object.entries(server.headers ?? {});
2565
+ if (headers.length > 0) {
2566
+ const mapping = headers.map(([header, value], index) => {
2567
+ const variable = `WARLOCK_MCP_${name.replace(/\W/g, "_").toUpperCase()}_HEADER_${index}`;
2568
+ claim(variable, value);
2569
+ return `${tomlString(header)}=${tomlString(variable)}`;
2570
+ });
2571
+ overrides.push("-c", `${prefix}.env_http_headers={${mapping.join(",")}}`);
2572
+ }
2573
+ }
2574
+ return overrides;
2575
+ }
2576
+ const TOOL_ITEMS = /* @__PURE__ */ new Set([
2577
+ "command_execution",
2578
+ "file_change",
2579
+ "mcp_tool_call",
2580
+ "web_search",
2581
+ "collab_tool_call"
2582
+ ]);
2583
+ function toolCall(item) {
2584
+ switch (stringField(item, "type")) {
2585
+ case "command_execution": return {
2586
+ name: "shell",
2587
+ input: { command: item.command }
2588
+ };
2589
+ case "file_change": return {
2590
+ name: "apply_patch",
2591
+ input: { changes: item.changes }
2592
+ };
2593
+ case "mcp_tool_call": return {
2594
+ name: `${stringField(item, "server") ?? "mcp"}.${stringField(item, "tool") ?? "tool"}`,
2595
+ input: item.arguments
2596
+ };
2597
+ case "web_search": return {
2598
+ name: "web_search",
2599
+ input: { query: item.query }
2600
+ };
2601
+ default: return {
2602
+ name: stringField(item, "type") ?? "tool",
2603
+ input: item
2604
+ };
2605
+ }
2606
+ }
2607
+ function toolResult(item) {
2608
+ const status = stringField(item, "status");
2609
+ const failed = status === "failed" || status === "declined";
2610
+ switch (stringField(item, "type")) {
2611
+ case "command_execution": {
2612
+ const exitCode = numberField(item, "exit_code");
2613
+ return {
2614
+ output: stringField(item, "aggregated_output") ?? "",
2615
+ isError: failed || exitCode !== void 0 && exitCode !== 0
2616
+ };
2617
+ }
2618
+ case "mcp_tool_call": {
2619
+ const error = stringField(recordField(item, "error"), "message");
2620
+ return error !== void 0 ? {
2621
+ output: error,
2622
+ isError: true
2623
+ } : {
2624
+ output: outputText(recordField(item, "result")?.content),
2625
+ isError: failed
2626
+ };
2627
+ }
2628
+ default: return {
2629
+ output: outputText(item.changes ?? item.result ?? ""),
2630
+ isError: failed
2631
+ };
2632
+ }
2633
+ }
2634
+ /** Parser for `codex exec --json`. */
2635
+ function createCodexParser() {
2636
+ const announced = /* @__PURE__ */ new Set();
2637
+ return jsonLinesParser((record) => {
2638
+ const type = stringField(record, "type");
2639
+ const item = recordField(record, "item");
2640
+ const itemType = stringField(item, "type");
2641
+ const id = stringField(item, "id") ?? "";
2642
+ if (type === "thread.started") return [{
2643
+ type: "session.started",
2644
+ sessionRef: stringField(record, "thread_id"),
2645
+ raw: record
2646
+ }];
2647
+ if (type === "item.started" && item !== void 0 && itemType !== void 0 && TOOL_ITEMS.has(itemType)) {
2648
+ announced.add(id);
2649
+ return [{
2650
+ type: "tool.call",
2651
+ id,
2652
+ ...toolCall(item),
2653
+ raw: record
2654
+ }];
2655
+ }
2656
+ if (type === "item.completed" && item !== void 0) {
2657
+ if (itemType === "agent_message") {
2658
+ const text = stringField(item, "text") ?? "";
2659
+ return text.length > 0 ? [{
2660
+ type: "message",
2661
+ text,
2662
+ delta: false,
2663
+ raw: record
2664
+ }] : [];
2665
+ }
2666
+ if (itemType === "error") return [{
2667
+ type: "error",
2668
+ message: stringField(item, "message") ?? "error",
2669
+ raw: record
2670
+ }];
2671
+ if (itemType !== void 0 && TOOL_ITEMS.has(itemType)) {
2672
+ const events = [];
2673
+ if (!announced.has(id)) events.push({
2674
+ type: "tool.call",
2675
+ id,
2676
+ ...toolCall(item),
2677
+ raw: record
2678
+ });
2679
+ events.push({
2680
+ type: "tool.result",
2681
+ id,
2682
+ ...toolResult(item),
2683
+ raw: record
2684
+ });
2685
+ return events;
2686
+ }
2687
+ return [];
2688
+ }
2689
+ if (type === "turn.completed") {
2690
+ const usage = recordField(record, "usage");
2691
+ if (usage === void 0) return [];
2692
+ const cached = numberField(usage, "cached_input_tokens");
2693
+ const written = numberField(usage, "cache_write_input_tokens");
2694
+ return [{
2695
+ type: "usage",
2696
+ cumulative: true,
2697
+ usage: usageOf({
2698
+ input: numberField(usage, "input_tokens"),
2699
+ output: numberField(usage, "output_tokens"),
2700
+ cachedTokens: cached !== void 0 && cached > 0 ? cached : void 0,
2701
+ cacheWriteTokens: written !== void 0 && written > 0 ? written : void 0,
2702
+ reasoningTokens: numberField(usage, "reasoning_output_tokens") || void 0
2703
+ }),
2704
+ raw: record
2705
+ }];
2706
+ }
2707
+ if (type === "turn.failed") return [{
2708
+ type: "error",
2709
+ message: stringField(recordField(record, "error"), "message") ?? "Codex turn failed",
2710
+ fatal: true,
2711
+ raw: record
2712
+ }];
2713
+ if (type === "error") return [{
2714
+ type: "error",
2715
+ message: stringField(record, "message") ?? "Codex error",
2716
+ fatal: true,
2717
+ raw: record
2718
+ }];
2719
+ return [];
2720
+ });
2721
+ }
2722
+
2723
+ //#endregion
2724
+ //#region ../../packages/ai-cli/src/adapters/copilot.ts
2725
+ /** Usage file (inside the run directory) Copilot writes on exit. */
2726
+ const USAGE_FILE = "copilot-usage.json";
2727
+ /**
2728
+ * GitHub Copilot CLI (`copilot`, npm `@github/copilot`).
2729
+ *
2730
+ * Verified against the real `copilot --help` of 1.0.98, the JSONL writer in
2731
+ * the shipped CLI, the event types of `@github/copilot-sdk` 1.0.22 and the
2732
+ * docs (`docs.github.com/copilot/how-tos/copilot-cli/…`, programmatic and
2733
+ * command references), 2026-10-17.
2734
+ *
2735
+ * - Headless: `copilot -p <prompt> --output-format json --stream on`
2736
+ * (piped stdin is the prompt when `-p` is absent). Stream lines are
2737
+ * session events `{type,data,id,timestamp}`: `assistant.message_delta`
2738
+ * (`deltaContent`), `assistant.message` (`content`),
2739
+ * `tool.execution_start` (`toolCallId`, `toolName`, `arguments`),
2740
+ * `tool.execution_complete` (`success`, `result.content`,
2741
+ * `error.message`), `session.error`, and a final CLI-built `result`
2742
+ * (`sessionId`, `exitCode`, premium-request usage).
2743
+ * - Tokens are not in the stream; the adapter passes `--usage-output-file`
2744
+ * and reads `modelMetrics.*.usage` from it when the stream ends. Billing
2745
+ * is in premium requests, so no USD cost is reported.
2746
+ * - Permissions: unapproved tool requests are auto-denied in `-p` mode;
2747
+ * bypass is `--allow-all` (tools, paths and URLs). `--no-ask-user` and
2748
+ * `--no-auto-update` are always passed.
2749
+ * - MCP: `--additional-mcp-config @<file>` (`mcpServers`, `type`
2750
+ * `local`/`http`/`sse`, `tools: ["*"]`), written to the run directory.
2751
+ * - Home `COPILOT_HOME`; auth `COPILOT_GITHUB_TOKEN`. Resume with
2752
+ * `--session-id <id>`.
2753
+ */
2754
+ const copilotAdapter = {
2755
+ name: "copilot",
2756
+ title: "Copilot CLI",
2757
+ command: "copilot",
2758
+ apiKeyEnv: ["COPILOT_GITHUB_TOKEN"],
2759
+ homeEnv: ["COPILOT_HOME"],
2760
+ transports: ["argv", "stdin"],
2761
+ effort: true,
2762
+ mcp: true,
2763
+ probe: {
2764
+ help: [["--help"]],
2765
+ version: ["--version"]
2766
+ },
2767
+ verification: {
2768
+ version: "1.0.98",
2769
+ verifiedBy: "help",
2770
+ sources: [
2771
+ "https://docs.github.com/en/copilot/how-tos/copilot-cli/automate-copilot-cli/run-cli-programmatically",
2772
+ "https://docs.github.com/en/copilot/reference/copilot-cli-reference/cli-command-reference",
2773
+ "https://www.npmjs.com/package/@github/copilot"
2774
+ ],
2775
+ checkedAt: "2026-10-17"
2776
+ },
2777
+ plan(context) {
2778
+ const args = [
2779
+ "--output-format",
2780
+ "json",
2781
+ "--stream",
2782
+ "on",
2783
+ "--no-ask-user",
2784
+ "--no-auto-update",
2785
+ "--usage-output-file",
2786
+ node_path.default.join(context.runDir, USAGE_FILE)
2787
+ ];
2788
+ const requires = [
2789
+ "--output-format",
2790
+ "--stream",
2791
+ "--no-ask-user",
2792
+ "--no-auto-update",
2793
+ "--usage-output-file"
2794
+ ];
2795
+ const files = [];
2796
+ if (context.model !== void 0) {
2797
+ args.push("--model", context.model);
2798
+ requires.push("--model");
2799
+ }
2800
+ if (context.effort !== void 0) {
2801
+ args.push("--reasoning-effort", context.effort);
2802
+ requires.push("--reasoning-effort");
2803
+ }
2804
+ if (context.resume !== void 0) {
2805
+ args.push("--session-id", context.resume);
2806
+ requires.push("--session-id");
2807
+ }
2808
+ if (context.permission === "bypass") {
2809
+ args.push("--allow-all");
2810
+ requires.push("--allow-all");
2811
+ }
2812
+ if (context.mcp !== void 0) {
2813
+ const file = node_path.default.join(context.runDir, "copilot-mcp.json");
2814
+ const servers = mcpServersJson(context.mcp, {
2815
+ typeField: true,
2816
+ extra: { tools: ["*"] }
2817
+ });
2818
+ for (const server of Object.values(servers)) if (server.type === "stdio") server.type = "local";
2819
+ files.push({
2820
+ path: file,
2821
+ content: jsonFile({ mcpServers: servers })
2822
+ });
2823
+ args.push("--additional-mcp-config", `@${file}`);
2824
+ requires.push("--additional-mcp-config");
2825
+ }
2826
+ return {
2827
+ args,
2828
+ tail: context.transport === "argv" ? ["-p", argvPrompt(context.prompt)] : [],
2829
+ stdin: context.transport === "stdin" ? context.prompt : void 0,
2830
+ env: context.home !== void 0 ? { COPILOT_HOME: context.home } : void 0,
2831
+ files,
2832
+ requires: context.transport === "argv" ? [...requires, "-p"] : requires
2833
+ };
2834
+ },
2835
+ parser: createCopilotParser
2836
+ };
2837
+ /** Token usage summed over `modelMetrics` of the usage file. */
2838
+ function usageFileEvent(context) {
2839
+ if (context === void 0) return;
2840
+ let report;
2841
+ try {
2842
+ report = parseJsonLine((0, node_fs.readFileSync)(node_path.default.join(context.runDir, USAGE_FILE), "utf8"));
2843
+ } catch {
2844
+ return;
2845
+ }
2846
+ const metrics = recordField(report, "modelMetrics");
2847
+ if (metrics === void 0) return;
2848
+ let input = 0;
2849
+ let output = 0;
2850
+ let cached = 0;
2851
+ let written = 0;
2852
+ let reasoning = 0;
2853
+ for (const value of Object.values(metrics)) {
2854
+ const usage = recordField(asRecord(value), "usage");
2855
+ input += numberField(usage, "inputTokens") ?? 0;
2856
+ output += numberField(usage, "outputTokens") ?? 0;
2857
+ cached += numberField(usage, "cacheReadTokens") ?? 0;
2858
+ written += numberField(usage, "cacheWriteTokens") ?? 0;
2859
+ reasoning += numberField(usage, "reasoningTokens") ?? 0;
2860
+ }
2861
+ return {
2862
+ type: "usage",
2863
+ cumulative: true,
2864
+ usage: usageOf({
2865
+ input,
2866
+ output,
2867
+ cachedTokens: cached > 0 ? cached : void 0,
2868
+ cacheWriteTokens: written > 0 ? written : void 0,
2869
+ reasoningTokens: reasoning > 0 ? reasoning : void 0
2870
+ }),
2871
+ raw: report
2872
+ };
2873
+ }
2874
+ /** Parser for `copilot -p … --output-format json` (plus its usage file). */
2875
+ function createCopilotParser(context) {
2876
+ let started = false;
2877
+ let sessionRef;
2878
+ return {
2879
+ ...jsonLinesParser((record) => {
2880
+ const events = [];
2881
+ const type = stringField(record, "type");
2882
+ const data = recordField(record, "data");
2883
+ if (!started) {
2884
+ started = true;
2885
+ events.push({
2886
+ type: "session.started",
2887
+ sessionRef: stringField(data, "sessionId"),
2888
+ raw: record
2889
+ });
2890
+ }
2891
+ switch (type) {
2892
+ case "assistant.message_delta": {
2893
+ const text = stringField(data, "deltaContent") ?? "";
2894
+ if (text.length > 0) events.push({
2895
+ type: "message",
2896
+ text,
2897
+ delta: true,
2898
+ raw: record
2899
+ });
2900
+ break;
2901
+ }
2902
+ case "assistant.message": {
2903
+ const text = stringField(data, "content") ?? "";
2904
+ if (text.length > 0) events.push({
2905
+ type: "message",
2906
+ text,
2907
+ delta: false,
2908
+ raw: record
2909
+ });
2910
+ break;
2911
+ }
2912
+ case "tool.execution_start":
2913
+ events.push({
2914
+ type: "tool.call",
2915
+ id: stringField(data, "toolCallId") ?? "",
2916
+ name: stringField(data, "toolName") ?? "",
2917
+ input: data?.arguments,
2918
+ raw: record
2919
+ });
2920
+ break;
2921
+ case "tool.execution_complete": {
2922
+ const success = data?.success === true;
2923
+ events.push({
2924
+ type: "tool.result",
2925
+ id: stringField(data, "toolCallId") ?? "",
2926
+ output: success ? outputText(recordField(data, "result")?.content) : stringField(recordField(data, "error"), "message") ?? "tool failed",
2927
+ isError: !success,
2928
+ raw: record
2929
+ });
2930
+ break;
2931
+ }
2932
+ case "session.error":
2933
+ events.push({
2934
+ type: "error",
2935
+ message: stringField(data, "message") ?? "Copilot session error",
2936
+ fatal: true,
2937
+ raw: record
2938
+ });
2939
+ break;
2940
+ case "result":
2941
+ sessionRef = stringField(record, "sessionId") ?? sessionRef;
2942
+ break;
2943
+ case "error": events.push({
2944
+ type: "error",
2945
+ message: stringField(record, "message") ?? "error",
2946
+ raw: record
2947
+ });
2948
+ }
2949
+ return events;
2950
+ }, () => {
2951
+ const usage = usageFileEvent(context);
2952
+ return usage !== void 0 ? [usage] : [];
2953
+ }),
2954
+ sessionRef: () => sessionRef
2955
+ };
2956
+ }
2957
+
2958
+ //#endregion
2959
+ //#region ../../packages/ai-cli/src/adapters/cursor.ts
2960
+ /**
2961
+ * Cursor CLI (`cursor-agent`, also installed as `agent`), installed by the
2962
+ * `cursor.com/install` script (no official npm package).
2963
+ *
2964
+ * Verified against the official docs (`cursor.com/docs/cli/{headless,mcp}`,
2965
+ * `/docs/cli/reference/{output-format,parameters,authentication,
2966
+ * permissions,configuration}`) and the option strings of the
2967
+ * 2026.10.16-e3cb207 release bundle (read, not executed), 2026-10-17. The
2968
+ * capability probe checks every flag against the installed `--help`.
2969
+ *
2970
+ * - Headless: `cursor-agent -p --output-format stream-json --trust <prompt>`;
2971
+ * with no positional prompt, stdin is the prompt. `--trust` is passed
2972
+ * because print mode exits in a directory that was never trusted — the
2973
+ * run's `cwd` is the caller's explicit choice.
2974
+ * - Stream: `system/init` (`session_id`, `model`), `user` echo, `assistant`
2975
+ * messages (`message.content[].text`), `tool_call` `started`/`completed`
2976
+ * (`call_id`; the tool is the single key of `tool_call`, e.g.
2977
+ * `readToolCall{args,result{success}}`), and a final `result` with
2978
+ * `usage{inputTokens (net of cache), outputTokens, cacheReadTokens,
2979
+ * cacheWriteTokens}`. Errors are not JSON: stderr and a non-zero exit.
2980
+ * - Permissions: headless default is the allowlist mode (unlisted actions are
2981
+ * rejected, changes only proposed); bypass is `--force`.
2982
+ * - MCP: no flag; `$HOME/.cursor/mcp.json` (the path ignores
2983
+ * `CURSOR_CONFIG_DIR`), plus `--approve-mcps` — needs a private home.
2984
+ * - Home: `HOME`/`USERPROFILE`, `CURSOR_CONFIG_DIR` and `CURSOR_DATA_DIR`
2985
+ * are pointed into it. Auth: `CURSOR_API_KEY`.
2986
+ */
2987
+ const cursorAdapter = {
2988
+ name: "cursor",
2989
+ title: "Cursor CLI",
2990
+ command: "cursor-agent",
2991
+ apiKeyEnv: ["CURSOR_API_KEY"],
2992
+ homeEnv: [
2993
+ "HOME",
2994
+ "USERPROFILE",
2995
+ "CURSOR_CONFIG_DIR",
2996
+ "CURSOR_DATA_DIR"
2997
+ ],
2998
+ transports: ["argv", "stdin"],
2999
+ effort: false,
3000
+ mcp: true,
3001
+ probe: {
3002
+ help: [["--help"]],
3003
+ version: ["--version"]
3004
+ },
3005
+ verification: {
3006
+ version: "2026.10.16-e3cb207",
3007
+ verifiedBy: "docs",
3008
+ sources: [
3009
+ "https://cursor.com/docs/cli/headless",
3010
+ "https://cursor.com/docs/cli/reference/output-format",
3011
+ "https://cursor.com/docs/cli/reference/parameters",
3012
+ "https://cursor.com/docs/cli/mcp"
3013
+ ],
3014
+ checkedAt: "2026-10-17"
3015
+ },
3016
+ plan(context) {
3017
+ const args = [
3018
+ "--print",
3019
+ "--output-format",
3020
+ "stream-json",
3021
+ "--trust"
3022
+ ];
3023
+ const requires = [
3024
+ "--print",
3025
+ "--output-format",
3026
+ "stream-json",
3027
+ "--trust"
3028
+ ];
3029
+ const files = [];
3030
+ if (context.model !== void 0) {
3031
+ args.push("--model", context.model);
3032
+ requires.push("--model");
3033
+ }
3034
+ if (context.resume !== void 0) {
3035
+ args.push("--resume", context.resume);
3036
+ requires.push("--resume");
3037
+ }
3038
+ if (context.permission === "bypass") {
3039
+ args.push("--force");
3040
+ requires.push("--force");
3041
+ }
3042
+ if (context.mcp !== void 0) {
3043
+ const home = requirePrivateHome(context, cursorAdapter, "MCP servers");
3044
+ files.push({
3045
+ path: node_path.default.join(home, ".cursor", "mcp.json"),
3046
+ content: jsonFile({ mcpServers: mcpServersJson(context.mcp, { typeField: true }) })
3047
+ });
3048
+ args.push("--approve-mcps");
3049
+ requires.push("--approve-mcps");
3050
+ }
3051
+ const env = context.home !== void 0 ? {
3052
+ HOME: context.home,
3053
+ USERPROFILE: context.home,
3054
+ CURSOR_CONFIG_DIR: node_path.default.join(context.home, ".cursor"),
3055
+ CURSOR_DATA_DIR: node_path.default.join(context.home, ".cursor")
3056
+ } : {};
3057
+ return {
3058
+ args,
3059
+ tail: context.transport === "argv" ? [argvPrompt(context.prompt)] : [],
3060
+ stdin: context.transport === "stdin" ? context.prompt : void 0,
3061
+ env,
3062
+ files,
3063
+ requires
3064
+ };
3065
+ },
3066
+ parser: createCursorParser
3067
+ };
3068
+ /** `readToolCall` → `read`; the documented `function{name}` fallback is used as-is. */
3069
+ function toolOf(toolCall) {
3070
+ const fn = recordField(toolCall, "function");
3071
+ if (fn !== void 0) return {
3072
+ name: stringField(fn, "name") ?? "function",
3073
+ body: fn
3074
+ };
3075
+ const key = Object.keys(toolCall ?? {})[0] ?? "tool";
3076
+ return {
3077
+ name: key.replace(/ToolCall$/, ""),
3078
+ body: recordField(toolCall, key)
3079
+ };
3080
+ }
3081
+ /** Parser for `cursor-agent -p --output-format stream-json`. */
3082
+ function createCursorParser() {
3083
+ return jsonLinesParser((record) => {
3084
+ switch (stringField(record, "type")) {
3085
+ case "system": return stringField(record, "subtype") === "init" ? [{
3086
+ type: "session.started",
3087
+ sessionRef: stringField(record, "session_id"),
3088
+ model: stringField(record, "model"),
3089
+ raw: record
3090
+ }] : [];
3091
+ case "assistant": {
3092
+ const text = asArray(recordField(record, "message")?.content).map((block) => stringField(asRecord(block), "text") ?? "").join("");
3093
+ return text.length > 0 ? [{
3094
+ type: "message",
3095
+ text,
3096
+ delta: false,
3097
+ raw: record
3098
+ }] : [];
3099
+ }
3100
+ case "tool_call": {
3101
+ const id = stringField(record, "call_id") ?? "";
3102
+ const { name, body } = toolOf(recordField(record, "tool_call"));
3103
+ if (stringField(record, "subtype") === "started") return [{
3104
+ type: "tool.call",
3105
+ id,
3106
+ name,
3107
+ input: body?.args ?? parseArguments(body?.arguments),
3108
+ raw: record
3109
+ }];
3110
+ if (stringField(record, "subtype") === "completed") {
3111
+ const result = recordField(body, "result");
3112
+ const success = recordField(result, "success");
3113
+ const content = stringField(success, "content");
3114
+ return [{
3115
+ type: "tool.result",
3116
+ id,
3117
+ output: success !== void 0 ? content ?? outputText(success) : outputText(result),
3118
+ isError: success === void 0,
3119
+ raw: record
3120
+ }];
3121
+ }
3122
+ return [];
3123
+ }
3124
+ case "result": {
3125
+ const events = [];
3126
+ const usage = recordField(record, "usage");
3127
+ if (usage !== void 0) {
3128
+ const cacheRead = numberField(usage, "cacheReadTokens") ?? 0;
3129
+ const cacheWrite = numberField(usage, "cacheWriteTokens") ?? 0;
3130
+ events.push({
3131
+ type: "usage",
3132
+ cumulative: true,
3133
+ usage: usageOf({
3134
+ input: (numberField(usage, "inputTokens") ?? 0) + cacheRead + cacheWrite,
3135
+ output: numberField(usage, "outputTokens"),
3136
+ cachedTokens: cacheRead > 0 ? cacheRead : void 0,
3137
+ cacheWriteTokens: cacheWrite > 0 ? cacheWrite : void 0
3138
+ }),
3139
+ raw: record
3140
+ });
3141
+ }
3142
+ if (record.is_error === true) events.push({
3143
+ type: "error",
3144
+ message: stringField(record, "result") || "Cursor CLI reported an error",
3145
+ fatal: true,
3146
+ raw: record
3147
+ });
3148
+ return events;
3149
+ }
3150
+ default: return [];
3151
+ }
3152
+ });
3153
+ }
3154
+
3155
+ //#endregion
3156
+ //#region ../../packages/ai-cli/src/adapters/droid.ts
3157
+ /**
3158
+ * Factory Droid (`droid`, npm `droid` / `@factory/cli`).
3159
+ *
3160
+ * Verified against the real `droid exec --help` of 0.244.0, a real
3161
+ * `droid exec` run without credentials (init and error lines), the field
3162
+ * names in the shipped code and the docs (`docs.factory.ai/cli/droid-exec`,
3163
+ * `/cli/configuration/mcp`), 2026-10-17.
3164
+ *
3165
+ * - Headless: `droid exec -o stream-json <prompt>` (or `-f <file>`).
3166
+ * - Stream: `system/init` (`session_id`, `model`), whole `message` records
3167
+ * (`role`, `text`), `tool_call` (`id`, `toolName`, `parameters`),
3168
+ * `tool_result` (`id`, `isError`, `value` | `error.message`), a final
3169
+ * `completion` (`finalText`, `usage` with Anthropic-style
3170
+ * `input_tokens`/`cache_read_input_tokens`/`cache_creation_input_tokens`,
3171
+ * read with Anthropic semantics) and `error` (`source`, `message`).
3172
+ * Billing is in Factory credits; no USD cost is reported.
3173
+ * - Permissions: `standard` is Droid's default **read-only** autonomy; raise
3174
+ * it with `args: ["--auto", "medium"]` (low | medium | high). Bypass is
3175
+ * `--skip-permissions-unsafe`. Blocked actions end the run non-zero.
3176
+ * - MCP: `<home>/.factory/mcp.json` (`mcpServers`); the home is relocated
3177
+ * with `FACTORY_HOME_OVERRIDE` (found in the shipped code and confirmed
3178
+ * with `droid mcp list`), so per-run MCP needs a private home.
3179
+ * - Auth: `FACTORY_API_KEY`.
3180
+ */
3181
+ const droidAdapter = {
3182
+ name: "droid",
3183
+ title: "Factory Droid",
3184
+ command: "droid",
3185
+ apiKeyEnv: ["FACTORY_API_KEY"],
3186
+ homeEnv: ["FACTORY_HOME_OVERRIDE"],
3187
+ transports: ["argv", "file"],
3188
+ effort: true,
3189
+ mcp: true,
3190
+ probe: {
3191
+ help: [["exec", "--help"]],
3192
+ version: ["--version"]
3193
+ },
3194
+ verification: {
3195
+ version: "0.244.0",
3196
+ verifiedBy: "help",
3197
+ sources: [
3198
+ "https://docs.factory.ai/cli/droid-exec/overview",
3199
+ "https://docs.factory.ai/cli/configuration/mcp",
3200
+ "https://www.npmjs.com/package/droid"
3201
+ ],
3202
+ checkedAt: "2026-10-17"
3203
+ },
3204
+ plan(context) {
3205
+ const args = [
3206
+ "exec",
3207
+ "--output-format",
3208
+ "stream-json",
3209
+ "--cwd",
3210
+ context.cwd
3211
+ ];
3212
+ const requires = [
3213
+ "--output-format",
3214
+ "stream-json",
3215
+ "--cwd"
3216
+ ];
3217
+ const files = [];
3218
+ let tail;
3219
+ if (context.model !== void 0) {
3220
+ args.push("--model", context.model);
3221
+ requires.push("--model");
3222
+ }
3223
+ if (context.effort !== void 0) {
3224
+ args.push("--reasoning-effort", context.effort);
3225
+ requires.push("--reasoning-effort");
3226
+ }
3227
+ if (context.resume !== void 0) {
3228
+ args.push("--session-id", context.resume);
3229
+ requires.push("--session-id");
3230
+ }
3231
+ if (context.permission === "bypass") {
3232
+ args.push("--skip-permissions-unsafe");
3233
+ requires.push("--skip-permissions-unsafe");
3234
+ }
3235
+ if (context.mcp !== void 0) {
3236
+ const home = requirePrivateHome(context, droidAdapter, "MCP servers");
3237
+ files.push({
3238
+ path: node_path.default.join(home, ".factory", "mcp.json"),
3239
+ content: jsonFile({ mcpServers: mcpServersJson(context.mcp, { typeField: true }) })
3240
+ });
3241
+ }
3242
+ if (context.transport === "file") {
3243
+ const prompt = promptFile(context);
3244
+ files.push(prompt);
3245
+ tail = ["--file", prompt.path];
3246
+ requires.push("--file");
3247
+ } else tail = [argvPrompt(context.prompt)];
3248
+ return {
3249
+ args,
3250
+ tail,
3251
+ env: {
3252
+ ...context.home !== void 0 ? { FACTORY_HOME_OVERRIDE: context.home } : {},
3253
+ FACTORY_DROID_AUTO_UPDATE_ENABLED: "false"
3254
+ },
3255
+ files,
3256
+ requires
3257
+ };
3258
+ },
3259
+ parser: createDroidParser
3260
+ };
3261
+ /** Parser for `droid exec -o stream-json`. */
3262
+ function createDroidParser() {
3263
+ return jsonLinesParser((record) => {
3264
+ switch (stringField(record, "type")) {
3265
+ case "system": return stringField(record, "subtype") === "init" ? [{
3266
+ type: "session.started",
3267
+ sessionRef: stringField(record, "session_id"),
3268
+ model: stringField(record, "model"),
3269
+ raw: record
3270
+ }] : [];
3271
+ case "message": {
3272
+ const text = stringField(record, "text") ?? "";
3273
+ return stringField(record, "role") === "assistant" && text.length > 0 ? [{
3274
+ type: "message",
3275
+ text,
3276
+ delta: false,
3277
+ raw: record
3278
+ }] : [];
3279
+ }
3280
+ case "tool_call": return [{
3281
+ type: "tool.call",
3282
+ id: stringField(record, "id") ?? "",
3283
+ name: stringField(record, "toolName") ?? stringField(record, "toolId") ?? "",
3284
+ input: record.parameters,
3285
+ raw: record
3286
+ }];
3287
+ case "tool_result": {
3288
+ const error = recordField(record, "error");
3289
+ const isError = record.isError === true || error !== void 0;
3290
+ return [{
3291
+ type: "tool.result",
3292
+ id: stringField(record, "id") ?? "",
3293
+ output: isError && error !== void 0 ? stringField(error, "message") ?? "" : outputText(record.value),
3294
+ isError,
3295
+ raw: record
3296
+ }];
3297
+ }
3298
+ case "completion": {
3299
+ const events = [];
3300
+ const usage = anthropicUsage(recordField(record, "usage"));
3301
+ if (usage !== void 0) {
3302
+ const thinking = recordField(record, "usage")?.thinking_tokens;
3303
+ if (typeof thinking === "number" && thinking > 0) usage.reasoningTokens = thinking;
3304
+ events.push({
3305
+ type: "usage",
3306
+ usage,
3307
+ cumulative: true,
3308
+ raw: record
3309
+ });
3310
+ }
3311
+ const text = stringField(record, "finalText") ?? "";
3312
+ if (text.length > 0) events.push({
3313
+ type: "message",
3314
+ text,
3315
+ delta: false,
3316
+ final: true,
3317
+ raw: record
3318
+ });
3319
+ return events;
3320
+ }
3321
+ case "error": return [{
3322
+ type: "error",
3323
+ message: stringField(record, "message") ?? "Droid reported an error",
3324
+ fatal: true,
3325
+ raw: record
3326
+ }];
3327
+ default: return [];
3328
+ }
3329
+ });
3330
+ }
3331
+
3332
+ //#endregion
3333
+ //#region ../../packages/ai-cli/src/adapters/grok.ts
3334
+ /**
3335
+ * Grok Build (`grok`) — xAI's official coding-agent CLI, installed by the
3336
+ * `x.ai/cli/install.sh` script (no npm package).
3337
+ *
3338
+ * Verified against the docs and source only (the binary is distributed by
3339
+ * a curl installer, which was not run): the repo user guide
3340
+ * `crates/codegen/xai-grok-pager/docs/user-guide/{14-headless-mode,
3341
+ * 07-mcp-servers,05-configuration,22-permissions-and-safety}.md`, the clap
3342
+ * definitions in `xai-grok-pager/src/app/cli.rs` and the headless reducer
3343
+ * `headless/reducer/acp.rs`, at source version 1.0.45; latest stable 1.0.62
3344
+ * (2026-10-17). The capability probe checks every flag against the
3345
+ * installed binary's `--help` before the first run.
3346
+ *
3347
+ * - Headless: `grok -p <prompt>` or `--prompt-file <path>`; piped stdin is
3348
+ * not read. `--output-format streaming-json` prints `type`-tagged lines:
3349
+ * `text`, `thought`, `tool_call`, `tool_call_update`, `usage` (per model
3350
+ * response, `input_tokens` uncached) and a final `end` (session id, totals,
3351
+ * `total_cost_usd`); failures print `{"type":"error","message"}`.
3352
+ * - Permissions: headless mode answers every approval request with
3353
+ * "cancelled" (auto-deny); bypass is `--always-approve`.
3354
+ * - MCP: no flag; `[mcp_servers.<name>]` in `$GROK_HOME/config.toml`.
3355
+ * - Auth: `XAI_API_KEY`. Update checks are disabled with
3356
+ * `GROK_DISABLE_AUTOUPDATER=1`.
3357
+ */
3358
+ const grokAdapter = {
3359
+ name: "grok",
3360
+ title: "Grok Build",
3361
+ command: "grok",
3362
+ apiKeyEnv: ["XAI_API_KEY"],
3363
+ homeEnv: ["GROK_HOME"],
3364
+ transports: ["argv", "file"],
3365
+ effort: true,
3366
+ mcp: true,
3367
+ probe: {
3368
+ help: [["--help"]],
3369
+ version: ["--version"]
3370
+ },
3371
+ verification: {
3372
+ version: "1.0.62",
3373
+ verifiedBy: "docs",
3374
+ sources: [
3375
+ "https://docs.x.ai/build/cli/headless-scripting",
3376
+ "https://github.com/xai-org/grok-build/blob/main/crates/codegen/xai-grok-pager/docs/user-guide/14-headless-mode.md",
3377
+ "https://github.com/xai-org/grok-build/blob/main/crates/codegen/xai-grok-pager/docs/user-guide/07-mcp-servers.md"
3378
+ ],
3379
+ checkedAt: "2026-10-17"
3380
+ },
3381
+ plan(context) {
3382
+ const args = ["--output-format", "streaming-json"];
3383
+ const requires = ["--output-format", "streaming-json"];
3384
+ const files = [];
3385
+ let tail;
3386
+ if (context.model !== void 0) {
3387
+ args.push("--model", context.model);
3388
+ requires.push("--model");
3389
+ }
3390
+ if (context.effort !== void 0) {
3391
+ args.push("--reasoning-effort", context.effort);
3392
+ requires.push("--reasoning-effort");
3393
+ }
3394
+ if (context.resume !== void 0) {
3395
+ args.push("--resume", context.resume);
3396
+ requires.push("--resume");
3397
+ }
3398
+ if (context.permission === "bypass") {
3399
+ args.push("--always-approve");
3400
+ requires.push("--always-approve");
3401
+ }
3402
+ if (context.mcp !== void 0) {
3403
+ const home = requirePrivateHome(context, grokAdapter, "MCP servers");
3404
+ files.push({
3405
+ path: node_path.default.join(home, "config.toml"),
3406
+ content: toToml({ mcp_servers: grokMcp(context.mcp) })
3407
+ });
3408
+ }
3409
+ if (context.transport === "file") {
3410
+ const prompt = promptFile(context);
3411
+ files.push(prompt);
3412
+ tail = ["--prompt-file", prompt.path];
3413
+ requires.push("--prompt-file");
3414
+ } else {
3415
+ tail = ["-p", argvPrompt(context.prompt)];
3416
+ requires.push("-p");
3417
+ }
3418
+ return {
3419
+ args,
3420
+ tail,
3421
+ env: {
3422
+ ...context.home !== void 0 ? { GROK_HOME: context.home } : {},
3423
+ GROK_DISABLE_AUTOUPDATER: "1"
3424
+ },
3425
+ files,
3426
+ requires
3427
+ };
3428
+ },
3429
+ parser: createGrokParser
3430
+ };
3431
+ function grokMcp(servers) {
3432
+ const table = {};
3433
+ for (const [name, server] of Object.entries(servers)) {
3434
+ if (server.type === "sse") throw unsupportedOption(grokAdapter, `MCP server "${name}" uses SSE; Grok Build supports stdio and HTTP servers.`);
3435
+ table[name] = server.type === "stdio" ? {
3436
+ command: server.command,
3437
+ args: server.args ?? [],
3438
+ env: server.env
3439
+ } : {
3440
+ url: server.url,
3441
+ headers: server.headers
3442
+ };
3443
+ }
3444
+ return table;
3445
+ }
3446
+ /** Parser for `grok -p … --output-format streaming-json`. */
3447
+ function createGrokParser() {
3448
+ let started = false;
3449
+ let sessionRef;
3450
+ return {
3451
+ ...jsonLinesParser((record) => {
3452
+ const events = [];
3453
+ if (!started) {
3454
+ started = true;
3455
+ events.push({ type: "session.started" });
3456
+ }
3457
+ switch (stringField(record, "type")) {
3458
+ case "text": {
3459
+ const text = stringField(record, "data") ?? "";
3460
+ if (text.length > 0) events.push({
3461
+ type: "message",
3462
+ text,
3463
+ delta: true,
3464
+ raw: record
3465
+ });
3466
+ break;
3467
+ }
3468
+ case "tool_call":
3469
+ events.push({
3470
+ type: "tool.call",
3471
+ id: stringField(record, "toolCallId") ?? "",
3472
+ name: stringField(record, "toolName") ?? stringField(record, "title") ?? "",
3473
+ input: record.rawInput,
3474
+ raw: record
3475
+ });
3476
+ break;
3477
+ case "tool_call_update": {
3478
+ const status = stringField(record, "status");
3479
+ if (status === "completed" || status === "failed") events.push({
3480
+ type: "tool.result",
3481
+ id: stringField(record, "toolCallId") ?? "",
3482
+ output: outputText(record.rawOutput ?? record.content),
3483
+ isError: status === "failed",
3484
+ raw: record
3485
+ });
3486
+ break;
3487
+ }
3488
+ case "usage": {
3489
+ const usage = anthropicUsage(recordField(record, "usage"));
3490
+ if (usage !== void 0) events.push({
3491
+ type: "usage",
3492
+ usage,
3493
+ cumulative: false,
3494
+ raw: record
3495
+ });
3496
+ break;
3497
+ }
3498
+ case "end": {
3499
+ sessionRef = stringField(record, "sessionId") ?? sessionRef;
3500
+ const usage = anthropicUsage(recordField(record, "usage"), numberField(record, "total_cost_usd"));
3501
+ if (usage !== void 0) events.push({
3502
+ type: "usage",
3503
+ usage,
3504
+ cumulative: true,
3505
+ raw: record
3506
+ });
3507
+ break;
3508
+ }
3509
+ case "error": events.push({
3510
+ type: "error",
3511
+ message: stringField(record, "message") ?? "Grok Build reported an error",
3512
+ fatal: true,
3513
+ raw: record
3514
+ });
3515
+ }
3516
+ return events;
3517
+ }),
3518
+ sessionRef: () => sessionRef
3519
+ };
3520
+ }
3521
+
3522
+ //#endregion
3523
+ //#region ../../packages/ai-cli/src/adapters/kimi.ts
3524
+ /**
3525
+ * Kimi Code CLI (`kimi`, npm `@moonshot-ai/kimi-code`) — Moonshot AI's
3526
+ * coding agent, the successor of the archived Python `kimi-cli`.
3527
+ *
3528
+ * Verified against the real `kimi --help` of 2.2.0 and the docs/source at
3529
+ * that tag (`docs/en/reference/kimi-command.md`, `apps/kimi-code/src/cli/
3530
+ * {options.ts,prompt-render.ts,v2/run-v2-print.ts}`), 2026-10-17.
3531
+ *
3532
+ * - Headless: `kimi -p <prompt> --output-format stream-json`; the prompt is
3533
+ * an argv value only (print mode never reads stdin).
3534
+ * - Stream: OpenAI-chat-shaped messages, one per line, never deltas:
3535
+ * `{"role":"assistant","content","tool_calls"}`, `{"role":"tool",
3536
+ * "tool_call_id","content"}`, and `{"role":"meta","type":…}` records. The
3537
+ * session id appears only in the final `session.resume_hint` record.
3538
+ * No token usage is printed.
3539
+ * - Permissions: print mode always runs in "Never Ask" mode (every tool call
3540
+ * runs; only static deny rules apply) and rejects `--yolo`/`--plan`, so
3541
+ * there is no standard mode to choose. The adapter refuses
3542
+ * `permission: "standard"` instead of pretending to enforce it; pass
3543
+ * `"bypass"` to accept the CLI's behaviour.
3544
+ * - MCP: no flag; `$KIMI_CODE_HOME/mcp.json` (`mcpServers`), so per-run MCP
3545
+ * needs a private home.
3546
+ * - Auth: shell `KIMI_API_KEY`/`MOONSHOT_API_KEY` are not read. For an API
3547
+ * key set `KIMI_MODEL_NAME` (and optionally `KIMI_MODEL_PROVIDER_TYPE`,
3548
+ * `KIMI_MODEL_BASE_URL`) in `env`; `apiKey` becomes `KIMI_MODEL_API_KEY`.
3549
+ */
3550
+ const kimiAdapter = {
3551
+ name: "kimi",
3552
+ title: "Kimi Code",
3553
+ command: "kimi",
3554
+ apiKeyEnv: ["KIMI_MODEL_API_KEY"],
3555
+ homeEnv: ["KIMI_CODE_HOME"],
3556
+ transports: ["argv"],
3557
+ effort: false,
3558
+ mcp: true,
3559
+ probe: {
3560
+ help: [["--help"]],
3561
+ version: ["--version"]
3562
+ },
3563
+ verification: {
3564
+ version: "2.2.0",
3565
+ verifiedBy: "help",
3566
+ sources: [
3567
+ "https://moonshotai.github.io/kimi-code/",
3568
+ "https://github.com/MoonshotAI/kimi-code/blob/%40moonshot-ai/kimi-code%402.2.0/docs/en/reference/kimi-command.md",
3569
+ "https://github.com/MoonshotAI/kimi-code/blob/%40moonshot-ai/kimi-code%402.2.0/docs/en/customization/mcp.md"
3570
+ ],
3571
+ checkedAt: "2026-10-17"
3572
+ },
3573
+ plan(context) {
3574
+ if (context.permission !== "bypass") throw unsupportedOption(kimiAdapter, "print mode runs every tool call without asking and has no standard approval mode; pass `permission: \"bypass\"` to run it.");
3575
+ const args = ["--output-format", "stream-json"];
3576
+ const requires = [
3577
+ "-p",
3578
+ "--output-format",
3579
+ "stream-json"
3580
+ ];
3581
+ const files = [];
3582
+ if (context.model !== void 0) {
3583
+ args.push("--model", context.model);
3584
+ requires.push("--model");
3585
+ }
3586
+ if (context.resume !== void 0) {
3587
+ args.push("--session", context.resume);
3588
+ requires.push("--session");
3589
+ }
3590
+ if (context.mcp !== void 0) {
3591
+ const home = requirePrivateHome(context, kimiAdapter, "MCP servers");
3592
+ files.push({
3593
+ path: node_path.default.join(home, "mcp.json"),
3594
+ content: jsonFile({ mcpServers: kimiMcp(context.mcp) })
3595
+ });
3596
+ }
3597
+ return {
3598
+ args,
3599
+ tail: ["-p", argvPrompt(context.prompt)],
3600
+ env: {
3601
+ ...context.home !== void 0 ? { KIMI_CODE_HOME: context.home } : {},
3602
+ KIMI_CODE_NO_AUTO_UPDATE: "1",
3603
+ KIMI_DISABLE_TELEMETRY: "1"
3604
+ },
3605
+ files,
3606
+ requires
3607
+ };
3608
+ },
3609
+ parser: createKimiParser
3610
+ };
3611
+ /** Kimi's `mcpServers`: SSE servers are marked with `transport: "sse"`. */
3612
+ function kimiMcp(servers) {
3613
+ const rendered = mcpServersJson(servers);
3614
+ for (const [name, server] of Object.entries(servers)) {
3615
+ const entry = rendered[name];
3616
+ if (server.type === "sse" && entry !== void 0) entry.transport = "sse";
3617
+ }
3618
+ return rendered;
3619
+ }
3620
+ /** Parser for `kimi -p … --output-format stream-json`. */
3621
+ function createKimiParser() {
3622
+ let sessionRef;
3623
+ return {
3624
+ ...jsonLinesParser((record) => {
3625
+ const role = stringField(record, "role");
3626
+ if (role === "meta") {
3627
+ const type = stringField(record, "type");
3628
+ if (type === "system.version") return [{
3629
+ type: "session.started",
3630
+ raw: record
3631
+ }];
3632
+ if (type === "session.resume_hint") {
3633
+ sessionRef = stringField(record, "session_id");
3634
+ return [];
3635
+ }
3636
+ if (type === "turn.step.retrying") return [{
3637
+ type: "error",
3638
+ message: `retrying after ${stringField(record, "error_name") ?? "error"}: ${stringField(record, "error_message") ?? ""}`,
3639
+ raw: record
3640
+ }];
3641
+ return [];
3642
+ }
3643
+ if (role === "assistant") {
3644
+ const events = [];
3645
+ const content = stringField(record, "content");
3646
+ if (content !== void 0 && content.length > 0) events.push({
3647
+ type: "message",
3648
+ text: content,
3649
+ delta: false,
3650
+ raw: record
3651
+ });
3652
+ for (const item of asArray(record.tool_calls)) {
3653
+ const call = asRecord(item);
3654
+ const fn = asRecord(call?.function);
3655
+ events.push({
3656
+ type: "tool.call",
3657
+ id: stringField(call, "id") ?? "",
3658
+ name: stringField(fn, "name") ?? "",
3659
+ input: parseArguments(fn?.arguments),
3660
+ raw: item
3661
+ });
3662
+ }
3663
+ return events;
3664
+ }
3665
+ if (role === "tool") return [{
3666
+ type: "tool.result",
3667
+ id: stringField(record, "tool_call_id") ?? "",
3668
+ output: outputText(record.content),
3669
+ isError: false,
3670
+ raw: record
3671
+ }];
3672
+ return [];
3673
+ }),
3674
+ sessionRef: () => sessionRef
3675
+ };
3676
+ }
3677
+
3678
+ //#endregion
3679
+ //#region ../../packages/ai-cli/src/adapters/mistral-vibe.ts
3680
+ /** Folder (inside the run directory) Vibe writes `export.json` to. */
3681
+ const EXPORT_DIR = "vibe-output";
3682
+ /**
3683
+ * Mistral Vibe (`vibe`, PyPI `mistral-vibe`).
3684
+ *
3685
+ * Verified against the official repo at tag v2.26.1 only (README,
3686
+ * `vibe/cli/entrypoint.py`, `vibe/cli/programmatic.py`,
3687
+ * `vibe/app_server/run_export.py`, `vibe/core/config/*`); Vibe needs Python
3688
+ * 3.12, so no real `--help` was run, 2026-10-17. The capability probe checks
3689
+ * every flag against the installed `--help`.
3690
+ *
3691
+ * - Headless: `vibe -p <prompt> --output streaming` (or `--prompt-file`;
3692
+ * `-p` with no value reads stdin). The stream is NDJSON of completed
3693
+ * history entries (camelCase): `message` (`role`, `content[].text`), and
3694
+ * `effect` — a tool call and its result in one line (`detail.toolName`,
3695
+ * `detail.input`, `state.status`, `state.outputText`/`state.error`).
3696
+ * There is no usage, cost or final record in the stream, so the adapter
3697
+ * adds `--output-dir` and reads usage, cost, session id and outcome from
3698
+ * `export.json` when the stream ends.
3699
+ * - Model: no flag; `VIBE_ACTIVE_MODEL=<alias>` (set from `model`). Effort
3700
+ * is a per-model config field, so `effort` is refused.
3701
+ * - Permissions: headless auto-denies every approval (the default agent
3702
+ * only auto-approves file edits); bypass is `--auto-approve`.
3703
+ * - MCP: `[[mcp_servers]]` in `$VIBE_HOME/config.toml` (private home).
3704
+ * - Auth: `MISTRAL_API_KEY`; home `VIBE_HOME`.
3705
+ */
3706
+ const mistralVibeAdapter = {
3707
+ name: "mistral-vibe",
3708
+ title: "Mistral Vibe",
3709
+ command: "vibe",
3710
+ apiKeyEnv: ["MISTRAL_API_KEY"],
3711
+ homeEnv: ["VIBE_HOME"],
3712
+ transports: [
3713
+ "argv",
3714
+ "file",
3715
+ "stdin"
3716
+ ],
3717
+ effort: false,
3718
+ mcp: true,
3719
+ probe: {
3720
+ help: [["--help"]],
3721
+ version: ["--version"]
3722
+ },
3723
+ verification: {
3724
+ version: "2.26.1",
3725
+ verifiedBy: "docs",
3726
+ sources: [
3727
+ "https://github.com/mistralai/mistral-vibe/blob/v2.26.1/README.md",
3728
+ "https://github.com/mistralai/mistral-vibe/blob/v2.26.1/vibe/cli/entrypoint.py",
3729
+ "https://github.com/mistralai/mistral-vibe/blob/v2.26.1/vibe/cli/programmatic.py"
3730
+ ],
3731
+ checkedAt: "2026-10-17"
3732
+ },
3733
+ plan(context) {
3734
+ const args = [
3735
+ "--output",
3736
+ "streaming",
3737
+ "--output-dir",
3738
+ node_path.default.join(context.runDir, EXPORT_DIR)
3739
+ ];
3740
+ const requires = [
3741
+ "--output",
3742
+ "streaming",
3743
+ "--output-dir"
3744
+ ];
3745
+ const files = [];
3746
+ const env = {};
3747
+ if (context.model !== void 0) env.VIBE_ACTIVE_MODEL = context.model;
3748
+ if (context.resume !== void 0) {
3749
+ args.push("--resume", context.resume);
3750
+ requires.push("--resume");
3751
+ }
3752
+ if (context.permission === "bypass") {
3753
+ args.push("--auto-approve");
3754
+ requires.push("--auto-approve");
3755
+ }
3756
+ if (context.mcp !== void 0) {
3757
+ const home = requirePrivateHome(context, mistralVibeAdapter, "MCP servers");
3758
+ files.push({
3759
+ path: node_path.default.join(home, "config.toml"),
3760
+ content: toToml({ mcp_servers: vibeMcp(context.mcp) })
3761
+ });
3762
+ }
3763
+ if (context.home !== void 0) env.VIBE_HOME = context.home;
3764
+ if (context.transport === "file") {
3765
+ const prompt = promptFile(context);
3766
+ files.push(prompt);
3767
+ requires.push("--prompt-file");
3768
+ return {
3769
+ args,
3770
+ tail: ["--prompt-file", prompt.path],
3771
+ env,
3772
+ files,
3773
+ requires
3774
+ };
3775
+ }
3776
+ requires.push("-p");
3777
+ if (context.transport === "stdin") return {
3778
+ args,
3779
+ tail: ["-p"],
3780
+ stdin: context.prompt,
3781
+ env,
3782
+ files,
3783
+ requires
3784
+ };
3785
+ return {
3786
+ args,
3787
+ tail: ["-p", argvPrompt(context.prompt)],
3788
+ env,
3789
+ files,
3790
+ requires
3791
+ };
3792
+ },
3793
+ parser: createVibeParser
3794
+ };
3795
+ function vibeMcp(servers) {
3796
+ return Object.entries(servers).map(([name, server]) => server.type === "stdio" ? {
3797
+ name,
3798
+ transport: "stdio",
3799
+ command: server.command,
3800
+ args: server.args ?? [],
3801
+ env: server.env
3802
+ } : {
3803
+ name,
3804
+ transport: server.type === "http" ? "streamable-http" : "http",
3805
+ url: server.url,
3806
+ auth: server.headers !== void 0 ? {
3807
+ type: "static",
3808
+ headers: server.headers
3809
+ } : void 0
3810
+ });
3811
+ }
3812
+ /** Read Vibe's `export.json`: usage, cost, session id and outcome of the run. */
3813
+ function exportEvents(context) {
3814
+ if (context === void 0) return { events: [] };
3815
+ let report;
3816
+ try {
3817
+ report = parseJsonLine((0, node_fs.readFileSync)(node_path.default.join(context.runDir, EXPORT_DIR, "export.json"), "utf8"));
3818
+ } catch {
3819
+ return { events: [] };
3820
+ }
3821
+ if (report === void 0) return { events: [] };
3822
+ const events = [];
3823
+ const usage = recordField(report, "usage");
3824
+ if (usage !== void 0) {
3825
+ const cached = numberField(usage, "cached_input_tokens");
3826
+ events.push({
3827
+ type: "usage",
3828
+ cumulative: true,
3829
+ usage: usageOf({
3830
+ input: numberField(usage, "input_tokens"),
3831
+ output: numberField(usage, "output_tokens"),
3832
+ total: numberField(usage, "total_tokens"),
3833
+ cachedTokens: cached !== void 0 && cached > 0 ? cached : void 0,
3834
+ costUSD: numberField(report, "cost_usd")
3835
+ }),
3836
+ raw: report
3837
+ });
3838
+ }
3839
+ const outcome = stringField(report, "outcome");
3840
+ if (outcome !== void 0 && outcome !== "finished") events.push({
3841
+ type: "error",
3842
+ message: stringField(recordField(report, "error"), "message") ?? `Mistral Vibe stopped: ${outcome}`,
3843
+ fatal: true,
3844
+ raw: report
3845
+ });
3846
+ return {
3847
+ events,
3848
+ sessionRef: stringField(report, "session_id")
3849
+ };
3850
+ }
3851
+ /** Parser for `vibe -p … --output streaming` (plus the run's `export.json`). */
3852
+ function createVibeParser(context) {
3853
+ let started = false;
3854
+ let sessionRef;
3855
+ return {
3856
+ ...jsonLinesParser((record) => {
3857
+ const events = [];
3858
+ const session = stringField(record, "sessionId");
3859
+ if (session !== void 0) sessionRef = session;
3860
+ if (!started) {
3861
+ started = true;
3862
+ events.push({
3863
+ type: "session.started",
3864
+ sessionRef: session,
3865
+ raw: record
3866
+ });
3867
+ }
3868
+ switch (stringField(record, "type")) {
3869
+ case "message": {
3870
+ if (stringField(record, "role") !== "assistant") break;
3871
+ const text = asArray(record.content).map((block) => stringField(asRecord(block), "text") ?? "").join("");
3872
+ if (text.length > 0) events.push({
3873
+ type: "message",
3874
+ text,
3875
+ delta: false,
3876
+ raw: record
3877
+ });
3878
+ break;
3879
+ }
3880
+ case "effect": {
3881
+ const detail = recordField(record, "detail");
3882
+ const state = recordField(record, "state");
3883
+ const status = stringField(state, "status");
3884
+ const id = stringField(record, "id") ?? "";
3885
+ events.push({
3886
+ type: "tool.call",
3887
+ id,
3888
+ name: stringField(detail, "toolName") ?? stringField(record, "title") ?? "",
3889
+ input: detail?.input,
3890
+ raw: record
3891
+ });
3892
+ if (status === "completed" || status === "failed" || status === "cancelled" || status === "skipped") {
3893
+ const error = stringField(recordField(state, "error"), "message");
3894
+ events.push({
3895
+ type: "tool.result",
3896
+ id,
3897
+ output: error ?? stringField(state, "outputText") ?? outputText(state?.output),
3898
+ isError: status !== "completed",
3899
+ raw: record
3900
+ });
3901
+ }
3902
+ break;
3903
+ }
3904
+ case "notice": if (stringField(record, "level") === "error") events.push({
3905
+ type: "error",
3906
+ message: stringField(record, "message") ?? "error",
3907
+ raw: record
3908
+ });
3909
+ }
3910
+ return events;
3911
+ }, () => {
3912
+ const exported = exportEvents(context);
3913
+ sessionRef = exported.sessionRef ?? sessionRef;
3914
+ return exported.events;
3915
+ }),
3916
+ sessionRef: () => sessionRef
3917
+ };
3918
+ }
3919
+
3920
+ //#endregion
3921
+ //#region ../../packages/ai-cli/src/adapters/opencode.ts
3922
+ /**
3923
+ * OpenCode (`opencode`, npm `opencode-ai`).
3924
+ *
3925
+ * Verified against the real `opencode --help` / `opencode run --help` of
3926
+ * 1.19.3, the CLI source at that tag (`packages/opencode/src/cli/cmd/run.ts`)
3927
+ * and the docs (`cli.mdx`, `config.mdx`, `mcp-servers.mdx`,
3928
+ * `permissions.mdx`), 2026-10-17.
3929
+ *
3930
+ * - Headless: `opencode run --format json <message..>`; with no message,
3931
+ * piped stdin is the prompt.
3932
+ * - Stream: `{"type","timestamp","sessionID","part"|"error"}` lines of six
3933
+ * types: `step_start`, `text` (whole text blocks, no deltas), `reasoning`,
3934
+ * `tool_use` (emitted once the tool finished — call and result together,
3935
+ * `state.status` `completed`/`error`), `step_finish` (per-step `cost` in
3936
+ * USD and `tokens{input,output,reasoning,cache{read,write}}`, input net of
3937
+ * cache) and `error{error:{name,data:{message}}}`. No init or final record.
3938
+ * - Permissions: `standard` keeps OpenCode's own rules (permissive by
3939
+ * default; anything that would ask is auto-rejected in `run`); bypass is
3940
+ * `--auto`.
3941
+ * - MCP: the `mcp` key of a config file named by `OPENCODE_CONFIG`, written
3942
+ * to the run directory — works with any home.
3943
+ * - Home: OpenCode follows XDG; a private/owned home sets `XDG_CONFIG_HOME`,
3944
+ * `XDG_DATA_HOME` (auth lives here), `XDG_STATE_HOME` and `XDG_CACHE_HOME`
3945
+ * beneath it. Provider keys use the provider's own variable
3946
+ * (`ANTHROPIC_API_KEY`, `OPENAI_API_KEY`, …) — pass them through `env`.
3947
+ * - The working directory is passed with `--dir`, because OpenCode prefers a
3948
+ * (possibly stale) `PWD` over the process cwd.
3949
+ */
3950
+ const opencodeAdapter = {
3951
+ name: "opencode",
3952
+ title: "OpenCode",
3953
+ command: "opencode",
3954
+ apiKeyEnv: [],
3955
+ homeEnv: [
3956
+ "XDG_CONFIG_HOME",
3957
+ "XDG_DATA_HOME",
3958
+ "XDG_STATE_HOME",
3959
+ "XDG_CACHE_HOME"
3960
+ ],
3961
+ transports: ["argv", "stdin"],
3962
+ effort: true,
3963
+ mcp: true,
3964
+ probe: {
3965
+ help: [["run", "--help"]],
3966
+ version: ["--version"]
3967
+ },
3968
+ verification: {
3969
+ version: "1.19.3",
3970
+ verifiedBy: "help",
3971
+ sources: [
3972
+ "https://opencode.ai/docs/cli/",
3973
+ "https://opencode.ai/docs/mcp-servers/",
3974
+ "https://opencode.ai/docs/permissions/",
3975
+ "https://github.com/anomalyco/opencode/blob/v1.19.3/packages/opencode/src/cli/cmd/run.ts"
3976
+ ],
3977
+ checkedAt: "2026-10-17"
3978
+ },
3979
+ plan(context) {
3980
+ const args = [
3981
+ "run",
3982
+ "--format",
3983
+ "json",
3984
+ "--dir",
3985
+ context.cwd
3986
+ ];
3987
+ const requires = ["--format", "--dir"];
3988
+ const files = [];
3989
+ const env = { OPENCODE_DISABLE_AUTOUPDATE: "1" };
3990
+ if (context.model !== void 0) {
3991
+ args.push("--model", context.model);
3992
+ requires.push("--model");
3993
+ }
3994
+ if (context.effort !== void 0) {
3995
+ args.push("--variant", context.effort);
3996
+ requires.push("--variant");
3997
+ }
3998
+ if (context.resume !== void 0) {
3999
+ args.push("--session", context.resume);
4000
+ requires.push("--session");
4001
+ }
4002
+ if (context.permission === "bypass") {
4003
+ args.push("--auto");
4004
+ requires.push("--auto");
4005
+ }
4006
+ if (context.mcp !== void 0) {
4007
+ const file = node_path.default.join(context.runDir, "opencode.json");
4008
+ files.push({
4009
+ path: file,
4010
+ content: jsonFile({
4011
+ $schema: "https://opencode.ai/config.json",
4012
+ mcp: opencodeMcp(context.mcp)
4013
+ })
4014
+ });
4015
+ env.OPENCODE_CONFIG = file;
4016
+ }
4017
+ if (context.home !== void 0) {
4018
+ env.XDG_CONFIG_HOME = node_path.default.join(context.home, "config");
4019
+ env.XDG_DATA_HOME = node_path.default.join(context.home, "data");
4020
+ env.XDG_STATE_HOME = node_path.default.join(context.home, "state");
4021
+ env.XDG_CACHE_HOME = node_path.default.join(context.home, "cache");
4022
+ }
4023
+ return {
4024
+ args,
4025
+ tail: context.transport === "argv" ? [argvPrompt(context.prompt)] : [],
4026
+ stdin: context.transport === "stdin" ? context.prompt : void 0,
4027
+ env,
4028
+ files,
4029
+ requires
4030
+ };
4031
+ },
4032
+ parser: createOpencodeParser
4033
+ };
4034
+ function opencodeMcp(servers) {
4035
+ const rendered = {};
4036
+ for (const [name, server] of Object.entries(servers)) rendered[name] = server.type === "stdio" ? {
4037
+ type: "local",
4038
+ command: [server.command, ...server.args ?? []],
4039
+ ...server.env !== void 0 ? { environment: server.env } : {},
4040
+ enabled: true
4041
+ } : {
4042
+ type: "remote",
4043
+ url: server.url,
4044
+ ...server.headers !== void 0 ? { headers: server.headers } : {},
4045
+ enabled: true
4046
+ };
4047
+ return rendered;
4048
+ }
4049
+ /** Parser for `opencode run --format json`. */
4050
+ function createOpencodeParser() {
4051
+ let started = false;
4052
+ return jsonLinesParser((record) => {
4053
+ const events = [];
4054
+ const part = recordField(record, "part");
4055
+ if (!started) {
4056
+ started = true;
4057
+ events.push({
4058
+ type: "session.started",
4059
+ sessionRef: stringField(record, "sessionID"),
4060
+ raw: record
4061
+ });
4062
+ }
4063
+ switch (stringField(record, "type")) {
4064
+ case "text": {
4065
+ const text = stringField(part, "text") ?? "";
4066
+ if (text.length > 0) events.push({
4067
+ type: "message",
4068
+ text,
4069
+ delta: false,
4070
+ raw: record
4071
+ });
4072
+ break;
4073
+ }
4074
+ case "tool_use": {
4075
+ const state = recordField(part, "state");
4076
+ const id = stringField(part, "callID") ?? stringField(part, "id") ?? "";
4077
+ const failed = stringField(state, "status") === "error";
4078
+ events.push({
4079
+ type: "tool.call",
4080
+ id,
4081
+ name: stringField(part, "tool") ?? "",
4082
+ input: state?.input,
4083
+ raw: record
4084
+ }, {
4085
+ type: "tool.result",
4086
+ id,
4087
+ output: (failed ? stringField(state, "error") : stringField(state, "output")) ?? "",
4088
+ isError: failed,
4089
+ raw: record
4090
+ });
4091
+ break;
4092
+ }
4093
+ case "step_finish": {
4094
+ const tokens = recordField(part, "tokens");
4095
+ const cache = recordField(tokens, "cache");
4096
+ if (tokens !== void 0) {
4097
+ const uncached = numberField(tokens, "input") ?? 0;
4098
+ const cacheRead = numberField(cache, "read") ?? 0;
4099
+ const cacheWrite = numberField(cache, "write") ?? 0;
4100
+ events.push({
4101
+ type: "usage",
4102
+ cumulative: false,
4103
+ usage: usageOf({
4104
+ input: uncached + cacheRead + cacheWrite,
4105
+ output: numberField(tokens, "output"),
4106
+ cachedTokens: cacheRead > 0 ? cacheRead : void 0,
4107
+ cacheWriteTokens: cacheWrite > 0 ? cacheWrite : void 0,
4108
+ reasoningTokens: numberField(tokens, "reasoning") || void 0,
4109
+ costUSD: numberField(part, "cost")
4110
+ }),
4111
+ raw: record
4112
+ });
4113
+ }
4114
+ break;
4115
+ }
4116
+ case "error": {
4117
+ const error = asRecord(record.error);
4118
+ const name = stringField(error, "name") ?? "Error";
4119
+ const message = stringField(recordField(error, "data"), "message") ?? name;
4120
+ events.push({
4121
+ type: "error",
4122
+ message: `${name}: ${message}`,
4123
+ fatal: true,
4124
+ raw: record
4125
+ });
4126
+ break;
4127
+ }
4128
+ }
4129
+ return events;
4130
+ });
4131
+ }
4132
+
4133
+ //#endregion
4134
+ //#region ../../packages/ai-cli/src/adapters/qwen-code.ts
4135
+ /**
4136
+ * Qwen Code (`qwen`, npm `@qwen-code/qwen-code`), a Gemini CLI fork.
4137
+ *
4138
+ * Verified against the real `qwen --help` of 0.25.2 and the docs/source at
4139
+ * tag v0.25.2 (`docs/users/features/headless.md`, `nonInteractive/types.ts`),
4140
+ * 2026-10-17.
4141
+ *
4142
+ * - Headless: positional prompt (`-p` is deprecated) with
4143
+ * `--output-format stream-json`; piped stdin is used as the prompt when no
4144
+ * positional prompt is given.
4145
+ * - Stream: the Claude Code `stream-json` shape (`system/init`, `assistant`,
4146
+ * `user` tool results, `result`), with `input_tokens` that already include
4147
+ * cached tokens and no cost field.
4148
+ * - Permissions: headless auto-denies tools that need confirmation (the tool
4149
+ * result says so and `permission_denials` lists it); bypass is `--yolo`.
4150
+ * - MCP: `--mcp-config <file>` with `{"mcpServers":…}` — written to the run
4151
+ * directory, so it works with any home. Remote servers use `httpUrl`
4152
+ * (streamable HTTP) or `url` (SSE).
4153
+ * - Home: `QWEN_HOME`. Auth depends on the auth type: `OPENAI_API_KEY`
4154
+ * (default for `apiKey`), or `ANTHROPIC_API_KEY` / `GEMINI_API_KEY` with
4155
+ * `--auth-type` in `args`.
4156
+ * - Effort has no flag (it is the `model.reasoningEffort` setting), so
4157
+ * `effort` is refused.
4158
+ */
4159
+ const qwenCodeAdapter = {
4160
+ name: "qwen-code",
4161
+ title: "Qwen Code",
4162
+ command: "qwen",
4163
+ apiKeyEnv: ["OPENAI_API_KEY"],
4164
+ homeEnv: ["QWEN_HOME"],
4165
+ transports: ["argv", "stdin"],
4166
+ effort: false,
4167
+ mcp: true,
4168
+ probe: {
4169
+ help: [["--help"]],
4170
+ version: ["--version"]
4171
+ },
4172
+ verification: {
4173
+ version: "0.25.2",
4174
+ verifiedBy: "help",
4175
+ sources: ["https://qwenlm.github.io/qwen-code-docs/", "https://github.com/QwenLM/qwen-code/blob/v0.25.2/docs/users/features/headless.md"],
4176
+ checkedAt: "2026-10-17"
4177
+ },
4178
+ plan(context) {
4179
+ const args = ["--output-format", "stream-json"];
4180
+ const requires = ["--output-format", "stream-json"];
4181
+ const files = [];
4182
+ if (context.model !== void 0) {
4183
+ args.push("--model", context.model);
4184
+ requires.push("--model");
4185
+ }
4186
+ if (context.resume !== void 0) {
4187
+ args.push("--resume", context.resume);
4188
+ requires.push("--resume");
4189
+ }
4190
+ if (context.permission === "bypass") {
4191
+ args.push("--yolo");
4192
+ requires.push("--yolo");
4193
+ }
4194
+ if (context.mcp !== void 0) {
4195
+ const file = node_path.default.join(context.runDir, "mcp.json");
4196
+ files.push({
4197
+ path: file,
4198
+ content: jsonFile({ mcpServers: mcpServersJson(context.mcp, { httpUrlKey: "httpUrl" }) })
4199
+ });
4200
+ args.push("--mcp-config", file);
4201
+ requires.push("--mcp-config");
4202
+ }
4203
+ return {
4204
+ args,
4205
+ tail: context.transport === "argv" ? [argvPrompt(context.prompt)] : [],
4206
+ stdin: context.transport === "stdin" ? context.prompt : void 0,
4207
+ env: {
4208
+ ...context.home !== void 0 ? { QWEN_HOME: context.home } : {},
4209
+ QWEN_CODE_SUPPRESS_YOLO_WARNING: "1"
4210
+ },
4211
+ files,
4212
+ requires
4213
+ };
4214
+ },
4215
+ parser: () => createClaudeStreamParser({
4216
+ usage: "inclusive",
4217
+ vendor: "Qwen Code"
4218
+ })
4219
+ };
4220
+
4221
+ //#endregion
4222
+ //#region ../../packages/ai-cli/src/adapters/reasonix.ts
4223
+ /**
4224
+ * Reasonix (`reasonix`, npm `reasonix`) — a cache-first coding agent built
4225
+ * around DeepSeek (now multi-provider); the npm package launches a prebuilt
4226
+ * Go binary.
4227
+ *
4228
+ * Verified against the real `reasonix run --help` / `--version` of 2.43.0,
4229
+ * real stream-json output (captured against a local fake model server) and
4230
+ * the docs at that tag (`docs/CLI.md`, `docs/CONFIG_PATHS.md`), 2026-10-17.
4231
+ *
4232
+ * - Headless: `reasonix run --output-format stream-json <task>`; with no
4233
+ * positional task, piped stdin is the prompt.
4234
+ * - Stream: every line has `kind` (`text` deltas, `message` full text,
4235
+ * `tool_dispatch`/`tool_started`/`tool_result` with a `tool` object whose
4236
+ * non-empty `err` marks a failure, `usage` per model call, `turn_done`) plus
4237
+ * `sessionId`; the final line is `{"type":"result",…}` with totals.
4238
+ * - Permissions: headless "ask" mode refuses writes (listed in
4239
+ * `permission_denials`; exit 4 in an untrusted folder); bypass is `--yolo`.
4240
+ * - MCP: no flag; `[[plugins]]` in `$REASONIX_HOME/config.toml`.
4241
+ * - Auth: provider keys are read only from `$REASONIX_HOME/.env`, never the
4242
+ * shell environment, so `apiKey` is written there (0600) as
4243
+ * `DEEPSEEK_API_KEY` — this needs a private home. For another provider,
4244
+ * put its variable in that file yourself via a caller-owned home.
4245
+ */
4246
+ const reasonixAdapter = {
4247
+ name: "reasonix",
4248
+ title: "Reasonix",
4249
+ command: "reasonix",
4250
+ apiKeyEnv: ["DEEPSEEK_API_KEY"],
4251
+ homeEnv: ["REASONIX_HOME"],
4252
+ transports: ["argv", "stdin"],
4253
+ effort: true,
4254
+ mcp: true,
4255
+ probe: {
4256
+ help: [["run", "--help"]],
4257
+ version: ["--version"]
4258
+ },
4259
+ verification: {
4260
+ version: "2.43.0",
4261
+ verifiedBy: "help",
4262
+ sources: [
4263
+ "https://github.com/esengine/DeepSeek-Reasonix/blob/v2.43.0/docs/CLI.md",
4264
+ "https://github.com/esengine/DeepSeek-Reasonix/blob/v2.43.0/docs/CONFIG_PATHS.md",
4265
+ "https://www.npmjs.com/package/reasonix"
4266
+ ],
4267
+ checkedAt: "2026-10-17"
4268
+ },
4269
+ plan(context) {
4270
+ const args = [
4271
+ "run",
4272
+ "--output-format",
4273
+ "stream-json"
4274
+ ];
4275
+ const requires = ["--output-format", "stream-json"];
4276
+ const files = [];
4277
+ if (context.model !== void 0) {
4278
+ args.push("--model", context.model);
4279
+ requires.push("--model");
4280
+ }
4281
+ if (context.effort !== void 0) {
4282
+ args.push("--effort", mapEffort(context.effort, {
4283
+ none: "disabled",
4284
+ low: "low",
4285
+ high: "high",
4286
+ max: "max"
4287
+ }));
4288
+ requires.push("--effort");
4289
+ }
4290
+ if (context.resume !== void 0) {
4291
+ args.push("--resume", context.resume);
4292
+ requires.push("--resume");
4293
+ }
4294
+ if (context.permission === "bypass") {
4295
+ args.push("--yolo");
4296
+ requires.push("--yolo");
4297
+ }
4298
+ if (context.mcp !== void 0) {
4299
+ const home = requirePrivateHome(context, reasonixAdapter, "MCP servers");
4300
+ files.push({
4301
+ path: node_path.default.join(home, "config.toml"),
4302
+ content: toToml({ plugins: reasonixPlugins(context.mcp) })
4303
+ });
4304
+ }
4305
+ if (context.apiKey !== void 0) {
4306
+ const home = requirePrivateHome(context, reasonixAdapter, "API keys (its .env file)");
4307
+ files.push({
4308
+ path: node_path.default.join(home, ".env"),
4309
+ content: dotenvFile({ DEEPSEEK_API_KEY: context.apiKey })
4310
+ });
4311
+ }
4312
+ return {
4313
+ args,
4314
+ tail: context.transport === "argv" ? [argvPrompt(context.prompt)] : [],
4315
+ stdin: context.transport === "stdin" ? context.prompt : void 0,
4316
+ env: {
4317
+ ...context.home !== void 0 ? { REASONIX_HOME: context.home } : {},
4318
+ REASONIX_TELEMETRY: "off"
4319
+ },
4320
+ files,
4321
+ requires
4322
+ };
4323
+ },
4324
+ parser: createReasonixParser
4325
+ };
4326
+ function reasonixPlugins(servers) {
4327
+ return Object.entries(servers).map(([name, server]) => server.type === "stdio" ? {
4328
+ name,
4329
+ type: "stdio",
4330
+ command: server.command,
4331
+ args: server.args ?? [],
4332
+ env: server.env
4333
+ } : {
4334
+ name,
4335
+ type: server.type,
4336
+ url: server.url,
4337
+ headers: server.headers
4338
+ });
4339
+ }
4340
+ /** Reasonix per-call usage (`promptTokens` includes cache hits). */
4341
+ function callUsage(usage) {
4342
+ if (usage === void 0) return;
4343
+ const cacheHit = numberField(usage, "cacheHitTokens");
4344
+ return usageOf({
4345
+ input: numberField(usage, "promptTokens"),
4346
+ output: numberField(usage, "completionTokens"),
4347
+ total: numberField(usage, "totalTokens"),
4348
+ cachedTokens: cacheHit !== void 0 && cacheHit > 0 ? cacheHit : void 0,
4349
+ reasoningTokens: numberField(usage, "reasoningTokens"),
4350
+ costUSD: numberField(usage, "costUsd")
4351
+ });
4352
+ }
4353
+ /** Parser for `reasonix run --output-format stream-json`. */
4354
+ function createReasonixParser() {
4355
+ let started = false;
4356
+ let sessionRef;
4357
+ return {
4358
+ ...jsonLinesParser((record) => {
4359
+ const events = [];
4360
+ const recordSession = stringField(record, "sessionId") ?? stringField(record, "session_id");
4361
+ if (recordSession !== void 0) sessionRef = recordSession;
4362
+ if (!started && recordSession !== void 0) {
4363
+ started = true;
4364
+ events.push({
4365
+ type: "session.started",
4366
+ sessionRef: recordSession,
4367
+ raw: record
4368
+ });
4369
+ }
4370
+ if (stringField(record, "type") === "result") {
4371
+ const usage = recordField(record, "usage");
4372
+ if (usage !== void 0) events.push({
4373
+ type: "usage",
4374
+ cumulative: true,
4375
+ usage: usageOf({
4376
+ input: numberField(usage, "input_tokens"),
4377
+ output: numberField(usage, "output_tokens"),
4378
+ cachedTokens: numberField(usage, "cache_read_input_tokens") || void 0,
4379
+ costUSD: numberField(record, "total_cost_usd")
4380
+ }),
4381
+ raw: record
4382
+ });
4383
+ const result = stringField(record, "result") ?? "";
4384
+ if (record.is_error === true) events.push({
4385
+ type: "error",
4386
+ message: result || "Reasonix run failed",
4387
+ fatal: true,
4388
+ raw: record
4389
+ });
4390
+ else if (result.length > 0) events.push({
4391
+ type: "message",
4392
+ text: result,
4393
+ delta: false,
4394
+ final: true,
4395
+ raw: record
4396
+ });
4397
+ return events;
4398
+ }
4399
+ const tool = asRecord(record.tool);
4400
+ switch (stringField(record, "kind")) {
4401
+ case "text": {
4402
+ const text = stringField(record, "text") ?? "";
4403
+ if (text.length > 0) events.push({
4404
+ type: "message",
4405
+ text,
4406
+ delta: true,
4407
+ raw: record
4408
+ });
4409
+ break;
4410
+ }
4411
+ case "message":
4412
+ events.push({
4413
+ type: "message",
4414
+ text: stringField(record, "text") ?? "",
4415
+ delta: false,
4416
+ raw: record
4417
+ });
4418
+ break;
4419
+ case "tool_dispatch":
4420
+ if (tool !== void 0 && tool.partial !== true) events.push({
4421
+ type: "tool.call",
4422
+ id: stringField(tool, "id") ?? "",
4423
+ name: stringField(tool, "name") ?? "",
4424
+ input: parseArguments(tool.args),
4425
+ raw: record
4426
+ });
4427
+ break;
4428
+ case "tool_result": {
4429
+ const error = stringField(tool, "err") ?? "";
4430
+ events.push({
4431
+ type: "tool.result",
4432
+ id: stringField(tool, "id") ?? "",
4433
+ output: error.length > 0 ? error : stringField(tool, "output") ?? "",
4434
+ isError: error.length > 0,
4435
+ raw: record
4436
+ });
4437
+ break;
4438
+ }
4439
+ case "usage": {
4440
+ const usage = callUsage(recordField(record, "usage"));
4441
+ if (usage !== void 0) events.push({
4442
+ type: "usage",
4443
+ usage,
4444
+ cumulative: false,
4445
+ raw: record
4446
+ });
4447
+ break;
4448
+ }
4449
+ case "turn_done": if (stringField(record, "status") === "failed") events.push({
4450
+ type: "error",
4451
+ message: stringField(record, "err") ?? "Reasonix turn failed",
4452
+ fatal: true,
4453
+ raw: record
4454
+ });
4455
+ }
4456
+ return events;
4457
+ }),
4458
+ sessionRef: () => sessionRef
4459
+ };
4460
+ }
4461
+
4462
+ //#endregion
4463
+ //#region ../../packages/ai-cli/src/cli.ts
4464
+ /** Every built-in adapter, keyed by its factory name. */
4465
+ const cliAdapters = {
4466
+ claudeCode: claudeCodeAdapter,
4467
+ codex: codexAdapter,
4468
+ antigravity: antigravityAdapter,
4469
+ qwenCode: qwenCodeAdapter,
4470
+ kimi: kimiAdapter,
4471
+ cursor: cursorAdapter,
4472
+ copilot: copilotAdapter,
4473
+ grok: grokAdapter,
4474
+ opencode: opencodeAdapter,
4475
+ amp: ampAdapter,
4476
+ droid: droidAdapter,
4477
+ mistralVibe: mistralVibeAdapter,
4478
+ aider: aiderAdapter,
4479
+ reasonix: reasonixAdapter
4480
+ };
4481
+ const factories = Object.fromEntries(Object.entries(cliAdapters).map(([vendor, adapter]) => [vendor, (options) => createCliAgent(adapter, options)]));
4482
+ /**
4483
+ * The CLI-agent namespace: one factory per vendor, `custom()` for your own
4484
+ * adapter, `model()` to hand a CLI agent to `ai.agent`. Also available as
4485
+ * `ai.cli` once the package is imported.
4486
+ *
4487
+ * @example
4488
+ * import { cli } from "@warlock.js/ai-cli";
4489
+ *
4490
+ * const claude = cli.claudeCode({ model: "sonnet", cwd: "/srv/repo" });
4491
+ * const session = await claude.run("Fix the failing test");
4492
+ * for await (const event of session.events) console.log(event.type);
4493
+ * const { text, usage, sessionRef } = await session.done();
4494
+ */
4495
+ const cli = {
4496
+ ...factories,
4497
+ custom: (adapter, options) => createCliAgent(adapter, options),
4498
+ model: cliModel,
4499
+ adapters: cliAdapters
4500
+ };
4501
+ _warlock_js_ai.ai.cli = cli;
4502
+
4503
+ //#endregion
4504
+ exports.CliError = CliError;
4505
+ exports.aiderAdapter = aiderAdapter;
4506
+ exports.ampAdapter = ampAdapter;
4507
+ exports.anthropicUsage = anthropicUsage;
4508
+ exports.antigravityAdapter = antigravityAdapter;
4509
+ exports.argvPrompt = argvPrompt;
4510
+ exports.asArray = asArray;
4511
+ exports.asRecord = asRecord;
4512
+ exports.buildInvocation = buildInvocation;
4513
+ exports.claudeCodeAdapter = claudeCodeAdapter;
4514
+ exports.cli = cli;
4515
+ exports.cliAdapters = cliAdapters;
4516
+ exports.cliModel = cliModel;
4517
+ exports.codexAdapter = codexAdapter;
4518
+ exports.copilotAdapter = copilotAdapter;
4519
+ exports.createAiderParser = createAiderParser;
4520
+ exports.createAntigravityParser = createAntigravityParser;
4521
+ exports.createClaudeStreamParser = createClaudeStreamParser;
4522
+ exports.createCliAgent = createCliAgent;
4523
+ exports.createCodexParser = createCodexParser;
4524
+ exports.createCopilotParser = createCopilotParser;
4525
+ exports.createCursorParser = createCursorParser;
4526
+ exports.createDroidParser = createDroidParser;
4527
+ exports.createGrokParser = createGrokParser;
4528
+ exports.createKimiParser = createKimiParser;
4529
+ exports.createOpencodeParser = createOpencodeParser;
4530
+ exports.createReasonixParser = createReasonixParser;
4531
+ exports.createVibeParser = createVibeParser;
4532
+ exports.currentHost = currentHost;
4533
+ exports.cursorAdapter = cursorAdapter;
4534
+ exports.dotenvFile = dotenvFile;
4535
+ exports.droidAdapter = droidAdapter;
4536
+ exports.findExecutable = findExecutable;
4537
+ exports.fitsCommandLine = fitsCommandLine;
4538
+ exports.grokAdapter = grokAdapter;
4539
+ exports.helpLists = helpLists;
4540
+ exports.jsonFile = jsonFile;
4541
+ exports.jsonLinesParser = jsonLinesParser;
4542
+ exports.kimiAdapter = kimiAdapter;
4543
+ exports.mapEffort = mapEffort;
4544
+ exports.mcpServersJson = mcpServersJson;
4545
+ exports.missingFlags = missingFlags;
4546
+ exports.mistralVibeAdapter = mistralVibeAdapter;
4547
+ exports.numberField = numberField;
4548
+ exports.opencodeAdapter = opencodeAdapter;
4549
+ exports.outputText = outputText;
4550
+ exports.parseAiderCount = parseAiderCount;
4551
+ exports.parseArguments = parseArguments;
4552
+ exports.parseCmdShim = parseCmdShim;
4553
+ exports.parseJsonLine = parseJsonLine;
4554
+ exports.parseVersion = parseVersion;
4555
+ exports.promptFile = promptFile;
4556
+ exports.quoteForCmd = quoteForCmd;
4557
+ exports.qwenCodeAdapter = qwenCodeAdapter;
4558
+ exports.reasonixAdapter = reasonixAdapter;
4559
+ exports.recordField = recordField;
4560
+ exports.renderPrompt = renderPrompt;
4561
+ exports.requirePrivateHome = requirePrivateHome;
4562
+ exports.resolveSpawnTarget = resolveSpawnTarget;
4563
+ exports.stringField = stringField;
4564
+ exports.toToml = toToml;
4565
+ exports.toUsage = toUsage;
4566
+ exports.unsupportedOption = unsupportedOption;
4567
+ exports.usageOf = usageOf;
4568
+ //# sourceMappingURL=index.cjs.map