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/bin/auth.js CHANGED
@@ -12,16 +12,23 @@
12
12
  */
13
13
 
14
14
  import { exec } from 'node:child_process';
15
- import { openWindowsBrowser } from './windows/process.js';
16
15
  import fs from 'node:fs/promises';
17
16
  import os from 'node:os';
18
17
  import path from 'node:path';
19
18
  import { c } from './colors.js';
20
- import { clearProfileApiKey, DEFAULT_PROFILE, ensureProfile } from './profiles.js';
19
+ import {
20
+ clearProfileApiKey,
21
+ DEFAULT_PROFILE,
22
+ ensureProfile,
23
+ } from './profiles.js';
21
24
  import { printLoginUpgradeWarning } from './upgrade.js';
25
+ import { openWindowsBrowser } from './windows/process.js';
22
26
 
23
27
  function configDir() {
24
- return process.env.SUBC_CONFIG_DIR?.trim() || path.join(os.homedir(), '.subconscious');
28
+ return (
29
+ process.env.SUBC_CONFIG_DIR?.trim() ||
30
+ path.join(os.homedir(), '.subconscious')
31
+ );
25
32
  }
26
33
 
27
34
  function configFile() {
@@ -78,7 +85,10 @@ export async function getApiKey(profile) {
78
85
 
79
86
  const config = await loadConfig();
80
87
  if (config.subconscious_api_key) {
81
- return { key: config.subconscious_api_key, source: '~/.subconscious/config.json' };
88
+ return {
89
+ key: config.subconscious_api_key,
90
+ source: '~/.subconscious/config.json',
91
+ };
82
92
  }
83
93
  return null;
84
94
  }
@@ -87,9 +97,11 @@ export async function getApiKey(profile) {
87
97
 
88
98
  function openBrowser(url) {
89
99
  if (process.platform === 'win32') {
90
- void openWindowsBrowser(url).then(code => {
91
- if (code) throw new Error('Browser could not be opened');
92
- }).catch(() => console.log(`Please open this URL manually:\n\n ${url}\n`));
100
+ void openWindowsBrowser(url)
101
+ .then((code) => {
102
+ if (code) throw new Error('Browser could not be opened');
103
+ })
104
+ .catch(() => console.log(`Please open this URL manually:\n\n ${url}\n`));
93
105
  return;
94
106
  }
95
107
  const cmd =
@@ -131,7 +143,11 @@ export async function registerDeviceLogin(platformUrl, fetchImpl = fetch) {
131
143
  return data;
132
144
  }
133
145
 
134
- export async function pollDeviceLogin(platformUrl, deviceCode, fetchImpl = fetch) {
146
+ export async function pollDeviceLogin(
147
+ platformUrl,
148
+ deviceCode,
149
+ fetchImpl = fetch,
150
+ ) {
135
151
  const res = await fetchImpl(`${platformUrl}/api/cli/device/token`, {
136
152
  method: 'POST',
137
153
  headers: { 'Content-Type': 'application/json' },
@@ -151,7 +167,9 @@ function printDeviceInstructions(platformUrl, userCode) {
151
167
  console.log(` ${c.dim}Opening browser to sign in...${c.reset}`);
152
168
  console.log();
153
169
  console.log(` ${c.dim}If a window doesn't open, go to:${c.reset}`);
154
- console.log(` ${c.underline}${c.cyan}${terminalLink(verificationUrl)}${c.reset}`);
170
+ console.log(
171
+ ` ${c.underline}${c.cyan}${terminalLink(verificationUrl)}${c.reset}`,
172
+ );
155
173
  console.log();
156
174
  console.log(
157
175
  ` ${c.dim}Or open ${c.reset}${c.underline}${c.cyan}${terminalLink(deviceUrl)}${c.reset}${c.dim} and enter:${c.reset} ${c.bold}${userCode}${c.reset}`,
@@ -161,7 +179,9 @@ function printDeviceInstructions(platformUrl, userCode) {
161
179
  }
162
180
 
163
181
  function printLoginFallback() {
164
- console.error(` ${c.dim}You can also copy an API key from the dashboard and run${c.reset} ${c.cyan}subc update-key <your-api-key>${c.reset}${c.dim}.${c.reset}`);
182
+ console.error(
183
+ ` ${c.dim}You can also copy an API key from the dashboard and run${c.reset} ${c.cyan}subc update-key <your-api-key>${c.reset}${c.dim}.${c.reset}`,
184
+ );
165
185
  }
166
186
 
167
187
  async function saveLoginKey(profileName, token) {
@@ -181,17 +201,23 @@ export async function loginCommand(_argv = [], options = {}) {
181
201
  if (existing) {
182
202
  const profile = await ensureProfile(profileName, existing.key);
183
203
  const logout =
184
- profileName === DEFAULT_PROFILE ? 'subc logout' : `subc --profile ${profileName} logout`;
185
- const masked = existing.key.slice(0, 8) + '...' + existing.key.slice(-4);
204
+ profileName === DEFAULT_PROFILE
205
+ ? 'subc logout'
206
+ : `subc --profile ${profileName} logout`;
207
+ const masked = `${existing.key.slice(0, 8)}...${existing.key.slice(-4)}`;
186
208
  console.log(`\n${c.yellow}Already logged in.${c.reset}`);
187
209
  console.log(` Key: ${c.dim}${masked}${c.reset}`);
188
210
  console.log(` Profile: ${c.dim}${profile.path}${c.reset}`);
189
- console.log(`\n Run ${c.cyan}${logout}${c.reset} first to switch accounts.\n`);
211
+ console.log(
212
+ `\n Run ${c.cyan}${logout}${c.reset} first to switch accounts.\n`,
213
+ );
190
214
  return;
191
215
  }
192
216
 
193
217
  console.log();
194
- console.log(` ${c.magenta}${c.bold}Subconscious${c.reset} ${c.dim}— CLI Login${c.reset}`);
218
+ console.log(
219
+ ` ${c.magenta}${c.bold}Subconscious${c.reset} ${c.dim}— CLI Login${c.reset}`,
220
+ );
195
221
  console.log();
196
222
 
197
223
  const platformUrl = getPlatformUrl(options.profile);
@@ -206,7 +232,10 @@ export async function loginCommand(_argv = [], options = {}) {
206
232
  return;
207
233
  }
208
234
 
209
- const verificationUrl = printDeviceInstructions(platformUrl, registered.user_code);
235
+ const verificationUrl = printDeviceInstructions(
236
+ platformUrl,
237
+ registered.user_code,
238
+ );
210
239
  (options.openBrowser || openBrowser)(verificationUrl);
211
240
 
212
241
  const frames = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
@@ -229,35 +258,47 @@ export async function loginCommand(_argv = [], options = {}) {
229
258
  if (Date.now() - started > DEVICE_POLL_TIMEOUT_MS) {
230
259
  throw new Error('Authentication timed out (5 min). Please try again.');
231
260
  }
232
- const polled = await pollDeviceLogin(platformUrl, registered.device_code, fetchImpl);
261
+ const polled = await pollDeviceLogin(
262
+ platformUrl,
263
+ registered.device_code,
264
+ fetchImpl,
265
+ );
233
266
  if (polled.status === 'approved') {
234
267
  clearInterval(spinner);
235
- process.stdout.write('\r' + ' '.repeat(50) + '\r');
268
+ process.stdout.write(`\r${' '.repeat(50)}\r`);
236
269
  const profile = await saveLoginKey(profileName, polled.key);
237
- const masked = polled.key.slice(0, 8) + '...' + polled.key.slice(-4);
270
+ const masked = `${polled.key.slice(0, 8)}...${polled.key.slice(-4)}`;
238
271
  console.log(` ${c.green}${c.bold}✓ Logged in successfully!${c.reset}`);
239
272
  console.log(` ${c.dim}Key: ${masked}${c.reset}`);
240
273
  if (profileName === DEFAULT_PROFILE) {
241
- console.log(` ${c.dim}Saved to ~/.subconscious/config.json${c.reset}`);
274
+ console.log(
275
+ ` ${c.dim}Saved to ~/.subconscious/config.json${c.reset}`,
276
+ );
242
277
  }
243
278
  console.log(` ${c.dim}Runbook profile: ${profile.path}${c.reset}`);
244
- console.log(` ${c.dim}Launch a terminal agent with ${c.reset}${c.cyan}subc claude${c.reset}${c.dim}, or install editor hooks with ${c.reset}${c.cyan}subc cursor install${c.reset}${c.dim}.${c.reset}`);
245
- console.log(` ${c.dim}Pi needs ${c.reset}${c.cyan}subc pi install${c.reset}${c.dim} first. List profiles with ${c.reset}${c.cyan}subc config${c.reset}${c.dim}.${c.reset}`);
279
+ console.log(
280
+ ` ${c.dim}Launch a terminal agent with ${c.reset}${c.cyan}subc claude${c.reset}${c.dim}, or install editor hooks with ${c.reset}${c.cyan}subc cursor install${c.reset}${c.dim}.${c.reset}`,
281
+ );
282
+ console.log(
283
+ ` ${c.dim}Pi needs ${c.reset}${c.cyan}subc pi install${c.reset}${c.dim} first. List profiles with ${c.reset}${c.cyan}subc config${c.reset}${c.dim}.${c.reset}`,
284
+ );
246
285
  console.log();
247
286
  return;
248
287
  }
249
288
  if (polled.status === 'expired') {
250
289
  throw new Error('Login code expired. Please try again.');
251
290
  }
252
- await new Promise((resolve) => setTimeout(resolve, DEVICE_POLL_INTERVAL_MS));
291
+ await new Promise((resolve) =>
292
+ setTimeout(resolve, DEVICE_POLL_INTERVAL_MS),
293
+ );
253
294
  }
254
295
  clearInterval(spinner);
255
- process.stdout.write('\r' + ' '.repeat(50) + '\r');
296
+ process.stdout.write(`\r${' '.repeat(50)}\r`);
256
297
  console.log(`\n ${c.dim}Login cancelled.${c.reset}\n`);
257
298
  process.exitCode = 1;
258
299
  } catch (error) {
259
300
  clearInterval(spinner);
260
- process.stdout.write('\r' + ' '.repeat(50) + '\r');
301
+ process.stdout.write(`\r${' '.repeat(50)}\r`);
261
302
  console.error(` ${c.red}✗ ${error.message}${c.reset}`);
262
303
  printLoginFallback();
263
304
  console.log();
@@ -282,7 +323,8 @@ export async function updateApiKeyCommand(argv = [], options = {}) {
282
323
  await saveConfig(config);
283
324
  }
284
325
 
285
- const masked = key.length <= 12 ? '********' : `${key.slice(0, 8)}...${key.slice(-4)}`;
326
+ const masked =
327
+ key.length <= 12 ? '********' : `${key.slice(0, 8)}...${key.slice(-4)}`;
286
328
  console.log(`\n ${c.green}${c.bold}✓ API key updated.${c.reset}`);
287
329
  console.log(` ${c.dim}Profile: ${profile.path}${c.reset}`);
288
330
  console.log(` ${c.dim}Key: ${masked}${c.reset}`);
@@ -298,7 +340,8 @@ export async function logoutCommand(_argv = [], options = {}) {
298
340
  const profileName = options.profileName || DEFAULT_PROFILE;
299
341
  const config = await loadConfig();
300
342
  const clearedProfile = await clearProfileApiKey(profileName);
301
- const clearSavedConfig = profileName === DEFAULT_PROFILE && config.subconscious_api_key;
343
+ const clearSavedConfig =
344
+ profileName === DEFAULT_PROFILE && config.subconscious_api_key;
302
345
 
303
346
  if (!clearSavedConfig && !clearedProfile) {
304
347
  console.log(`\n ${c.dim}Not logged in.${c.reset}\n`);
@@ -331,16 +374,19 @@ export async function whoamiCommand(_argv = [], options = {}) {
331
374
  }
332
375
 
333
376
  const { key, source } = auth;
334
- const masked = key.slice(0, 8) + '...' + key.slice(-4);
377
+ const masked = `${key.slice(0, 8)}...${key.slice(-4)}`;
335
378
 
336
379
  console.log();
337
380
 
338
381
  // Validate the key against the server; falls back to offline display if unreachable
339
382
  try {
340
- const res = await fetch(`${getPlatformUrl(options.profile)}/api/cli/whoami`, {
341
- headers: { Authorization: `Bearer ${key}` },
342
- signal: AbortSignal.timeout(5000),
343
- });
383
+ const res = await fetch(
384
+ `${getPlatformUrl(options.profile)}/api/cli/whoami`,
385
+ {
386
+ headers: { Authorization: `Bearer ${key}` },
387
+ signal: AbortSignal.timeout(5000),
388
+ },
389
+ );
344
390
 
345
391
  if (res.status === 404) {
346
392
  printLoginUpgradeWarning();
@@ -355,7 +401,7 @@ export async function whoamiCommand(_argv = [], options = {}) {
355
401
  }
356
402
  console.log(` ${c.dim}Key: ${masked}${c.reset}`);
357
403
  console.log(` ${c.dim}Source: ${source}${c.reset}`);
358
- } else {
404
+ } else if (res.status === 401) {
359
405
  console.log(` ${c.red}✗ Key is invalid or revoked${c.reset}`);
360
406
  console.log(` ${c.dim}Key: ${masked}${c.reset}`);
361
407
  console.log(` ${c.dim}Source: ${source}${c.reset}`);
@@ -363,9 +409,19 @@ export async function whoamiCommand(_argv = [], options = {}) {
363
409
  console.log(
364
410
  ` Run ${c.cyan}subc ${profileFlag}logout${c.reset} then ${c.cyan}subc ${profileFlag}login${c.reset} to re-authenticate.`,
365
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}`);
366
420
  }
367
421
  } catch {
368
- console.log(` ${c.green}✓ Authenticated${c.reset} ${c.dim}(offline — key not verified)${c.reset}`);
422
+ console.log(
423
+ ` ${c.green}✓ Authenticated${c.reset} ${c.dim}(offline — key not verified)${c.reset}`,
424
+ );
369
425
  console.log(` ${c.dim}Key: ${masked}${c.reset}`);
370
426
  console.log(` ${c.dim}Source: ${source}${c.reset}`);
371
427
  }
package/bin/branding.js CHANGED
@@ -28,6 +28,8 @@ export function renderBanner(options = {}) {
28
28
  // still get the same logo without ANSI styling.
29
29
  if (!isTTY || term === 'dumb') return ` ${title}`;
30
30
 
31
- const logo = color ? `${BRAND_ORANGE}${LOGO_ART_SMALL}${RESET}` : LOGO_ART_SMALL;
31
+ const logo = color
32
+ ? `${BRAND_ORANGE}${LOGO_ART_SMALL}${RESET}`
33
+ : LOGO_ART_SMALL;
32
34
  return `${logo}\n\n ${title}`;
33
35
  }
@@ -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
@@ -10,43 +10,49 @@
10
10
  */
11
11
 
12
12
  import fs from 'node:fs/promises';
13
- import { c } from './colors.js';
14
- import { renderBanner } from './branding.js';
15
13
  import {
14
+ agentCommandName,
15
+ agentList,
16
+ isAgentHelpRequest,
17
+ isHeadlessRequest,
18
+ parseAgentAction,
19
+ resolveAgent,
20
+ runAgent,
21
+ } from './agents.js';
22
+ import {
23
+ getApiKey,
16
24
  loginCommand,
17
25
  logoutCommand,
18
- getApiKey,
19
26
  updateApiKeyCommand,
20
27
  whoamiCommand,
21
28
  } from './auth.js';
22
- import { upgradeCommand } from './upgrade.js';
29
+ import { renderBanner } from './branding.js';
30
+ import { c } from './colors.js';
31
+ import { feedbackCommand, printFeedbackHelp } from './feedback.js';
23
32
  import {
24
- resolveAgent,
25
- runAgent,
26
- agentList,
27
- agentCommandName,
28
- isAgentHelpRequest,
29
- parseAgentAction,
30
- } from './agents.js';
33
+ harnessManifestCommand,
34
+ printHarnessManifestHelp,
35
+ } from './harness-manifest.js';
36
+ import { headlessPromptIndex } from './headless-args.js';
37
+ import { resolveModelCatalog } from './models.js';
31
38
  import {
32
39
  configCommand,
33
40
  DEFAULT_PROFILE,
34
41
  loadProfile,
35
42
  modelsCommand,
43
+ SUPPORTED_MODELS as PACKAGED_MODELS,
36
44
  printConfigHelp,
37
- resolvedModelSetting,
38
45
  RUNBOOK_DEFAULTS,
39
- SUPPORTED_MODELS as PACKAGED_MODELS,
46
+ resolvedModelSetting,
40
47
  updatePlatformUrlCommand,
41
48
  updateUrlCommand,
42
49
  validateProfileName,
43
50
  } from './profiles.js';
44
- import { resolveModelCatalog } from './models.js';
45
- import { showUpdateNotice } from './update-check.js';
46
- import { runTui } from './tui.js';
47
51
  import { sessionsCommand } from './sessions.js';
52
+ import { runTui } from './tui.js';
53
+ import { showUpdateNotice } from './update-check.js';
54
+ import { upgradeCommand } from './upgrade.js';
48
55
  import { printUsageHelp, usageCommand } from './usage.js';
49
- import { feedbackCommand, printFeedbackHelp } from './feedback.js';
50
56
 
51
57
  function isHelpArg(arg) {
52
58
  return arg === 'help' || arg === '-h' || arg === '--help';
@@ -54,7 +60,10 @@ function isHelpArg(arg) {
54
60
 
55
61
  function printHelp() {
56
62
  const agents = agentList()
57
- .map(({ name, alias, action }) => ` ${c.cyan}${alias.padEnd(13)}${c.reset}${c.dim}${action} ${name}${c.reset}`)
63
+ .map(
64
+ ({ name, alias, action }) =>
65
+ ` ${c.cyan}${alias.padEnd(13)}${c.reset}${c.dim}${action} ${name}${c.reset}`,
66
+ )
58
67
  .join('\n');
59
68
 
60
69
  console.log(`${renderBanner()}
@@ -83,6 +92,7 @@ function printHelp() {
83
92
 
84
93
  ${c.bold}Coding agents${c.reset}
85
94
  ${agents}
95
+ ${c.cyan}harness-manifest${c.reset} Print how each agent is installed and launched (JSON)
86
96
 
87
97
  ${c.bold}Options${c.reset}
88
98
  ${c.dim}--model <id>${c.reset} Model to use (profile MODEL, or first live catalog model if UNSET)
@@ -108,6 +118,8 @@ ${agents}
108
118
  ${c.dim}$${c.reset} subc cursor install
109
119
  ${c.dim}$${c.reset} subc cursor uninstall
110
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"
111
123
  ${c.dim}$${c.reset} subc -p staging codex
112
124
 
113
125
  ${c.dim}Use subc <command> help for command-specific usage.${c.reset}
@@ -242,12 +254,45 @@ const authCommands = {
242
254
  whoami: whoamiCommand,
243
255
  };
244
256
 
257
+ /**
258
+ * Position of the headless prompt in the full argv, or -1. It is decided on
259
+ * the words left once profile flags are removed, which is what the agent
260
+ * launcher sees, so both agree on whether the run is headless.
261
+ */
262
+ function headlessPromptPosition(argv) {
263
+ const kept = [];
264
+ for (let i = 0; i < argv.length; i++) {
265
+ const arg = argv[i];
266
+ const afterCommand = kept.slice(1).map((index) => argv[index]);
267
+ if (
268
+ kept.length > 0 &&
269
+ headlessPromptIndex(afterCommand) === afterCommand.length
270
+ ) {
271
+ return i;
272
+ }
273
+ if (arg === '--') return -1;
274
+ if (arg === '--profile' || arg === '-p') {
275
+ i++;
276
+ continue;
277
+ }
278
+ if (arg.startsWith('--profile=')) continue;
279
+ kept.push(i);
280
+ }
281
+ return -1;
282
+ }
283
+
245
284
  function extractProfile(argv) {
246
285
  let profileName = process.env.SUBC_PROFILE?.trim() || DEFAULT_PROFILE;
247
286
  let profileExplicit = false;
248
287
  const args = [];
288
+ // The headless prompt is opaque, even when it looks like --profile.
289
+ const promptIndex = headlessPromptPosition(argv);
249
290
  for (let i = 0; i < argv.length; i++) {
250
291
  const arg = argv[i];
292
+ if (i === promptIndex) {
293
+ args.push(arg);
294
+ continue;
295
+ }
251
296
  if (arg === '--') {
252
297
  args.push(...argv.slice(i));
253
298
  break;
@@ -293,15 +338,32 @@ async function main() {
293
338
  return;
294
339
  }
295
340
 
341
+ // Machine-readable output: skip the update check so nothing else reaches
342
+ // stdout and no npm request is made.
343
+ if (command === 'harness-manifest') {
344
+ if (isHelpArg(args[1])) {
345
+ printHarnessManifestHelp();
346
+ return;
347
+ }
348
+ harnessManifestCommand(args.slice(1));
349
+ return;
350
+ }
351
+
296
352
  const launchTui =
297
353
  !command &&
298
354
  process.stdin.isTTY === true &&
299
355
  process.stdout.isTTY === true &&
300
356
  process.env.TERM !== 'dumb';
301
357
 
358
+ // Headless runs are scripted: no npm request and no update prompt.
359
+ const headlessAgent = resolveAgent(command);
360
+ const headless = Boolean(
361
+ headlessAgent && isHeadlessRequest(headlessAgent, args.slice(1)),
362
+ );
363
+
302
364
  // Explicit commands can wait on npm. The TUI opens immediately and prompts
303
365
  // only after a newer version is found.
304
- if (!launchTui) {
366
+ if (!launchTui && !headless) {
305
367
  const update = await showUpdateNotice();
306
368
  if (update?.action === 'updated' || update?.action === 'cancel') return;
307
369
  }
@@ -330,7 +392,12 @@ async function main() {
330
392
  }
331
393
  }
332
394
 
333
- if (!command || command === '--help' || command === '-h' || (command === 'help' && !args[1])) {
395
+ if (
396
+ !command ||
397
+ command === '--help' ||
398
+ command === '-h' ||
399
+ (command === 'help' && !args[1])
400
+ ) {
334
401
  printHelp();
335
402
  return;
336
403
  }
@@ -8,13 +8,23 @@ import { promisify } from 'node:util';
8
8
 
9
9
  const execFileAsync = promisify(execFile);
10
10
 
11
- const PNG_SIGNATURE = Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]);
11
+ const PNG_SIGNATURE = Buffer.from([
12
+ 0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a,
13
+ ]);
12
14
 
13
15
  export function imageContentType(buffer) {
14
- if (buffer.length >= 3 && buffer[0] === 0xff && buffer[1] === 0xd8 && buffer[2] === 0xff) {
16
+ if (
17
+ buffer.length >= 3 &&
18
+ buffer[0] === 0xff &&
19
+ buffer[1] === 0xd8 &&
20
+ buffer[2] === 0xff
21
+ ) {
15
22
  return 'image/jpeg';
16
23
  }
17
- if (buffer.length >= PNG_SIGNATURE.length && buffer.subarray(0, PNG_SIGNATURE.length).equals(PNG_SIGNATURE)) {
24
+ if (
25
+ buffer.length >= PNG_SIGNATURE.length &&
26
+ buffer.subarray(0, PNG_SIGNATURE.length).equals(PNG_SIGNATURE)
27
+ ) {
18
28
  return 'image/png';
19
29
  }
20
30
  return null;
@@ -48,11 +58,16 @@ async function readDarwinClipboard() {
48
58
  async function readLinuxClipboard() {
49
59
  const commands = [
50
60
  ['wl-paste', ['--type', 'image/png']],
61
+ ['wl-paste', ['--type', 'image/jpeg']],
51
62
  ['xclip', ['-selection', 'clipboard', '-t', 'image/png', '-o']],
63
+ ['xclip', ['-selection', 'clipboard', '-t', 'image/jpeg', '-o']],
52
64
  ];
53
65
  for (const [command, args] of commands) {
54
66
  try {
55
- const { stdout } = await execFileAsync(command, args, { encoding: 'buffer', maxBuffer: 8 * 1024 * 1024 });
67
+ const { stdout } = await execFileAsync(command, args, {
68
+ encoding: 'buffer',
69
+ maxBuffer: 8 * 1024 * 1024,
70
+ });
56
71
  if (stdout?.length) return stdout;
57
72
  } catch {
58
73
  // Try the next clipboard tool.
@@ -72,7 +87,11 @@ async function readWindowsClipboard() {
72
87
  Write-Output $path
73
88
  `;
74
89
  try {
75
- const { stdout } = await execFileAsync('powershell', ['-NoProfile', '-Command', script]);
90
+ const { stdout } = await execFileAsync('powershell', [
91
+ '-NoProfile',
92
+ '-Command',
93
+ script,
94
+ ]);
76
95
  const outPath = String(stdout).trim();
77
96
  if (!outPath) return null;
78
97
  const { readFile, rm } = await import('node:fs/promises');
package/bin/colors.js CHANGED
@@ -1,6 +1,6 @@
1
1
  // ANSI color helpers shared across the CLI. Keep redirected output machine
2
2
  // readable and honor the standard NO_COLOR opt-out.
3
- const noColor = Object.prototype.hasOwnProperty.call(process.env, 'NO_COLOR');
3
+ const noColor = Object.hasOwn(process.env, 'NO_COLOR');
4
4
  const forceColor =
5
5
  process.env.FORCE_COLOR !== undefined && process.env.FORCE_COLOR !== '0';
6
6