@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
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { CliUsage } from "../contracts/events.type.mjs";
|
|
2
|
+
import { CliRunOptions } from "../contracts/options.type.mjs";
|
|
3
|
+
import { CliAgent } from "../cli-agent.mjs";
|
|
4
|
+
import { Message, ModelContract, ModelPricing, Usage } from "@warlock.js/ai";
|
|
5
|
+
|
|
6
|
+
//#region ../../packages/ai-cli/src/model/cli-model.d.ts
|
|
7
|
+
/** Configuration of a CLI-backed model. Run options apply to every call. */
|
|
8
|
+
type CliModelConfig = CliRunOptions & {
|
|
9
|
+
/** Model name reported to the agent, reports and observers. Default: `model`, else the adapter name. */name?: string; /** Per-million-token pricing, so the agent computes `Usage.cost` (and USD budgets apply). */
|
|
10
|
+
pricing?: ModelPricing;
|
|
11
|
+
};
|
|
12
|
+
/**
|
|
13
|
+
* Render an agent conversation into one CLI prompt. System messages become
|
|
14
|
+
* a leading instruction block; a single user turn is passed as-is; a longer
|
|
15
|
+
* history is rendered as a labelled transcript ending with the latest turn.
|
|
16
|
+
*
|
|
17
|
+
* @example
|
|
18
|
+
* renderPrompt([{ role: "system", content: "Be brief." }, { role: "user", content: "Hi" }], "codex");
|
|
19
|
+
* // "Be brief.\n\nHi"
|
|
20
|
+
*/
|
|
21
|
+
declare function renderPrompt(messages: Message[], adapter: string): string;
|
|
22
|
+
/** Map CLI usage to the framework `Usage` (zeros when the CLI reported none). */
|
|
23
|
+
declare function toUsage(usage: CliUsage | undefined): Usage;
|
|
24
|
+
/**
|
|
25
|
+
* A `ModelContract` backed by a CLI agent, so `ai.agent({ model })` — and
|
|
26
|
+
* its budget, guardrail, observer and fallback middleware — can drive a
|
|
27
|
+
* coding CLI.
|
|
28
|
+
*
|
|
29
|
+
* Each call is one fresh CLI session: the conversation is rendered into a
|
|
30
|
+
* single prompt (see {@link renderPrompt}) and the session's final answer
|
|
31
|
+
* becomes the response. The CLI runs its own tools; framework tools cannot
|
|
32
|
+
* be handed to it this way (give the CLI MCP servers with `mcp` instead),
|
|
33
|
+
* so a call carrying `tools` is refused. `stream()` yields the final answer
|
|
34
|
+
* once the session ends — read `session.events` from `agent.run()` for live
|
|
35
|
+
* progress.
|
|
36
|
+
*
|
|
37
|
+
* @example
|
|
38
|
+
* const coder = cli.codex({ cwd: "/srv/repo" });
|
|
39
|
+
* const reviewer = ai.agent({
|
|
40
|
+
* model: cliModel(coder, { name: "codex" }),
|
|
41
|
+
* middleware: [budget({ maxTokens: 200_000 })],
|
|
42
|
+
* });
|
|
43
|
+
* const { text } = await reviewer.execute("Review the last commit.");
|
|
44
|
+
*/
|
|
45
|
+
declare function cliModel(agent: CliAgent, config?: CliModelConfig): ModelContract;
|
|
46
|
+
//#endregion
|
|
47
|
+
export { CliModelConfig, cliModel, renderPrompt, toUsage };
|
|
48
|
+
//# sourceMappingURL=cli-model.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cli-model.d.mts","names":[],"sources":["../../../../../../../../packages/ai-cli/src/model/cli-model.ts"],"mappings":";;;;;;;KAeY,cAAA,GAAiB,aAAA;0GAE3B,IAAA,WAFU;EAIV,OAAA,GAAU,YAAY;AAAA;;;;;;;AAAA;AAgCxB;;iBAAgB,YAAA,CAAa,QAAA,EAAU,OAAO,IAAI,OAAA;;iBA2BlC,OAAA,CAAQ,KAAA,EAAO,QAAA,eAAuB,KAAK;;;;AA3BM;AA2BjE;;;;;;;;AAA2D;AA2C3D;;;;;;;;iBAAgB,QAAA,CAAS,KAAA,EAAO,QAAA,EAAU,MAAA,GAAQ,cAAA,GAAsB,aAAA"}
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import { CliError } from "../errors.mjs";
|
|
2
|
+
|
|
3
|
+
//#region ../../packages/ai-cli/src/model/cli-model.ts
|
|
4
|
+
/** The text of one message; CLI models accept text only. */
|
|
5
|
+
function messageText(message, adapter) {
|
|
6
|
+
if (typeof message.content === "string") return message.content;
|
|
7
|
+
return message.content.map((part) => {
|
|
8
|
+
if (part.type !== "text") throw new CliError(`${adapter}: CLI models accept text only; a "${part.type}" attachment cannot be passed to the CLI.`, {
|
|
9
|
+
type: "unsupported-option",
|
|
10
|
+
adapter
|
|
11
|
+
});
|
|
12
|
+
return part.text;
|
|
13
|
+
}).join("\n");
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Render an agent conversation into one CLI prompt. System messages become
|
|
17
|
+
* a leading instruction block; a single user turn is passed as-is; a longer
|
|
18
|
+
* history is rendered as a labelled transcript ending with the latest turn.
|
|
19
|
+
*
|
|
20
|
+
* @example
|
|
21
|
+
* renderPrompt([{ role: "system", content: "Be brief." }, { role: "user", content: "Hi" }], "codex");
|
|
22
|
+
* // "Be brief.\n\nHi"
|
|
23
|
+
*/
|
|
24
|
+
function renderPrompt(messages, adapter) {
|
|
25
|
+
const system = messages.filter((message) => message.role === "system").map((message) => messageText(message, adapter).trim()).filter((text) => text.length > 0);
|
|
26
|
+
const turns = messages.filter((message) => message.role !== "system");
|
|
27
|
+
const only = turns.length === 1 ? turns[0] : void 0;
|
|
28
|
+
const body = only !== void 0 && only.role === "user" ? messageText(only, adapter) : turns.map((message) => {
|
|
29
|
+
return `${message.role === "user" ? "User" : message.role === "assistant" ? "Assistant" : `Tool result${message.toolCallId ? ` (${message.toolCallId})` : ""}`}:\n${messageText(message, adapter)}`;
|
|
30
|
+
}).join("\n\n");
|
|
31
|
+
return [...system, body].filter((part) => part.length > 0).join("\n\n");
|
|
32
|
+
}
|
|
33
|
+
/** Map CLI usage to the framework `Usage` (zeros when the CLI reported none). */
|
|
34
|
+
function toUsage(usage) {
|
|
35
|
+
if (usage === void 0) return {
|
|
36
|
+
input: 0,
|
|
37
|
+
output: 0,
|
|
38
|
+
total: 0
|
|
39
|
+
};
|
|
40
|
+
const mapped = {
|
|
41
|
+
input: usage.input,
|
|
42
|
+
output: usage.output,
|
|
43
|
+
total: usage.total
|
|
44
|
+
};
|
|
45
|
+
if (usage.cachedTokens !== void 0) mapped.cachedTokens = usage.cachedTokens;
|
|
46
|
+
if (usage.cacheWriteTokens !== void 0) mapped.cacheWriteTokens = usage.cacheWriteTokens;
|
|
47
|
+
if (usage.reasoningTokens !== void 0) mapped.reasoningTokens = usage.reasoningTokens;
|
|
48
|
+
return mapped;
|
|
49
|
+
}
|
|
50
|
+
function abortError(message) {
|
|
51
|
+
const error = new Error(message);
|
|
52
|
+
error.name = "AbortError";
|
|
53
|
+
return error;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* A `ModelContract` backed by a CLI agent, so `ai.agent({ model })` — and
|
|
57
|
+
* its budget, guardrail, observer and fallback middleware — can drive a
|
|
58
|
+
* coding CLI.
|
|
59
|
+
*
|
|
60
|
+
* Each call is one fresh CLI session: the conversation is rendered into a
|
|
61
|
+
* single prompt (see {@link renderPrompt}) and the session's final answer
|
|
62
|
+
* becomes the response. The CLI runs its own tools; framework tools cannot
|
|
63
|
+
* be handed to it this way (give the CLI MCP servers with `mcp` instead),
|
|
64
|
+
* so a call carrying `tools` is refused. `stream()` yields the final answer
|
|
65
|
+
* once the session ends — read `session.events` from `agent.run()` for live
|
|
66
|
+
* progress.
|
|
67
|
+
*
|
|
68
|
+
* @example
|
|
69
|
+
* const coder = cli.codex({ cwd: "/srv/repo" });
|
|
70
|
+
* const reviewer = ai.agent({
|
|
71
|
+
* model: cliModel(coder, { name: "codex" }),
|
|
72
|
+
* middleware: [budget({ maxTokens: 200_000 })],
|
|
73
|
+
* });
|
|
74
|
+
* const { text } = await reviewer.execute("Review the last commit.");
|
|
75
|
+
*/
|
|
76
|
+
function cliModel(agent, config = {}) {
|
|
77
|
+
const { adapter } = agent;
|
|
78
|
+
const capabilities = { reasoning: adapter.effort === true };
|
|
79
|
+
const execute = async (messages, options = {}) => {
|
|
80
|
+
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\`.`, {
|
|
81
|
+
type: "unsupported-option",
|
|
82
|
+
adapter: adapter.name
|
|
83
|
+
});
|
|
84
|
+
const { name: _name, pricing: _pricing, ...runDefaults } = config;
|
|
85
|
+
const effort = options.reasoning?.effort;
|
|
86
|
+
const result = await (await agent.run(renderPrompt(messages, adapter.name), {
|
|
87
|
+
...runDefaults,
|
|
88
|
+
signal: options.signal ?? runDefaults.signal,
|
|
89
|
+
effort: adapter.effort === true && effort !== void 0 ? effort : runDefaults.effort
|
|
90
|
+
})).done();
|
|
91
|
+
if (result.status === "cancelled") throw abortError(result.error ?? `${adapter.name} run was cancelled`);
|
|
92
|
+
if (result.status !== "completed") throw new CliError(`${adapter.title}: ${result.error ?? `run ${result.status}`}`, {
|
|
93
|
+
type: "run-failed",
|
|
94
|
+
adapter: adapter.name,
|
|
95
|
+
context: {
|
|
96
|
+
status: result.status,
|
|
97
|
+
exitCode: result.exitCode,
|
|
98
|
+
sessionRef: result.sessionRef
|
|
99
|
+
}
|
|
100
|
+
});
|
|
101
|
+
return result;
|
|
102
|
+
};
|
|
103
|
+
return {
|
|
104
|
+
name: config.name ?? config.model ?? agent.options.model ?? adapter.name,
|
|
105
|
+
provider: adapter.name,
|
|
106
|
+
capabilities,
|
|
107
|
+
pricing: config.pricing,
|
|
108
|
+
async complete(messages, options) {
|
|
109
|
+
const result = await execute(messages, options);
|
|
110
|
+
return {
|
|
111
|
+
content: result.text,
|
|
112
|
+
finishReason: "stop",
|
|
113
|
+
usage: toUsage(result.usage)
|
|
114
|
+
};
|
|
115
|
+
},
|
|
116
|
+
async *stream(messages, options) {
|
|
117
|
+
const result = await execute(messages, options);
|
|
118
|
+
if (result.text.length > 0) yield {
|
|
119
|
+
type: "delta",
|
|
120
|
+
content: result.text
|
|
121
|
+
};
|
|
122
|
+
yield {
|
|
123
|
+
type: "done",
|
|
124
|
+
finishReason: "stop",
|
|
125
|
+
usage: toUsage(result.usage)
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
//#endregion
|
|
132
|
+
export { cliModel, renderPrompt, toUsage };
|
|
133
|
+
//# sourceMappingURL=cli-model.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cli-model.mjs","names":[],"sources":["../../../../../../../../packages/ai-cli/src/model/cli-model.ts"],"sourcesContent":["import type {\n Message,\n ModelCallOptions,\n ModelCapabilities,\n ModelContract,\n ModelPricing,\n ModelResponse,\n ModelStreamChunk,\n Usage,\n} from \"@warlock.js/ai\";\nimport type { CliAgent } from \"../cli-agent\";\nimport type { CliResult, CliRunOptions, CliUsage } from \"../contracts\";\nimport { CliError } from \"../errors\";\n\n/** Configuration of a CLI-backed model. Run options apply to every call. */\nexport type CliModelConfig = CliRunOptions & {\n /** Model name reported to the agent, reports and observers. Default: `model`, else the adapter name. */\n name?: string;\n /** Per-million-token pricing, so the agent computes `Usage.cost` (and USD budgets apply). */\n pricing?: ModelPricing;\n};\n\n/** The text of one message; CLI models accept text only. */\nfunction messageText(message: Message, adapter: string): string {\n if (typeof message.content === \"string\") {\n return message.content;\n }\n\n return message.content\n .map((part) => {\n if (part.type !== \"text\") {\n throw new CliError(\n `${adapter}: CLI models accept text only; a \"${part.type}\" attachment cannot be passed to the CLI.`,\n { type: \"unsupported-option\", adapter },\n );\n }\n\n return part.text;\n })\n .join(\"\\n\");\n}\n\n/**\n * Render an agent conversation into one CLI prompt. System messages become\n * a leading instruction block; a single user turn is passed as-is; a longer\n * history is rendered as a labelled transcript ending with the latest turn.\n *\n * @example\n * renderPrompt([{ role: \"system\", content: \"Be brief.\" }, { role: \"user\", content: \"Hi\" }], \"codex\");\n * // \"Be brief.\\n\\nHi\"\n */\nexport function renderPrompt(messages: Message[], adapter: string): string {\n const system = messages\n .filter((message) => message.role === \"system\")\n .map((message) => messageText(message, adapter).trim())\n .filter((text) => text.length > 0);\n const turns = messages.filter((message) => message.role !== \"system\");\n const only = turns.length === 1 ? turns[0] : undefined;\n const body =\n only !== undefined && only.role === \"user\"\n ? messageText(only, adapter)\n : turns\n .map((message) => {\n const label =\n message.role === \"user\"\n ? \"User\"\n : message.role === \"assistant\"\n ? \"Assistant\"\n : `Tool result${message.toolCallId ? ` (${message.toolCallId})` : \"\"}`;\n\n return `${label}:\\n${messageText(message, adapter)}`;\n })\n .join(\"\\n\\n\");\n\n return [...system, body].filter((part) => part.length > 0).join(\"\\n\\n\");\n}\n\n/** Map CLI usage to the framework `Usage` (zeros when the CLI reported none). */\nexport function toUsage(usage: CliUsage | undefined): Usage {\n if (usage === undefined) {\n return { input: 0, output: 0, total: 0 };\n }\n\n const mapped: Usage = { input: usage.input, output: usage.output, total: usage.total };\n\n if (usage.cachedTokens !== undefined) mapped.cachedTokens = usage.cachedTokens;\n if (usage.cacheWriteTokens !== undefined) mapped.cacheWriteTokens = usage.cacheWriteTokens;\n if (usage.reasoningTokens !== undefined) mapped.reasoningTokens = usage.reasoningTokens;\n\n return mapped;\n}\n\nfunction abortError(message: string): Error {\n const error = new Error(message);\n\n error.name = \"AbortError\";\n\n return error;\n}\n\n/**\n * A `ModelContract` backed by a CLI agent, so `ai.agent({ model })` — and\n * its budget, guardrail, observer and fallback middleware — can drive a\n * coding CLI.\n *\n * Each call is one fresh CLI session: the conversation is rendered into a\n * single prompt (see {@link renderPrompt}) and the session's final answer\n * becomes the response. The CLI runs its own tools; framework tools cannot\n * be handed to it this way (give the CLI MCP servers with `mcp` instead),\n * so a call carrying `tools` is refused. `stream()` yields the final answer\n * once the session ends — read `session.events` from `agent.run()` for live\n * progress.\n *\n * @example\n * const coder = cli.codex({ cwd: \"/srv/repo\" });\n * const reviewer = ai.agent({\n * model: cliModel(coder, { name: \"codex\" }),\n * middleware: [budget({ maxTokens: 200_000 })],\n * });\n * const { text } = await reviewer.execute(\"Review the last commit.\");\n */\nexport function cliModel(agent: CliAgent, config: CliModelConfig = {}): ModelContract {\n const { adapter } = agent;\n const capabilities: ModelCapabilities = { reasoning: adapter.effort === true };\n\n const execute = async (\n messages: Message[],\n options: ModelCallOptions = {},\n ): Promise<CliResult> => {\n if (options.tools !== undefined && options.tools.length > 0) {\n throw new CliError(\n `${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\\`.`,\n { type: \"unsupported-option\", adapter: adapter.name },\n );\n }\n\n const { name: _name, pricing: _pricing, ...runDefaults } = config;\n const effort = options.reasoning?.effort;\n const session = await agent.run(renderPrompt(messages, adapter.name), {\n ...runDefaults,\n signal: options.signal ?? runDefaults.signal,\n effort: adapter.effort === true && effort !== undefined ? effort : runDefaults.effort,\n });\n const result = await session.done();\n\n if (result.status === \"cancelled\") {\n throw abortError(result.error ?? `${adapter.name} run was cancelled`);\n }\n\n if (result.status !== \"completed\") {\n throw new CliError(`${adapter.title}: ${result.error ?? `run ${result.status}`}`, {\n type: \"run-failed\",\n adapter: adapter.name,\n context: {\n status: result.status,\n exitCode: result.exitCode,\n sessionRef: result.sessionRef,\n },\n });\n }\n\n return result;\n };\n\n return {\n name: config.name ?? config.model ?? agent.options.model ?? adapter.name,\n provider: adapter.name,\n capabilities,\n pricing: config.pricing,\n async complete(messages: Message[], options?: ModelCallOptions): Promise<ModelResponse> {\n const result = await execute(messages, options);\n\n return { content: result.text, finishReason: \"stop\", usage: toUsage(result.usage) };\n },\n async *stream(\n messages: Message[],\n options?: ModelCallOptions,\n ): AsyncIterable<ModelStreamChunk> {\n const result = await execute(messages, options);\n\n if (result.text.length > 0) {\n yield { type: \"delta\", content: result.text };\n }\n\n yield { type: \"done\", finishReason: \"stop\", usage: toUsage(result.usage) };\n },\n };\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
//#region ../../packages/ai-cli/src/probe/probe.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* Whether a help screen lists a flag or subcommand. Flags (`--json`, `-p`)
|
|
4
|
+
* must appear as a whole token — `--json` does not match `--json-schema`;
|
|
5
|
+
* a bare word (`exec`) is matched as a whole word.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* helpLists(" -p, --print Print response", "--print"); // true
|
|
9
|
+
* helpLists(" --output-format <format>", "--output"); // false
|
|
10
|
+
*/
|
|
11
|
+
declare function helpLists(help: string, flag: string): boolean;
|
|
12
|
+
/** The first version-looking token of `--version` output, else its first line. */
|
|
13
|
+
declare function parseVersion(output: string): string;
|
|
14
|
+
/** Flags a plan requires that the help screen does not list. */
|
|
15
|
+
declare function missingFlags(help: string, required: readonly string[]): string[];
|
|
16
|
+
//#endregion
|
|
17
|
+
export { helpLists, missingFlags, parseVersion };
|
|
18
|
+
//# sourceMappingURL=probe.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"probe.d.mts","names":[],"sources":["../../../../../../../../packages/ai-cli/src/probe/probe.ts"],"mappings":";;;;AA0BoD;AAapD;;;;AAA2C;iBAb3B,SAAA,CAAU,IAAA,UAAc,IAAY;;iBAapC,YAAA,CAAa,MAAc;;iBAa3B,YAAA,CAAa,IAAA,UAAc,QAA2B"}
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import { CliError } from "../errors.mjs";
|
|
2
|
+
import { killProcessTree } from "../spawn/process-tree.mjs";
|
|
3
|
+
import { buildInvocation } from "../spawn/invocation.mjs";
|
|
4
|
+
import { spawn } from "node:child_process";
|
|
5
|
+
|
|
6
|
+
//#region ../../packages/ai-cli/src/probe/probe.ts
|
|
7
|
+
/** How long a single `--help`/`--version` call may take. */
|
|
8
|
+
const PROBE_TIMEOUT_MS = 3e4;
|
|
9
|
+
/** Output cap per probe call; help screens are a few KiB. */
|
|
10
|
+
const PROBE_OUTPUT_LIMIT = 1024 * 1024;
|
|
11
|
+
function escapeRegExp(text) {
|
|
12
|
+
return text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Whether a help screen lists a flag or subcommand. Flags (`--json`, `-p`)
|
|
16
|
+
* must appear as a whole token — `--json` does not match `--json-schema`;
|
|
17
|
+
* a bare word (`exec`) is matched as a whole word.
|
|
18
|
+
*
|
|
19
|
+
* @example
|
|
20
|
+
* helpLists(" -p, --print Print response", "--print"); // true
|
|
21
|
+
* helpLists(" --output-format <format>", "--output"); // false
|
|
22
|
+
*/
|
|
23
|
+
function helpLists(help, flag) {
|
|
24
|
+
const escaped = escapeRegExp(flag);
|
|
25
|
+
if (flag.startsWith("-")) return new RegExp(`(^|[\\s,\\[(|/"'\`])${escaped}(?=$|[\\s,=\\])|<\\[.:;"'\`])`, "m").test(help);
|
|
26
|
+
return new RegExp(`(^|[^\\w-])${escaped}(?=$|[^\\w-])`, "m").test(help);
|
|
27
|
+
}
|
|
28
|
+
/** The first version-looking token of `--version` output, else its first line. */
|
|
29
|
+
function parseVersion(output) {
|
|
30
|
+
const match = /\d+\.\d+(?:\.\d+)?(?:[-+][0-9A-Za-z.-]+)?/.exec(output);
|
|
31
|
+
if (match) return match[0];
|
|
32
|
+
const firstLine = output.trim().split(/\r?\n/)[0]?.trim();
|
|
33
|
+
return firstLine && firstLine.length > 0 ? firstLine : "unknown";
|
|
34
|
+
}
|
|
35
|
+
/** Flags a plan requires that the help screen does not list. */
|
|
36
|
+
function missingFlags(help, required) {
|
|
37
|
+
return required.filter((flag) => !helpLists(help, flag));
|
|
38
|
+
}
|
|
39
|
+
/** Run a probe invocation and capture stdout + stderr (help is printed to either). */
|
|
40
|
+
function capture(input) {
|
|
41
|
+
return new Promise((resolve) => {
|
|
42
|
+
const invocation = buildInvocation(input.target, input.args, input.comspec);
|
|
43
|
+
if ("refused" in invocation) {
|
|
44
|
+
resolve({
|
|
45
|
+
output: "",
|
|
46
|
+
exitCode: null,
|
|
47
|
+
error: new Error(invocation.refused)
|
|
48
|
+
});
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
const child = spawn(invocation.file, invocation.args, {
|
|
52
|
+
cwd: input.cwd,
|
|
53
|
+
env: input.env,
|
|
54
|
+
shell: false,
|
|
55
|
+
windowsHide: true,
|
|
56
|
+
windowsVerbatimArguments: invocation.windowsVerbatimArguments,
|
|
57
|
+
detached: input.platform !== "win32",
|
|
58
|
+
stdio: [
|
|
59
|
+
"ignore",
|
|
60
|
+
"pipe",
|
|
61
|
+
"pipe"
|
|
62
|
+
]
|
|
63
|
+
});
|
|
64
|
+
const chunks = [];
|
|
65
|
+
let size = 0;
|
|
66
|
+
let settled = false;
|
|
67
|
+
const collect = (chunk) => {
|
|
68
|
+
if (size < PROBE_OUTPUT_LIMIT) {
|
|
69
|
+
chunks.push(chunk);
|
|
70
|
+
size += chunk.length;
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
const timer = setTimeout(() => {
|
|
74
|
+
killProcessTree(child, input.platform, 0);
|
|
75
|
+
}, PROBE_TIMEOUT_MS);
|
|
76
|
+
const finish = (exitCode, error) => {
|
|
77
|
+
if (settled) return;
|
|
78
|
+
settled = true;
|
|
79
|
+
clearTimeout(timer);
|
|
80
|
+
resolve({
|
|
81
|
+
output: Buffer.concat(chunks).toString("utf8"),
|
|
82
|
+
exitCode,
|
|
83
|
+
error
|
|
84
|
+
});
|
|
85
|
+
};
|
|
86
|
+
child.stdout?.on("data", collect);
|
|
87
|
+
child.stderr?.on("data", collect);
|
|
88
|
+
child.on("error", (error) => finish(null, error));
|
|
89
|
+
child.on("close", (code) => finish(code));
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Read the CLI's version and help screens. A help invocation that cannot
|
|
94
|
+
* run at all (spawn error, no output) fails the probe; a non-zero exit with
|
|
95
|
+
* output is accepted, because several CLIs exit 1 after printing help.
|
|
96
|
+
*/
|
|
97
|
+
async function probeCli(input) {
|
|
98
|
+
const { adapter } = input;
|
|
99
|
+
const versionRun = await capture({
|
|
100
|
+
...input,
|
|
101
|
+
args: adapter.probe.version
|
|
102
|
+
});
|
|
103
|
+
if (versionRun.error !== void 0) throw new CliError(`${adapter.title}: could not run "${input.target.executable} ${adapter.probe.version.join(" ")}": ${versionRun.error.message}`, {
|
|
104
|
+
type: "probe-failed",
|
|
105
|
+
adapter: adapter.name,
|
|
106
|
+
executable: input.target.executable,
|
|
107
|
+
cause: versionRun.error
|
|
108
|
+
});
|
|
109
|
+
const version = parseVersion(versionRun.output);
|
|
110
|
+
const helps = [];
|
|
111
|
+
for (const args of adapter.probe.help) {
|
|
112
|
+
const helpRun = await capture({
|
|
113
|
+
...input,
|
|
114
|
+
args
|
|
115
|
+
});
|
|
116
|
+
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.", {
|
|
117
|
+
type: "probe-failed",
|
|
118
|
+
adapter: adapter.name,
|
|
119
|
+
version,
|
|
120
|
+
executable: input.target.executable,
|
|
121
|
+
cause: helpRun.error
|
|
122
|
+
});
|
|
123
|
+
helps.push(helpRun.output);
|
|
124
|
+
}
|
|
125
|
+
return {
|
|
126
|
+
executable: input.target.executable,
|
|
127
|
+
version,
|
|
128
|
+
help: helps.join("\n")
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
/** Throw a `capability` error naming the first required flag the help does not list. */
|
|
132
|
+
function assertCapabilities(adapter, probe, required) {
|
|
133
|
+
const missing = missingFlags(probe.help, required);
|
|
134
|
+
const flag = missing[0];
|
|
135
|
+
if (flag === void 0) return;
|
|
136
|
+
const helpCommands = adapter.probe.help.map((args) => `${adapter.command} ${args.join(" ")}`).join(", ");
|
|
137
|
+
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}.`, {
|
|
138
|
+
type: "capability",
|
|
139
|
+
adapter: adapter.name,
|
|
140
|
+
flag,
|
|
141
|
+
version: probe.version,
|
|
142
|
+
executable: probe.executable
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
//#endregion
|
|
147
|
+
export { assertCapabilities, helpLists, missingFlags, parseVersion, probeCli };
|
|
148
|
+
//# sourceMappingURL=probe.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"probe.mjs","names":[],"sources":["../../../../../../../../packages/ai-cli/src/probe/probe.ts"],"sourcesContent":["import { spawn } from \"node:child_process\";\nimport type { CliAdapter, CliProbeResult } from \"../contracts\";\nimport { CliError } from \"../errors\";\nimport { killProcessTree } from \"../spawn/process-tree\";\nimport { buildInvocation } from \"../spawn/invocation\";\nimport type { SpawnTarget } from \"../spawn/resolve-command\";\n\n/** How long a single `--help`/`--version` call may take. */\nexport const PROBE_TIMEOUT_MS = 30_000;\n\n/** Output cap per probe call; help screens are a few KiB. */\nconst PROBE_OUTPUT_LIMIT = 1024 * 1024;\n\nfunction escapeRegExp(text: string): string {\n return text.replace(/[.*+?^${}()|[\\]\\\\]/g, \"\\\\$&\");\n}\n\n/**\n * Whether a help screen lists a flag or subcommand. Flags (`--json`, `-p`)\n * must appear as a whole token — `--json` does not match `--json-schema`;\n * a bare word (`exec`) is matched as a whole word.\n *\n * @example\n * helpLists(\" -p, --print Print response\", \"--print\"); // true\n * helpLists(\" --output-format <format>\", \"--output\"); // false\n */\nexport function helpLists(help: string, flag: string): boolean {\n const escaped = escapeRegExp(flag);\n\n if (flag.startsWith(\"-\")) {\n return new RegExp(`(^|[\\\\s,\\\\[(|/\"'\\`])${escaped}(?=$|[\\\\s,=\\\\])|<\\\\[.:;\"'\\`])`, \"m\").test(\n help,\n );\n }\n\n return new RegExp(`(^|[^\\\\w-])${escaped}(?=$|[^\\\\w-])`, \"m\").test(help);\n}\n\n/** The first version-looking token of `--version` output, else its first line. */\nexport function parseVersion(output: string): string {\n const match = /\\d+\\.\\d+(?:\\.\\d+)?(?:[-+][0-9A-Za-z.-]+)?/.exec(output);\n\n if (match) {\n return match[0];\n }\n\n const firstLine = output.trim().split(/\\r?\\n/)[0]?.trim();\n\n return firstLine && firstLine.length > 0 ? firstLine : \"unknown\";\n}\n\n/** Flags a plan requires that the help screen does not list. */\nexport function missingFlags(help: string, required: readonly string[]): string[] {\n return required.filter((flag) => !helpLists(help, flag));\n}\n\ntype CaptureInput = {\n target: SpawnTarget;\n args: string[];\n env: Record<string, string>;\n cwd: string;\n comspec: string;\n platform: NodeJS.Platform;\n};\n\n/** Run a probe invocation and capture stdout + stderr (help is printed to either). */\nfunction capture(\n input: CaptureInput,\n): Promise<{ output: string; exitCode: number | null; error?: Error }> {\n return new Promise((resolve) => {\n const invocation = buildInvocation(input.target, input.args, input.comspec);\n\n if (\"refused\" in invocation) {\n resolve({ output: \"\", exitCode: null, error: new Error(invocation.refused) });\n\n return;\n }\n\n const child = spawn(invocation.file, invocation.args, {\n cwd: input.cwd,\n env: input.env,\n shell: false,\n windowsHide: true,\n windowsVerbatimArguments: invocation.windowsVerbatimArguments,\n detached: input.platform !== \"win32\",\n stdio: [\"ignore\", \"pipe\", \"pipe\"],\n });\n\n const chunks: Buffer[] = [];\n let size = 0;\n let settled = false;\n\n const collect = (chunk: Buffer): void => {\n if (size < PROBE_OUTPUT_LIMIT) {\n chunks.push(chunk);\n size += chunk.length;\n }\n };\n\n const timer = setTimeout(() => {\n killProcessTree(child, input.platform, 0);\n }, PROBE_TIMEOUT_MS);\n\n const finish = (exitCode: number | null, error?: Error): void => {\n if (settled) {\n return;\n }\n\n settled = true;\n clearTimeout(timer);\n resolve({ output: Buffer.concat(chunks).toString(\"utf8\"), exitCode, error });\n };\n\n child.stdout?.on(\"data\", collect);\n child.stderr?.on(\"data\", collect);\n child.on(\"error\", (error) => finish(null, error));\n child.on(\"close\", (code) => finish(code));\n });\n}\n\nexport type ProbeInput = Omit<CaptureInput, \"args\"> & { adapter: CliAdapter };\n\n/**\n * Read the CLI's version and help screens. A help invocation that cannot\n * run at all (spawn error, no output) fails the probe; a non-zero exit with\n * output is accepted, because several CLIs exit 1 after printing help.\n */\nexport async function probeCli(input: ProbeInput): Promise<CliProbeResult> {\n const { adapter } = input;\n const versionRun = await capture({ ...input, args: adapter.probe.version });\n\n if (versionRun.error !== undefined) {\n throw new CliError(\n `${adapter.title}: could not run \"${input.target.executable} ${adapter.probe.version.join(\" \")}\": ${versionRun.error.message}`,\n {\n type: \"probe-failed\",\n adapter: adapter.name,\n executable: input.target.executable,\n cause: versionRun.error,\n },\n );\n }\n\n const version = parseVersion(versionRun.output);\n const helps: string[] = [];\n\n for (const args of adapter.probe.help) {\n const helpRun = await capture({ ...input, args });\n\n if (helpRun.error !== undefined || helpRun.output.trim().length === 0) {\n throw new CliError(\n `${adapter.title} ${version}: \"${input.target.executable} ${args.join(\" \")}\" printed no help` +\n (helpRun.error ? ` (${helpRun.error.message})` : \"\") +\n \"; the capability probe cannot verify the flags this adapter needs.\",\n {\n type: \"probe-failed\",\n adapter: adapter.name,\n version,\n executable: input.target.executable,\n cause: helpRun.error,\n },\n );\n }\n\n helps.push(helpRun.output);\n }\n\n return { executable: input.target.executable, version, help: helps.join(\"\\n\") };\n}\n\n/** Throw a `capability` error naming the first required flag the help does not list. */\nexport function assertCapabilities(\n adapter: CliAdapter,\n probe: CliProbeResult,\n required: readonly string[],\n): void {\n const missing = missingFlags(probe.help, required);\n const flag = missing[0];\n\n if (flag === undefined) {\n return;\n }\n\n const helpCommands = adapter.probe.help\n .map((args) => `${adapter.command} ${args.join(\" \")}`)\n .join(\", \");\n\n throw new CliError(\n `${adapter.title} ${probe.version} (${probe.executable}) does not support ${missing.join(\", \")}: ` +\n `not listed in \"${helpCommands}\". This adapter was verified against ${adapter.title} ` +\n `${adapter.verification.version}; install a compatible version or drop the option that needs ${flag}.`,\n {\n type: \"capability\",\n adapter: adapter.name,\n flag,\n version: probe.version,\n executable: probe.executable,\n },\n );\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
//#region ../../packages/ai-cli/src/session/aggregate.ts
|
|
2
|
+
function sum(a, b) {
|
|
3
|
+
return a === void 0 && b === void 0 ? void 0 : (a ?? 0) + (b ?? 0);
|
|
4
|
+
}
|
|
5
|
+
/** Add two usages field by field; optional fields stay absent unless either side has them. */
|
|
6
|
+
function addUsage(left, right) {
|
|
7
|
+
if (left === void 0) return { ...right };
|
|
8
|
+
const merged = {
|
|
9
|
+
input: left.input + right.input,
|
|
10
|
+
output: left.output + right.output,
|
|
11
|
+
total: left.total + right.total
|
|
12
|
+
};
|
|
13
|
+
const cachedTokens = sum(left.cachedTokens, right.cachedTokens);
|
|
14
|
+
const cacheWriteTokens = sum(left.cacheWriteTokens, right.cacheWriteTokens);
|
|
15
|
+
const reasoningTokens = sum(left.reasoningTokens, right.reasoningTokens);
|
|
16
|
+
const costUSD = sum(left.costUSD, right.costUSD);
|
|
17
|
+
if (cachedTokens !== void 0) merged.cachedTokens = cachedTokens;
|
|
18
|
+
if (cacheWriteTokens !== void 0) merged.cacheWriteTokens = cacheWriteTokens;
|
|
19
|
+
if (reasoningTokens !== void 0) merged.reasoningTokens = reasoningTokens;
|
|
20
|
+
if (costUSD !== void 0) merged.costUSD = costUSD;
|
|
21
|
+
return merged;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Folds parsed events into the pieces of a `CliResult`: the session ref,
|
|
25
|
+
* usage, every complete message, and the final answer.
|
|
26
|
+
*
|
|
27
|
+
* Final-answer rule: the last `final: true` message (the vendor's own
|
|
28
|
+
* result record) wins; otherwise the newest assistant text — a complete
|
|
29
|
+
* message, or the deltas streamed since the last message or tool activity.
|
|
30
|
+
*/
|
|
31
|
+
var ResultAggregator = class {
|
|
32
|
+
constructor(resume) {
|
|
33
|
+
this.messages = [];
|
|
34
|
+
this.deltaBuffer = "";
|
|
35
|
+
this.sessionRef = resume;
|
|
36
|
+
}
|
|
37
|
+
add(event) {
|
|
38
|
+
switch (event.type) {
|
|
39
|
+
case "session.started":
|
|
40
|
+
if (event.sessionRef) this.sessionRef = event.sessionRef;
|
|
41
|
+
break;
|
|
42
|
+
case "message":
|
|
43
|
+
if (event.delta) {
|
|
44
|
+
this.deltaBuffer += event.text;
|
|
45
|
+
break;
|
|
46
|
+
}
|
|
47
|
+
if (event.final) {
|
|
48
|
+
this.finalText = event.text;
|
|
49
|
+
break;
|
|
50
|
+
}
|
|
51
|
+
this.messages.push(event.text);
|
|
52
|
+
this.lastCompleteText = event.text;
|
|
53
|
+
this.deltaBuffer = "";
|
|
54
|
+
break;
|
|
55
|
+
case "tool.call":
|
|
56
|
+
case "tool.result":
|
|
57
|
+
this.flushDeltas();
|
|
58
|
+
break;
|
|
59
|
+
case "usage":
|
|
60
|
+
this.usage = event.cumulative ? { ...event.usage } : addUsage(this.usage, event.usage);
|
|
61
|
+
break;
|
|
62
|
+
case "error":
|
|
63
|
+
if (event.fatal && this.fatalError === void 0) this.fatalError = event.message;
|
|
64
|
+
break;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
/** Close the stream: deltas never followed by a complete message count as one. */
|
|
68
|
+
finish() {
|
|
69
|
+
this.flushDeltas();
|
|
70
|
+
}
|
|
71
|
+
get text() {
|
|
72
|
+
if (this.finalText !== void 0) return this.finalText;
|
|
73
|
+
return this.deltaBuffer.length > 0 ? this.deltaBuffer : this.lastCompleteText ?? "";
|
|
74
|
+
}
|
|
75
|
+
flushDeltas() {
|
|
76
|
+
if (this.deltaBuffer.trim().length > 0) {
|
|
77
|
+
this.messages.push(this.deltaBuffer);
|
|
78
|
+
this.lastCompleteText = this.deltaBuffer;
|
|
79
|
+
}
|
|
80
|
+
this.deltaBuffer = "";
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
//#endregion
|
|
85
|
+
export { ResultAggregator };
|
|
86
|
+
//# sourceMappingURL=aggregate.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"aggregate.mjs","names":[],"sources":["../../../../../../../../packages/ai-cli/src/session/aggregate.ts"],"sourcesContent":["import type { CliParsedEvent, CliUsage } from \"../contracts\";\n\nfunction sum(a: number | undefined, b: number | undefined): number | undefined {\n return a === undefined && b === undefined ? undefined : (a ?? 0) + (b ?? 0);\n}\n\n/** Add two usages field by field; optional fields stay absent unless either side has them. */\nexport function addUsage(left: CliUsage | undefined, right: CliUsage): CliUsage {\n if (left === undefined) {\n return { ...right };\n }\n\n const merged: CliUsage = {\n input: left.input + right.input,\n output: left.output + right.output,\n total: left.total + right.total,\n };\n\n const cachedTokens = sum(left.cachedTokens, right.cachedTokens);\n const cacheWriteTokens = sum(left.cacheWriteTokens, right.cacheWriteTokens);\n const reasoningTokens = sum(left.reasoningTokens, right.reasoningTokens);\n const costUSD = sum(left.costUSD, right.costUSD);\n\n if (cachedTokens !== undefined) merged.cachedTokens = cachedTokens;\n if (cacheWriteTokens !== undefined) merged.cacheWriteTokens = cacheWriteTokens;\n if (reasoningTokens !== undefined) merged.reasoningTokens = reasoningTokens;\n if (costUSD !== undefined) merged.costUSD = costUSD;\n\n return merged;\n}\n\n/**\n * Folds parsed events into the pieces of a `CliResult`: the session ref,\n * usage, every complete message, and the final answer.\n *\n * Final-answer rule: the last `final: true` message (the vendor's own\n * result record) wins; otherwise the newest assistant text — a complete\n * message, or the deltas streamed since the last message or tool activity.\n */\nexport class ResultAggregator {\n public sessionRef?: string;\n public usage?: CliUsage;\n public readonly messages: string[] = [];\n public fatalError?: string;\n private finalText?: string;\n private deltaBuffer = \"\";\n private lastCompleteText?: string;\n\n public constructor(resume?: string) {\n this.sessionRef = resume;\n }\n\n public add(event: CliParsedEvent): void {\n switch (event.type) {\n case \"session.started\":\n if (event.sessionRef) {\n this.sessionRef = event.sessionRef;\n }\n break;\n case \"message\":\n if (event.delta) {\n this.deltaBuffer += event.text;\n break;\n }\n\n if (event.final) {\n this.finalText = event.text;\n break;\n }\n\n this.messages.push(event.text);\n this.lastCompleteText = event.text;\n this.deltaBuffer = \"\";\n break;\n case \"tool.call\":\n case \"tool.result\":\n this.flushDeltas();\n break;\n case \"usage\":\n this.usage = event.cumulative ? { ...event.usage } : addUsage(this.usage, event.usage);\n break;\n case \"error\":\n if (event.fatal && this.fatalError === undefined) {\n this.fatalError = event.message;\n }\n break;\n }\n }\n\n /** Close the stream: deltas never followed by a complete message count as one. */\n public finish(): void {\n this.flushDeltas();\n }\n\n public get text(): string {\n if (this.finalText !== undefined) {\n return this.finalText;\n }\n\n return this.deltaBuffer.length > 0 ? this.deltaBuffer : (this.lastCompleteText ?? \"\");\n }\n\n private flushDeltas(): void {\n if (this.deltaBuffer.trim().length > 0) {\n this.messages.push(this.deltaBuffer);\n this.lastCompleteText = this.deltaBuffer;\n }\n\n this.deltaBuffer = \"\";\n }\n}\n"],"mappings":";AAEA,SAAS,IAAI,GAAuB,GAA2C;CAC7E,OAAO,MAAM,UAAa,MAAM,SAAY,UAAa,KAAK,MAAM,KAAK;AAC3E;;AAGA,SAAgB,SAAS,MAA4B,OAA2B;CAC9E,IAAI,SAAS,QACX,OAAO,EAAE,GAAG,MAAM;CAGpB,MAAM,SAAmB;EACvB,OAAO,KAAK,QAAQ,MAAM;EAC1B,QAAQ,KAAK,SAAS,MAAM;EAC5B,OAAO,KAAK,QAAQ,MAAM;CAC5B;CAEA,MAAM,eAAe,IAAI,KAAK,cAAc,MAAM,YAAY;CAC9D,MAAM,mBAAmB,IAAI,KAAK,kBAAkB,MAAM,gBAAgB;CAC1E,MAAM,kBAAkB,IAAI,KAAK,iBAAiB,MAAM,eAAe;CACvE,MAAM,UAAU,IAAI,KAAK,SAAS,MAAM,OAAO;CAE/C,IAAI,iBAAiB,QAAW,OAAO,eAAe;CACtD,IAAI,qBAAqB,QAAW,OAAO,mBAAmB;CAC9D,IAAI,oBAAoB,QAAW,OAAO,kBAAkB;CAC5D,IAAI,YAAY,QAAW,OAAO,UAAU;CAE5C,OAAO;AACT;;;;;;;;;AAUA,IAAa,mBAAb,MAA8B;CAS5B,AAAO,YAAY,QAAiB;kBANC,CAAC;qBAGhB;EAIpB,KAAK,aAAa;CACpB;CAEA,AAAO,IAAI,OAA6B;EACtC,QAAQ,MAAM,MAAd;GACE,KAAK;IACH,IAAI,MAAM,YACR,KAAK,aAAa,MAAM;IAE1B;GACF,KAAK;IACH,IAAI,MAAM,OAAO;KACf,KAAK,eAAe,MAAM;KAC1B;IACF;IAEA,IAAI,MAAM,OAAO;KACf,KAAK,YAAY,MAAM;KACvB;IACF;IAEA,KAAK,SAAS,KAAK,MAAM,IAAI;IAC7B,KAAK,mBAAmB,MAAM;IAC9B,KAAK,cAAc;IACnB;GACF,KAAK;GACL,KAAK;IACH,KAAK,YAAY;IACjB;GACF,KAAK;IACH,KAAK,QAAQ,MAAM,aAAa,EAAE,GAAG,MAAM,MAAM,IAAI,SAAS,KAAK,OAAO,MAAM,KAAK;IACrF;GACF,KAAK;IACH,IAAI,MAAM,SAAS,KAAK,eAAe,QACrC,KAAK,aAAa,MAAM;IAE1B;EACJ;CACF;;CAGA,AAAO,SAAe;EACpB,KAAK,YAAY;CACnB;CAEA,IAAW,OAAe;EACxB,IAAI,KAAK,cAAc,QACrB,OAAO,KAAK;EAGd,OAAO,KAAK,YAAY,SAAS,IAAI,KAAK,cAAe,KAAK,oBAAoB;CACpF;CAEA,AAAQ,cAAoB;EAC1B,IAAI,KAAK,YAAY,KAAK,CAAC,CAAC,SAAS,GAAG;GACtC,KAAK,SAAS,KAAK,KAAK,WAAW;GACnC,KAAK,mBAAmB,KAAK;EAC/B;EAEA,KAAK,cAAc;CACrB;AACF"}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
//#region ../../packages/ai-cli/src/session/environment.ts
|
|
2
|
+
/**
|
|
3
|
+
* Variables a CLI needs to start at all: executable lookup, home and temp
|
|
4
|
+
* directories, locale, TLS/proxy settings and the Windows system set.
|
|
5
|
+
* Passed through even with `inheritEnv: false`.
|
|
6
|
+
*/
|
|
7
|
+
const MINIMAL_ENV = [
|
|
8
|
+
"PATH",
|
|
9
|
+
"PATHEXT",
|
|
10
|
+
"HOME",
|
|
11
|
+
"USERPROFILE",
|
|
12
|
+
"HOMEDRIVE",
|
|
13
|
+
"HOMEPATH",
|
|
14
|
+
"APPDATA",
|
|
15
|
+
"LOCALAPPDATA",
|
|
16
|
+
"SYSTEMROOT",
|
|
17
|
+
"SYSTEMDRIVE",
|
|
18
|
+
"WINDIR",
|
|
19
|
+
"COMSPEC",
|
|
20
|
+
"TEMP",
|
|
21
|
+
"TMP",
|
|
22
|
+
"TMPDIR",
|
|
23
|
+
"LANG",
|
|
24
|
+
"LC_ALL",
|
|
25
|
+
"LC_CTYPE",
|
|
26
|
+
"TERM",
|
|
27
|
+
"SHELL",
|
|
28
|
+
"USER",
|
|
29
|
+
"LOGNAME",
|
|
30
|
+
"XDG_RUNTIME_DIR",
|
|
31
|
+
"HTTP_PROXY",
|
|
32
|
+
"HTTPS_PROXY",
|
|
33
|
+
"NO_PROXY",
|
|
34
|
+
"http_proxy",
|
|
35
|
+
"https_proxy",
|
|
36
|
+
"no_proxy",
|
|
37
|
+
"NODE_EXTRA_CA_CERTS",
|
|
38
|
+
"SSL_CERT_FILE",
|
|
39
|
+
"SSL_CERT_DIR"
|
|
40
|
+
];
|
|
41
|
+
/**
|
|
42
|
+
* Build a CLI's environment. Precedence, lowest first: inherited variables
|
|
43
|
+
* (minus the adapter's managed home variables), the adapter plan, the API
|
|
44
|
+
* key, then the caller's `env`. Windows variable names compare
|
|
45
|
+
* case-insensitively.
|
|
46
|
+
*/
|
|
47
|
+
function buildEnvironment(input) {
|
|
48
|
+
const normalize = (name) => input.platform === "win32" ? name.toUpperCase() : name;
|
|
49
|
+
const managed = new Set(input.managed.map(normalize));
|
|
50
|
+
const allowed = input.inherit === true ? void 0 : new Set([...MINIMAL_ENV, ...input.inherit === false ? [] : input.inherit].map(normalize));
|
|
51
|
+
const environment = {};
|
|
52
|
+
for (const [name, value] of Object.entries(input.source)) {
|
|
53
|
+
if (value === void 0 || managed.has(normalize(name))) continue;
|
|
54
|
+
if (allowed !== void 0 && !allowed.has(normalize(name))) continue;
|
|
55
|
+
environment[name] = value;
|
|
56
|
+
}
|
|
57
|
+
const assign = (name, value) => {
|
|
58
|
+
if (input.platform === "win32") {
|
|
59
|
+
for (const existing of Object.keys(environment)) if (existing.toUpperCase() === name.toUpperCase()) delete environment[existing];
|
|
60
|
+
}
|
|
61
|
+
environment[name] = value;
|
|
62
|
+
};
|
|
63
|
+
for (const [name, value] of Object.entries(input.plan ?? {})) assign(name, value);
|
|
64
|
+
if (input.apiKey !== void 0) assign(input.apiKey.name, input.apiKey.value);
|
|
65
|
+
for (const [name, value] of Object.entries(input.user ?? {})) assign(name, value);
|
|
66
|
+
return environment;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
//#endregion
|
|
70
|
+
export { buildEnvironment };
|
|
71
|
+
//# sourceMappingURL=environment.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"environment.mjs","names":[],"sources":["../../../../../../../../packages/ai-cli/src/session/environment.ts"],"sourcesContent":["/**\n * Variables a CLI needs to start at all: executable lookup, home and temp\n * directories, locale, TLS/proxy settings and the Windows system set.\n * Passed through even with `inheritEnv: false`.\n */\nexport const MINIMAL_ENV = [\n \"PATH\",\n \"PATHEXT\",\n \"HOME\",\n \"USERPROFILE\",\n \"HOMEDRIVE\",\n \"HOMEPATH\",\n \"APPDATA\",\n \"LOCALAPPDATA\",\n \"SYSTEMROOT\",\n \"SYSTEMDRIVE\",\n \"WINDIR\",\n \"COMSPEC\",\n \"TEMP\",\n \"TMP\",\n \"TMPDIR\",\n \"LANG\",\n \"LC_ALL\",\n \"LC_CTYPE\",\n \"TERM\",\n \"SHELL\",\n \"USER\",\n \"LOGNAME\",\n \"XDG_RUNTIME_DIR\",\n \"HTTP_PROXY\",\n \"HTTPS_PROXY\",\n \"NO_PROXY\",\n \"http_proxy\",\n \"https_proxy\",\n \"no_proxy\",\n \"NODE_EXTRA_CA_CERTS\",\n \"SSL_CERT_FILE\",\n \"SSL_CERT_DIR\",\n] as const;\n\nexport type BuildEnvironmentInput = {\n source: Record<string, string | undefined>;\n inherit: boolean | readonly string[];\n /** Variables the adapter manages (config home); never inherited from `source`. */\n managed: readonly string[];\n /** Adapter plan variables (config home, …). */\n plan?: Record<string, string>;\n /** Caller variables; win over everything else. */\n user?: Record<string, string>;\n /** API key and the variable it goes to. */\n apiKey?: { name: string; value: string };\n platform: NodeJS.Platform;\n};\n\n/**\n * Build a CLI's environment. Precedence, lowest first: inherited variables\n * (minus the adapter's managed home variables), the adapter plan, the API\n * key, then the caller's `env`. Windows variable names compare\n * case-insensitively.\n */\nexport function buildEnvironment(input: BuildEnvironmentInput): Record<string, string> {\n const normalize = (name: string): string =>\n input.platform === \"win32\" ? name.toUpperCase() : name;\n const managed = new Set(input.managed.map(normalize));\n const allowed =\n input.inherit === true\n ? undefined\n : new Set([...MINIMAL_ENV, ...(input.inherit === false ? [] : input.inherit)].map(normalize));\n\n const environment: Record<string, string> = {};\n\n for (const [name, value] of Object.entries(input.source)) {\n if (value === undefined || managed.has(normalize(name))) {\n continue;\n }\n\n if (allowed !== undefined && !allowed.has(normalize(name))) {\n continue;\n }\n\n environment[name] = value;\n }\n\n const assign = (name: string, value: string): void => {\n if (input.platform === \"win32\") {\n for (const existing of Object.keys(environment)) {\n if (existing.toUpperCase() === name.toUpperCase()) {\n delete environment[existing];\n }\n }\n }\n\n environment[name] = value;\n };\n\n for (const [name, value] of Object.entries(input.plan ?? {})) {\n assign(name, value);\n }\n\n if (input.apiKey !== undefined) {\n assign(input.apiKey.name, input.apiKey.value);\n }\n\n for (const [name, value] of Object.entries(input.user ?? {})) {\n assign(name, value);\n }\n\n return environment;\n}\n"],"mappings":";;;;;;AAKA,MAAa,cAAc;CACzB;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;AACF;;;;;;;AAsBA,SAAgB,iBAAiB,OAAsD;CACrF,MAAM,aAAa,SACjB,MAAM,aAAa,UAAU,KAAK,YAAY,IAAI;CACpD,MAAM,UAAU,IAAI,IAAI,MAAM,QAAQ,IAAI,SAAS,CAAC;CACpD,MAAM,UACJ,MAAM,YAAY,OACd,SACA,IAAI,IAAI,CAAC,GAAG,aAAa,GAAI,MAAM,YAAY,QAAQ,CAAC,IAAI,MAAM,OAAQ,CAAC,CAAC,IAAI,SAAS,CAAC;CAEhG,MAAM,cAAsC,CAAC;CAE7C,KAAK,MAAM,CAAC,MAAM,UAAU,OAAO,QAAQ,MAAM,MAAM,GAAG;EACxD,IAAI,UAAU,UAAa,QAAQ,IAAI,UAAU,IAAI,CAAC,GACpD;EAGF,IAAI,YAAY,UAAa,CAAC,QAAQ,IAAI,UAAU,IAAI,CAAC,GACvD;EAGF,YAAY,QAAQ;CACtB;CAEA,MAAM,UAAU,MAAc,UAAwB;EACpD,IAAI,MAAM,aAAa,SACrB;QAAK,MAAM,YAAY,OAAO,KAAK,WAAW,GAC5C,IAAI,SAAS,YAAY,MAAM,KAAK,YAAY,GAC9C,OAAO,YAAY;EAEvB;EAGF,YAAY,QAAQ;CACtB;CAEA,KAAK,MAAM,CAAC,MAAM,UAAU,OAAO,QAAQ,MAAM,QAAQ,CAAC,CAAC,GACzD,OAAO,MAAM,KAAK;CAGpB,IAAI,MAAM,WAAW,QACnB,OAAO,MAAM,OAAO,MAAM,MAAM,OAAO,KAAK;CAG9C,KAAK,MAAM,CAAC,MAAM,UAAU,OAAO,QAAQ,MAAM,QAAQ,CAAC,CAAC,GACzD,OAAO,MAAM,KAAK;CAGpB,OAAO;AACT"}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { chmod, mkdir, mkdtemp, rm, writeFile } from "node:fs/promises";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
|
|
5
|
+
//#region ../../packages/ai-cli/src/session/run-directory.ts
|
|
6
|
+
/**
|
|
7
|
+
* Create the run's private directory and resolve its config home.
|
|
8
|
+
*
|
|
9
|
+
* The run directory is `mkdtemp`'d under the OS temp dir with mode 0700, so
|
|
10
|
+
* other local users cannot read the MCP config (and the tokens in it)
|
|
11
|
+
* written there. A `"private"` home is a `home/` folder inside it.
|
|
12
|
+
*/
|
|
13
|
+
async function createRunDirectories(home, cwd) {
|
|
14
|
+
const runDir = await mkdtemp(path.join(tmpdir(), "warlock-cli-"));
|
|
15
|
+
await chmod(runDir, 448);
|
|
16
|
+
const dispose = async () => {
|
|
17
|
+
await rm(runDir, {
|
|
18
|
+
recursive: true,
|
|
19
|
+
force: true
|
|
20
|
+
});
|
|
21
|
+
};
|
|
22
|
+
if (home === "inherit") return {
|
|
23
|
+
runDir,
|
|
24
|
+
homeIsPrivate: false,
|
|
25
|
+
dispose
|
|
26
|
+
};
|
|
27
|
+
if (home === "private") {
|
|
28
|
+
const privateHome = path.join(runDir, "home");
|
|
29
|
+
await mkdir(privateHome, { mode: 448 });
|
|
30
|
+
return {
|
|
31
|
+
runDir,
|
|
32
|
+
home: privateHome,
|
|
33
|
+
homeIsPrivate: true,
|
|
34
|
+
dispose
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
const ownedHome = path.resolve(cwd, home);
|
|
38
|
+
await mkdir(ownedHome, {
|
|
39
|
+
recursive: true,
|
|
40
|
+
mode: 448
|
|
41
|
+
});
|
|
42
|
+
return {
|
|
43
|
+
runDir,
|
|
44
|
+
home: ownedHome,
|
|
45
|
+
homeIsPrivate: false,
|
|
46
|
+
dispose
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Write launch files with owner-only permissions (0600; parent directories
|
|
51
|
+
* 0700). Only paths inside the run directory or a private home are
|
|
52
|
+
* accepted, so an adapter can never overwrite a file in a caller-owned
|
|
53
|
+
* home or elsewhere on disk.
|
|
54
|
+
*/
|
|
55
|
+
async function writeLaunchFiles(files, directories) {
|
|
56
|
+
const roots = [directories.runDir];
|
|
57
|
+
for (const file of files) {
|
|
58
|
+
const target = path.resolve(file.path);
|
|
59
|
+
if (!roots.some((root) => {
|
|
60
|
+
const relative = path.relative(root, target);
|
|
61
|
+
return relative.length > 0 && !relative.startsWith("..") && !path.isAbsolute(relative);
|
|
62
|
+
})) throw new Error(`Refusing to write ${target}: launch files must live inside the run directory.`);
|
|
63
|
+
await mkdir(path.dirname(target), {
|
|
64
|
+
recursive: true,
|
|
65
|
+
mode: 448
|
|
66
|
+
});
|
|
67
|
+
await writeFile(target, file.content, { mode: 384 });
|
|
68
|
+
await chmod(target, 384);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
//#endregion
|
|
73
|
+
export { createRunDirectories, writeLaunchFiles };
|
|
74
|
+
//# sourceMappingURL=run-directory.mjs.map
|