@resolflow/cli 0.0.0-stage → 0.1.0-beta.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Resolflow
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,202 @@
1
- # Temporary Holding Version
1
+ # Resolflow CLI (Beta)
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ `resolflow` runs your [Resolflow](https://resolflow.ai) agent profiles on **your own machine**: on a local model
4
+ server (Ollama, LM Studio, vLLM, llama.cpp server) or on a coding agent you have installed (Claude Code, Codex).
5
+ Resolflow keeps the agent loop, tools and data in the cloud; your machine only turns a prompt into text.
6
+ Runs you start go only to your own devices and cost 0 Resolflow credits.
7
+
8
+ ## Install
9
+
10
+ Needs **Node.js 20 or later** (`node --version`).
11
+
12
+ ```bash
13
+ npm i -g @resolflow/cli
14
+ resolflow --version
15
+ ```
16
+
17
+ One-shot, without a global install (pairs this machine; then use `npx @resolflow/cli start`):
18
+
19
+ ```bash
20
+ npx @resolflow/cli pair <CODE>
21
+ ```
22
+
23
+ npm is the only way to install during the Beta; standalone binaries (no Node.js needed) will come later.
24
+
25
+ Keychain: the CLI stores the device token in the macOS keychain (`/usr/bin/security`) or the Linux Secret Service
26
+ (`secret-tool`), else a 0600 file with a warning. You can add the optional native `keytar` module next to the CLI
27
+ (`npm i -g keytar`) to use it instead.
28
+
29
+ ## Quick start
30
+
31
+ ```bash
32
+ # 1. a local model server, e.g. Ollama (or skip this and use a coding agent, see below)
33
+ ollama pull llama3.2 # or qwen3:4b
34
+ # 2. in Resolflow: Agents › Devices › "Pair this device" → run the command it shows
35
+ resolflow pair ABCD-EFGH
36
+ # 3. serve (keep it running while you want to serve runs; Ctrl+C stops)
37
+ resolflow start
38
+ ```
39
+
40
+ Then mark a custom agent as **Use CLI** (Agents › Custom Agents) and, in Agents › Devices, choose its device and run
41
+ its purpose checks.
42
+
43
+ ## Commands
44
+
45
+ | Command | What it does |
46
+ |---|---|
47
+ | `resolflow pair [code] [--api URL] [--name NAME]` | Exchanges a pairing code (10 min, single use) for a device token; stores it in the keychain. `resolflow login [--pair code]` is the same. |
48
+ | `resolflow start [--endpoint URL] [--concurrency 1-4] [--allow-background] [--ui] [--ui-port N]` | Connects out to Resolflow and serves requests until Ctrl+C. Reconnects with backoff; stops for good if the device is revoked. |
49
+ | `resolflow status` | API, endpoint, pairing (asks the server whether the token is still valid and the device online), local models, coding agent spend today against the daily budget (Codex not counted), last runs. |
50
+ | `resolflow models [--endpoint URL]` | Lists the models of the local server (`GET /v1/models`). |
51
+ | `resolflow pause` / `resolflow resume` | Kill switch. A running daemon picks it up within 2 s: in-flight requests stop, new ones are refused, the server marks the device paused. |
52
+ | `resolflow config [key value]` | Shows or sets `endpoint`, `concurrency` (1-4), `background` (on/off), `api` (only when unpaired), `agent-turns` (1-20), `agent-budget` (USD per job), `agent-timeout` (minutes per job), `agent-daily-budget` (USD per local day, default 10, `0` = no limit; Claude Code only). `resolflow config set <key> <value>` works too. |
53
+ | `resolflow agents` | Local coding agents (Claude Code, Codex, Antigravity): installed, version, auth (`api key` / `CLI login` / `CLI login — not available (disabled by the administrator)`), whether an API key is configured (never the key), offered or not. |
54
+ | `resolflow config agent-key <claude\|codex> [--delete]` | Stores a coding agent's API key in the OS keychain (hidden prompt, or piped stdin). The key never leaves this machine. |
55
+ | `resolflow config agent-auth <claude\|codex> <api_key\|login>` | How that tool authenticates on this machine: its API key (default) or your existing CLI login (Beta, see below). `login` asks Resolflow first: refused (exit code 2, not saved) while the administrator has it off. |
56
+ | `resolflow unpair` | Revokes the device on the server and deletes the token locally. |
57
+ | `resolflow --version` / `--help` | Version of this CLI / all commands. |
58
+
59
+ `-v/--verbose` adds debug lines (still never prompts, answers or secrets).
60
+
61
+ ## Files
62
+
63
+ `$RESOLFLOW_CONFIG_DIR`, else `$XDG_CONFIG_HOME/resolflow`, else `~/.config/resolflow` (all platforms). Directory 0700.
64
+
65
+ | File | Content |
66
+ |---|---|
67
+ | `config.json` (0600) | API URL, local endpoint, concurrency, background consent, paused flag, device id/name/workspace. **No secret.** |
68
+ | `device-token` (0600) | Only when no keychain is available (a warning is printed). |
69
+ | `agent-key-<tool>` (0600) | A coding agent's API key, only when no keychain is available. |
70
+ | `agent-spend.json` (0600) | Today's coding agent spend: `{ "day": "YYYY-MM-DD", "spentUsd": n }` (Claude Code only; see Daily budget). |
71
+ | `runs.log` (0600) | One JSON line per served request: time, request id, model, status, error kind, duration, token counts. Never content. Last 1000 kept. |
72
+
73
+ ## Local coding agents (Beta)
74
+
75
+ A local agent profile can use a coding agent installed on this machine instead of a model server (Agents › Devices ›
76
+ "Local engine"). Resolflow sends one `agent_job` (prompt + JSON schema of the expected plan / document); the CLI runs
77
+ the tool as a **text generator** and returns the structured result. Credits on Resolflow: 0; the tool's usage and
78
+ cost estimate are recorded for reference. Your provider bills your API key as usual.
79
+
80
+ ```bash
81
+ resolflow config agent-key claude # or: export ANTHROPIC_API_KEY=… (Codex: CODEX_API_KEY / OPENAI_API_KEY)
82
+ resolflow agents # Claude Code … status: ready
83
+ resolflow start # "Coding agents offered: claude-code"
84
+ ```
85
+
86
+ | Tool | How it runs (flags from the official docs) |
87
+ |---|---|
88
+ | Claude Code ([CLI reference](https://code.claude.com/docs/en/cli-reference), [headless](https://code.claude.com/docs/en/headless)) | `claude --bare -p --output-format stream-json --verbose --include-partial-messages --tools "" --disallowedTools "mcp__*" --strict-mcp-config --permission-mode dontAsk --no-session-persistence --max-turns N --max-budget-usd X --system-prompt-file F [--json-schema S] [--model M]`, prompt on stdin, `ANTHROPIC_API_KEY`, own empty `CLAUDE_CONFIG_DIR` |
89
+ | Codex CLI ([non-interactive](https://learn.chatgpt.com/docs/non-interactive-mode), [auth](https://learn.chatgpt.com/docs/auth)) | `codex exec --json --sandbox read-only --ephemeral --skip-git-repo-check -C <empty dir> -o F [--output-schema F] [-m M] -`, prompt on stdin, `CODEX_API_KEY`, own empty `CODEX_HOME` = `HOME` = XDG dirs. **Off by default**: offered only with `RESOLFLOW_ALLOW_CODEX_JOBS=1` (see below) |
90
+ | Antigravity CLI | Not supported yet (stub): no stable, officially documented headless JSON mode with an API key. Never a Google login. |
91
+
92
+ Safety: every job runs in an **empty private temp dir** that is deleted afterwards; no project config, hooks or MCP
93
+ servers load; tools are off (Codex: read-only sandbox, it has no tools-off switch); never a bypass / auto-approve flag;
94
+ the child process gets only PATH/HOME/locale/proxy variables plus its API key; **API key only** (`--bare` ignores
95
+ Claude OAuth and the keychain; Codex's empty `CODEX_HOME` hides a saved ChatGPT login). Caps per job: turns, budget
96
+ (Claude Code), wall time — Resolflow may ask for less, this device's `agent-*` settings are the ceiling (defaults 4
97
+ turns, 1 USD, 10 min). Cancel from the web: SIGINT to the process group, then SIGTERM, then SIGKILL. Errors map to
98
+ `invalid_key`, `rate_limited`, `quota`, `model_not_found`, `budget_exceeded`, `timeout`, `unsupported`, `device_error`;
99
+ the vendor's own error text is never forwarded or logged.
100
+
101
+ ### Daily budget
102
+
103
+ On top of the per-job caps, this device keeps a **daily ceiling on coding agent spend**: default **10 USD per local
104
+ day** (this machine's time zone; the total starts again from 0 at local midnight).
105
+
106
+ - Every Claude Code job reports its cost (`total_cost_usd`); the CLI adds it to today's total in `agent-spend.json`
107
+ (0600, next to `config.json`).
108
+ - Once today's total reaches the ceiling, new jobs are refused with `budget_exceeded` (reported to Resolflow like any
109
+ job error) until midnight, or until you raise the ceiling.
110
+ - When less is left than the per-job budget, the job's `--max-budget-usd` is lowered to what is left (at least
111
+ 0.01 USD). Budget held by jobs still running counts as used, so jobs running at the same time cannot together go
112
+ over.
113
+ - **Codex reports no cost, so Codex jobs are not counted** and are never refused by the daily ceiling (they keep
114
+ their own per-job turn and time caps).
115
+ - Change it with `resolflow config agent-daily-budget <USD>` (or `resolflow config set agent-daily-budget <USD>`);
116
+ `0` = no daily limit. A running `resolflow start` uses the new value for the next job.
117
+ - `resolflow status` and `resolflow agents` show `Agent spend today: 2.30 / 10.00 USD`.
118
+ - A missing `agent-spend.json` counts as 0; a damaged one counts as 0 with a warning, and is rewritten by the next
119
+ job's cost.
120
+
121
+ **Codex reads outside the job folder.** Its read-only sandbox blocks writes and network but not reads: a job prompt
122
+ (written by the Resolflow server or a project member) could make Codex run `cat` on any file your user can read —
123
+ SSH keys, cloud credentials, this CLI's device token — and return it in the answer. `HOME`, `CODEX_HOME` and the XDG
124
+ dirs point at the job's empty folder, so `~` resolves there, but absolute paths still work. So `resolflow start`
125
+ offers Codex only when you set `RESOLFLOW_ALLOW_CODEX_JOBS=1` (on a machine or user account without secrets, e.g. a
126
+ VM or a separate OS user); `resolflow agents` says why it is not offered. Claude Code runs with tools off and is
127
+ offered as before.
128
+
129
+ ### Using your existing CLI login (Beta, off by default)
130
+
131
+ `resolflow config agent-auth claude login` (or `codex`) lets that tool run Resolflow jobs with the login you already
132
+ have on this machine (your own Claude / ChatGPT plan and its limits) instead of an API key. It is available only while
133
+ the Resolflow administrator has switched it on for that tool (Admin › Settings › Agents); otherwise the command
134
+ answers "CLI login for Claude Code is not available: it is turned off by the Resolflow administrator" and saves
135
+ nothing (when Resolflow cannot be reached it saves the choice with a warning — it is not used until the administrator
136
+ allows it). You read a short terms summary with the vendor's terms links and type `yes`. Undo: `… agent-auth claude
137
+ api_key`. Gemini / Antigravity: never (Google's terms do not allow third-party use of a Gemini CLI login).
138
+
139
+ - The unmodified vendor binary reads its own login. This CLI never opens, copies, logs or uploads login files or tokens;
140
+ Resolflow only learns the mode name (`api_key` / `login`) per tool.
141
+ - Only work you start yourself: background (system-triggered) runs never use a login (refused by the server and here).
142
+ A task review you request by hand runs through the same background review queue, so it is refused on a login
143
+ device too (`cli_login_background`); planning and document generation you start yourself are not background.
144
+ - Resolflow can only refuse login mode (the `policy` it sends); it can never switch this machine to it. Until the
145
+ server has sent a policy, login mode is off.
146
+ - What still loads of your own setup, and what is switched off (checked against the official docs, 2026-10-24):
147
+
148
+ | Tool | Login mode |
149
+ |---|---|
150
+ | Claude Code | `--bare` is dropped (it never reads OAuth / the keychain) and `CLAUDE_CONFIG_DIR` is not replaced (yours is passed on if set); instead `--safe-mode` (no CLAUDE.md, skills, plugins, hooks, MCP servers, custom commands / agents, output styles, LSP servers, auto memory), `--settings '{"disableAllHooks":true}'`, `--disable-slash-commands`, plus every flag above (`--tools ""`, `--disallowedTools "mcp__*"`, `--strict-mcp-config`, `dontAsk`, no session persistence, caps). Still read: your user settings such as `env`, `model`, `apiKeyHelper`, permission rules, and your organisation's managed policy (incl. its hooks). The run is refused if any tool or MCP server is live, or a plugin that is not built into Claude Code loaded (its name is logged on this machine only); built-in plugins (`…@builtin`) and listed subagents are fine — with no tools nothing can start a subagent. Checked on a real Claude Code 2.1.308 with an OAuth login. |
151
+ | Codex CLI | Only with `RESOLFLOW_ALLOW_CODEX_JOBS=1` as well. Your `CODEX_HOME` (else `~/.codex`, where `codex login` keeps its login) instead of an empty one; `--ignore-user-config` (no `config.toml`: MCP servers, `notify`, inline hooks, profiles, plugins, providers), `--ignore-rules`, `-c features.hooks=false` (`hooks.json`), `-c features.apps=false`, `-c features.shell_tool=false` (no commands at all), `-c features.multi_agent=false`, `-c features.memories=false`, `-c web_search="disabled"`, `-c history.persistence="none"`, plus `--sandbox read-only --ephemeral`. Still read: your global `AGENTS.md` (text) and skill metadata. A login kept in the OS keyring through `config.toml` may not be found (the job fails as "not signed in"). |
152
+
153
+ Mode B (the agent works in a folder of yours with the Resolflow MCP server) is **not** available: it needs its own
154
+ security design review (folder allow-list, write opt-in on the device, scoped MCP token). A Mode B job is answered
155
+ `unsupported`.
156
+
157
+ ## Security model
158
+
159
+ - **No listener by default.** The daemon only opens one outbound connection. `--ui` adds a status page bound to
160
+ `127.0.0.1` only, with a per-session random token (sent in the `X-Resolflow-Token` header, delivered via the URL
161
+ fragment), strict `Host` (DNS rebinding) and `Origin` checks, no CORS headers, a strict CSP; it can show status and
162
+ pause/resume — nothing else (no endpoint/token changes, no processes, no secrets).
163
+ - **Secrets:** the device token goes to the OS keychain — `keytar` if installed, else macOS `/usr/bin/security`
164
+ (secret passed on stdin) or Linux `secret-tool` (stdin); otherwise a 0600 file with a warning. Never in config,
165
+ never printed, redacted from logs.
166
+ - **Network:** API https only (http only to loopback, for development); local model endpoint loopback only; no
167
+ redirects followed; TLS verification always on.
168
+ - **No shell, no file tools.** The only child processes are the fixed keychain tools above and, when you set one up,
169
+ a local coding agent run as described above; the device turns prompts into text, the tool loop runs in the cloud.
170
+ - **No payload logging** on the device (logs and `runs.log` hold ids, model, timings, counts) or in the gateway.
171
+ - **Server side:** token `cli_<id>_<secret>` checked by id + one constant-time hash compare, membership at handshake
172
+ and every minute (sweep), revocation on member removal / leave / viewer / block, handshake and pairing rate limits,
173
+ a device can only answer requests sent to it, one connection per device.
174
+ - **Other traffic:** only the once-a-day version check against the npm registry (see *Updates*; off with
175
+ `RESOLFLOW_NO_UPDATE_CHECK=1`).
176
+ - **Beta:** a security review of this package is part of the GA criteria.
177
+
178
+ ## Updates
179
+
180
+ `resolflow` never updates itself. At most once a day it asks the npm registry for the latest version of
181
+ `@resolflow/cli` (only the package name is sent; the answer is cached in `update-check.json` in the config directory)
182
+ and prints one line after the command when a newer version exists. Update with `npm i -g @resolflow/cli@latest`. Turn the check off with `RESOLFLOW_NO_UPDATE_CHECK=1` (it is also off when `CI` or `NO_UPDATE_NOTIFIER`
183
+ is set, or when stderr is not a terminal).
184
+
185
+ ## Troubleshooting
186
+
187
+ | Problem | What to do |
188
+ |---|---|
189
+ | `resolflow: command not found` after `npm i -g` | npm's global bin directory is not on your PATH: `npm prefix -g` shows it (add its `bin`, on Windows the prefix itself). Or use `npx @resolflow/cli …`. |
190
+ | `EACCES` during `npm i -g` | Do not use `sudo npm`; use a Node version manager (nvm, fnm, Volta) or set a user prefix: `npm config set prefix ~/.npm-global`. |
191
+ | `Pairing code … invalid or expired` | Codes are valid for 10 minutes and once. Create a new one in Agents › Devices. |
192
+ | `Cannot reach https://api.resolflow.ai` | Check the network / proxy (`HTTPS_PROXY`). The CLI only makes outbound HTTPS/WSS connections. |
193
+ | `resolflow status` shows no models | Start your model server (`ollama serve`) or point the CLI at it: `resolflow config endpoint http://127.0.0.1:1234/v1`. Only addresses on this machine are allowed. |
194
+ | The device shows **Offline** in Resolflow | `resolflow start` must be running. A revoked device stops with a message: pair it again. |
195
+ | A coding agent shows `needs an API key` | `resolflow config agent-key claude` (or `codex`), or set `ANTHROPIC_API_KEY` / `CODEX_API_KEY`. A CLI login is used only if you chose it (`resolflow config agent-auth`). |
196
+ | `Auth: CLI login — not available (disabled by the administrator)` | The Resolflow administrator has CLI logins off for that tool: `resolflow config agent-auth claude api_key` and `resolflow config agent-key claude`. |
197
+ | `No OS keychain available` warning | The token is in a 0600 file in the config directory. Install `secret-tool` (Linux) or `keytar` to use a keychain. |
198
+ | Something else | `resolflow -v status` adds debug lines (never prompts, answers or secrets). Email support@resolflow.ai with the output. |
199
+
200
+ ## License
201
+
202
+ MIT
@@ -0,0 +1,29 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.AntigravityAdapter = void 0;
4
+ const tools_1 = require("../agents/tools");
5
+ const coding_agent_1 = require("./coding-agent");
6
+ /**
7
+ * Antigravity CLI (`agy`; Gemini CLI was replaced by it on 2026-06-18) — documented STUB (RF-5cdd488d).
8
+ *
9
+ * Checked 2026-10-18: Google publishes no official reference for a headless JSON mode of the Antigravity CLI with an
10
+ * API key. Third-party write-ups describe `agy -p … --output-format json|stream-json --json-schema …` (since 1.1.8)
11
+ * and a GEMINI_API_KEY path behind a `modelProvider` setting (1.1.13), but also call the JSON output unstable,
12
+ * report print mode hanging without a TTY, and conflict on whether GEMINI_API_KEY is honoured. Decision 5a forbids
13
+ * the Google / Gemini login (account suspension risk), so a login-based fallback is not an option.
14
+ *
15
+ * Until official docs give a stable headless JSON + API-key mode, this adapter is never offered to Resolflow (not in
16
+ * hello.capabilities.adapters) and answers any job with `unsupported`. `resolflow agents` shows it as not supported.
17
+ */
18
+ class AntigravityAdapter extends coding_agent_1.CodingAgentAdapter {
19
+ constructor(opts) {
20
+ super(tools_1.AGENT_TOOLS.antigravity, opts);
21
+ }
22
+ invocation() {
23
+ throw new Error('Antigravity CLI is not supported yet');
24
+ }
25
+ parser() {
26
+ throw new Error('Antigravity CLI is not supported yet');
27
+ }
28
+ }
29
+ exports.AntigravityAdapter = AntigravityAdapter;
@@ -0,0 +1,302 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.ClaudeStreamParser = exports.ClaudeCodeAdapter = void 0;
37
+ exports.nonBuiltinPlugins = nonBuiltinPlugins;
38
+ const path = __importStar(require("path"));
39
+ const tools_1 = require("../agents/tools");
40
+ const errors_1 = require("../agents/errors");
41
+ const run_process_1 = require("../agents/run-process");
42
+ const coding_agent_1 = require("./coding-agent");
43
+ /**
44
+ * Claude Code, Mode A (RF-5cdd488d). Flags from the official docs (checked 2026-10-18):
45
+ * https://code.claude.com/docs/en/cli-reference · https://code.claude.com/docs/en/headless
46
+ *
47
+ * claude --bare -p bare: no hooks, skills, plugins, MCP servers, CLAUDE.md; API key only
48
+ * ("In bare mode, Claude Code never reads OAuth credentials or the system keychain")
49
+ * --output-format stream-json --verbose --include-partial-messages newline-delimited events, text deltas
50
+ * --tools "" --disallowedTools "mcp__*" --strict-mcp-config no built-in tools, no MCP tools/servers
51
+ * --permission-mode dontAsk anything that would prompt is denied
52
+ * --no-session-persistence nothing saved to disk
53
+ * --max-turns N --max-budget-usd X per-job caps
54
+ * --system-prompt-file <file> --json-schema '<schema>' --model <alias>
55
+ * prompt on stdin; cwd = an empty temp dir; CLAUDE_CONFIG_DIR = an empty per-job dir (no user settings);
56
+ * ANTHROPIC_API_KEY = the key. Never --dangerously-skip-permissions / bypassPermissions / --allowedTools.
57
+ *
58
+ * Login mode (RF-90e75d6d; the person chose `resolflow config agent-auth claude login` and the administrator allows
59
+ * it). Checked against the official docs on 2026-10-24 (cli-reference, headless, settings):
60
+ * - `--bare` goes ("In bare mode, Claude Code never reads OAuth credentials or the system keychain"), and
61
+ * CLAUDE_CONFIG_DIR is not pointed at an empty dir (the person's own CLAUDE_CONFIG_DIR, if set, is passed on).
62
+ * No ANTHROPIC_API_KEY. The unmodified `claude` binary reads its own login; this CLI never touches it.
63
+ * - In its place `--safe-mode`: "CLAUDE.md, skills, plugins, hooks, MCP servers, custom commands and agents, output
64
+ * styles, workflows, ... status line and file-suggestion commands, LSP servers, and auto memory do not load.
65
+ * Authentication, model selection, built-in tools, and permissions work normally." Managed (organisation) policy
66
+ * still applies, incl. policy hooks: the person's employer's settings, not Resolflow's to override.
67
+ * - `--settings '{"disableAllHooks":true}'` (also off: hooks, custom status line, file suggestion; any scope) and
68
+ * `--disable-slash-commands` ("Disable all skills and commands") on top, belt and braces.
69
+ * - Everything else stays: `--tools ""`, `--disallowedTools "mcp__*"`, `--strict-mcp-config` (no MCP config at
70
+ * all), `--permission-mode dontAsk`, `--no-session-persistence`, the caps, an empty cwd; and the `system/init`
71
+ * check below refuses to run when any tool or MCP server is live, or any plugin that is not built in loaded.
72
+ * - Real Claude Code 2.1.308 with an OAuth login and exactly these flags (owner-approved run, 2026-10-24): tools 0,
73
+ * mcp_servers 0, slash_commands 0, skills 0, apiKeySource none — but `plugins` still lists 3 BUILT-IN entries
74
+ * (cc-plugin-agents-md@builtin, cc-plugin-telemetry@builtin, cc-plugin-plugin-authoring@builtin, path "builtin")
75
+ * and `agents` lists 4. Built-in plugins ship inside the unmodified binary: allowed. Anything else is refused
76
+ * and its name logged on this machine only. `agents` (subagents) are allowed: they can only be started through
77
+ * the Agent / Task tool, and with `--tools ""` no tool exists (FORBIDDEN_TOOLS refuses a run where Agent / Task
78
+ * is live), so no subagent can run.
79
+ * - Still read from the person's user settings (safe mode keeps settings): `env`, `model`, `apiKeyHelper`,
80
+ * permissions: their own machine and choices; no tool exists to use a permission.
81
+ *
82
+ * Output: `system/init`, `system/api_retry` (error categories → ErrorKind), `stream_event` (partial deltas),
83
+ * `assistant` (complete messages, `error` category), and a final `result` (subtype success | error_max_turns |
84
+ * error_max_budget_usd | error_during_execution | error_max_structured_output_retries; `structured_output`,
85
+ * `usage`, `total_cost_usd`).
86
+ */
87
+ class ClaudeCodeAdapter extends coding_agent_1.CodingAgentAdapter {
88
+ constructor(opts) {
89
+ super(tools_1.AGENT_TOOLS['claude-code'], opts);
90
+ }
91
+ /** `result.total_cost_usd` → usage.costUsd: counted toward the daily budget (RF-27cf99e3). */
92
+ reportsCost = true;
93
+ invocation(i) {
94
+ const sys = [i.payload.systemPrompt?.trim(), coding_agent_1.MODE_A_NOTE].filter(Boolean).join('\n\n');
95
+ const sysFile = (0, run_process_1.writeJobFile)(i.root, 'system-prompt.txt', sys);
96
+ const login = i.authMode === 'login';
97
+ const args = [
98
+ // api_key: --bare (no OAuth / keychain, no user config). login: --safe-mode (the login works; no user
99
+ // customisations) + hooks off + no skills/commands (see the header)
100
+ ...(login ? ['--safe-mode', '--settings', '{"disableAllHooks":true}', '--disable-slash-commands'] : ['--bare']),
101
+ '-p',
102
+ '--output-format',
103
+ 'stream-json',
104
+ '--verbose',
105
+ '--include-partial-messages',
106
+ '--tools',
107
+ '',
108
+ '--disallowedTools',
109
+ 'mcp__*',
110
+ '--strict-mcp-config',
111
+ '--permission-mode',
112
+ 'dontAsk',
113
+ '--no-session-persistence',
114
+ '--max-turns',
115
+ String(i.caps.maxTurns),
116
+ '--max-budget-usd',
117
+ i.caps.maxBudgetUsd.toFixed(2),
118
+ '--system-prompt-file',
119
+ sysFile,
120
+ ];
121
+ if (i.payload.outputSchema)
122
+ args.push('--json-schema', JSON.stringify(i.payload.outputSchema));
123
+ if (i.model)
124
+ args.push('--model', i.model);
125
+ const from = this.opts.env ?? process.env;
126
+ // login: the person's own config dir (only when they set one), no key; api_key: the key, an empty config dir
127
+ const extra = login
128
+ ? from.CLAUDE_CONFIG_DIR && path.isAbsolute(from.CLAUDE_CONFIG_DIR)
129
+ ? { CLAUDE_CONFIG_DIR: from.CLAUDE_CONFIG_DIR }
130
+ : {}
131
+ : { ANTHROPIC_API_KEY: i.apiKey, CLAUDE_CONFIG_DIR: path.join(i.root, 'home') };
132
+ return { args, env: (0, run_process_1.agentEnv)(extra, from), stdin: i.payload.prompt };
133
+ }
134
+ parser(em, i) {
135
+ return new ClaudeStreamParser(em, !!i.payload.outputSchema, this.opts.warn);
136
+ }
137
+ }
138
+ exports.ClaudeCodeAdapter = ClaudeCodeAdapter;
139
+ /**
140
+ * Plugins in `system/init` that are NOT shipped inside the Claude Code binary (path "builtin" / name "…@builtin").
141
+ * Their names, for the local warning. A malformed entry counts as not built in.
142
+ */
143
+ function nonBuiltinPlugins(raw) {
144
+ if (!Array.isArray(raw))
145
+ return [];
146
+ const out = [];
147
+ for (const p of raw) {
148
+ const name = typeof p === 'string' ? p : typeof p?.name === 'string' ? p.name : '';
149
+ const builtin = typeof p === 'object' && p !== null && p.path === 'builtin' && /@builtin$/.test(name);
150
+ if (!builtin)
151
+ out.push((name || 'unnamed').replace(/[^\w@./:-]/g, '').slice(0, 80) || 'unnamed');
152
+ }
153
+ return out;
154
+ }
155
+ /** Built-in tools that must never be live in a Mode A run (a newer Claude Code that ignored --tools ""). */
156
+ const FORBIDDEN_TOOLS = /^(Bash|BashOutput|KillShell|Edit|MultiEdit|Write|Read|NotebookEdit|Glob|Grep|LS|WebFetch|WebSearch|Task|Agent|Skill|Monitor|mcp__.*)$/;
157
+ class ClaudeStreamParser {
158
+ em;
159
+ wantsSchema;
160
+ warn;
161
+ result = null;
162
+ sawDelta = false;
163
+ text = '';
164
+ lastErrorKind = null;
165
+ /** An assistant message carried an API error: its text is the vendor's, never the answer. */
166
+ errorTextSeen = false;
167
+ openTools = new Map();
168
+ constructor(em, wantsSchema,
169
+ /** Local log only (never sent to Resolflow): names of plugins that made the run refuse. */
170
+ warn) {
171
+ this.em = em;
172
+ this.wantsSchema = wantsSchema;
173
+ this.warn = warn;
174
+ }
175
+ line(raw) {
176
+ const m = (0, coding_agent_1.safeJson)(raw);
177
+ if (!m)
178
+ return;
179
+ switch (m.type) {
180
+ case 'system':
181
+ if (m.subtype === 'init') {
182
+ const live = (Array.isArray(m.tools) ? m.tools : []).filter((t) => typeof t === 'string' && FORBIDDEN_TOOLS.test(t));
183
+ const mcp = Array.isArray(m.mcp_servers) ? m.mcp_servers.length : 0;
184
+ // RF-90e75d6d: a plugin that loaded anyway (login mode reads the person's settings) is refused as well
185
+ const plugins = nonBuiltinPlugins(m.plugins);
186
+ if (plugins.length)
187
+ this.warn?.(`Claude Code loaded plugins that are not built in: ${plugins.join(', ')}; refusing to run this job.`);
188
+ if (live.length || mcp || plugins.length)
189
+ return this.em.failFast('unsupported', 'Claude Code started with tools enabled; refusing to run (update Claude Code or the resolflow CLI).');
190
+ this.em.progress('agent ready', true);
191
+ }
192
+ else if (m.subtype === 'api_retry') {
193
+ const kind = (0, errors_1.claudeErrorKind)(m.error);
194
+ if (kind && errors_1.HARD_AGENT_KINDS.has(kind))
195
+ return this.em.failFast(kind);
196
+ this.em.progress(kind === 'rate_limited' ? 'rate limited, retrying' : 'retrying', true);
197
+ }
198
+ return;
199
+ case 'stream_event': {
200
+ const e = m.event ?? {};
201
+ if (m.parent_tool_use_id)
202
+ return;
203
+ if (e.type === 'content_block_delta') {
204
+ if (e.delta?.type === 'text_delta' && typeof e.delta.text === 'string' && e.delta.text) {
205
+ this.sawDelta = true;
206
+ this.text += e.delta.text;
207
+ this.em.emit({ type: 'text_delta', text: e.delta.text });
208
+ }
209
+ else if (e.delta?.type === 'thinking_delta') {
210
+ this.em.progress('thinking');
211
+ }
212
+ }
213
+ else if (e.type === 'content_block_start' && e.content_block?.type === 'tool_use') {
214
+ this.toolStart(String(e.content_block.id ?? ''), String(e.content_block.name ?? 'tool'));
215
+ }
216
+ return;
217
+ }
218
+ case 'assistant': {
219
+ // An API error comes as an assistant message with `error` and the vendor's text: map it to the kind only,
220
+ // never forward that text (it is not an answer, and can quote request details)
221
+ if (m.error !== undefined && m.error !== null) {
222
+ this.lastErrorKind = (0, errors_1.claudeErrorKind)(m.error) ?? 'device_error';
223
+ this.errorTextSeen = true;
224
+ return;
225
+ }
226
+ const blocks = Array.isArray(m.message?.content) ? m.message.content : [];
227
+ for (const b of blocks) {
228
+ if (b?.type === 'text' && typeof b.text === 'string' && !this.sawDelta && !m.parent_tool_use_id) {
229
+ this.text += b.text;
230
+ this.em.emit({ type: 'text_delta', text: b.text });
231
+ }
232
+ else if (b?.type === 'tool_use') {
233
+ this.toolStart(String(b.id ?? ''), String(b.name ?? 'tool'));
234
+ }
235
+ }
236
+ return;
237
+ }
238
+ case 'user': {
239
+ const blocks = Array.isArray(m.message?.content) ? m.message.content : [];
240
+ for (const b of blocks)
241
+ if (b?.type === 'tool_result')
242
+ this.toolEnd(String(b.tool_use_id ?? ''));
243
+ return;
244
+ }
245
+ case 'result':
246
+ this.result = m;
247
+ return;
248
+ }
249
+ }
250
+ toolStart(id, name) {
251
+ if (id && this.openTools.has(id))
252
+ return;
253
+ if (id)
254
+ this.openTools.set(id, name);
255
+ this.em.emit({ type: 'tool_activity', tool: name.slice(0, 100), status: 'start' });
256
+ }
257
+ toolEnd(id) {
258
+ const name = this.openTools.get(id);
259
+ if (!name)
260
+ return;
261
+ this.openTools.delete(id);
262
+ this.em.emit({ type: 'tool_activity', tool: name, status: 'end' });
263
+ }
264
+ finish(o) {
265
+ const r = this.result;
266
+ if (!r) {
267
+ const kind = this.lastErrorKind ?? (o.code === 0 ? 'device_error' : (0, errors_1.classifyAgentErrorText)(o.stderrTail));
268
+ return this.em.error(kind);
269
+ }
270
+ const u = r.usage ?? {};
271
+ const promptTokens = (0, coding_agent_1.num)(u.input_tokens) + (0, coding_agent_1.num)(u.cache_creation_input_tokens) + (0, coding_agent_1.num)(u.cache_read_input_tokens);
272
+ const completionTokens = (0, coding_agent_1.num)(u.output_tokens);
273
+ if (promptTokens || completionTokens || (0, coding_agent_1.num)(r.total_cost_usd)) {
274
+ this.em.usage({ promptTokens, completionTokens, ...((0, coding_agent_1.num)(r.total_cost_usd) ? { costUsd: (0, coding_agent_1.num)(r.total_cost_usd) } : {}) });
275
+ }
276
+ switch (r.subtype) {
277
+ case 'success': {
278
+ if (r.is_error || this.errorTextSeen) {
279
+ const kind = this.lastErrorKind ?? (0, errors_1.classifyAgentErrorText)(String(r.result ?? ''));
280
+ return this.em.error(kind === 'unsupported' ? 'device_error' : kind);
281
+ }
282
+ const text = typeof r.result === 'string' ? r.result : this.text;
283
+ if (!this.wantsSchema)
284
+ return this.em.emit({ type: 'result', text });
285
+ const data = r.structured_output !== undefined && r.structured_output !== null ? r.structured_output : (0, coding_agent_1.parseJsonAnswer)(text);
286
+ if (data === undefined)
287
+ return this.em.error('device_error', 'The coding agent finished without the structured output.');
288
+ return this.em.emit({ type: 'result', text: JSON.stringify(data), data });
289
+ }
290
+ case 'error_max_turns':
291
+ case 'error_max_budget_usd':
292
+ return this.em.error('budget_exceeded');
293
+ case 'error_max_structured_output_retries':
294
+ return this.em.error('device_error', 'The coding agent could not produce output matching the schema.');
295
+ default: {
296
+ const errs = Array.isArray(r.errors) ? r.errors.join('\n') : '';
297
+ return this.em.error(this.lastErrorKind ?? (0, errors_1.classifyAgentErrorText)(errs || o.stderrTail));
298
+ }
299
+ }
300
+ }
301
+ }
302
+ exports.ClaudeStreamParser = ClaudeStreamParser;