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.
Files changed (73) hide show
  1. package/README.md +119 -0
  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 +236 -932
  8. package/bin/auth.js +9 -1
  9. package/bin/claude-picker.js +143 -0
  10. package/bin/cli.js +60 -1
  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 +13 -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 +66 -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 +461 -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 +185 -0
  35. package/bin/runbook/copilot/install.sh +45 -63
  36. package/bin/runbook/cursor/agent.json +137 -0
  37. package/bin/runbook/cursor/install.sh +8 -35
  38. package/bin/runbook/deepseek-harness/agent.json +163 -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 +158 -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 +216 -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 +58 -0
  53. package/bin/runbook/subconscious-code/agent.json +156 -0
  54. package/bin/sessions.js +52 -22
  55. package/bin/tui.js +13 -0
  56. package/bin/update-check.js +5 -1
  57. package/bin/upgrade.js +24 -31
  58. package/bin/usage.js +21 -10
  59. package/bin/windows/agents.js +18 -17
  60. package/bin/windows/common.js +2 -28
  61. package/bin/windows/install.js +11 -17
  62. package/bin/windows/launch.js +252 -231
  63. package/bin/windows/process.js +12 -1
  64. package/bin/windows/setup.js +29 -9
  65. package/package.json +2 -3
  66. package/bin/registry.generated.json +0 -406
  67. package/bin/runbook/claude-code/run.sh +0 -254
  68. package/bin/runbook/model-capabilities.generated.sh +0 -9
  69. package/bin/runbook/opencode/provider.sh +0 -93
  70. package/bin/runbook/opencode/run.sh +0 -99
  71. package/bin/runbook/opencode/subconscious-compaction.ts +0 -100
  72. package/bin/runbook/subconscious-code/run.sh +0 -21
  73. package/bin/windows/claude.js +0 -100
package/bin/sessions.js CHANGED
@@ -2,6 +2,7 @@ import { spawnSync } from 'node:child_process';
2
2
  import fs from 'node:fs/promises';
3
3
  import os from 'node:os';
4
4
  import path from 'node:path';
5
+ import { AGENTS, agentById, agentCommandName } from './agent-data.js';
5
6
  import { resolveAgent, runAgent } from './agents.js';
6
7
 
7
8
  import { c } from './colors.js';
@@ -14,15 +15,25 @@ const MAX_INDEX_BYTES = 4 * 1024 * 1024;
14
15
  const MAX_HANDOFF_MESSAGES = 24;
15
16
  const MAX_HANDOFF_CHARS = 24_000;
16
17
  const MAX_MESSAGE_CHARS = 4_000;
17
- const SESSION_KEY_PATTERN = /^(claude|codex|opencode|pi|sc):[-A-Za-z0-9._]+$/;
18
-
19
- export const SESSION_HARNESSES = Object.freeze({
20
- claude: { name: 'Claude Code', command: 'claude', portable: true },
21
- codex: { name: 'Codex CLI', command: 'codex', portable: true },
22
- opencode: { name: 'OpenCode', command: 'opencode', portable: true },
23
- pi: { name: 'Pi', command: 'pi', portable: true },
24
- sc: { name: 'Marathon', command: 'marathon', portable: false },
25
- });
18
+ const SESSION_KEY_PATTERN = new RegExp(
19
+ `^(${AGENTS.filter((agent) => agent.sessions)
20
+ .map((agent) => agent.sessions.key)
21
+ .join('|')}):[-A-Za-z0-9._]+$`,
22
+ );
23
+
24
+ export const SESSION_HARNESSES = Object.freeze(
25
+ Object.fromEntries(
26
+ AGENTS.filter((agent) => agent.sessions).map((agent) => [
27
+ agent.sessions.key,
28
+ Object.freeze({
29
+ name: agent.name,
30
+ command: agentCommandName(agent),
31
+ portable: agent.sessions.portable,
32
+ agentId: agent.id,
33
+ }),
34
+ ]),
35
+ ),
36
+ );
26
37
 
27
38
  function cleanText(value) {
28
39
  return String(value || '')
@@ -56,7 +67,10 @@ function parseLines(text) {
56
67
  for (const line of text.split('\n')) {
57
68
  if (!line.trim()) continue;
58
69
  try {
59
- records.push(JSON.parse(line));
70
+ const record = JSON.parse(line);
71
+ if (record && typeof record === 'object' && !Array.isArray(record)) {
72
+ records.push(record);
73
+ }
60
74
  } catch {
61
75
  // Session writers can leave a partial final line after an interrupted run.
62
76
  }
@@ -594,22 +608,38 @@ export function buildHandoffPrompt(session, messages) {
594
608
  .join('\n\n');
595
609
  }
596
610
 
611
+ function sessionLaunch(harness) {
612
+ return agentById(SESSION_HARNESSES[harness]?.agentId)?.launch ?? {};
613
+ }
614
+
615
+ // One pass, so a prompt that mentions a placeholder is passed unchanged.
616
+ function fillWords(words, values) {
617
+ return words.map((word) =>
618
+ word.replace(/\{(sessionId|sessionFile|prompt)\}/g, (_, name) =>
619
+ String(values[name]),
620
+ ),
621
+ );
622
+ }
623
+
597
624
  export function nativeResumeArgs(session) {
598
- if (session.harness === 'claude') return ['--resume', session.id];
599
- if (session.harness === 'codex') return ['resume', session.id];
600
- if (session.harness === 'opencode') return ['--session', session.id];
601
- if (session.harness === 'pi')
602
- return ['--session', session.sourcePath || session.id];
603
- if (session.harness === 'sc') return ['--resume', session.sourcePath];
604
- throw new Error(`Native resume is not supported for ${session.harnessName}.`);
625
+ const { resume } = sessionLaunch(session.harness);
626
+ if (!resume)
627
+ throw new Error(
628
+ `Native resume is not supported for ${session.harnessName}.`,
629
+ );
630
+ return fillWords(resume, {
631
+ sessionId: session.id,
632
+ sessionFile: session.sourcePath || session.id,
633
+ });
605
634
  }
606
635
 
607
636
  export function handoffLaunchArgs(targetHarness, prompt) {
608
- if (targetHarness === 'opencode') return ['--prompt', prompt];
609
- if (['claude', 'codex', 'pi'].includes(targetHarness)) return [prompt];
610
- throw new Error(
611
- `${SESSION_HARNESSES[targetHarness]?.name || targetHarness} cannot start an interactive handoff yet.`,
612
- );
637
+ const { handoff } = sessionLaunch(targetHarness);
638
+ if (!handoff)
639
+ throw new Error(
640
+ `${SESSION_HARNESSES[targetHarness]?.name || targetHarness} cannot start an interactive handoff yet.`,
641
+ );
642
+ return fillWords(handoff, { prompt });
613
643
  }
614
644
 
615
645
  function sessionByKey(sessions, key) {
package/bin/tui.js CHANGED
@@ -422,6 +422,19 @@ function spawnAndWait(command, args, options = {}) {
422
422
  }
423
423
 
424
424
  export async function runTui(options = {}) {
425
+ for (;;) {
426
+ const result = await runTuiOnce(options);
427
+ if (result?.args?.length !== 2 || result.args[0] !== '-p') return result;
428
+ options = {
429
+ ...options,
430
+ profileName: result.args[1],
431
+ profile: undefined,
432
+ state: undefined,
433
+ };
434
+ }
435
+ }
436
+
437
+ async function runTuiOnce(options) {
425
438
  const executable = await resolveTuiExecutable(options);
426
439
  if (!executable) return null;
427
440
 
@@ -86,6 +86,10 @@ function parseVersion(version) {
86
86
  };
87
87
  }
88
88
 
89
+ export function isVersion(version) {
90
+ return parseVersion(version) !== null;
91
+ }
92
+
89
93
  function comparePrerelease(a, b) {
90
94
  if (!a.length && !b.length) return 0;
91
95
  if (!a.length) return 1;
@@ -102,7 +106,7 @@ function comparePrerelease(a, b) {
102
106
  return Math.sign(aNumber - bNumber);
103
107
  if (aNumber !== null) return -1;
104
108
  if (bNumber !== null) return 1;
105
- return a[index].localeCompare(b[index]);
109
+ return a[index] < b[index] ? -1 : 1;
106
110
  }
107
111
  return 0;
108
112
  }
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`,
@@ -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,6 +1,8 @@
1
1
  import os from 'node:os';
2
2
  import path from 'node:path';
3
3
  import readline from 'node:readline';
4
+ import { agentBinary, agentInstallDirs } from '../agent-data.js';
5
+ import { separatorIndex } from '../headless-args.js';
4
6
  import { parseOptions } from './common.js';
5
7
  import { installWindowsAgent, windowsInstallSpec } from './install.js';
6
8
  import { executeWindowsLaunch, windowsLaunch } from './launch.js';
@@ -41,18 +43,12 @@ export async function runWindowsAgent(agent, argv, dependencies) {
41
43
  } = dependencies;
42
44
  const parsed = parseAgentAction(agent, argv);
43
45
  let args =
44
- parsed.action !== 'launch' && parsed.action === argv[0]
46
+ !['launch', 'headless'].includes(parsed.action) && parsed.action === argv[0]
45
47
  ? argv.slice(1)
46
48
  : argv;
47
49
  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
- : [];
50
+ const preferredDirs = agentInstallDirs(agent, environment, os.homedir());
51
+ const bin = agentBinary(agent);
56
52
  environment.PATH = windowsPath([
57
53
  ...preferredDirs,
58
54
  ...(environment.PATH || '').split(';'),
@@ -69,7 +65,7 @@ export async function runWindowsAgent(agent, argv, dependencies) {
69
65
  }
70
66
 
71
67
  // Honor -- after which all arguments belong to the underlying agent.
72
- const boundary = args.indexOf('--');
68
+ const boundary = separatorIndex(args);
73
69
  const tail = boundary < 0 ? [] : args.slice(boundary);
74
70
  const extracted = extractModel(
75
71
  boundary < 0 ? args : args.slice(0, boundary),
@@ -79,7 +75,7 @@ export async function runWindowsAgent(agent, argv, dependencies) {
79
75
  let explicit = {};
80
76
  if (parsed.action === 'install' || agent.id === 'claude-code') {
81
77
  const handled = parseOptions(
82
- boundary < 0 ? args : args.slice(0, args.indexOf('--')),
78
+ boundary < 0 ? args : args.slice(0, boundary),
83
79
  { '--api-key': 1, '--gateway-url': 1 },
84
80
  );
85
81
  explicit = handled.options;
@@ -106,9 +102,10 @@ export async function runWindowsAgent(agent, argv, dependencies) {
106
102
  console.error(
107
103
  `Configured model ${extracted.model} is not in the live catalog; using ${model}.`,
108
104
  );
105
+ const headless = dependencies.headless ?? parsed.action === 'headless';
109
106
  let executable;
110
- if (parsed.action === 'launch') {
111
- executable = resolveWindowsExecutable(agent.bin, {
107
+ if (parsed.action === 'launch' || headless) {
108
+ executable = resolveWindowsExecutable(bin, {
112
109
  env: environment,
113
110
  preferredDirs,
114
111
  });
@@ -116,6 +113,7 @@ export async function runWindowsAgent(agent, argv, dependencies) {
116
113
  const installer = windowsInstallSpec(agent.id, environment);
117
114
  if (
118
115
  !installer ||
116
+ headless ||
119
117
  !process.stdin.isTTY ||
120
118
  !process.stdout.isTTY ||
121
119
  !(await askInstall(agent.name))
@@ -127,13 +125,13 @@ export async function runWindowsAgent(agent, argv, dependencies) {
127
125
  }
128
126
  const installed = await installWindowsAgent(agent.id, environment);
129
127
  if (installed) return finish(installed);
130
- executable = resolveWindowsExecutable(agent.bin, {
128
+ executable = resolveWindowsExecutable(bin, {
131
129
  env: environment,
132
130
  preferredDirs,
133
131
  });
134
132
  if (!executable) {
135
133
  console.error(
136
- `Installed ${agent.name}, but ${agent.bin} was not found. Add its installation directory to PATH or restart your terminal.`,
134
+ `Installed ${agent.name}, but ${bin} was not found. Add its installation directory to PATH or restart your terminal.`,
137
135
  );
138
136
  return finish(127);
139
137
  }
@@ -179,8 +177,11 @@ export async function runWindowsAgent(agent, argv, dependencies) {
179
177
  }
180
178
  }
181
179
  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`);
180
+ if (spec.command === bin) spec.command = executable;
181
+ // Headless stdout belongs to the agent alone.
182
+ (headless ? console.error : console.log)(
183
+ ` Launching ${agent.name} on Subconscious (${model})\n`,
184
+ );
184
185
  return finish(await executeWindowsLaunch(spec, runWindows));
185
186
  }
186
187
 
@@ -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 = [];
@@ -1,14 +1,12 @@
1
1
  import { createHash, randomUUID } from 'node:crypto';
2
- import { readFileSync } from 'node:fs';
3
2
  import fs from 'node:fs/promises';
4
3
  import os from 'node:os';
5
4
  import path from 'node:path';
5
+ import { agentById, installCommands } from '../agent-data.js';
6
6
  import { powershellCommand, runWindows } from './process.js';
7
7
 
8
- const registry = JSON.parse(
9
- readFileSync(new URL('../registry.generated.json', import.meta.url), 'utf8'),
10
- );
11
- const repository = 'subconscious-systems/subconscious-code';
8
+ const marathon = agentById('subconscious-code').install;
9
+ const repository = marathon.repository;
12
10
 
13
11
  function npmInstall(command) {
14
12
  const match = /^npm (?:i|install) -g ([@a-zA-Z0-9/._-]+)$/.exec(
@@ -18,8 +16,8 @@ function npmInstall(command) {
18
16
  }
19
17
 
20
18
  export function windowsInstallSpec(id, env = process.env) {
21
- const install = registry.agents.find((agent) => agent.id === id)?.install;
22
- const command = typeof install === 'object' ? install.win32 : undefined;
19
+ const install = installCommands(agentById(id)) || {};
20
+ const command = install.win32;
23
21
  if (id === 'subconscious-code')
24
22
  return { nativeRelease: true, display: 'subc marathon install' };
25
23
  if (!command) return null; // Never fall back to a Linux installer.
@@ -40,22 +38,18 @@ export function windowsInstallSpec(id, env = process.env) {
40
38
  }
41
39
 
42
40
  export function windowsReleaseAsset(release, arch = process.arch) {
43
- const target = {
44
- x64: 'x86_64-pc-windows-msvc',
45
- arm64: 'aarch64-pc-windows-msvc',
46
- }[arch];
41
+ const target = marathon.windows_targets.find((triple) =>
42
+ triple.startsWith({ x64: 'x86_64-', arm64: 'aarch64-' }[arch] ?? '-'),
43
+ );
47
44
  if (!target)
48
45
  throw new Error(
49
46
  `Subconscious Code does not support Windows architecture ${arch}`,
50
47
  );
51
48
  // Pinned older releases are installed under the new executable name too.
52
49
  // Never resolve or overwrite Windows' own sc.exe.
53
- const names = [
54
- `marathon-${target}.zip`,
55
- `marathon-${target}.exe`,
56
- `sc-${target}.zip`,
57
- `sc-${target}.exe`,
58
- ];
50
+ const names = marathon.windows_assets.map((name) =>
51
+ name.replaceAll('{target}', target),
52
+ );
59
53
  const asset = names
60
54
  .map((name) => release.assets?.find((asset) => asset.name === name))
61
55
  .find(Boolean);