subconscious-cli 4.0.17 → 4.1.0-windows.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 (38) hide show
  1. package/README.md +125 -7
  2. package/bin/agents.js +10 -1
  3. package/bin/auth.js +15 -5
  4. package/bin/cli.js +69 -0
  5. package/bin/native/subc-tui-darwin-amd64 +0 -0
  6. package/bin/native/subc-tui-darwin-arm64 +0 -0
  7. package/bin/native/subc-tui-linux-amd64 +0 -0
  8. package/bin/native/subc-tui-linux-arm64 +0 -0
  9. package/bin/native/subc-tui-windows-amd64.exe +0 -0
  10. package/bin/native/subc-tui-windows-arm64.exe +0 -0
  11. package/bin/profiles.js +60 -16
  12. package/bin/registry.generated.json +14 -4
  13. package/bin/runbook/claude-code/install.sh +4 -4
  14. package/bin/runbook/claude-code/run.sh +3 -3
  15. package/bin/runbook/codex/run.sh +3 -2
  16. package/bin/runbook/copilot/install.sh +6 -5
  17. package/bin/runbook/cursor/install.sh +3 -2
  18. package/bin/runbook/deepseek-harness/run.sh +3 -2
  19. package/bin/runbook/opencode/install.sh +6 -5
  20. package/bin/runbook/opencode/run.sh +3 -2
  21. package/bin/runbook/pi/install.sh +4 -3
  22. package/bin/runbook/pi/run.sh +1 -1
  23. package/bin/runbook/subconscious-code/run.sh +1 -1
  24. package/bin/sessions.js +622 -0
  25. package/bin/tui.js +27 -1
  26. package/bin/update-check.js +90 -17
  27. package/bin/upgrade.js +27 -11
  28. package/bin/usage.js +229 -0
  29. package/bin/windows/agents.js +87 -0
  30. package/bin/windows/claude.js +71 -0
  31. package/bin/windows/common.js +58 -0
  32. package/bin/windows/hook.cjs +71 -0
  33. package/bin/windows/install.js +97 -0
  34. package/bin/windows/launch.js +97 -0
  35. package/bin/windows/pi-compaction.ts +29 -0
  36. package/bin/windows/process.js +157 -0
  37. package/bin/windows/setup.js +168 -0
  38. package/package.json +2 -1
@@ -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
+ }
@@ -0,0 +1,97 @@
1
+ // Native Windows launch specifications; never source or modify Unix runbooks.
2
+ import fs from 'node:fs/promises';
3
+ import os from 'node:os';
4
+ import path from 'node:path';
5
+ import { claudeNativeLaunch } from './claude.js';
6
+ import { defaults, value, origin, modelIds, positiveInteger, parseOptions, writeJson } from './common.js';
7
+
8
+ function connection(env, keyName) {
9
+ return {
10
+ model: value(env, 'MODEL', defaults.model),
11
+ base: origin(value(env, 'GATEWAY_URL', defaults.baseUrl)),
12
+ key: value(env, keyName, value(env, 'API_KEY')),
13
+ };
14
+ }
15
+
16
+ export async function windowsLaunch(id, argv, env, { tempRoot = os.tmpdir() } = {}) {
17
+ if (id === 'claude-code') return { command: 'claude', ...claudeNativeLaunch(argv, env) };
18
+ const keyName = { codex: 'CODEX_API_KEY', opencode: 'OPENCODE_API_KEY', pi: 'PI_API_KEY', 'subconscious-code': 'SC_API_KEY', 'deepseek-harness': 'DEEPSEEK_HARNESS_API_KEY' }[id];
19
+ const { model, base, key } = connection(env, keyName);
20
+ const childEnv = { ...env, SUBCONSCIOUS_API_KEY: key, SUBCONSCIOUS_GATEWAY_URL: base };
21
+ if (id === 'subconscious-code') {
22
+ return { command: 'sc', args: argv, env: { ...childEnv, SC_API_KEY: key, SC_BASE_URL: `${base}/v1`, SC_DLR_URL: base, SC_DLR_ENABLED: 'true', SC_MODEL: model } };
23
+ }
24
+ if (id === 'pi') return { command: 'pi', args: ['--provider', 'subconscious', '--model', model, ...argv], env: childEnv };
25
+ if (id === 'opencode') {
26
+ const context = positiveInteger(value(env, 'OPENCODE_CONTEXT_LIMIT', '5000000'), 'OPENCODE_CONTEXT_LIMIT');
27
+ const output = positiveInteger(value(env, 'OPENCODE_OUTPUT_LIMIT', '65536'), 'OPENCODE_OUTPUT_LIMIT');
28
+ const models = Object.fromEntries(modelIds(env).map(id => [id, { name: id, tools: true, limit: { context, output } }]));
29
+ childEnv.OPENCODE_CONFIG_CONTENT = JSON.stringify({
30
+ $schema: 'https://opencode.ai/config.json', disabled_providers: ['subconscious'],
31
+ provider: { 'subconscious-cli': { npm: '@ai-sdk/openai-compatible', name: 'Subconscious Gateway', options: {
32
+ baseURL: `${base}/v1`, apiKey: '{env:SUBCONSCIOUS_API_KEY}', headers: { 'x-subconscious-client': 'opencode' },
33
+ }, models } }, model: `subconscious-cli/${model}`,
34
+ });
35
+ return { command: 'opencode', args: parseOptions(argv, {}).rest, env: childEnv };
36
+ }
37
+ if (id === 'codex') {
38
+ const { options: opts, rest } = parseOptions(argv, { '--context-window': 1, '--max-context-window': 1, '--auto-compact-token-limit': 1, '--reasoning-effort': 1, '--subagents': true, '--external-tools': true });
39
+ const context = positiveInteger(opts['--context-window'] || value(env, 'CODEX_CONTEXT_WINDOW', '5000000'), 'context-window');
40
+ const maxContext = positiveInteger(opts['--max-context-window'] || value(env, 'CODEX_MAX_CONTEXT_WINDOW', String(context)), 'max-context-window');
41
+ const compact = positiveInteger(opts['--auto-compact-token-limit'] || value(env, 'CODEX_AUTO_COMPACT_TOKEN_LIMIT', '4500000'), 'auto-compact-token-limit');
42
+ const reasoning = opts['--reasoning-effort'] || value(env, 'CODEX_REASONING_EFFORT', 'max');
43
+ if (!['none', 'low', 'medium', 'high', 'max'].includes(reasoning)) throw new Error('reasoning-effort must be none, low, medium, high, or max');
44
+ const catalog = { models: modelIds(env).map(id => ({
45
+ slug: id, display_name: id, description: `Subconscious model ${id}`,
46
+ context_window: context, max_context_window: maxContext, auto_compact_token_limit: compact,
47
+ effective_context_window_percent: 95, supported_reasoning_levels: [], shell_type: 'shell_command',
48
+ visibility: 'list', supported_in_api: true, priority: 0,
49
+ service_tiers: [{ id: 'priority', name: 'Priority', description: 'Route requests through the configured priority service tier' }],
50
+ availability_nux: null, upgrade: null, base_instructions: 'You are Codex, a coding agent.',
51
+ supports_reasoning_summaries: false, support_verbosity: false, default_verbosity: null,
52
+ apply_patch_tool_type: 'freeform', truncation_policy: { mode: 'tokens', limit: 10000 },
53
+ supports_parallel_tool_calls: true, experimental_supported_tools: [],
54
+ })) };
55
+ // Validate all flags before creating a temporary directory.
56
+ const threads = positiveInteger(value(env, 'MAX_CONCURRENT_SUBAGENTS', '4'), 'MAX_CONCURRENT_SUBAGENTS');
57
+ const dir = await fs.mkdtemp(path.join(tempRoot, 'subc-codex-'));
58
+ const cleanup = () => fs.rm(dir, { recursive: true, force: true });
59
+ try {
60
+ const file = path.join(dir, 'models.json');
61
+ await writeJson(file, catalog);
62
+ const config = {
63
+ model, model_provider: 'subconscious', model_catalog_json: file, model_reasoning_effort: reasoning,
64
+ web_search: 'disabled', 'model_providers.subconscious.name': 'Subconscious',
65
+ 'model_providers.subconscious.base_url': `${base}/v1`, 'model_providers.subconscious.wire_api': 'responses',
66
+ 'model_providers.subconscious.env_key': 'SUBCONSCIOUS_API_KEY', 'model_providers.subconscious.stream_idle_timeout_ms': 300000,
67
+ };
68
+ if (!opts['--external-tools'] && value(env, 'CODEX_EXTERNAL_TOOLS', 'false') !== 'true') {
69
+ Object.assign(config, { 'features.apps': false, 'features.plugins': false, 'apps._default.enabled': false });
70
+ }
71
+ if (opts['--subagents']) Object.assign(config, { 'features.multi_agent': true, 'agents.max_threads': threads, 'agents.max_depth': 1, 'agents.interrupt_message': true });
72
+ const args = Object.entries(config).flatMap(([k, v]) => ['-c', `${k}=${JSON.stringify(v)}`]);
73
+ return { command: opts['--subagents'] ? 'npx' : 'codex', args: [...(opts['--subagents'] ? ['-y', '@openai/codex@0.132.0'] : []), ...args, ...rest], env: childEnv, cleanup };
74
+ } catch (error) { await cleanup(); throw error; }
75
+ }
76
+ if (id === 'deepseek-harness') {
77
+ const context = positiveInteger(value(env, 'DEEPSEEK_HARNESS_CONTEXT_WINDOW', '5000000'), 'DEEPSEEK_HARNESS_CONTEXT_WINDOW');
78
+ const maxTokens = positiveInteger(value(env, 'DEEPSEEK_HARNESS_MAX_TOKENS', '65536'), 'DEEPSEEK_HARNESS_MAX_TOKENS');
79
+ const models = modelIds(env);
80
+ const dir = await fs.mkdtemp(path.join(tempRoot, 'subc-dsh-'));
81
+ const cleanup = () => fs.rm(dir, { recursive: true, force: true });
82
+ try {
83
+ const file = path.join(dir, 'subconscious.cordis.yml');
84
+ const text = `- id: llm-pi-ai\n config:\n providers:\n subconscious:\n apiKeyEnv: SUBCONSCIOUS_API_KEY\n displayName: Subconscious Gateway\n api: openai-completions\n baseURL: !!js process.env.SUBCONSCIOUS_DSH_BASE_URL\n headers:\n x-subconscious-client: deepseek-harness\n compat:\n supportsDeveloperRole: false\n maxTokensField: max_tokens\n defaultContextWindow: ${context}\n defaultMaxTokens: ${maxTokens}\n models:\n${models.map(id => ` - id: '${id}'\n name: '${id}'\n contextWindow: ${context}\n maxTokens: ${maxTokens}`).join('\n')}\n- id: agent-default-model\n config:\n provider: subconscious\n model: '${model}'\n`;
85
+ await fs.writeFile(file, text, { mode: 0o600, flag: 'wx' });
86
+ const mode = ['web', 'headless'].includes(argv[0]) ? argv[0] : 'web';
87
+ const rest = ['web', 'headless'].includes(argv[0]) ? argv.slice(1) : argv;
88
+ return { command: 'dsh', args: [...(mode === 'web' ? ['web'] : ['--profile', 'headless']), '--patch', file, ...rest], env: { ...childEnv, SUBCONSCIOUS_DSH_BASE_URL: `${base}/v1` }, cleanup };
89
+ } catch (error) { await cleanup(); throw error; }
90
+ }
91
+ throw new Error(`No native Windows launcher is available for ${id}`);
92
+ }
93
+
94
+ export async function executeWindowsLaunch(spec, run) {
95
+ try { return await run(spec.command, spec.args, { env: spec.env }); }
96
+ finally { await spec.cleanup?.(); }
97
+ }
@@ -0,0 +1,29 @@
1
+ // Windows-only Pi extension. Loaded config is relative to this installed file.
2
+ import { readFileSync } from 'node:fs';
3
+ import { randomUUID } from 'node:crypto';
4
+ import { fileURLToPath } from 'node:url';
5
+ import { dirname, join } from 'node:path';
6
+
7
+ export default function (pi: { on: (event: string, handler: (...args: any[]) => any) => void }) {
8
+ for (const [eventName, phase] of [['session_before_compact', 'start'], ['session_compact', 'end']]) {
9
+ pi.on(eventName, async (event: any, ctx: any) => {
10
+ const session = ctx?.sessionManager?.getSessionId?.();
11
+ if (!session) return;
12
+ const controller = new AbortController();
13
+ const timer = setTimeout(() => controller.abort(), 1500);
14
+ try {
15
+ const config = JSON.parse(readFileSync(join(dirname(fileURLToPath(import.meta.url)), '..', 'subconscious-windows.json'), 'utf8'));
16
+ const url = process.env.SUBCONSCIOUS_GATEWAY_URL || config.gatewayUrl;
17
+ const key = process.env.SUBCONSCIOUS_API_KEY || config.apiKey;
18
+ if (!url || !key) return;
19
+ const response = await fetch(`${url.replace(/\/+$/, '')}/v1/agent-hooks`, {
20
+ method: 'POST', headers: { 'content-type': 'application/json', authorization: `Bearer ${key}`, 'x-subconscious-client': 'pi' },
21
+ body: JSON.stringify({ event: 'conversation_compaction', conversation_id: session, phase, hook_event_name: eventName, dedupe_key: `${session}:${phase}:${randomUUID()}`, metadata: { reason: event?.reason, tokens_before: event?.preparation?.tokensBefore, from_extension: event?.fromExtension } }),
22
+ signal: controller.signal,
23
+ });
24
+ await response.body?.cancel();
25
+ } catch { /* observational: never cancel or replace compaction */ }
26
+ finally { clearTimeout(timer); }
27
+ });
28
+ }
29
+ }
@@ -0,0 +1,157 @@
1
+ // Windows process handling. Unix callers keep using their existing launch paths.
2
+ import fs from 'node:fs';
3
+ import os from 'node:os';
4
+ import path from 'node:path';
5
+ import { spawn, spawnSync, execFileSync } from 'node:child_process';
6
+
7
+ export function windowsEnv(...sources) {
8
+ const result = {};
9
+ for (const source of sources) {
10
+ for (const [key, value] of Object.entries(source || {})) {
11
+ if (value !== undefined && value !== null) result[key.toUpperCase()] = String(value);
12
+ }
13
+ }
14
+ return result;
15
+ }
16
+
17
+ export function windowsBinDirs(environment = process.env, home = os.homedir()) {
18
+ const env = windowsEnv(environment);
19
+ return [...new Set([
20
+ env.APPDATA && path.win32.join(env.APPDATA, 'npm'),
21
+ env.USERPROFILE && path.win32.join(env.USERPROFILE, '.local', 'bin'),
22
+ home && path.win32.join(home, '.local', 'bin'),
23
+ env.LOCALAPPDATA && path.win32.join(env.LOCALAPPDATA, 'Microsoft', 'WinGet', 'Links'),
24
+ env.SYSTEMROOT && path.win32.join(env.SYSTEMROOT, 'System32', 'config', 'systemprofile', '.local', 'bin'),
25
+ env.SYSTEMROOT && path.win32.join(env.SYSTEMROOT, 'System32'),
26
+ env.SYSTEMROOT && path.win32.join(env.SYSTEMROOT, 'System32', 'WindowsPowerShell', 'v1.0'),
27
+ path.dirname(process.execPath),
28
+ ].filter(Boolean))];
29
+ }
30
+
31
+ export function windowsPath(extraDirs, currentPath = '') {
32
+ const seen = new Set();
33
+ return [...extraDirs, ...currentPath.split(';')].filter(dir => {
34
+ const key = dir.replace(/^"|"$/g, '').replace(/[\\/]+$/, '').toLowerCase();
35
+ if (!key || seen.has(key)) return false;
36
+ seen.add(key);
37
+ return true;
38
+ }).map(dir => dir.replace(/^"|"$/g, '')).join(';');
39
+ }
40
+
41
+ export function resolveWindowsExecutable(command, options = {}) {
42
+ const env = windowsEnv(options.env || process.env);
43
+ const pathApi = options.pathApi || path.win32;
44
+ const isFile = options.isFile || (file => { try { return fs.statSync(file).isFile(); } catch { return false; } });
45
+ const extensions = [...new Set(['.exe', '.com', ...(env.PATHEXT || '.CMD;.BAT').split(';').map(x => x.toLowerCase()), ''])];
46
+ const hasExtension = extensions.some(ext => ext && command.toLowerCase().endsWith(ext));
47
+ const dirs = /[\\/]/.test(command) ? [''] : [
48
+ ...(options.preferredDirs || []),
49
+ ...(env.PATH || '').split(';').filter(Boolean),
50
+ ...windowsBinDirs(env, options.home),
51
+ ];
52
+ for (const dir of dirs) {
53
+ for (const ext of hasExtension ? [''] : extensions) {
54
+ const candidate = dir ? pathApi.join(dir.replace(/^"|"$/g, ''), command + ext) : command + ext;
55
+ if (isFile(candidate)) return candidate;
56
+ }
57
+ }
58
+ return null;
59
+ }
60
+
61
+ // npm shims pass %* through cmd.exe, which reinterprets quotes, JSON, %, &,
62
+ // newlines, etc. Resolve their quoted entry point and run it with Node directly.
63
+ // This also supports npm/npx and VS Code's code.cmd without invoking a shell.
64
+ export function windowsInvocation(command, args = [], environment = process.env, options = {}) {
65
+ const env = windowsEnv(environment);
66
+ const resolve = options.resolve || resolveWindowsExecutable;
67
+ const executable = resolve(command, { env, ...options });
68
+ if (!executable) throw new Error(`Could not find ${command} on the Windows PATH.`);
69
+ if (!/\.(cmd|bat)$/i.test(executable)) return { command: executable, args, env };
70
+ const readFile = options.readFile || (file => fs.readFileSync(file, 'utf8'));
71
+ const pathApi = options.pathApi || path.win32;
72
+ const contents = readFile(executable);
73
+ const candidates = [...contents.matchAll(/%(?:dp0%|~dp0)[\\/]?([^"\r\n]+)"/gi)];
74
+ const name = pathApi.basename(executable).replace(/\.(cmd|bat)$/i, '').toLowerCase();
75
+ let electron;
76
+ if (/ELECTRON_RUN_AS_NODE\s*=\s*1/i.test(contents)) {
77
+ electron = candidates.map(match => pathApi.resolve(pathApi.dirname(executable), match[1])).find(target => /\.exe$/i.test(target) && !/node\.exe$/i.test(target) && fs.existsSync(target));
78
+ }
79
+ for (const match of candidates) {
80
+ const target = pathApi.resolve(pathApi.dirname(executable), match[1]);
81
+ // A shim's first executable is often node.exe, not the agent entry point.
82
+ if (/\.(exe|com)$/i.test(target)) {
83
+ if (!electron && !/node\.exe$/i.test(target) && fs.existsSync(target)) return { command: target, args, env };
84
+ continue;
85
+ }
86
+ if (['npm', 'npx'].includes(name) && pathApi.basename(target) !== `${name}-cli.js`) continue;
87
+ let source;
88
+ try { source = readFile(target); } catch { continue; }
89
+ if (!/\.[cm]?js$/i.test(target) && !/^#![^\r\n]*\bnode\b/.test(source)) continue;
90
+ return { command: electron || process.execPath, args: [target, ...args], env: electron ? { ...env, ELECTRON_RUN_AS_NODE: '1' } : env };
91
+ }
92
+ throw new Error(`Cannot safely launch the custom batch wrapper ${executable}. Install the native executable or standard npm package for ${command}.`);
93
+ }
94
+
95
+ export function spawnWindows(command, args, options = {}) {
96
+ const invocation = windowsInvocation(command, args, options.env || process.env);
97
+ return spawn(invocation.command, invocation.args, { ...options, env: invocation.env, shell: false });
98
+ }
99
+
100
+ export function spawnWindowsSync(command, args, options = {}) {
101
+ try {
102
+ const invocation = windowsInvocation(command, args, options.env || process.env);
103
+ return spawnSync(invocation.command, invocation.args, { ...options, env: invocation.env, shell: false });
104
+ } catch (error) { return { error, status: null, stdout: '', stderr: '' }; }
105
+ }
106
+
107
+ export function runWindows(command, args, options = {}) {
108
+ return new Promise((resolve, reject) => {
109
+ const child = spawnWindows(command, args, { stdio: 'inherit', ...options });
110
+ const forward = signal => { try { child.kill(signal); } catch { /* already exited */ } };
111
+ const interrupt = () => forward('SIGINT');
112
+ const terminate = () => forward('SIGTERM');
113
+ process.on('SIGINT', interrupt);
114
+ process.on('SIGTERM', terminate);
115
+ const cleanup = () => {
116
+ process.off('SIGINT', interrupt);
117
+ process.off('SIGTERM', terminate);
118
+ };
119
+ child.once('error', error => { cleanup(); reject(error); });
120
+ child.once('close', (code, signal) => {
121
+ cleanup();
122
+ resolve(code ?? (signal === 'SIGINT' ? 130 : 143));
123
+ });
124
+ });
125
+ }
126
+
127
+ export function readWindowsVersion(command, env) {
128
+ try {
129
+ const invocation = windowsInvocation(command, ['--version'], env);
130
+ return execFileSync(invocation.command, invocation.args, {
131
+ env: invocation.env, encoding: 'utf8', timeout: 3000, stdio: ['ignore', 'pipe', 'ignore'],
132
+ });
133
+ } catch { return ''; }
134
+ }
135
+
136
+ export function powershellCommand(source, env = process.env) {
137
+ // Only fixed application source is encoded here. Dynamic data uses env vars.
138
+ return { command: 'powershell.exe', args: ['-NoLogo', '-NoProfile', '-NonInteractive', '-EncodedCommand', Buffer.from(source, 'utf16le').toString('base64')], env: windowsEnv(env) };
139
+ }
140
+
141
+ export function openWindowsBrowser(url) {
142
+ const invocation = powershellCommand('Start-Process -FilePath $env:SUBC_BROWSER_URL', { ...process.env, SUBC_BROWSER_URL: url });
143
+ return runWindows(invocation.command, invocation.args, { env: invocation.env, stdio: 'ignore' });
144
+ }
145
+
146
+ export function windowsEditor(requested, environment = process.env) {
147
+ const env = windowsEnv(environment);
148
+ const text = requested || env.VISUAL?.trim() || env.EDITOR?.trim() || 'notepad.exe';
149
+ // An existing full path may contain spaces without quotes. Otherwise accept
150
+ // double-quoted argv, preserving Windows backslashes and rejecting shell use.
151
+ const parts = fs.existsSync(text) ? [text] : text.match(/"[^"]*"|[^\s"]+/g) || [];
152
+ if ((text.match(/"/g) || []).length % 2) throw new Error('Unmatched quote in VISUAL / EDITOR');
153
+ const [command, ...args] = parts.map(part => part.replace(/^"|"$/g, ''));
154
+ if (!command) throw new Error('Editor is empty');
155
+ if (/^(code|code-insiders)(\.cmd|\.exe)?$/i.test(path.win32.basename(command)) && !args.includes('--wait')) args.unshift('--wait');
156
+ return { command, args };
157
+ }
@@ -0,0 +1,168 @@
1
+ import fs from 'node:fs/promises';
2
+ import os from 'node:os';
3
+ import path from 'node:path';
4
+ import { defaults, origin, value, modelIds, positiveInteger, parseOptions, object, readJson, writeJson } from './common.js';
5
+
6
+ const owned = command => {
7
+ if (typeof command !== 'string') return false;
8
+ if (/subconscious-hook\.(?:sh|cjs)/.test(command)) return true;
9
+ const encoded = /-EncodedCommand ([A-Za-z0-9+/=]+)$/i.exec(command)?.[1];
10
+ return Boolean(encoded && Buffer.from(encoded, 'base64').toString('utf16le').startsWith('# SUBC_WINDOWS_HOOK\n'));
11
+ };
12
+ const providerName = 'Subconscious Gateway';
13
+ const exists = async file => { try { await fs.access(file); return true; } catch { return false; } };
14
+
15
+ // Hook hosts expect command strings. Keep paths and credentials out of shell
16
+ // syntax: PowerShell decodes data, then invokes Node with an argv array.
17
+ export function windowsHookCommand(client, script, config, node = process.execPath) {
18
+ const data = Buffer.from(JSON.stringify({ client, script, config, node })).toString('base64');
19
+ const source = `# SUBC_WINDOWS_HOOK\n$d = [Text.Encoding]::UTF8.GetString([Convert]::FromBase64String('${data}')) | ConvertFrom-Json\n& $d.node $d.script $d.client $d.config\nexit 0`;
20
+ return `powershell.exe -NoLogo -NoProfile -NonInteractive -EncodedCommand ${Buffer.from(source, 'utf16le').toString('base64')}`;
21
+ }
22
+
23
+ export function mergeHooks(document, client, command) {
24
+ const result = structuredClone(object(document, 'hooks configuration'));
25
+ const hooks = object(result.hooks ?? {}, 'hooks');
26
+ for (const [event, entries] of Object.entries(hooks)) {
27
+ if (!Array.isArray(entries)) throw new Error(`hooks.${event} must be an array; leaving it unchanged`);
28
+ const kept = [];
29
+ for (const entry of entries) {
30
+ object(entry, `hooks.${event} entry`);
31
+ if (Array.isArray(entry.hooks)) {
32
+ for (const hook of entry.hooks) object(hook, `hooks.${event} command`);
33
+ const filtered = entry.hooks.filter(hook => !owned(hook.command));
34
+ if (filtered.length || entry.hooks.length === 0) kept.push({ ...entry, hooks: filtered });
35
+ } else if (entry.hooks !== undefined) {
36
+ throw new Error(`hooks.${event}.hooks must be an array; leaving it unchanged`);
37
+ } else if (!owned(entry.command)) kept.push(entry);
38
+ }
39
+ if (kept.length) hooks[event] = kept;
40
+ else delete hooks[event];
41
+ }
42
+ if (command) {
43
+ const events = { codex: ['PreCompact', 'PostCompact'], cursor: ['beforeSubmitPrompt', 'preCompact'], copilot: ['UserPromptSubmit', 'PreCompact'] }[client];
44
+ for (const event of events) {
45
+ const hook = { type: 'command', command, timeout: client === 'copilot' ? 5 : 3 };
46
+ const entry = client === 'codex' ? { hooks: [{ ...hook, statusMessage: 'Reporting Subconscious compaction' }] } : hook;
47
+ hooks[event] = [...(hooks[event] || []), entry];
48
+ }
49
+ }
50
+ result.hooks = hooks;
51
+ if (client === 'cursor' && command) result.version ??= 1;
52
+ return result;
53
+ }
54
+
55
+ export async function windowsSetup(id, action, argv, environment, { home = os.homedir(), log = console.log } = {}) {
56
+ const schema = { '--gateway-url': 1, '--api-key': 1, '--model': 1 };
57
+ if (id === 'copilot') Object.assign(schema, { '--vscode-app': 1, '--max-input-tokens': 1, '--max-output-tokens': 1 });
58
+ if (id === 'pi') Object.assign(schema, { '--context-window': 1, '--max-tokens': 1 });
59
+ const { options } = parseOptions(argv, schema, { strict: true });
60
+ const env = { ...environment, ...(options['--model'] ? { MODEL: options['--model'] } : {}) };
61
+ const gatewayUrl = origin(options['--gateway-url'] || value(env, 'GATEWAY_URL', defaults.baseUrl));
62
+ const apiKey = options['--api-key'] || value(env, 'API_KEY');
63
+ const installing = action === 'install';
64
+ const removing = action === 'uninstall';
65
+ const needsKey = ['codex', 'cursor', 'copilot', 'pi'].includes(id);
66
+ if (installing && needsKey && !apiKey) throw new Error('An API key is required for setup. Run subc login first.');
67
+
68
+ if (['codex', 'cursor', 'copilot'].includes(id)) {
69
+ const dir = id === 'codex' ? (env.CODEX_DIR || env.CODEX_HOME || path.join(home, '.codex')) : path.join(home, `.${id}`);
70
+ const hooksFile = id === 'copilot' ? path.join(dir, 'hooks', 'subconscious-hooks.json') : path.join(dir, 'hooks.json');
71
+ const script = path.join(dir, 'hooks', 'subconscious-hook.cjs');
72
+ const config = path.join(dir, 'subconscious-windows.json');
73
+ const document = await readJson(hooksFile, {});
74
+ // Validate all existing user configuration before writing any files.
75
+ const merged = mergeHooks(document, id, installing ? windowsHookCommand(id, script, config) : null);
76
+ let providerFile, providers;
77
+ if (id === 'copilot') {
78
+ const apps = ['Code', 'Code - Insiders', 'VSCodium'];
79
+ const appData = env.APPDATA || path.join(home, 'AppData', 'Roaming');
80
+ let app = options['--vscode-app'] || env.VSCODE_APP;
81
+ if (app && !apps.includes(app)) throw new Error('--vscode-app must be Code, Code - Insiders, or VSCodium');
82
+ if (!app) {
83
+ for (const candidate of apps) if (await exists(path.join(appData, candidate, 'User'))) { app = candidate; break; }
84
+ app ||= 'Code';
85
+ }
86
+ providerFile = path.join(appData, app, 'User', 'chatLanguageModels.json');
87
+ const current = await readJson(providerFile, []);
88
+ if (!Array.isArray(current)) throw new Error(`${providerFile} must be a JSON array; leaving it unchanged`);
89
+ for (const provider of current) object(provider, 'language model provider');
90
+ providers = current.filter(provider => provider.name !== providerName);
91
+ if (installing) {
92
+ const context = positiveInteger(options['--max-input-tokens'] || value(env, 'COPILOT_MAX_INPUT_TOKENS', '5000000'), 'max-input-tokens');
93
+ const maxTokens = positiveInteger(options['--max-output-tokens'] || value(env, 'COPILOT_MAX_OUTPUT_TOKENS', '65536'), 'max-output-tokens');
94
+ providers.push({ name: providerName, vendor: 'customendpoint', apiKey: '${input:chat.lm.secret.subconscious-gateway}', apiType: 'messages', models: modelIds(env).map(id => ({
95
+ id, name: `Subconscious ${id}`, url: `${gatewayUrl}/v1/messages`, toolCalling: true, vision: false,
96
+ maxInputTokens: context, maxOutputTokens: maxTokens, thinking: true, streaming: true,
97
+ requestHeaders: { 'x-subconscious-client': 'copilot' },
98
+ })) });
99
+ }
100
+ }
101
+ if (action === 'status') {
102
+ const hasHook = Object.values(document.hooks || {}).some(entries => entries.some(entry => owned(entry.command) || entry.hooks?.some(hook => owned(hook.command))));
103
+ log(`${id}: ${hasHook ? 'Subconscious hooks installed' : 'no Subconscious hooks'} (${hooksFile})`);
104
+ if (providerFile) log(`Model configuration: ${providerFile}`);
105
+ return 0;
106
+ }
107
+ if (installing) {
108
+ await fs.mkdir(path.dirname(script), { recursive: true });
109
+ await fs.copyFile(new URL('./hook.cjs', import.meta.url), script);
110
+ await writeJson(config, { gatewayUrl, apiKey });
111
+ }
112
+ if (installing || await exists(hooksFile)) await writeJson(hooksFile, merged);
113
+ if (providerFile && (installing || await exists(providerFile))) await writeJson(providerFile, providers);
114
+ if (removing) {
115
+ await fs.rm(script, { force: true });
116
+ await fs.rm(config, { force: true });
117
+ }
118
+ if (installing && id === 'cursor') log(`In Cursor Settings, use ${gatewayUrl}/v1, your API key, and model ${value(env, 'MODEL', defaults.model)}. Restart Cursor to load hooks.`);
119
+ if (installing && id === 'copilot') log('Restart VS Code, select a Subconscious model, and enter your API key when prompted.');
120
+ log(`${id}: Windows integration ${installing ? 'installed' : 'removed'}.`);
121
+ return 0;
122
+ }
123
+ if (id === 'pi') {
124
+ const dir = env.PI_CODING_AGENT_DIR || path.join(home, '.pi', 'agent');
125
+ const modelsFile = path.join(dir, 'models.json');
126
+ const document = object(await readJson(modelsFile, {}), 'Pi models');
127
+ const providers = object(document.providers ?? {}, 'Pi providers');
128
+ if (action === 'status') { log(`Pi: ${providers.subconscious ? 'Subconscious provider installed' : 'not installed'} (${modelsFile})`); return 0; }
129
+ const extension = path.join(dir, 'extensions', 'subconscious-compaction.ts');
130
+ const config = path.join(dir, 'subconscious-windows.json');
131
+ if (installing) {
132
+ const context = positiveInteger(options['--context-window'] || value(env, 'PI_CONTEXT_WINDOW', '5000000'), 'context-window');
133
+ const maxTokens = positiveInteger(options['--max-tokens'] || value(env, 'PI_MAX_TOKENS', '65536'), 'max-tokens');
134
+ providers.subconscious = { baseUrl: `${gatewayUrl}/v1`, api: 'openai-completions', apiKey, headers: { 'x-subconscious-client': 'pi' }, models: modelIds(env).map(id => ({ id, contextWindow: context, maxTokens, compat: { sendSessionAffinityHeaders: true, sessionAffinityFormat: 'openai-nosession' } })) };
135
+ await fs.mkdir(path.dirname(extension), { recursive: true });
136
+ await fs.copyFile(new URL('./pi-compaction.ts', import.meta.url), extension);
137
+ await writeJson(config, { gatewayUrl, apiKey });
138
+ } else {
139
+ delete providers.subconscious;
140
+ await fs.rm(extension, { force: true });
141
+ await fs.rm(config, { force: true });
142
+ }
143
+ if (installing || await exists(modelsFile)) await writeJson(modelsFile, { ...document, providers });
144
+ log(`Pi: Windows integration ${installing ? 'installed' : 'removed'}.`);
145
+ return 0;
146
+ }
147
+ if (id === 'claude-code' || id === 'opencode') {
148
+ const dir = path.join(home, id === 'claude-code' ? '.claude' : '.opencode');
149
+ const legacy = path.join(dir, id === 'claude-code' ? 'subconscious-gateway.env' : 'subconscious.env');
150
+ let present = await exists(legacy);
151
+ if (id === 'opencode') {
152
+ const file = path.join(dir, 'opencode.json');
153
+ const document = object(await readJson(file, {}), 'OpenCode configuration');
154
+ if (document.provider !== undefined) object(document.provider, 'OpenCode providers');
155
+ present ||= Boolean(document.provider?.subconscious);
156
+ if (removing && await exists(file)) {
157
+ if (document.provider) delete document.provider.subconscious;
158
+ if (document.model?.startsWith('subconscious/')) delete document.model;
159
+ await writeJson(file, document);
160
+ }
161
+ if (removing) await fs.rm(path.join(env.XDG_CONFIG_HOME || path.join(home, '.config'), 'opencode', 'plugins', 'subconscious-compaction.ts'), { force: true });
162
+ }
163
+ if (removing) await fs.rm(legacy, { force: true });
164
+ log(`${id}: ${removing ? 'legacy integration removed' : present ? 'legacy integration exists' : 'launch-only; no legacy integration'}.`);
165
+ return 0;
166
+ }
167
+ throw new Error(`No native Windows setup is available for ${id}`);
168
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "subconscious-cli",
3
- "version": "4.0.17",
3
+ "version": "4.1.0-windows.0",
4
4
  "description": "CLI for Subconscious — run Subconscious Code, Claude Code, Codex, OpenCode, DeepSeek Harness, Cursor, Copilot, and Pi",
5
5
  "bin": {
6
6
  "subc": "bin/cli.js"
@@ -9,6 +9,7 @@
9
9
  "build:tui": "node scripts/build-tui.js",
10
10
  "build:tui:host": "node scripts/build-tui.js --host",
11
11
  "test": "node --test",
12
+ "test:windows": "node --test test/windows.test.js test/models.test.js test/auth.test.js test/tui.test.js test/update-check.test.js test/sessions.test.js test/branding.test.js test/claude-version.test.js test/usage.test.js test/release.test.js",
12
13
  "test:tui": "go -C tui test ./...",
13
14
  "prepack": "npm run build:tui",
14
15
  "prepublishOnly": "node scripts/require-publish-script.js"