@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/llms-full.txt
ADDED
|
@@ -0,0 +1,373 @@
|
|
|
1
|
+
# Warlock AI Cli — full skills
|
|
2
|
+
|
|
3
|
+
> Package: `@warlock.js/ai-cli`
|
|
4
|
+
|
|
5
|
+
> Generated artifact. Concatenates every SKILL.md and reference file under `@warlock.js/ai-cli/skills/`. Re-run `node scripts/generate-llms.mjs` after any change.
|
|
6
|
+
|
|
7
|
+
## run-cli-agents `@warlock.js/ai-cli/run-cli-agents/SKILL.md`
|
|
8
|
+
|
|
9
|
+
---
|
|
10
|
+
name: run-cli-agents
|
|
11
|
+
description: 'Run vendor coding CLIs headless as agents with @warlock.js/ai-cli. Triggers: `ai.cli`, `cli.claudeCode`, `cli.codex`, `cli.antigravity`, `cli.qwenCode`, `cli.kimi`, `cli.cursor`, `cli.copilot`, `cli.grok`, `cli.opencode`, `cli.amp`, `cli.droid`, `cli.mistralVibe`, `cli.aider`, `cli.reasonix`, `createCliAgent`, `CliSession`, `session.events`, `session.done`, `CliEvent`, `CliResult`, `sessionRef`, `resume`, `permission: "bypass"`, `CliMcpServers`, `home: "private"`, `inheritEnv`, `CliError`, `agent.model`, `cliModel`; ''run claude code from node'', ''orchestrate codex / copilot / cursor as workers'', ''stream a CLI agent''s tool calls'', ''resume a CLI session'', ''cancel a coding CLI and its child processes'', ''read token usage from a CLI run'', ''give a CLI MCP servers without putting tokens in argv'', ''use a CLI as an ai.agent model''; typical import `import { cli } from "@warlock.js/ai-cli"`. Skip: writing an adapter for another CLI — the `write-a-cli-adapter` topic; direct provider calls — the `run-ai-agent` topic of the `warlock-js-ai` skill.'
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
# Run coding CLIs as agents
|
|
15
|
+
|
|
16
|
+
`@warlock.js/ai-cli` drives vendor coding CLIs headless. One run is one session. The vendor's stream is reduced to one event model, and `done()` resolves the final text, the usage and a session reference you can resume. It ships fourteen adapters: Claude Code, Codex, Antigravity, Qwen Code, Kimi Code, Cursor, Copilot, Grok Build, OpenCode, Amp, Factory Droid, Mistral Vibe, Aider and Reasonix. The CLIs themselves are not installed by the package.
|
|
17
|
+
|
|
18
|
+
## Run a prompt and read the events
|
|
19
|
+
|
|
20
|
+
```ts
|
|
21
|
+
import { cli } from "@warlock.js/ai-cli";
|
|
22
|
+
|
|
23
|
+
const claude = cli.claudeCode({ model: "sonnet", apiKey: process.env.ANTHROPIC_API_KEY });
|
|
24
|
+
const session = await claude.run("Fix the failing test in src/math.spec.ts", {
|
|
25
|
+
cwd: "/srv/acme-api",
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
for await (const event of session.events) {
|
|
29
|
+
switch (event.type) {
|
|
30
|
+
case "session.started":
|
|
31
|
+
console.log("session", event.sessionRef);
|
|
32
|
+
break;
|
|
33
|
+
case "message":
|
|
34
|
+
if (event.delta) process.stdout.write(event.text);
|
|
35
|
+
break;
|
|
36
|
+
case "tool.call":
|
|
37
|
+
console.log("\ntool", event.name, event.input);
|
|
38
|
+
break;
|
|
39
|
+
case "tool.result":
|
|
40
|
+
console.log(event.isError ? "failed:" : "ok:", event.output.slice(0, 200));
|
|
41
|
+
break;
|
|
42
|
+
case "usage":
|
|
43
|
+
console.log("tokens", event.usage.total);
|
|
44
|
+
break;
|
|
45
|
+
case "error":
|
|
46
|
+
console.error(event.fatal ? "fatal:" : "warning:", event.message);
|
|
47
|
+
break;
|
|
48
|
+
case "session.ended":
|
|
49
|
+
console.log("ended", event.status, event.exitCode);
|
|
50
|
+
break;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const result = await session.done();
|
|
55
|
+
|
|
56
|
+
if (result.status === "completed") {
|
|
57
|
+
console.log(result.text, result.usage, result.sessionRef);
|
|
58
|
+
} else {
|
|
59
|
+
console.error(result.status, result.error, result.stderr);
|
|
60
|
+
}
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
- **`session.events`** replays from the first event on every iteration, and ends after `session.ended`.
|
|
64
|
+
- **`done()` never rejects.** Branch on `status`: `completed`, `failed`, `cancelled` or `timeout`.
|
|
65
|
+
- **`text`** is the vendor's final record, or else the last assistant message. `messages` holds every complete message.
|
|
66
|
+
- **`run()` rejects only for setup problems,** with a `CliError` whose `type` is one of:
|
|
67
|
+
- `not-found` — the executable is not on `PATH`;
|
|
68
|
+
- `capability` — `--help` lacks a needed flag; the error carries `flag` and `version`;
|
|
69
|
+
- `probe-failed` — the CLI's help could not be read;
|
|
70
|
+
- `unsupported-option` — the adapter cannot honour an option;
|
|
71
|
+
- `prompt-transport` — the prompt fits neither argv nor stdin.
|
|
72
|
+
|
|
73
|
+
`ai.cli` is the same namespace once `import "@warlock.js/ai-cli"` has run.
|
|
74
|
+
|
|
75
|
+
## Resume, cancel, time out
|
|
76
|
+
|
|
77
|
+
```ts
|
|
78
|
+
import { cli } from "@warlock.js/ai-cli";
|
|
79
|
+
|
|
80
|
+
const codex = cli.codex({ cwd: "/srv/acme-api" });
|
|
81
|
+
const plan = await (await codex.run("Plan the billing refactor; do not edit files")).done();
|
|
82
|
+
|
|
83
|
+
const controller = new AbortController();
|
|
84
|
+
const step = await codex.run("Implement step 1 of the plan", {
|
|
85
|
+
resume: plan.sessionRef,
|
|
86
|
+
signal: controller.signal,
|
|
87
|
+
timeoutMs: 15 * 60_000,
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
setTimeout(() => controller.abort(), 60_000); // or step.cancel()
|
|
91
|
+
|
|
92
|
+
const outcome = await step.done();
|
|
93
|
+
|
|
94
|
+
console.log(outcome.status); // "cancelled" after the abort
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
Abort and timeout kill the whole process tree. On POSIX that is the CLI's process group (SIGTERM, then SIGKILL after `killGraceMs`, default 2000); on Windows it is `taskkill /T /F` on its PID. A run whose signal is already aborted never spawns.
|
|
98
|
+
|
|
99
|
+
## Permissions — bypass is opt-in
|
|
100
|
+
|
|
101
|
+
```ts
|
|
102
|
+
import { cli } from "@warlock.js/ai-cli";
|
|
103
|
+
|
|
104
|
+
// Default "standard": the CLI's own non-interactive rules; anything needing approval is refused.
|
|
105
|
+
const reviewer = cli.copilot({ cwd: "/srv/acme-api" });
|
|
106
|
+
|
|
107
|
+
// "bypass": every tool call approved without asking. Only for a sandboxed run.
|
|
108
|
+
const fixer = cli.grok({ cwd: "/sandbox/acme-api", permission: "bypass" });
|
|
109
|
+
|
|
110
|
+
const review = await (await reviewer.run("Review src/billing for bugs")).done();
|
|
111
|
+
const fix = await (
|
|
112
|
+
await fixer.run("Apply the fixes from the review", { permission: "bypass" })
|
|
113
|
+
).done();
|
|
114
|
+
|
|
115
|
+
console.log(review.text, fix.status);
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
What `"standard"` allows depends on the vendor:
|
|
119
|
+
|
|
120
|
+
- **Read-only:** Codex and Droid. Allow edits with `args: ["-c", "sandbox_mode=workspace-write"]` (Codex) or `args: ["--auto", "medium"]` (Droid).
|
|
121
|
+
- **Permissive:** OpenCode and Amp.
|
|
122
|
+
- **Refused:** Kimi Code always approves in print mode, so its adapter rejects `"standard"` and needs `"bypass"`.
|
|
123
|
+
|
|
124
|
+
## MCP servers, secrets and homes
|
|
125
|
+
|
|
126
|
+
```ts
|
|
127
|
+
import { cli } from "@warlock.js/ai-cli";
|
|
128
|
+
|
|
129
|
+
const claude = cli.claudeCode({
|
|
130
|
+
apiKey: process.env.ANTHROPIC_API_KEY, // exported as ANTHROPIC_API_KEY, never argv
|
|
131
|
+
inheritEnv: ["GITHUB_TOKEN"], // minimal system env + these names only
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
const session = await claude.run("Triage the open issues", {
|
|
135
|
+
mcp: {
|
|
136
|
+
tracker: {
|
|
137
|
+
type: "stdio",
|
|
138
|
+
command: "npx",
|
|
139
|
+
args: ["-y", "@acme/tracker-mcp"],
|
|
140
|
+
env: { TRACKER_TOKEN: process.env.TRACKER_TOKEN ?? "" },
|
|
141
|
+
},
|
|
142
|
+
docs: {
|
|
143
|
+
type: "http",
|
|
144
|
+
url: "https://mcp.acme.dev/mcp",
|
|
145
|
+
headers: { Authorization: `Bearer ${process.env.DOCS_TOKEN ?? ""}` },
|
|
146
|
+
},
|
|
147
|
+
},
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
console.log((await session.done()).text);
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
**MCP servers.** They are written as a mode-0600 file inside a private per-run directory (mode 0700), never into argv, and the file is removed after the run. CLIs that read MCP only from their config home (Kimi, Grok, Cursor, Droid, Antigravity, Mistral Vibe, Reasonix) need the default private home. Aider has no MCP support.
|
|
154
|
+
|
|
155
|
+
**Homes.** The `home` option takes one of:
|
|
156
|
+
|
|
157
|
+
- `"private"` (default) — a fresh config home per run;
|
|
158
|
+
- an absolute path — a home you own, for example one you logged in to;
|
|
159
|
+
- `"inherit"` — your normal config.
|
|
160
|
+
|
|
161
|
+
The adapter's home variable (`CLAUDE_CONFIG_DIR`, `CODEX_HOME`, `COPILOT_HOME`, `GROK_HOME`, …) is pointed at it.
|
|
162
|
+
|
|
163
|
+
## Use a CLI as an `ai.agent` model
|
|
164
|
+
|
|
165
|
+
```ts
|
|
166
|
+
import { ai, budget } from "@warlock.js/ai";
|
|
167
|
+
import { cli } from "@warlock.js/ai-cli";
|
|
168
|
+
|
|
169
|
+
const coder = cli.codex({ cwd: "/srv/acme-api" });
|
|
170
|
+
|
|
171
|
+
const reviewer = ai.agent({
|
|
172
|
+
model: coder.model({ name: "codex-reviewer" }),
|
|
173
|
+
middleware: [budget({ maxTokens: 200_000 })],
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
const review = await reviewer.execute("Review the last commit for bugs.");
|
|
177
|
+
|
|
178
|
+
console.log(review.text, review.usage.total);
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
How the model behaves:
|
|
182
|
+
|
|
183
|
+
- **One fresh CLI session per call.** System messages lead the prompt, and a history is rendered as a transcript.
|
|
184
|
+
- **The CLI uses its own tools.** A call with agent `tools` is refused; give the CLI `mcp` servers instead.
|
|
185
|
+
- **Streaming yields the final answer once the session ends.**
|
|
186
|
+
- **A failed run throws `CliError` with `type: "run-failed"`,** so `fallbackModel` works.
|
|
187
|
+
- **Pass `pricing`** for USD cost.
|
|
188
|
+
|
|
189
|
+
## Pitfalls
|
|
190
|
+
|
|
191
|
+
- Install and log in to the vendor CLI yourself. With the default private home, authenticate with `apiKey`/`env`.
|
|
192
|
+
- `effort` is refused by CLIs without an effort control (Qwen Code, Kimi, Cursor, Mistral Vibe), and `model` is refused by Amp. Nothing is silently ignored.
|
|
193
|
+
- `args` is passed verbatim; never put a token there.
|
|
194
|
+
- The vendor table in the docs ("Vendor adapters") lists each CLI's flags, stream format, and the version it was verified against.
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
## write-a-cli-adapter `@warlock.js/ai-cli/write-a-cli-adapter/SKILL.md`
|
|
198
|
+
|
|
199
|
+
---
|
|
200
|
+
name: write-a-cli-adapter
|
|
201
|
+
description: 'Write a CliAdapter for a coding CLI that @warlock.js/ai-cli does not ship. Triggers: `CliAdapter`, `CliLaunchContext`, `CliLaunchPlan`, `CliStreamParser`, `cli.custom`, `createCliAgent`, `jsonLinesParser`, `stringField`, `numberField`, `usageOf`, `mcpServersJson`, `jsonFile`, `toToml`, `mapEffort`, `argvPrompt`, `promptFile`, `requirePrivateHome`, `unsupportedOption`, `createClaudeStreamParser`, `helpLists`, `missingFlags`, `requires`, `verification`; ''add a new coding CLI'', ''support another agent CLI'', ''parse a vendor JSONL stream'', ''adapter for a new CLI version''; typical import `import { cli, jsonLinesParser, type CliAdapter } from "@warlock.js/ai-cli"`. Skip: using the built-in adapters — the `run-cli-agents` topic.'
|
|
202
|
+
---
|
|
203
|
+
|
|
204
|
+
# Write a CLI adapter
|
|
205
|
+
|
|
206
|
+
An adapter is a plain object with two jobs: `plan(context)` builds argv, stdin, environment and files for one run, and `parser(context)` turns stdout lines into the unified events. The runner does the rest: executable lookup, the Windows `.cmd` rules, the capability probe, homes, secrets, cancellation and the result.
|
|
207
|
+
|
|
208
|
+
```ts
|
|
209
|
+
import path from "node:path";
|
|
210
|
+
import {
|
|
211
|
+
cli,
|
|
212
|
+
jsonFile,
|
|
213
|
+
jsonLinesParser,
|
|
214
|
+
mcpServersJson,
|
|
215
|
+
numberField,
|
|
216
|
+
stringField,
|
|
217
|
+
usageOf,
|
|
218
|
+
type CliAdapter,
|
|
219
|
+
type CliLaunchFile,
|
|
220
|
+
} from "@warlock.js/ai-cli";
|
|
221
|
+
|
|
222
|
+
export const acmeAdapter: CliAdapter = {
|
|
223
|
+
name: "acme",
|
|
224
|
+
title: "Acme Agent",
|
|
225
|
+
command: "acme",
|
|
226
|
+
apiKeyEnv: ["ACME_API_KEY"],
|
|
227
|
+
homeEnv: ["ACME_HOME"],
|
|
228
|
+
transports: ["argv", "stdin"],
|
|
229
|
+
effort: false,
|
|
230
|
+
mcp: true,
|
|
231
|
+
probe: { help: [["run", "--help"]], version: ["--version"] },
|
|
232
|
+
verification: {
|
|
233
|
+
version: "3.2.0",
|
|
234
|
+
verifiedBy: "help",
|
|
235
|
+
sources: ["https://acme.dev/docs/cli"],
|
|
236
|
+
checkedAt: "2026-10-17",
|
|
237
|
+
},
|
|
238
|
+
plan(context) {
|
|
239
|
+
const args = ["run", "--json"];
|
|
240
|
+
const requires = ["--json"];
|
|
241
|
+
const files: CliLaunchFile[] = [];
|
|
242
|
+
|
|
243
|
+
if (context.model !== undefined) {
|
|
244
|
+
args.push("--model", context.model);
|
|
245
|
+
requires.push("--model");
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
if (context.resume !== undefined) {
|
|
249
|
+
args.push("--resume", context.resume);
|
|
250
|
+
requires.push("--resume");
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
if (context.permission === "bypass") {
|
|
254
|
+
args.push("--yes-to-all");
|
|
255
|
+
requires.push("--yes-to-all");
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
if (context.mcp !== undefined) {
|
|
259
|
+
const file = path.join(context.runDir, "mcp.json");
|
|
260
|
+
|
|
261
|
+
files.push({ path: file, content: jsonFile({ mcpServers: mcpServersJson(context.mcp) }) });
|
|
262
|
+
args.push("--mcp-config", file);
|
|
263
|
+
requires.push("--mcp-config");
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
return {
|
|
267
|
+
args,
|
|
268
|
+
tail: context.transport === "argv" ? [context.prompt] : [],
|
|
269
|
+
stdin: context.transport === "stdin" ? context.prompt : undefined,
|
|
270
|
+
env: context.home !== undefined ? { ACME_HOME: context.home } : undefined,
|
|
271
|
+
files,
|
|
272
|
+
requires,
|
|
273
|
+
};
|
|
274
|
+
},
|
|
275
|
+
parser: () =>
|
|
276
|
+
jsonLinesParser((record) => {
|
|
277
|
+
switch (record.type) {
|
|
278
|
+
case "start":
|
|
279
|
+
return [{ type: "session.started", sessionRef: stringField(record, "id") }];
|
|
280
|
+
case "text":
|
|
281
|
+
return [{ type: "message", text: stringField(record, "text") ?? "", delta: true }];
|
|
282
|
+
case "tool":
|
|
283
|
+
return [
|
|
284
|
+
{
|
|
285
|
+
type: "tool.call",
|
|
286
|
+
id: String(record.id),
|
|
287
|
+
name: String(record.name),
|
|
288
|
+
input: record.input,
|
|
289
|
+
},
|
|
290
|
+
];
|
|
291
|
+
case "tool_done":
|
|
292
|
+
return [
|
|
293
|
+
{
|
|
294
|
+
type: "tool.result",
|
|
295
|
+
id: String(record.id),
|
|
296
|
+
output: String(record.output),
|
|
297
|
+
isError: record.ok !== true,
|
|
298
|
+
},
|
|
299
|
+
];
|
|
300
|
+
case "usage":
|
|
301
|
+
return [
|
|
302
|
+
{
|
|
303
|
+
type: "usage",
|
|
304
|
+
cumulative: true,
|
|
305
|
+
usage: usageOf({
|
|
306
|
+
input: numberField(record, "in"),
|
|
307
|
+
output: numberField(record, "out"),
|
|
308
|
+
}),
|
|
309
|
+
},
|
|
310
|
+
];
|
|
311
|
+
case "error":
|
|
312
|
+
return [{ type: "error", message: String(record.message), fatal: true }];
|
|
313
|
+
default:
|
|
314
|
+
return [];
|
|
315
|
+
}
|
|
316
|
+
}),
|
|
317
|
+
};
|
|
318
|
+
|
|
319
|
+
const acme = cli.custom(acmeAdapter);
|
|
320
|
+
const result = await (await acme.run("Add a health check endpoint", { cwd: "/srv/api" })).done();
|
|
321
|
+
|
|
322
|
+
console.log(result.status, result.text);
|
|
323
|
+
```
|
|
324
|
+
|
|
325
|
+
## Rules
|
|
326
|
+
|
|
327
|
+
- **`requires`:** list every flag and subcommand the plan uses, spelled as `--help` prints it. The probe then fails the run up front, naming the flag and the installed version, if a release drops or renames one. Leave out only a flag the CLI accepts but no longer documents, and say so in a comment.
|
|
328
|
+
- **Secrets:** never put one in `args` or `tail`. Use `env`, or a file under `context.runDir` (written 0600, removed after the run). A home-level config file may be written only when `context.homeIsPrivate`; `requirePrivateHome(context, adapter, "MCP servers")` fails closed otherwise.
|
|
329
|
+
- **Prompt placement:** `tail` goes after the caller's `args`. Wrap a positional prompt in `argvPrompt(prompt)`, so a prompt starting with `-` is not read as a flag. For a `"file"` transport, `promptFile(context)` writes the prompt into the run directory.
|
|
330
|
+
- **Effort:** `mapEffort(effort, { low: "low", high: "high" })` follows the framework clamping rule. Set `effort: true` only when you map it. The runner refuses `effort` for adapters that do not.
|
|
331
|
+
- **Unsupported options:** `throw unsupportedOption(adapter, "why")` for an option the CLI cannot honour; never drop it silently.
|
|
332
|
+
- **Failures:** report a vendor failure as `{ type: "error", fatal: true }`, so a CLI that exits 0 still ends `failed`.
|
|
333
|
+
- **Late session ids:** if the id arrives only at the end, return it from the parser's `sessionRef()`. If the CLI writes its result to a file, point it at `context.runDir` and read the file in the parser's `end()`.
|
|
334
|
+
- **Claude-style streams:** for a CLI that reproduces Claude Code's `stream-json`, reuse `createClaudeStreamParser({ usage: "anthropic" | "inclusive" })`.
|
|
335
|
+
- **Verification:** record `verification` — the version, the sources, and `"help"` or `"docs"`.
|
|
336
|
+
|
|
337
|
+
## Test without the real CLI
|
|
338
|
+
|
|
339
|
+
```ts
|
|
340
|
+
// acme-adapter.spec.ts — `acmeAdapter` from above; `readFileSync` from "node:fs",
|
|
341
|
+
// `missingFlags` from "@warlock.js/ai-cli", `expect`/`it` from "vitest".
|
|
342
|
+
it("normalizes a recorded stream and needs only listed flags", () => {
|
|
343
|
+
const parser = acmeAdapter.parser({
|
|
344
|
+
prompt: "x",
|
|
345
|
+
transport: "argv",
|
|
346
|
+
cwd: "/work",
|
|
347
|
+
permission: "standard",
|
|
348
|
+
homeIsPrivate: true,
|
|
349
|
+
runDir: "/tmp/run",
|
|
350
|
+
platform: "linux",
|
|
351
|
+
});
|
|
352
|
+
const events = readFileSync("fixtures/acme/stream.jsonl", "utf8")
|
|
353
|
+
.split("\n")
|
|
354
|
+
.flatMap((line) => parser.line(line));
|
|
355
|
+
|
|
356
|
+
expect(events[0]).toMatchObject({ type: "session.started" });
|
|
357
|
+
|
|
358
|
+
const plan = acmeAdapter.plan({
|
|
359
|
+
prompt: "x",
|
|
360
|
+
transport: "argv",
|
|
361
|
+
cwd: "/work",
|
|
362
|
+
model: "fast",
|
|
363
|
+
permission: "bypass",
|
|
364
|
+
homeIsPrivate: true,
|
|
365
|
+
runDir: "/tmp/run",
|
|
366
|
+
platform: "linux",
|
|
367
|
+
});
|
|
368
|
+
|
|
369
|
+
expect(missingFlags(readFileSync("fixtures/acme/help.txt", "utf8"), plan.requires)).toEqual([]);
|
|
370
|
+
});
|
|
371
|
+
```
|
|
372
|
+
|
|
373
|
+
|
package/llms.txt
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
# Warlock AI Cli
|
|
2
|
+
|
|
3
|
+
> Package: `@warlock.js/ai-cli`
|
|
4
|
+
|
|
5
|
+
> Drive vendor coding-agent CLIs (Claude Code, Codex, Copilot, Cursor, …) as first-class @warlock.js/ai agents
|
|
6
|
+
|
|
7
|
+
## Skills
|
|
8
|
+
|
|
9
|
+
- [run-cli-agents](@warlock.js/ai-cli/run-cli-agents/SKILL.md): Run vendor coding CLIs headless as agents with @warlock.js/ai-cli. Triggers: `ai.cli`, `cli.claudeCode`, `cli.codex`, `cli.antigravity`, `cli.qwenCode`, `cli.kimi`, `cli.cursor`, `cli.copilot`, `cli.grok`, `cli.opencode`, `cli.amp`, `cli.droid`, `cli.mistralVibe`, `cli.aider`, `cli.reasonix`, `createCliAgent`, `CliSession`, `session.events`, `session.done`, `CliEvent`, `CliResult`, `sessionRef`, `resume`, `permission: "bypass"`, `CliMcpServers`, `home: "private"`, `inheritEnv`, `CliError`, `agent.model`, `cliModel`; 'run claude code from node', 'orchestrate codex / copilot / cursor as workers', 'stream a CLI agent's tool calls', 'resume a CLI session', 'cancel a coding CLI and its child processes', 'read token usage from a CLI run', 'give a CLI MCP servers without putting tokens in argv', 'use a CLI as an ai.agent model'; typical import `import { cli } from "@warlock.js/ai-cli"`. Skip: writing an adapter for another CLI — the `write-a-cli-adapter` topic; direct provider calls — the `run-ai-agent` topic of the `warlock-js-ai` skill.
|
|
10
|
+
- [write-a-cli-adapter](@warlock.js/ai-cli/write-a-cli-adapter/SKILL.md): Write a CliAdapter for a coding CLI that @warlock.js/ai-cli does not ship. Triggers: `CliAdapter`, `CliLaunchContext`, `CliLaunchPlan`, `CliStreamParser`, `cli.custom`, `createCliAgent`, `jsonLinesParser`, `stringField`, `numberField`, `usageOf`, `mcpServersJson`, `jsonFile`, `toToml`, `mapEffort`, `argvPrompt`, `promptFile`, `requirePrivateHome`, `unsupportedOption`, `createClaudeStreamParser`, `helpLists`, `missingFlags`, `requires`, `verification`; 'add a new coding CLI', 'support another agent CLI', 'parse a vendor JSONL stream', 'adapter for a new CLI version'; typical import `import { cli, jsonLinesParser, type CliAdapter } from "@warlock.js/ai-cli"`. Skip: using the built-in adapters — the `run-cli-agents` topic.
|
package/package.json
CHANGED
|
@@ -1,6 +1,41 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@warlock.js/ai-cli",
|
|
3
|
-
"
|
|
4
|
-
"
|
|
5
|
-
|
|
6
|
-
|
|
3
|
+
"description": "Drive vendor coding-agent CLIs (Claude Code, Codex, Copilot, Cursor, …) as first-class @warlock.js/ai agents",
|
|
4
|
+
"keywords": [
|
|
5
|
+
"warlock",
|
|
6
|
+
"ai",
|
|
7
|
+
"agent",
|
|
8
|
+
"cli",
|
|
9
|
+
"claude-code",
|
|
10
|
+
"codex",
|
|
11
|
+
"copilot",
|
|
12
|
+
"cursor",
|
|
13
|
+
"coding-agent"
|
|
14
|
+
],
|
|
15
|
+
"author": "Hasan Zohdy",
|
|
16
|
+
"license": "MIT",
|
|
17
|
+
"repository": {
|
|
18
|
+
"type": "git",
|
|
19
|
+
"url": "git+https://github.com/warlockjs/framework.git",
|
|
20
|
+
"directory": "packages/ai-cli"
|
|
21
|
+
},
|
|
22
|
+
"peerDependencies": {
|
|
23
|
+
"@warlock.js/ai": "5.31.0"
|
|
24
|
+
},
|
|
25
|
+
"version": "5.31.0",
|
|
26
|
+
"main": "./cjs/index.cjs",
|
|
27
|
+
"module": "./esm/index.mjs",
|
|
28
|
+
"types": "./esm/index.d.mts",
|
|
29
|
+
"exports": {
|
|
30
|
+
".": {
|
|
31
|
+
"import": {
|
|
32
|
+
"types": "./esm/index.d.mts",
|
|
33
|
+
"default": "./esm/index.mjs"
|
|
34
|
+
},
|
|
35
|
+
"require": {
|
|
36
|
+
"types": "./esm/index.d.mts",
|
|
37
|
+
"default": "./cjs/index.cjs"
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
package/skills/README.md
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# `@warlock.js/ai-cli` — skills index
|
|
2
|
+
|
|
3
|
+
Per-task skills. Cross-references name the topic ("the `<topic>` topic"), and for another package add its skill ("the `<topic>` topic of the `warlock-js-<pkg>` skill").
|
|
4
|
+
|
|
5
|
+
## Skills
|
|
6
|
+
|
|
7
|
+
### `run-cli-agents`
|
|
8
|
+
|
|
9
|
+
Drive a vendor coding CLI headless with `cli.<vendor>({...})` / `ai.cli.<vendor>` — `run(prompt, { cwd, resume, signal, model, effort, permission, mcp, env })`, the unified `session.events` (`session.started`, `message`, `tool.call`, `tool.result`, `usage`, `error`, `session.ended`), `session.done()` (`status`, `text`, `usage`, `sessionRef`, `exitCode`), resume, cancellation and timeouts, the opt-in `permission: "bypass"`, per-run MCP servers, config homes, `CliError` types, and `agent.model()` to run a CLI as an `ai.agent` model. Load when orchestrating Claude Code, Codex, Copilot, Cursor, Grok, Kimi, Qwen Code, OpenCode, Amp, Droid, Antigravity, Mistral Vibe, Aider or Reasonix from code.
|
|
10
|
+
|
|
11
|
+
### `write-a-cli-adapter`
|
|
12
|
+
|
|
13
|
+
Implement a `CliAdapter` for a coding CLI the package does not ship — the launch plan (`args`, `tail`, `stdin`, `env`, `files`, `requires`), the stream parser (`jsonLinesParser`, `sessionRef()`, reading a result file in `end()`), the verification record, and testing against a recorded stream and `--help`. Load when adding a vendor or adjusting an adapter for a new CLI release.
|
package/skills/index.md
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: "Drive vendor coding-agent CLIs headless as @warlock.js/ai agents — Claude Code, Codex, Antigravity, Qwen Code, Kimi Code, Cursor, Copilot, Grok Build, OpenCode, Amp, Factory Droid, Mistral Vibe, Aider, Reasonix. Exports `cli`, `createCliAgent`, `cliModel`, `CliError`, the per-vendor adapters and the adapter building blocks; registers `ai.cli`. Use for: run a coding CLI from code, stream its tool calls and text as one event model, resume or cancel a CLI session, read its token usage, give a CLI per-run MCP servers without leaking tokens, use a CLI as an ai.agent model, add an adapter for another CLI. Not this package: direct model calls → @warlock.js/ai with a provider adapter; a policy-jailed shell/file workspace → @warlock.js/ai-workspace; MCP clients and servers → @warlock.js/ai-tools."
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
# @warlock.js/ai-cli
|
|
6
|
+
|
|
7
|
+
Many teams run the vendors' own agent CLIs on the subscriptions they already pay for. This package launches those CLIs headless, one session per run, and reduces each vendor's JSONL stream to one event model, with resume, process-tree cancellation, usage and per-run MCP servers. Importing the package registers `ai.cli` on the shared `ai` object.
|
|
8
|
+
|
|
9
|
+
## The 80% path
|
|
10
|
+
|
|
11
|
+
1. Pick a vendor factory and run a prompt; read `session.events` and `await session.done()` (`run-cli-agents`).
|
|
12
|
+
2. Resume with `done().sessionRef`, cancel with a `signal`, and attach MCP servers with `mcp`.
|
|
13
|
+
3. To give the CLI to `ai.agent` (budgets, guardrails, observers), use `agent.model()`.
|
|
14
|
+
4. For a CLI that is not built in, write an adapter (`write-a-cli-adapter`).
|
|
15
|
+
|
|
16
|
+
## Conventions and pitfalls
|
|
17
|
+
|
|
18
|
+
- `permission` defaults to `"standard"` (the CLI's own non-interactive rules); `"bypass"` must be opted into. Kimi Code has no standard mode and requires `"bypass"`.
|
|
19
|
+
- Secrets go in `apiKey`, `env`, or MCP `env`/`headers`, never in `args`.
|
|
20
|
+
- The default home is private per run: authenticate with `apiKey`/`env`, or pass a home you logged in to.
|
|
21
|
+
- `run()` rejects only for setup problems (`CliError`: `not-found`, `capability`, `unsupported-option`, …). `done()` never rejects; check `status`.
|
|
22
|
+
- The installed CLI's `--help` must list every flag the adapter uses, so a CLI release with renamed flags fails closed with the flag and version in the error.
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: run-cli-agents
|
|
3
|
+
description: 'Run vendor coding CLIs headless as agents with @warlock.js/ai-cli. Triggers: `ai.cli`, `cli.claudeCode`, `cli.codex`, `cli.antigravity`, `cli.qwenCode`, `cli.kimi`, `cli.cursor`, `cli.copilot`, `cli.grok`, `cli.opencode`, `cli.amp`, `cli.droid`, `cli.mistralVibe`, `cli.aider`, `cli.reasonix`, `createCliAgent`, `CliSession`, `session.events`, `session.done`, `CliEvent`, `CliResult`, `sessionRef`, `resume`, `permission: "bypass"`, `CliMcpServers`, `home: "private"`, `inheritEnv`, `CliError`, `agent.model`, `cliModel`; ''run claude code from node'', ''orchestrate codex / copilot / cursor as workers'', ''stream a CLI agent''s tool calls'', ''resume a CLI session'', ''cancel a coding CLI and its child processes'', ''read token usage from a CLI run'', ''give a CLI MCP servers without putting tokens in argv'', ''use a CLI as an ai.agent model''; typical import `import { cli } from "@warlock.js/ai-cli"`. Skip: writing an adapter for another CLI — the `write-a-cli-adapter` topic; direct provider calls — the `run-ai-agent` topic of the `warlock-js-ai` skill.'
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Run coding CLIs as agents
|
|
7
|
+
|
|
8
|
+
`@warlock.js/ai-cli` drives vendor coding CLIs headless. One run is one session. The vendor's stream is reduced to one event model, and `done()` resolves the final text, the usage and a session reference you can resume. It ships fourteen adapters: Claude Code, Codex, Antigravity, Qwen Code, Kimi Code, Cursor, Copilot, Grok Build, OpenCode, Amp, Factory Droid, Mistral Vibe, Aider and Reasonix. The CLIs themselves are not installed by the package.
|
|
9
|
+
|
|
10
|
+
## Run a prompt and read the events
|
|
11
|
+
|
|
12
|
+
```ts
|
|
13
|
+
import { cli } from "@warlock.js/ai-cli";
|
|
14
|
+
|
|
15
|
+
const claude = cli.claudeCode({ model: "sonnet", apiKey: process.env.ANTHROPIC_API_KEY });
|
|
16
|
+
const session = await claude.run("Fix the failing test in src/math.spec.ts", {
|
|
17
|
+
cwd: "/srv/acme-api",
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
for await (const event of session.events) {
|
|
21
|
+
switch (event.type) {
|
|
22
|
+
case "session.started":
|
|
23
|
+
console.log("session", event.sessionRef);
|
|
24
|
+
break;
|
|
25
|
+
case "message":
|
|
26
|
+
if (event.delta) process.stdout.write(event.text);
|
|
27
|
+
break;
|
|
28
|
+
case "tool.call":
|
|
29
|
+
console.log("\ntool", event.name, event.input);
|
|
30
|
+
break;
|
|
31
|
+
case "tool.result":
|
|
32
|
+
console.log(event.isError ? "failed:" : "ok:", event.output.slice(0, 200));
|
|
33
|
+
break;
|
|
34
|
+
case "usage":
|
|
35
|
+
console.log("tokens", event.usage.total);
|
|
36
|
+
break;
|
|
37
|
+
case "error":
|
|
38
|
+
console.error(event.fatal ? "fatal:" : "warning:", event.message);
|
|
39
|
+
break;
|
|
40
|
+
case "session.ended":
|
|
41
|
+
console.log("ended", event.status, event.exitCode);
|
|
42
|
+
break;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const result = await session.done();
|
|
47
|
+
|
|
48
|
+
if (result.status === "completed") {
|
|
49
|
+
console.log(result.text, result.usage, result.sessionRef);
|
|
50
|
+
} else {
|
|
51
|
+
console.error(result.status, result.error, result.stderr);
|
|
52
|
+
}
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
- **`session.events`** replays from the first event on every iteration, and ends after `session.ended`.
|
|
56
|
+
- **`done()` never rejects.** Branch on `status`: `completed`, `failed`, `cancelled` or `timeout`.
|
|
57
|
+
- **`text`** is the vendor's final record, or else the last assistant message. `messages` holds every complete message.
|
|
58
|
+
- **`run()` rejects only for setup problems,** with a `CliError` whose `type` is one of:
|
|
59
|
+
- `not-found` — the executable is not on `PATH`;
|
|
60
|
+
- `capability` — `--help` lacks a needed flag; the error carries `flag` and `version`;
|
|
61
|
+
- `probe-failed` — the CLI's help could not be read;
|
|
62
|
+
- `unsupported-option` — the adapter cannot honour an option;
|
|
63
|
+
- `prompt-transport` — the prompt fits neither argv nor stdin.
|
|
64
|
+
|
|
65
|
+
`ai.cli` is the same namespace once `import "@warlock.js/ai-cli"` has run.
|
|
66
|
+
|
|
67
|
+
## Resume, cancel, time out
|
|
68
|
+
|
|
69
|
+
```ts
|
|
70
|
+
import { cli } from "@warlock.js/ai-cli";
|
|
71
|
+
|
|
72
|
+
const codex = cli.codex({ cwd: "/srv/acme-api" });
|
|
73
|
+
const plan = await (await codex.run("Plan the billing refactor; do not edit files")).done();
|
|
74
|
+
|
|
75
|
+
const controller = new AbortController();
|
|
76
|
+
const step = await codex.run("Implement step 1 of the plan", {
|
|
77
|
+
resume: plan.sessionRef,
|
|
78
|
+
signal: controller.signal,
|
|
79
|
+
timeoutMs: 15 * 60_000,
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
setTimeout(() => controller.abort(), 60_000); // or step.cancel()
|
|
83
|
+
|
|
84
|
+
const outcome = await step.done();
|
|
85
|
+
|
|
86
|
+
console.log(outcome.status); // "cancelled" after the abort
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
Abort and timeout kill the whole process tree. On POSIX that is the CLI's process group (SIGTERM, then SIGKILL after `killGraceMs`, default 2000); on Windows it is `taskkill /T /F` on its PID. A run whose signal is already aborted never spawns.
|
|
90
|
+
|
|
91
|
+
## Permissions — bypass is opt-in
|
|
92
|
+
|
|
93
|
+
```ts
|
|
94
|
+
import { cli } from "@warlock.js/ai-cli";
|
|
95
|
+
|
|
96
|
+
// Default "standard": the CLI's own non-interactive rules; anything needing approval is refused.
|
|
97
|
+
const reviewer = cli.copilot({ cwd: "/srv/acme-api" });
|
|
98
|
+
|
|
99
|
+
// "bypass": every tool call approved without asking. Only for a sandboxed run.
|
|
100
|
+
const fixer = cli.grok({ cwd: "/sandbox/acme-api", permission: "bypass" });
|
|
101
|
+
|
|
102
|
+
const review = await (await reviewer.run("Review src/billing for bugs")).done();
|
|
103
|
+
const fix = await (
|
|
104
|
+
await fixer.run("Apply the fixes from the review", { permission: "bypass" })
|
|
105
|
+
).done();
|
|
106
|
+
|
|
107
|
+
console.log(review.text, fix.status);
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
What `"standard"` allows depends on the vendor:
|
|
111
|
+
|
|
112
|
+
- **Read-only:** Codex and Droid. Allow edits with `args: ["-c", "sandbox_mode=workspace-write"]` (Codex) or `args: ["--auto", "medium"]` (Droid).
|
|
113
|
+
- **Permissive:** OpenCode and Amp.
|
|
114
|
+
- **Refused:** Kimi Code always approves in print mode, so its adapter rejects `"standard"` and needs `"bypass"`.
|
|
115
|
+
|
|
116
|
+
## MCP servers, secrets and homes
|
|
117
|
+
|
|
118
|
+
```ts
|
|
119
|
+
import { cli } from "@warlock.js/ai-cli";
|
|
120
|
+
|
|
121
|
+
const claude = cli.claudeCode({
|
|
122
|
+
apiKey: process.env.ANTHROPIC_API_KEY, // exported as ANTHROPIC_API_KEY, never argv
|
|
123
|
+
inheritEnv: ["GITHUB_TOKEN"], // minimal system env + these names only
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
const session = await claude.run("Triage the open issues", {
|
|
127
|
+
mcp: {
|
|
128
|
+
tracker: {
|
|
129
|
+
type: "stdio",
|
|
130
|
+
command: "npx",
|
|
131
|
+
args: ["-y", "@acme/tracker-mcp"],
|
|
132
|
+
env: { TRACKER_TOKEN: process.env.TRACKER_TOKEN ?? "" },
|
|
133
|
+
},
|
|
134
|
+
docs: {
|
|
135
|
+
type: "http",
|
|
136
|
+
url: "https://mcp.acme.dev/mcp",
|
|
137
|
+
headers: { Authorization: `Bearer ${process.env.DOCS_TOKEN ?? ""}` },
|
|
138
|
+
},
|
|
139
|
+
},
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
console.log((await session.done()).text);
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
**MCP servers.** They are written as a mode-0600 file inside a private per-run directory (mode 0700), never into argv, and the file is removed after the run. CLIs that read MCP only from their config home (Kimi, Grok, Cursor, Droid, Antigravity, Mistral Vibe, Reasonix) need the default private home. Aider has no MCP support.
|
|
146
|
+
|
|
147
|
+
**Homes.** The `home` option takes one of:
|
|
148
|
+
|
|
149
|
+
- `"private"` (default) — a fresh config home per run;
|
|
150
|
+
- an absolute path — a home you own, for example one you logged in to;
|
|
151
|
+
- `"inherit"` — your normal config.
|
|
152
|
+
|
|
153
|
+
The adapter's home variable (`CLAUDE_CONFIG_DIR`, `CODEX_HOME`, `COPILOT_HOME`, `GROK_HOME`, …) is pointed at it.
|
|
154
|
+
|
|
155
|
+
## Use a CLI as an `ai.agent` model
|
|
156
|
+
|
|
157
|
+
```ts
|
|
158
|
+
import { ai, budget } from "@warlock.js/ai";
|
|
159
|
+
import { cli } from "@warlock.js/ai-cli";
|
|
160
|
+
|
|
161
|
+
const coder = cli.codex({ cwd: "/srv/acme-api" });
|
|
162
|
+
|
|
163
|
+
const reviewer = ai.agent({
|
|
164
|
+
model: coder.model({ name: "codex-reviewer" }),
|
|
165
|
+
middleware: [budget({ maxTokens: 200_000 })],
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
const review = await reviewer.execute("Review the last commit for bugs.");
|
|
169
|
+
|
|
170
|
+
console.log(review.text, review.usage.total);
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
How the model behaves:
|
|
174
|
+
|
|
175
|
+
- **One fresh CLI session per call.** System messages lead the prompt, and a history is rendered as a transcript.
|
|
176
|
+
- **The CLI uses its own tools.** A call with agent `tools` is refused; give the CLI `mcp` servers instead.
|
|
177
|
+
- **Streaming yields the final answer once the session ends.**
|
|
178
|
+
- **A failed run throws `CliError` with `type: "run-failed"`,** so `fallbackModel` works.
|
|
179
|
+
- **Pass `pricing`** for USD cost.
|
|
180
|
+
|
|
181
|
+
## Pitfalls
|
|
182
|
+
|
|
183
|
+
- Install and log in to the vendor CLI yourself. With the default private home, authenticate with `apiKey`/`env`.
|
|
184
|
+
- `effort` is refused by CLIs without an effort control (Qwen Code, Kimi, Cursor, Mistral Vibe), and `model` is refused by Amp. Nothing is silently ignored.
|
|
185
|
+
- `args` is passed verbatim; never put a token there.
|
|
186
|
+
- The vendor table in the docs ("Vendor adapters") lists each CLI's flags, stream format, and the version it was verified against.
|