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
@@ -1,5 +1,7 @@
1
1
  import { spawn } from 'node:child_process';
2
+ import { readFileSync } from 'node:fs';
2
3
  import fs from 'node:fs/promises';
4
+ import path from 'node:path';
3
5
  import readline from 'node:readline';
4
6
  import { fileURLToPath } from 'node:url';
5
7
  import { LOGO_ART_SMALL_LINES } from './branding.js';
@@ -8,6 +10,9 @@ import { spawnWindows } from './windows/process.js';
8
10
 
9
11
  export const PACKAGE_NAME = 'subconscious-cli';
10
12
  export const UPDATE_CHECK_TIMEOUT_MS = 1500;
13
+ export const INSTALL_SCRIPT_URL =
14
+ 'https://raw.githubusercontent.com/subconscious-systems/subconscious/main/scripts/install.sh';
15
+
11
16
  const UPDATE_ACTIONS = ['Update now', 'Skip for now'];
12
17
  const UPDATE_LOGO = LOGO_ART_SMALL_LINES;
13
18
 
@@ -18,10 +23,35 @@ function shellQuote(value) {
18
23
  : `'${text.replaceAll("'", `'"'"'`)}'`;
19
24
  }
20
25
 
26
+ /** The record scripts/install.sh leaves in the package it installed. */
27
+ function readCurlInstall(packageDir) {
28
+ try {
29
+ const record = JSON.parse(
30
+ readFileSync(path.join(packageDir, '.subc-install.json'), 'utf-8'),
31
+ );
32
+ return record?.method === 'curl' && typeof record.bin_dir === 'string'
33
+ ? record
34
+ : null;
35
+ } catch {
36
+ return null;
37
+ }
38
+ }
39
+
21
40
  export function detectInstallTarget(
22
41
  here = fileURLToPath(import.meta.url),
23
42
  platform = process.platform,
24
43
  ) {
44
+ const packageDir = path.resolve(path.dirname(here), '..');
45
+ const curl = platform !== 'win32' && readCurlInstall(packageDir);
46
+ if (curl) {
47
+ // Rerun the installer into the same directories it used before.
48
+ const display = `curl -fsSL ${INSTALL_SCRIPT_URL} | SUBC_INSTALL_DIR=${shellQuote(packageDir)} SUBC_BIN_DIR=${shellQuote(curl.bin_dir)} bash`;
49
+ return {
50
+ command: 'bash',
51
+ args: ['-c', `set -o pipefail; ${display}`],
52
+ display,
53
+ };
54
+ }
25
55
  const normalized = here.replace(/\\/g, '/');
26
56
  if (normalized.includes('/.pnpm/') || normalized.includes('/pnpm/global/')) {
27
57
  return {
@@ -86,6 +116,10 @@ function parseVersion(version) {
86
116
  };
87
117
  }
88
118
 
119
+ export function isVersion(version) {
120
+ return parseVersion(version) !== null;
121
+ }
122
+
89
123
  function comparePrerelease(a, b) {
90
124
  if (!a.length && !b.length) return 0;
91
125
  if (!a.length) return 1;
@@ -102,7 +136,7 @@ function comparePrerelease(a, b) {
102
136
  return Math.sign(aNumber - bNumber);
103
137
  if (aNumber !== null) return -1;
104
138
  if (bNumber !== null) return 1;
105
- return a[index].localeCompare(b[index]);
139
+ return a[index] < b[index] ? -1 : 1;
106
140
  }
107
141
  return 0;
108
142
  }
package/bin/upgrade.js CHANGED
@@ -10,7 +10,11 @@ import fs from 'node:fs/promises';
10
10
  import readline from 'node:readline';
11
11
  import { fileURLToPath } from 'node:url';
12
12
  import { c } from './colors.js';
13
- import { detectInstallTarget } from './update-check.js';
13
+ import {
14
+ compareVersions,
15
+ detectInstallTarget,
16
+ isVersion,
17
+ } from './update-check.js';
14
18
  import { runWindows } from './windows/process.js';
15
19
 
16
20
  export const PACKAGE_NAME = 'subconscious-cli';
@@ -38,23 +42,6 @@ export async function currentCliVersion() {
38
42
  return pkg.version;
39
43
  }
40
44
 
41
- export function compareVersions(a, b) {
42
- const pa = String(a)
43
- .split('.')
44
- .map((part) => Number.parseInt(part, 10) || 0);
45
- const pb = String(b)
46
- .split('.')
47
- .map((part) => Number.parseInt(part, 10) || 0);
48
- const n = Math.max(pa.length, pb.length);
49
- for (let i = 0; i < n; i++) {
50
- const da = pa[i] || 0;
51
- const db = pb[i] || 0;
52
- if (da > db) return 1;
53
- if (da < db) return -1;
54
- }
55
- return 0;
56
- }
57
-
58
45
  export async function fetchLatestVersion(fetchImpl = fetch) {
59
46
  const res = await fetchImpl(
60
47
  `https://registry.npmjs.org/${PACKAGE_NAME}/latest`,
@@ -98,16 +85,16 @@ function askYesNo(question) {
98
85
  });
99
86
  }
100
87
 
101
- function runInstall(command) {
88
+ function runInstall() {
89
+ const target = detectInstallTarget();
102
90
  if (process.platform === 'win32') {
103
- const target = detectInstallTarget();
104
91
  return runWindows(target.command, target.args).then(
105
92
  (code) => code === 0,
106
93
  () => false,
107
94
  );
108
95
  }
109
96
  return new Promise((resolve) => {
110
- const child = spawn(command, { shell: true, stdio: 'inherit' });
97
+ const child = spawn(target.command, target.args, { stdio: 'inherit' });
111
98
  child.on('error', () => resolve(false));
112
99
  child.on('exit', (code) => resolve(code === 0));
113
100
  });
@@ -121,13 +108,17 @@ export async function installLatest(options = {}) {
121
108
 
122
109
  let latest;
123
110
  try {
124
- latest = await fetchLatest();
111
+ const version = await fetchLatest();
112
+ if (!isVersion(version)) {
113
+ throw new Error(`npm returned an invalid version (${version}).`);
114
+ }
115
+ latest = version;
125
116
  } catch (error) {
126
117
  console.error(` ${c.yellow}${error.message}${c.reset}`);
127
118
  console.error(` ${c.dim}Installing @latest anyway.${c.reset}\n`);
128
119
  }
129
120
 
130
- if (latest && compareVersions(current, latest) >= 0) {
121
+ if (latest && isVersion(current) && compareVersions(current, latest) >= 0) {
131
122
  console.log(
132
123
  `\n ${c.green}Already up to date${c.reset} ${c.dim}(${current}).${c.reset}\n`,
133
124
  );
@@ -153,18 +144,12 @@ export async function installLatest(options = {}) {
153
144
  }
154
145
 
155
146
  if (latest) {
147
+ const readVersion = options.readVersion || currentCliVersion;
148
+ let installedVersion;
156
149
  try {
157
- const readVersion = options.readVersion || currentCliVersion;
158
- const installedVersion = await readVersion();
159
- if (compareVersions(installedVersion, latest) < 0) {
160
- console.error(
161
- `\n ${c.red}Upgrade command completed, but this installation is still ${installedVersion}.${c.reset}`,
162
- );
163
- console.error(` Run the exact detected command manually:\n`);
164
- console.error(` ${c.cyan}${command}${c.reset}\n`);
165
- return false;
166
- }
167
- } catch {
150
+ installedVersion = await readVersion();
151
+ } catch {}
152
+ if (!isVersion(installedVersion)) {
168
153
  console.error(
169
154
  `\n ${c.red}Could not verify the updated CLI installation.${c.reset}`,
170
155
  );
@@ -172,6 +157,14 @@ export async function installLatest(options = {}) {
172
157
  console.error(` ${c.cyan}${command}${c.reset}\n`);
173
158
  return false;
174
159
  }
160
+ if (compareVersions(installedVersion, latest) < 0) {
161
+ console.error(
162
+ `\n ${c.red}Upgrade command completed, but this installation is still ${installedVersion}.${c.reset}`,
163
+ );
164
+ console.error(` Run the exact detected command manually:\n`);
165
+ console.error(` ${c.cyan}${command}${c.reset}\n`);
166
+ return false;
167
+ }
175
168
  }
176
169
 
177
170
  console.log(
package/bin/usage.js CHANGED
@@ -9,19 +9,30 @@ import { printLoginUpgradeWarning } from './upgrade.js';
9
9
 
10
10
  export const USAGE_API_PATH = '/api/v1/usage';
11
11
 
12
- export function formatTokens(value) {
12
+ // Largest first: [size, suffix].
13
+ const TOKEN_UNITS = [
14
+ [1_000_000_000, 'B'],
15
+ [1_000_000, 'M'],
16
+ [1_000, 'K'],
17
+ ];
18
+
19
+ export function formatTokens(value, { sigFigs = 3 } = {}) {
13
20
  const n = Number(value);
21
+
14
22
  if (!Number.isFinite(n) || n < 0) return '0';
15
- if (n >= 1_000_000_000) {
16
- const scaled = n / 1_000_000_000;
17
- return `${scaled.toFixed(n % 1_000_000_000 ? 1 : 0)}B`;
18
- }
19
- if (n >= 1_000_000) {
20
- const scaled = n / 1_000_000;
21
- return `${scaled.toFixed(n % 1_000_000 ? 1 : 0)}M`;
23
+
24
+ let index = TOKEN_UNITS.findIndex(([size]) => n >= size);
25
+ if (index === -1) return n.toLocaleString('en-US');
26
+
27
+ // Number() also turns toPrecision's exponent form ("1.00e+3") back into 1000.
28
+
29
+ let scaled = Number((n / TOKEN_UNITS[index][0]).toPrecision(sigFigs));
30
+ // Rounding can reach the next unit: 999_500 is 1M, not 1000K.
31
+ if (scaled >= 1_000 && index > 0) {
32
+ index -= 1;
33
+ scaled = Number((n / TOKEN_UNITS[index][0]).toPrecision(sigFigs));
22
34
  }
23
- if (n >= 1_000) return `${Math.round(n / 1_000)}K`;
24
- return n.toLocaleString('en-US');
35
+ return `${scaled}${TOKEN_UNITS[index][1]}`;
25
36
  }
26
37
 
27
38
  export function formatCurrency(value) {
@@ -1,8 +1,13 @@
1
1
  import os from 'node:os';
2
2
  import path from 'node:path';
3
- import readline from 'node:readline';
3
+ import {
4
+ agentBinary,
5
+ agentCommandName,
6
+ agentInstallDirs,
7
+ installAdvice,
8
+ } from '../agent-data.js';
9
+ import { separatorIndex } from '../headless-args.js';
4
10
  import { parseOptions } from './common.js';
5
- import { installWindowsAgent, windowsInstallSpec } from './install.js';
6
11
  import { executeWindowsLaunch, windowsLaunch } from './launch.js';
7
12
  import {
8
13
  readWindowsVersion,
@@ -14,21 +19,6 @@ import {
14
19
  } from './process.js';
15
20
  import { windowsSetup } from './setup.js';
16
21
 
17
- const askInstall = (name) =>
18
- new Promise((resolve) => {
19
- const rl = readline.createInterface({
20
- input: process.stdin,
21
- output: process.stdout,
22
- });
23
- rl.question(
24
- ` ${name} isn't installed. Install it now? [Y/n] `,
25
- (answer) => {
26
- rl.close();
27
- resolve(['', 'y', 'yes'].includes(answer.trim().toLowerCase()));
28
- },
29
- );
30
- });
31
-
32
22
  export async function runWindowsAgent(agent, argv, dependencies) {
33
23
  const {
34
24
  profile,
@@ -41,27 +31,17 @@ export async function runWindowsAgent(agent, argv, dependencies) {
41
31
  } = dependencies;
42
32
  const parsed = parseAgentAction(agent, argv);
43
33
  let args =
44
- parsed.action !== 'launch' && parsed.action === argv[0]
34
+ !['launch', 'headless'].includes(parsed.action) && parsed.action === argv[0]
45
35
  ? argv.slice(1)
46
36
  : argv;
47
37
  const environment = windowsEnv(profile?.values, process.env);
48
- const preferredDirs = agent.runbook?.installDir
49
- ? [
50
- path.resolve(
51
- environment[agent.runbook.installDirEnv] ||
52
- path.join(os.homedir(), agent.runbook.installDir),
53
- ),
54
- ]
55
- : [];
38
+ const preferredDirs = agentInstallDirs(agent, environment, os.homedir());
39
+ const bin = agentBinary(agent);
56
40
  environment.PATH = windowsPath([
57
41
  ...preferredDirs,
58
42
  ...(environment.PATH || '').split(';'),
59
43
  ...windowsBinDirs(environment),
60
44
  ]);
61
- if (agent.id === 'subconscious-code' && parsed.action === 'install') {
62
- if (args.length) throw new Error('Usage: subc marathon install');
63
- return finish(await installWindowsAgent(agent.id, environment));
64
- }
65
45
  if (['status', 'uninstall'].includes(parsed.action)) {
66
46
  return finish(
67
47
  await windowsSetup(agent.id, parsed.action, args, environment),
@@ -69,7 +49,7 @@ export async function runWindowsAgent(agent, argv, dependencies) {
69
49
  }
70
50
 
71
51
  // Honor -- after which all arguments belong to the underlying agent.
72
- const boundary = args.indexOf('--');
52
+ const boundary = separatorIndex(args);
73
53
  const tail = boundary < 0 ? [] : args.slice(boundary);
74
54
  const extracted = extractModel(
75
55
  boundary < 0 ? args : args.slice(0, boundary),
@@ -79,7 +59,7 @@ export async function runWindowsAgent(agent, argv, dependencies) {
79
59
  let explicit = {};
80
60
  if (parsed.action === 'install' || agent.id === 'claude-code') {
81
61
  const handled = parseOptions(
82
- boundary < 0 ? args : args.slice(0, args.indexOf('--')),
62
+ boundary < 0 ? args : args.slice(0, boundary),
83
63
  { '--api-key': 1, '--gateway-url': 1 },
84
64
  );
85
65
  explicit = handled.options;
@@ -106,37 +86,22 @@ export async function runWindowsAgent(agent, argv, dependencies) {
106
86
  console.error(
107
87
  `Configured model ${extracted.model} is not in the live catalog; using ${model}.`,
108
88
  );
89
+ const headless = dependencies.headless ?? parsed.action === 'headless';
109
90
  let executable;
110
- if (parsed.action === 'launch') {
111
- executable = resolveWindowsExecutable(agent.bin, {
91
+ if (parsed.action === 'launch' || headless) {
92
+ executable = resolveWindowsExecutable(bin, {
112
93
  env: environment,
113
94
  preferredDirs,
114
95
  });
115
96
  if (!executable) {
116
- const installer = windowsInstallSpec(agent.id, environment);
117
- if (
118
- !installer ||
119
- !process.stdin.isTTY ||
120
- !process.stdout.isTTY ||
121
- !(await askInstall(agent.name))
122
- ) {
123
- console.error(
124
- `${agent.name} isn't installed. ${installer ? `Install it with: ${installer.display}` : 'Install it separately, then rerun subc.'}`,
125
- );
126
- return finish(127);
127
- }
128
- const installed = await installWindowsAgent(agent.id, environment);
129
- if (installed) return finish(installed);
130
- executable = resolveWindowsExecutable(agent.bin, {
131
- env: environment,
132
- preferredDirs,
133
- });
134
- if (!executable) {
135
- console.error(
136
- `Installed ${agent.name}, but ${agent.bin} was not found. Add its installation directory to PATH or restart your terminal.`,
137
- );
138
- return finish(127);
139
- }
97
+ // subc never installs a third-party agent; it only says how.
98
+ const { command, url } = installAdvice(agent, 'win32');
99
+ console.error(
100
+ `${agent.name} isn't installed. subc doesn't install it. Install it yourself, then rerun subc ${agentCommandName(agent)}.`,
101
+ );
102
+ if (command) console.error(` Install it with: ${command}`);
103
+ console.error(` Official install page: ${url}`);
104
+ return finish(127);
140
105
  }
141
106
  if (agent.id === 'claude-code') {
142
107
  const version = dependencies.parseClaudeVersion(
@@ -179,8 +144,11 @@ export async function runWindowsAgent(agent, argv, dependencies) {
179
144
  }
180
145
  }
181
146
  const spec = await windowsLaunch(agent.id, args, env);
182
- if (spec.command === agent.bin) spec.command = executable;
183
- console.log(` Launching ${agent.name} on Subconscious (${model})\n`);
147
+ if (spec.command === bin) spec.command = executable;
148
+ // Headless stdout belongs to the agent alone.
149
+ (headless ? console.error : console.log)(
150
+ ` Launching ${agent.name} on Subconscious (${model})\n`,
151
+ );
184
152
  return finish(await executeWindowsLaunch(spec, runWindows));
185
153
  }
186
154
 
@@ -1,40 +1,14 @@
1
1
  import { randomUUID } from 'node:crypto';
2
- import { readFileSync } from 'node:fs';
3
2
  import fs from 'node:fs/promises';
4
3
  import path from 'node:path';
5
4
 
6
- export const defaults = JSON.parse(
7
- readFileSync(new URL('../registry.generated.json', import.meta.url), 'utf8'),
8
- ).defaults;
5
+ export { DEFAULTS as defaults } from '../agent-data.js';
6
+ export { modelIds, positiveInteger } from '../launch-values.js';
9
7
  export const value = (env, key, fallback = '') => env[key]?.trim() || fallback;
10
8
  export const origin = (url) =>
11
9
  url
12
10
  .replace(/\/+$/, '')
13
11
  .replace(/\/v1(?:\/(?:messages|responses|chat\/completions))?$/, '');
14
- export function positiveInteger(input, name) {
15
- if (
16
- !/^[1-9][0-9]*$/.test(String(input)) ||
17
- !Number.isSafeInteger(Number(input))
18
- )
19
- throw new Error(`${name} must be a positive integer`);
20
- return Number(input);
21
- }
22
- export function modelIds(env) {
23
- const models = [
24
- ...new Set(
25
- [
26
- value(env, 'MODEL', defaults.model),
27
- ...value(env, 'SUBCONSCIOUS_MODELS', defaults.models.join('\n')).split(
28
- /\r?\n/,
29
- ),
30
- ].filter(Boolean),
31
- ),
32
- ];
33
- for (const model of models)
34
- if (!/^[-A-Za-z0-9._:/+]+$/.test(model))
35
- throw new Error(`Invalid model id: ${model}`);
36
- return models;
37
- }
38
12
  export function parseOptions(argv, schema, { strict = false } = {}) {
39
13
  const options = {},
40
14
  rest = [];