subconscious-cli 6.0.1 → 6.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (73) hide show
  1. package/README.md +167 -43
  2. package/bin/agent-data.js +278 -0
  3. package/bin/agent-help.js +114 -0
  4. package/bin/agent-install.js +238 -0
  5. package/bin/agent-launch.js +199 -0
  6. package/bin/agent-spawn.js +137 -0
  7. package/bin/agents.js +236 -931
  8. package/bin/auth.js +9 -1
  9. package/bin/claude-picker.js +143 -0
  10. package/bin/cli.js +62 -2
  11. package/bin/clipboard-image.js +2 -0
  12. package/bin/feedback.js +14 -3
  13. package/bin/harness-manifest.js +286 -0
  14. package/bin/headless-args.js +35 -0
  15. package/bin/launch-values.js +25 -0
  16. package/bin/model-capabilities.js +10 -9
  17. package/bin/models.js +25 -13
  18. package/bin/native/subc-tui-darwin-amd64 +0 -0
  19. package/bin/native/subc-tui-darwin-arm64 +0 -0
  20. package/bin/native/subc-tui-linux-amd64 +0 -0
  21. package/bin/native/subc-tui-linux-arm64 +0 -0
  22. package/bin/native/subc-tui-windows-amd64.exe +0 -0
  23. package/bin/native/subc-tui-windows-arm64.exe +0 -0
  24. package/bin/opencode-provider.js +21 -0
  25. package/bin/profile-settings.js +129 -0
  26. package/bin/profiles.js +29 -424
  27. package/bin/runbook/README.md +77 -23
  28. package/bin/runbook/claude-code/agent.json +300 -0
  29. package/bin/runbook/claude-code/install.sh +9 -252
  30. package/bin/runbook/codex/agent.json +462 -0
  31. package/bin/runbook/codex/hooks-lib.sh +17 -4
  32. package/bin/runbook/codex/install.sh +0 -4
  33. package/bin/runbook/codex/run.sh +16 -235
  34. package/bin/runbook/copilot/agent.json +186 -0
  35. package/bin/runbook/copilot/install.sh +45 -63
  36. package/bin/runbook/cursor/agent.json +138 -0
  37. package/bin/runbook/cursor/install.sh +8 -35
  38. package/bin/runbook/deepseek-harness/agent.json +164 -0
  39. package/bin/runbook/deepseek-harness/run.sh +20 -59
  40. package/bin/runbook/image-window/index.ts +14 -0
  41. package/bin/runbook/image-window/window.d.ts +19 -0
  42. package/bin/runbook/image-window/window.js +140 -0
  43. package/bin/runbook/lib.sh +41 -0
  44. package/bin/runbook/opencode/agent.json +159 -0
  45. package/bin/runbook/opencode/install.sh +9 -183
  46. package/bin/runbook/opencode/opencode-plugin.d.ts +10 -3
  47. package/bin/runbook/opencode/subconscious-image-window.ts +24 -0
  48. package/bin/runbook/pi/agent.json +217 -0
  49. package/bin/runbook/pi/install.sh +18 -44
  50. package/bin/runbook/pi/run.sh +6 -18
  51. package/bin/runbook/pi/subconscious-compaction.ts +3 -1
  52. package/bin/runbook/shared.json +57 -0
  53. package/bin/sessions.js +53 -48
  54. package/bin/tui.js +13 -0
  55. package/bin/update-check.js +35 -1
  56. package/bin/upgrade.js +27 -34
  57. package/bin/usage.js +21 -10
  58. package/bin/windows/agents.js +28 -60
  59. package/bin/windows/common.js +2 -28
  60. package/bin/windows/launch.js +252 -231
  61. package/bin/windows/process.js +12 -1
  62. package/bin/windows/setup.js +29 -9
  63. package/package.json +2 -4
  64. package/bin/registry.generated.json +0 -406
  65. package/bin/runbook/claude-code/run.sh +0 -254
  66. package/bin/runbook/model-capabilities.generated.sh +0 -9
  67. package/bin/runbook/opencode/provider.sh +0 -93
  68. package/bin/runbook/opencode/run.sh +0 -99
  69. package/bin/runbook/opencode/subconscious-compaction.ts +0 -100
  70. package/bin/runbook/subconscious-code/install.sh +0 -154
  71. package/bin/runbook/subconscious-code/run.sh +0 -21
  72. package/bin/windows/claude.js +0 -100
  73. package/bin/windows/install.js +0 -188
package/README.md CHANGED
@@ -4,25 +4,59 @@ Log in to Subconscious, then run coding agents against the Subconscious gateway.
4
4
 
5
5
  ## Quick start
6
6
 
7
+ Install with npm:
8
+
7
9
  ```bash
8
10
  npm install -g subconscious-cli
11
+ ```
12
+
13
+ Or install on macOS or Linux without npm:
14
+
15
+ ```bash
16
+ curl -fsSL https://raw.githubusercontent.com/subconscious-systems/subconscious/main/scripts/install.sh | bash
17
+ ```
18
+
19
+ Then log in and launch an agent:
20
+
21
+ ```bash
9
22
  subc login
10
- subc marathon
23
+ subc opencode
11
24
  subc claude
12
25
  ```
13
26
 
27
+ The curl installer downloads the `subconscious-cli` tarball from the npm
28
+ registry, checks it against the registry's integrity hash, and installs it in
29
+ `~/.local/share/subconscious-cli` with a `subc` link in `~/.local/bin`. It does
30
+ not need npm, so a registry policy that blocks `npm install -g` does not stop
31
+ it. Node.js 18 or newer must already be installed, because `subc` runs on it.
32
+ If `~/.local/bin` is not on your `PATH`, the installer prints the line to add.
33
+ Run it again to reinstall or update. These variables change its defaults:
34
+
35
+ | Variable | Default |
36
+ | --- | --- |
37
+ | `SUBC_VERSION` | `latest` (a version such as `6.1.0`, or a dist-tag) |
38
+ | `SUBC_INSTALL_DIR` | `~/.local/share/subconscious-cli` |
39
+ | `SUBC_BIN_DIR` | `~/.local/bin` |
40
+ | `SUBC_REGISTRY` | `https://registry.npmjs.org` |
41
+
42
+ On Windows, use npm (see [Native Windows support](#native-windows-support)).
43
+
44
+ subc does not install the coding agents. Install each agent you use from its
45
+ vendor. If an agent is missing, `subc <agent>` prints its install command and
46
+ official install page, then exits with status 127.
47
+
14
48
  Every interactive `subc` command checks npm for a newer CLI release. When an
15
49
  update is available, a notice shows the installed and latest versions and lets
16
50
  you select **Update now** or **Skip for now** with the arrow keys and Enter.
17
51
  Each option describes what it will do, and the active option is highlighted.
18
- Update runs `npm install -g subconscious-cli@latest`; Skip continues the
19
- requested command.
52
+ Update runs `npm install -g subconscious-cli@latest`, or the curl installer
53
+ again if that is how subc was installed; Skip continues the requested command.
20
54
  Non-interactive commands automatically skip, and registry errors and timeouts
21
55
  never block the requested command. Set `SUBC_DISABLE_UPDATE_CHECK=1` to suppress
22
56
  the check in offline automation.
23
57
 
24
58
  Login creates both the saved credential and a ready-to-use `default` profile,
25
- so Subconscious Code, Claude Code, Codex, OpenCode, and DeepSeek Harness can launch immediately.
59
+ so OpenCode, Claude Code, Codex, and DeepSeek Harness can launch immediately.
26
60
  Persistent editor and Pi integrations are installed per agent:
27
61
 
28
62
  ```bash
@@ -53,8 +87,8 @@ Every command accepts `help` as a subcommand. These only read the selected
53
87
  profile; they do not authenticate, install, configure, or launch anything:
54
88
 
55
89
  ```bash
90
+ subc opencode help
56
91
  subc claude help
57
- subc marathon help
58
92
  subc codex help
59
93
  subc cursor help
60
94
  subc config help
@@ -69,7 +103,6 @@ gateway environment applied for that process. Arguments pass through as usual:
69
103
 
70
104
  ```bash
71
105
  subc claude --continue
72
- subc marathon -- -p "fix the tests"
73
106
  subc codex exec "write a test"
74
107
  subc opencode
75
108
  subc pi
@@ -105,8 +138,9 @@ Update with `npm.cmd install -g subconscious-cli@latest` or `subc.cmd upgrade --
105
138
 
106
139
  If PowerShell's execution policy blocks npm-generated `.ps1` entry points, use
107
140
  `npm.cmd` and `subc.cmd` or run the same commands in Command Prompt. No policy
108
- change is required. Pi must already be installed; other terminal agents offer
109
- their Windows installer when missing in an interactive terminal.
141
+ change is required. subc does not install the agents on Windows either. If an
142
+ agent is missing, subc prints its install command and official install page,
143
+ then exits with status 127.
110
144
 
111
145
  Native executables and standard npm `.cmd` shims are supported. The launcher
112
146
  invokes the underlying executable or Node entry point directly, preserving
@@ -129,19 +163,6 @@ VS Code's secret store and prompts for its key. Its configuration goes under
129
163
  `%APPDATA%\Code\User` (or Code - Insiders/VSCodium). Rerun install after moving
130
164
  your Node installation, because hooks record the absolute Node executable path.
131
165
 
132
- To install the native x64 `marathon.exe` (stable releases starting with 0.1.4), clear
133
- any preview version pin from the current PowerShell session:
134
-
135
- ```powershell
136
- Remove-Item Env:SC_CODE_VERSION -ErrorAction SilentlyContinue
137
- subc.cmd marathon install
138
- subc.cmd marathon
139
- ```
140
-
141
- The installer requires the selected release's Windows asset and SHA-256
142
- checksum; it reports a clear error if unavailable and never downloads a Unix
143
- binary as a fallback. Windows ARM64 binaries are not currently published.
144
-
145
166
  Run `npm run test:windows` from the repository root for the separate Windows suite. The
146
167
  Windows CI job also builds/tests the Go TUI. Existing Unix tests and runbooks
147
168
  remain separate and unchanged.
@@ -152,15 +173,132 @@ The packaged integrations live in `bin/runbook`.
152
173
 
153
174
  | Command | Behavior |
154
175
  | --- | --- |
155
- | `subc marathon` | Launch Subconscious Code with the active gateway, key, model, and DLR transport |
176
+ | `subc opencode` | Launch OpenCode with the runbook provider, client header, and context/output limits |
156
177
  | `subc claude` | Launch Claude Code with the runbook environment, context limits, subagent limits, and OTEL usage reporting |
157
178
  | `subc codex` | Launch Codex with the runbook provider, temporary model catalog, and surgically merged compaction hooks |
158
- | `subc opencode` | Launch OpenCode with the runbook provider, client header, and context/output limits |
159
- | `subc cursor install` | Install/update Cursor conversation and compaction hooks |
179
+ | `subc cursor install` | Install/update Cursor conversation and compaction hooks (Cursor desktop app and IDE only — the Cursor CLI is not supported) |
160
180
  | `subc copilot install` | Install/update the VS Code custom endpoint and Copilot hooks |
161
181
  | `subc pi` | Refresh the Pi provider from the live catalog, then launch |
162
182
  | `subc dsh` | Launch the DeepSeek Harness Web UI with a temporary provider populated from the live catalog |
163
183
 
184
+ ## Headless runs
185
+
186
+ `subc <agent> headless PROMPT [args...]` runs one task without a terminal UI and exits with the agent's status. It works for `claude`, `codex`, `opencode`, `pi`, and `dsh`, and uses the same setup as an interactive launch. It never prompts: there is no update check, a missing agent fails with its install command, stdin is not read, and stdout carries only the agent's own output. Remaining arguments go to the agent, for example `subc claude headless "fix the failing test" --output-format stream-json --verbose`. subc reads `-p`/`--profile` and `--model` itself; put agent options with those names after `--`.
187
+
188
+ The agents keep their own permission defaults, so a run that should edit files usually needs the agent's permission flags in `[args...]`. For example, `codex exec` uses a read-only sandbox and requires a git repository unless given `--skip-git-repo-check`.
189
+
190
+ Model, endpoint, and key resolve as for any launch, so a logged-in user needs nothing extra. To point a run somewhere else:
191
+
192
+ | Input | How to set it |
193
+ | --- | --- |
194
+ | Model | `--model`, or `SUBCONSCIOUS_MODEL` |
195
+ | Endpoint | `SUBCONSCIOUS_BASE_URL`. In a headless run every agent uses it, including Claude Code (`CLAUDE_GATEWAY_URL` is ignored). |
196
+ | Key | `SUBCONSCIOUS_API_KEY`, otherwise the `subc login` key |
197
+
198
+ ```bash
199
+ SUBCONSCIOUS_BASE_URL=https://gateway.example SUBCONSCIOUS_API_KEY=... \
200
+ subc codex headless "fix the failing test" --model subconscious/deepseek-v4.1-flash-marathon
201
+ ```
202
+
203
+ The endpoint must speak the same APIs as the Subconscious gateway: Anthropic Messages for Claude Code, OpenAI Responses for Codex, and OpenAI Chat Completions for the others.
204
+
205
+ | Agent | Runs |
206
+ | --- | --- |
207
+ | `claude` | `claude -p -- PROMPT` |
208
+ | `codex` | `codex exec -- PROMPT` |
209
+ | `opencode` | `opencode run`, with the prompt on stdin |
210
+ | `pi` | `pi --print`, with the prompt on stdin |
211
+ | `dsh` | `dsh --profile headless`, with the prompt on stdin |
212
+
213
+ The prompt is always the argument after `headless`, including for `dsh`, which no longer reads a task piped into subc. Where the table says stdin, subc passes the prompt that way so it reaches the agent unchanged even when it starts with `-` or `@`. On Windows only `dsh` supports headless runs.
214
+
215
+ The exit status is the agent's own. Pi 1.0.1 exits 1 when the model request fails in its default text output, but 0 with `--mode json`, so check its JSON output for errors. Pi also trims whitespace around a prompt it reads from stdin.
216
+
217
+ A blank prompt, and `-h` or `--help` anywhere in a headless run, are refused so a scripted run never reports success without doing the task.
218
+
219
+ Stopping subc with SIGTERM or SIGHUP stops the agent too. SIGKILL cannot be forwarded, so a runner that may kill subc should start it in its own process group and kill the group.
220
+
221
+ ## Harness manifest
222
+
223
+ `subc harness-manifest` prints how this version of subc installs, configures, and launches each agent above, so other tools can copy the setup exactly instead of re-implementing it.
224
+
225
+ ```bash
226
+ subc harness-manifest --json # full manifest
227
+ subc harness-manifest # summary table in a terminal, JSON when piped
228
+ ```
229
+
230
+ The manifest is built when the command runs, from the agent files subc ships (`bin/runbook/<id>/agent.json`). Those files are what the launcher itself executes: launch and headless argv, flags, input defaults, env, and Codex `-c` overrides come from them, so the manifest describes what runs on macOS and Linux. Windows builds its own Codex catalog and `-c` list. The command adds `cli_version` from `package.json`.
231
+
232
+ | Field | Meaning |
233
+ | --- | --- |
234
+ | `schema_version` | Raised when a field is renamed, removed, or changes meaning. New fields do not raise it. |
235
+ | `cli_version` | The installed `subconscious-cli` version. |
236
+ | `tokens` | Placeholders used in values (listed below). |
237
+ | `runbook_env` | What subc passes to every launch: `order` of the layers, `inherited` sources (profile values, then `process.env`), `fixed` values set last, `runbook_scripts` extras, and `per_harness` extras (Claude's model picker and `SUBC_CLAUDE_SETTINGS`). |
238
+ | `harnesses.<id>` | One entry per agent. |
239
+
240
+ Each `harnesses.<id>` entry has:
241
+
242
+ | Field | Meaning |
243
+ | --- | --- |
244
+ | `install` | `method`, `package` or `repository`, `version` or `channel`, `minimum_version`, release `targets`, the per-OS `commands`, and `url`, the vendor's official install page. subc shows `commands` and `url` and never runs them. |
245
+ | `prerequisites` | Commands the runbook needs, such as `jq`, with what needs them and whether they are required. `a\|b` means either one. |
246
+ | `binary` | Executable name, or `null` for IDE integrations. |
247
+ | `launch` | `argv` template, `headless_argv` for `subc <agent> headless` (absent when unsupported), `headless_stdin` when the prompt is passed on stdin instead, `headless_platforms` (`darwin`, `linux`, `win32`) where it works, `resume` and `handoff` argv for `subc sessions`, Codex's `config_flag`, and whether a launch also writes persistent files. |
248
+ | `inputs` | Profile or environment variables subc reads for the agent, with `default`, `flag`, `aliases`, and profile metadata. `export: true` inputs are also passed to the agent and listed in `env`. |
249
+ | `env` | Variables the agent receives: `value`, what it is (`description`), and the `override` variables that win when set. |
250
+ | `config` | Files, directories, `-c` overrides (with `condition`), flags, and JSON-in-env. |
251
+ | `capabilities.compaction` | `default`, plus `on`, `off`, and `threshold`, each with the exact knob and whether subc sets it (`set_by_subc`). |
252
+ | `capabilities.mcp` | Whether subc configures MCP, and which transports. |
253
+ | `capabilities.headers` | Headers sent and on which requests. |
254
+ | `capabilities.hooks` | Hook events installed, what each posts, and where. |
255
+
256
+ A compaction knob has `kind`, `name`, `value`, and `override`. When `kind` is `config-field` or `model-catalog-field`, `config` names the `config` entry (its `path` or `name`) that holds it. `threshold.semantics` says what the number means: `window` is the context size the agent compacts against (Claude Code, OpenCode, Pi, DeepSeek Harness, Copilot); `trigger` is the token count that starts compaction (Codex).
257
+
258
+ Enumerated values:
259
+
260
+ | Field | Values |
261
+ | --- | --- |
262
+ | `install.method` | `npm`, `user`, `null` |
263
+ | `compaction.default` | `on`, `unknown` |
264
+ | knob `kind` | `env`, `cli-config`, `config-field`, `model-catalog-field`, `null` |
265
+ | `threshold.semantics` | `window`, `trigger` |
266
+ | `config[].kind` | `file`, `dir`, `cli-flag`, `cli-config`, `env-json` |
267
+ | `config[].lifetime` | `launch` (removed or not written to disk), `persistent`, `legacy` (left by older setups) |
268
+
269
+ Placeholders:
270
+
271
+ | Token | Meaning |
272
+ | --- | --- |
273
+ | `{baseUrl}`, `{baseUrlV1}` | Gateway origin, and the origin followed by `/v1` |
274
+ | `{apiKey}`, `{model}` | The agent's API key (its own key input, else `API_KEY`) and the launch model |
275
+ | `${NAME}` | The value of env var `NAME` at launch; left in place only where it has no default |
276
+ | `{catalog}`, `{catalog[N]}` | All live catalog models (newline-separated), or the one at index N (clamped to the last) |
277
+ | `{args}`, `{config}` | Passed-through arguments, and Codex's `-c` overrides |
278
+ | `{prompt}` | The task given to `subc <agent> headless` |
279
+ | `{sessionId}`, `{sessionFile}` | A local session's ID or file, for `launch.resume` |
280
+ | `{tempFile}`, `{tmp}` | A temporary file the runbook writes (its `config` entry says whether it is removed), the system temp directory |
281
+ | `{opencodeConfig}`, `{claudeSettings}` | Documents shown in the matching `config` entries |
282
+ | `{target}` | A release target triple from `install.targets` or `install.windows_targets` |
283
+ | `{binDir}`, `{PATH}`, `<install dirs>` | Parts of the `PATH` subc builds |
284
+ | `<agent key>` | The agent-specific API key input, such as `CODEX_API_KEY` |
285
+ | `<id>`, `models[]` | Any catalog model ID, and every element of a models array |
286
+ | `<runbook>` | The installed `bin/runbook` directory |
287
+ | `<VS Code user dir>` | The VS Code user settings directory |
288
+
289
+ `verified: false` means subc does not set the value, or cannot prove its effect; a `note` says which. Entries describing files a runbook writes (the Codex catalog, Pi's `models.json`, the DeepSeek overlay, Copilot's provider, hooks) are descriptions of that script; values they take from inputs are filled in from the input defaults.
290
+
291
+ ## Long screenshot sessions
292
+
293
+ Coding agents resend every screenshot on every turn, so a long computer-use or screenshot session grows until the gateway refuses it. For vision models, subc applies one rule in Pi and OpenCode before a request leaves the machine: keep the newest screenshots, up to 30 and about 25 MiB in total, and put the text `image` where older ones were. It never removes more than one screenshot per turn, because Subconscious Cache reuses around one removed screenshot, not several; a big screenshot can take the total past 25 MiB for a few turns while it catches up, and only past 30 MiB does it drop straight back to 25. The gateway applies the same rule, so it receives exactly what its own window would produce, and Subconscious Cache still reuses the rest of the conversation: each turn only reads the new screenshot.
294
+
295
+ | Agent | How subc applies it |
296
+ | --- | --- |
297
+ | OpenCode | A plugin loaded from the launch config (`bin/runbook/opencode/subconscious-image-window.ts`) that gives the provider a trimming `fetch`. |
298
+ | Pi | An extension installed with the provider (`bin/runbook/image-window/index.ts`, on `before_provider_request`). |
299
+
300
+ The rule is in `bin/runbook/image-window/window.js`. `SUBCONSCIOUS_IMAGE_WINDOW_SOFT_MIB` and `SUBCONSCIOUS_IMAGE_WINDOW_HARD_MIB` change the two limits (defaults 25 and 30). Text-only models are left alone, and a request the integrations do not recognize is sent unchanged.
301
+
164
302
  ## Sessions and cross-harness handoff
165
303
 
166
304
  Run `subc` and choose **Coding sessions**, or list the same local catalog from
@@ -174,7 +312,7 @@ subc sessions resume codex:SESSION_ID --harness opencode
174
312
  ```
175
313
 
176
314
  The catalog discovers recent sessions written locally by Claude Code, Codex,
177
- OpenCode, Pi, and Subconscious Code. It shows each session's originating
315
+ OpenCode, and Pi. It shows each session's originating
178
316
  harness, title, last activity, project directory, and model when the harness
179
317
  records one. Selecting the original harness uses its native resume mechanism.
180
318
 
@@ -190,26 +328,12 @@ conversation stores do not expose a stable local resume interface. A session
190
328
  whose local transcript is missing remains available for native resume but does
191
329
  not offer cross-harness destinations.
192
330
 
193
- If Subconscious Code, Claude Code, Codex, OpenCode, or DeepSeek Harness is missing, an interactive terminal offers
194
- to install it before launching. Pi refreshes its Subconscious provider on every
331
+ subc never installs OpenCode, Claude Code, Codex, DeepSeek Harness, or Pi. If
332
+ one is missing, subc prints its install command and official install page, then
333
+ exits with status 127, in interactive and headless runs alike. Pi refreshes its Subconscious provider on every
195
334
  `subc pi` launch while preserving all other providers in `models.json`; its
196
335
  executable must already be installed.
197
336
 
198
- `subc marathon install` detects the operating system and architecture, then downloads
199
- and checksum-verifies the matching precompiled release from
200
- `subconscious-systems/subconscious-code`. Apple Silicon and Intel macOS plus
201
- x86_64 and ARM64 Linux and Windows x64 are supported; Cargo is not required.
202
-
203
- Starting with CLI 4.1.1, `subc marathon` is the native agent command. On Windows,
204
- the installer writes `marathon.exe`, avoiding the system `sc.exe` command.
205
- `subc sc` remains a compatibility alias but never launches Windows service
206
- control. Older pinned release binaries are also installed as `marathon.exe`.
207
- Starting with CLI 4.1.2 and native agent 0.1.6, macOS and Linux also install
208
- and launch `marathon`. Existing `.sc` settings, sessions, and `SC_*` variables
209
- are preserved on every platform. Shell behavior is unchanged. Older Unix
210
- release archives are installed under the new name; existing `sc` files are
211
- left untouched. `subc sc` remains a compatibility alias on every platform.
212
-
213
337
  `subc codex` disables Codex apps and plugin tools for that launch by default so
214
338
  requests remain below the gateway's 128-tool limit. Core coding tools remain
215
339
  available. Use `subc codex --external-tools` to opt back in when targeting a
@@ -264,7 +388,7 @@ rewritten. Run a one-shot task with `subc dsh headless "PROMPT"`.
264
388
  ```
265
389
 
266
390
  The file is mode `600` and contains the shared gateway URL, API key, model,
267
- optional per-agent key overrides, and all Subconscious Code/Claude/Codex/OpenCode/Pi/Copilot/
391
+ optional per-agent key overrides, and all Claude/Codex/OpenCode/Pi/Copilot/
268
392
  DeepSeek Harness context and output settings used by the packaged runbook scripts.
269
393
 
270
394
  ```bash
@@ -0,0 +1,278 @@
1
+ import { readFileSync } from 'node:fs';
2
+ import path from 'node:path';
3
+ import { fileURLToPath } from 'node:url';
4
+
5
+ export const RUNBOOK_DIR = fileURLToPath(
6
+ new URL('./runbook/', import.meta.url),
7
+ );
8
+
9
+ const ENV_NAME = /^[A-Z_][A-Z0-9_]*$/;
10
+ const PLATFORMS = new Set(['darwin', 'linux', 'win32']);
11
+ const MODES = new Set(['launch', 'setup']);
12
+ const SETUP_ACTIONS = new Set(['install', 'status', 'uninstall']);
13
+ const INPUT_TYPES = new Set([
14
+ 'secret',
15
+ 'integer',
16
+ 'text',
17
+ 'choice',
18
+ 'url',
19
+ 'model',
20
+ ]);
21
+
22
+ function readJson(relative) {
23
+ const file = new URL(`./runbook/${relative}`, import.meta.url);
24
+ try {
25
+ return JSON.parse(readFileSync(file, 'utf-8'));
26
+ } catch (error) {
27
+ throw new Error(`Cannot read bin/runbook/${relative}: ${error.message}`);
28
+ }
29
+ }
30
+
31
+ function check(condition, where, message) {
32
+ if (!condition) throw new Error(`Invalid ${where}: ${message}`);
33
+ }
34
+
35
+ const isObject = (value) =>
36
+ Boolean(value) && typeof value === 'object' && !Array.isArray(value);
37
+ const isStringList = (value) =>
38
+ Array.isArray(value) && value.every((item) => typeof item === 'string');
39
+
40
+ function validateEntry(entry, where, { needsValue = false } = {}) {
41
+ check(isObject(entry), where, 'expected an object');
42
+ check(ENV_NAME.test(entry.name ?? ''), where, `bad name ${entry.name}`);
43
+ if (needsValue) check('value' in entry, where, `${entry.name} has no value`);
44
+ if (entry.type !== undefined)
45
+ check(INPUT_TYPES.has(entry.type), where, `bad type ${entry.type}`);
46
+ if (entry.override !== undefined)
47
+ check(isStringList(entry.override), where, `${entry.name} override`);
48
+ if (entry.aliases !== undefined)
49
+ check(isStringList(entry.aliases), where, `${entry.name} aliases`);
50
+ if (entry.flag !== undefined)
51
+ check(/^--[a-z][a-z-]*$/.test(entry.flag), where, `bad flag ${entry.flag}`);
52
+ if (entry.strict)
53
+ check(isStringList(entry.choices), where, `${entry.name} needs choices`);
54
+ }
55
+
56
+ function validateLaunch(launch, where) {
57
+ check(isObject(launch), where, 'launch agents need a launch block');
58
+ for (const key of ['argv', 'headless_argv', 'resume', 'handoff']) {
59
+ if (launch[key] === undefined) continue;
60
+ check(
61
+ isStringList(launch[key]) && launch[key].length > 0,
62
+ where,
63
+ `launch.${key} must be a non-empty list of strings`,
64
+ );
65
+ }
66
+ check(launch.argv !== undefined, where, 'launch.argv is required');
67
+ for (const key of ['mode_word', 'headless_stdin', 'config_flag'])
68
+ if (launch[key] !== undefined)
69
+ check(typeof launch[key] === 'string', where, `launch.${key}`);
70
+ if (launch.headless_argv) {
71
+ const platforms = launch.headless_platforms;
72
+ check(
73
+ isStringList(platforms) && platforms.every((p) => PLATFORMS.has(p)),
74
+ where,
75
+ 'launch.headless_platforms must list darwin, linux, or win32',
76
+ );
77
+ }
78
+ }
79
+
80
+ function validateInstall(install, where) {
81
+ check(isObject(install), where, 'install block is required');
82
+ check(
83
+ typeof install.url === 'string' && install.url.startsWith('https://'),
84
+ where,
85
+ 'install.url must be an https:// link to the official install page',
86
+ );
87
+ const { commands } = install;
88
+ if (commands === null) return;
89
+ check(
90
+ isObject(commands),
91
+ where,
92
+ 'install.commands must be an object or null',
93
+ );
94
+ for (const [os, command] of Object.entries(commands)) {
95
+ check(PLATFORMS.has(os), where, `install.commands.${os} is not an OS`);
96
+ check(
97
+ typeof command === 'string' && command,
98
+ where,
99
+ `install.commands.${os}`,
100
+ );
101
+ }
102
+ }
103
+
104
+ function validateRunbook(runbook, where) {
105
+ check(isObject(runbook), where, 'runbook block is required');
106
+ check(MODES.has(runbook.mode), where, `bad runbook.mode ${runbook.mode}`);
107
+ const actions = runbook.setup_actions ?? [];
108
+ check(
109
+ isStringList(actions) && actions.every((a) => SETUP_ACTIONS.has(a)),
110
+ where,
111
+ 'runbook.setup_actions may only list install, status, uninstall',
112
+ );
113
+ check(
114
+ actions.length === 0 || typeof runbook.setup_script === 'string',
115
+ where,
116
+ 'setup actions need runbook.setup_script',
117
+ );
118
+ }
119
+
120
+ /** Check one agent file; throws with the file and the first problem found. */
121
+ export function validateAgent(agent, id) {
122
+ const where = `bin/runbook/${id}/agent.json`;
123
+ check(isObject(agent), where, 'expected an object');
124
+ check(agent.id === id, where, `id must be ${id}`);
125
+ for (const key of ['command', 'name', 'description', 'protocol'])
126
+ check(typeof agent[key] === 'string' && agent[key], where, `${key}`);
127
+ check(isStringList(agent.aliases), where, 'aliases must be a list');
128
+ validateInstall(agent.install, where);
129
+ check(isObject(agent.help), where, 'help block is required');
130
+ check(Array.isArray(agent.help.options), where, 'help.options');
131
+ validateRunbook(agent.runbook, where);
132
+ if (agent.runbook.mode === 'launch') validateLaunch(agent.launch, where);
133
+ for (const key of ['prerequisites', 'inputs', 'env', 'config'])
134
+ check(Array.isArray(agent[key]), where, `${key} must be a list`);
135
+ for (const input of agent.inputs) validateEntry(input, `${where} inputs`);
136
+ check(
137
+ !agent.config.some((entry) => entry.kind === 'cli-config') ||
138
+ typeof agent.launch?.config_flag === 'string',
139
+ where,
140
+ 'cli-config entries need launch.config_flag',
141
+ );
142
+ for (const entry of agent.env)
143
+ validateEntry(entry, `${where} env`, { needsValue: true });
144
+ const caps = agent.capabilities;
145
+ check(isObject(caps) && isObject(caps.compaction), where, 'capabilities');
146
+ return agent;
147
+ }
148
+
149
+ function loadShared() {
150
+ const shared = readJson('shared.json');
151
+ check(isStringList(shared.agents), 'shared.json', 'agents must be ids');
152
+ check(isObject(shared.defaults), 'shared.json', 'defaults is required');
153
+ for (const input of shared.inputs ?? [])
154
+ validateEntry(input, 'shared.json inputs');
155
+ return shared;
156
+ }
157
+
158
+ const shared = loadShared();
159
+
160
+ export const DEFAULTS = Object.freeze(shared.defaults);
161
+ export const PACKAGED_MODELS = Object.freeze(
162
+ DEFAULTS.models?.length ? [...DEFAULTS.models] : [DEFAULTS.model],
163
+ );
164
+ export const MODEL_CAPABILITIES = Object.freeze(
165
+ shared.model_capabilities ?? {},
166
+ );
167
+ export const SHARED_INPUTS = Object.freeze(shared.inputs ?? []);
168
+
169
+ export const AGENTS = Object.freeze(
170
+ shared.agents.map((id) => validateAgent(readJson(`${id}/agent.json`), id)),
171
+ );
172
+
173
+ const SHARED_BY_NAME = new Map(SHARED_INPUTS.map((i) => [i.name, i]));
174
+
175
+ export function agentById(id) {
176
+ return AGENTS.find((agent) => agent.id === id) ?? null;
177
+ }
178
+
179
+ /** An agent's inputs, each merged over the shared input of the same name. */
180
+ export function agentInputs(agent) {
181
+ return agent.inputs.map((input) => ({
182
+ ...(SHARED_BY_NAME.get(input.name) || {}),
183
+ ...input,
184
+ }));
185
+ }
186
+
187
+ export function agentBinary(agent) {
188
+ return agent.launch?.argv?.[0] ?? null;
189
+ }
190
+
191
+ /** Per-OS install commands with `{package}` filled in, or null. */
192
+ export function installCommands(agent) {
193
+ const { commands, package: name } = agent.install;
194
+ if (!commands) return null;
195
+ return Object.fromEntries(
196
+ Object.entries(commands).map(([os, command]) => [
197
+ os,
198
+ name ? command.replaceAll('{package}', name) : command,
199
+ ]),
200
+ );
201
+ }
202
+
203
+ /**
204
+ * The install command and official install page to show when the agent's
205
+ * binary is missing. subc prints these and never runs them. Windows never
206
+ * borrows the Linux command.
207
+ */
208
+ export function installAdvice(agent, platform = process.platform) {
209
+ const commands = installCommands(agent);
210
+ const command =
211
+ commands?.[platform] ?? (platform === 'win32' ? null : commands?.linux);
212
+ return { command: command ?? null, url: agent.install.url };
213
+ }
214
+
215
+ /** The API key input that overrides API_KEY for this agent, if any. */
216
+ export function agentKeyName(agent) {
217
+ return (
218
+ agent.inputs.find(
219
+ (input) => input.type === 'secret' && input.name !== 'API_KEY',
220
+ )?.name ?? null
221
+ );
222
+ }
223
+
224
+ export function agentCommandName(agent) {
225
+ return agent.command || agent.id;
226
+ }
227
+
228
+ export function agentSetupActions(agent) {
229
+ return agent.runbook?.setup_actions ?? [];
230
+ }
231
+
232
+ /** Where the agent's own installer puts its binary, if it says. */
233
+ export function agentInstallDirs(agent, environment, home) {
234
+ const { install_dir: dir, install_dir_env: envName } = agent.install;
235
+ const configured = envName ? environment[envName]?.trim() : '';
236
+ if (configured) return [path.resolve(configured)];
237
+ return dir ? [path.resolve(home, dir.replace(/^~[/\\]/, ''))] : [];
238
+ }
239
+
240
+ const WHOLE_REF = /^\$\{([A-Z_][A-Z0-9_]*)\}$/;
241
+ const REF = /\$\{([A-Z_][A-Z0-9_]*)\}/g;
242
+
243
+ /**
244
+ * Replace `${NAME}` references with `lookup(NAME)`, keeping the referenced
245
+ * value's type when the reference is the whole string. Unknown names stay.
246
+ */
247
+ export function resolveRefs(value, lookup) {
248
+ if (typeof value === 'string') {
249
+ const whole = WHOLE_REF.exec(value);
250
+ if (whole) {
251
+ const target = lookup(whole[1]);
252
+ return target === undefined ? value : resolveRefs(target, lookup);
253
+ }
254
+ return value.replace(REF, (match, name) => {
255
+ const target = lookup(name);
256
+ return target === undefined ? match : String(resolveRefs(target, lookup));
257
+ });
258
+ }
259
+ if (Array.isArray(value))
260
+ return value.map((item) => resolveRefs(item, lookup));
261
+ if (isObject(value))
262
+ return Object.fromEntries(
263
+ Object.entries(value).map(([key, item]) => [
264
+ key,
265
+ resolveRefs(item, lookup),
266
+ ]),
267
+ );
268
+ return value;
269
+ }
270
+
271
+ /** Look up an input's default or an env entry's value by name. */
272
+ export function staticLookup(agent) {
273
+ const values = new Map();
274
+ for (const entry of agent.env) values.set(entry.name, entry.value);
275
+ for (const input of agentInputs(agent))
276
+ if (input.default !== undefined) values.set(input.name, input.default);
277
+ return (name) => values.get(name);
278
+ }