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.
- package/README.md +167 -43
- package/bin/agent-data.js +278 -0
- package/bin/agent-help.js +114 -0
- package/bin/agent-install.js +238 -0
- package/bin/agent-launch.js +199 -0
- package/bin/agent-spawn.js +137 -0
- package/bin/agents.js +236 -931
- package/bin/auth.js +9 -1
- package/bin/claude-picker.js +143 -0
- package/bin/cli.js +62 -2
- package/bin/clipboard-image.js +2 -0
- package/bin/feedback.js +14 -3
- package/bin/harness-manifest.js +286 -0
- package/bin/headless-args.js +35 -0
- package/bin/launch-values.js +25 -0
- package/bin/model-capabilities.js +10 -9
- package/bin/models.js +25 -13
- package/bin/native/subc-tui-darwin-amd64 +0 -0
- package/bin/native/subc-tui-darwin-arm64 +0 -0
- package/bin/native/subc-tui-linux-amd64 +0 -0
- package/bin/native/subc-tui-linux-arm64 +0 -0
- package/bin/native/subc-tui-windows-amd64.exe +0 -0
- package/bin/native/subc-tui-windows-arm64.exe +0 -0
- package/bin/opencode-provider.js +21 -0
- package/bin/profile-settings.js +129 -0
- package/bin/profiles.js +29 -424
- package/bin/runbook/README.md +77 -23
- package/bin/runbook/claude-code/agent.json +300 -0
- package/bin/runbook/claude-code/install.sh +9 -252
- package/bin/runbook/codex/agent.json +462 -0
- package/bin/runbook/codex/hooks-lib.sh +17 -4
- package/bin/runbook/codex/install.sh +0 -4
- package/bin/runbook/codex/run.sh +16 -235
- package/bin/runbook/copilot/agent.json +186 -0
- package/bin/runbook/copilot/install.sh +45 -63
- package/bin/runbook/cursor/agent.json +138 -0
- package/bin/runbook/cursor/install.sh +8 -35
- package/bin/runbook/deepseek-harness/agent.json +164 -0
- package/bin/runbook/deepseek-harness/run.sh +20 -59
- package/bin/runbook/image-window/index.ts +14 -0
- package/bin/runbook/image-window/window.d.ts +19 -0
- package/bin/runbook/image-window/window.js +140 -0
- package/bin/runbook/lib.sh +41 -0
- package/bin/runbook/opencode/agent.json +159 -0
- package/bin/runbook/opencode/install.sh +9 -183
- package/bin/runbook/opencode/opencode-plugin.d.ts +10 -3
- package/bin/runbook/opencode/subconscious-image-window.ts +24 -0
- package/bin/runbook/pi/agent.json +217 -0
- package/bin/runbook/pi/install.sh +18 -44
- package/bin/runbook/pi/run.sh +6 -18
- package/bin/runbook/pi/subconscious-compaction.ts +3 -1
- package/bin/runbook/shared.json +57 -0
- package/bin/sessions.js +53 -48
- package/bin/tui.js +13 -0
- package/bin/update-check.js +35 -1
- package/bin/upgrade.js +27 -34
- package/bin/usage.js +21 -10
- package/bin/windows/agents.js +28 -60
- package/bin/windows/common.js +2 -28
- package/bin/windows/launch.js +252 -231
- package/bin/windows/process.js +12 -1
- package/bin/windows/setup.js +29 -9
- package/package.json +2 -4
- package/bin/registry.generated.json +0 -406
- package/bin/runbook/claude-code/run.sh +0 -254
- package/bin/runbook/model-capabilities.generated.sh +0 -9
- package/bin/runbook/opencode/provider.sh +0 -93
- package/bin/runbook/opencode/run.sh +0 -99
- package/bin/runbook/opencode/subconscious-compaction.ts +0 -100
- package/bin/runbook/subconscious-code/install.sh +0 -154
- package/bin/runbook/subconscious-code/run.sh +0 -21
- package/bin/windows/claude.js +0 -100
- 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
|
|
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
|
|
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
|
|
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.
|
|
109
|
-
|
|
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
|
|
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
|
|
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,
|
|
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
|
-
|
|
194
|
-
|
|
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
|
|
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
|
+
}
|