subconscious-cli 4.0.18 → 4.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.
@@ -1,16 +1,69 @@
1
1
  import fs from 'node:fs/promises';
2
2
  import { spawn } from 'node:child_process';
3
+ import { spawnWindows } from './windows/process.js';
3
4
  import readline from 'node:readline';
4
5
  import { fileURLToPath } from 'node:url';
5
6
  import { LOGO_ART_SMALL_LINES } from './branding.js';
6
7
  import { c, colorEnabled } from './colors.js';
7
- import { detectInstallCommand } from './upgrade.js';
8
8
 
9
9
  export const PACKAGE_NAME = 'subconscious-cli';
10
10
  export const UPDATE_CHECK_TIMEOUT_MS = 1500;
11
11
  const UPDATE_ACTIONS = ['Update now', 'Skip for now'];
12
12
  const UPDATE_LOGO = LOGO_ART_SMALL_LINES;
13
13
 
14
+ function shellQuote(value) {
15
+ const text = String(value);
16
+ return /^[-/@A-Za-z0-9._:]+$/.test(text)
17
+ ? text
18
+ : `'${text.replaceAll("'", `'"'"'`)}'`;
19
+ }
20
+
21
+ export function detectInstallTarget(
22
+ here = fileURLToPath(import.meta.url),
23
+ platform = process.platform,
24
+ ) {
25
+ const normalized = here.replace(/\\/g, '/');
26
+ if (normalized.includes('/.pnpm/') || normalized.includes('/pnpm/global/')) {
27
+ return {
28
+ command: 'pnpm',
29
+ args: ['add', '-g', `${PACKAGE_NAME}@latest`],
30
+ display: `pnpm add -g ${PACKAGE_NAME}@latest`,
31
+ };
32
+ }
33
+ if (normalized.includes('/.yarn/') || normalized.includes('/yarn/global/')) {
34
+ return {
35
+ command: 'yarn',
36
+ args: ['global', 'add', `${PACKAGE_NAME}@latest`],
37
+ display: `yarn global add ${PACKAGE_NAME}@latest`,
38
+ };
39
+ }
40
+ if (normalized.includes('/.bun/install/global/')) {
41
+ return {
42
+ command: 'bun',
43
+ args: ['add', '-g', `${PACKAGE_NAME}@latest`],
44
+ display: `bun add -g ${PACKAGE_NAME}@latest`,
45
+ };
46
+ }
47
+
48
+ const marker = `/lib/node_modules/${PACKAGE_NAME}/`;
49
+ let prefix = normalized.includes(marker) ? normalized.slice(0, normalized.indexOf(marker)) : '';
50
+ if (!prefix && platform === 'win32') {
51
+ const windowsMarker = `/node_modules/${PACKAGE_NAME}/`;
52
+ if (normalized.includes(windowsMarker)) {
53
+ prefix = normalized.slice(0, normalized.indexOf(windowsMarker));
54
+ }
55
+ }
56
+ const args = ['install', '-g'];
57
+ if (prefix) args.push('--prefix', prefix);
58
+ args.push(`${PACKAGE_NAME}@latest`);
59
+ return {
60
+ command: 'npm',
61
+ args,
62
+ display: ['npm', ...args].map(platform === 'win32' ? value => /^[-/@A-Za-z0-9._:]+$/.test(value) ? value : `"${value}"` : shellQuote).join(' '),
63
+ prefix,
64
+ };
65
+ }
66
+
14
67
  function parseVersion(version) {
15
68
  const match = String(version)
16
69
  .trim()
@@ -123,13 +176,8 @@ export function renderUpdateNotice(installedVersion, latestVersion) {
123
176
  ].join('\n');
124
177
  }
125
178
 
126
- export function resolveInstallCommand(here = fileURLToPath(import.meta.url)) {
127
- return detectInstallCommand(here);
128
- }
129
-
130
179
  export function renderUpdateOptions(selectedIndex = 0, versions = {}) {
131
- const installCommand =
132
- versions.installCommand || `npm install -g ${PACKAGE_NAME}@latest`;
180
+ const installCommand = versions.installCommand || detectInstallTarget().display;
133
181
  const descriptions = [
134
182
  `Runs \`${installCommand}\``,
135
183
  versions.installedVersion
@@ -209,12 +257,30 @@ export async function selectUpdateAction(options = {}) {
209
257
  }
210
258
 
211
259
  export async function installLatest(options = {}) {
212
- const spawnImpl = options.spawnImpl || spawn;
213
- const command = options.command || resolveInstallCommand();
260
+ const spawnImpl = options.spawnImpl || (process.platform === 'win32' ? spawnWindows : spawn);
261
+ const target = options.target || detectInstallTarget();
214
262
  return new Promise((resolve) => {
215
- const child = spawnImpl(command, { shell: true, stdio: 'inherit' });
263
+ const child = spawnImpl(target.command, target.args, {
264
+ stdio: 'inherit',
265
+ });
216
266
  child.on('error', () => resolve(false));
217
- child.on('exit', (code) => resolve(code === 0));
267
+ child.on('exit', async (code) => {
268
+ if (code !== 0) {
269
+ resolve(false);
270
+ return;
271
+ }
272
+ if (!options.expectedVersion) {
273
+ resolve(true);
274
+ return;
275
+ }
276
+ try {
277
+ const readVersion = options.readVersion || currentVersion;
278
+ const actualVersion = await readVersion();
279
+ resolve(compareVersions(actualVersion, options.expectedVersion) >= 0);
280
+ } catch {
281
+ resolve(false);
282
+ }
283
+ });
218
284
  });
219
285
  }
220
286
 
@@ -241,9 +307,13 @@ export async function showUpdateNotice(options = {}) {
241
307
  options.interactive ?? (process.stdin.isTTY === true && process.stderr.isTTY === true);
242
308
  if (!interactive) return { installedVersion, latestVersion, action: 'skip' };
243
309
 
244
- const installCommand = options.installCommand || resolveInstallCommand();
245
310
  const select = options.select || selectUpdateAction;
246
- const action = await select({ installedVersion, latestVersion, installCommand });
311
+ const installTarget = options.installTarget || detectInstallTarget();
312
+ const action = await select({
313
+ installedVersion,
314
+ latestVersion,
315
+ installCommand: installTarget.display,
316
+ });
247
317
  if (action === 'cancel') return { installedVersion, latestVersion, action };
248
318
  if (action === 'skip') {
249
319
  write(`\n ${c.dim}Skipping update.${c.reset}\n\n`);
@@ -251,8 +321,10 @@ export async function showUpdateNotice(options = {}) {
251
321
  }
252
322
 
253
323
  write(`\n ${c.cyan}Updating ${PACKAGE_NAME} to ${latestVersion}...${c.reset}\n\n`);
254
- const install = options.install || installLatest;
255
- const installed = await install();
324
+ const install =
325
+ options.install ||
326
+ ((expectedVersion) => installLatest({ expectedVersion, target: installTarget }));
327
+ const installed = await install(latestVersion);
256
328
  if (installed) {
257
329
  write(
258
330
  `\n ${c.green}${c.bold}Update complete.${c.reset} Re-run your subc command.\n\n`,
@@ -260,8 +332,9 @@ export async function showUpdateNotice(options = {}) {
260
332
  return { installedVersion, latestVersion, action: 'updated' };
261
333
  }
262
334
 
263
- write(`\n ${c.red}Update failed.${c.reset} Try it manually:\n\n`);
264
- write(` ${c.cyan}${installCommand}${c.reset}\n\n`);
335
+ write(`\n ${c.red}Update failed or the running installation stayed unchanged.${c.reset}\n`);
336
+ write(` Try the exact detected install target manually:\n\n`);
337
+ write(` ${c.cyan}${installTarget.display}${c.reset}\n\n`);
265
338
  return { installedVersion, latestVersion, action: 'failed' };
266
339
  } catch {
267
340
  // Update discovery must never prevent the requested command from running.
package/bin/upgrade.js CHANGED
@@ -6,10 +6,12 @@
6
6
  */
7
7
 
8
8
  import { spawn } from 'node:child_process';
9
+ import { runWindows } from './windows/process.js';
9
10
  import fs from 'node:fs/promises';
10
11
  import readline from 'node:readline';
11
12
  import { fileURLToPath } from 'node:url';
12
13
  import { c } from './colors.js';
14
+ import { detectInstallTarget } from './update-check.js';
13
15
 
14
16
  export const PACKAGE_NAME = 'subconscious-cli';
15
17
  export const MIN_LOGIN_VERSION = '4.0';
@@ -24,17 +26,7 @@ export function parseUpgradeArgs(argv = []) {
24
26
  }
25
27
 
26
28
  export function detectInstallCommand(here = fileURLToPath(import.meta.url)) {
27
- const normalized = here.replace(/\\/g, '/');
28
- if (normalized.includes('/.pnpm/') || normalized.includes('/pnpm/global/')) {
29
- return `pnpm add -g ${PACKAGE_NAME}@latest`;
30
- }
31
- if (normalized.includes('/.yarn/') || normalized.includes('/yarn/global/')) {
32
- return `yarn global add ${PACKAGE_NAME}@latest`;
33
- }
34
- if (normalized.includes('/.bun/install/global/')) {
35
- return `bun add -g ${PACKAGE_NAME}@latest`;
36
- }
37
- return `npm install -g ${PACKAGE_NAME}@latest`;
29
+ return detectInstallTarget(here).display;
38
30
  }
39
31
 
40
32
  export async function currentCliVersion() {
@@ -90,6 +82,10 @@ function askYesNo(question) {
90
82
  }
91
83
 
92
84
  function runInstall(command) {
85
+ if (process.platform === 'win32') {
86
+ const target = detectInstallTarget();
87
+ return runWindows(target.command, target.args).then(code => code === 0, () => false);
88
+ }
93
89
  return new Promise((resolve) => {
94
90
  const child = spawn(command, { shell: true, stdio: 'inherit' });
95
91
  child.on('error', () => resolve(false));
@@ -132,6 +128,26 @@ export async function installLatest(options = {}) {
132
128
  return false;
133
129
  }
134
130
 
131
+ if (latest) {
132
+ try {
133
+ const readVersion = options.readVersion || currentCliVersion;
134
+ const installedVersion = await readVersion();
135
+ if (compareVersions(installedVersion, latest) < 0) {
136
+ console.error(
137
+ `\n ${c.red}Upgrade command completed, but this installation is still ${installedVersion}.${c.reset}`,
138
+ );
139
+ console.error(` Run the exact detected command manually:\n`);
140
+ console.error(` ${c.cyan}${command}${c.reset}\n`);
141
+ return false;
142
+ }
143
+ } catch {
144
+ console.error(`\n ${c.red}Could not verify the updated CLI installation.${c.reset}`);
145
+ console.error(` Run the exact detected command manually:\n`);
146
+ console.error(` ${c.cyan}${command}${c.reset}\n`);
147
+ return false;
148
+ }
149
+ }
150
+
135
151
  console.log(`\n ${c.green}${c.bold}✓ Upgraded.${c.reset} Re-run ${c.cyan}subc login${c.reset} if you were signing in.\n`);
136
152
  return true;
137
153
  }
@@ -0,0 +1,87 @@
1
+ import path from 'node:path';
2
+ import os from 'node:os';
3
+ import readline from 'node:readline';
4
+ import { windowsEnv, windowsBinDirs, windowsPath, resolveWindowsExecutable, readWindowsVersion, runWindows } from './process.js';
5
+ import { windowsInstallSpec, installWindowsAgent } from './install.js';
6
+ import { windowsSetup } from './setup.js';
7
+ import { windowsLaunch, executeWindowsLaunch } from './launch.js';
8
+ import { parseOptions } from './common.js';
9
+
10
+ const askInstall = name => new Promise(resolve => {
11
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
12
+ rl.question(` ${name} isn't installed. Install it now? [Y/n] `, answer => { rl.close(); resolve(['', 'y', 'yes'].includes(answer.trim().toLowerCase())); });
13
+ });
14
+
15
+ export async function runWindowsAgent(agent, argv, dependencies) {
16
+ const { profile, parseAgentAction, extractModel, requireApiKey, resolvedModelsForLaunch, selectLaunchModel, runbookEnv } = dependencies;
17
+ const parsed = parseAgentAction(agent, argv);
18
+ let args = parsed.action !== 'launch' && parsed.action === argv[0] ? argv.slice(1) : argv;
19
+ const environment = windowsEnv(profile?.values, process.env);
20
+ const preferredDirs = agent.runbook?.installDir
21
+ ? [path.resolve(environment[agent.runbook.installDirEnv] || path.join(os.homedir(), agent.runbook.installDir))] : [];
22
+ environment.PATH = windowsPath([...preferredDirs, ...(environment.PATH || '').split(';'), ...windowsBinDirs(environment)]);
23
+ if (agent.id === 'subconscious-code' && parsed.action === 'install') {
24
+ if (args.length) throw new Error('Usage: subc sc install');
25
+ return finish(await installWindowsAgent(agent.id, environment));
26
+ }
27
+ if (['status', 'uninstall'].includes(parsed.action)) {
28
+ return finish(await windowsSetup(agent.id, parsed.action, args, environment));
29
+ }
30
+
31
+ // Honor -- after which all arguments belong to the underlying agent.
32
+ const boundary = args.indexOf('--');
33
+ const tail = boundary < 0 ? [] : args.slice(boundary);
34
+ const extracted = extractModel(boundary < 0 ? args : args.slice(0, boundary), profile);
35
+ args = [...extracted.rest, ...tail];
36
+ let explicit = {};
37
+ if (parsed.action === 'install' || agent.id === 'claude-code') {
38
+ const handled = parseOptions(boundary < 0 ? args : args.slice(0, args.indexOf('--')), { '--api-key': 1, '--gateway-url': 1 });
39
+ explicit = handled.options;
40
+ }
41
+ const key = explicit['--api-key'] || await requireApiKey(profile, agent);
42
+ if (!key) return finish(1);
43
+ const catalogProfile = explicit['--gateway-url'] ? { ...profile, values: { ...profile?.values, GATEWAY_URL: explicit['--gateway-url'] } } : profile;
44
+ const catalog = await resolvedModelsForLaunch(catalogProfile, key, extracted.model);
45
+ const model = selectLaunchModel(extracted.model, extracted.modelSource, catalog);
46
+ if (extracted.model && model !== extracted.model) console.error(`Configured model ${extracted.model} is not in the live catalog; using ${model}.`);
47
+ let executable;
48
+ if (parsed.action === 'launch') {
49
+ executable = resolveWindowsExecutable(agent.bin, { env: environment, preferredDirs });
50
+ if (!executable) {
51
+ const installer = windowsInstallSpec(agent.id, environment);
52
+ if (!installer || !process.stdin.isTTY || !process.stdout.isTTY || !await askInstall(agent.name)) {
53
+ console.error(`${agent.name} isn't installed. ${installer ? `Install it with: ${installer.display}` : 'Install it separately, then rerun subc.'}`);
54
+ return finish(127);
55
+ }
56
+ const installed = await installWindowsAgent(agent.id, environment);
57
+ if (installed) return finish(installed);
58
+ executable = resolveWindowsExecutable(agent.bin, { env: environment, preferredDirs });
59
+ if (!executable) {
60
+ console.error(`Installed ${agent.name}, but ${agent.bin} was not found. Add its installation directory to PATH or restart your terminal.`);
61
+ return finish(127);
62
+ }
63
+ }
64
+ if (agent.id === 'claude-code') {
65
+ const version = dependencies.parseClaudeVersion(readWindowsVersion(executable, environment));
66
+ if (dependencies.claudeVersionNeedsUpgrade(version)) console.error(`Minimum supported Claude Code version is ${dependencies.minimumClaudeVersion}; installed version is ${version}. Please upgrade Claude Code.`);
67
+ }
68
+ }
69
+ // Reuse profile/catalog semantics, but use Windows path and environment rules.
70
+ const env = windowsEnv(runbookEnv(key, model, executable && path.dirname(executable), catalogProfile, agent, catalog.models), { PATH: windowsPath(executable ? [path.dirname(executable)] : [], environment.PATH) });
71
+ if (explicit['--gateway-url']) env.GATEWAY_URL = explicit['--gateway-url'];
72
+ if (parsed.action === 'install') return finish(await windowsSetup(agent.id, 'install', args, env));
73
+ if (agent.id === 'pi') await windowsSetup('pi', 'install', [], env);
74
+ if (agent.id === 'codex') {
75
+ try { await windowsSetup('codex', 'install', [], env, { log: () => {} }); }
76
+ catch (error) { console.error(`Could not refresh Codex hooks (launch will continue): ${error.message}`); }
77
+ }
78
+ const spec = await windowsLaunch(agent.id, args, env);
79
+ if (spec.command === agent.bin) spec.command = executable;
80
+ console.log(` Launching ${agent.name} on Subconscious (${model})\n`);
81
+ return finish(await executeWindowsLaunch(spec, runWindows));
82
+ }
83
+
84
+ function finish(code) {
85
+ if (code) process.exitCode = code;
86
+ return code;
87
+ }
@@ -0,0 +1,71 @@
1
+ // Windows-only equivalent of the existing Claude shell launch.
2
+ import { readFileSync } from 'node:fs';
3
+ import { resolvedModelSetting } from '../profiles.js';
4
+ import { positiveInteger, parseOptions } from './common.js';
5
+ const DEFAULTS = JSON.parse(readFileSync(new URL('../registry.generated.json', import.meta.url), 'utf8')).defaults;
6
+ function claudePickerSettings(models) {
7
+ return { availableModels: models, modelPicker: { replaceBuiltInOptions: true, options: models.map(model => ({model, label: model, description: 'Subconscious model ' + model})) } };
8
+ }
9
+
10
+ export function claudeNativeLaunch(argv, environment) {
11
+ const { options, rest: passthrough } = parseOptions(argv, {
12
+ '--gateway-url': 1, '--api-key': 1, '--model': 1,
13
+ '--compact-window': 1, '--max-context-tokens': 1,
14
+ });
15
+ const gatewayUrl = options['--gateway-url'] || environment.GATEWAY_URL?.trim() || '';
16
+ const apiKey = options['--api-key'] || environment.CLAUDE_CODE_API_KEY?.trim() || environment.API_KEY?.trim() || '';
17
+ const model = options['--model'] || environment.MODEL?.trim() || DEFAULTS.model;
18
+ const compactWindow = options['--compact-window'] ||
19
+ environment.CLAUDE_CODE_AUTO_COMPACT_WINDOW?.trim() ||
20
+ environment.COMPACT_WINDOW?.trim() ||
21
+ '1000000';
22
+ const maxContextTokens = options['--max-context-tokens'] ||
23
+ environment.CLAUDE_CODE_MAX_CONTEXT_TOKENS?.trim() ||
24
+ environment.MAX_CONTEXT_TOKENS?.trim() ||
25
+ '3000000';
26
+
27
+ if (!gatewayUrl || !apiKey) {
28
+ throw new Error('GATEWAY_URL and API_KEY are required to launch Claude Code');
29
+ }
30
+ positiveInteger(compactWindow, 'compact-window');
31
+ positiveInteger(maxContextTokens, 'max-context-tokens');
32
+
33
+ const effectiveGatewayUrl = environment.CLAUDE_GATEWAY_URL?.trim() || gatewayUrl;
34
+ const subagentModel = resolvedModelSetting(environment.CLAUDE_CODE_SUBAGENT_MODEL) || model;
35
+ const settings =
36
+ environment.SUBC_CLAUDE_SETTINGS?.trim() ||
37
+ JSON.stringify(claudePickerSettings([model], model));
38
+ const withoutTrailingSlash = effectiveGatewayUrl.replace(/\/+$/, '');
39
+ const env = {
40
+ ...environment,
41
+ ANTHROPIC_BASE_URL: effectiveGatewayUrl,
42
+ ANTHROPIC_AUTH_TOKEN: apiKey,
43
+ ANTHROPIC_MODEL: environment.ANTHROPIC_MODEL?.trim() || model,
44
+ ANTHROPIC_SMALL_FAST_MODEL: environment.ANTHROPIC_SMALL_FAST_MODEL?.trim() || model,
45
+ CLAUDE_CODE_ENABLE_GATEWAY_MODEL_DISCOVERY:
46
+ environment.CLAUDE_CODE_ENABLE_GATEWAY_MODEL_DISCOVERY?.trim() || '0',
47
+ CLAUDE_CODE_SUBAGENT_MODEL: subagentModel,
48
+ CLAUDE_CODE_MAX_CONCURRENT_SUBAGENTS:
49
+ environment.MAX_CONCURRENT_SUBAGENTS?.trim() || '4',
50
+ CLAUDE_CODE_MAX_SUBAGENT_SPAWN_DEPTH:
51
+ environment.MAX_SUBAGENT_SPAWN_DEPTH?.trim() || '1',
52
+ CLAUDE_CODE_AUTO_COMPACT_WINDOW: compactWindow,
53
+ CLAUDE_CODE_MAX_CONTEXT_TOKENS: maxContextTokens,
54
+ CLAUDE_CODE_ENABLE_TELEMETRY:
55
+ environment.CLAUDE_CODE_ENABLE_TELEMETRY?.trim() || '1',
56
+ CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC:
57
+ environment.CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC?.trim() || '1',
58
+ ENABLE_CLAUDEAI_MCP_SERVERS: 'false',
59
+ OTEL_LOGS_EXPORTER: environment.OTEL_LOGS_EXPORTER?.trim() || 'otlp',
60
+ OTEL_EXPORTER_OTLP_PROTOCOL:
61
+ environment.OTEL_EXPORTER_OTLP_PROTOCOL?.trim() || 'http/json',
62
+ OTEL_EXPORTER_OTLP_LOGS_ENDPOINT: `${withoutTrailingSlash}/v1/logs`,
63
+ OTEL_EXPORTER_OTLP_HEADERS: `x-api-key=${apiKey}`,
64
+ OTEL_EXPORTER_OTLP_TIMEOUT:
65
+ environment.OTEL_EXPORTER_OTLP_TIMEOUT?.trim() || '2000',
66
+ OTEL_LOGS_EXPORT_INTERVAL:
67
+ environment.OTEL_LOGS_EXPORT_INTERVAL?.trim() || '2000',
68
+ };
69
+
70
+ return { args: ['--settings', settings, ...passthrough], env };
71
+ }
@@ -0,0 +1,58 @@
1
+ import fs from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import { randomUUID } from 'node:crypto';
4
+ import { readFileSync } from 'node:fs';
5
+
6
+ export const defaults = JSON.parse(readFileSync(new URL('../registry.generated.json', import.meta.url), 'utf8')).defaults;
7
+ export const value = (env, key, fallback = '') => env[key]?.trim() || fallback;
8
+ export const origin = url => url.replace(/\/+$/, '').replace(/\/v1(?:\/(?:messages|responses|chat\/completions))?$/, '');
9
+ export function positiveInteger(input, name) {
10
+ if (!/^[1-9][0-9]*$/.test(String(input)) || !Number.isSafeInteger(Number(input))) throw new Error(`${name} must be a positive integer`);
11
+ return Number(input);
12
+ }
13
+ export function modelIds(env) {
14
+ const models = [...new Set([value(env, 'MODEL', defaults.model), ...value(env, 'SUBCONSCIOUS_MODELS', defaults.models.join('\n')).split(/\r?\n/)].filter(Boolean))];
15
+ for (const model of models) if (!/^[-A-Za-z0-9._:/+]+$/.test(model)) throw new Error(`Invalid model id: ${model}`);
16
+ return models;
17
+ }
18
+ export function parseOptions(argv, schema, { strict = false } = {}) {
19
+ const options = {}, rest = [];
20
+ for (let i = 0; i < argv.length; i++) {
21
+ if (argv[i] === '--') {
22
+ if (strict && i + 1 < argv.length) throw new Error(`Unexpected setup argument: ${argv[i + 1]}`);
23
+ rest.push(...argv.slice(i + 1)); break;
24
+ }
25
+ const [flag, ...equals] = argv[i].split('=');
26
+ if (!Object.hasOwn(schema, flag)) {
27
+ if (strict) throw new Error(`Unknown setup option: ${argv[i]}`);
28
+ rest.push(argv[i]); continue;
29
+ }
30
+ if (schema[flag] === true) {
31
+ if (equals.length) throw new Error(`${flag} does not take a value`);
32
+ options[flag] = true; continue;
33
+ }
34
+ const next = equals.length ? equals.join('=') : argv[++i];
35
+ if (!next || next.startsWith('--')) throw new Error(`${flag} requires a value`);
36
+ options[flag] = next;
37
+ }
38
+ return { options, rest };
39
+ }
40
+ export async function readJson(file, fallback) {
41
+ try { return JSON.parse((await fs.readFile(file, 'utf8')).replace(/^\uFEFF/, '')); }
42
+ catch (error) {
43
+ if (error.code === 'ENOENT') return fallback;
44
+ throw new Error(`Cannot read ${file}; leaving it unchanged: ${error.message}`);
45
+ }
46
+ }
47
+ export function object(value, description) {
48
+ if (!value || typeof value !== 'object' || Array.isArray(value)) throw new Error(`${description} must be a JSON object; leaving it unchanged`);
49
+ return value;
50
+ }
51
+ export async function writeJson(file, data) {
52
+ await fs.mkdir(path.dirname(file), { recursive: true });
53
+ const temp = `${file}.${randomUUID()}.tmp`;
54
+ try {
55
+ await fs.writeFile(temp, JSON.stringify(data, null, 2) + '\n', { mode: 0o600, flag: 'wx' });
56
+ await fs.rename(temp, file);
57
+ } finally { await fs.rm(temp, { force: true }); }
58
+ }
@@ -0,0 +1,71 @@
1
+ // Standalone Windows hook, copied into the agent's user configuration.
2
+ // Observational only: never block a session, leak credentials, or print payloads.
3
+ const fs = require('node:fs/promises');
4
+ const path = require('node:path');
5
+ const { randomUUID, createHash } = require('node:crypto');
6
+
7
+ async function main() {
8
+ const [client, configPath] = process.argv.slice(2);
9
+ let payload = {};
10
+ const input = await new Promise(resolve => {
11
+ let text = '';
12
+ const finish = () => { clearTimeout(timer); process.stdin.pause(); resolve(text); };
13
+ const timer = setTimeout(finish, 500);
14
+ process.stdin.setEncoding('utf8');
15
+ process.stdin.on('data', chunk => { text += chunk; if (text.length > 4 * 1024 * 1024) finish(); });
16
+ process.stdin.on('end', finish);
17
+ process.stdin.on('error', finish);
18
+ });
19
+ try { payload = JSON.parse(input); } catch { /* fail open */ }
20
+ const event = payload?.hook_event_name || '';
21
+ const response = client === 'cursor'
22
+ ? (event === 'preCompact' ? {} : { continue: true, permission: 'allow' })
23
+ : client === 'copilot' ? { continue: true } : {};
24
+ try {
25
+ const config = JSON.parse(await fs.readFile(configPath, 'utf8'));
26
+ const cid = payload?.conversation_id || payload?.session_id;
27
+ if (!cid || typeof cid !== 'string' || !config.apiKey || !config.gatewayUrl) return;
28
+ const post = async body => {
29
+ const controller = new AbortController();
30
+ const timer = setTimeout(() => controller.abort(), 1500);
31
+ try {
32
+ const res = await fetch(`${config.gatewayUrl}/v1/agent-hooks`, {
33
+ method: 'POST', headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${config.apiKey}`, 'x-subconscious-client': client },
34
+ body: JSON.stringify({ conversation_id: cid, hook_event_name: event, ...body }), signal: controller.signal,
35
+ });
36
+ await res.body?.cancel();
37
+ } catch { /* fail open */ } finally { clearTimeout(timer); }
38
+ };
39
+ if ((client === 'cursor' && event === 'beforeSubmitPrompt') || (client === 'copilot' && event === 'UserPromptSubmit')) {
40
+ if (typeof payload.prompt !== 'string' || !payload.prompt) return;
41
+ const root = payload.workspace_roots?.[0] || payload.cwd;
42
+ await post({ event: 'conversation_ensure', prompt: payload.prompt, workspace: typeof root === 'string' ? path.win32.basename(root) : undefined });
43
+ }
44
+ if (client === 'codex' && ['PreCompact', 'PostCompact'].includes(event)) {
45
+ const phase = event === 'PreCompact' ? 'start' : 'end';
46
+ await post({ event: 'conversation_compaction', phase, dedupe_key: `${cid}:${phase}:${payload.turn_id || randomUUID()}`, metadata: { trigger: payload.trigger, turn_id: payload.turn_id } });
47
+ }
48
+ if (client === 'cursor' && event === 'preCompact') {
49
+ const metadata = Object.fromEntries(['trigger', 'context_tokens', 'context_window_size', 'context_usage_percent', 'message_count', 'messages_to_compact', 'is_first_compaction'].filter(k => payload[k] != null).map(k => [k, payload[k]]));
50
+ await post({ event: 'conversation_compaction', phase: 'point', dedupe_key: `${cid}:${payload.generation_id ?? 'gen'}:${payload.is_first_compaction ?? false}`, metadata });
51
+ }
52
+ if (client === 'copilot' && ['PreCompact', 'UserPromptSubmit'].includes(event)) {
53
+ const pendingDir = path.join(path.dirname(configPath), 'subconscious-compact-pending');
54
+ const pending = path.join(pendingDir, createHash('sha256').update(cid).digest('hex') + '.json');
55
+ if (event === 'PreCompact') {
56
+ const token = randomUUID();
57
+ await fs.mkdir(pendingDir, { recursive: true });
58
+ await fs.writeFile(pending, JSON.stringify({ token }), { mode: 0o600 });
59
+ await post({ event: 'conversation_compaction', phase: 'start', dedupe_key: `${cid}:start:${token}`, metadata: { trigger: payload.trigger || 'auto' } });
60
+ } else {
61
+ try {
62
+ const { token } = JSON.parse(await fs.readFile(pending, 'utf8'));
63
+ await fs.rm(pending, { force: true });
64
+ await post({ event: 'conversation_compaction', phase: 'end', dedupe_key: `${cid}:end:${token}` });
65
+ } catch { /* no pending compaction */ }
66
+ }
67
+ }
68
+ } catch { /* missing/malformed config, I/O and transport errors are nonfatal */ }
69
+ finally { process.stdout.write(JSON.stringify(response) + '\n'); }
70
+ }
71
+ main().catch(() => { process.stdout.write('{}\n'); }).finally(() => { process.exitCode = 0; });
@@ -0,0 +1,97 @@
1
+ import fs from 'node:fs/promises';
2
+ import os from 'node:os';
3
+ import path from 'node:path';
4
+ import { createHash, randomUUID } from 'node:crypto';
5
+ import { readFileSync } from 'node:fs';
6
+ import { powershellCommand, runWindows } from './process.js';
7
+
8
+ const registry = JSON.parse(readFileSync(new URL('../registry.generated.json', import.meta.url), 'utf8'));
9
+ const repository = 'subconscious-systems/subconscious-code';
10
+
11
+ function npmInstall(command) {
12
+ const match = /^npm (?:i|install) -g ([@a-zA-Z0-9/._-]+)$/.exec(command || '');
13
+ return match ? { command: 'npm', args: ['install', '-g', match[1]] } : null;
14
+ }
15
+
16
+ export function windowsInstallSpec(id, env = process.env) {
17
+ const install = registry.agents.find(agent => agent.id === id)?.install;
18
+ const command = typeof install === 'object' ? install.win32 : undefined;
19
+ if (id === 'subconscious-code') return { nativeRelease: true, display: 'subc sc install' };
20
+ if (!command) return null; // Never fall back to a Linux installer.
21
+ if (id === 'claude-code') {
22
+ const primary = powershellCommand("$ErrorActionPreference = 'Stop'; Invoke-RestMethod 'https://claude.ai/install.ps1' | Invoke-Expression; if (-not $?) { exit 1 }", env);
23
+ return { ...primary, display: command, fallback: npmInstall(install.fallback) };
24
+ }
25
+ const spec = npmInstall(command);
26
+ if (!spec) throw new Error(`No safe Windows installer is defined for ${id}`);
27
+ return { ...spec, display: command };
28
+ }
29
+
30
+ export function windowsReleaseAsset(release, arch = process.arch) {
31
+ const target = { x64: 'x86_64-pc-windows-msvc', arm64: 'aarch64-pc-windows-msvc' }[arch];
32
+ if (!target) throw new Error(`Subconscious Code does not support Windows architecture ${arch}`);
33
+ const names = [`sc-${target}.zip`, `sc-${target}.exe`];
34
+ const asset = release.assets?.find(asset => names.includes(asset.name));
35
+ if (!asset) throw new Error(`Subconscious Code ${release.tag_name || 'latest'} has no published native Windows ${arch} binary. The CLI can launch an installed sc.exe, but the upstream release must include a Windows build before subc sc install can install it.`);
36
+ const checksum = release.assets.find(candidate => candidate.name === `${asset.name}.sha256`);
37
+ if (!checksum) throw new Error(`The Windows release is missing ${asset.name}.sha256; refusing an unverified installation.`);
38
+ return { asset, checksum };
39
+ }
40
+
41
+ export async function installWindowsSC(env, { fetchImpl = fetch, run = runWindows, home = os.homedir(), arch = process.arch, log = console.log } = {}) {
42
+ const version = env.SC_CODE_VERSION?.replace(/^v/, '');
43
+ const endpoint = version ? `tags/${encodeURIComponent('v' + version)}` : 'latest';
44
+ const response = await fetchImpl(`https://api.github.com/repos/${repository}/releases/${endpoint}`, { headers: { Accept: 'application/vnd.github+json' }, signal: AbortSignal.timeout(15000) });
45
+ if (!response.ok) throw new Error(`Could not find a Subconscious Code release (HTTP ${response.status}).`);
46
+ const release = await response.json();
47
+ const { asset, checksum } = windowsReleaseAsset(release, arch);
48
+ const download = async asset => {
49
+ const url = new URL(asset.browser_download_url);
50
+ if (url.origin !== 'https://github.com' || !url.pathname.startsWith(`/${repository}/releases/download/`)) throw new Error('Unexpected release download URL');
51
+ const response = await fetchImpl(url.href, { signal: AbortSignal.timeout(120000) });
52
+ if (!response.ok) throw new Error(`Download failed (HTTP ${response.status}): ${asset.name}`);
53
+ return Buffer.from(await response.arrayBuffer());
54
+ };
55
+ log(`Downloading Subconscious Code ${release.tag_name} for Windows ${arch}...`);
56
+ const [binary, checksumText] = await Promise.all([download(asset), download(checksum)]);
57
+ const expected = checksumText.toString('utf8').trim().split(/\s+/)[0];
58
+ if (!/^[a-f0-9]{64}$/i.test(expected) || createHash('sha256').update(binary).digest('hex') !== expected.toLowerCase()) throw new Error('Subconscious Code checksum verification failed');
59
+ const dir = await fs.mkdtemp(path.join(os.tmpdir(), 'subc-sc-install-'));
60
+ const installDir = path.resolve(env.SC_INSTALL_DIR || path.join(home, '.local', 'bin'));
61
+ const destination = path.join(installDir, 'sc.exe');
62
+ const staging = path.join(installDir, `sc-${randomUUID()}.tmp`);
63
+ try {
64
+ const extracted = path.join(dir, 'sc.exe');
65
+ if (asset.name.endsWith('.zip')) {
66
+ const archive = path.join(dir, 'release.zip');
67
+ await fs.writeFile(archive, binary, { flag: 'wx' });
68
+ // Extract only the exact root executable. No archive-controlled paths.
69
+ const spec = powershellCommand("$ErrorActionPreference = 'Stop'; Add-Type -AssemblyName System.IO.Compression.FileSystem; $z = [IO.Compression.ZipFile]::OpenRead($env:SUBC_ARCHIVE); try { $e = $z.GetEntry('sc.exe'); if ($null -eq $e) { throw 'Archive has no root sc.exe' }; [IO.Compression.ZipFileExtensions]::ExtractToFile($e, $env:SUBC_EXTRACTED, $false) } finally { $z.Dispose() }", { ...env, SUBC_ARCHIVE: archive, SUBC_EXTRACTED: extracted });
70
+ if (await run(spec.command, spec.args, { env: spec.env })) throw new Error('Could not extract the Windows release');
71
+ } else await fs.writeFile(extracted, binary, { flag: 'wx' });
72
+ const magic = (await fs.readFile(extracted)).subarray(0, 2).toString('ascii');
73
+ if (magic !== 'MZ') throw new Error('Release is not a Windows executable');
74
+ await fs.mkdir(installDir, { recursive: true });
75
+ await fs.copyFile(extracted, staging);
76
+ await fs.rename(staging, destination);
77
+ log(`Installed ${destination}. Run subc sc to launch it.`);
78
+ return 0;
79
+ } finally {
80
+ await fs.rm(staging, { force: true });
81
+ await fs.rm(dir, { recursive: true, force: true });
82
+ }
83
+ }
84
+
85
+ export async function installWindowsAgent(id, env, run = runWindows) {
86
+ const spec = windowsInstallSpec(id, env);
87
+ if (!spec) throw new Error('Install this agent separately, then rerun subc.');
88
+ if (spec.nativeRelease) return installWindowsSC(env, { run });
89
+ let code;
90
+ try { code = await run(spec.command, spec.args, { env: spec.env || env }); }
91
+ catch (error) { if (!spec.fallback) throw error; code = 1; }
92
+ if (code && spec.fallback) {
93
+ console.error('Native installer failed; trying the npm package.');
94
+ code = await run(spec.fallback.command, spec.fallback.args, { env });
95
+ }
96
+ return code;
97
+ }