subconscious-cli 6.0.0 → 6.1.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 (76) hide show
  1. package/README.md +135 -2
  2. package/bin/agent-data.js +242 -0
  3. package/bin/agent-help.js +106 -0
  4. package/bin/agent-install.js +354 -0
  5. package/bin/agent-launch.js +199 -0
  6. package/bin/agent-spawn.js +137 -0
  7. package/bin/agents.js +284 -840
  8. package/bin/auth.js +89 -33
  9. package/bin/branding.js +3 -1
  10. package/bin/claude-picker.js +143 -0
  11. package/bin/cli.js +87 -20
  12. package/bin/clipboard-image.js +24 -5
  13. package/bin/colors.js +1 -1
  14. package/bin/feedback.js +71 -21
  15. package/bin/harness-manifest.js +286 -0
  16. package/bin/headless-args.js +35 -0
  17. package/bin/launch-values.js +25 -0
  18. package/bin/model-capabilities.js +12 -8
  19. package/bin/models.js +35 -19
  20. package/bin/native/subc-tui-darwin-amd64 +0 -0
  21. package/bin/native/subc-tui-darwin-arm64 +0 -0
  22. package/bin/native/subc-tui-linux-amd64 +0 -0
  23. package/bin/native/subc-tui-linux-arm64 +0 -0
  24. package/bin/native/subc-tui-windows-amd64.exe +0 -0
  25. package/bin/native/subc-tui-windows-arm64.exe +0 -0
  26. package/bin/opencode-provider.js +48 -5
  27. package/bin/profile-settings.js +129 -0
  28. package/bin/profiles.js +136 -449
  29. package/bin/runbook/README.md +66 -23
  30. package/bin/runbook/claude-code/agent.json +300 -0
  31. package/bin/runbook/claude-code/install.sh +9 -252
  32. package/bin/runbook/codex/agent.json +461 -0
  33. package/bin/runbook/codex/hooks-lib.sh +17 -4
  34. package/bin/runbook/codex/install.sh +0 -4
  35. package/bin/runbook/codex/run.sh +16 -235
  36. package/bin/runbook/copilot/agent.json +185 -0
  37. package/bin/runbook/copilot/install.sh +45 -63
  38. package/bin/runbook/cursor/agent.json +137 -0
  39. package/bin/runbook/cursor/install.sh +8 -35
  40. package/bin/runbook/deepseek-harness/agent.json +163 -0
  41. package/bin/runbook/deepseek-harness/run.sh +20 -59
  42. package/bin/runbook/image-window/index.ts +14 -0
  43. package/bin/runbook/image-window/window.d.ts +19 -0
  44. package/bin/runbook/image-window/window.js +140 -0
  45. package/bin/runbook/lib.sh +41 -0
  46. package/bin/runbook/opencode/agent.json +158 -0
  47. package/bin/runbook/opencode/install.sh +9 -183
  48. package/bin/runbook/opencode/opencode-plugin.d.ts +16 -0
  49. package/bin/runbook/opencode/subconscious-image-window.ts +24 -0
  50. package/bin/runbook/pi/agent.json +216 -0
  51. package/bin/runbook/pi/install.sh +18 -44
  52. package/bin/runbook/pi/run.sh +6 -18
  53. package/bin/runbook/pi/subconscious-compaction.ts +71 -49
  54. package/bin/runbook/shared.json +58 -0
  55. package/bin/runbook/subconscious-code/agent.json +156 -0
  56. package/bin/sessions.js +173 -58
  57. package/bin/tui.js +104 -32
  58. package/bin/update-check.js +54 -21
  59. package/bin/upgrade.js +67 -43
  60. package/bin/usage.js +47 -24
  61. package/bin/windows/agents.js +141 -37
  62. package/bin/windows/common.js +36 -25
  63. package/bin/windows/install.js +116 -34
  64. package/bin/windows/launch.js +260 -77
  65. package/bin/windows/pi-compaction.ts +68 -13
  66. package/bin/windows/process.js +210 -57
  67. package/bin/windows/setup.js +251 -56
  68. package/package.json +9 -4
  69. package/bin/registry.generated.json +0 -406
  70. package/bin/runbook/claude-code/run.sh +0 -254
  71. package/bin/runbook/model-capabilities.generated.sh +0 -9
  72. package/bin/runbook/opencode/provider.sh +0 -93
  73. package/bin/runbook/opencode/run.sh +0 -99
  74. package/bin/runbook/opencode/subconscious-compaction.ts +0 -100
  75. package/bin/runbook/subconscious-code/run.sh +0 -21
  76. package/bin/windows/claude.js +0 -71
package/README.md CHANGED
@@ -82,7 +82,7 @@ Windows has its own implementation in `bin/windows/`, selected only when
82
82
  Node runs on Windows. macOS/Linux continue to use the existing, unchanged shell
83
83
  runbooks. WSL continues to use the Linux implementation.
84
84
 
85
- Use Node.js 22 or 24 and Windows PowerShell 5.1+ (included with Windows), from either
85
+ Use Node.js 20, 22, or 24 and Windows PowerShell 5.1+ (included with Windows), from either
86
86
  PowerShell or Command Prompt. The Windows CLI integrations do not require Bash,
87
87
  `jq`, `curl`, or WSL. The agents themselves must support your Windows version;
88
88
  their own dependencies still apply.
@@ -129,7 +129,7 @@ VS Code's secret store and prompts for its key. Its configuration goes under
129
129
  `%APPDATA%\Code\User` (or Code - Insiders/VSCodium). Rerun install after moving
130
130
  your Node installation, because hooks record the absolute Node executable path.
131
131
 
132
- To install the native x64 `sc.exe` (stable releases starting with 0.1.4), clear
132
+ To install the native x64 `marathon.exe` (stable releases starting with 0.1.4), clear
133
133
  any preview version pin from the current PowerShell session:
134
134
 
135
135
  ```powershell
@@ -161,6 +161,125 @@ The packaged integrations live in `bin/runbook`.
161
161
  | `subc pi` | Refresh the Pi provider from the live catalog, then launch |
162
162
  | `subc dsh` | Launch the DeepSeek Harness Web UI with a temporary provider populated from the live catalog |
163
163
 
164
+ ## Headless runs
165
+
166
+ `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`, `marathon`, 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 `--`.
167
+
168
+ 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`.
169
+
170
+ Model, endpoint, and key resolve as for any launch, so a logged-in user needs nothing extra. To point a run somewhere else:
171
+
172
+ | Input | How to set it |
173
+ | --- | --- |
174
+ | Model | `--model`, or `SUBCONSCIOUS_MODEL` |
175
+ | Endpoint | `SUBCONSCIOUS_BASE_URL`. In a headless run every agent uses it, including Claude Code (`CLAUDE_GATEWAY_URL` is ignored). |
176
+ | Key | `SUBCONSCIOUS_API_KEY`, otherwise the `subc login` key |
177
+
178
+ ```bash
179
+ SUBCONSCIOUS_BASE_URL=https://gateway.example SUBCONSCIOUS_API_KEY=... \
180
+ subc codex headless "fix the failing test" --model subconscious/deepseek-v4.1-flash-marathon
181
+ ```
182
+
183
+ 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.
184
+
185
+ | Agent | Runs |
186
+ | --- | --- |
187
+ | `claude` | `claude -p -- PROMPT` |
188
+ | `codex` | `codex exec -- PROMPT` |
189
+ | `opencode` | `opencode run`, with the prompt on stdin |
190
+ | `pi` | `pi --print`, with the prompt on stdin |
191
+ | `marathon` | `marathon --print=PROMPT` |
192
+ | `dsh` | `dsh --profile headless`, with the prompt on stdin |
193
+
194
+ 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.
195
+
196
+ 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.
197
+
198
+ 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.
199
+
200
+ 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.
201
+
202
+ ## Harness manifest
203
+
204
+ `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.
205
+
206
+ ```bash
207
+ subc harness-manifest --json # full manifest
208
+ subc harness-manifest # summary table in a terminal, JSON when piped
209
+ ```
210
+
211
+ 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`.
212
+
213
+ | Field | Meaning |
214
+ | --- | --- |
215
+ | `schema_version` | Raised when a field is renamed, removed, or changes meaning. New fields do not raise it. |
216
+ | `cli_version` | The installed `subconscious-cli` version. |
217
+ | `tokens` | Placeholders used in values (listed below). |
218
+ | `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`). |
219
+ | `harnesses.<id>` | One entry per agent. |
220
+
221
+ Each `harnesses.<id>` entry has:
222
+
223
+ | Field | Meaning |
224
+ | --- | --- |
225
+ | `install` | `method`, `package` or `repository`, `version` or `channel`, `minimum_version`, release `targets`, and the per-OS `commands`. |
226
+ | `prerequisites` | Commands the runbook needs, such as `jq`, with what needs them and whether they are required. `a\|b` means either one. |
227
+ | `binary` | Executable name, or `null` for IDE integrations. |
228
+ | `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. |
229
+ | `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`. |
230
+ | `env` | Variables the agent receives: `value`, what it is (`description`), and the `override` variables that win when set. |
231
+ | `config` | Files, directories, `-c` overrides (with `condition`), flags, and JSON-in-env. |
232
+ | `capabilities.compaction` | `default`, plus `on`, `off`, and `threshold`, each with the exact knob and whether subc sets it (`set_by_subc`). |
233
+ | `capabilities.mcp` | Whether subc configures MCP, and which transports. |
234
+ | `capabilities.headers` | Headers sent and on which requests. |
235
+ | `capabilities.hooks` | Hook events installed, what each posts, and where. |
236
+
237
+ 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).
238
+
239
+ Enumerated values:
240
+
241
+ | Field | Values |
242
+ | --- | --- |
243
+ | `install.method` | `npm`, `script`, `github-release`, `user`, `null` |
244
+ | `compaction.default` | `on`, `unknown` |
245
+ | knob `kind` | `env`, `cli-config`, `config-field`, `model-catalog-field`, `null` |
246
+ | `threshold.semantics` | `window`, `trigger` |
247
+ | `config[].kind` | `file`, `dir`, `cli-flag`, `cli-config`, `env-json` |
248
+ | `config[].lifetime` | `launch` (removed or not written to disk), `persistent`, `legacy` (left by older setups) |
249
+
250
+ Placeholders:
251
+
252
+ | Token | Meaning |
253
+ | --- | --- |
254
+ | `{baseUrl}`, `{baseUrlV1}` | Gateway origin, and the origin followed by `/v1` |
255
+ | `{apiKey}`, `{model}` | The agent's API key (its own key input, else `API_KEY`) and the launch model |
256
+ | `${NAME}` | The value of env var `NAME` at launch; left in place only where it has no default |
257
+ | `{catalog}`, `{catalog[N]}` | All live catalog models (newline-separated), or the one at index N (clamped to the last) |
258
+ | `{args}`, `{config}` | Passed-through arguments, and Codex's `-c` overrides |
259
+ | `{prompt}` | The task given to `subc <agent> headless` |
260
+ | `{sessionId}`, `{sessionFile}` | A local session's ID or file, for `launch.resume` |
261
+ | `{tempFile}`, `{tmp}` | A temporary file the runbook writes (its `config` entry says whether it is removed), the system temp directory |
262
+ | `{opencodeConfig}`, `{claudeSettings}` | Documents shown in the matching `config` entries |
263
+ | `{target}` | A release target triple from `install.targets` or `install.windows_targets` |
264
+ | `{binDir}`, `{PATH}`, `<install dirs>` | Parts of the `PATH` subc builds |
265
+ | `<agent key>` | The agent-specific API key input, such as `CODEX_API_KEY` |
266
+ | `<id>`, `models[]` | Any catalog model ID, and every element of a models array |
267
+ | `<runbook>` | The installed `bin/runbook` directory |
268
+ | `<VS Code user dir>` | The VS Code user settings directory |
269
+
270
+ `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.
271
+
272
+ ## Long screenshot sessions
273
+
274
+ 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.
275
+
276
+ | Agent | How subc applies it |
277
+ | --- | --- |
278
+ | OpenCode | A plugin loaded from the launch config (`bin/runbook/opencode/subconscious-image-window.ts`) that gives the provider a trimming `fetch`. |
279
+ | Pi | An extension installed with the provider (`bin/runbook/image-window/index.ts`, on `before_provider_request`). |
280
+
281
+ 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.
282
+
164
283
  ## Sessions and cross-harness handoff
165
284
 
166
285
  Run `subc` and choose **Coding sessions**, or list the same local catalog from
@@ -330,6 +449,7 @@ subconscious/glm-5.3-marathon (default)
330
449
  subconscious/glm-5.2
331
450
  subconscious/tim-qwen3.6-27b
332
451
  subconscious/deepseek-v4-flash-marathon
452
+ subconscious/deepseek-v4.1-flash-marathon
333
453
  ```
334
454
 
335
455
  Select a model per run, save it in the current profile, or override it through
@@ -443,6 +563,19 @@ Existing credentials and profiles under `~/.subcon` are copied into
443
563
  `~/.subconscious` automatically on first use. The legacy files are left in
444
564
  place so migration is recoverable.
445
565
 
566
+ Set `SUBC_CONFIG_DIR` to keep the saved key and profiles in another directory
567
+ instead of `~/.subconscious`. Legacy `~/.subcon` files are not migrated when it
568
+ is set.
569
+
570
+ `subc feedback` sends a message to the Subconscious support team from the
571
+ terminal, using the active profile's API key. Pass `-s`/`--subject`,
572
+ `-m`/`--message`, and `--image <path>` (JPEG or PNG, repeatable), or run it
573
+ without `--message` in a terminal to be prompted:
574
+
575
+ ```bash
576
+ subc feedback -s "Bug" -m "Playground won't load"
577
+ ```
578
+
446
579
  ## Contributing
447
580
 
448
581
  Setup, commit message rules, and local checks are in [CONTRIBUTING.md](CONTRIBUTING.md). `npm install` installs the commit-message hook. Version bumps and the changelog are created by Release Please when a release pull request merges to `main`.
@@ -0,0 +1,242 @@
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 validateRunbook(runbook, where) {
81
+ check(isObject(runbook), where, 'runbook block is required');
82
+ check(MODES.has(runbook.mode), where, `bad runbook.mode ${runbook.mode}`);
83
+ const actions = runbook.setup_actions ?? [];
84
+ check(
85
+ isStringList(actions) && actions.every((a) => SETUP_ACTIONS.has(a)),
86
+ where,
87
+ 'runbook.setup_actions may only list install, status, uninstall',
88
+ );
89
+ check(
90
+ actions.length === 0 || typeof runbook.setup_script === 'string',
91
+ where,
92
+ 'setup actions need runbook.setup_script',
93
+ );
94
+ }
95
+
96
+ /** Check one agent file; throws with the file and the first problem found. */
97
+ export function validateAgent(agent, id) {
98
+ const where = `bin/runbook/${id}/agent.json`;
99
+ check(isObject(agent), where, 'expected an object');
100
+ check(agent.id === id, where, `id must be ${id}`);
101
+ for (const key of ['command', 'name', 'description', 'protocol'])
102
+ check(typeof agent[key] === 'string' && agent[key], where, `${key}`);
103
+ check(isStringList(agent.aliases), where, 'aliases must be a list');
104
+ check(isObject(agent.install), where, 'install block is required');
105
+ check(isObject(agent.help), where, 'help block is required');
106
+ check(Array.isArray(agent.help.options), where, 'help.options');
107
+ validateRunbook(agent.runbook, where);
108
+ if (agent.runbook.mode === 'launch') validateLaunch(agent.launch, where);
109
+ for (const key of ['prerequisites', 'inputs', 'env', 'config'])
110
+ check(Array.isArray(agent[key]), where, `${key} must be a list`);
111
+ for (const input of agent.inputs) validateEntry(input, `${where} inputs`);
112
+ check(
113
+ !agent.config.some((entry) => entry.kind === 'cli-config') ||
114
+ typeof agent.launch?.config_flag === 'string',
115
+ where,
116
+ 'cli-config entries need launch.config_flag',
117
+ );
118
+ for (const entry of agent.env)
119
+ validateEntry(entry, `${where} env`, { needsValue: true });
120
+ const caps = agent.capabilities;
121
+ check(isObject(caps) && isObject(caps.compaction), where, 'capabilities');
122
+ return agent;
123
+ }
124
+
125
+ function loadShared() {
126
+ const shared = readJson('shared.json');
127
+ check(isStringList(shared.agents), 'shared.json', 'agents must be ids');
128
+ check(isObject(shared.defaults), 'shared.json', 'defaults is required');
129
+ for (const input of shared.inputs ?? [])
130
+ validateEntry(input, 'shared.json inputs');
131
+ return shared;
132
+ }
133
+
134
+ const shared = loadShared();
135
+
136
+ export const DEFAULTS = Object.freeze(shared.defaults);
137
+ export const PACKAGED_MODELS = Object.freeze(
138
+ DEFAULTS.models?.length ? [...DEFAULTS.models] : [DEFAULTS.model],
139
+ );
140
+ export const MODEL_CAPABILITIES = Object.freeze(
141
+ shared.model_capabilities ?? {},
142
+ );
143
+ export const SHARED_INPUTS = Object.freeze(shared.inputs ?? []);
144
+
145
+ export const AGENTS = Object.freeze(
146
+ shared.agents.map((id) => validateAgent(readJson(`${id}/agent.json`), id)),
147
+ );
148
+
149
+ const SHARED_BY_NAME = new Map(SHARED_INPUTS.map((i) => [i.name, i]));
150
+
151
+ export function agentById(id) {
152
+ return AGENTS.find((agent) => agent.id === id) ?? null;
153
+ }
154
+
155
+ /** An agent's inputs, each merged over the shared input of the same name. */
156
+ export function agentInputs(agent) {
157
+ return agent.inputs.map((input) => ({
158
+ ...(SHARED_BY_NAME.get(input.name) || {}),
159
+ ...input,
160
+ }));
161
+ }
162
+
163
+ export function agentBinary(agent) {
164
+ return agent.launch?.argv?.[0] ?? null;
165
+ }
166
+
167
+ /** Per-OS install commands with `{package}` filled in, or null. */
168
+ export function installCommands(agent) {
169
+ const { commands, package: name } = agent.install;
170
+ if (!commands) return null;
171
+ return Object.fromEntries(
172
+ Object.entries(commands).map(([os, command]) => [
173
+ os,
174
+ name ? command.replaceAll('{package}', name) : command,
175
+ ]),
176
+ );
177
+ }
178
+
179
+ /** The API key input that overrides API_KEY for this agent, if any. */
180
+ export function agentKeyName(agent) {
181
+ return (
182
+ agent.inputs.find(
183
+ (input) => input.type === 'secret' && input.name !== 'API_KEY',
184
+ )?.name ?? null
185
+ );
186
+ }
187
+
188
+ export function agentCommandName(agent) {
189
+ return agent.command || agent.id;
190
+ }
191
+
192
+ export function agentSetupActions(agent) {
193
+ return agent.runbook?.setup_actions ?? [];
194
+ }
195
+
196
+ /** Where the agent's own installer puts its binary, if it says. */
197
+ export function agentInstallDirs(agent, environment, home) {
198
+ const { install_dir: dir, install_dir_env: envName } = agent.install;
199
+ const configured = envName ? environment[envName]?.trim() : '';
200
+ if (configured) return [path.resolve(configured)];
201
+ return dir ? [path.resolve(home, dir.replace(/^~[/\\]/, ''))] : [];
202
+ }
203
+
204
+ const WHOLE_REF = /^\$\{([A-Z_][A-Z0-9_]*)\}$/;
205
+ const REF = /\$\{([A-Z_][A-Z0-9_]*)\}/g;
206
+
207
+ /**
208
+ * Replace `${NAME}` references with `lookup(NAME)`, keeping the referenced
209
+ * value's type when the reference is the whole string. Unknown names stay.
210
+ */
211
+ export function resolveRefs(value, lookup) {
212
+ if (typeof value === 'string') {
213
+ const whole = WHOLE_REF.exec(value);
214
+ if (whole) {
215
+ const target = lookup(whole[1]);
216
+ return target === undefined ? value : resolveRefs(target, lookup);
217
+ }
218
+ return value.replace(REF, (match, name) => {
219
+ const target = lookup(name);
220
+ return target === undefined ? match : String(resolveRefs(target, lookup));
221
+ });
222
+ }
223
+ if (Array.isArray(value))
224
+ return value.map((item) => resolveRefs(item, lookup));
225
+ if (isObject(value))
226
+ return Object.fromEntries(
227
+ Object.entries(value).map(([key, item]) => [
228
+ key,
229
+ resolveRefs(item, lookup),
230
+ ]),
231
+ );
232
+ return value;
233
+ }
234
+
235
+ /** Look up an input's default or an env entry's value by name. */
236
+ export function staticLookup(agent) {
237
+ const values = new Map();
238
+ for (const entry of agent.env) values.set(entry.name, entry.value);
239
+ for (const input of agentInputs(agent))
240
+ if (input.default !== undefined) values.set(input.name, input.default);
241
+ return (name) => values.get(name);
242
+ }
@@ -0,0 +1,106 @@
1
+ import {
2
+ agentCommandName,
3
+ agentInputs,
4
+ agentSetupActions,
5
+ } from './agent-data.js';
6
+ import { c } from './colors.js';
7
+ import { profileSettingsForAgent, resolvedProfileValues } from './profiles.js';
8
+
9
+ export function isAgentHelpRequest(argv = []) {
10
+ return ['help', '-h', '--help'].includes(argv[0]);
11
+ }
12
+
13
+ function displayProfileValue(setting, value, values) {
14
+ if (setting.type === 'secret') {
15
+ if (value) return '(set)';
16
+ return setting.key !== 'API_KEY' && values.API_KEY
17
+ ? '(shared key)'
18
+ : '(not set)';
19
+ }
20
+ if (!value && setting.model) return 'UNSET';
21
+ return value || '(auto)';
22
+ }
23
+
24
+ function optionText(description) {
25
+ if (typeof description === 'string') return description;
26
+ return description[process.platform] ?? description.default;
27
+ }
28
+
29
+ /** Static options, then documented flags, then the pass-through argument. */
30
+ function helpOptions(agent) {
31
+ const { options, args } = agent.help;
32
+ const flags = agentInputs(agent)
33
+ .filter((input) => input.flag && input.help)
34
+ .map((input) => [
35
+ input.metavar ? `${input.flag} ${input.metavar}` : input.flag,
36
+ input.help,
37
+ ]);
38
+ const listed = [...options, ...flags, ...(args ? [args] : [])].map(
39
+ ([option, description]) => [option, optionText(description)],
40
+ );
41
+ const headless =
42
+ agent.launch?.headless_argv &&
43
+ !listed.some(([option]) => option.startsWith('headless'));
44
+ return headless
45
+ ? [
46
+ ...listed,
47
+ ['headless PROMPT', 'Run one task without prompting, then exit'],
48
+ ]
49
+ : listed;
50
+ }
51
+
52
+ function printSettings(agent, profile) {
53
+ const settings = profileSettingsForAgent(agent.id);
54
+ const values = resolvedProfileValues(profile);
55
+ const width = Math.max(0, ...settings.map((setting) => setting.key.length));
56
+ console.log(
57
+ ` ${c.bold}Profile settings${c.reset} ${c.dim}(${profile?.name || 'default'})${c.reset}`,
58
+ );
59
+ for (const setting of settings) {
60
+ const value = displayProfileValue(setting, values[setting.key], values);
61
+ console.log(
62
+ ` ${c.cyan}${setting.key.padEnd(width)}${c.reset} ${value}`,
63
+ );
64
+ console.log(
65
+ ` ${' '.repeat(width)} ${c.dim}${setting.description}${c.reset}`,
66
+ );
67
+ }
68
+ }
69
+
70
+ function printSetupHints(agent, profile) {
71
+ const command = agentCommandName(agent);
72
+ console.log(
73
+ `\n Edit the env file with ${c.cyan}subc -p ${profile?.name || 'default'} config edit${c.reset}.`,
74
+ );
75
+ const actions = agentSetupActions(agent);
76
+ if (actions.includes('install')) {
77
+ const what = agent.runbook.binary_install_script
78
+ ? 'Install the agent binary with'
79
+ : 'Install the persistent integration with';
80
+ console.log(` ${what} ${c.cyan}subc ${command} install${c.reset}.`);
81
+ }
82
+ if (actions.includes('uninstall')) {
83
+ console.log(
84
+ ` Remove it with ${c.cyan}subc ${command} uninstall${c.reset}.`,
85
+ );
86
+ }
87
+ }
88
+
89
+ export function printAgentHelp(agent, profile) {
90
+ const options = helpOptions(agent);
91
+ const width = Math.max(0, ...options.map(([option]) => option.length));
92
+ console.log(`\n ${c.bold}${agent.name} + Subconscious${c.reset}\n`);
93
+ console.log(` ${agent.help.behavior}\n`);
94
+ console.log(` ${c.bold}Usage${c.reset}\n ${agent.help.usage}\n`);
95
+ if (options.length) {
96
+ console.log(` ${c.bold}Commands and options${c.reset}`);
97
+ for (const [option, description] of options)
98
+ console.log(
99
+ ` ${c.cyan}${option.padEnd(width)}${c.reset} ${description}`,
100
+ );
101
+ console.log();
102
+ }
103
+ printSettings(agent, profile);
104
+ printSetupHints(agent, profile);
105
+ console.log();
106
+ }