subconscious-cli 6.0.1 → 6.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (73) hide show
  1. package/README.md +167 -43
  2. package/bin/agent-data.js +278 -0
  3. package/bin/agent-help.js +114 -0
  4. package/bin/agent-install.js +238 -0
  5. package/bin/agent-launch.js +199 -0
  6. package/bin/agent-spawn.js +137 -0
  7. package/bin/agents.js +236 -931
  8. package/bin/auth.js +9 -1
  9. package/bin/claude-picker.js +143 -0
  10. package/bin/cli.js +62 -2
  11. package/bin/clipboard-image.js +2 -0
  12. package/bin/feedback.js +14 -3
  13. package/bin/harness-manifest.js +286 -0
  14. package/bin/headless-args.js +35 -0
  15. package/bin/launch-values.js +25 -0
  16. package/bin/model-capabilities.js +10 -9
  17. package/bin/models.js +25 -13
  18. package/bin/native/subc-tui-darwin-amd64 +0 -0
  19. package/bin/native/subc-tui-darwin-arm64 +0 -0
  20. package/bin/native/subc-tui-linux-amd64 +0 -0
  21. package/bin/native/subc-tui-linux-arm64 +0 -0
  22. package/bin/native/subc-tui-windows-amd64.exe +0 -0
  23. package/bin/native/subc-tui-windows-arm64.exe +0 -0
  24. package/bin/opencode-provider.js +21 -0
  25. package/bin/profile-settings.js +129 -0
  26. package/bin/profiles.js +29 -424
  27. package/bin/runbook/README.md +77 -23
  28. package/bin/runbook/claude-code/agent.json +300 -0
  29. package/bin/runbook/claude-code/install.sh +9 -252
  30. package/bin/runbook/codex/agent.json +462 -0
  31. package/bin/runbook/codex/hooks-lib.sh +17 -4
  32. package/bin/runbook/codex/install.sh +0 -4
  33. package/bin/runbook/codex/run.sh +16 -235
  34. package/bin/runbook/copilot/agent.json +186 -0
  35. package/bin/runbook/copilot/install.sh +45 -63
  36. package/bin/runbook/cursor/agent.json +138 -0
  37. package/bin/runbook/cursor/install.sh +8 -35
  38. package/bin/runbook/deepseek-harness/agent.json +164 -0
  39. package/bin/runbook/deepseek-harness/run.sh +20 -59
  40. package/bin/runbook/image-window/index.ts +14 -0
  41. package/bin/runbook/image-window/window.d.ts +19 -0
  42. package/bin/runbook/image-window/window.js +140 -0
  43. package/bin/runbook/lib.sh +41 -0
  44. package/bin/runbook/opencode/agent.json +159 -0
  45. package/bin/runbook/opencode/install.sh +9 -183
  46. package/bin/runbook/opencode/opencode-plugin.d.ts +10 -3
  47. package/bin/runbook/opencode/subconscious-image-window.ts +24 -0
  48. package/bin/runbook/pi/agent.json +217 -0
  49. package/bin/runbook/pi/install.sh +18 -44
  50. package/bin/runbook/pi/run.sh +6 -18
  51. package/bin/runbook/pi/subconscious-compaction.ts +3 -1
  52. package/bin/runbook/shared.json +57 -0
  53. package/bin/sessions.js +53 -48
  54. package/bin/tui.js +13 -0
  55. package/bin/update-check.js +35 -1
  56. package/bin/upgrade.js +27 -34
  57. package/bin/usage.js +21 -10
  58. package/bin/windows/agents.js +28 -60
  59. package/bin/windows/common.js +2 -28
  60. package/bin/windows/launch.js +252 -231
  61. package/bin/windows/process.js +12 -1
  62. package/bin/windows/setup.js +29 -9
  63. package/package.json +2 -4
  64. package/bin/registry.generated.json +0 -406
  65. package/bin/runbook/claude-code/run.sh +0 -254
  66. package/bin/runbook/model-capabilities.generated.sh +0 -9
  67. package/bin/runbook/opencode/provider.sh +0 -93
  68. package/bin/runbook/opencode/run.sh +0 -99
  69. package/bin/runbook/opencode/subconscious-compaction.ts +0 -100
  70. package/bin/runbook/subconscious-code/install.sh +0 -154
  71. package/bin/runbook/subconscious-code/run.sh +0 -21
  72. package/bin/windows/claude.js +0 -100
  73. package/bin/windows/install.js +0 -188
package/bin/auth.js CHANGED
@@ -401,7 +401,7 @@ export async function whoamiCommand(_argv = [], options = {}) {
401
401
  }
402
402
  console.log(` ${c.dim}Key: ${masked}${c.reset}`);
403
403
  console.log(` ${c.dim}Source: ${source}${c.reset}`);
404
- } else {
404
+ } else if (res.status === 401) {
405
405
  console.log(` ${c.red}✗ Key is invalid or revoked${c.reset}`);
406
406
  console.log(` ${c.dim}Key: ${masked}${c.reset}`);
407
407
  console.log(` ${c.dim}Source: ${source}${c.reset}`);
@@ -409,6 +409,14 @@ export async function whoamiCommand(_argv = [], options = {}) {
409
409
  console.log(
410
410
  ` Run ${c.cyan}subc ${profileFlag}logout${c.reset} then ${c.cyan}subc ${profileFlag}login${c.reset} to re-authenticate.`,
411
411
  );
412
+ } else {
413
+ // Only a 401 means the key was rejected. A 5xx or 429 says nothing
414
+ // about the key, so don't tell the user to log out.
415
+ console.log(
416
+ ` ${c.yellow}Could not verify the key (platform returned HTTP ${res.status})${c.reset}`,
417
+ );
418
+ console.log(` ${c.dim}Key: ${masked}${c.reset}`);
419
+ console.log(` ${c.dim}Source: ${source}${c.reset}`);
412
420
  }
413
421
  } catch {
414
422
  console.log(
@@ -0,0 +1,143 @@
1
+ // The env claudeModelPickerEnv sets, described for readers that cannot run it.
2
+ export const CLAUDE_PICKER_ENV_DESCRIPTION = [
3
+ ...['OPUS', 'SONNET', 'HAIKU', 'FABLE'].flatMap((role, index) => {
4
+ const alias = role.toLowerCase();
5
+ return [
6
+ {
7
+ name: `ANTHROPIC_DEFAULT_${role}_MODEL`,
8
+ value: `{catalog[${index}]}`,
9
+ override: [`ANTHROPIC_DEFAULT_${role}_MODEL`],
10
+ description: `Model behind the ${alias} alias. Set from the catalog, padded to four entries; a profile or environment value is kept only when it is in the live catalog.`,
11
+ },
12
+ {
13
+ name: `ANTHROPIC_DEFAULT_${role}_MODEL_NAME`,
14
+ value: `{catalog[${index}]}`,
15
+ override: [`ANTHROPIC_DEFAULT_${role}_MODEL_NAME`],
16
+ description: `Picker label for the ${alias} alias.`,
17
+ },
18
+ {
19
+ name: `ANTHROPIC_DEFAULT_${role}_MODEL_DESCRIPTION`,
20
+ value: `Subconscious model {catalog[${index}]}`,
21
+ override: [`ANTHROPIC_DEFAULT_${role}_MODEL_DESCRIPTION`],
22
+ description: `Picker description for the ${alias} alias.`,
23
+ },
24
+ ];
25
+ }),
26
+ {
27
+ name: 'ANTHROPIC_CUSTOM_MODEL_OPTION',
28
+ value: '{catalog[4]}',
29
+ description:
30
+ 'Fifth picker entry, with _NAME and _DESCRIPTION. Only when the catalog has five or more models.',
31
+ },
32
+ {
33
+ name: 'SUBC_CLAUDE_SETTINGS',
34
+ value: '{claudeSettings}',
35
+ description: 'Picker settings JSON passed to --settings.',
36
+ },
37
+ ];
38
+
39
+ const CLAUDE_MODEL_PICKER_KEYS = [
40
+ 'ANTHROPIC_DEFAULT_OPUS_MODEL',
41
+ 'ANTHROPIC_DEFAULT_OPUS_MODEL_NAME',
42
+ 'ANTHROPIC_DEFAULT_OPUS_MODEL_DESCRIPTION',
43
+ 'ANTHROPIC_DEFAULT_SONNET_MODEL',
44
+ 'ANTHROPIC_DEFAULT_SONNET_MODEL_NAME',
45
+ 'ANTHROPIC_DEFAULT_SONNET_MODEL_DESCRIPTION',
46
+ 'ANTHROPIC_DEFAULT_HAIKU_MODEL',
47
+ 'ANTHROPIC_DEFAULT_HAIKU_MODEL_NAME',
48
+ 'ANTHROPIC_DEFAULT_HAIKU_MODEL_DESCRIPTION',
49
+ 'ANTHROPIC_DEFAULT_FABLE_MODEL',
50
+ 'ANTHROPIC_DEFAULT_FABLE_MODEL_NAME',
51
+ 'ANTHROPIC_DEFAULT_FABLE_MODEL_DESCRIPTION',
52
+ 'ANTHROPIC_CUSTOM_MODEL_OPTION',
53
+ 'ANTHROPIC_CUSTOM_MODEL_OPTION_NAME',
54
+ 'ANTHROPIC_CUSTOM_MODEL_OPTION_DESCRIPTION',
55
+ ];
56
+
57
+ const CLAUDE_MODEL_PICKER_ID_KEYS = [
58
+ 'ANTHROPIC_DEFAULT_OPUS_MODEL',
59
+ 'ANTHROPIC_DEFAULT_SONNET_MODEL',
60
+ 'ANTHROPIC_DEFAULT_HAIKU_MODEL',
61
+ 'ANTHROPIC_DEFAULT_FABLE_MODEL',
62
+ 'ANTHROPIC_CUSTOM_MODEL_OPTION',
63
+ ];
64
+
65
+ function uniqueCatalogModels(models, fallbackModel) {
66
+ const unique = [];
67
+ for (const model of models) {
68
+ if (model && !unique.includes(model)) unique.push(model);
69
+ }
70
+ if (unique.length === 0 && fallbackModel) unique.push(fallbackModel);
71
+ return unique;
72
+ }
73
+
74
+ export function claudePickerSettings(models, fallbackModel) {
75
+ const unique = uniqueCatalogModels(models, fallbackModel);
76
+ return {
77
+ availableModels: unique,
78
+ modelPicker: {
79
+ replaceBuiltInOptions: true,
80
+ options: unique.map((model) => ({
81
+ model,
82
+ label: model,
83
+ description: `Subconscious model ${model}`,
84
+ })),
85
+ },
86
+ };
87
+ }
88
+
89
+ export function claudeModelPickerEnv(model, models) {
90
+ // Only the live catalog belongs in Claude's picker. Packaged registry slots
91
+ // (Haiku → DeepSeek, etc.) must not be unioned back in — that advertised
92
+ // models the key cannot call.
93
+ //
94
+ // Pad through Fable so the built-in `fable` alias cannot resolve to Anthropic
95
+ // Fable 5. The /model menu itself is replaced via claudePickerSettings.
96
+ const pickerModels = uniqueCatalogModels(models, model);
97
+ while (pickerModels.length < 4)
98
+ pickerModels.push(pickerModels.at(-1) || model);
99
+
100
+ const roles = ['OPUS', 'SONNET', 'HAIKU', 'FABLE'];
101
+ const env = {};
102
+ for (let index = 0; index < roles.length; index++) {
103
+ const role = roles[index];
104
+ const model = pickerModels[index];
105
+ if (!model) continue;
106
+ env[`ANTHROPIC_DEFAULT_${role}_MODEL`] = model;
107
+ env[`ANTHROPIC_DEFAULT_${role}_MODEL_NAME`] = model;
108
+ env[`ANTHROPIC_DEFAULT_${role}_MODEL_DESCRIPTION`] =
109
+ `Subconscious model ${model}`;
110
+ }
111
+ const customModel = pickerModels[4];
112
+ if (customModel) {
113
+ env.ANTHROPIC_CUSTOM_MODEL_OPTION = customModel;
114
+ env.ANTHROPIC_CUSTOM_MODEL_OPTION_NAME = customModel;
115
+ env.ANTHROPIC_CUSTOM_MODEL_OPTION_DESCRIPTION = `Subconscious model ${customModel}`;
116
+ }
117
+ return Object.fromEntries(
118
+ CLAUDE_MODEL_PICKER_KEYS.map((key) => [key, env[key]]).filter(
119
+ ([, value]) => value,
120
+ ),
121
+ );
122
+ }
123
+
124
+ export function applyClaudePickerCatalog(env, picker, models) {
125
+ if (Object.keys(picker).length === 0) return env;
126
+ const allowed = new Set(models);
127
+
128
+ for (const key of CLAUDE_MODEL_PICKER_KEYS) {
129
+ if (!picker[key]) {
130
+ delete env[key];
131
+ }
132
+ }
133
+
134
+ for (const key of CLAUDE_MODEL_PICKER_ID_KEYS) {
135
+ if (!picker[key] || !env[key] || allowed.has(env[key])) continue;
136
+ env[key] = picker[key];
137
+ const nameKey = `${key}_NAME`;
138
+ const descriptionKey = `${key}_DESCRIPTION`;
139
+ if (picker[nameKey]) env[nameKey] = picker[nameKey];
140
+ if (picker[descriptionKey]) env[descriptionKey] = picker[descriptionKey];
141
+ }
142
+ return env;
143
+ }
package/bin/cli.js CHANGED
@@ -14,6 +14,7 @@ import {
14
14
  agentCommandName,
15
15
  agentList,
16
16
  isAgentHelpRequest,
17
+ isHeadlessRequest,
17
18
  parseAgentAction,
18
19
  resolveAgent,
19
20
  runAgent,
@@ -28,6 +29,11 @@ import {
28
29
  import { renderBanner } from './branding.js';
29
30
  import { c } from './colors.js';
30
31
  import { feedbackCommand, printFeedbackHelp } from './feedback.js';
32
+ import {
33
+ harnessManifestCommand,
34
+ printHarnessManifestHelp,
35
+ } from './harness-manifest.js';
36
+ import { headlessPromptIndex } from './headless-args.js';
31
37
  import { resolveModelCatalog } from './models.js';
32
38
  import {
33
39
  configCommand,
@@ -86,6 +92,7 @@ function printHelp() {
86
92
 
87
93
  ${c.bold}Coding agents${c.reset}
88
94
  ${agents}
95
+ ${c.cyan}harness-manifest${c.reset} Print how each agent is installed and launched (JSON)
89
96
 
90
97
  ${c.bold}Options${c.reset}
91
98
  ${c.dim}--model <id>${c.reset} Model to use (profile MODEL, or first live catalog model if UNSET)
@@ -111,6 +118,8 @@ ${agents}
111
118
  ${c.dim}$${c.reset} subc cursor install
112
119
  ${c.dim}$${c.reset} subc cursor uninstall
113
120
  ${c.dim}$${c.reset} subc pi install
121
+ ${c.dim}$${c.reset} subc harness-manifest --json
122
+ ${c.dim}$${c.reset} subc codex headless "fix the failing test"
114
123
  ${c.dim}$${c.reset} subc -p staging codex
115
124
 
116
125
  ${c.dim}Use subc <command> help for command-specific usage.${c.reset}
@@ -150,7 +159,8 @@ Usage:
150
159
  subc upgrade --latest
151
160
  subc upgrade help
152
161
 
153
- Upgrade subconscious-cli to the latest published version.
162
+ Upgrade subconscious-cli to the latest published version, the same way it
163
+ was installed: npm (or pnpm, yarn, bun), or the curl installer.
154
164
 
155
165
  subc upgrade Prompt "Do you want to upgrade?" then install @latest
156
166
  subc upgrade --latest Skip the prompt and install @latest
@@ -245,12 +255,45 @@ const authCommands = {
245
255
  whoami: whoamiCommand,
246
256
  };
247
257
 
258
+ /**
259
+ * Position of the headless prompt in the full argv, or -1. It is decided on
260
+ * the words left once profile flags are removed, which is what the agent
261
+ * launcher sees, so both agree on whether the run is headless.
262
+ */
263
+ function headlessPromptPosition(argv) {
264
+ const kept = [];
265
+ for (let i = 0; i < argv.length; i++) {
266
+ const arg = argv[i];
267
+ const afterCommand = kept.slice(1).map((index) => argv[index]);
268
+ if (
269
+ kept.length > 0 &&
270
+ headlessPromptIndex(afterCommand) === afterCommand.length
271
+ ) {
272
+ return i;
273
+ }
274
+ if (arg === '--') return -1;
275
+ if (arg === '--profile' || arg === '-p') {
276
+ i++;
277
+ continue;
278
+ }
279
+ if (arg.startsWith('--profile=')) continue;
280
+ kept.push(i);
281
+ }
282
+ return -1;
283
+ }
284
+
248
285
  function extractProfile(argv) {
249
286
  let profileName = process.env.SUBC_PROFILE?.trim() || DEFAULT_PROFILE;
250
287
  let profileExplicit = false;
251
288
  const args = [];
289
+ // The headless prompt is opaque, even when it looks like --profile.
290
+ const promptIndex = headlessPromptPosition(argv);
252
291
  for (let i = 0; i < argv.length; i++) {
253
292
  const arg = argv[i];
293
+ if (i === promptIndex) {
294
+ args.push(arg);
295
+ continue;
296
+ }
254
297
  if (arg === '--') {
255
298
  args.push(...argv.slice(i));
256
299
  break;
@@ -296,15 +339,32 @@ async function main() {
296
339
  return;
297
340
  }
298
341
 
342
+ // Machine-readable output: skip the update check so nothing else reaches
343
+ // stdout and no npm request is made.
344
+ if (command === 'harness-manifest') {
345
+ if (isHelpArg(args[1])) {
346
+ printHarnessManifestHelp();
347
+ return;
348
+ }
349
+ harnessManifestCommand(args.slice(1));
350
+ return;
351
+ }
352
+
299
353
  const launchTui =
300
354
  !command &&
301
355
  process.stdin.isTTY === true &&
302
356
  process.stdout.isTTY === true &&
303
357
  process.env.TERM !== 'dumb';
304
358
 
359
+ // Headless runs are scripted: no npm request and no update prompt.
360
+ const headlessAgent = resolveAgent(command);
361
+ const headless = Boolean(
362
+ headlessAgent && isHeadlessRequest(headlessAgent, args.slice(1)),
363
+ );
364
+
305
365
  // Explicit commands can wait on npm. The TUI opens immediately and prompts
306
366
  // only after a newer version is found.
307
- if (!launchTui) {
367
+ if (!launchTui && !headless) {
308
368
  const update = await showUpdateNotice();
309
369
  if (update?.action === 'updated' || update?.action === 'cancel') return;
310
370
  }
@@ -58,7 +58,9 @@ async function readDarwinClipboard() {
58
58
  async function readLinuxClipboard() {
59
59
  const commands = [
60
60
  ['wl-paste', ['--type', 'image/png']],
61
+ ['wl-paste', ['--type', 'image/jpeg']],
61
62
  ['xclip', ['-selection', 'clipboard', '-t', 'image/png', '-o']],
63
+ ['xclip', ['-selection', 'clipboard', '-t', 'image/jpeg', '-o']],
62
64
  ];
63
65
  for (const [command, args] of commands) {
64
66
  try {
package/bin/feedback.js CHANGED
@@ -17,6 +17,17 @@ import { DEFAULT_PROFILE } from './profiles.js';
17
17
  export const FEEDBACK_API_PATH = '/api/cli/feedback';
18
18
  export const SUPPORT_EMAIL = 'support@subconscious.dev';
19
19
 
20
+ function feedbackOptionValue(argv, index) {
21
+ const value = argv[index + 1];
22
+ if (
23
+ value === undefined ||
24
+ /^(?:--[A-Za-z][\w-]*|-[A-Za-z])(?:=|$)/.test(value)
25
+ ) {
26
+ throw new Error(`${argv[index]} requires a value`);
27
+ }
28
+ return value;
29
+ }
30
+
20
31
  /**
21
32
  * Parses feedback command arguments. Shared with tests.
22
33
  *
@@ -31,15 +42,15 @@ export function parseFeedbackArgs(argv = []) {
31
42
  for (let i = 0; i < argv.length; i++) {
32
43
  const arg = argv[i];
33
44
  if (arg === '-s' || arg === '--subject') {
34
- subject = argv[++i] ?? '';
45
+ subject = feedbackOptionValue(argv, i++);
35
46
  } else if (arg.startsWith('--subject=')) {
36
47
  subject = arg.slice('--subject='.length);
37
48
  } else if (arg === '-m' || arg === '--message') {
38
- message = argv[++i] ?? '';
49
+ message = feedbackOptionValue(argv, i++);
39
50
  } else if (arg.startsWith('--message=')) {
40
51
  message = arg.slice('--message='.length);
41
52
  } else if (arg === '--image') {
42
- images.push(argv[++i] ?? '');
53
+ images.push(feedbackOptionValue(argv, i++));
43
54
  } else if (arg.startsWith('--image=')) {
44
55
  images.push(arg.slice('--image='.length));
45
56
  } else if (arg !== '') {
@@ -0,0 +1,286 @@
1
+ import { readFileSync } from 'node:fs';
2
+ import path from 'node:path';
3
+ import {
4
+ AGENTS,
5
+ agentBinary,
6
+ agentCommandName,
7
+ agentInputs,
8
+ installCommands,
9
+ RUNBOOK_DIR,
10
+ resolveRefs,
11
+ staticLookup,
12
+ } from './agent-data.js';
13
+ import { RUNBOOK_ENV_DESCRIPTION } from './agents.js';
14
+ import { CLAUDE_PICKER_ENV_DESCRIPTION } from './claude-picker.js';
15
+ import { c } from './colors.js';
16
+ import { buildOpenCodeConfig } from './opencode-provider.js';
17
+
18
+ // Bump on any change that can break a reader: a renamed or removed field, or a
19
+ // changed meaning. Adding a field does not need a bump.
20
+ export const HARNESS_MANIFEST_SCHEMA_VERSION = 1;
21
+
22
+ export const MANIFEST_TOKENS = {
23
+ '{baseUrl}': 'Gateway origin, without a trailing slash.',
24
+ '{baseUrlV1}': 'Gateway origin followed by /v1.',
25
+ '{apiKey}': "The agent's API key input when set, otherwise API_KEY.",
26
+ '{model}': 'Launch model ID.',
27
+ '${NAME}':
28
+ 'Value of env var NAME at launch. The manifest replaces it with the default when NAME is an input or env entry; a reference followed by / drops trailing slashes.',
29
+ '{catalog}': 'Every live catalog model ID, newline-separated.',
30
+ '{catalog[N]}':
31
+ 'Catalog model at index N, clamped to the last entry; {model} when the catalog is empty.',
32
+ '{args}': 'Arguments passed through from subc, after its own flags.',
33
+ '{config}':
34
+ 'config_flag NAME=VALUE for each cli-config entry whose condition holds.',
35
+ '{prompt}': 'The task given to subc <agent> headless PROMPT.',
36
+ '{sessionId}': 'A local session ID, for launch.resume.',
37
+ '{sessionFile}': "A local session's file, or its ID when it has none.",
38
+ '{tempFile}':
39
+ 'Temporary file the runbook writes; the matching config entry says whether it is removed.',
40
+ '{tmp}': 'System temporary directory.',
41
+ '{opencodeConfig}':
42
+ 'The OpenCode config document shown in the env-json config entry.',
43
+ '{claudeSettings}': 'JSON document described by the --settings config entry.',
44
+ '{target}':
45
+ 'Release target triple, one of install.targets or install.windows_targets.',
46
+ '{binDir}': "Directory the agent's binary was found in.",
47
+ '{PATH}': "The caller's PATH.",
48
+ '<install dirs>': 'Common install directories subc adds to PATH.',
49
+ '<agent key>': 'The agent-specific API key input, for example CODEX_API_KEY.',
50
+ '<id>': 'Any model ID in the catalog; the field repeats for each model.',
51
+ 'models[]': 'Every element of the models array.',
52
+ '<runbook>': 'Absolute path of the installed bin/runbook directory.',
53
+ '<VS Code user dir>':
54
+ 'The VS Code user settings directory, for example ~/Library/Application Support/Code/User.',
55
+ };
56
+
57
+ const PACKAGE_URL = new URL('../package.json', import.meta.url);
58
+
59
+ function conditionText(when) {
60
+ return Object.entries(when)
61
+ .map(([name, test]) =>
62
+ test === 'set'
63
+ ? `${name} non-empty`
64
+ : test === 'empty'
65
+ ? `${name} empty`
66
+ : `${name} != ${test.not}`,
67
+ )
68
+ .join(' and ');
69
+ }
70
+
71
+ /** The OpenCode config with placeholders, built by the launch code itself. */
72
+ function openCodeConfigDocument(lookup) {
73
+ const document = buildOpenCodeConfig({
74
+ baseUrl: '{baseUrl}',
75
+ model: '{model}',
76
+ modelIds: ['<id>'],
77
+ context: lookup('OPENCODE_CONTEXT_LIMIT'),
78
+ output: lookup('OPENCODE_OUTPUT_LIMIT'),
79
+ });
80
+ // The manifest is the same on every platform, so a Windows path keeps `/`.
81
+ const portable = (file) =>
82
+ file.startsWith(RUNBOOK_DIR)
83
+ ? `<runbook>/${path.relative(RUNBOOK_DIR, file).split(path.sep).join('/')}`
84
+ : file;
85
+ return { ...document, plugin: document.plugin.map(portable) };
86
+ }
87
+
88
+ function resolver(agent) {
89
+ const lookup = staticLookup(agent);
90
+ return (value) =>
91
+ value === '{opencodeConfig}'
92
+ ? openCodeConfigDocument(lookup)
93
+ : resolveRefs(value, lookup);
94
+ }
95
+
96
+ function inputEntry(input, resolve) {
97
+ return input.default === undefined
98
+ ? input
99
+ : { ...input, default: resolve(input.default) };
100
+ }
101
+
102
+ function exportedEnv(input, resolve) {
103
+ return {
104
+ name: input.name,
105
+ value: String(resolve(input.default)),
106
+ override: [input.name, ...(input.aliases || [])],
107
+ description: input.description,
108
+ };
109
+ }
110
+
111
+ function envEntries(agent, resolve) {
112
+ const inputs = agentInputs(agent).filter((input) => input.export);
113
+ const own = agent.env.map((entry) => ({
114
+ ...entry,
115
+ value:
116
+ entry.value === '{opencodeConfig}'
117
+ ? entry.value
118
+ : String(resolve(entry.value)),
119
+ override: entry.override || [],
120
+ }));
121
+ const launcher =
122
+ agent.id === 'claude-code' ? CLAUDE_PICKER_ENV_DESCRIPTION : [];
123
+ return [
124
+ ...inputs.map((input) => exportedEnv(input, resolve)),
125
+ ...own,
126
+ ...launcher,
127
+ ];
128
+ }
129
+
130
+ function inputBehindRef(agent, value) {
131
+ const name = /^\$\{([A-Z_][A-Z0-9_]*)\}$/.exec(value ?? '')?.[1];
132
+ return name && agentInputs(agent).find((input) => input.name === name);
133
+ }
134
+
135
+ /** Where a value that reads an input can be changed: its env names and flag. */
136
+ function overrideFor(input) {
137
+ return [
138
+ input.name,
139
+ ...(input.aliases || []),
140
+ ...(input.flag ? [input.flag] : []),
141
+ ];
142
+ }
143
+
144
+ function configEntry(agent, entry, resolve) {
145
+ const { when, ...rest } = entry;
146
+ const out = { ...rest };
147
+ if ('value' in entry) out.value = resolve(entry.value);
148
+ const input = inputBehindRef(agent, entry.value);
149
+ if (input) out.override = overrideFor(input);
150
+ if (when) out.condition = conditionText(when);
151
+ return out;
152
+ }
153
+
154
+ /** A knob that reads an input says where the value comes from and its bounds. */
155
+ function knob(agent, setting, resolve) {
156
+ const input = inputBehindRef(agent, setting.value);
157
+ const value = resolve(setting.value);
158
+ const out = {
159
+ ...setting,
160
+ value: setting.kind === 'env' ? String(value) : value,
161
+ };
162
+ if (input && !setting.override) out.override = overrideFor(input);
163
+ if (input && setting.range)
164
+ out.range = { min: input.min, max: input.max, ...setting.range };
165
+ return out;
166
+ }
167
+
168
+ function capabilities(agent, resolve) {
169
+ const { compaction, ...rest } = agent.capabilities;
170
+ const knobs = Object.fromEntries(
171
+ ['on', 'off', 'threshold'].map((mode) => [
172
+ mode,
173
+ knob(agent, compaction[mode], resolve),
174
+ ]),
175
+ );
176
+ return { ...resolve(rest), compaction: { ...compaction, ...knobs } };
177
+ }
178
+
179
+ function harnessEntry(agent) {
180
+ const resolve = resolver(agent);
181
+ const { runbook } = agent;
182
+ const script = (name) => (name ? `${agent.id}/${name}` : null);
183
+ return {
184
+ id: agent.id,
185
+ name: agent.name,
186
+ command: `subc ${agentCommandName(agent)}`,
187
+ aliases: agent.aliases,
188
+ mode: runbook.mode,
189
+ protocol: agent.protocol,
190
+ runbook: {
191
+ dir: agent.id,
192
+ launch: script(runbook.script),
193
+ setup: script(runbook.setup_script),
194
+ setup_actions: runbook.setup_actions || [],
195
+ },
196
+ binary: agentBinary(agent),
197
+ install: { ...agent.install, commands: installCommands(agent) },
198
+ prerequisites: agent.prerequisites,
199
+ launch: agent.launch ?? null,
200
+ inputs: agentInputs(agent).map((input) => inputEntry(input, resolve)),
201
+ env: envEntries(agent, resolve),
202
+ config: agent.config.map((entry) => configEntry(agent, entry, resolve)),
203
+ capabilities: capabilities(agent, resolve),
204
+ };
205
+ }
206
+
207
+ /** The manifest, computed from the agent files this subc ships. */
208
+ export function loadHarnessManifest() {
209
+ return {
210
+ schema_version: HARNESS_MANIFEST_SCHEMA_VERSION,
211
+ cli_version: JSON.parse(readFileSync(PACKAGE_URL, 'utf-8')).version,
212
+ cli_package: 'subconscious-cli',
213
+ tokens: MANIFEST_TOKENS,
214
+ runbook_env: RUNBOOK_ENV_DESCRIPTION,
215
+ harnesses: Object.fromEntries(
216
+ AGENTS.map((agent) => [agent.id, harnessEntry(agent)]),
217
+ ),
218
+ };
219
+ }
220
+
221
+ export function printHarnessManifestHelp() {
222
+ console.log(`
223
+ Usage:
224
+ subc harness-manifest [--json]
225
+ subc harness-manifest help
226
+
227
+ Print how this subc version installs, configures, and launches each coding
228
+ harness: install source, binary, env vars, config files, compaction knobs,
229
+ MCP, headers, and hooks.
230
+
231
+ --json Print the full manifest as JSON. This is the default when stdout
232
+ is not a terminal; in a terminal, a summary table is printed.
233
+ `);
234
+ }
235
+
236
+ function compactionSummary(compaction) {
237
+ const supported = ['on', 'off', 'threshold'].filter(
238
+ (mode) => compaction[mode]?.set_by_subc,
239
+ );
240
+ return supported.length ? supported.join(',') : '-';
241
+ }
242
+
243
+ function tableRows(manifest) {
244
+ return Object.values(manifest.harnesses).map((harness) => [
245
+ harness.id,
246
+ harness.binary || '-',
247
+ harness.install.package || harness.install.repository || '-',
248
+ String(harness.env.length),
249
+ compactionSummary(harness.capabilities.compaction),
250
+ String(harness.capabilities.hooks.length),
251
+ ]);
252
+ }
253
+
254
+ function printTable(manifest) {
255
+ const header = ['harness', 'binary', 'install', 'env', 'compaction', 'hooks'];
256
+ const rows = tableRows(manifest);
257
+ const widths = header.map((title, index) =>
258
+ Math.max(title.length, ...rows.map((row) => row[index].length)),
259
+ );
260
+ const line = (cells) =>
261
+ ` ${cells.map((cell, index) => cell.padEnd(widths[index])).join(' ')}`;
262
+ console.log(
263
+ `\n ${c.bold}Harness manifest${c.reset} ${c.dim}schema ${manifest.schema_version}, subc ${manifest.cli_version}${c.reset}\n`,
264
+ );
265
+ console.log(`${c.dim}${line(header)}${c.reset}`);
266
+ for (const row of rows) console.log(line(row));
267
+ console.log(
268
+ `\n ${c.dim}compaction lists the modes subc sets. Use --json for every field.${c.reset}\n`,
269
+ );
270
+ }
271
+
272
+ export function harnessManifestCommand(args = [], options = {}) {
273
+ const unknown = args.filter((arg) => arg !== '--json');
274
+ if (unknown.length) {
275
+ throw new Error(
276
+ `Unknown argument: ${unknown[0]}. Try subc harness-manifest help.`,
277
+ );
278
+ }
279
+ const isTTY = options.isTTY ?? process.stdout.isTTY === true;
280
+ const manifest = loadHarnessManifest();
281
+ if (args.includes('--json') || !isTTY) {
282
+ process.stdout.write(`${JSON.stringify(manifest, null, 2)}\n`);
283
+ return;
284
+ }
285
+ printTable(manifest);
286
+ }
@@ -0,0 +1,35 @@
1
+ const SUBC_PROFILE_FLAGS = new Set(['-p', '--profile']);
2
+ const SUBC_INLINE_FLAGS = ['--model=', '--profile='];
3
+
4
+ /** Whether `--model` consumes the word after it. An empty word is a value. */
5
+ export function takesModelValue(value) {
6
+ return value !== undefined && !value.startsWith('-');
7
+ }
8
+
9
+ /**
10
+ * Index of the headless prompt when `argv` (the words after the agent name)
11
+ * asks for a headless run, otherwise -1. Only subc's own flags may come
12
+ * before "headless"; later on it is an ordinary agent argument.
13
+ */
14
+ export function headlessPromptIndex(argv) {
15
+ for (let i = 0; i < argv.length; i++) {
16
+ const arg = argv[i];
17
+ if (SUBC_PROFILE_FLAGS.has(arg)) {
18
+ i++;
19
+ continue;
20
+ }
21
+ if (arg === '--model') {
22
+ if (takesModelValue(argv[i + 1])) i++;
23
+ continue;
24
+ }
25
+ if (SUBC_INLINE_FLAGS.some((prefix) => arg.startsWith(prefix))) continue;
26
+ return arg === 'headless' ? i + 1 : -1;
27
+ }
28
+ return -1;
29
+ }
30
+
31
+ /** Index of the first "--" separator; a headless prompt of "--" is not one. */
32
+ export function separatorIndex(argv) {
33
+ const prompt = headlessPromptIndex(argv);
34
+ return argv.findIndex((arg, index) => arg === '--' && index !== prompt);
35
+ }
@@ -0,0 +1,25 @@
1
+ import { DEFAULTS, PACKAGED_MODELS } from './agent-data.js';
2
+
3
+ const MODEL_ID = /^[-A-Za-z0-9._:/+]+$/;
4
+
5
+ /** The launch model first, then the catalog, deduplicated and validated. */
6
+ export function modelIds(env) {
7
+ const catalog = (
8
+ env.SUBCONSCIOUS_MODELS?.trim() || PACKAGED_MODELS.join('\n')
9
+ ).split(/\r?\n/);
10
+ const models = [
11
+ ...new Set([env.MODEL?.trim() || DEFAULTS.model, ...catalog]),
12
+ ].filter(Boolean);
13
+ for (const model of models)
14
+ if (!MODEL_ID.test(model)) throw new Error(`Invalid model id: ${model}`);
15
+ return models;
16
+ }
17
+
18
+ export function positiveInteger(input, name) {
19
+ if (
20
+ !/^[1-9][0-9]*$/.test(String(input)) ||
21
+ !Number.isSafeInteger(Number(input))
22
+ )
23
+ throw new Error(`${name} must be a positive integer`);
24
+ return Number(input);
25
+ }