@warlock.js/ai-cli 0.0.0-stage → 5.31.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +31 -0
- package/LICENSE +21 -0
- package/README.md +69 -2
- package/cjs/index.cjs +4576 -0
- package/cjs/index.cjs.map +1 -0
- package/esm/adapters/aider.d.mts +41 -0
- package/esm/adapters/aider.d.mts.map +1 -0
- package/esm/adapters/aider.mjs +294 -0
- package/esm/adapters/aider.mjs.map +1 -0
- package/esm/adapters/amp.d.mts +33 -0
- package/esm/adapters/amp.d.mts.map +1 -0
- package/esm/adapters/amp.mjs +119 -0
- package/esm/adapters/amp.mjs.map +1 -0
- package/esm/adapters/antigravity.d.mts +37 -0
- package/esm/adapters/antigravity.d.mts.map +1 -0
- package/esm/adapters/antigravity.mjs +235 -0
- package/esm/adapters/antigravity.mjs.map +1 -0
- package/esm/adapters/claude-code.d.mts +30 -0
- package/esm/adapters/claude-code.d.mts.map +1 -0
- package/esm/adapters/claude-code.mjs +121 -0
- package/esm/adapters/claude-code.mjs.map +1 -0
- package/esm/adapters/claude-stream.d.mts +32 -0
- package/esm/adapters/claude-stream.d.mts.map +1 -0
- package/esm/adapters/claude-stream.mjs +140 -0
- package/esm/adapters/claude-stream.mjs.map +1 -0
- package/esm/adapters/codex.d.mts +37 -0
- package/esm/adapters/codex.d.mts.map +1 -0
- package/esm/adapters/codex.mjs +320 -0
- package/esm/adapters/codex.mjs.map +1 -0
- package/esm/adapters/copilot.d.mts +35 -0
- package/esm/adapters/copilot.d.mts.map +1 -0
- package/esm/adapters/copilot.mjs +244 -0
- package/esm/adapters/copilot.mjs.map +1 -0
- package/esm/adapters/cursor.d.mts +35 -0
- package/esm/adapters/cursor.d.mts.map +1 -0
- package/esm/adapters/cursor.mjs +203 -0
- package/esm/adapters/cursor.mjs.map +1 -0
- package/esm/adapters/droid.d.mts +32 -0
- package/esm/adapters/droid.d.mts.map +1 -0
- package/esm/adapters/droid.mjs +183 -0
- package/esm/adapters/droid.mjs.map +1 -0
- package/esm/adapters/grok.d.mts +32 -0
- package/esm/adapters/grok.d.mts.map +1 -0
- package/esm/adapters/grok.mjs +199 -0
- package/esm/adapters/grok.mjs.map +1 -0
- package/esm/adapters/index.d.mts +16 -0
- package/esm/adapters/index.mjs +18 -0
- package/esm/adapters/kimi.d.mts +34 -0
- package/esm/adapters/kimi.d.mts.map +1 -0
- package/esm/adapters/kimi.mjs +162 -0
- package/esm/adapters/kimi.mjs.map +1 -0
- package/esm/adapters/mistral-vibe.d.mts +32 -0
- package/esm/adapters/mistral-vibe.d.mts.map +1 -0
- package/esm/adapters/mistral-vibe.mjs +252 -0
- package/esm/adapters/mistral-vibe.mjs.map +1 -0
- package/esm/adapters/opencode.d.mts +36 -0
- package/esm/adapters/opencode.d.mts.map +1 -0
- package/esm/adapters/opencode.mjs +219 -0
- package/esm/adapters/opencode.mjs.map +1 -0
- package/esm/adapters/qwen-code.d.mts +30 -0
- package/esm/adapters/qwen-code.d.mts.map +1 -0
- package/esm/adapters/qwen-code.mjs +94 -0
- package/esm/adapters/qwen-code.mjs.map +1 -0
- package/esm/adapters/reasonix.d.mts +31 -0
- package/esm/adapters/reasonix.d.mts.map +1 -0
- package/esm/adapters/reasonix.mjs +250 -0
- package/esm/adapters/reasonix.mjs.map +1 -0
- package/esm/adapters/shared.d.mts +78 -0
- package/esm/adapters/shared.d.mts.map +1 -0
- package/esm/adapters/shared.mjs +161 -0
- package/esm/adapters/shared.mjs.map +1 -0
- package/esm/cli-agent.d.mts +40 -0
- package/esm/cli-agent.d.mts.map +1 -0
- package/esm/cli-agent.mjs +207 -0
- package/esm/cli-agent.mjs.map +1 -0
- package/esm/cli.d.mts +53 -0
- package/esm/cli.d.mts.map +1 -0
- package/esm/cli.mjs +61 -0
- package/esm/cli.mjs.map +1 -0
- package/esm/contracts/adapter.contract.d.mts +154 -0
- package/esm/contracts/adapter.contract.d.mts.map +1 -0
- package/esm/contracts/events.type.d.mts +111 -0
- package/esm/contracts/events.type.d.mts.map +1 -0
- package/esm/contracts/index.d.mts +4 -0
- package/esm/contracts/mcp.type.d.mts +29 -0
- package/esm/contracts/mcp.type.d.mts.map +1 -0
- package/esm/contracts/options.type.d.mts +87 -0
- package/esm/contracts/options.type.d.mts.map +1 -0
- package/esm/errors.d.mts +46 -0
- package/esm/errors.d.mts.map +1 -0
- package/esm/errors.mjs +32 -0
- package/esm/errors.mjs.map +1 -0
- package/esm/index.d.mts +31 -0
- package/esm/index.mjs +28 -0
- package/esm/model/cli-model.d.mts +48 -0
- package/esm/model/cli-model.d.mts.map +1 -0
- package/esm/model/cli-model.mjs +133 -0
- package/esm/model/cli-model.mjs.map +1 -0
- package/esm/probe/probe.d.mts +18 -0
- package/esm/probe/probe.d.mts.map +1 -0
- package/esm/probe/probe.mjs +148 -0
- package/esm/probe/probe.mjs.map +1 -0
- package/esm/session/aggregate.mjs +86 -0
- package/esm/session/aggregate.mjs.map +1 -0
- package/esm/session/environment.mjs +71 -0
- package/esm/session/environment.mjs.map +1 -0
- package/esm/session/run-directory.mjs +74 -0
- package/esm/session/run-directory.mjs.map +1 -0
- package/esm/session/session.mjs +188 -0
- package/esm/session/session.mjs.map +1 -0
- package/esm/spawn/invocation.d.mts +35 -0
- package/esm/spawn/invocation.d.mts.map +1 -0
- package/esm/spawn/invocation.mjs +76 -0
- package/esm/spawn/invocation.mjs.map +1 -0
- package/esm/spawn/process-tree.mjs +57 -0
- package/esm/spawn/process-tree.mjs.map +1 -0
- package/esm/spawn/resolve-command.d.mts +73 -0
- package/esm/spawn/resolve-command.d.mts.map +1 -0
- package/esm/spawn/resolve-command.mjs +109 -0
- package/esm/spawn/resolve-command.mjs.map +1 -0
- package/esm/stream/event-log.mjs +52 -0
- package/esm/stream/event-log.mjs.map +1 -0
- package/esm/stream/json-lines.d.mts +33 -0
- package/esm/stream/json-lines.d.mts.map +1 -0
- package/esm/stream/json-lines.mjs +65 -0
- package/esm/stream/json-lines.mjs.map +1 -0
- package/esm/stream/line-splitter.mjs +53 -0
- package/esm/stream/line-splitter.mjs.map +1 -0
- package/esm/utils/toml.d.mts +22 -0
- package/esm/utils/toml.d.mts.map +1 -0
- package/esm/utils/toml.mjs +53 -0
- package/esm/utils/toml.mjs.map +1 -0
- package/llms-full.txt +373 -0
- package/llms.txt +10 -0
- package/package.json +39 -4
- package/skills/README.md +13 -0
- package/skills/index.md +22 -0
- package/skills/run-cli-agents/SKILL.md +186 -0
- package/skills/write-a-cli-adapter/SKILL.md +173 -0
package/cjs/index.cjs
ADDED
|
@@ -0,0 +1,4576 @@
|
|
|
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 ? __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 = 120 * 1024;
|
|
262
|
+
/** Conservative total argv budget on POSIX (ARG_MAX is shared with the environment). */
|
|
263
|
+
const POSIX_TOTAL_LIMIT = 512 * 1024;
|
|
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 = 1024 * 1024;
|
|
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 = 32 * 1024 * 1024;
|
|
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":
|
|
761
|
+
if (event.fatal && this.fatalError === void 0) this.fatalError = event.message;
|
|
762
|
+
break;
|
|
763
|
+
}
|
|
764
|
+
}
|
|
765
|
+
/** Close the stream: deltas never followed by a complete message count as one. */
|
|
766
|
+
finish() {
|
|
767
|
+
this.flushDeltas();
|
|
768
|
+
}
|
|
769
|
+
get text() {
|
|
770
|
+
if (this.finalText !== void 0) return this.finalText;
|
|
771
|
+
return this.deltaBuffer.length > 0 ? this.deltaBuffer : this.lastCompleteText ?? "";
|
|
772
|
+
}
|
|
773
|
+
flushDeltas() {
|
|
774
|
+
if (this.deltaBuffer.trim().length > 0) {
|
|
775
|
+
this.messages.push(this.deltaBuffer);
|
|
776
|
+
this.lastCompleteText = this.deltaBuffer;
|
|
777
|
+
}
|
|
778
|
+
this.deltaBuffer = "";
|
|
779
|
+
}
|
|
780
|
+
};
|
|
781
|
+
|
|
782
|
+
//#endregion
|
|
783
|
+
//#region ../../packages/ai-cli/src/session/session.ts
|
|
784
|
+
/**
|
|
785
|
+
* Spawn the CLI and expose it as a {@link CliSession}: stdout lines go
|
|
786
|
+
* through the adapter's parser into a replayable event log, and the
|
|
787
|
+
* process's end becomes the `session.ended` event and the `done()` result.
|
|
788
|
+
*/
|
|
789
|
+
function startSession(input) {
|
|
790
|
+
const startedAt = Date.now();
|
|
791
|
+
const log = new EventLog();
|
|
792
|
+
const aggregator = new ResultAggregator(input.resume);
|
|
793
|
+
const controller = new AbortController();
|
|
794
|
+
let stderr = "";
|
|
795
|
+
let timedOut = false;
|
|
796
|
+
let cancelled = false;
|
|
797
|
+
let closed = false;
|
|
798
|
+
let spawnError;
|
|
799
|
+
const emit = (events) => {
|
|
800
|
+
for (const event of events) {
|
|
801
|
+
aggregator.add(event);
|
|
802
|
+
log.push(event);
|
|
803
|
+
}
|
|
804
|
+
};
|
|
805
|
+
const parse = (step) => {
|
|
806
|
+
try {
|
|
807
|
+
emit(step());
|
|
808
|
+
} catch (error) {
|
|
809
|
+
emit([{
|
|
810
|
+
type: "error",
|
|
811
|
+
message: `stream parser failed: ${error instanceof Error ? error.message : String(error)}`
|
|
812
|
+
}]);
|
|
813
|
+
}
|
|
814
|
+
};
|
|
815
|
+
const child = (0, node_child_process.spawn)(input.invocation.file, input.invocation.args, {
|
|
816
|
+
cwd: input.cwd,
|
|
817
|
+
env: input.env,
|
|
818
|
+
shell: false,
|
|
819
|
+
windowsHide: true,
|
|
820
|
+
windowsVerbatimArguments: input.invocation.windowsVerbatimArguments,
|
|
821
|
+
detached: input.platform !== "win32",
|
|
822
|
+
stdio: [
|
|
823
|
+
"pipe",
|
|
824
|
+
"pipe",
|
|
825
|
+
"pipe"
|
|
826
|
+
]
|
|
827
|
+
});
|
|
828
|
+
let cancelPendingKill;
|
|
829
|
+
const kill = () => {
|
|
830
|
+
cancelPendingKill = killProcessTree(child, input.platform, input.killGraceMs);
|
|
831
|
+
};
|
|
832
|
+
const onAbort = () => {
|
|
833
|
+
if (closed || timedOut || cancelled) return;
|
|
834
|
+
cancelled = true;
|
|
835
|
+
kill();
|
|
836
|
+
};
|
|
837
|
+
input.signal?.addEventListener("abort", onAbort, { once: true });
|
|
838
|
+
controller.signal.addEventListener("abort", onAbort, { once: true });
|
|
839
|
+
if (input.signal?.aborted) onAbort();
|
|
840
|
+
const timer = input.timeoutMs !== void 0 && input.timeoutMs > 0 ? setTimeout(() => {
|
|
841
|
+
if (cancelled) return;
|
|
842
|
+
timedOut = true;
|
|
843
|
+
kill();
|
|
844
|
+
}, input.timeoutMs) : void 0;
|
|
845
|
+
const splitter = createLineSplitter((line) => parse(() => input.parser.line(line)), () => emit([{
|
|
846
|
+
type: "error",
|
|
847
|
+
message: "dropped an output line longer than 32 MiB"
|
|
848
|
+
}]));
|
|
849
|
+
child.stdout?.on("data", (chunk) => splitter.write(chunk));
|
|
850
|
+
child.stderr?.on("data", (chunk) => {
|
|
851
|
+
stderr = (stderr + chunk.toString("utf8")).slice(-65536);
|
|
852
|
+
});
|
|
853
|
+
child.stdin?.on("error", () => void 0);
|
|
854
|
+
child.stdin?.end(input.stdin ?? "");
|
|
855
|
+
const done = new Promise((resolve) => {
|
|
856
|
+
let settled = false;
|
|
857
|
+
const settle = async (exitCode, signal) => {
|
|
858
|
+
if (settled) return;
|
|
859
|
+
settled = true;
|
|
860
|
+
if (timer !== void 0) clearTimeout(timer);
|
|
861
|
+
input.signal?.removeEventListener("abort", onAbort);
|
|
862
|
+
controller.signal.removeEventListener("abort", onAbort);
|
|
863
|
+
splitter.end();
|
|
864
|
+
parse(() => input.parser.end());
|
|
865
|
+
aggregator.finish();
|
|
866
|
+
const lateRef = input.parser.sessionRef?.();
|
|
867
|
+
if (lateRef !== void 0 && lateRef.length > 0) aggregator.sessionRef = lateRef;
|
|
868
|
+
const status = cancelled ? "cancelled" : timedOut ? "timeout" : spawnError === void 0 && exitCode === 0 && aggregator.fatalError === void 0 ? "completed" : "failed";
|
|
869
|
+
let error;
|
|
870
|
+
if (status === "failed") {
|
|
871
|
+
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)}` : ""));
|
|
872
|
+
if (aggregator.fatalError === void 0) emit([{
|
|
873
|
+
type: "error",
|
|
874
|
+
message: error,
|
|
875
|
+
fatal: true
|
|
876
|
+
}]);
|
|
877
|
+
} else if (status === "timeout") error = `${input.adapter} timed out after ${input.timeoutMs} ms`;
|
|
878
|
+
else if (status === "cancelled") error = `${input.adapter} run was cancelled`;
|
|
879
|
+
const durationMs = Date.now() - startedAt;
|
|
880
|
+
log.push({
|
|
881
|
+
type: "session.ended",
|
|
882
|
+
status,
|
|
883
|
+
exitCode,
|
|
884
|
+
signal,
|
|
885
|
+
sessionRef: aggregator.sessionRef,
|
|
886
|
+
durationMs
|
|
887
|
+
});
|
|
888
|
+
log.close();
|
|
889
|
+
try {
|
|
890
|
+
await input.cleanup();
|
|
891
|
+
} catch {}
|
|
892
|
+
resolve({
|
|
893
|
+
status,
|
|
894
|
+
text: aggregator.text,
|
|
895
|
+
messages: [...aggregator.messages],
|
|
896
|
+
usage: aggregator.usage,
|
|
897
|
+
sessionRef: aggregator.sessionRef,
|
|
898
|
+
exitCode,
|
|
899
|
+
signal,
|
|
900
|
+
error,
|
|
901
|
+
stderr,
|
|
902
|
+
durationMs
|
|
903
|
+
});
|
|
904
|
+
};
|
|
905
|
+
child.on("error", (error) => {
|
|
906
|
+
if (child.pid === void 0) {
|
|
907
|
+
closed = true;
|
|
908
|
+
spawnError = error;
|
|
909
|
+
settle(null, null);
|
|
910
|
+
}
|
|
911
|
+
});
|
|
912
|
+
child.on("close", (code, signal) => {
|
|
913
|
+
closed = true;
|
|
914
|
+
cancelPendingKill?.();
|
|
915
|
+
settle(code, signal);
|
|
916
|
+
});
|
|
917
|
+
});
|
|
918
|
+
return {
|
|
919
|
+
adapter: input.adapter,
|
|
920
|
+
pid: child.pid,
|
|
921
|
+
events: log,
|
|
922
|
+
done: () => done,
|
|
923
|
+
cancel: () => controller.abort()
|
|
924
|
+
};
|
|
925
|
+
}
|
|
926
|
+
/**
|
|
927
|
+
* A session for a run whose signal was already aborted: nothing is spawned,
|
|
928
|
+
* the stream is a single `session.ended`, and `done()` resolves `cancelled`.
|
|
929
|
+
*/
|
|
930
|
+
function cancelledSession(adapter, resume) {
|
|
931
|
+
const log = new EventLog();
|
|
932
|
+
const error = `${adapter} run was cancelled before it started`;
|
|
933
|
+
log.push({
|
|
934
|
+
type: "session.ended",
|
|
935
|
+
status: "cancelled",
|
|
936
|
+
exitCode: null,
|
|
937
|
+
signal: null,
|
|
938
|
+
sessionRef: resume,
|
|
939
|
+
durationMs: 0
|
|
940
|
+
});
|
|
941
|
+
log.close();
|
|
942
|
+
const result = {
|
|
943
|
+
status: "cancelled",
|
|
944
|
+
text: "",
|
|
945
|
+
messages: [],
|
|
946
|
+
sessionRef: resume,
|
|
947
|
+
exitCode: null,
|
|
948
|
+
signal: null,
|
|
949
|
+
error,
|
|
950
|
+
stderr: "",
|
|
951
|
+
durationMs: 0
|
|
952
|
+
};
|
|
953
|
+
return {
|
|
954
|
+
adapter,
|
|
955
|
+
pid: void 0,
|
|
956
|
+
events: log,
|
|
957
|
+
done: () => Promise.resolve(result),
|
|
958
|
+
cancel: () => void 0
|
|
959
|
+
};
|
|
960
|
+
}
|
|
961
|
+
|
|
962
|
+
//#endregion
|
|
963
|
+
//#region ../../packages/ai-cli/src/spawn/resolve-command.ts
|
|
964
|
+
/** Windows' default `PATHEXT` when the variable is unset. */
|
|
965
|
+
const DEFAULT_PATHEXT = ".COM;.EXE;.BAT;.CMD";
|
|
966
|
+
/** Case-insensitive read on Windows (`Path` vs `PATH`), exact elsewhere. */
|
|
967
|
+
function readEnv(env, name, platform) {
|
|
968
|
+
if (platform !== "win32") return env[name];
|
|
969
|
+
const wanted = name.toUpperCase();
|
|
970
|
+
for (const [key, value] of Object.entries(env)) if (key.toUpperCase() === wanted && value !== void 0) return value;
|
|
971
|
+
}
|
|
972
|
+
function pathApi(platform) {
|
|
973
|
+
return platform === "win32" ? node_path.default.win32 : node_path.default.posix;
|
|
974
|
+
}
|
|
975
|
+
/**
|
|
976
|
+
* Find an executable the way the platform's shell would.
|
|
977
|
+
*
|
|
978
|
+
* POSIX walks `PATH`. Windows walks `PATH` trying each `PATHEXT` extension
|
|
979
|
+
* (`claude` → `claude.cmd`), and also accepts a name that already carries
|
|
980
|
+
* one of those extensions. A command containing a path separator is
|
|
981
|
+
* checked directly instead of searched.
|
|
982
|
+
*
|
|
983
|
+
* @example
|
|
984
|
+
* findExecutable("claude", { platform: "win32", env: { PATH: "C:\\npm", PATHEXT: ".EXE;.CMD" }, ... });
|
|
985
|
+
* // → "C:\\npm\\claude.cmd"
|
|
986
|
+
*/
|
|
987
|
+
function findExecutable(command, lookup) {
|
|
988
|
+
const api = pathApi(lookup.platform);
|
|
989
|
+
const isWindows = lookup.platform === "win32";
|
|
990
|
+
const extensions = isWindows ? (readEnv(lookup.env, "PATHEXT", "win32") ?? DEFAULT_PATHEXT).split(";").map((extension) => extension.trim()).filter((extension) => extension.length > 0) : [];
|
|
991
|
+
const candidatesFor = (base) => {
|
|
992
|
+
if (!isWindows) return [base];
|
|
993
|
+
const hasKnownExtension = extensions.some((extension) => base.toUpperCase().endsWith(extension.toUpperCase()));
|
|
994
|
+
const withExtensions = extensions.map((extension) => base + extension.toLowerCase());
|
|
995
|
+
return hasKnownExtension ? [base, ...withExtensions] : withExtensions;
|
|
996
|
+
};
|
|
997
|
+
if ((isWindows ? /[\\/]/.test(command) : command.includes("/")) || api.isAbsolute(command)) return candidatesFor(api.resolve(command)).find((candidate) => lookup.isFile(candidate));
|
|
998
|
+
const directories = (readEnv(lookup.env, "PATH", lookup.platform) ?? "").split(isWindows ? ";" : ":").map((directory) => directory.trim().replace(/^"(.*)"$/, "$1")).filter((directory) => directory.length > 0);
|
|
999
|
+
for (const directory of directories) for (const candidate of candidatesFor(api.join(directory, command))) if (lookup.isFile(candidate)) return candidate;
|
|
1000
|
+
}
|
|
1001
|
+
/**
|
|
1002
|
+
* Read the target of an npm (`cmd-shim`) or pnpm `.cmd` shim.
|
|
1003
|
+
*
|
|
1004
|
+
* Both generators end the shim with a line that runs the target with `%*`;
|
|
1005
|
+
* the target is a quoted path relative to the shim's own directory
|
|
1006
|
+
* (`"%dp0%\node_modules\pkg\cli.js"` for npm, `"%~dp0\..\pkg\cli.js"` for
|
|
1007
|
+
* pnpm). A JavaScript target is run by Node; an `.exe`/`.com` target is run
|
|
1008
|
+
* directly. Anything else returns `undefined` and the caller falls back to
|
|
1009
|
+
* `cmd.exe`.
|
|
1010
|
+
*/
|
|
1011
|
+
function parseCmdShim(content, shimPath) {
|
|
1012
|
+
const directory = node_path.default.win32.dirname(shimPath);
|
|
1013
|
+
const line = content.split(/\r?\n/).filter((line) => line.includes("%*")).at(-1);
|
|
1014
|
+
if (line === void 0) return;
|
|
1015
|
+
const raw = [...line.matchAll(/"(%~?dp0%?[\\/][^"]+)"/gi)].map((match) => match[1] ?? "").filter((target) => !/[\\/]node(\.exe)?$/i.test(target)).at(-1);
|
|
1016
|
+
if (raw === void 0) return;
|
|
1017
|
+
const relative = raw.replace(/^%~?dp0%?[\\/]/i, "");
|
|
1018
|
+
const target = node_path.default.win32.resolve(directory, relative);
|
|
1019
|
+
if (/\.(c|m)?js$/i.test(target)) return {
|
|
1020
|
+
type: "node",
|
|
1021
|
+
script: target
|
|
1022
|
+
};
|
|
1023
|
+
if (/\.(exe|com)$/i.test(target)) return {
|
|
1024
|
+
type: "native",
|
|
1025
|
+
file: target
|
|
1026
|
+
};
|
|
1027
|
+
}
|
|
1028
|
+
/**
|
|
1029
|
+
* Resolve a command to something `child_process.spawn` can start without a
|
|
1030
|
+
* shell. On Windows a `.cmd`/`.bat` npm shim is unwrapped to
|
|
1031
|
+
* `node <script>` (or its native `.exe`), so argv reaches the CLI intact
|
|
1032
|
+
* with no `cmd.exe` parsing in between; any other batch file falls back
|
|
1033
|
+
* to `cmd.exe`.
|
|
1034
|
+
*/
|
|
1035
|
+
function resolveSpawnTarget(command, lookup) {
|
|
1036
|
+
const executable = findExecutable(command, lookup);
|
|
1037
|
+
if (executable === void 0) return;
|
|
1038
|
+
if (lookup.platform !== "win32" || !/\.(cmd|bat)$/i.test(executable)) return {
|
|
1039
|
+
type: "direct",
|
|
1040
|
+
file: executable,
|
|
1041
|
+
prefixArgs: [],
|
|
1042
|
+
executable
|
|
1043
|
+
};
|
|
1044
|
+
const shim = parseCmdShim(lookup.readFile(executable) ?? "", executable);
|
|
1045
|
+
if (shim?.type === "node") {
|
|
1046
|
+
const bundledNode = node_path.default.win32.join(node_path.default.win32.dirname(executable), "node.exe");
|
|
1047
|
+
return {
|
|
1048
|
+
type: "direct",
|
|
1049
|
+
file: lookup.isFile(bundledNode) ? bundledNode : lookup.execPath,
|
|
1050
|
+
prefixArgs: [shim.script],
|
|
1051
|
+
executable
|
|
1052
|
+
};
|
|
1053
|
+
}
|
|
1054
|
+
if (shim?.type === "native" && lookup.isFile(shim.file)) return {
|
|
1055
|
+
type: "direct",
|
|
1056
|
+
file: shim.file,
|
|
1057
|
+
prefixArgs: [],
|
|
1058
|
+
executable
|
|
1059
|
+
};
|
|
1060
|
+
return {
|
|
1061
|
+
type: "cmd",
|
|
1062
|
+
file: executable,
|
|
1063
|
+
executable
|
|
1064
|
+
};
|
|
1065
|
+
}
|
|
1066
|
+
|
|
1067
|
+
//#endregion
|
|
1068
|
+
//#region ../../packages/ai-cli/src/cli-agent.ts
|
|
1069
|
+
function isRunnableFile(filePath, platform) {
|
|
1070
|
+
try {
|
|
1071
|
+
if (!(0, node_fs.statSync)(filePath).isFile()) return false;
|
|
1072
|
+
if (platform !== "win32") (0, node_fs.accessSync)(filePath, node_fs.constants.X_OK);
|
|
1073
|
+
return true;
|
|
1074
|
+
} catch {
|
|
1075
|
+
return false;
|
|
1076
|
+
}
|
|
1077
|
+
}
|
|
1078
|
+
/** The current machine as a {@link CliHost}. */
|
|
1079
|
+
function currentHost() {
|
|
1080
|
+
const platform = process.platform;
|
|
1081
|
+
return {
|
|
1082
|
+
platform,
|
|
1083
|
+
env: process.env,
|
|
1084
|
+
isFile: (filePath) => isRunnableFile(filePath, platform),
|
|
1085
|
+
readFile: (filePath) => {
|
|
1086
|
+
try {
|
|
1087
|
+
return (0, node_fs.readFileSync)(filePath, "utf8");
|
|
1088
|
+
} catch {
|
|
1089
|
+
return;
|
|
1090
|
+
}
|
|
1091
|
+
},
|
|
1092
|
+
execPath: process.execPath,
|
|
1093
|
+
comspec: readEnv(process.env, "ComSpec", platform) ?? "cmd.exe"
|
|
1094
|
+
};
|
|
1095
|
+
}
|
|
1096
|
+
function unsupported(adapter, message) {
|
|
1097
|
+
return new CliError(`${adapter.title}: ${message}`, {
|
|
1098
|
+
type: "unsupported-option",
|
|
1099
|
+
adapter: adapter.name
|
|
1100
|
+
});
|
|
1101
|
+
}
|
|
1102
|
+
/**
|
|
1103
|
+
* Create a CLI agent from an adapter. The vendor factories
|
|
1104
|
+
* (`cli.claudeCode(...)`, `cli.codex(...)`, …) call this with their
|
|
1105
|
+
* built-in adapter; call it directly with your own {@link CliAdapter}.
|
|
1106
|
+
*
|
|
1107
|
+
* @example
|
|
1108
|
+
* const agent = createCliAgent(myAdapter, { model: "fast", permission: "standard" });
|
|
1109
|
+
* const session = await agent.run("Summarize README.md", { cwd: "/srv/repo" });
|
|
1110
|
+
* console.log((await session.done()).text);
|
|
1111
|
+
*/
|
|
1112
|
+
function createCliAgent(adapter, options = {}, host = currentHost()) {
|
|
1113
|
+
const probes = /* @__PURE__ */ new Map();
|
|
1114
|
+
const command = options.command ?? adapter.command;
|
|
1115
|
+
const resolveTarget = () => {
|
|
1116
|
+
const target = resolveSpawnTarget(command, host);
|
|
1117
|
+
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\`.`, {
|
|
1118
|
+
type: "not-found",
|
|
1119
|
+
adapter: adapter.name
|
|
1120
|
+
});
|
|
1121
|
+
return target;
|
|
1122
|
+
};
|
|
1123
|
+
const probeEnvironment = () => buildEnvironment({
|
|
1124
|
+
source: host.env,
|
|
1125
|
+
inherit: options.inheritEnv ?? true,
|
|
1126
|
+
managed: [],
|
|
1127
|
+
user: options.env,
|
|
1128
|
+
platform: host.platform
|
|
1129
|
+
});
|
|
1130
|
+
const probeTarget = (target) => {
|
|
1131
|
+
const cached = probes.get(target.executable);
|
|
1132
|
+
if (cached !== void 0) return cached;
|
|
1133
|
+
const pending = probeCli({
|
|
1134
|
+
adapter,
|
|
1135
|
+
target,
|
|
1136
|
+
env: probeEnvironment(),
|
|
1137
|
+
cwd: node_path.default.resolve(options.cwd ?? process.cwd()),
|
|
1138
|
+
comspec: host.comspec,
|
|
1139
|
+
platform: host.platform
|
|
1140
|
+
});
|
|
1141
|
+
probes.set(target.executable, pending);
|
|
1142
|
+
pending.catch(() => probes.delete(target.executable));
|
|
1143
|
+
return pending;
|
|
1144
|
+
};
|
|
1145
|
+
const run = async (prompt, runOptions = {}) => {
|
|
1146
|
+
const merged = {
|
|
1147
|
+
...options,
|
|
1148
|
+
...runOptions,
|
|
1149
|
+
env: {
|
|
1150
|
+
...options.env,
|
|
1151
|
+
...runOptions.env
|
|
1152
|
+
},
|
|
1153
|
+
args: [...options.args ?? [], ...runOptions.args ?? []],
|
|
1154
|
+
mcp: runOptions.mcp ?? options.mcp
|
|
1155
|
+
};
|
|
1156
|
+
const permission = merged.permission ?? "standard";
|
|
1157
|
+
if (permission !== "standard" && permission !== "bypass") throw unsupported(adapter, `unknown permission "${String(permission)}" (use "standard" or "bypass").`);
|
|
1158
|
+
if (merged.effort !== void 0 && adapter.effort !== true) throw unsupported(adapter, "this CLI has no reasoning-effort control; drop `effort`.");
|
|
1159
|
+
const mcp = merged.mcp !== void 0 && Object.keys(merged.mcp).length > 0 ? merged.mcp : void 0;
|
|
1160
|
+
if (mcp !== void 0 && adapter.mcp !== true) throw unsupported(adapter, "this CLI cannot attach per-run MCP servers; drop `mcp`.");
|
|
1161
|
+
if (merged.apiKey !== void 0 && adapter.apiKeyEnv.length === 0) throw unsupported(adapter, "this CLI reads no API key from the environment; drop `apiKey`.");
|
|
1162
|
+
const home = options.home ?? "private";
|
|
1163
|
+
if (home !== "inherit" && adapter.homeEnv.length === 0) throw unsupported(adapter, "this CLI cannot relocate its config home; use `home: \"inherit\"`.");
|
|
1164
|
+
const cwd = node_path.default.resolve(merged.cwd ?? process.cwd());
|
|
1165
|
+
const target = resolveTarget();
|
|
1166
|
+
const probe = options.probe === false ? void 0 : await probeTarget(target);
|
|
1167
|
+
if (merged.signal?.aborted) return cancelledSession(adapter.name, merged.resume);
|
|
1168
|
+
const directories = await createRunDirectories(home, cwd);
|
|
1169
|
+
try {
|
|
1170
|
+
let chosen;
|
|
1171
|
+
const reasons = [];
|
|
1172
|
+
for (const transport of adapter.transports) {
|
|
1173
|
+
const context = {
|
|
1174
|
+
prompt,
|
|
1175
|
+
transport,
|
|
1176
|
+
cwd,
|
|
1177
|
+
model: merged.model,
|
|
1178
|
+
effort: merged.effort,
|
|
1179
|
+
permission,
|
|
1180
|
+
resume: merged.resume,
|
|
1181
|
+
mcp,
|
|
1182
|
+
apiKey: merged.apiKey,
|
|
1183
|
+
home: directories.home,
|
|
1184
|
+
homeIsPrivate: directories.homeIsPrivate,
|
|
1185
|
+
runDir: directories.runDir,
|
|
1186
|
+
platform: host.platform
|
|
1187
|
+
};
|
|
1188
|
+
const plan = adapter.plan(context);
|
|
1189
|
+
if (probe !== void 0) assertCapabilities(adapter, probe, plan.requires);
|
|
1190
|
+
const invocation = buildInvocation(target, [
|
|
1191
|
+
...plan.args,
|
|
1192
|
+
...merged.args ?? [],
|
|
1193
|
+
...plan.tail ?? []
|
|
1194
|
+
], host.comspec);
|
|
1195
|
+
if ("refused" in invocation) {
|
|
1196
|
+
reasons.push(`${transport}: ${invocation.refused}`);
|
|
1197
|
+
continue;
|
|
1198
|
+
}
|
|
1199
|
+
if (!fitsCommandLine(invocation, host.platform)) {
|
|
1200
|
+
reasons.push(`${transport}: the command line exceeds the platform limit`);
|
|
1201
|
+
continue;
|
|
1202
|
+
}
|
|
1203
|
+
chosen = {
|
|
1204
|
+
plan,
|
|
1205
|
+
invocation,
|
|
1206
|
+
context
|
|
1207
|
+
};
|
|
1208
|
+
break;
|
|
1209
|
+
}
|
|
1210
|
+
if (chosen === void 0) throw new CliError(`${adapter.title}: the prompt cannot be passed to "${target.executable}" (${reasons.join("; ")}).`, {
|
|
1211
|
+
type: "prompt-transport",
|
|
1212
|
+
adapter: adapter.name,
|
|
1213
|
+
executable: target.executable
|
|
1214
|
+
});
|
|
1215
|
+
await writeLaunchFiles(chosen.plan.files ?? [], directories);
|
|
1216
|
+
const apiKeyEnv = adapter.apiKeyEnv[0];
|
|
1217
|
+
const env = buildEnvironment({
|
|
1218
|
+
source: host.env,
|
|
1219
|
+
inherit: options.inheritEnv ?? true,
|
|
1220
|
+
managed: home === "inherit" ? [] : adapter.homeEnv,
|
|
1221
|
+
plan: host.platform === "win32" ? chosen.plan.env : {
|
|
1222
|
+
PWD: cwd,
|
|
1223
|
+
...chosen.plan.env
|
|
1224
|
+
},
|
|
1225
|
+
user: merged.env,
|
|
1226
|
+
apiKey: merged.apiKey !== void 0 && apiKeyEnv !== void 0 ? {
|
|
1227
|
+
name: apiKeyEnv,
|
|
1228
|
+
value: merged.apiKey
|
|
1229
|
+
} : void 0,
|
|
1230
|
+
platform: host.platform
|
|
1231
|
+
});
|
|
1232
|
+
return startSession({
|
|
1233
|
+
adapter: adapter.name,
|
|
1234
|
+
invocation: chosen.invocation,
|
|
1235
|
+
cwd,
|
|
1236
|
+
env,
|
|
1237
|
+
stdin: chosen.plan.stdin,
|
|
1238
|
+
parser: adapter.parser(chosen.context),
|
|
1239
|
+
signal: merged.signal,
|
|
1240
|
+
timeoutMs: merged.timeoutMs,
|
|
1241
|
+
killGraceMs: options.killGraceMs ?? 2e3,
|
|
1242
|
+
platform: host.platform,
|
|
1243
|
+
resume: merged.resume,
|
|
1244
|
+
cleanup: directories.dispose
|
|
1245
|
+
});
|
|
1246
|
+
} catch (error) {
|
|
1247
|
+
await directories.dispose();
|
|
1248
|
+
throw error;
|
|
1249
|
+
}
|
|
1250
|
+
};
|
|
1251
|
+
const agent = {
|
|
1252
|
+
adapter,
|
|
1253
|
+
options,
|
|
1254
|
+
run,
|
|
1255
|
+
probe: () => probeTarget(resolveTarget()),
|
|
1256
|
+
model: (config) => cliModel(agent, config)
|
|
1257
|
+
};
|
|
1258
|
+
return agent;
|
|
1259
|
+
}
|
|
1260
|
+
|
|
1261
|
+
//#endregion
|
|
1262
|
+
//#region ../../packages/ai-cli/src/stream/json-lines.ts
|
|
1263
|
+
/** `value` as a JSON object, or `undefined`. */
|
|
1264
|
+
function asRecord(value) {
|
|
1265
|
+
return typeof value === "object" && value !== null && !Array.isArray(value) ? value : void 0;
|
|
1266
|
+
}
|
|
1267
|
+
/** `value` as an array, or an empty array. */
|
|
1268
|
+
function asArray(value) {
|
|
1269
|
+
return Array.isArray(value) ? value : [];
|
|
1270
|
+
}
|
|
1271
|
+
/** A string field, or `undefined`. */
|
|
1272
|
+
function stringField(record, key) {
|
|
1273
|
+
const value = record?.[key];
|
|
1274
|
+
return typeof value === "string" ? value : void 0;
|
|
1275
|
+
}
|
|
1276
|
+
/** A finite number field, or `undefined`. */
|
|
1277
|
+
function numberField(record, key) {
|
|
1278
|
+
const value = record?.[key];
|
|
1279
|
+
return typeof value === "number" && Number.isFinite(value) ? value : void 0;
|
|
1280
|
+
}
|
|
1281
|
+
/** A nested object field, or `undefined`. */
|
|
1282
|
+
function recordField(record, key) {
|
|
1283
|
+
return asRecord(record?.[key]);
|
|
1284
|
+
}
|
|
1285
|
+
/** Tool output as text: strings verbatim, text-block arrays joined, anything else as JSON. */
|
|
1286
|
+
function outputText(value) {
|
|
1287
|
+
if (typeof value === "string") return value;
|
|
1288
|
+
if (value === void 0 || value === null) return "";
|
|
1289
|
+
if (Array.isArray(value)) return value.map((item) => {
|
|
1290
|
+
return stringField(asRecord(item), "text") ?? (typeof item === "string" ? item : JSON.stringify(item));
|
|
1291
|
+
}).join("\n");
|
|
1292
|
+
return JSON.stringify(value);
|
|
1293
|
+
}
|
|
1294
|
+
/** Parse one line as a JSON object; anything else (banners, blank lines) is `undefined`. */
|
|
1295
|
+
function parseJsonLine(line) {
|
|
1296
|
+
const trimmed = line.trim();
|
|
1297
|
+
if (!trimmed.startsWith("{")) return;
|
|
1298
|
+
try {
|
|
1299
|
+
return asRecord(JSON.parse(trimmed));
|
|
1300
|
+
} catch {
|
|
1301
|
+
return;
|
|
1302
|
+
}
|
|
1303
|
+
}
|
|
1304
|
+
/**
|
|
1305
|
+
* Build a parser for a JSONL/NDJSON stream. Non-JSON lines (progress
|
|
1306
|
+
* banners, warnings) are skipped; each JSON object goes to `onRecord`.
|
|
1307
|
+
* `onEnd` may flush buffered state when stdout closes.
|
|
1308
|
+
*
|
|
1309
|
+
* @example
|
|
1310
|
+
* const parser = jsonLinesParser((record) =>
|
|
1311
|
+
* record.type === "text" ? [{ type: "message", text: String(record.text), delta: false }] : [],
|
|
1312
|
+
* );
|
|
1313
|
+
*/
|
|
1314
|
+
function jsonLinesParser(onRecord, onEnd = () => []) {
|
|
1315
|
+
return {
|
|
1316
|
+
line(line) {
|
|
1317
|
+
const record = parseJsonLine(line);
|
|
1318
|
+
return record === void 0 ? [] : onRecord(record);
|
|
1319
|
+
},
|
|
1320
|
+
end: onEnd
|
|
1321
|
+
};
|
|
1322
|
+
}
|
|
1323
|
+
|
|
1324
|
+
//#endregion
|
|
1325
|
+
//#region ../../packages/ai-cli/src/utils/toml.ts
|
|
1326
|
+
function isTable(value) {
|
|
1327
|
+
return typeof value === "object" && !Array.isArray(value);
|
|
1328
|
+
}
|
|
1329
|
+
/** A bare key when possible, else a quoted one. */
|
|
1330
|
+
function tomlKey(key) {
|
|
1331
|
+
return /^[A-Za-z0-9_-]+$/.test(key) ? key : JSON.stringify(key);
|
|
1332
|
+
}
|
|
1333
|
+
/** A TOML scalar or inline value. JSON string escapes are valid TOML basic-string escapes. */
|
|
1334
|
+
function inlineValue(value) {
|
|
1335
|
+
if (typeof value === "string") return JSON.stringify(value);
|
|
1336
|
+
if (typeof value === "number") {
|
|
1337
|
+
if (!Number.isFinite(value)) throw new Error("TOML cannot encode a non-finite number");
|
|
1338
|
+
return String(value);
|
|
1339
|
+
}
|
|
1340
|
+
if (typeof value === "boolean") return value ? "true" : "false";
|
|
1341
|
+
if (Array.isArray(value)) return `[${value.map(inlineValue).join(", ")}]`;
|
|
1342
|
+
return `{ ${Object.entries(value).filter((entry) => entry[1] !== void 0).map(([key, item]) => `${tomlKey(key)} = ${inlineValue(item)}`).join(", ")} }`;
|
|
1343
|
+
}
|
|
1344
|
+
/**
|
|
1345
|
+
* Serialize a table to TOML: scalars and arrays first, then each nested
|
|
1346
|
+
* table as a `[dotted.header]` section. Enough for the CLI config files
|
|
1347
|
+
* this package writes (Codex and Mistral Vibe MCP sections).
|
|
1348
|
+
*
|
|
1349
|
+
* @example
|
|
1350
|
+
* toToml({ mcp_servers: { docs: { command: "npx", args: ["-y", "docs-mcp"] } } });
|
|
1351
|
+
* // [mcp_servers.docs]
|
|
1352
|
+
* // command = "npx"
|
|
1353
|
+
* // args = ["-y", "docs-mcp"]
|
|
1354
|
+
*/
|
|
1355
|
+
function toToml(table, prefix = []) {
|
|
1356
|
+
const lines = [];
|
|
1357
|
+
const nested = [];
|
|
1358
|
+
for (const [key, value] of Object.entries(table)) {
|
|
1359
|
+
if (value === void 0) continue;
|
|
1360
|
+
if (isTable(value)) {
|
|
1361
|
+
nested.push([key, value]);
|
|
1362
|
+
continue;
|
|
1363
|
+
}
|
|
1364
|
+
lines.push(`${tomlKey(key)} = ${inlineValue(value)}`);
|
|
1365
|
+
}
|
|
1366
|
+
const sections = [];
|
|
1367
|
+
if (lines.length > 0) sections.push((prefix.length > 0 ? `[${prefix.map(tomlKey).join(".")}]\n` : "") + lines.join("\n"));
|
|
1368
|
+
for (const [key, value] of nested) {
|
|
1369
|
+
const section = toToml(value, [...prefix, key]);
|
|
1370
|
+
if (section.length > 0) sections.push(section.trimEnd());
|
|
1371
|
+
}
|
|
1372
|
+
return sections.length > 0 ? `${sections.join("\n\n")}\n` : "";
|
|
1373
|
+
}
|
|
1374
|
+
|
|
1375
|
+
//#endregion
|
|
1376
|
+
//#region ../../packages/ai-cli/src/adapters/shared.ts
|
|
1377
|
+
/**
|
|
1378
|
+
* The prompt as an argv value. A prompt starting with `-` would be read as a
|
|
1379
|
+
* flag by the CLI's argument parser, so it gets one leading space (harmless
|
|
1380
|
+
* to the model, and no parser treats it as an option).
|
|
1381
|
+
*/
|
|
1382
|
+
function argvPrompt(prompt) {
|
|
1383
|
+
return prompt.startsWith("-") ? ` ${prompt}` : prompt;
|
|
1384
|
+
}
|
|
1385
|
+
/** The prompt written to `prompt.txt` in the run directory, for `transport: "file"`. */
|
|
1386
|
+
function promptFile(context) {
|
|
1387
|
+
return {
|
|
1388
|
+
path: node_path.default.join(context.runDir, "prompt.txt"),
|
|
1389
|
+
content: context.prompt
|
|
1390
|
+
};
|
|
1391
|
+
}
|
|
1392
|
+
/**
|
|
1393
|
+
* Fail closed when an option needs a file inside the CLI's config home but
|
|
1394
|
+
* the home is not private to this run (writing it into a caller-owned or
|
|
1395
|
+
* inherited home would leak tokens into it and race concurrent runs).
|
|
1396
|
+
*/
|
|
1397
|
+
function requirePrivateHome(context, adapter, option) {
|
|
1398
|
+
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}.`, {
|
|
1399
|
+
type: "unsupported-option",
|
|
1400
|
+
adapter: adapter.name
|
|
1401
|
+
});
|
|
1402
|
+
return context.home;
|
|
1403
|
+
}
|
|
1404
|
+
/** A `.env` file body. Values are written verbatim; newlines are refused. */
|
|
1405
|
+
function dotenvFile(values) {
|
|
1406
|
+
return Object.entries(values).map(([name, value]) => {
|
|
1407
|
+
if (/[\r\n]/.test(value)) throw new Error(`${name} contains a newline and cannot be written to a .env file`);
|
|
1408
|
+
return `${name}=${value}`;
|
|
1409
|
+
}).join("\n").concat("\n");
|
|
1410
|
+
}
|
|
1411
|
+
/** The framework effort scale, lowest first. */
|
|
1412
|
+
const EFFORT_ORDER = [
|
|
1413
|
+
"none",
|
|
1414
|
+
"minimal",
|
|
1415
|
+
"low",
|
|
1416
|
+
"medium",
|
|
1417
|
+
"high",
|
|
1418
|
+
"xhigh",
|
|
1419
|
+
"max"
|
|
1420
|
+
];
|
|
1421
|
+
/**
|
|
1422
|
+
* Map a framework effort to a vendor's levels, following the framework
|
|
1423
|
+
* rule: an exact level wins; otherwise the nearest higher level the vendor
|
|
1424
|
+
* has (so `minimal` becomes `low`); a level above the vendor's highest is
|
|
1425
|
+
* clamped to it.
|
|
1426
|
+
*
|
|
1427
|
+
* @example
|
|
1428
|
+
* mapEffort("xhigh", { low: "low", medium: "medium", high: "high" }); // "high"
|
|
1429
|
+
* mapEffort("minimal", { low: "low", medium: "medium", high: "high" }); // "low"
|
|
1430
|
+
*/
|
|
1431
|
+
function mapEffort(effort, levels) {
|
|
1432
|
+
const exact = levels[effort];
|
|
1433
|
+
if (exact !== void 0) return exact;
|
|
1434
|
+
const requested = EFFORT_ORDER.indexOf(effort);
|
|
1435
|
+
const supported = EFFORT_ORDER.filter((level) => levels[level] !== void 0);
|
|
1436
|
+
const chosen = supported.find((level) => EFFORT_ORDER.indexOf(level) > requested) ?? supported.at(-1);
|
|
1437
|
+
const value = chosen === void 0 ? void 0 : levels[chosen];
|
|
1438
|
+
if (value === void 0) throw new Error("mapEffort needs at least one vendor level");
|
|
1439
|
+
return value;
|
|
1440
|
+
}
|
|
1441
|
+
/**
|
|
1442
|
+
* The common `mcpServers` JSON shape (Claude Code, Cursor, Kimi, Droid,
|
|
1443
|
+
* Copilot, Amp, Qwen Code …): stdio servers as `command`/`args`/`env`,
|
|
1444
|
+
* remote servers as `url`/`headers`. `typeField` adds the `type` key the
|
|
1445
|
+
* vendor expects (`"stdio"`, `"http"`, `"sse"`), or a renamed remote
|
|
1446
|
+
* transport (`"streamable-http"`).
|
|
1447
|
+
*/
|
|
1448
|
+
function mcpServersJson(servers, options = {}) {
|
|
1449
|
+
const rendered = {};
|
|
1450
|
+
for (const [name, server] of Object.entries(servers)) {
|
|
1451
|
+
if (server.type === "stdio") {
|
|
1452
|
+
rendered[name] = {
|
|
1453
|
+
...options.typeField ? { type: "stdio" } : {},
|
|
1454
|
+
command: server.command,
|
|
1455
|
+
args: server.args ?? [],
|
|
1456
|
+
...server.env !== void 0 ? { env: server.env } : {},
|
|
1457
|
+
...options.extra
|
|
1458
|
+
};
|
|
1459
|
+
continue;
|
|
1460
|
+
}
|
|
1461
|
+
const urlKey = server.type === "http" ? options.httpUrlKey ?? "url" : "url";
|
|
1462
|
+
rendered[name] = {
|
|
1463
|
+
...options.typeField ? { type: server.type === "http" ? options.httpType ?? "http" : "sse" } : {},
|
|
1464
|
+
[urlKey]: server.url,
|
|
1465
|
+
...server.headers !== void 0 ? { headers: server.headers } : {},
|
|
1466
|
+
...options.extra
|
|
1467
|
+
};
|
|
1468
|
+
}
|
|
1469
|
+
return rendered;
|
|
1470
|
+
}
|
|
1471
|
+
/** JSON for a config file, pretty-printed with a trailing newline. */
|
|
1472
|
+
function jsonFile(value) {
|
|
1473
|
+
return `${JSON.stringify(value, null, 2)}\n`;
|
|
1474
|
+
}
|
|
1475
|
+
/** Build a `CliUsage`, computing `total` when the vendor omits it. */
|
|
1476
|
+
function usageOf(fields) {
|
|
1477
|
+
const input = fields.input ?? 0;
|
|
1478
|
+
const output = fields.output ?? 0;
|
|
1479
|
+
const usage = {
|
|
1480
|
+
input,
|
|
1481
|
+
output,
|
|
1482
|
+
total: fields.total ?? input + output
|
|
1483
|
+
};
|
|
1484
|
+
if (fields.cachedTokens !== void 0) usage.cachedTokens = fields.cachedTokens;
|
|
1485
|
+
if (fields.cacheWriteTokens !== void 0) usage.cacheWriteTokens = fields.cacheWriteTokens;
|
|
1486
|
+
if (fields.reasoningTokens !== void 0) usage.reasoningTokens = fields.reasoningTokens;
|
|
1487
|
+
if (fields.costUSD !== void 0) usage.costUSD = fields.costUSD;
|
|
1488
|
+
return usage;
|
|
1489
|
+
}
|
|
1490
|
+
/** An `unsupported-option` error for an adapter. */
|
|
1491
|
+
function unsupportedOption(adapter, message) {
|
|
1492
|
+
return new CliError(`${adapter.title}: ${message}`, {
|
|
1493
|
+
type: "unsupported-option",
|
|
1494
|
+
adapter: adapter.name
|
|
1495
|
+
});
|
|
1496
|
+
}
|
|
1497
|
+
/** Parse a JSON-encoded tool-arguments string; keeps the raw string when it is not JSON. */
|
|
1498
|
+
function parseArguments(value) {
|
|
1499
|
+
if (typeof value !== "string") return value;
|
|
1500
|
+
try {
|
|
1501
|
+
return JSON.parse(value);
|
|
1502
|
+
} catch {
|
|
1503
|
+
return value;
|
|
1504
|
+
}
|
|
1505
|
+
}
|
|
1506
|
+
/**
|
|
1507
|
+
* Usage in the Anthropic Messages shape (`input_tokens` excludes cache reads
|
|
1508
|
+
* and writes, which are reported separately). `input` is the sum of all
|
|
1509
|
+
* three, matching the framework rule that `input` includes cached tokens.
|
|
1510
|
+
*/
|
|
1511
|
+
function anthropicUsage(usage, costUSD) {
|
|
1512
|
+
if (usage === void 0) return;
|
|
1513
|
+
const read = (key) => {
|
|
1514
|
+
const value = usage[key];
|
|
1515
|
+
return typeof value === "number" && Number.isFinite(value) ? value : void 0;
|
|
1516
|
+
};
|
|
1517
|
+
const uncached = read("input_tokens") ?? 0;
|
|
1518
|
+
const cacheRead = read("cache_read_input_tokens");
|
|
1519
|
+
const cacheWrite = read("cache_creation_input_tokens");
|
|
1520
|
+
const output = read("output_tokens") ?? 0;
|
|
1521
|
+
return usageOf({
|
|
1522
|
+
input: uncached + (cacheRead ?? 0) + (cacheWrite ?? 0),
|
|
1523
|
+
output,
|
|
1524
|
+
cachedTokens: cacheRead !== void 0 && cacheRead > 0 ? cacheRead : void 0,
|
|
1525
|
+
cacheWriteTokens: cacheWrite !== void 0 && cacheWrite > 0 ? cacheWrite : void 0,
|
|
1526
|
+
reasoningTokens: read("reasoning_tokens") || void 0,
|
|
1527
|
+
costUSD
|
|
1528
|
+
});
|
|
1529
|
+
}
|
|
1530
|
+
|
|
1531
|
+
//#endregion
|
|
1532
|
+
//#region ../../packages/ai-cli/src/adapters/aider.ts
|
|
1533
|
+
/** Local analytics log the adapter asks Aider to write (exact token counts). */
|
|
1534
|
+
const ANALYTICS_LOG = "aider-analytics.jsonl";
|
|
1535
|
+
/**
|
|
1536
|
+
* Aider (`aider`, PyPI `aider-chat`).
|
|
1537
|
+
*
|
|
1538
|
+
* Verified against the official docs (`aider.chat/docs/scripting.html`,
|
|
1539
|
+
* `/docs/config/options.html`, `/docs/config/reasoning.html`) and the
|
|
1540
|
+
* source at tag v0.86.2 (`aider/main.py`, `aider/args.py`,
|
|
1541
|
+
* `aider/coders/base_coder.py`, `aider/analytics.py`); Aider needs Python
|
|
1542
|
+
* 3.10+, so no real `--help` was run, 2026-10-17. The capability probe
|
|
1543
|
+
* checks every flag against the installed `--help`.
|
|
1544
|
+
*
|
|
1545
|
+
* - Headless: `aider --message <text>` (or `--message-file`). Aider has **no
|
|
1546
|
+
* machine-readable output**: the parser reads the plain-text stdout —
|
|
1547
|
+
* startup banner (`Main model: …`), the reply, the usage line
|
|
1548
|
+
* (`Tokens: 2.4k sent, 61 received. Cost: $0.0081 message, …`),
|
|
1549
|
+
* `Applied edit to <path>` (reported as an `edit` tool call) and
|
|
1550
|
+
* `Commit <hash> <message>`. Exact token counts and cost come from the
|
|
1551
|
+
* local `--analytics-log` file (`message_send` events), read when the
|
|
1552
|
+
* stream ends.
|
|
1553
|
+
* - Aider exits 0 even when the model call fails, so `litellm.*Error` lines
|
|
1554
|
+
* and the "not able to authenticate you" notice are reported as fatal.
|
|
1555
|
+
* - Permissions: Aider applies edits without approval and, headless with
|
|
1556
|
+
* `--yes-always`, never runs the shell commands it suggests; there is no
|
|
1557
|
+
* stricter or looser mode, so `standard` and `bypass` behave the same.
|
|
1558
|
+
* Auto-commits are turned off (`--no-auto-commits`); pass
|
|
1559
|
+
* `args: ["--auto-commits"]` to keep them.
|
|
1560
|
+
* - No session ids (`resume` is refused) and no MCP. Chat and input
|
|
1561
|
+
* history are written to the run directory, not the repository.
|
|
1562
|
+
* - Home: Aider uses `HOME`; provider keys use their own variables
|
|
1563
|
+
* (`OPENAI_API_KEY`, `ANTHROPIC_API_KEY`, …) via `env`. Always pass
|
|
1564
|
+
* `model` — without one Aider may start an interactive OAuth flow.
|
|
1565
|
+
*/
|
|
1566
|
+
const aiderAdapter = {
|
|
1567
|
+
name: "aider",
|
|
1568
|
+
title: "Aider",
|
|
1569
|
+
command: "aider",
|
|
1570
|
+
apiKeyEnv: [],
|
|
1571
|
+
homeEnv: ["HOME", "USERPROFILE"],
|
|
1572
|
+
transports: ["argv", "file"],
|
|
1573
|
+
effort: true,
|
|
1574
|
+
mcp: false,
|
|
1575
|
+
probe: {
|
|
1576
|
+
help: [["--help"]],
|
|
1577
|
+
version: ["--version"]
|
|
1578
|
+
},
|
|
1579
|
+
verification: {
|
|
1580
|
+
version: "0.86.2",
|
|
1581
|
+
verifiedBy: "docs",
|
|
1582
|
+
sources: [
|
|
1583
|
+
"https://aider.chat/docs/scripting.html",
|
|
1584
|
+
"https://aider.chat/docs/config/options.html",
|
|
1585
|
+
"https://aider.chat/docs/config/reasoning.html",
|
|
1586
|
+
"https://github.com/Aider-AI/aider/tree/v0.86.2"
|
|
1587
|
+
],
|
|
1588
|
+
checkedAt: "2026-10-17"
|
|
1589
|
+
},
|
|
1590
|
+
plan(context) {
|
|
1591
|
+
if (context.resume !== void 0) throw unsupportedOption(aiderAdapter, "Aider has no session ids, so `resume` is not supported.");
|
|
1592
|
+
const args = [
|
|
1593
|
+
"--yes-always",
|
|
1594
|
+
"--no-pretty",
|
|
1595
|
+
"--no-stream",
|
|
1596
|
+
"--no-fancy-input",
|
|
1597
|
+
"--no-auto-commits",
|
|
1598
|
+
"--no-check-update",
|
|
1599
|
+
"--no-show-release-notes",
|
|
1600
|
+
"--no-analytics",
|
|
1601
|
+
"--no-show-model-warnings",
|
|
1602
|
+
"--no-gitignore",
|
|
1603
|
+
"--analytics-log",
|
|
1604
|
+
node_path.default.join(context.runDir, ANALYTICS_LOG),
|
|
1605
|
+
"--chat-history-file",
|
|
1606
|
+
node_path.default.join(context.runDir, "chat-history.md"),
|
|
1607
|
+
"--input-history-file",
|
|
1608
|
+
node_path.default.join(context.runDir, "input-history")
|
|
1609
|
+
];
|
|
1610
|
+
const requires = [
|
|
1611
|
+
"--yes-always",
|
|
1612
|
+
"--no-pretty",
|
|
1613
|
+
"--no-stream",
|
|
1614
|
+
"--no-auto-commits",
|
|
1615
|
+
"--analytics-log",
|
|
1616
|
+
"--chat-history-file",
|
|
1617
|
+
"--input-history-file"
|
|
1618
|
+
];
|
|
1619
|
+
const files = [];
|
|
1620
|
+
let tail;
|
|
1621
|
+
if (context.model !== void 0) {
|
|
1622
|
+
args.push("--model", context.model);
|
|
1623
|
+
requires.push("--model");
|
|
1624
|
+
}
|
|
1625
|
+
if (context.effort !== void 0) {
|
|
1626
|
+
args.push("--reasoning-effort", mapEffort(context.effort, {
|
|
1627
|
+
low: "low",
|
|
1628
|
+
medium: "medium",
|
|
1629
|
+
high: "high"
|
|
1630
|
+
}));
|
|
1631
|
+
requires.push("--reasoning-effort");
|
|
1632
|
+
}
|
|
1633
|
+
if (context.transport === "file") {
|
|
1634
|
+
const prompt = promptFile(context);
|
|
1635
|
+
files.push(prompt);
|
|
1636
|
+
tail = ["--message-file", prompt.path];
|
|
1637
|
+
requires.push("--message-file");
|
|
1638
|
+
} else {
|
|
1639
|
+
tail = ["--message", argvPrompt(context.prompt)];
|
|
1640
|
+
requires.push("--message");
|
|
1641
|
+
}
|
|
1642
|
+
return {
|
|
1643
|
+
args,
|
|
1644
|
+
tail,
|
|
1645
|
+
env: {
|
|
1646
|
+
...context.home !== void 0 ? {
|
|
1647
|
+
HOME: context.home,
|
|
1648
|
+
USERPROFILE: context.home
|
|
1649
|
+
} : {},
|
|
1650
|
+
TERM: "dumb"
|
|
1651
|
+
},
|
|
1652
|
+
files,
|
|
1653
|
+
requires
|
|
1654
|
+
};
|
|
1655
|
+
},
|
|
1656
|
+
parser: createAiderParser
|
|
1657
|
+
};
|
|
1658
|
+
/** `2.4k` → 2400, `12k` → 12000, `61` → 61. */
|
|
1659
|
+
function parseAiderCount(value) {
|
|
1660
|
+
if (value === void 0) return;
|
|
1661
|
+
const match = /^([\d.]+)(k|M)?$/.exec(value.trim());
|
|
1662
|
+
if (!match) return;
|
|
1663
|
+
const number = Number(match[1]);
|
|
1664
|
+
const scale = match[2] === "k" ? 1e3 : match[2] === "M" ? 1e6 : 1;
|
|
1665
|
+
return Number.isFinite(number) ? Math.round(number * scale) : void 0;
|
|
1666
|
+
}
|
|
1667
|
+
const TOKENS_LINE = /^Tokens: ([\d.]+[kM]?) sent(?:, ([\d.]+[kM]?) cache write)?(?:, ([\d.]+[kM]?) cache hit)?, ([\d.]+[kM]?) received\.(?:\s+Cost: \$([\d.]+) message, \$([\d.]+) session\.)?/;
|
|
1668
|
+
const BANNER = [
|
|
1669
|
+
/^Aider v\d/,
|
|
1670
|
+
/^Main model: /,
|
|
1671
|
+
/^Weak model: /,
|
|
1672
|
+
/^Editor model: /,
|
|
1673
|
+
/^Model: /,
|
|
1674
|
+
/^Git repo: /,
|
|
1675
|
+
/^Repo-map: /,
|
|
1676
|
+
/^Added .+ to the chat\.$/,
|
|
1677
|
+
/^Cost: \$[\d.]+ message, \$[\d.]+ session\.$/,
|
|
1678
|
+
/^Use \/help /
|
|
1679
|
+
];
|
|
1680
|
+
/** Usage from the analytics log (`message_send` events), summed. */
|
|
1681
|
+
function analyticsUsage(context) {
|
|
1682
|
+
if (context === void 0) return;
|
|
1683
|
+
let content;
|
|
1684
|
+
try {
|
|
1685
|
+
content = (0, node_fs.readFileSync)(node_path.default.join(context.runDir, ANALYTICS_LOG), "utf8");
|
|
1686
|
+
} catch {
|
|
1687
|
+
return;
|
|
1688
|
+
}
|
|
1689
|
+
let input = 0;
|
|
1690
|
+
let output = 0;
|
|
1691
|
+
let cost;
|
|
1692
|
+
let seen = false;
|
|
1693
|
+
for (const line of content.split(/\r?\n/)) {
|
|
1694
|
+
const record = parseJsonLine(line);
|
|
1695
|
+
if (stringField(record, "event") !== "message_send") continue;
|
|
1696
|
+
const properties = recordField(record, "properties");
|
|
1697
|
+
seen = true;
|
|
1698
|
+
input += numberField(properties, "prompt_tokens") ?? 0;
|
|
1699
|
+
output += numberField(properties, "completion_tokens") ?? 0;
|
|
1700
|
+
const messageCost = numberField(properties, "cost");
|
|
1701
|
+
if (messageCost !== void 0) cost = (cost ?? 0) + messageCost;
|
|
1702
|
+
}
|
|
1703
|
+
return seen ? {
|
|
1704
|
+
type: "usage",
|
|
1705
|
+
cumulative: true,
|
|
1706
|
+
usage: usageOf({
|
|
1707
|
+
input,
|
|
1708
|
+
output,
|
|
1709
|
+
costUSD: cost
|
|
1710
|
+
})
|
|
1711
|
+
} : void 0;
|
|
1712
|
+
}
|
|
1713
|
+
/** Parser for Aider's plain-text stdout (plus its analytics log). */
|
|
1714
|
+
function createAiderParser(context) {
|
|
1715
|
+
let started = false;
|
|
1716
|
+
let body = [];
|
|
1717
|
+
const flush = () => {
|
|
1718
|
+
const text = body.join("\n").trim();
|
|
1719
|
+
body = [];
|
|
1720
|
+
return text.length > 0 ? [{
|
|
1721
|
+
type: "message",
|
|
1722
|
+
text,
|
|
1723
|
+
delta: false
|
|
1724
|
+
}] : [];
|
|
1725
|
+
};
|
|
1726
|
+
return {
|
|
1727
|
+
line(line) {
|
|
1728
|
+
const events = [];
|
|
1729
|
+
const model = /^Main model: (\S+)/.exec(line);
|
|
1730
|
+
if (!started && (model || line.trim().length > 0 && !BANNER.some((pattern) => pattern.test(line)))) {
|
|
1731
|
+
started = true;
|
|
1732
|
+
events.push({
|
|
1733
|
+
type: "session.started",
|
|
1734
|
+
model: model?.[1]
|
|
1735
|
+
});
|
|
1736
|
+
}
|
|
1737
|
+
if (BANNER.some((pattern) => pattern.test(line))) return events;
|
|
1738
|
+
const tokens = TOKENS_LINE.exec(line);
|
|
1739
|
+
if (tokens) {
|
|
1740
|
+
const cacheWrite = parseAiderCount(tokens[2]);
|
|
1741
|
+
const cacheHit = parseAiderCount(tokens[3]);
|
|
1742
|
+
const sent = parseAiderCount(tokens[1]) ?? 0;
|
|
1743
|
+
events.push(...flush(), {
|
|
1744
|
+
type: "usage",
|
|
1745
|
+
cumulative: false,
|
|
1746
|
+
usage: usageOf({
|
|
1747
|
+
input: sent,
|
|
1748
|
+
output: parseAiderCount(tokens[4]),
|
|
1749
|
+
cachedTokens: cacheHit,
|
|
1750
|
+
cacheWriteTokens: cacheWrite,
|
|
1751
|
+
costUSD: tokens[5] !== void 0 ? Number(tokens[5]) : void 0
|
|
1752
|
+
})
|
|
1753
|
+
});
|
|
1754
|
+
return events;
|
|
1755
|
+
}
|
|
1756
|
+
const applied = /^Applied edit to (.+)$/.exec(line);
|
|
1757
|
+
if (applied) {
|
|
1758
|
+
const file = applied[1] ?? "";
|
|
1759
|
+
const id = `edit-${file}`;
|
|
1760
|
+
events.push(...flush(), {
|
|
1761
|
+
type: "tool.call",
|
|
1762
|
+
id,
|
|
1763
|
+
name: "edit",
|
|
1764
|
+
input: { path: file }
|
|
1765
|
+
}, {
|
|
1766
|
+
type: "tool.result",
|
|
1767
|
+
id,
|
|
1768
|
+
output: line,
|
|
1769
|
+
isError: false
|
|
1770
|
+
});
|
|
1771
|
+
return events;
|
|
1772
|
+
}
|
|
1773
|
+
const commit = /^Commit ([0-9a-f]{7,40}) (.*)$/.exec(line);
|
|
1774
|
+
if (commit) {
|
|
1775
|
+
const id = `commit-${commit[1] ?? ""}`;
|
|
1776
|
+
events.push({
|
|
1777
|
+
type: "tool.call",
|
|
1778
|
+
id,
|
|
1779
|
+
name: "commit",
|
|
1780
|
+
input: {
|
|
1781
|
+
hash: commit[1],
|
|
1782
|
+
message: commit[2]
|
|
1783
|
+
}
|
|
1784
|
+
}, {
|
|
1785
|
+
type: "tool.result",
|
|
1786
|
+
id,
|
|
1787
|
+
output: line,
|
|
1788
|
+
isError: false
|
|
1789
|
+
});
|
|
1790
|
+
return events;
|
|
1791
|
+
}
|
|
1792
|
+
if (/^litellm\.\w+Error:/.test(line) || line.startsWith("The API provider is not able to authenticate you")) {
|
|
1793
|
+
events.push({
|
|
1794
|
+
type: "error",
|
|
1795
|
+
message: line,
|
|
1796
|
+
fatal: true
|
|
1797
|
+
});
|
|
1798
|
+
return events;
|
|
1799
|
+
}
|
|
1800
|
+
if (/^Retrying in [\d.]+ seconds/.test(line)) {
|
|
1801
|
+
events.push({
|
|
1802
|
+
type: "error",
|
|
1803
|
+
message: line
|
|
1804
|
+
});
|
|
1805
|
+
return events;
|
|
1806
|
+
}
|
|
1807
|
+
body.push(line);
|
|
1808
|
+
return events;
|
|
1809
|
+
},
|
|
1810
|
+
end() {
|
|
1811
|
+
const events = flush();
|
|
1812
|
+
const usage = analyticsUsage(context);
|
|
1813
|
+
return usage !== void 0 ? [...events, usage] : events;
|
|
1814
|
+
}
|
|
1815
|
+
};
|
|
1816
|
+
}
|
|
1817
|
+
|
|
1818
|
+
//#endregion
|
|
1819
|
+
//#region ../../packages/ai-cli/src/adapters/claude-stream.ts
|
|
1820
|
+
function inclusiveUsage(usage, costUSD) {
|
|
1821
|
+
if (usage === void 0) return;
|
|
1822
|
+
const cached = numberField(usage, "cache_read_input_tokens");
|
|
1823
|
+
const written = numberField(usage, "cache_creation_input_tokens");
|
|
1824
|
+
return usageOf({
|
|
1825
|
+
input: numberField(usage, "input_tokens"),
|
|
1826
|
+
output: numberField(usage, "output_tokens"),
|
|
1827
|
+
total: numberField(usage, "total_tokens"),
|
|
1828
|
+
cachedTokens: cached !== void 0 && cached > 0 ? cached : void 0,
|
|
1829
|
+
cacheWriteTokens: written !== void 0 && written > 0 ? written : void 0,
|
|
1830
|
+
costUSD
|
|
1831
|
+
});
|
|
1832
|
+
}
|
|
1833
|
+
/**
|
|
1834
|
+
* Parser for the Claude Code `stream-json` format, which several CLIs
|
|
1835
|
+
* reproduce (Qwen Code, Grok Build's `streaming-messages-json`):
|
|
1836
|
+
*
|
|
1837
|
+
* - `{"type":"system","subtype":"init","session_id","model",…}`
|
|
1838
|
+
* - `{"type":"assistant","message":{"content":[text | tool_use | thinking]}}`
|
|
1839
|
+
* - `{"type":"user","message":{"content":[{"type":"tool_result","tool_use_id","content","is_error"}]}}`
|
|
1840
|
+
* - `{"type":"stream_event","event":{"type":"content_block_delta","delta":{"type":"text_delta","text"}}}`
|
|
1841
|
+
* (only with partial messages enabled)
|
|
1842
|
+
* - `{"type":"result","subtype","is_error","result","session_id","usage","total_cost_usd"}`
|
|
1843
|
+
*
|
|
1844
|
+
* Per-message `usage` is ignored (Claude Code repeats it on every content
|
|
1845
|
+
* block of a message); the `result` totals are reported as cumulative usage.
|
|
1846
|
+
* Sub-agent traffic (`parent_tool_use_id` set) is reported like the main
|
|
1847
|
+
* thread's, so its tool calls stay visible.
|
|
1848
|
+
*/
|
|
1849
|
+
function createClaudeStreamParser(options = {}) {
|
|
1850
|
+
const style = options.usage ?? "anthropic";
|
|
1851
|
+
const vendor = options.vendor ?? "The CLI";
|
|
1852
|
+
return jsonLinesParser((record) => {
|
|
1853
|
+
switch (stringField(record, "type")) {
|
|
1854
|
+
case "system": {
|
|
1855
|
+
const subtype = stringField(record, "subtype");
|
|
1856
|
+
if (subtype?.startsWith("error") && stringField(record, "error") !== void 0) return [{
|
|
1857
|
+
type: "error",
|
|
1858
|
+
message: stringField(record, "error") ?? subtype,
|
|
1859
|
+
raw: record
|
|
1860
|
+
}];
|
|
1861
|
+
return subtype === "init" || subtype === "session_start" ? [{
|
|
1862
|
+
type: "session.started",
|
|
1863
|
+
sessionRef: stringField(record, "session_id"),
|
|
1864
|
+
model: stringField(record, "model"),
|
|
1865
|
+
raw: record
|
|
1866
|
+
}] : [];
|
|
1867
|
+
}
|
|
1868
|
+
case "assistant": {
|
|
1869
|
+
const events = [];
|
|
1870
|
+
for (const item of asArray(recordField(record, "message")?.content)) {
|
|
1871
|
+
const block = asRecord(item);
|
|
1872
|
+
const blockType = stringField(block, "type");
|
|
1873
|
+
if (blockType === "text") {
|
|
1874
|
+
const text = stringField(block, "text") ?? "";
|
|
1875
|
+
if (text.length > 0) events.push({
|
|
1876
|
+
type: "message",
|
|
1877
|
+
text,
|
|
1878
|
+
delta: false,
|
|
1879
|
+
raw: record
|
|
1880
|
+
});
|
|
1881
|
+
} else if (blockType === "tool_use" || blockType === "server_tool_use") events.push({
|
|
1882
|
+
type: "tool.call",
|
|
1883
|
+
id: stringField(block, "id") ?? "",
|
|
1884
|
+
name: stringField(block, "name") ?? "",
|
|
1885
|
+
input: block?.input,
|
|
1886
|
+
raw: record
|
|
1887
|
+
});
|
|
1888
|
+
}
|
|
1889
|
+
return events;
|
|
1890
|
+
}
|
|
1891
|
+
case "user": {
|
|
1892
|
+
const events = [];
|
|
1893
|
+
for (const item of asArray(recordField(record, "message")?.content)) {
|
|
1894
|
+
const block = asRecord(item);
|
|
1895
|
+
if (stringField(block, "type") === "tool_result") events.push({
|
|
1896
|
+
type: "tool.result",
|
|
1897
|
+
id: stringField(block, "tool_use_id") ?? "",
|
|
1898
|
+
output: outputText(block?.content),
|
|
1899
|
+
isError: block?.is_error === true,
|
|
1900
|
+
raw: record
|
|
1901
|
+
});
|
|
1902
|
+
}
|
|
1903
|
+
return events;
|
|
1904
|
+
}
|
|
1905
|
+
case "stream_event": {
|
|
1906
|
+
const event = recordField(record, "event");
|
|
1907
|
+
const delta = recordField(event, "delta");
|
|
1908
|
+
if (stringField(event, "type") === "content_block_delta" && stringField(delta, "type") === "text_delta") {
|
|
1909
|
+
const text = stringField(delta, "text") ?? "";
|
|
1910
|
+
return text.length > 0 ? [{
|
|
1911
|
+
type: "message",
|
|
1912
|
+
text,
|
|
1913
|
+
delta: true,
|
|
1914
|
+
raw: record
|
|
1915
|
+
}] : [];
|
|
1916
|
+
}
|
|
1917
|
+
return [];
|
|
1918
|
+
}
|
|
1919
|
+
case "result": {
|
|
1920
|
+
const events = [];
|
|
1921
|
+
const cost = numberField(record, "total_cost_usd");
|
|
1922
|
+
const usage = style === "anthropic" ? anthropicUsage(recordField(record, "usage"), cost) : inclusiveUsage(recordField(record, "usage"), cost);
|
|
1923
|
+
if (usage !== void 0) events.push({
|
|
1924
|
+
type: "usage",
|
|
1925
|
+
usage,
|
|
1926
|
+
cumulative: true,
|
|
1927
|
+
raw: record
|
|
1928
|
+
});
|
|
1929
|
+
const result = stringField(record, "result");
|
|
1930
|
+
if (record.is_error === true) {
|
|
1931
|
+
const errors = asArray(record.errors).map((item) => typeof item === "string" ? item : stringField(asRecord(item), "message") ?? "").filter((item) => item.length > 0).join("; ");
|
|
1932
|
+
const message = stringField(recordField(record, "error"), "message") || stringField(record, "error") || errors || result || `${vendor} run ended with ${stringField(record, "subtype") ?? "an error"}`;
|
|
1933
|
+
events.push({
|
|
1934
|
+
type: "error",
|
|
1935
|
+
message,
|
|
1936
|
+
fatal: true,
|
|
1937
|
+
raw: record
|
|
1938
|
+
});
|
|
1939
|
+
} else if (result !== void 0 && result.length > 0) events.push({
|
|
1940
|
+
type: "message",
|
|
1941
|
+
text: result,
|
|
1942
|
+
delta: false,
|
|
1943
|
+
final: true,
|
|
1944
|
+
raw: record
|
|
1945
|
+
});
|
|
1946
|
+
return events;
|
|
1947
|
+
}
|
|
1948
|
+
default: return [];
|
|
1949
|
+
}
|
|
1950
|
+
});
|
|
1951
|
+
}
|
|
1952
|
+
|
|
1953
|
+
//#endregion
|
|
1954
|
+
//#region ../../packages/ai-cli/src/adapters/amp.ts
|
|
1955
|
+
/**
|
|
1956
|
+
* Amp (`amp`, npm `@ampcode/cli`; `@sourcegraph/amp` is a shim to it).
|
|
1957
|
+
*
|
|
1958
|
+
* Verified against the real `amp --help` of 0.0.1792254886-g14d018 and the
|
|
1959
|
+
* docs (`ampcode.com/docs/cli/{streaming-json,execute-mode,settings}`,
|
|
1960
|
+
* `/docs/tools`), 2026-10-17.
|
|
1961
|
+
*
|
|
1962
|
+
* - Headless: `amp -x <prompt> --stream-json` (with `-x` and no message,
|
|
1963
|
+
* stdin is the prompt); resume is `amp threads continue <T-id> -x …`.
|
|
1964
|
+
* Threads are kept unarchived (`--no-archive-after-execute`) so they can
|
|
1965
|
+
* be continued.
|
|
1966
|
+
* - Stream: Claude Code-compatible (`system/init` with `session_id`
|
|
1967
|
+
* `T-…`, `assistant` text/`tool_use`, `user` `tool_result`, `result`),
|
|
1968
|
+
* no deltas and no cost.
|
|
1969
|
+
* - Model: Amp has no model flag; the mode (`-m low|medium|high|ultra`)
|
|
1970
|
+
* picks model and effort, so `effort` maps to the mode and `model` is
|
|
1971
|
+
* refused.
|
|
1972
|
+
* - Permissions: Amp's own rules (tools run without approval by default;
|
|
1973
|
+
* anything that needs approval is rejected in execute mode). Bypass passes
|
|
1974
|
+
* `--dangerously-allow-all`, which the CLI accepts but no longer lists in
|
|
1975
|
+
* `--help`, so it is not part of the capability probe — a CLI that drops
|
|
1976
|
+
* it rejects the run as an unknown option.
|
|
1977
|
+
* - MCP: `--mcp-config <file>` holding the server map itself (no
|
|
1978
|
+
* `mcpServers` wrapper), written to the run directory.
|
|
1979
|
+
* - Home: Amp follows XDG (`$XDG_CONFIG_HOME/amp/settings.json`); auth
|
|
1980
|
+
* `AMP_API_KEY`; update checks off with `AMP_SKIP_UPDATE_CHECK=1`.
|
|
1981
|
+
*/
|
|
1982
|
+
const ampAdapter = {
|
|
1983
|
+
name: "amp",
|
|
1984
|
+
title: "Amp",
|
|
1985
|
+
command: "amp",
|
|
1986
|
+
apiKeyEnv: ["AMP_API_KEY"],
|
|
1987
|
+
homeEnv: [
|
|
1988
|
+
"XDG_CONFIG_HOME",
|
|
1989
|
+
"XDG_DATA_HOME",
|
|
1990
|
+
"XDG_STATE_HOME",
|
|
1991
|
+
"XDG_CACHE_HOME"
|
|
1992
|
+
],
|
|
1993
|
+
transports: ["argv", "stdin"],
|
|
1994
|
+
effort: true,
|
|
1995
|
+
mcp: true,
|
|
1996
|
+
probe: {
|
|
1997
|
+
help: [["--help"]],
|
|
1998
|
+
version: ["--version"]
|
|
1999
|
+
},
|
|
2000
|
+
verification: {
|
|
2001
|
+
version: "0.0.1792254886-g14d018",
|
|
2002
|
+
verifiedBy: "help",
|
|
2003
|
+
sources: [
|
|
2004
|
+
"https://ampcode.com/docs/cli/streaming-json",
|
|
2005
|
+
"https://ampcode.com/docs/cli/execute-mode",
|
|
2006
|
+
"https://ampcode.com/docs/cli/settings"
|
|
2007
|
+
],
|
|
2008
|
+
checkedAt: "2026-10-17"
|
|
2009
|
+
},
|
|
2010
|
+
plan(context) {
|
|
2011
|
+
if (context.model !== void 0) throw unsupportedOption(ampAdapter, "Amp has no model flag; its mode picks the model — use `effort` instead of `model`.");
|
|
2012
|
+
const args = context.resume !== void 0 ? [
|
|
2013
|
+
"threads",
|
|
2014
|
+
"continue",
|
|
2015
|
+
context.resume
|
|
2016
|
+
] : [];
|
|
2017
|
+
const requires = [
|
|
2018
|
+
"--execute",
|
|
2019
|
+
"--stream-json",
|
|
2020
|
+
"--no-archive-after-execute"
|
|
2021
|
+
];
|
|
2022
|
+
const files = [];
|
|
2023
|
+
if (context.resume !== void 0) requires.push("threads");
|
|
2024
|
+
args.push("--stream-json", "--no-archive-after-execute");
|
|
2025
|
+
if (context.effort !== void 0) {
|
|
2026
|
+
args.push("--mode", mapEffort(context.effort, {
|
|
2027
|
+
low: "low",
|
|
2028
|
+
medium: "medium",
|
|
2029
|
+
high: "high",
|
|
2030
|
+
max: "ultra"
|
|
2031
|
+
}));
|
|
2032
|
+
requires.push("--mode");
|
|
2033
|
+
}
|
|
2034
|
+
if (context.permission === "bypass") args.push("--dangerously-allow-all");
|
|
2035
|
+
if (context.mcp !== void 0) {
|
|
2036
|
+
const file = node_path.default.join(context.runDir, "amp-mcp.json");
|
|
2037
|
+
files.push({
|
|
2038
|
+
path: file,
|
|
2039
|
+
content: jsonFile(mcpServersJson(context.mcp))
|
|
2040
|
+
});
|
|
2041
|
+
args.push("--mcp-config", file);
|
|
2042
|
+
requires.push("--mcp-config");
|
|
2043
|
+
}
|
|
2044
|
+
const env = { AMP_SKIP_UPDATE_CHECK: "1" };
|
|
2045
|
+
if (context.home !== void 0) {
|
|
2046
|
+
env.XDG_CONFIG_HOME = node_path.default.join(context.home, "config");
|
|
2047
|
+
env.XDG_DATA_HOME = node_path.default.join(context.home, "data");
|
|
2048
|
+
env.XDG_STATE_HOME = node_path.default.join(context.home, "state");
|
|
2049
|
+
env.XDG_CACHE_HOME = node_path.default.join(context.home, "cache");
|
|
2050
|
+
}
|
|
2051
|
+
return {
|
|
2052
|
+
args,
|
|
2053
|
+
tail: context.transport === "argv" ? ["--execute", argvPrompt(context.prompt)] : ["--execute"],
|
|
2054
|
+
stdin: context.transport === "stdin" ? context.prompt : void 0,
|
|
2055
|
+
env,
|
|
2056
|
+
files,
|
|
2057
|
+
requires
|
|
2058
|
+
};
|
|
2059
|
+
},
|
|
2060
|
+
parser: () => createClaudeStreamParser({
|
|
2061
|
+
usage: "anthropic",
|
|
2062
|
+
vendor: "Amp"
|
|
2063
|
+
})
|
|
2064
|
+
};
|
|
2065
|
+
|
|
2066
|
+
//#endregion
|
|
2067
|
+
//#region ../../packages/ai-cli/src/adapters/antigravity.ts
|
|
2068
|
+
/**
|
|
2069
|
+
* Google Antigravity CLI (`agy`), installed by the
|
|
2070
|
+
* `antigravity.google/cli/install.sh` script (no npm package). Not Gemini
|
|
2071
|
+
* CLI, which it succeeds.
|
|
2072
|
+
*
|
|
2073
|
+
* Verified against the official docs only (the binary is distributed by a
|
|
2074
|
+
* curl installer, which was not run): `antigravity.google/docs/cli/headless`,
|
|
2075
|
+
* `/docs/cli/reference`, `/docs/mcp`, `/docs/permissions`, `/docs/settings`,
|
|
2076
|
+
* `/docs/install`, and the 1.4.0 release notes, 2026-10-17. The capability
|
|
2077
|
+
* probe checks every flag against the installed binary's `--help`.
|
|
2078
|
+
*
|
|
2079
|
+
* - Headless: `agy -p <prompt> --output-format stream-json`. For long
|
|
2080
|
+
* prompts the adapter uses the documented stdin protocol instead:
|
|
2081
|
+
* `--input-format stream-json` with one `{"event":"user","message":
|
|
2082
|
+
* {"content":…}}` line, then stdin is closed.
|
|
2083
|
+
* - Stream: `{"event":X, X:{…}}` lines — `init` (with `conversation_id`),
|
|
2084
|
+
* `step_update` (`agent_response` steps carry `text_delta` fragments;
|
|
2085
|
+
* `tool` steps carry `tool_info{name,parameters,output,error}` with call
|
|
2086
|
+
* and result together, correlated by `step_index`) and a final `result`
|
|
2087
|
+
* (`status`, `response`, `error`, cumulative `usage`). No cost field.
|
|
2088
|
+
* - Permissions: headless soft-denies tools that need approval (the run
|
|
2089
|
+
* continues and exits 0); bypass is `--dangerously-skip-permissions`.
|
|
2090
|
+
* - Home: no variable relocates `~/.gemini`, so a private/owned home sets
|
|
2091
|
+
* `HOME` (and `USERPROFILE`). MCP goes to
|
|
2092
|
+
* `$HOME/.gemini/config/mcp_config.json` (remote servers use `serverUrl`).
|
|
2093
|
+
* - Auth: `GEMINI_API_KEY` only works with `"modelProvider": "gemini"` in
|
|
2094
|
+
* `$HOME/.gemini/antigravity-cli/settings.json`; the adapter writes that
|
|
2095
|
+
* file when `apiKey` is set on a private home.
|
|
2096
|
+
*/
|
|
2097
|
+
const antigravityAdapter = {
|
|
2098
|
+
name: "antigravity",
|
|
2099
|
+
title: "Antigravity CLI",
|
|
2100
|
+
command: "agy",
|
|
2101
|
+
apiKeyEnv: ["GEMINI_API_KEY"],
|
|
2102
|
+
homeEnv: ["HOME", "USERPROFILE"],
|
|
2103
|
+
transports: ["argv", "stdin"],
|
|
2104
|
+
effort: true,
|
|
2105
|
+
mcp: true,
|
|
2106
|
+
probe: {
|
|
2107
|
+
help: [["--help"]],
|
|
2108
|
+
version: ["--version"]
|
|
2109
|
+
},
|
|
2110
|
+
verification: {
|
|
2111
|
+
version: "1.4.0",
|
|
2112
|
+
verifiedBy: "docs",
|
|
2113
|
+
sources: [
|
|
2114
|
+
"https://antigravity.google/docs/cli/headless",
|
|
2115
|
+
"https://antigravity.google/docs/cli/reference",
|
|
2116
|
+
"https://antigravity.google/docs/mcp",
|
|
2117
|
+
"https://antigravity.google/docs/permissions",
|
|
2118
|
+
"https://github.com/google-antigravity/antigravity-cli"
|
|
2119
|
+
],
|
|
2120
|
+
checkedAt: "2026-10-17"
|
|
2121
|
+
},
|
|
2122
|
+
plan(context) {
|
|
2123
|
+
const args = ["--output-format", "stream-json"];
|
|
2124
|
+
const requires = ["--output-format", "stream-json"];
|
|
2125
|
+
const files = [];
|
|
2126
|
+
if (context.model !== void 0) {
|
|
2127
|
+
args.push("--model", context.model);
|
|
2128
|
+
requires.push("--model");
|
|
2129
|
+
}
|
|
2130
|
+
if (context.effort !== void 0) {
|
|
2131
|
+
args.push("--effort", mapEffort(context.effort, {
|
|
2132
|
+
low: "low",
|
|
2133
|
+
medium: "medium",
|
|
2134
|
+
high: "high"
|
|
2135
|
+
}));
|
|
2136
|
+
requires.push("--effort");
|
|
2137
|
+
}
|
|
2138
|
+
if (context.resume !== void 0) {
|
|
2139
|
+
args.push("--conversation", context.resume);
|
|
2140
|
+
requires.push("--conversation");
|
|
2141
|
+
}
|
|
2142
|
+
if (context.permission === "bypass") {
|
|
2143
|
+
args.push("--dangerously-skip-permissions");
|
|
2144
|
+
requires.push("--dangerously-skip-permissions");
|
|
2145
|
+
}
|
|
2146
|
+
if (context.mcp !== void 0) {
|
|
2147
|
+
const home = requirePrivateHome(context, antigravityAdapter, "MCP servers");
|
|
2148
|
+
files.push({
|
|
2149
|
+
path: node_path.default.join(home, ".gemini", "config", "mcp_config.json"),
|
|
2150
|
+
content: jsonFile({ mcpServers: antigravityMcp(context.mcp) })
|
|
2151
|
+
});
|
|
2152
|
+
}
|
|
2153
|
+
if (context.apiKey !== void 0 && context.homeIsPrivate && context.home !== void 0) files.push({
|
|
2154
|
+
path: node_path.default.join(context.home, ".gemini", "antigravity-cli", "settings.json"),
|
|
2155
|
+
content: jsonFile({ modelProvider: "gemini" })
|
|
2156
|
+
});
|
|
2157
|
+
const env = context.home !== void 0 ? {
|
|
2158
|
+
HOME: context.home,
|
|
2159
|
+
USERPROFILE: context.home
|
|
2160
|
+
} : {};
|
|
2161
|
+
if (context.transport === "stdin") {
|
|
2162
|
+
args.push("--input-format", "stream-json");
|
|
2163
|
+
requires.push("--input-format");
|
|
2164
|
+
return {
|
|
2165
|
+
args,
|
|
2166
|
+
stdin: `${JSON.stringify({
|
|
2167
|
+
event: "user",
|
|
2168
|
+
message: { content: context.prompt }
|
|
2169
|
+
})}\n`,
|
|
2170
|
+
env,
|
|
2171
|
+
files,
|
|
2172
|
+
requires
|
|
2173
|
+
};
|
|
2174
|
+
}
|
|
2175
|
+
requires.push("-p");
|
|
2176
|
+
return {
|
|
2177
|
+
args,
|
|
2178
|
+
tail: ["-p", argvPrompt(context.prompt)],
|
|
2179
|
+
env,
|
|
2180
|
+
files,
|
|
2181
|
+
requires
|
|
2182
|
+
};
|
|
2183
|
+
},
|
|
2184
|
+
parser: createAntigravityParser
|
|
2185
|
+
};
|
|
2186
|
+
function antigravityMcp(servers) {
|
|
2187
|
+
const rendered = {};
|
|
2188
|
+
for (const [name, server] of Object.entries(servers)) rendered[name] = server.type === "stdio" ? {
|
|
2189
|
+
command: server.command,
|
|
2190
|
+
args: server.args ?? [],
|
|
2191
|
+
...server.env ? { env: server.env } : {}
|
|
2192
|
+
} : {
|
|
2193
|
+
serverUrl: server.url,
|
|
2194
|
+
...server.headers ? { headers: server.headers } : {}
|
|
2195
|
+
};
|
|
2196
|
+
return rendered;
|
|
2197
|
+
}
|
|
2198
|
+
function stepUsage(usage) {
|
|
2199
|
+
if (usage === void 0) return;
|
|
2200
|
+
const cached = numberField(usage, "cache_read_tokens");
|
|
2201
|
+
return usageOf({
|
|
2202
|
+
input: numberField(usage, "input_tokens"),
|
|
2203
|
+
output: numberField(usage, "output_tokens"),
|
|
2204
|
+
total: numberField(usage, "total_tokens"),
|
|
2205
|
+
cachedTokens: cached !== void 0 && cached > 0 ? cached : void 0,
|
|
2206
|
+
reasoningTokens: numberField(usage, "thinking_tokens") || void 0
|
|
2207
|
+
});
|
|
2208
|
+
}
|
|
2209
|
+
/** Parser for `agy -p … --output-format stream-json`. */
|
|
2210
|
+
function createAntigravityParser() {
|
|
2211
|
+
return jsonLinesParser((record) => {
|
|
2212
|
+
const event = stringField(record, "event");
|
|
2213
|
+
if (event === "init") {
|
|
2214
|
+
const init = recordField(record, "init");
|
|
2215
|
+
return [{
|
|
2216
|
+
type: "session.started",
|
|
2217
|
+
sessionRef: stringField(record, "conversation_id"),
|
|
2218
|
+
model: stringField(init, "model"),
|
|
2219
|
+
raw: record
|
|
2220
|
+
}];
|
|
2221
|
+
}
|
|
2222
|
+
if (event === "step_update") {
|
|
2223
|
+
const step = recordField(record, "step_update");
|
|
2224
|
+
const stepType = stringField(step, "step_type");
|
|
2225
|
+
const events = [];
|
|
2226
|
+
if (stepType === "agent_response") {
|
|
2227
|
+
const text = stringField(step, "text_delta") ?? "";
|
|
2228
|
+
if (text.length > 0) events.push({
|
|
2229
|
+
type: "message",
|
|
2230
|
+
text,
|
|
2231
|
+
delta: true,
|
|
2232
|
+
raw: record
|
|
2233
|
+
});
|
|
2234
|
+
}
|
|
2235
|
+
if (stepType === "tool" && stringField(step, "state") === "DONE") {
|
|
2236
|
+
const info = recordField(step, "tool_info");
|
|
2237
|
+
const error = recordField(info, "error");
|
|
2238
|
+
const id = `step-${numberField(step, "step_index") ?? 0}`;
|
|
2239
|
+
events.push({
|
|
2240
|
+
type: "tool.call",
|
|
2241
|
+
id,
|
|
2242
|
+
name: stringField(info, "name") ?? stringField(step, "tool_name") ?? "",
|
|
2243
|
+
input: info?.parameters,
|
|
2244
|
+
raw: record
|
|
2245
|
+
}, {
|
|
2246
|
+
type: "tool.result",
|
|
2247
|
+
id,
|
|
2248
|
+
output: error !== void 0 ? stringField(error, "message") ?? stringField(error, "type") ?? "tool failed" : stringField(info, "output") ?? "",
|
|
2249
|
+
isError: error !== void 0,
|
|
2250
|
+
raw: record
|
|
2251
|
+
});
|
|
2252
|
+
}
|
|
2253
|
+
const usage = stepUsage(recordField(step, "usage"));
|
|
2254
|
+
if (usage !== void 0) events.push({
|
|
2255
|
+
type: "usage",
|
|
2256
|
+
usage,
|
|
2257
|
+
cumulative: false,
|
|
2258
|
+
raw: record
|
|
2259
|
+
});
|
|
2260
|
+
return events;
|
|
2261
|
+
}
|
|
2262
|
+
if (event === "result") {
|
|
2263
|
+
const result = recordField(record, "result");
|
|
2264
|
+
const events = [];
|
|
2265
|
+
const usage = stepUsage(recordField(result, "usage"));
|
|
2266
|
+
if (usage !== void 0) events.push({
|
|
2267
|
+
type: "usage",
|
|
2268
|
+
usage,
|
|
2269
|
+
cumulative: true,
|
|
2270
|
+
raw: record
|
|
2271
|
+
});
|
|
2272
|
+
const status = stringField(result, "status");
|
|
2273
|
+
if (status !== void 0 && status !== "SUCCESS") events.push({
|
|
2274
|
+
type: "error",
|
|
2275
|
+
message: stringField(result, "error") || `Antigravity run ended ${status}`,
|
|
2276
|
+
fatal: true,
|
|
2277
|
+
raw: record
|
|
2278
|
+
});
|
|
2279
|
+
else {
|
|
2280
|
+
const response = stringField(result, "response") ?? "";
|
|
2281
|
+
if (response.length > 0) events.push({
|
|
2282
|
+
type: "message",
|
|
2283
|
+
text: response,
|
|
2284
|
+
delta: false,
|
|
2285
|
+
final: true,
|
|
2286
|
+
raw: record
|
|
2287
|
+
});
|
|
2288
|
+
}
|
|
2289
|
+
return events;
|
|
2290
|
+
}
|
|
2291
|
+
return [];
|
|
2292
|
+
});
|
|
2293
|
+
}
|
|
2294
|
+
|
|
2295
|
+
//#endregion
|
|
2296
|
+
//#region ../../packages/ai-cli/src/adapters/claude-code.ts
|
|
2297
|
+
/**
|
|
2298
|
+
* Claude Code (`claude`, npm `@anthropic-ai/claude-code`).
|
|
2299
|
+
*
|
|
2300
|
+
* Verified against the real `claude --help` of 2.1.302, the event types of
|
|
2301
|
+
* `@anthropic-ai/claude-agent-sdk` 0.3.302 and the docs
|
|
2302
|
+
* (`code.claude.com/docs/en/{headless,cli-reference,mcp,env-vars,
|
|
2303
|
+
* permission-modes}`), 2026-10-17.
|
|
2304
|
+
*
|
|
2305
|
+
* - Headless: `claude -p --output-format stream-json --verbose
|
|
2306
|
+
* --include-partial-messages <prompt>` (`--verbose` is required with
|
|
2307
|
+
* stream-json in print mode); piped stdin is the prompt when no positional
|
|
2308
|
+
* prompt is given.
|
|
2309
|
+
* - Stream: `system/init` (`session_id`), `stream_event` text deltas,
|
|
2310
|
+
* `assistant` messages (text / `tool_use`), `user` messages with
|
|
2311
|
+
* `tool_result`, and a final `result` (`result`, `usage`,
|
|
2312
|
+
* `total_cost_usd`, `is_error`, `errors[]`).
|
|
2313
|
+
* - Permissions: print mode auto-denies anything that would prompt; bypass
|
|
2314
|
+
* is `--dangerously-skip-permissions` (Claude Code refuses it as root
|
|
2315
|
+
* outside a recognised sandbox).
|
|
2316
|
+
* - MCP: `--mcp-config <file>` with `{"mcpServers":…}`, written to the run
|
|
2317
|
+
* directory.
|
|
2318
|
+
* - Home: `CLAUDE_CONFIG_DIR`; auth `ANTHROPIC_API_KEY` (or a home you
|
|
2319
|
+
* logged in to); auto-update off with `DISABLE_AUTOUPDATER=1`.
|
|
2320
|
+
*/
|
|
2321
|
+
const claudeCodeAdapter = {
|
|
2322
|
+
name: "claude-code",
|
|
2323
|
+
title: "Claude Code",
|
|
2324
|
+
command: "claude",
|
|
2325
|
+
apiKeyEnv: ["ANTHROPIC_API_KEY"],
|
|
2326
|
+
homeEnv: ["CLAUDE_CONFIG_DIR"],
|
|
2327
|
+
transports: ["argv", "stdin"],
|
|
2328
|
+
effort: true,
|
|
2329
|
+
mcp: true,
|
|
2330
|
+
probe: {
|
|
2331
|
+
help: [["--help"]],
|
|
2332
|
+
version: ["--version"]
|
|
2333
|
+
},
|
|
2334
|
+
verification: {
|
|
2335
|
+
version: "2.1.302",
|
|
2336
|
+
verifiedBy: "help",
|
|
2337
|
+
sources: [
|
|
2338
|
+
"https://code.claude.com/docs/en/headless",
|
|
2339
|
+
"https://code.claude.com/docs/en/cli-reference",
|
|
2340
|
+
"https://code.claude.com/docs/en/mcp",
|
|
2341
|
+
"https://code.claude.com/docs/en/env-vars"
|
|
2342
|
+
],
|
|
2343
|
+
checkedAt: "2026-10-17"
|
|
2344
|
+
},
|
|
2345
|
+
plan(context) {
|
|
2346
|
+
const args = [
|
|
2347
|
+
"--print",
|
|
2348
|
+
"--output-format",
|
|
2349
|
+
"stream-json",
|
|
2350
|
+
"--verbose",
|
|
2351
|
+
"--include-partial-messages"
|
|
2352
|
+
];
|
|
2353
|
+
const requires = [
|
|
2354
|
+
"--print",
|
|
2355
|
+
"--output-format",
|
|
2356
|
+
"stream-json",
|
|
2357
|
+
"--verbose",
|
|
2358
|
+
"--include-partial-messages"
|
|
2359
|
+
];
|
|
2360
|
+
const files = [];
|
|
2361
|
+
if (context.model !== void 0) {
|
|
2362
|
+
args.push("--model", context.model);
|
|
2363
|
+
requires.push("--model");
|
|
2364
|
+
}
|
|
2365
|
+
if (context.effort !== void 0) {
|
|
2366
|
+
args.push("--effort", mapEffort(context.effort, {
|
|
2367
|
+
low: "low",
|
|
2368
|
+
medium: "medium",
|
|
2369
|
+
high: "high",
|
|
2370
|
+
xhigh: "xhigh",
|
|
2371
|
+
max: "max"
|
|
2372
|
+
}));
|
|
2373
|
+
requires.push("--effort");
|
|
2374
|
+
}
|
|
2375
|
+
if (context.resume !== void 0) {
|
|
2376
|
+
args.push("--resume", context.resume);
|
|
2377
|
+
requires.push("--resume");
|
|
2378
|
+
}
|
|
2379
|
+
if (context.permission === "bypass") {
|
|
2380
|
+
args.push("--dangerously-skip-permissions");
|
|
2381
|
+
requires.push("--dangerously-skip-permissions");
|
|
2382
|
+
}
|
|
2383
|
+
if (context.mcp !== void 0) {
|
|
2384
|
+
const file = node_path.default.join(context.runDir, "mcp.json");
|
|
2385
|
+
files.push({
|
|
2386
|
+
path: file,
|
|
2387
|
+
content: jsonFile({ mcpServers: mcpServersJson(context.mcp, { typeField: true }) })
|
|
2388
|
+
});
|
|
2389
|
+
args.push("--mcp-config", file);
|
|
2390
|
+
requires.push("--mcp-config");
|
|
2391
|
+
}
|
|
2392
|
+
return {
|
|
2393
|
+
args,
|
|
2394
|
+
tail: context.transport === "argv" ? [argvPrompt(context.prompt)] : [],
|
|
2395
|
+
stdin: context.transport === "stdin" ? context.prompt : void 0,
|
|
2396
|
+
env: {
|
|
2397
|
+
...context.home !== void 0 ? { CLAUDE_CONFIG_DIR: context.home } : {},
|
|
2398
|
+
DISABLE_AUTOUPDATER: "1"
|
|
2399
|
+
},
|
|
2400
|
+
files,
|
|
2401
|
+
requires
|
|
2402
|
+
};
|
|
2403
|
+
},
|
|
2404
|
+
parser: () => createClaudeStreamParser({
|
|
2405
|
+
usage: "anthropic",
|
|
2406
|
+
vendor: "Claude Code"
|
|
2407
|
+
})
|
|
2408
|
+
};
|
|
2409
|
+
|
|
2410
|
+
//#endregion
|
|
2411
|
+
//#region ../../packages/ai-cli/src/adapters/codex.ts
|
|
2412
|
+
/**
|
|
2413
|
+
* OpenAI Codex CLI (`codex`, npm `@openai/codex`).
|
|
2414
|
+
*
|
|
2415
|
+
* Verified against the real `codex exec --help` / `codex exec resume
|
|
2416
|
+
* --help` of 0.163.0, the source at tag `rust-v0.163.0` (`exec_events.rs`,
|
|
2417
|
+
* the JSONL event processor, `cli.rs`) and the docs (non-interactive mode,
|
|
2418
|
+
* config reference), 2026-10-17.
|
|
2419
|
+
*
|
|
2420
|
+
* - Headless: `codex exec --json --skip-git-repo-check <prompt>` (`-` reads
|
|
2421
|
+
* the prompt from stdin); resume is `codex exec resume … <thread_id>
|
|
2422
|
+
* <prompt>`.
|
|
2423
|
+
* - Stream: `thread.started` (`thread_id`), `item.started`/`item.completed`
|
|
2424
|
+
* with `item.type` `agent_message`, `reasoning`, `command_execution`,
|
|
2425
|
+
* `file_change`, `mcp_tool_call`, `web_search`, `error`; `turn.completed`
|
|
2426
|
+
* with the thread's cumulative `usage` (`input_tokens` includes
|
|
2427
|
+
* `cached_input_tokens`); `turn.failed` and `error` on failure. No text
|
|
2428
|
+
* deltas and no cost field.
|
|
2429
|
+
* - Permissions: exec never asks (approval policy "never"); `standard` keeps
|
|
2430
|
+
* Codex's default **read-only** sandbox — allow edits with
|
|
2431
|
+
* `args: ["-c", "sandbox_mode=workspace-write"]`. Bypass is
|
|
2432
|
+
* `--dangerously-bypass-approvals-and-sandbox`.
|
|
2433
|
+
* - Effort: `-c model_reasoning_effort=<level>`.
|
|
2434
|
+
* - MCP: no flag. With a private home the servers go into
|
|
2435
|
+
* `$CODEX_HOME/config.toml` (0600). With a caller-owned or inherited home
|
|
2436
|
+
* they are passed as `-c mcp_servers.<name>.…` overrides that carry only
|
|
2437
|
+
* the command, args and URL; secret `env` values and headers travel in the
|
|
2438
|
+
* process environment, named through `env_vars` / `env_http_headers`.
|
|
2439
|
+
* - Home `CODEX_HOME`; auth `CODEX_API_KEY`.
|
|
2440
|
+
*/
|
|
2441
|
+
const codexAdapter = {
|
|
2442
|
+
name: "codex",
|
|
2443
|
+
title: "Codex CLI",
|
|
2444
|
+
command: "codex",
|
|
2445
|
+
apiKeyEnv: ["CODEX_API_KEY"],
|
|
2446
|
+
homeEnv: ["CODEX_HOME"],
|
|
2447
|
+
transports: ["argv", "stdin"],
|
|
2448
|
+
effort: true,
|
|
2449
|
+
mcp: true,
|
|
2450
|
+
probe: {
|
|
2451
|
+
help: [["exec", "--help"], [
|
|
2452
|
+
"exec",
|
|
2453
|
+
"resume",
|
|
2454
|
+
"--help"
|
|
2455
|
+
]],
|
|
2456
|
+
version: ["--version"]
|
|
2457
|
+
},
|
|
2458
|
+
verification: {
|
|
2459
|
+
version: "0.163.0",
|
|
2460
|
+
verifiedBy: "help",
|
|
2461
|
+
sources: [
|
|
2462
|
+
"https://developers.openai.com/codex/noninteractive",
|
|
2463
|
+
"https://developers.openai.com/codex/config-reference",
|
|
2464
|
+
"https://github.com/openai/codex/blob/rust-v0.163.0/codex-rs/exec/src/exec_events.rs"
|
|
2465
|
+
],
|
|
2466
|
+
checkedAt: "2026-10-17"
|
|
2467
|
+
},
|
|
2468
|
+
plan(context) {
|
|
2469
|
+
const args = ["exec"];
|
|
2470
|
+
const requires = [
|
|
2471
|
+
"exec",
|
|
2472
|
+
"--json",
|
|
2473
|
+
"--skip-git-repo-check"
|
|
2474
|
+
];
|
|
2475
|
+
const files = [];
|
|
2476
|
+
const env = {};
|
|
2477
|
+
if (context.resume !== void 0) {
|
|
2478
|
+
args.push("resume");
|
|
2479
|
+
requires.push("resume");
|
|
2480
|
+
}
|
|
2481
|
+
args.push("--json", "--skip-git-repo-check", "-c", "check_for_update_on_startup=false");
|
|
2482
|
+
if (context.model !== void 0) {
|
|
2483
|
+
args.push("--model", context.model);
|
|
2484
|
+
requires.push("--model");
|
|
2485
|
+
}
|
|
2486
|
+
if (context.effort !== void 0) {
|
|
2487
|
+
const effort = mapEffort(context.effort, {
|
|
2488
|
+
low: "low",
|
|
2489
|
+
medium: "medium",
|
|
2490
|
+
high: "high",
|
|
2491
|
+
xhigh: "xhigh",
|
|
2492
|
+
max: "max"
|
|
2493
|
+
});
|
|
2494
|
+
args.push("-c", `model_reasoning_effort=${effort}`);
|
|
2495
|
+
requires.push("--config");
|
|
2496
|
+
}
|
|
2497
|
+
if (context.permission === "bypass") {
|
|
2498
|
+
args.push("--dangerously-bypass-approvals-and-sandbox");
|
|
2499
|
+
requires.push("--dangerously-bypass-approvals-and-sandbox");
|
|
2500
|
+
}
|
|
2501
|
+
if (context.mcp !== void 0) if (context.home !== void 0 && context.homeIsPrivate) files.push({
|
|
2502
|
+
path: node_path.default.join(context.home, "config.toml"),
|
|
2503
|
+
content: toToml({ mcp_servers: codexMcpTable(context.mcp) })
|
|
2504
|
+
});
|
|
2505
|
+
else {
|
|
2506
|
+
args.push(...codexMcpOverrides(context.mcp, env));
|
|
2507
|
+
requires.push("--config");
|
|
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 = 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":
|
|
2944
|
+
events.push({
|
|
2945
|
+
type: "error",
|
|
2946
|
+
message: stringField(record, "message") ?? "error",
|
|
2947
|
+
raw: record
|
|
2948
|
+
});
|
|
2949
|
+
break;
|
|
2950
|
+
}
|
|
2951
|
+
return events;
|
|
2952
|
+
}, () => {
|
|
2953
|
+
const usage = usageFileEvent(context);
|
|
2954
|
+
return usage !== void 0 ? [usage] : [];
|
|
2955
|
+
}),
|
|
2956
|
+
sessionRef: () => sessionRef
|
|
2957
|
+
};
|
|
2958
|
+
}
|
|
2959
|
+
|
|
2960
|
+
//#endregion
|
|
2961
|
+
//#region ../../packages/ai-cli/src/adapters/cursor.ts
|
|
2962
|
+
/**
|
|
2963
|
+
* Cursor CLI (`cursor-agent`, also installed as `agent`), installed by the
|
|
2964
|
+
* `cursor.com/install` script (no official npm package).
|
|
2965
|
+
*
|
|
2966
|
+
* Verified against the official docs (`cursor.com/docs/cli/{headless,mcp}`,
|
|
2967
|
+
* `/docs/cli/reference/{output-format,parameters,authentication,
|
|
2968
|
+
* permissions,configuration}`) and the option strings of the
|
|
2969
|
+
* 2026.10.16-e3cb207 release bundle (read, not executed), 2026-10-17. The
|
|
2970
|
+
* capability probe checks every flag against the installed `--help`.
|
|
2971
|
+
*
|
|
2972
|
+
* - Headless: `cursor-agent -p --output-format stream-json --trust <prompt>`;
|
|
2973
|
+
* with no positional prompt, stdin is the prompt. `--trust` is passed
|
|
2974
|
+
* because print mode exits in a directory that was never trusted — the
|
|
2975
|
+
* run's `cwd` is the caller's explicit choice.
|
|
2976
|
+
* - Stream: `system/init` (`session_id`, `model`), `user` echo, `assistant`
|
|
2977
|
+
* messages (`message.content[].text`), `tool_call` `started`/`completed`
|
|
2978
|
+
* (`call_id`; the tool is the single key of `tool_call`, e.g.
|
|
2979
|
+
* `readToolCall{args,result{success}}`), and a final `result` with
|
|
2980
|
+
* `usage{inputTokens (net of cache), outputTokens, cacheReadTokens,
|
|
2981
|
+
* cacheWriteTokens}`. Errors are not JSON: stderr and a non-zero exit.
|
|
2982
|
+
* - Permissions: headless default is the allowlist mode (unlisted actions are
|
|
2983
|
+
* rejected, changes only proposed); bypass is `--force`.
|
|
2984
|
+
* - MCP: no flag; `$HOME/.cursor/mcp.json` (the path ignores
|
|
2985
|
+
* `CURSOR_CONFIG_DIR`), plus `--approve-mcps` — needs a private home.
|
|
2986
|
+
* - Home: `HOME`/`USERPROFILE`, `CURSOR_CONFIG_DIR` and `CURSOR_DATA_DIR`
|
|
2987
|
+
* are pointed into it. Auth: `CURSOR_API_KEY`.
|
|
2988
|
+
*/
|
|
2989
|
+
const cursorAdapter = {
|
|
2990
|
+
name: "cursor",
|
|
2991
|
+
title: "Cursor CLI",
|
|
2992
|
+
command: "cursor-agent",
|
|
2993
|
+
apiKeyEnv: ["CURSOR_API_KEY"],
|
|
2994
|
+
homeEnv: [
|
|
2995
|
+
"HOME",
|
|
2996
|
+
"USERPROFILE",
|
|
2997
|
+
"CURSOR_CONFIG_DIR",
|
|
2998
|
+
"CURSOR_DATA_DIR"
|
|
2999
|
+
],
|
|
3000
|
+
transports: ["argv", "stdin"],
|
|
3001
|
+
effort: false,
|
|
3002
|
+
mcp: true,
|
|
3003
|
+
probe: {
|
|
3004
|
+
help: [["--help"]],
|
|
3005
|
+
version: ["--version"]
|
|
3006
|
+
},
|
|
3007
|
+
verification: {
|
|
3008
|
+
version: "2026.10.16-e3cb207",
|
|
3009
|
+
verifiedBy: "docs",
|
|
3010
|
+
sources: [
|
|
3011
|
+
"https://cursor.com/docs/cli/headless",
|
|
3012
|
+
"https://cursor.com/docs/cli/reference/output-format",
|
|
3013
|
+
"https://cursor.com/docs/cli/reference/parameters",
|
|
3014
|
+
"https://cursor.com/docs/cli/mcp"
|
|
3015
|
+
],
|
|
3016
|
+
checkedAt: "2026-10-17"
|
|
3017
|
+
},
|
|
3018
|
+
plan(context) {
|
|
3019
|
+
const args = [
|
|
3020
|
+
"--print",
|
|
3021
|
+
"--output-format",
|
|
3022
|
+
"stream-json",
|
|
3023
|
+
"--trust"
|
|
3024
|
+
];
|
|
3025
|
+
const requires = [
|
|
3026
|
+
"--print",
|
|
3027
|
+
"--output-format",
|
|
3028
|
+
"stream-json",
|
|
3029
|
+
"--trust"
|
|
3030
|
+
];
|
|
3031
|
+
const files = [];
|
|
3032
|
+
if (context.model !== void 0) {
|
|
3033
|
+
args.push("--model", context.model);
|
|
3034
|
+
requires.push("--model");
|
|
3035
|
+
}
|
|
3036
|
+
if (context.resume !== void 0) {
|
|
3037
|
+
args.push("--resume", context.resume);
|
|
3038
|
+
requires.push("--resume");
|
|
3039
|
+
}
|
|
3040
|
+
if (context.permission === "bypass") {
|
|
3041
|
+
args.push("--force");
|
|
3042
|
+
requires.push("--force");
|
|
3043
|
+
}
|
|
3044
|
+
if (context.mcp !== void 0) {
|
|
3045
|
+
const home = requirePrivateHome(context, cursorAdapter, "MCP servers");
|
|
3046
|
+
files.push({
|
|
3047
|
+
path: node_path.default.join(home, ".cursor", "mcp.json"),
|
|
3048
|
+
content: jsonFile({ mcpServers: mcpServersJson(context.mcp, { typeField: true }) })
|
|
3049
|
+
});
|
|
3050
|
+
args.push("--approve-mcps");
|
|
3051
|
+
requires.push("--approve-mcps");
|
|
3052
|
+
}
|
|
3053
|
+
const env = context.home !== void 0 ? {
|
|
3054
|
+
HOME: context.home,
|
|
3055
|
+
USERPROFILE: context.home,
|
|
3056
|
+
CURSOR_CONFIG_DIR: node_path.default.join(context.home, ".cursor"),
|
|
3057
|
+
CURSOR_DATA_DIR: node_path.default.join(context.home, ".cursor")
|
|
3058
|
+
} : {};
|
|
3059
|
+
return {
|
|
3060
|
+
args,
|
|
3061
|
+
tail: context.transport === "argv" ? [argvPrompt(context.prompt)] : [],
|
|
3062
|
+
stdin: context.transport === "stdin" ? context.prompt : void 0,
|
|
3063
|
+
env,
|
|
3064
|
+
files,
|
|
3065
|
+
requires
|
|
3066
|
+
};
|
|
3067
|
+
},
|
|
3068
|
+
parser: createCursorParser
|
|
3069
|
+
};
|
|
3070
|
+
/** `readToolCall` → `read`; the documented `function{name}` fallback is used as-is. */
|
|
3071
|
+
function toolOf(toolCall) {
|
|
3072
|
+
const fn = recordField(toolCall, "function");
|
|
3073
|
+
if (fn !== void 0) return {
|
|
3074
|
+
name: stringField(fn, "name") ?? "function",
|
|
3075
|
+
body: fn
|
|
3076
|
+
};
|
|
3077
|
+
const key = Object.keys(toolCall ?? {})[0] ?? "tool";
|
|
3078
|
+
return {
|
|
3079
|
+
name: key.replace(/ToolCall$/, ""),
|
|
3080
|
+
body: recordField(toolCall, key)
|
|
3081
|
+
};
|
|
3082
|
+
}
|
|
3083
|
+
/** Parser for `cursor-agent -p --output-format stream-json`. */
|
|
3084
|
+
function createCursorParser() {
|
|
3085
|
+
return jsonLinesParser((record) => {
|
|
3086
|
+
switch (stringField(record, "type")) {
|
|
3087
|
+
case "system": return stringField(record, "subtype") === "init" ? [{
|
|
3088
|
+
type: "session.started",
|
|
3089
|
+
sessionRef: stringField(record, "session_id"),
|
|
3090
|
+
model: stringField(record, "model"),
|
|
3091
|
+
raw: record
|
|
3092
|
+
}] : [];
|
|
3093
|
+
case "assistant": {
|
|
3094
|
+
const text = asArray(recordField(record, "message")?.content).map((block) => stringField(asRecord(block), "text") ?? "").join("");
|
|
3095
|
+
return text.length > 0 ? [{
|
|
3096
|
+
type: "message",
|
|
3097
|
+
text,
|
|
3098
|
+
delta: false,
|
|
3099
|
+
raw: record
|
|
3100
|
+
}] : [];
|
|
3101
|
+
}
|
|
3102
|
+
case "tool_call": {
|
|
3103
|
+
const id = stringField(record, "call_id") ?? "";
|
|
3104
|
+
const { name, body } = toolOf(recordField(record, "tool_call"));
|
|
3105
|
+
if (stringField(record, "subtype") === "started") return [{
|
|
3106
|
+
type: "tool.call",
|
|
3107
|
+
id,
|
|
3108
|
+
name,
|
|
3109
|
+
input: body?.args ?? parseArguments(body?.arguments),
|
|
3110
|
+
raw: record
|
|
3111
|
+
}];
|
|
3112
|
+
if (stringField(record, "subtype") === "completed") {
|
|
3113
|
+
const result = recordField(body, "result");
|
|
3114
|
+
const success = recordField(result, "success");
|
|
3115
|
+
const content = stringField(success, "content");
|
|
3116
|
+
return [{
|
|
3117
|
+
type: "tool.result",
|
|
3118
|
+
id,
|
|
3119
|
+
output: success !== void 0 ? content ?? outputText(success) : outputText(result),
|
|
3120
|
+
isError: success === void 0,
|
|
3121
|
+
raw: record
|
|
3122
|
+
}];
|
|
3123
|
+
}
|
|
3124
|
+
return [];
|
|
3125
|
+
}
|
|
3126
|
+
case "result": {
|
|
3127
|
+
const events = [];
|
|
3128
|
+
const usage = recordField(record, "usage");
|
|
3129
|
+
if (usage !== void 0) {
|
|
3130
|
+
const cacheRead = numberField(usage, "cacheReadTokens") ?? 0;
|
|
3131
|
+
const cacheWrite = numberField(usage, "cacheWriteTokens") ?? 0;
|
|
3132
|
+
events.push({
|
|
3133
|
+
type: "usage",
|
|
3134
|
+
cumulative: true,
|
|
3135
|
+
usage: usageOf({
|
|
3136
|
+
input: (numberField(usage, "inputTokens") ?? 0) + cacheRead + cacheWrite,
|
|
3137
|
+
output: numberField(usage, "outputTokens"),
|
|
3138
|
+
cachedTokens: cacheRead > 0 ? cacheRead : void 0,
|
|
3139
|
+
cacheWriteTokens: cacheWrite > 0 ? cacheWrite : void 0
|
|
3140
|
+
}),
|
|
3141
|
+
raw: record
|
|
3142
|
+
});
|
|
3143
|
+
}
|
|
3144
|
+
if (record.is_error === true) events.push({
|
|
3145
|
+
type: "error",
|
|
3146
|
+
message: stringField(record, "result") || "Cursor CLI reported an error",
|
|
3147
|
+
fatal: true,
|
|
3148
|
+
raw: record
|
|
3149
|
+
});
|
|
3150
|
+
return events;
|
|
3151
|
+
}
|
|
3152
|
+
default: return [];
|
|
3153
|
+
}
|
|
3154
|
+
});
|
|
3155
|
+
}
|
|
3156
|
+
|
|
3157
|
+
//#endregion
|
|
3158
|
+
//#region ../../packages/ai-cli/src/adapters/droid.ts
|
|
3159
|
+
/**
|
|
3160
|
+
* Factory Droid (`droid`, npm `droid` / `@factory/cli`).
|
|
3161
|
+
*
|
|
3162
|
+
* Verified against the real `droid exec --help` of 0.244.0, a real
|
|
3163
|
+
* `droid exec` run without credentials (init and error lines), the field
|
|
3164
|
+
* names in the shipped code and the docs (`docs.factory.ai/cli/droid-exec`,
|
|
3165
|
+
* `/cli/configuration/mcp`), 2026-10-17.
|
|
3166
|
+
*
|
|
3167
|
+
* - Headless: `droid exec -o stream-json <prompt>` (or `-f <file>`).
|
|
3168
|
+
* - Stream: `system/init` (`session_id`, `model`), whole `message` records
|
|
3169
|
+
* (`role`, `text`), `tool_call` (`id`, `toolName`, `parameters`),
|
|
3170
|
+
* `tool_result` (`id`, `isError`, `value` | `error.message`), a final
|
|
3171
|
+
* `completion` (`finalText`, `usage` with Anthropic-style
|
|
3172
|
+
* `input_tokens`/`cache_read_input_tokens`/`cache_creation_input_tokens`,
|
|
3173
|
+
* read with Anthropic semantics) and `error` (`source`, `message`).
|
|
3174
|
+
* Billing is in Factory credits; no USD cost is reported.
|
|
3175
|
+
* - Permissions: `standard` is Droid's default **read-only** autonomy; raise
|
|
3176
|
+
* it with `args: ["--auto", "medium"]` (low | medium | high). Bypass is
|
|
3177
|
+
* `--skip-permissions-unsafe`. Blocked actions end the run non-zero.
|
|
3178
|
+
* - MCP: `<home>/.factory/mcp.json` (`mcpServers`); the home is relocated
|
|
3179
|
+
* with `FACTORY_HOME_OVERRIDE` (found in the shipped code and confirmed
|
|
3180
|
+
* with `droid mcp list`), so per-run MCP needs a private home.
|
|
3181
|
+
* - Auth: `FACTORY_API_KEY`.
|
|
3182
|
+
*/
|
|
3183
|
+
const droidAdapter = {
|
|
3184
|
+
name: "droid",
|
|
3185
|
+
title: "Factory Droid",
|
|
3186
|
+
command: "droid",
|
|
3187
|
+
apiKeyEnv: ["FACTORY_API_KEY"],
|
|
3188
|
+
homeEnv: ["FACTORY_HOME_OVERRIDE"],
|
|
3189
|
+
transports: ["argv", "file"],
|
|
3190
|
+
effort: true,
|
|
3191
|
+
mcp: true,
|
|
3192
|
+
probe: {
|
|
3193
|
+
help: [["exec", "--help"]],
|
|
3194
|
+
version: ["--version"]
|
|
3195
|
+
},
|
|
3196
|
+
verification: {
|
|
3197
|
+
version: "0.244.0",
|
|
3198
|
+
verifiedBy: "help",
|
|
3199
|
+
sources: [
|
|
3200
|
+
"https://docs.factory.ai/cli/droid-exec/overview",
|
|
3201
|
+
"https://docs.factory.ai/cli/configuration/mcp",
|
|
3202
|
+
"https://www.npmjs.com/package/droid"
|
|
3203
|
+
],
|
|
3204
|
+
checkedAt: "2026-10-17"
|
|
3205
|
+
},
|
|
3206
|
+
plan(context) {
|
|
3207
|
+
const args = [
|
|
3208
|
+
"exec",
|
|
3209
|
+
"--output-format",
|
|
3210
|
+
"stream-json",
|
|
3211
|
+
"--cwd",
|
|
3212
|
+
context.cwd
|
|
3213
|
+
];
|
|
3214
|
+
const requires = [
|
|
3215
|
+
"--output-format",
|
|
3216
|
+
"stream-json",
|
|
3217
|
+
"--cwd"
|
|
3218
|
+
];
|
|
3219
|
+
const files = [];
|
|
3220
|
+
let tail;
|
|
3221
|
+
if (context.model !== void 0) {
|
|
3222
|
+
args.push("--model", context.model);
|
|
3223
|
+
requires.push("--model");
|
|
3224
|
+
}
|
|
3225
|
+
if (context.effort !== void 0) {
|
|
3226
|
+
args.push("--reasoning-effort", context.effort);
|
|
3227
|
+
requires.push("--reasoning-effort");
|
|
3228
|
+
}
|
|
3229
|
+
if (context.resume !== void 0) {
|
|
3230
|
+
args.push("--session-id", context.resume);
|
|
3231
|
+
requires.push("--session-id");
|
|
3232
|
+
}
|
|
3233
|
+
if (context.permission === "bypass") {
|
|
3234
|
+
args.push("--skip-permissions-unsafe");
|
|
3235
|
+
requires.push("--skip-permissions-unsafe");
|
|
3236
|
+
}
|
|
3237
|
+
if (context.mcp !== void 0) {
|
|
3238
|
+
const home = requirePrivateHome(context, droidAdapter, "MCP servers");
|
|
3239
|
+
files.push({
|
|
3240
|
+
path: node_path.default.join(home, ".factory", "mcp.json"),
|
|
3241
|
+
content: jsonFile({ mcpServers: mcpServersJson(context.mcp, { typeField: true }) })
|
|
3242
|
+
});
|
|
3243
|
+
}
|
|
3244
|
+
if (context.transport === "file") {
|
|
3245
|
+
const prompt = promptFile(context);
|
|
3246
|
+
files.push(prompt);
|
|
3247
|
+
tail = ["--file", prompt.path];
|
|
3248
|
+
requires.push("--file");
|
|
3249
|
+
} else tail = [argvPrompt(context.prompt)];
|
|
3250
|
+
return {
|
|
3251
|
+
args,
|
|
3252
|
+
tail,
|
|
3253
|
+
env: {
|
|
3254
|
+
...context.home !== void 0 ? { FACTORY_HOME_OVERRIDE: context.home } : {},
|
|
3255
|
+
FACTORY_DROID_AUTO_UPDATE_ENABLED: "false"
|
|
3256
|
+
},
|
|
3257
|
+
files,
|
|
3258
|
+
requires
|
|
3259
|
+
};
|
|
3260
|
+
},
|
|
3261
|
+
parser: createDroidParser
|
|
3262
|
+
};
|
|
3263
|
+
/** Parser for `droid exec -o stream-json`. */
|
|
3264
|
+
function createDroidParser() {
|
|
3265
|
+
return jsonLinesParser((record) => {
|
|
3266
|
+
switch (stringField(record, "type")) {
|
|
3267
|
+
case "system": return stringField(record, "subtype") === "init" ? [{
|
|
3268
|
+
type: "session.started",
|
|
3269
|
+
sessionRef: stringField(record, "session_id"),
|
|
3270
|
+
model: stringField(record, "model"),
|
|
3271
|
+
raw: record
|
|
3272
|
+
}] : [];
|
|
3273
|
+
case "message": {
|
|
3274
|
+
const text = stringField(record, "text") ?? "";
|
|
3275
|
+
return stringField(record, "role") === "assistant" && text.length > 0 ? [{
|
|
3276
|
+
type: "message",
|
|
3277
|
+
text,
|
|
3278
|
+
delta: false,
|
|
3279
|
+
raw: record
|
|
3280
|
+
}] : [];
|
|
3281
|
+
}
|
|
3282
|
+
case "tool_call": return [{
|
|
3283
|
+
type: "tool.call",
|
|
3284
|
+
id: stringField(record, "id") ?? "",
|
|
3285
|
+
name: stringField(record, "toolName") ?? stringField(record, "toolId") ?? "",
|
|
3286
|
+
input: record.parameters,
|
|
3287
|
+
raw: record
|
|
3288
|
+
}];
|
|
3289
|
+
case "tool_result": {
|
|
3290
|
+
const error = recordField(record, "error");
|
|
3291
|
+
const isError = record.isError === true || error !== void 0;
|
|
3292
|
+
return [{
|
|
3293
|
+
type: "tool.result",
|
|
3294
|
+
id: stringField(record, "id") ?? "",
|
|
3295
|
+
output: isError && error !== void 0 ? stringField(error, "message") ?? "" : outputText(record.value),
|
|
3296
|
+
isError,
|
|
3297
|
+
raw: record
|
|
3298
|
+
}];
|
|
3299
|
+
}
|
|
3300
|
+
case "completion": {
|
|
3301
|
+
const events = [];
|
|
3302
|
+
const usage = anthropicUsage(recordField(record, "usage"));
|
|
3303
|
+
if (usage !== void 0) {
|
|
3304
|
+
const thinking = recordField(record, "usage")?.thinking_tokens;
|
|
3305
|
+
if (typeof thinking === "number" && thinking > 0) usage.reasoningTokens = thinking;
|
|
3306
|
+
events.push({
|
|
3307
|
+
type: "usage",
|
|
3308
|
+
usage,
|
|
3309
|
+
cumulative: true,
|
|
3310
|
+
raw: record
|
|
3311
|
+
});
|
|
3312
|
+
}
|
|
3313
|
+
const text = stringField(record, "finalText") ?? "";
|
|
3314
|
+
if (text.length > 0) events.push({
|
|
3315
|
+
type: "message",
|
|
3316
|
+
text,
|
|
3317
|
+
delta: false,
|
|
3318
|
+
final: true,
|
|
3319
|
+
raw: record
|
|
3320
|
+
});
|
|
3321
|
+
return events;
|
|
3322
|
+
}
|
|
3323
|
+
case "error": return [{
|
|
3324
|
+
type: "error",
|
|
3325
|
+
message: stringField(record, "message") ?? "Droid reported an error",
|
|
3326
|
+
fatal: true,
|
|
3327
|
+
raw: record
|
|
3328
|
+
}];
|
|
3329
|
+
default: return [];
|
|
3330
|
+
}
|
|
3331
|
+
});
|
|
3332
|
+
}
|
|
3333
|
+
|
|
3334
|
+
//#endregion
|
|
3335
|
+
//#region ../../packages/ai-cli/src/adapters/grok.ts
|
|
3336
|
+
/**
|
|
3337
|
+
* Grok Build (`grok`) — xAI's official coding-agent CLI, installed by the
|
|
3338
|
+
* `x.ai/cli/install.sh` script (no npm package).
|
|
3339
|
+
*
|
|
3340
|
+
* Verified against the docs and source only (the binary is distributed by
|
|
3341
|
+
* a curl installer, which was not run): the repo user guide
|
|
3342
|
+
* `crates/codegen/xai-grok-pager/docs/user-guide/{14-headless-mode,
|
|
3343
|
+
* 07-mcp-servers,05-configuration,22-permissions-and-safety}.md`, the clap
|
|
3344
|
+
* definitions in `xai-grok-pager/src/app/cli.rs` and the headless reducer
|
|
3345
|
+
* `headless/reducer/acp.rs`, at source version 1.0.45; latest stable 1.0.62
|
|
3346
|
+
* (2026-10-17). The capability probe checks every flag against the
|
|
3347
|
+
* installed binary's `--help` before the first run.
|
|
3348
|
+
*
|
|
3349
|
+
* - Headless: `grok -p <prompt>` or `--prompt-file <path>`; piped stdin is
|
|
3350
|
+
* not read. `--output-format streaming-json` prints `type`-tagged lines:
|
|
3351
|
+
* `text`, `thought`, `tool_call`, `tool_call_update`, `usage` (per model
|
|
3352
|
+
* response, `input_tokens` uncached) and a final `end` (session id, totals,
|
|
3353
|
+
* `total_cost_usd`); failures print `{"type":"error","message"}`.
|
|
3354
|
+
* - Permissions: headless mode answers every approval request with
|
|
3355
|
+
* "cancelled" (auto-deny); bypass is `--always-approve`.
|
|
3356
|
+
* - MCP: no flag; `[mcp_servers.<name>]` in `$GROK_HOME/config.toml`.
|
|
3357
|
+
* - Auth: `XAI_API_KEY`. Update checks are disabled with
|
|
3358
|
+
* `GROK_DISABLE_AUTOUPDATER=1`.
|
|
3359
|
+
*/
|
|
3360
|
+
const grokAdapter = {
|
|
3361
|
+
name: "grok",
|
|
3362
|
+
title: "Grok Build",
|
|
3363
|
+
command: "grok",
|
|
3364
|
+
apiKeyEnv: ["XAI_API_KEY"],
|
|
3365
|
+
homeEnv: ["GROK_HOME"],
|
|
3366
|
+
transports: ["argv", "file"],
|
|
3367
|
+
effort: true,
|
|
3368
|
+
mcp: true,
|
|
3369
|
+
probe: {
|
|
3370
|
+
help: [["--help"]],
|
|
3371
|
+
version: ["--version"]
|
|
3372
|
+
},
|
|
3373
|
+
verification: {
|
|
3374
|
+
version: "1.0.62",
|
|
3375
|
+
verifiedBy: "docs",
|
|
3376
|
+
sources: [
|
|
3377
|
+
"https://docs.x.ai/build/cli/headless-scripting",
|
|
3378
|
+
"https://github.com/xai-org/grok-build/blob/main/crates/codegen/xai-grok-pager/docs/user-guide/14-headless-mode.md",
|
|
3379
|
+
"https://github.com/xai-org/grok-build/blob/main/crates/codegen/xai-grok-pager/docs/user-guide/07-mcp-servers.md"
|
|
3380
|
+
],
|
|
3381
|
+
checkedAt: "2026-10-17"
|
|
3382
|
+
},
|
|
3383
|
+
plan(context) {
|
|
3384
|
+
const args = ["--output-format", "streaming-json"];
|
|
3385
|
+
const requires = ["--output-format", "streaming-json"];
|
|
3386
|
+
const files = [];
|
|
3387
|
+
let tail;
|
|
3388
|
+
if (context.model !== void 0) {
|
|
3389
|
+
args.push("--model", context.model);
|
|
3390
|
+
requires.push("--model");
|
|
3391
|
+
}
|
|
3392
|
+
if (context.effort !== void 0) {
|
|
3393
|
+
args.push("--reasoning-effort", context.effort);
|
|
3394
|
+
requires.push("--reasoning-effort");
|
|
3395
|
+
}
|
|
3396
|
+
if (context.resume !== void 0) {
|
|
3397
|
+
args.push("--resume", context.resume);
|
|
3398
|
+
requires.push("--resume");
|
|
3399
|
+
}
|
|
3400
|
+
if (context.permission === "bypass") {
|
|
3401
|
+
args.push("--always-approve");
|
|
3402
|
+
requires.push("--always-approve");
|
|
3403
|
+
}
|
|
3404
|
+
if (context.mcp !== void 0) {
|
|
3405
|
+
const home = requirePrivateHome(context, grokAdapter, "MCP servers");
|
|
3406
|
+
files.push({
|
|
3407
|
+
path: node_path.default.join(home, "config.toml"),
|
|
3408
|
+
content: toToml({ mcp_servers: grokMcp(context.mcp) })
|
|
3409
|
+
});
|
|
3410
|
+
}
|
|
3411
|
+
if (context.transport === "file") {
|
|
3412
|
+
const prompt = promptFile(context);
|
|
3413
|
+
files.push(prompt);
|
|
3414
|
+
tail = ["--prompt-file", prompt.path];
|
|
3415
|
+
requires.push("--prompt-file");
|
|
3416
|
+
} else {
|
|
3417
|
+
tail = ["-p", argvPrompt(context.prompt)];
|
|
3418
|
+
requires.push("-p");
|
|
3419
|
+
}
|
|
3420
|
+
return {
|
|
3421
|
+
args,
|
|
3422
|
+
tail,
|
|
3423
|
+
env: {
|
|
3424
|
+
...context.home !== void 0 ? { GROK_HOME: context.home } : {},
|
|
3425
|
+
GROK_DISABLE_AUTOUPDATER: "1"
|
|
3426
|
+
},
|
|
3427
|
+
files,
|
|
3428
|
+
requires
|
|
3429
|
+
};
|
|
3430
|
+
},
|
|
3431
|
+
parser: createGrokParser
|
|
3432
|
+
};
|
|
3433
|
+
function grokMcp(servers) {
|
|
3434
|
+
const table = {};
|
|
3435
|
+
for (const [name, server] of Object.entries(servers)) {
|
|
3436
|
+
if (server.type === "sse") throw unsupportedOption(grokAdapter, `MCP server "${name}" uses SSE; Grok Build supports stdio and HTTP servers.`);
|
|
3437
|
+
table[name] = server.type === "stdio" ? {
|
|
3438
|
+
command: server.command,
|
|
3439
|
+
args: server.args ?? [],
|
|
3440
|
+
env: server.env
|
|
3441
|
+
} : {
|
|
3442
|
+
url: server.url,
|
|
3443
|
+
headers: server.headers
|
|
3444
|
+
};
|
|
3445
|
+
}
|
|
3446
|
+
return table;
|
|
3447
|
+
}
|
|
3448
|
+
/** Parser for `grok -p … --output-format streaming-json`. */
|
|
3449
|
+
function createGrokParser() {
|
|
3450
|
+
let started = false;
|
|
3451
|
+
let sessionRef;
|
|
3452
|
+
return {
|
|
3453
|
+
...jsonLinesParser((record) => {
|
|
3454
|
+
const events = [];
|
|
3455
|
+
if (!started) {
|
|
3456
|
+
started = true;
|
|
3457
|
+
events.push({ type: "session.started" });
|
|
3458
|
+
}
|
|
3459
|
+
switch (stringField(record, "type")) {
|
|
3460
|
+
case "text": {
|
|
3461
|
+
const text = stringField(record, "data") ?? "";
|
|
3462
|
+
if (text.length > 0) events.push({
|
|
3463
|
+
type: "message",
|
|
3464
|
+
text,
|
|
3465
|
+
delta: true,
|
|
3466
|
+
raw: record
|
|
3467
|
+
});
|
|
3468
|
+
break;
|
|
3469
|
+
}
|
|
3470
|
+
case "tool_call":
|
|
3471
|
+
events.push({
|
|
3472
|
+
type: "tool.call",
|
|
3473
|
+
id: stringField(record, "toolCallId") ?? "",
|
|
3474
|
+
name: stringField(record, "toolName") ?? stringField(record, "title") ?? "",
|
|
3475
|
+
input: record.rawInput,
|
|
3476
|
+
raw: record
|
|
3477
|
+
});
|
|
3478
|
+
break;
|
|
3479
|
+
case "tool_call_update": {
|
|
3480
|
+
const status = stringField(record, "status");
|
|
3481
|
+
if (status === "completed" || status === "failed") events.push({
|
|
3482
|
+
type: "tool.result",
|
|
3483
|
+
id: stringField(record, "toolCallId") ?? "",
|
|
3484
|
+
output: outputText(record.rawOutput ?? record.content),
|
|
3485
|
+
isError: status === "failed",
|
|
3486
|
+
raw: record
|
|
3487
|
+
});
|
|
3488
|
+
break;
|
|
3489
|
+
}
|
|
3490
|
+
case "usage": {
|
|
3491
|
+
const usage = anthropicUsage(recordField(record, "usage"));
|
|
3492
|
+
if (usage !== void 0) events.push({
|
|
3493
|
+
type: "usage",
|
|
3494
|
+
usage,
|
|
3495
|
+
cumulative: false,
|
|
3496
|
+
raw: record
|
|
3497
|
+
});
|
|
3498
|
+
break;
|
|
3499
|
+
}
|
|
3500
|
+
case "end": {
|
|
3501
|
+
sessionRef = stringField(record, "sessionId") ?? sessionRef;
|
|
3502
|
+
const usage = anthropicUsage(recordField(record, "usage"), numberField(record, "total_cost_usd"));
|
|
3503
|
+
if (usage !== void 0) events.push({
|
|
3504
|
+
type: "usage",
|
|
3505
|
+
usage,
|
|
3506
|
+
cumulative: true,
|
|
3507
|
+
raw: record
|
|
3508
|
+
});
|
|
3509
|
+
break;
|
|
3510
|
+
}
|
|
3511
|
+
case "error":
|
|
3512
|
+
events.push({
|
|
3513
|
+
type: "error",
|
|
3514
|
+
message: stringField(record, "message") ?? "Grok Build reported an error",
|
|
3515
|
+
fatal: true,
|
|
3516
|
+
raw: record
|
|
3517
|
+
});
|
|
3518
|
+
break;
|
|
3519
|
+
}
|
|
3520
|
+
return events;
|
|
3521
|
+
}),
|
|
3522
|
+
sessionRef: () => sessionRef
|
|
3523
|
+
};
|
|
3524
|
+
}
|
|
3525
|
+
|
|
3526
|
+
//#endregion
|
|
3527
|
+
//#region ../../packages/ai-cli/src/adapters/kimi.ts
|
|
3528
|
+
/**
|
|
3529
|
+
* Kimi Code CLI (`kimi`, npm `@moonshot-ai/kimi-code`) — Moonshot AI's
|
|
3530
|
+
* coding agent, the successor of the archived Python `kimi-cli`.
|
|
3531
|
+
*
|
|
3532
|
+
* Verified against the real `kimi --help` of 2.2.0 and the docs/source at
|
|
3533
|
+
* that tag (`docs/en/reference/kimi-command.md`, `apps/kimi-code/src/cli/
|
|
3534
|
+
* {options.ts,prompt-render.ts,v2/run-v2-print.ts}`), 2026-10-17.
|
|
3535
|
+
*
|
|
3536
|
+
* - Headless: `kimi -p <prompt> --output-format stream-json`; the prompt is
|
|
3537
|
+
* an argv value only (print mode never reads stdin).
|
|
3538
|
+
* - Stream: OpenAI-chat-shaped messages, one per line, never deltas:
|
|
3539
|
+
* `{"role":"assistant","content","tool_calls"}`, `{"role":"tool",
|
|
3540
|
+
* "tool_call_id","content"}`, and `{"role":"meta","type":…}` records. The
|
|
3541
|
+
* session id appears only in the final `session.resume_hint` record.
|
|
3542
|
+
* No token usage is printed.
|
|
3543
|
+
* - Permissions: print mode always runs in "Never Ask" mode (every tool call
|
|
3544
|
+
* runs; only static deny rules apply) and rejects `--yolo`/`--plan`, so
|
|
3545
|
+
* there is no standard mode to choose. The adapter refuses
|
|
3546
|
+
* `permission: "standard"` instead of pretending to enforce it; pass
|
|
3547
|
+
* `"bypass"` to accept the CLI's behaviour.
|
|
3548
|
+
* - MCP: no flag; `$KIMI_CODE_HOME/mcp.json` (`mcpServers`), so per-run MCP
|
|
3549
|
+
* needs a private home.
|
|
3550
|
+
* - Auth: shell `KIMI_API_KEY`/`MOONSHOT_API_KEY` are not read. For an API
|
|
3551
|
+
* key set `KIMI_MODEL_NAME` (and optionally `KIMI_MODEL_PROVIDER_TYPE`,
|
|
3552
|
+
* `KIMI_MODEL_BASE_URL`) in `env`; `apiKey` becomes `KIMI_MODEL_API_KEY`.
|
|
3553
|
+
*/
|
|
3554
|
+
const kimiAdapter = {
|
|
3555
|
+
name: "kimi",
|
|
3556
|
+
title: "Kimi Code",
|
|
3557
|
+
command: "kimi",
|
|
3558
|
+
apiKeyEnv: ["KIMI_MODEL_API_KEY"],
|
|
3559
|
+
homeEnv: ["KIMI_CODE_HOME"],
|
|
3560
|
+
transports: ["argv"],
|
|
3561
|
+
effort: false,
|
|
3562
|
+
mcp: true,
|
|
3563
|
+
probe: {
|
|
3564
|
+
help: [["--help"]],
|
|
3565
|
+
version: ["--version"]
|
|
3566
|
+
},
|
|
3567
|
+
verification: {
|
|
3568
|
+
version: "2.2.0",
|
|
3569
|
+
verifiedBy: "help",
|
|
3570
|
+
sources: [
|
|
3571
|
+
"https://moonshotai.github.io/kimi-code/",
|
|
3572
|
+
"https://github.com/MoonshotAI/kimi-code/blob/%40moonshot-ai/kimi-code%402.2.0/docs/en/reference/kimi-command.md",
|
|
3573
|
+
"https://github.com/MoonshotAI/kimi-code/blob/%40moonshot-ai/kimi-code%402.2.0/docs/en/customization/mcp.md"
|
|
3574
|
+
],
|
|
3575
|
+
checkedAt: "2026-10-17"
|
|
3576
|
+
},
|
|
3577
|
+
plan(context) {
|
|
3578
|
+
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.");
|
|
3579
|
+
const args = ["--output-format", "stream-json"];
|
|
3580
|
+
const requires = [
|
|
3581
|
+
"-p",
|
|
3582
|
+
"--output-format",
|
|
3583
|
+
"stream-json"
|
|
3584
|
+
];
|
|
3585
|
+
const files = [];
|
|
3586
|
+
if (context.model !== void 0) {
|
|
3587
|
+
args.push("--model", context.model);
|
|
3588
|
+
requires.push("--model");
|
|
3589
|
+
}
|
|
3590
|
+
if (context.resume !== void 0) {
|
|
3591
|
+
args.push("--session", context.resume);
|
|
3592
|
+
requires.push("--session");
|
|
3593
|
+
}
|
|
3594
|
+
if (context.mcp !== void 0) {
|
|
3595
|
+
const home = requirePrivateHome(context, kimiAdapter, "MCP servers");
|
|
3596
|
+
files.push({
|
|
3597
|
+
path: node_path.default.join(home, "mcp.json"),
|
|
3598
|
+
content: jsonFile({ mcpServers: kimiMcp(context.mcp) })
|
|
3599
|
+
});
|
|
3600
|
+
}
|
|
3601
|
+
return {
|
|
3602
|
+
args,
|
|
3603
|
+
tail: ["-p", argvPrompt(context.prompt)],
|
|
3604
|
+
env: {
|
|
3605
|
+
...context.home !== void 0 ? { KIMI_CODE_HOME: context.home } : {},
|
|
3606
|
+
KIMI_CODE_NO_AUTO_UPDATE: "1",
|
|
3607
|
+
KIMI_DISABLE_TELEMETRY: "1"
|
|
3608
|
+
},
|
|
3609
|
+
files,
|
|
3610
|
+
requires
|
|
3611
|
+
};
|
|
3612
|
+
},
|
|
3613
|
+
parser: createKimiParser
|
|
3614
|
+
};
|
|
3615
|
+
/** Kimi's `mcpServers`: SSE servers are marked with `transport: "sse"`. */
|
|
3616
|
+
function kimiMcp(servers) {
|
|
3617
|
+
const rendered = mcpServersJson(servers);
|
|
3618
|
+
for (const [name, server] of Object.entries(servers)) {
|
|
3619
|
+
const entry = rendered[name];
|
|
3620
|
+
if (server.type === "sse" && entry !== void 0) entry.transport = "sse";
|
|
3621
|
+
}
|
|
3622
|
+
return rendered;
|
|
3623
|
+
}
|
|
3624
|
+
/** Parser for `kimi -p … --output-format stream-json`. */
|
|
3625
|
+
function createKimiParser() {
|
|
3626
|
+
let sessionRef;
|
|
3627
|
+
return {
|
|
3628
|
+
...jsonLinesParser((record) => {
|
|
3629
|
+
const role = stringField(record, "role");
|
|
3630
|
+
if (role === "meta") {
|
|
3631
|
+
const type = stringField(record, "type");
|
|
3632
|
+
if (type === "system.version") return [{
|
|
3633
|
+
type: "session.started",
|
|
3634
|
+
raw: record
|
|
3635
|
+
}];
|
|
3636
|
+
if (type === "session.resume_hint") {
|
|
3637
|
+
sessionRef = stringField(record, "session_id");
|
|
3638
|
+
return [];
|
|
3639
|
+
}
|
|
3640
|
+
if (type === "turn.step.retrying") return [{
|
|
3641
|
+
type: "error",
|
|
3642
|
+
message: `retrying after ${stringField(record, "error_name") ?? "error"}: ${stringField(record, "error_message") ?? ""}`,
|
|
3643
|
+
raw: record
|
|
3644
|
+
}];
|
|
3645
|
+
return [];
|
|
3646
|
+
}
|
|
3647
|
+
if (role === "assistant") {
|
|
3648
|
+
const events = [];
|
|
3649
|
+
const content = stringField(record, "content");
|
|
3650
|
+
if (content !== void 0 && content.length > 0) events.push({
|
|
3651
|
+
type: "message",
|
|
3652
|
+
text: content,
|
|
3653
|
+
delta: false,
|
|
3654
|
+
raw: record
|
|
3655
|
+
});
|
|
3656
|
+
for (const item of asArray(record.tool_calls)) {
|
|
3657
|
+
const call = asRecord(item);
|
|
3658
|
+
const fn = asRecord(call?.function);
|
|
3659
|
+
events.push({
|
|
3660
|
+
type: "tool.call",
|
|
3661
|
+
id: stringField(call, "id") ?? "",
|
|
3662
|
+
name: stringField(fn, "name") ?? "",
|
|
3663
|
+
input: parseArguments(fn?.arguments),
|
|
3664
|
+
raw: item
|
|
3665
|
+
});
|
|
3666
|
+
}
|
|
3667
|
+
return events;
|
|
3668
|
+
}
|
|
3669
|
+
if (role === "tool") return [{
|
|
3670
|
+
type: "tool.result",
|
|
3671
|
+
id: stringField(record, "tool_call_id") ?? "",
|
|
3672
|
+
output: outputText(record.content),
|
|
3673
|
+
isError: false,
|
|
3674
|
+
raw: record
|
|
3675
|
+
}];
|
|
3676
|
+
return [];
|
|
3677
|
+
}),
|
|
3678
|
+
sessionRef: () => sessionRef
|
|
3679
|
+
};
|
|
3680
|
+
}
|
|
3681
|
+
|
|
3682
|
+
//#endregion
|
|
3683
|
+
//#region ../../packages/ai-cli/src/adapters/mistral-vibe.ts
|
|
3684
|
+
/** Folder (inside the run directory) Vibe writes `export.json` to. */
|
|
3685
|
+
const EXPORT_DIR = "vibe-output";
|
|
3686
|
+
/**
|
|
3687
|
+
* Mistral Vibe (`vibe`, PyPI `mistral-vibe`).
|
|
3688
|
+
*
|
|
3689
|
+
* Verified against the official repo at tag v2.26.1 only (README,
|
|
3690
|
+
* `vibe/cli/entrypoint.py`, `vibe/cli/programmatic.py`,
|
|
3691
|
+
* `vibe/app_server/run_export.py`, `vibe/core/config/*`); Vibe needs Python
|
|
3692
|
+
* 3.12, so no real `--help` was run, 2026-10-17. The capability probe checks
|
|
3693
|
+
* every flag against the installed `--help`.
|
|
3694
|
+
*
|
|
3695
|
+
* - Headless: `vibe -p <prompt> --output streaming` (or `--prompt-file`;
|
|
3696
|
+
* `-p` with no value reads stdin). The stream is NDJSON of completed
|
|
3697
|
+
* history entries (camelCase): `message` (`role`, `content[].text`), and
|
|
3698
|
+
* `effect` — a tool call and its result in one line (`detail.toolName`,
|
|
3699
|
+
* `detail.input`, `state.status`, `state.outputText`/`state.error`).
|
|
3700
|
+
* There is no usage, cost or final record in the stream, so the adapter
|
|
3701
|
+
* adds `--output-dir` and reads usage, cost, session id and outcome from
|
|
3702
|
+
* `export.json` when the stream ends.
|
|
3703
|
+
* - Model: no flag; `VIBE_ACTIVE_MODEL=<alias>` (set from `model`). Effort
|
|
3704
|
+
* is a per-model config field, so `effort` is refused.
|
|
3705
|
+
* - Permissions: headless auto-denies every approval (the default agent
|
|
3706
|
+
* only auto-approves file edits); bypass is `--auto-approve`.
|
|
3707
|
+
* - MCP: `[[mcp_servers]]` in `$VIBE_HOME/config.toml` (private home).
|
|
3708
|
+
* - Auth: `MISTRAL_API_KEY`; home `VIBE_HOME`.
|
|
3709
|
+
*/
|
|
3710
|
+
const mistralVibeAdapter = {
|
|
3711
|
+
name: "mistral-vibe",
|
|
3712
|
+
title: "Mistral Vibe",
|
|
3713
|
+
command: "vibe",
|
|
3714
|
+
apiKeyEnv: ["MISTRAL_API_KEY"],
|
|
3715
|
+
homeEnv: ["VIBE_HOME"],
|
|
3716
|
+
transports: [
|
|
3717
|
+
"argv",
|
|
3718
|
+
"file",
|
|
3719
|
+
"stdin"
|
|
3720
|
+
],
|
|
3721
|
+
effort: false,
|
|
3722
|
+
mcp: true,
|
|
3723
|
+
probe: {
|
|
3724
|
+
help: [["--help"]],
|
|
3725
|
+
version: ["--version"]
|
|
3726
|
+
},
|
|
3727
|
+
verification: {
|
|
3728
|
+
version: "2.26.1",
|
|
3729
|
+
verifiedBy: "docs",
|
|
3730
|
+
sources: [
|
|
3731
|
+
"https://github.com/mistralai/mistral-vibe/blob/v2.26.1/README.md",
|
|
3732
|
+
"https://github.com/mistralai/mistral-vibe/blob/v2.26.1/vibe/cli/entrypoint.py",
|
|
3733
|
+
"https://github.com/mistralai/mistral-vibe/blob/v2.26.1/vibe/cli/programmatic.py"
|
|
3734
|
+
],
|
|
3735
|
+
checkedAt: "2026-10-17"
|
|
3736
|
+
},
|
|
3737
|
+
plan(context) {
|
|
3738
|
+
const args = [
|
|
3739
|
+
"--output",
|
|
3740
|
+
"streaming",
|
|
3741
|
+
"--output-dir",
|
|
3742
|
+
node_path.default.join(context.runDir, EXPORT_DIR)
|
|
3743
|
+
];
|
|
3744
|
+
const requires = [
|
|
3745
|
+
"--output",
|
|
3746
|
+
"streaming",
|
|
3747
|
+
"--output-dir"
|
|
3748
|
+
];
|
|
3749
|
+
const files = [];
|
|
3750
|
+
const env = {};
|
|
3751
|
+
if (context.model !== void 0) env.VIBE_ACTIVE_MODEL = context.model;
|
|
3752
|
+
if (context.resume !== void 0) {
|
|
3753
|
+
args.push("--resume", context.resume);
|
|
3754
|
+
requires.push("--resume");
|
|
3755
|
+
}
|
|
3756
|
+
if (context.permission === "bypass") {
|
|
3757
|
+
args.push("--auto-approve");
|
|
3758
|
+
requires.push("--auto-approve");
|
|
3759
|
+
}
|
|
3760
|
+
if (context.mcp !== void 0) {
|
|
3761
|
+
const home = requirePrivateHome(context, mistralVibeAdapter, "MCP servers");
|
|
3762
|
+
files.push({
|
|
3763
|
+
path: node_path.default.join(home, "config.toml"),
|
|
3764
|
+
content: toToml({ mcp_servers: vibeMcp(context.mcp) })
|
|
3765
|
+
});
|
|
3766
|
+
}
|
|
3767
|
+
if (context.home !== void 0) env.VIBE_HOME = context.home;
|
|
3768
|
+
if (context.transport === "file") {
|
|
3769
|
+
const prompt = promptFile(context);
|
|
3770
|
+
files.push(prompt);
|
|
3771
|
+
requires.push("--prompt-file");
|
|
3772
|
+
return {
|
|
3773
|
+
args,
|
|
3774
|
+
tail: ["--prompt-file", prompt.path],
|
|
3775
|
+
env,
|
|
3776
|
+
files,
|
|
3777
|
+
requires
|
|
3778
|
+
};
|
|
3779
|
+
}
|
|
3780
|
+
requires.push("-p");
|
|
3781
|
+
if (context.transport === "stdin") return {
|
|
3782
|
+
args,
|
|
3783
|
+
tail: ["-p"],
|
|
3784
|
+
stdin: context.prompt,
|
|
3785
|
+
env,
|
|
3786
|
+
files,
|
|
3787
|
+
requires
|
|
3788
|
+
};
|
|
3789
|
+
return {
|
|
3790
|
+
args,
|
|
3791
|
+
tail: ["-p", argvPrompt(context.prompt)],
|
|
3792
|
+
env,
|
|
3793
|
+
files,
|
|
3794
|
+
requires
|
|
3795
|
+
};
|
|
3796
|
+
},
|
|
3797
|
+
parser: createVibeParser
|
|
3798
|
+
};
|
|
3799
|
+
function vibeMcp(servers) {
|
|
3800
|
+
return Object.entries(servers).map(([name, server]) => server.type === "stdio" ? {
|
|
3801
|
+
name,
|
|
3802
|
+
transport: "stdio",
|
|
3803
|
+
command: server.command,
|
|
3804
|
+
args: server.args ?? [],
|
|
3805
|
+
env: server.env
|
|
3806
|
+
} : {
|
|
3807
|
+
name,
|
|
3808
|
+
transport: server.type === "http" ? "streamable-http" : "http",
|
|
3809
|
+
url: server.url,
|
|
3810
|
+
auth: server.headers !== void 0 ? {
|
|
3811
|
+
type: "static",
|
|
3812
|
+
headers: server.headers
|
|
3813
|
+
} : void 0
|
|
3814
|
+
});
|
|
3815
|
+
}
|
|
3816
|
+
/** Read Vibe's `export.json`: usage, cost, session id and outcome of the run. */
|
|
3817
|
+
function exportEvents(context) {
|
|
3818
|
+
if (context === void 0) return { events: [] };
|
|
3819
|
+
let report;
|
|
3820
|
+
try {
|
|
3821
|
+
report = parseJsonLine((0, node_fs.readFileSync)(node_path.default.join(context.runDir, EXPORT_DIR, "export.json"), "utf8"));
|
|
3822
|
+
} catch {
|
|
3823
|
+
return { events: [] };
|
|
3824
|
+
}
|
|
3825
|
+
if (report === void 0) return { events: [] };
|
|
3826
|
+
const events = [];
|
|
3827
|
+
const usage = recordField(report, "usage");
|
|
3828
|
+
if (usage !== void 0) {
|
|
3829
|
+
const cached = numberField(usage, "cached_input_tokens");
|
|
3830
|
+
events.push({
|
|
3831
|
+
type: "usage",
|
|
3832
|
+
cumulative: true,
|
|
3833
|
+
usage: usageOf({
|
|
3834
|
+
input: numberField(usage, "input_tokens"),
|
|
3835
|
+
output: numberField(usage, "output_tokens"),
|
|
3836
|
+
total: numberField(usage, "total_tokens"),
|
|
3837
|
+
cachedTokens: cached !== void 0 && cached > 0 ? cached : void 0,
|
|
3838
|
+
costUSD: numberField(report, "cost_usd")
|
|
3839
|
+
}),
|
|
3840
|
+
raw: report
|
|
3841
|
+
});
|
|
3842
|
+
}
|
|
3843
|
+
const outcome = stringField(report, "outcome");
|
|
3844
|
+
if (outcome !== void 0 && outcome !== "finished") events.push({
|
|
3845
|
+
type: "error",
|
|
3846
|
+
message: stringField(recordField(report, "error"), "message") ?? `Mistral Vibe stopped: ${outcome}`,
|
|
3847
|
+
fatal: true,
|
|
3848
|
+
raw: report
|
|
3849
|
+
});
|
|
3850
|
+
return {
|
|
3851
|
+
events,
|
|
3852
|
+
sessionRef: stringField(report, "session_id")
|
|
3853
|
+
};
|
|
3854
|
+
}
|
|
3855
|
+
/** Parser for `vibe -p … --output streaming` (plus the run's `export.json`). */
|
|
3856
|
+
function createVibeParser(context) {
|
|
3857
|
+
let started = false;
|
|
3858
|
+
let sessionRef;
|
|
3859
|
+
return {
|
|
3860
|
+
...jsonLinesParser((record) => {
|
|
3861
|
+
const events = [];
|
|
3862
|
+
const session = stringField(record, "sessionId");
|
|
3863
|
+
if (session !== void 0) sessionRef = session;
|
|
3864
|
+
if (!started) {
|
|
3865
|
+
started = true;
|
|
3866
|
+
events.push({
|
|
3867
|
+
type: "session.started",
|
|
3868
|
+
sessionRef: session,
|
|
3869
|
+
raw: record
|
|
3870
|
+
});
|
|
3871
|
+
}
|
|
3872
|
+
switch (stringField(record, "type")) {
|
|
3873
|
+
case "message": {
|
|
3874
|
+
if (stringField(record, "role") !== "assistant") break;
|
|
3875
|
+
const text = asArray(record.content).map((block) => stringField(asRecord(block), "text") ?? "").join("");
|
|
3876
|
+
if (text.length > 0) events.push({
|
|
3877
|
+
type: "message",
|
|
3878
|
+
text,
|
|
3879
|
+
delta: false,
|
|
3880
|
+
raw: record
|
|
3881
|
+
});
|
|
3882
|
+
break;
|
|
3883
|
+
}
|
|
3884
|
+
case "effect": {
|
|
3885
|
+
const detail = recordField(record, "detail");
|
|
3886
|
+
const state = recordField(record, "state");
|
|
3887
|
+
const status = stringField(state, "status");
|
|
3888
|
+
const id = stringField(record, "id") ?? "";
|
|
3889
|
+
events.push({
|
|
3890
|
+
type: "tool.call",
|
|
3891
|
+
id,
|
|
3892
|
+
name: stringField(detail, "toolName") ?? stringField(record, "title") ?? "",
|
|
3893
|
+
input: detail?.input,
|
|
3894
|
+
raw: record
|
|
3895
|
+
});
|
|
3896
|
+
if (status === "completed" || status === "failed" || status === "cancelled" || status === "skipped") {
|
|
3897
|
+
const error = stringField(recordField(state, "error"), "message");
|
|
3898
|
+
events.push({
|
|
3899
|
+
type: "tool.result",
|
|
3900
|
+
id,
|
|
3901
|
+
output: error ?? stringField(state, "outputText") ?? outputText(state?.output),
|
|
3902
|
+
isError: status !== "completed",
|
|
3903
|
+
raw: record
|
|
3904
|
+
});
|
|
3905
|
+
}
|
|
3906
|
+
break;
|
|
3907
|
+
}
|
|
3908
|
+
case "notice":
|
|
3909
|
+
if (stringField(record, "level") === "error") events.push({
|
|
3910
|
+
type: "error",
|
|
3911
|
+
message: stringField(record, "message") ?? "error",
|
|
3912
|
+
raw: record
|
|
3913
|
+
});
|
|
3914
|
+
break;
|
|
3915
|
+
}
|
|
3916
|
+
return events;
|
|
3917
|
+
}, () => {
|
|
3918
|
+
const exported = exportEvents(context);
|
|
3919
|
+
sessionRef = exported.sessionRef ?? sessionRef;
|
|
3920
|
+
return exported.events;
|
|
3921
|
+
}),
|
|
3922
|
+
sessionRef: () => sessionRef
|
|
3923
|
+
};
|
|
3924
|
+
}
|
|
3925
|
+
|
|
3926
|
+
//#endregion
|
|
3927
|
+
//#region ../../packages/ai-cli/src/adapters/opencode.ts
|
|
3928
|
+
/**
|
|
3929
|
+
* OpenCode (`opencode`, npm `opencode-ai`).
|
|
3930
|
+
*
|
|
3931
|
+
* Verified against the real `opencode --help` / `opencode run --help` of
|
|
3932
|
+
* 1.19.3, the CLI source at that tag (`packages/opencode/src/cli/cmd/run.ts`)
|
|
3933
|
+
* and the docs (`cli.mdx`, `config.mdx`, `mcp-servers.mdx`,
|
|
3934
|
+
* `permissions.mdx`), 2026-10-17.
|
|
3935
|
+
*
|
|
3936
|
+
* - Headless: `opencode run --format json <message..>`; with no message,
|
|
3937
|
+
* piped stdin is the prompt.
|
|
3938
|
+
* - Stream: `{"type","timestamp","sessionID","part"|"error"}` lines of six
|
|
3939
|
+
* types: `step_start`, `text` (whole text blocks, no deltas), `reasoning`,
|
|
3940
|
+
* `tool_use` (emitted once the tool finished — call and result together,
|
|
3941
|
+
* `state.status` `completed`/`error`), `step_finish` (per-step `cost` in
|
|
3942
|
+
* USD and `tokens{input,output,reasoning,cache{read,write}}`, input net of
|
|
3943
|
+
* cache) and `error{error:{name,data:{message}}}`. No init or final record.
|
|
3944
|
+
* - Permissions: `standard` keeps OpenCode's own rules (permissive by
|
|
3945
|
+
* default; anything that would ask is auto-rejected in `run`); bypass is
|
|
3946
|
+
* `--auto`.
|
|
3947
|
+
* - MCP: the `mcp` key of a config file named by `OPENCODE_CONFIG`, written
|
|
3948
|
+
* to the run directory — works with any home.
|
|
3949
|
+
* - Home: OpenCode follows XDG; a private/owned home sets `XDG_CONFIG_HOME`,
|
|
3950
|
+
* `XDG_DATA_HOME` (auth lives here), `XDG_STATE_HOME` and `XDG_CACHE_HOME`
|
|
3951
|
+
* beneath it. Provider keys use the provider's own variable
|
|
3952
|
+
* (`ANTHROPIC_API_KEY`, `OPENAI_API_KEY`, …) — pass them through `env`.
|
|
3953
|
+
* - The working directory is passed with `--dir`, because OpenCode prefers a
|
|
3954
|
+
* (possibly stale) `PWD` over the process cwd.
|
|
3955
|
+
*/
|
|
3956
|
+
const opencodeAdapter = {
|
|
3957
|
+
name: "opencode",
|
|
3958
|
+
title: "OpenCode",
|
|
3959
|
+
command: "opencode",
|
|
3960
|
+
apiKeyEnv: [],
|
|
3961
|
+
homeEnv: [
|
|
3962
|
+
"XDG_CONFIG_HOME",
|
|
3963
|
+
"XDG_DATA_HOME",
|
|
3964
|
+
"XDG_STATE_HOME",
|
|
3965
|
+
"XDG_CACHE_HOME"
|
|
3966
|
+
],
|
|
3967
|
+
transports: ["argv", "stdin"],
|
|
3968
|
+
effort: true,
|
|
3969
|
+
mcp: true,
|
|
3970
|
+
probe: {
|
|
3971
|
+
help: [["run", "--help"]],
|
|
3972
|
+
version: ["--version"]
|
|
3973
|
+
},
|
|
3974
|
+
verification: {
|
|
3975
|
+
version: "1.19.3",
|
|
3976
|
+
verifiedBy: "help",
|
|
3977
|
+
sources: [
|
|
3978
|
+
"https://opencode.ai/docs/cli/",
|
|
3979
|
+
"https://opencode.ai/docs/mcp-servers/",
|
|
3980
|
+
"https://opencode.ai/docs/permissions/",
|
|
3981
|
+
"https://github.com/anomalyco/opencode/blob/v1.19.3/packages/opencode/src/cli/cmd/run.ts"
|
|
3982
|
+
],
|
|
3983
|
+
checkedAt: "2026-10-17"
|
|
3984
|
+
},
|
|
3985
|
+
plan(context) {
|
|
3986
|
+
const args = [
|
|
3987
|
+
"run",
|
|
3988
|
+
"--format",
|
|
3989
|
+
"json",
|
|
3990
|
+
"--dir",
|
|
3991
|
+
context.cwd
|
|
3992
|
+
];
|
|
3993
|
+
const requires = ["--format", "--dir"];
|
|
3994
|
+
const files = [];
|
|
3995
|
+
const env = { OPENCODE_DISABLE_AUTOUPDATE: "1" };
|
|
3996
|
+
if (context.model !== void 0) {
|
|
3997
|
+
args.push("--model", context.model);
|
|
3998
|
+
requires.push("--model");
|
|
3999
|
+
}
|
|
4000
|
+
if (context.effort !== void 0) {
|
|
4001
|
+
args.push("--variant", context.effort);
|
|
4002
|
+
requires.push("--variant");
|
|
4003
|
+
}
|
|
4004
|
+
if (context.resume !== void 0) {
|
|
4005
|
+
args.push("--session", context.resume);
|
|
4006
|
+
requires.push("--session");
|
|
4007
|
+
}
|
|
4008
|
+
if (context.permission === "bypass") {
|
|
4009
|
+
args.push("--auto");
|
|
4010
|
+
requires.push("--auto");
|
|
4011
|
+
}
|
|
4012
|
+
if (context.mcp !== void 0) {
|
|
4013
|
+
const file = node_path.default.join(context.runDir, "opencode.json");
|
|
4014
|
+
files.push({
|
|
4015
|
+
path: file,
|
|
4016
|
+
content: jsonFile({
|
|
4017
|
+
$schema: "https://opencode.ai/config.json",
|
|
4018
|
+
mcp: opencodeMcp(context.mcp)
|
|
4019
|
+
})
|
|
4020
|
+
});
|
|
4021
|
+
env.OPENCODE_CONFIG = file;
|
|
4022
|
+
}
|
|
4023
|
+
if (context.home !== void 0) {
|
|
4024
|
+
env.XDG_CONFIG_HOME = node_path.default.join(context.home, "config");
|
|
4025
|
+
env.XDG_DATA_HOME = node_path.default.join(context.home, "data");
|
|
4026
|
+
env.XDG_STATE_HOME = node_path.default.join(context.home, "state");
|
|
4027
|
+
env.XDG_CACHE_HOME = node_path.default.join(context.home, "cache");
|
|
4028
|
+
}
|
|
4029
|
+
return {
|
|
4030
|
+
args,
|
|
4031
|
+
tail: context.transport === "argv" ? [argvPrompt(context.prompt)] : [],
|
|
4032
|
+
stdin: context.transport === "stdin" ? context.prompt : void 0,
|
|
4033
|
+
env,
|
|
4034
|
+
files,
|
|
4035
|
+
requires
|
|
4036
|
+
};
|
|
4037
|
+
},
|
|
4038
|
+
parser: createOpencodeParser
|
|
4039
|
+
};
|
|
4040
|
+
function opencodeMcp(servers) {
|
|
4041
|
+
const rendered = {};
|
|
4042
|
+
for (const [name, server] of Object.entries(servers)) rendered[name] = server.type === "stdio" ? {
|
|
4043
|
+
type: "local",
|
|
4044
|
+
command: [server.command, ...server.args ?? []],
|
|
4045
|
+
...server.env !== void 0 ? { environment: server.env } : {},
|
|
4046
|
+
enabled: true
|
|
4047
|
+
} : {
|
|
4048
|
+
type: "remote",
|
|
4049
|
+
url: server.url,
|
|
4050
|
+
...server.headers !== void 0 ? { headers: server.headers } : {},
|
|
4051
|
+
enabled: true
|
|
4052
|
+
};
|
|
4053
|
+
return rendered;
|
|
4054
|
+
}
|
|
4055
|
+
/** Parser for `opencode run --format json`. */
|
|
4056
|
+
function createOpencodeParser() {
|
|
4057
|
+
let started = false;
|
|
4058
|
+
return jsonLinesParser((record) => {
|
|
4059
|
+
const events = [];
|
|
4060
|
+
const part = recordField(record, "part");
|
|
4061
|
+
if (!started) {
|
|
4062
|
+
started = true;
|
|
4063
|
+
events.push({
|
|
4064
|
+
type: "session.started",
|
|
4065
|
+
sessionRef: stringField(record, "sessionID"),
|
|
4066
|
+
raw: record
|
|
4067
|
+
});
|
|
4068
|
+
}
|
|
4069
|
+
switch (stringField(record, "type")) {
|
|
4070
|
+
case "text": {
|
|
4071
|
+
const text = stringField(part, "text") ?? "";
|
|
4072
|
+
if (text.length > 0) events.push({
|
|
4073
|
+
type: "message",
|
|
4074
|
+
text,
|
|
4075
|
+
delta: false,
|
|
4076
|
+
raw: record
|
|
4077
|
+
});
|
|
4078
|
+
break;
|
|
4079
|
+
}
|
|
4080
|
+
case "tool_use": {
|
|
4081
|
+
const state = recordField(part, "state");
|
|
4082
|
+
const id = stringField(part, "callID") ?? stringField(part, "id") ?? "";
|
|
4083
|
+
const failed = stringField(state, "status") === "error";
|
|
4084
|
+
events.push({
|
|
4085
|
+
type: "tool.call",
|
|
4086
|
+
id,
|
|
4087
|
+
name: stringField(part, "tool") ?? "",
|
|
4088
|
+
input: state?.input,
|
|
4089
|
+
raw: record
|
|
4090
|
+
}, {
|
|
4091
|
+
type: "tool.result",
|
|
4092
|
+
id,
|
|
4093
|
+
output: (failed ? stringField(state, "error") : stringField(state, "output")) ?? "",
|
|
4094
|
+
isError: failed,
|
|
4095
|
+
raw: record
|
|
4096
|
+
});
|
|
4097
|
+
break;
|
|
4098
|
+
}
|
|
4099
|
+
case "step_finish": {
|
|
4100
|
+
const tokens = recordField(part, "tokens");
|
|
4101
|
+
const cache = recordField(tokens, "cache");
|
|
4102
|
+
if (tokens !== void 0) {
|
|
4103
|
+
const uncached = numberField(tokens, "input") ?? 0;
|
|
4104
|
+
const cacheRead = numberField(cache, "read") ?? 0;
|
|
4105
|
+
const cacheWrite = numberField(cache, "write") ?? 0;
|
|
4106
|
+
events.push({
|
|
4107
|
+
type: "usage",
|
|
4108
|
+
cumulative: false,
|
|
4109
|
+
usage: usageOf({
|
|
4110
|
+
input: uncached + cacheRead + cacheWrite,
|
|
4111
|
+
output: numberField(tokens, "output"),
|
|
4112
|
+
cachedTokens: cacheRead > 0 ? cacheRead : void 0,
|
|
4113
|
+
cacheWriteTokens: cacheWrite > 0 ? cacheWrite : void 0,
|
|
4114
|
+
reasoningTokens: numberField(tokens, "reasoning") || void 0,
|
|
4115
|
+
costUSD: numberField(part, "cost")
|
|
4116
|
+
}),
|
|
4117
|
+
raw: record
|
|
4118
|
+
});
|
|
4119
|
+
}
|
|
4120
|
+
break;
|
|
4121
|
+
}
|
|
4122
|
+
case "error": {
|
|
4123
|
+
const error = asRecord(record.error);
|
|
4124
|
+
const name = stringField(error, "name") ?? "Error";
|
|
4125
|
+
const message = stringField(recordField(error, "data"), "message") ?? name;
|
|
4126
|
+
events.push({
|
|
4127
|
+
type: "error",
|
|
4128
|
+
message: `${name}: ${message}`,
|
|
4129
|
+
fatal: true,
|
|
4130
|
+
raw: record
|
|
4131
|
+
});
|
|
4132
|
+
break;
|
|
4133
|
+
}
|
|
4134
|
+
}
|
|
4135
|
+
return events;
|
|
4136
|
+
});
|
|
4137
|
+
}
|
|
4138
|
+
|
|
4139
|
+
//#endregion
|
|
4140
|
+
//#region ../../packages/ai-cli/src/adapters/qwen-code.ts
|
|
4141
|
+
/**
|
|
4142
|
+
* Qwen Code (`qwen`, npm `@qwen-code/qwen-code`), a Gemini CLI fork.
|
|
4143
|
+
*
|
|
4144
|
+
* Verified against the real `qwen --help` of 0.25.2 and the docs/source at
|
|
4145
|
+
* tag v0.25.2 (`docs/users/features/headless.md`, `nonInteractive/types.ts`),
|
|
4146
|
+
* 2026-10-17.
|
|
4147
|
+
*
|
|
4148
|
+
* - Headless: positional prompt (`-p` is deprecated) with
|
|
4149
|
+
* `--output-format stream-json`; piped stdin is used as the prompt when no
|
|
4150
|
+
* positional prompt is given.
|
|
4151
|
+
* - Stream: the Claude Code `stream-json` shape (`system/init`, `assistant`,
|
|
4152
|
+
* `user` tool results, `result`), with `input_tokens` that already include
|
|
4153
|
+
* cached tokens and no cost field.
|
|
4154
|
+
* - Permissions: headless auto-denies tools that need confirmation (the tool
|
|
4155
|
+
* result says so and `permission_denials` lists it); bypass is `--yolo`.
|
|
4156
|
+
* - MCP: `--mcp-config <file>` with `{"mcpServers":…}` — written to the run
|
|
4157
|
+
* directory, so it works with any home. Remote servers use `httpUrl`
|
|
4158
|
+
* (streamable HTTP) or `url` (SSE).
|
|
4159
|
+
* - Home: `QWEN_HOME`. Auth depends on the auth type: `OPENAI_API_KEY`
|
|
4160
|
+
* (default for `apiKey`), or `ANTHROPIC_API_KEY` / `GEMINI_API_KEY` with
|
|
4161
|
+
* `--auth-type` in `args`.
|
|
4162
|
+
* - Effort has no flag (it is the `model.reasoningEffort` setting), so
|
|
4163
|
+
* `effort` is refused.
|
|
4164
|
+
*/
|
|
4165
|
+
const qwenCodeAdapter = {
|
|
4166
|
+
name: "qwen-code",
|
|
4167
|
+
title: "Qwen Code",
|
|
4168
|
+
command: "qwen",
|
|
4169
|
+
apiKeyEnv: ["OPENAI_API_KEY"],
|
|
4170
|
+
homeEnv: ["QWEN_HOME"],
|
|
4171
|
+
transports: ["argv", "stdin"],
|
|
4172
|
+
effort: false,
|
|
4173
|
+
mcp: true,
|
|
4174
|
+
probe: {
|
|
4175
|
+
help: [["--help"]],
|
|
4176
|
+
version: ["--version"]
|
|
4177
|
+
},
|
|
4178
|
+
verification: {
|
|
4179
|
+
version: "0.25.2",
|
|
4180
|
+
verifiedBy: "help",
|
|
4181
|
+
sources: ["https://qwenlm.github.io/qwen-code-docs/", "https://github.com/QwenLM/qwen-code/blob/v0.25.2/docs/users/features/headless.md"],
|
|
4182
|
+
checkedAt: "2026-10-17"
|
|
4183
|
+
},
|
|
4184
|
+
plan(context) {
|
|
4185
|
+
const args = ["--output-format", "stream-json"];
|
|
4186
|
+
const requires = ["--output-format", "stream-json"];
|
|
4187
|
+
const files = [];
|
|
4188
|
+
if (context.model !== void 0) {
|
|
4189
|
+
args.push("--model", context.model);
|
|
4190
|
+
requires.push("--model");
|
|
4191
|
+
}
|
|
4192
|
+
if (context.resume !== void 0) {
|
|
4193
|
+
args.push("--resume", context.resume);
|
|
4194
|
+
requires.push("--resume");
|
|
4195
|
+
}
|
|
4196
|
+
if (context.permission === "bypass") {
|
|
4197
|
+
args.push("--yolo");
|
|
4198
|
+
requires.push("--yolo");
|
|
4199
|
+
}
|
|
4200
|
+
if (context.mcp !== void 0) {
|
|
4201
|
+
const file = node_path.default.join(context.runDir, "mcp.json");
|
|
4202
|
+
files.push({
|
|
4203
|
+
path: file,
|
|
4204
|
+
content: jsonFile({ mcpServers: mcpServersJson(context.mcp, { httpUrlKey: "httpUrl" }) })
|
|
4205
|
+
});
|
|
4206
|
+
args.push("--mcp-config", file);
|
|
4207
|
+
requires.push("--mcp-config");
|
|
4208
|
+
}
|
|
4209
|
+
return {
|
|
4210
|
+
args,
|
|
4211
|
+
tail: context.transport === "argv" ? [argvPrompt(context.prompt)] : [],
|
|
4212
|
+
stdin: context.transport === "stdin" ? context.prompt : void 0,
|
|
4213
|
+
env: {
|
|
4214
|
+
...context.home !== void 0 ? { QWEN_HOME: context.home } : {},
|
|
4215
|
+
QWEN_CODE_SUPPRESS_YOLO_WARNING: "1"
|
|
4216
|
+
},
|
|
4217
|
+
files,
|
|
4218
|
+
requires
|
|
4219
|
+
};
|
|
4220
|
+
},
|
|
4221
|
+
parser: () => createClaudeStreamParser({
|
|
4222
|
+
usage: "inclusive",
|
|
4223
|
+
vendor: "Qwen Code"
|
|
4224
|
+
})
|
|
4225
|
+
};
|
|
4226
|
+
|
|
4227
|
+
//#endregion
|
|
4228
|
+
//#region ../../packages/ai-cli/src/adapters/reasonix.ts
|
|
4229
|
+
/**
|
|
4230
|
+
* Reasonix (`reasonix`, npm `reasonix`) — a cache-first coding agent built
|
|
4231
|
+
* around DeepSeek (now multi-provider); the npm package launches a prebuilt
|
|
4232
|
+
* Go binary.
|
|
4233
|
+
*
|
|
4234
|
+
* Verified against the real `reasonix run --help` / `--version` of 2.43.0,
|
|
4235
|
+
* real stream-json output (captured against a local fake model server) and
|
|
4236
|
+
* the docs at that tag (`docs/CLI.md`, `docs/CONFIG_PATHS.md`), 2026-10-17.
|
|
4237
|
+
*
|
|
4238
|
+
* - Headless: `reasonix run --output-format stream-json <task>`; with no
|
|
4239
|
+
* positional task, piped stdin is the prompt.
|
|
4240
|
+
* - Stream: every line has `kind` (`text` deltas, `message` full text,
|
|
4241
|
+
* `tool_dispatch`/`tool_started`/`tool_result` with a `tool` object whose
|
|
4242
|
+
* non-empty `err` marks a failure, `usage` per model call, `turn_done`) plus
|
|
4243
|
+
* `sessionId`; the final line is `{"type":"result",…}` with totals.
|
|
4244
|
+
* - Permissions: headless "ask" mode refuses writes (listed in
|
|
4245
|
+
* `permission_denials`; exit 4 in an untrusted folder); bypass is `--yolo`.
|
|
4246
|
+
* - MCP: no flag; `[[plugins]]` in `$REASONIX_HOME/config.toml`.
|
|
4247
|
+
* - Auth: provider keys are read only from `$REASONIX_HOME/.env`, never the
|
|
4248
|
+
* shell environment, so `apiKey` is written there (0600) as
|
|
4249
|
+
* `DEEPSEEK_API_KEY` — this needs a private home. For another provider,
|
|
4250
|
+
* put its variable in that file yourself via a caller-owned home.
|
|
4251
|
+
*/
|
|
4252
|
+
const reasonixAdapter = {
|
|
4253
|
+
name: "reasonix",
|
|
4254
|
+
title: "Reasonix",
|
|
4255
|
+
command: "reasonix",
|
|
4256
|
+
apiKeyEnv: ["DEEPSEEK_API_KEY"],
|
|
4257
|
+
homeEnv: ["REASONIX_HOME"],
|
|
4258
|
+
transports: ["argv", "stdin"],
|
|
4259
|
+
effort: true,
|
|
4260
|
+
mcp: true,
|
|
4261
|
+
probe: {
|
|
4262
|
+
help: [["run", "--help"]],
|
|
4263
|
+
version: ["--version"]
|
|
4264
|
+
},
|
|
4265
|
+
verification: {
|
|
4266
|
+
version: "2.43.0",
|
|
4267
|
+
verifiedBy: "help",
|
|
4268
|
+
sources: [
|
|
4269
|
+
"https://github.com/esengine/DeepSeek-Reasonix/blob/v2.43.0/docs/CLI.md",
|
|
4270
|
+
"https://github.com/esengine/DeepSeek-Reasonix/blob/v2.43.0/docs/CONFIG_PATHS.md",
|
|
4271
|
+
"https://www.npmjs.com/package/reasonix"
|
|
4272
|
+
],
|
|
4273
|
+
checkedAt: "2026-10-17"
|
|
4274
|
+
},
|
|
4275
|
+
plan(context) {
|
|
4276
|
+
const args = [
|
|
4277
|
+
"run",
|
|
4278
|
+
"--output-format",
|
|
4279
|
+
"stream-json"
|
|
4280
|
+
];
|
|
4281
|
+
const requires = ["--output-format", "stream-json"];
|
|
4282
|
+
const files = [];
|
|
4283
|
+
if (context.model !== void 0) {
|
|
4284
|
+
args.push("--model", context.model);
|
|
4285
|
+
requires.push("--model");
|
|
4286
|
+
}
|
|
4287
|
+
if (context.effort !== void 0) {
|
|
4288
|
+
args.push("--effort", mapEffort(context.effort, {
|
|
4289
|
+
none: "disabled",
|
|
4290
|
+
low: "low",
|
|
4291
|
+
high: "high",
|
|
4292
|
+
max: "max"
|
|
4293
|
+
}));
|
|
4294
|
+
requires.push("--effort");
|
|
4295
|
+
}
|
|
4296
|
+
if (context.resume !== void 0) {
|
|
4297
|
+
args.push("--resume", context.resume);
|
|
4298
|
+
requires.push("--resume");
|
|
4299
|
+
}
|
|
4300
|
+
if (context.permission === "bypass") {
|
|
4301
|
+
args.push("--yolo");
|
|
4302
|
+
requires.push("--yolo");
|
|
4303
|
+
}
|
|
4304
|
+
if (context.mcp !== void 0) {
|
|
4305
|
+
const home = requirePrivateHome(context, reasonixAdapter, "MCP servers");
|
|
4306
|
+
files.push({
|
|
4307
|
+
path: node_path.default.join(home, "config.toml"),
|
|
4308
|
+
content: toToml({ plugins: reasonixPlugins(context.mcp) })
|
|
4309
|
+
});
|
|
4310
|
+
}
|
|
4311
|
+
if (context.apiKey !== void 0) {
|
|
4312
|
+
const home = requirePrivateHome(context, reasonixAdapter, "API keys (its .env file)");
|
|
4313
|
+
files.push({
|
|
4314
|
+
path: node_path.default.join(home, ".env"),
|
|
4315
|
+
content: dotenvFile({ DEEPSEEK_API_KEY: context.apiKey })
|
|
4316
|
+
});
|
|
4317
|
+
}
|
|
4318
|
+
return {
|
|
4319
|
+
args,
|
|
4320
|
+
tail: context.transport === "argv" ? [argvPrompt(context.prompt)] : [],
|
|
4321
|
+
stdin: context.transport === "stdin" ? context.prompt : void 0,
|
|
4322
|
+
env: {
|
|
4323
|
+
...context.home !== void 0 ? { REASONIX_HOME: context.home } : {},
|
|
4324
|
+
REASONIX_TELEMETRY: "off"
|
|
4325
|
+
},
|
|
4326
|
+
files,
|
|
4327
|
+
requires
|
|
4328
|
+
};
|
|
4329
|
+
},
|
|
4330
|
+
parser: createReasonixParser
|
|
4331
|
+
};
|
|
4332
|
+
function reasonixPlugins(servers) {
|
|
4333
|
+
return Object.entries(servers).map(([name, server]) => server.type === "stdio" ? {
|
|
4334
|
+
name,
|
|
4335
|
+
type: "stdio",
|
|
4336
|
+
command: server.command,
|
|
4337
|
+
args: server.args ?? [],
|
|
4338
|
+
env: server.env
|
|
4339
|
+
} : {
|
|
4340
|
+
name,
|
|
4341
|
+
type: server.type,
|
|
4342
|
+
url: server.url,
|
|
4343
|
+
headers: server.headers
|
|
4344
|
+
});
|
|
4345
|
+
}
|
|
4346
|
+
/** Reasonix per-call usage (`promptTokens` includes cache hits). */
|
|
4347
|
+
function callUsage(usage) {
|
|
4348
|
+
if (usage === void 0) return;
|
|
4349
|
+
const cacheHit = numberField(usage, "cacheHitTokens");
|
|
4350
|
+
return usageOf({
|
|
4351
|
+
input: numberField(usage, "promptTokens"),
|
|
4352
|
+
output: numberField(usage, "completionTokens"),
|
|
4353
|
+
total: numberField(usage, "totalTokens"),
|
|
4354
|
+
cachedTokens: cacheHit !== void 0 && cacheHit > 0 ? cacheHit : void 0,
|
|
4355
|
+
reasoningTokens: numberField(usage, "reasoningTokens"),
|
|
4356
|
+
costUSD: numberField(usage, "costUsd")
|
|
4357
|
+
});
|
|
4358
|
+
}
|
|
4359
|
+
/** Parser for `reasonix run --output-format stream-json`. */
|
|
4360
|
+
function createReasonixParser() {
|
|
4361
|
+
let started = false;
|
|
4362
|
+
let sessionRef;
|
|
4363
|
+
return {
|
|
4364
|
+
...jsonLinesParser((record) => {
|
|
4365
|
+
const events = [];
|
|
4366
|
+
const recordSession = stringField(record, "sessionId") ?? stringField(record, "session_id");
|
|
4367
|
+
if (recordSession !== void 0) sessionRef = recordSession;
|
|
4368
|
+
if (!started && recordSession !== void 0) {
|
|
4369
|
+
started = true;
|
|
4370
|
+
events.push({
|
|
4371
|
+
type: "session.started",
|
|
4372
|
+
sessionRef: recordSession,
|
|
4373
|
+
raw: record
|
|
4374
|
+
});
|
|
4375
|
+
}
|
|
4376
|
+
if (stringField(record, "type") === "result") {
|
|
4377
|
+
const usage = recordField(record, "usage");
|
|
4378
|
+
if (usage !== void 0) events.push({
|
|
4379
|
+
type: "usage",
|
|
4380
|
+
cumulative: true,
|
|
4381
|
+
usage: usageOf({
|
|
4382
|
+
input: numberField(usage, "input_tokens"),
|
|
4383
|
+
output: numberField(usage, "output_tokens"),
|
|
4384
|
+
cachedTokens: numberField(usage, "cache_read_input_tokens") || void 0,
|
|
4385
|
+
costUSD: numberField(record, "total_cost_usd")
|
|
4386
|
+
}),
|
|
4387
|
+
raw: record
|
|
4388
|
+
});
|
|
4389
|
+
const result = stringField(record, "result") ?? "";
|
|
4390
|
+
if (record.is_error === true) events.push({
|
|
4391
|
+
type: "error",
|
|
4392
|
+
message: result || "Reasonix run failed",
|
|
4393
|
+
fatal: true,
|
|
4394
|
+
raw: record
|
|
4395
|
+
});
|
|
4396
|
+
else if (result.length > 0) events.push({
|
|
4397
|
+
type: "message",
|
|
4398
|
+
text: result,
|
|
4399
|
+
delta: false,
|
|
4400
|
+
final: true,
|
|
4401
|
+
raw: record
|
|
4402
|
+
});
|
|
4403
|
+
return events;
|
|
4404
|
+
}
|
|
4405
|
+
const tool = asRecord(record.tool);
|
|
4406
|
+
switch (stringField(record, "kind")) {
|
|
4407
|
+
case "text": {
|
|
4408
|
+
const text = stringField(record, "text") ?? "";
|
|
4409
|
+
if (text.length > 0) events.push({
|
|
4410
|
+
type: "message",
|
|
4411
|
+
text,
|
|
4412
|
+
delta: true,
|
|
4413
|
+
raw: record
|
|
4414
|
+
});
|
|
4415
|
+
break;
|
|
4416
|
+
}
|
|
4417
|
+
case "message":
|
|
4418
|
+
events.push({
|
|
4419
|
+
type: "message",
|
|
4420
|
+
text: stringField(record, "text") ?? "",
|
|
4421
|
+
delta: false,
|
|
4422
|
+
raw: record
|
|
4423
|
+
});
|
|
4424
|
+
break;
|
|
4425
|
+
case "tool_dispatch":
|
|
4426
|
+
if (tool !== void 0 && tool.partial !== true) events.push({
|
|
4427
|
+
type: "tool.call",
|
|
4428
|
+
id: stringField(tool, "id") ?? "",
|
|
4429
|
+
name: stringField(tool, "name") ?? "",
|
|
4430
|
+
input: parseArguments(tool.args),
|
|
4431
|
+
raw: record
|
|
4432
|
+
});
|
|
4433
|
+
break;
|
|
4434
|
+
case "tool_result": {
|
|
4435
|
+
const error = stringField(tool, "err") ?? "";
|
|
4436
|
+
events.push({
|
|
4437
|
+
type: "tool.result",
|
|
4438
|
+
id: stringField(tool, "id") ?? "",
|
|
4439
|
+
output: error.length > 0 ? error : stringField(tool, "output") ?? "",
|
|
4440
|
+
isError: error.length > 0,
|
|
4441
|
+
raw: record
|
|
4442
|
+
});
|
|
4443
|
+
break;
|
|
4444
|
+
}
|
|
4445
|
+
case "usage": {
|
|
4446
|
+
const usage = callUsage(recordField(record, "usage"));
|
|
4447
|
+
if (usage !== void 0) events.push({
|
|
4448
|
+
type: "usage",
|
|
4449
|
+
usage,
|
|
4450
|
+
cumulative: false,
|
|
4451
|
+
raw: record
|
|
4452
|
+
});
|
|
4453
|
+
break;
|
|
4454
|
+
}
|
|
4455
|
+
case "turn_done":
|
|
4456
|
+
if (stringField(record, "status") === "failed") events.push({
|
|
4457
|
+
type: "error",
|
|
4458
|
+
message: stringField(record, "err") ?? "Reasonix turn failed",
|
|
4459
|
+
fatal: true,
|
|
4460
|
+
raw: record
|
|
4461
|
+
});
|
|
4462
|
+
break;
|
|
4463
|
+
}
|
|
4464
|
+
return events;
|
|
4465
|
+
}),
|
|
4466
|
+
sessionRef: () => sessionRef
|
|
4467
|
+
};
|
|
4468
|
+
}
|
|
4469
|
+
|
|
4470
|
+
//#endregion
|
|
4471
|
+
//#region ../../packages/ai-cli/src/cli.ts
|
|
4472
|
+
/** Every built-in adapter, keyed by its factory name. */
|
|
4473
|
+
const cliAdapters = {
|
|
4474
|
+
claudeCode: claudeCodeAdapter,
|
|
4475
|
+
codex: codexAdapter,
|
|
4476
|
+
antigravity: antigravityAdapter,
|
|
4477
|
+
qwenCode: qwenCodeAdapter,
|
|
4478
|
+
kimi: kimiAdapter,
|
|
4479
|
+
cursor: cursorAdapter,
|
|
4480
|
+
copilot: copilotAdapter,
|
|
4481
|
+
grok: grokAdapter,
|
|
4482
|
+
opencode: opencodeAdapter,
|
|
4483
|
+
amp: ampAdapter,
|
|
4484
|
+
droid: droidAdapter,
|
|
4485
|
+
mistralVibe: mistralVibeAdapter,
|
|
4486
|
+
aider: aiderAdapter,
|
|
4487
|
+
reasonix: reasonixAdapter
|
|
4488
|
+
};
|
|
4489
|
+
const factories = Object.fromEntries(Object.entries(cliAdapters).map(([vendor, adapter]) => [vendor, (options) => createCliAgent(adapter, options)]));
|
|
4490
|
+
/**
|
|
4491
|
+
* The CLI-agent namespace: one factory per vendor, `custom()` for your own
|
|
4492
|
+
* adapter, `model()` to hand a CLI agent to `ai.agent`. Also available as
|
|
4493
|
+
* `ai.cli` once the package is imported.
|
|
4494
|
+
*
|
|
4495
|
+
* @example
|
|
4496
|
+
* import { cli } from "@warlock.js/ai-cli";
|
|
4497
|
+
*
|
|
4498
|
+
* const claude = cli.claudeCode({ model: "sonnet", cwd: "/srv/repo" });
|
|
4499
|
+
* const session = await claude.run("Fix the failing test");
|
|
4500
|
+
* for await (const event of session.events) console.log(event.type);
|
|
4501
|
+
* const { text, usage, sessionRef } = await session.done();
|
|
4502
|
+
*/
|
|
4503
|
+
const cli = {
|
|
4504
|
+
...factories,
|
|
4505
|
+
custom: (adapter, options) => createCliAgent(adapter, options),
|
|
4506
|
+
model: cliModel,
|
|
4507
|
+
adapters: cliAdapters
|
|
4508
|
+
};
|
|
4509
|
+
_warlock_js_ai.ai.cli = cli;
|
|
4510
|
+
|
|
4511
|
+
//#endregion
|
|
4512
|
+
exports.CliError = CliError;
|
|
4513
|
+
exports.aiderAdapter = aiderAdapter;
|
|
4514
|
+
exports.ampAdapter = ampAdapter;
|
|
4515
|
+
exports.anthropicUsage = anthropicUsage;
|
|
4516
|
+
exports.antigravityAdapter = antigravityAdapter;
|
|
4517
|
+
exports.argvPrompt = argvPrompt;
|
|
4518
|
+
exports.asArray = asArray;
|
|
4519
|
+
exports.asRecord = asRecord;
|
|
4520
|
+
exports.buildInvocation = buildInvocation;
|
|
4521
|
+
exports.claudeCodeAdapter = claudeCodeAdapter;
|
|
4522
|
+
exports.cli = cli;
|
|
4523
|
+
exports.cliAdapters = cliAdapters;
|
|
4524
|
+
exports.cliModel = cliModel;
|
|
4525
|
+
exports.codexAdapter = codexAdapter;
|
|
4526
|
+
exports.copilotAdapter = copilotAdapter;
|
|
4527
|
+
exports.createAiderParser = createAiderParser;
|
|
4528
|
+
exports.createAntigravityParser = createAntigravityParser;
|
|
4529
|
+
exports.createClaudeStreamParser = createClaudeStreamParser;
|
|
4530
|
+
exports.createCliAgent = createCliAgent;
|
|
4531
|
+
exports.createCodexParser = createCodexParser;
|
|
4532
|
+
exports.createCopilotParser = createCopilotParser;
|
|
4533
|
+
exports.createCursorParser = createCursorParser;
|
|
4534
|
+
exports.createDroidParser = createDroidParser;
|
|
4535
|
+
exports.createGrokParser = createGrokParser;
|
|
4536
|
+
exports.createKimiParser = createKimiParser;
|
|
4537
|
+
exports.createOpencodeParser = createOpencodeParser;
|
|
4538
|
+
exports.createReasonixParser = createReasonixParser;
|
|
4539
|
+
exports.createVibeParser = createVibeParser;
|
|
4540
|
+
exports.currentHost = currentHost;
|
|
4541
|
+
exports.cursorAdapter = cursorAdapter;
|
|
4542
|
+
exports.dotenvFile = dotenvFile;
|
|
4543
|
+
exports.droidAdapter = droidAdapter;
|
|
4544
|
+
exports.findExecutable = findExecutable;
|
|
4545
|
+
exports.fitsCommandLine = fitsCommandLine;
|
|
4546
|
+
exports.grokAdapter = grokAdapter;
|
|
4547
|
+
exports.helpLists = helpLists;
|
|
4548
|
+
exports.jsonFile = jsonFile;
|
|
4549
|
+
exports.jsonLinesParser = jsonLinesParser;
|
|
4550
|
+
exports.kimiAdapter = kimiAdapter;
|
|
4551
|
+
exports.mapEffort = mapEffort;
|
|
4552
|
+
exports.mcpServersJson = mcpServersJson;
|
|
4553
|
+
exports.missingFlags = missingFlags;
|
|
4554
|
+
exports.mistralVibeAdapter = mistralVibeAdapter;
|
|
4555
|
+
exports.numberField = numberField;
|
|
4556
|
+
exports.opencodeAdapter = opencodeAdapter;
|
|
4557
|
+
exports.outputText = outputText;
|
|
4558
|
+
exports.parseAiderCount = parseAiderCount;
|
|
4559
|
+
exports.parseArguments = parseArguments;
|
|
4560
|
+
exports.parseCmdShim = parseCmdShim;
|
|
4561
|
+
exports.parseJsonLine = parseJsonLine;
|
|
4562
|
+
exports.parseVersion = parseVersion;
|
|
4563
|
+
exports.promptFile = promptFile;
|
|
4564
|
+
exports.quoteForCmd = quoteForCmd;
|
|
4565
|
+
exports.qwenCodeAdapter = qwenCodeAdapter;
|
|
4566
|
+
exports.reasonixAdapter = reasonixAdapter;
|
|
4567
|
+
exports.recordField = recordField;
|
|
4568
|
+
exports.renderPrompt = renderPrompt;
|
|
4569
|
+
exports.requirePrivateHome = requirePrivateHome;
|
|
4570
|
+
exports.resolveSpawnTarget = resolveSpawnTarget;
|
|
4571
|
+
exports.stringField = stringField;
|
|
4572
|
+
exports.toToml = toToml;
|
|
4573
|
+
exports.toUsage = toUsage;
|
|
4574
|
+
exports.unsupportedOption = unsupportedOption;
|
|
4575
|
+
exports.usageOf = usageOf;
|
|
4576
|
+
//# sourceMappingURL=index.cjs.map
|