subconscious-cli 6.0.1 → 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.
- package/README.md +119 -0
- package/bin/agent-data.js +242 -0
- package/bin/agent-help.js +106 -0
- package/bin/agent-install.js +354 -0
- package/bin/agent-launch.js +199 -0
- package/bin/agent-spawn.js +137 -0
- package/bin/agents.js +236 -932
- package/bin/auth.js +9 -1
- package/bin/claude-picker.js +143 -0
- package/bin/cli.js +60 -1
- 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 +13 -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 +66 -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 +461 -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 +185 -0
- package/bin/runbook/copilot/install.sh +45 -63
- package/bin/runbook/cursor/agent.json +137 -0
- package/bin/runbook/cursor/install.sh +8 -35
- package/bin/runbook/deepseek-harness/agent.json +163 -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 +158 -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 +216 -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 +58 -0
- package/bin/runbook/subconscious-code/agent.json +156 -0
- package/bin/sessions.js +52 -22
- package/bin/tui.js +13 -0
- package/bin/update-check.js +5 -1
- package/bin/upgrade.js +24 -31
- package/bin/usage.js +21 -10
- package/bin/windows/agents.js +18 -17
- package/bin/windows/common.js +2 -28
- package/bin/windows/install.js +11 -17
- 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 -3
- 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/run.sh +0 -21
- package/bin/windows/claude.js +0 -100
package/README.md
CHANGED
|
@@ -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
|
|
@@ -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
|
+
}
|