subconscious-cli 5.0.3 → 6.0.1

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.
@@ -2,21 +2,50 @@ import fs from 'node:fs/promises';
2
2
  import os from 'node:os';
3
3
  import path from 'node:path';
4
4
  import { modelSupportsVision } from '../model-capabilities.js';
5
- import { defaults, origin, value, modelIds, positiveInteger, parseOptions, object, readJson, writeJson } from './common.js';
5
+ import {
6
+ defaults,
7
+ modelIds,
8
+ object,
9
+ origin,
10
+ parseOptions,
11
+ positiveInteger,
12
+ readJson,
13
+ value,
14
+ writeJson,
15
+ } from './common.js';
6
16
 
7
- const owned = command => {
17
+ const owned = (command) => {
8
18
  if (typeof command !== 'string') return false;
9
19
  if (/subconscious-hook\.(?:sh|cjs)/.test(command)) return true;
10
20
  const encoded = /-EncodedCommand ([A-Za-z0-9+/=]+)$/i.exec(command)?.[1];
11
- return Boolean(encoded && Buffer.from(encoded, 'base64').toString('utf16le').startsWith('# SUBC_WINDOWS_HOOK\n'));
21
+ return Boolean(
22
+ encoded &&
23
+ Buffer.from(encoded, 'base64')
24
+ .toString('utf16le')
25
+ .startsWith('# SUBC_WINDOWS_HOOK\n'),
26
+ );
12
27
  };
13
28
  const providerName = 'Subconscious Gateway';
14
- const exists = async file => { try { await fs.access(file); return true; } catch { return false; } };
29
+ const exists = async (file) => {
30
+ try {
31
+ await fs.access(file);
32
+ return true;
33
+ } catch {
34
+ return false;
35
+ }
36
+ };
15
37
 
16
38
  // Hook hosts expect command strings. Keep paths and credentials out of shell
17
39
  // syntax: PowerShell decodes data, then invokes Node with an argv array.
18
- export function windowsHookCommand(client, script, config, node = process.execPath) {
19
- const data = Buffer.from(JSON.stringify({ client, script, config, node })).toString('base64');
40
+ export function windowsHookCommand(
41
+ client,
42
+ script,
43
+ config,
44
+ node = process.execPath,
45
+ ) {
46
+ const data = Buffer.from(
47
+ JSON.stringify({ client, script, config, node }),
48
+ ).toString('base64');
20
49
  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`;
21
50
  return `powershell.exe -NoLogo -NoProfile -NonInteractive -EncodedCommand ${Buffer.from(source, 'utf16le').toString('base64')}`;
22
51
  }
@@ -25,26 +54,45 @@ export function mergeHooks(document, client, command) {
25
54
  const result = structuredClone(object(document, 'hooks configuration'));
26
55
  const hooks = object(result.hooks ?? {}, 'hooks');
27
56
  for (const [event, entries] of Object.entries(hooks)) {
28
- if (!Array.isArray(entries)) throw new Error(`hooks.${event} must be an array; leaving it unchanged`);
57
+ if (!Array.isArray(entries))
58
+ throw new Error(`hooks.${event} must be an array; leaving it unchanged`);
29
59
  const kept = [];
30
60
  for (const entry of entries) {
31
61
  object(entry, `hooks.${event} entry`);
32
62
  if (Array.isArray(entry.hooks)) {
33
63
  for (const hook of entry.hooks) object(hook, `hooks.${event} command`);
34
- const filtered = entry.hooks.filter(hook => !owned(hook.command));
35
- if (filtered.length || entry.hooks.length === 0) kept.push({ ...entry, hooks: filtered });
64
+ const filtered = entry.hooks.filter((hook) => !owned(hook.command));
65
+ if (filtered.length || entry.hooks.length === 0)
66
+ kept.push({ ...entry, hooks: filtered });
36
67
  } else if (entry.hooks !== undefined) {
37
- throw new Error(`hooks.${event}.hooks must be an array; leaving it unchanged`);
68
+ throw new Error(
69
+ `hooks.${event}.hooks must be an array; leaving it unchanged`,
70
+ );
38
71
  } else if (!owned(entry.command)) kept.push(entry);
39
72
  }
40
73
  if (kept.length) hooks[event] = kept;
41
74
  else delete hooks[event];
42
75
  }
43
76
  if (command) {
44
- const events = { codex: ['PreCompact', 'PostCompact'], cursor: ['beforeSubmitPrompt', 'preCompact'], copilot: ['UserPromptSubmit', 'PreCompact'] }[client];
77
+ const events = {
78
+ codex: ['PreCompact', 'PostCompact'],
79
+ cursor: ['beforeSubmitPrompt', 'preCompact'],
80
+ copilot: ['UserPromptSubmit', 'PreCompact'],
81
+ }[client];
45
82
  for (const event of events) {
46
- const hook = { type: 'command', command, timeout: client === 'copilot' ? 5 : 3 };
47
- const entry = client === 'codex' ? { hooks: [{ ...hook, statusMessage: 'Reporting Subconscious compaction' }] } : hook;
83
+ const hook = {
84
+ type: 'command',
85
+ command,
86
+ timeout: client === 'copilot' ? 5 : 3,
87
+ };
88
+ const entry =
89
+ client === 'codex'
90
+ ? {
91
+ hooks: [
92
+ { ...hook, statusMessage: 'Reporting Subconscious compaction' },
93
+ ],
94
+ }
95
+ : hook;
48
96
  hooks[event] = [...(hooks[event] || []), entry];
49
97
  }
50
98
  }
@@ -53,55 +101,124 @@ export function mergeHooks(document, client, command) {
53
101
  return result;
54
102
  }
55
103
 
56
- export async function windowsSetup(id, action, argv, environment, { home = os.homedir(), log = console.log } = {}) {
104
+ export async function windowsSetup(
105
+ id,
106
+ action,
107
+ argv,
108
+ environment,
109
+ { home = os.homedir(), log = console.log } = {},
110
+ ) {
57
111
  const schema = { '--gateway-url': 1, '--api-key': 1, '--model': 1 };
58
- if (id === 'copilot') Object.assign(schema, { '--vscode-app': 1, '--max-input-tokens': 1, '--max-output-tokens': 1 });
59
- if (id === 'pi') Object.assign(schema, { '--context-window': 1, '--max-tokens': 1 });
112
+ if (id === 'copilot')
113
+ Object.assign(schema, {
114
+ '--vscode-app': 1,
115
+ '--max-input-tokens': 1,
116
+ '--max-output-tokens': 1,
117
+ });
118
+ if (id === 'pi')
119
+ Object.assign(schema, { '--context-window': 1, '--max-tokens': 1 });
60
120
  const { options } = parseOptions(argv, schema, { strict: true });
61
- const env = { ...environment, ...(options['--model'] ? { MODEL: options['--model'] } : {}) };
62
- const gatewayUrl = origin(options['--gateway-url'] || value(env, 'GATEWAY_URL', defaults.baseUrl));
121
+ const env = {
122
+ ...environment,
123
+ ...(options['--model'] ? { MODEL: options['--model'] } : {}),
124
+ };
125
+ const gatewayUrl = origin(
126
+ options['--gateway-url'] || value(env, 'GATEWAY_URL', defaults.baseUrl),
127
+ );
63
128
  const apiKey = options['--api-key'] || value(env, 'API_KEY');
64
129
  const installing = action === 'install';
65
130
  const removing = action === 'uninstall';
66
131
  const needsKey = ['codex', 'cursor', 'copilot', 'pi'].includes(id);
67
- if (installing && needsKey && !apiKey) throw new Error('An API key is required for setup. Run subc login first.');
132
+ if (installing && needsKey && !apiKey)
133
+ throw new Error('An API key is required for setup. Run subc login first.');
68
134
 
69
135
  if (['codex', 'cursor', 'copilot'].includes(id)) {
70
- const dir = id === 'codex' ? (env.CODEX_DIR || env.CODEX_HOME || path.join(home, '.codex')) : path.join(home, `.${id}`);
71
- const hooksFile = id === 'copilot' ? path.join(dir, 'hooks', 'subconscious-hooks.json') : path.join(dir, 'hooks.json');
136
+ const dir =
137
+ id === 'codex'
138
+ ? env.CODEX_DIR || env.CODEX_HOME || path.join(home, '.codex')
139
+ : path.join(home, `.${id}`);
140
+ const hooksFile =
141
+ id === 'copilot'
142
+ ? path.join(dir, 'hooks', 'subconscious-hooks.json')
143
+ : path.join(dir, 'hooks.json');
72
144
  const script = path.join(dir, 'hooks', 'subconscious-hook.cjs');
73
145
  const config = path.join(dir, 'subconscious-windows.json');
74
146
  const document = await readJson(hooksFile, {});
75
147
  // Validate all existing user configuration before writing any files.
76
- const merged = mergeHooks(document, id, installing ? windowsHookCommand(id, script, config) : null);
148
+ const merged = mergeHooks(
149
+ document,
150
+ id,
151
+ installing ? windowsHookCommand(id, script, config) : null,
152
+ );
77
153
  let providerFile, providers;
78
154
  if (id === 'copilot') {
79
155
  const apps = ['Code', 'Code - Insiders', 'VSCodium'];
80
156
  const appData = env.APPDATA || path.join(home, 'AppData', 'Roaming');
81
157
  let app = options['--vscode-app'] || env.VSCODE_APP;
82
- if (app && !apps.includes(app)) throw new Error('--vscode-app must be Code, Code - Insiders, or VSCodium');
158
+ if (app && !apps.includes(app))
159
+ throw new Error(
160
+ '--vscode-app must be Code, Code - Insiders, or VSCodium',
161
+ );
83
162
  if (!app) {
84
- for (const candidate of apps) if (await exists(path.join(appData, candidate, 'User'))) { app = candidate; break; }
163
+ for (const candidate of apps)
164
+ if (await exists(path.join(appData, candidate, 'User'))) {
165
+ app = candidate;
166
+ break;
167
+ }
85
168
  app ||= 'Code';
86
169
  }
87
170
  providerFile = path.join(appData, app, 'User', 'chatLanguageModels.json');
88
171
  const current = await readJson(providerFile, []);
89
- if (!Array.isArray(current)) throw new Error(`${providerFile} must be a JSON array; leaving it unchanged`);
90
- for (const provider of current) object(provider, 'language model provider');
91
- providers = current.filter(provider => provider.name !== providerName);
172
+ if (!Array.isArray(current))
173
+ throw new Error(
174
+ `${providerFile} must be a JSON array; leaving it unchanged`,
175
+ );
176
+ for (const provider of current)
177
+ object(provider, 'language model provider');
178
+ providers = current.filter((provider) => provider.name !== providerName);
92
179
  if (installing) {
93
- const context = positiveInteger(options['--max-input-tokens'] || value(env, 'COPILOT_MAX_INPUT_TOKENS', '5000000'), 'max-input-tokens');
94
- const maxTokens = positiveInteger(options['--max-output-tokens'] || value(env, 'COPILOT_MAX_OUTPUT_TOKENS', '65536'), 'max-output-tokens');
95
- providers.push({ name: providerName, vendor: 'customendpoint', apiKey: '${input:chat.lm.secret.subconscious-gateway}', apiType: 'messages', models: modelIds(env).map(id => ({
96
- id, name: `Subconscious ${id}`, url: `${gatewayUrl}/v1/messages`, toolCalling: true, vision: modelSupportsVision(id),
97
- maxInputTokens: context, maxOutputTokens: maxTokens, thinking: true, streaming: true,
98
- requestHeaders: { 'x-subconscious-client': 'copilot' },
99
- })) });
180
+ const context = positiveInteger(
181
+ options['--max-input-tokens'] ||
182
+ value(env, 'COPILOT_MAX_INPUT_TOKENS', '5000000'),
183
+ 'max-input-tokens',
184
+ );
185
+ const maxTokens = positiveInteger(
186
+ options['--max-output-tokens'] ||
187
+ value(env, 'COPILOT_MAX_OUTPUT_TOKENS', '65536'),
188
+ 'max-output-tokens',
189
+ );
190
+ providers.push({
191
+ name: providerName,
192
+ vendor: 'customendpoint',
193
+ // biome-ignore lint/suspicious/noTemplateCurlyInString: VS Code secret input, not a JS template
194
+ apiKey: '${input:chat.lm.secret.subconscious-gateway}',
195
+ apiType: 'messages',
196
+ models: modelIds(env).map((id) => ({
197
+ id,
198
+ name: `Subconscious ${id}`,
199
+ url: `${gatewayUrl}/v1/messages`,
200
+ toolCalling: true,
201
+ vision: modelSupportsVision(id),
202
+ maxInputTokens: context,
203
+ maxOutputTokens: maxTokens,
204
+ thinking: true,
205
+ streaming: true,
206
+ requestHeaders: { 'x-subconscious-client': 'copilot' },
207
+ })),
208
+ });
100
209
  }
101
210
  }
102
211
  if (action === 'status') {
103
- const hasHook = Object.values(document.hooks || {}).some(entries => entries.some(entry => owned(entry.command) || entry.hooks?.some(hook => owned(hook.command))));
104
- log(`${id}: ${hasHook ? 'Subconscious hooks installed' : 'no Subconscious hooks'} (${hooksFile})`);
212
+ const hasHook = Object.values(document.hooks || {}).some((entries) =>
213
+ entries.some(
214
+ (entry) =>
215
+ owned(entry.command) ||
216
+ entry.hooks?.some((hook) => owned(hook.command)),
217
+ ),
218
+ );
219
+ log(
220
+ `${id}: ${hasHook ? 'Subconscious hooks installed' : 'no Subconscious hooks'} (${hooksFile})`,
221
+ );
105
222
  if (providerFile) log(`Model configuration: ${providerFile}`);
106
223
  return 0;
107
224
  }
@@ -110,14 +227,22 @@ export async function windowsSetup(id, action, argv, environment, { home = os.ho
110
227
  await fs.copyFile(new URL('./hook.cjs', import.meta.url), script);
111
228
  await writeJson(config, { gatewayUrl, apiKey });
112
229
  }
113
- if (installing || await exists(hooksFile)) await writeJson(hooksFile, merged);
114
- if (providerFile && (installing || await exists(providerFile))) await writeJson(providerFile, providers);
230
+ if (installing || (await exists(hooksFile)))
231
+ await writeJson(hooksFile, merged);
232
+ if (providerFile && (installing || (await exists(providerFile))))
233
+ await writeJson(providerFile, providers);
115
234
  if (removing) {
116
235
  await fs.rm(script, { force: true });
117
236
  await fs.rm(config, { force: true });
118
237
  }
119
- 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.`);
120
- if (installing && id === 'copilot') log('Restart VS Code, select a Subconscious model, and enter your API key when prompted.');
238
+ if (installing && id === 'cursor')
239
+ log(
240
+ `In Cursor Settings, use ${gatewayUrl}/v1, your API key, and model ${value(env, 'MODEL', defaults.model)}. Restart Cursor to load hooks.`,
241
+ );
242
+ if (installing && id === 'copilot')
243
+ log(
244
+ 'Restart VS Code, select a Subconscious model, and enter your API key when prompted.',
245
+ );
121
246
  log(`${id}: Windows integration ${installing ? 'installed' : 'removed'}.`);
122
247
  return 0;
123
248
  }
@@ -126,46 +251,96 @@ export async function windowsSetup(id, action, argv, environment, { home = os.ho
126
251
  const modelsFile = path.join(dir, 'models.json');
127
252
  const document = object(await readJson(modelsFile, {}), 'Pi models');
128
253
  const providers = object(document.providers ?? {}, 'Pi providers');
129
- if (action === 'status') { log(`Pi: ${providers.subconscious ? 'Subconscious provider installed' : 'not installed'} (${modelsFile})`); return 0; }
130
- const extension = path.join(dir, 'extensions', 'subconscious-compaction.ts');
254
+ if (action === 'status') {
255
+ log(
256
+ `Pi: ${providers.subconscious ? 'Subconscious provider installed' : 'not installed'} (${modelsFile})`,
257
+ );
258
+ return 0;
259
+ }
260
+ const extension = path.join(
261
+ dir,
262
+ 'extensions',
263
+ 'subconscious-compaction.ts',
264
+ );
131
265
  const config = path.join(dir, 'subconscious-windows.json');
132
266
  if (installing) {
133
- const context = positiveInteger(options['--context-window'] || value(env, 'PI_CONTEXT_WINDOW', '5000000'), 'context-window');
134
- const maxTokens = positiveInteger(options['--max-tokens'] || value(env, 'PI_MAX_TOKENS', '65536'), 'max-tokens');
135
- providers.subconscious = { baseUrl: `${gatewayUrl}/v1`, api: 'openai-completions', apiKey, headers: { 'x-subconscious-client': 'pi' }, models: modelIds(env).map(id => ({
136
- id, contextWindow: context, maxTokens, compat: { sendSessionAffinityHeaders: true, sessionAffinityFormat: 'openai-nosession' },
137
- ...(modelSupportsVision(id) ? { input: ['text', 'image'] } : {}),
138
- })) };
267
+ const context = positiveInteger(
268
+ options['--context-window'] ||
269
+ value(env, 'PI_CONTEXT_WINDOW', '5000000'),
270
+ 'context-window',
271
+ );
272
+ const maxTokens = positiveInteger(
273
+ options['--max-tokens'] || value(env, 'PI_MAX_TOKENS', '65536'),
274
+ 'max-tokens',
275
+ );
276
+ providers.subconscious = {
277
+ baseUrl: `${gatewayUrl}/v1`,
278
+ api: 'openai-completions',
279
+ apiKey,
280
+ headers: { 'x-subconscious-client': 'pi' },
281
+ models: modelIds(env).map((id) => ({
282
+ id,
283
+ contextWindow: context,
284
+ maxTokens,
285
+ compat: {
286
+ sendSessionAffinityHeaders: true,
287
+ sessionAffinityFormat: 'openai-nosession',
288
+ },
289
+ ...(modelSupportsVision(id) ? { input: ['text', 'image'] } : {}),
290
+ })),
291
+ };
139
292
  await fs.mkdir(path.dirname(extension), { recursive: true });
140
- await fs.copyFile(new URL('./pi-compaction.ts', import.meta.url), extension);
293
+ await fs.copyFile(
294
+ new URL('./pi-compaction.ts', import.meta.url),
295
+ extension,
296
+ );
141
297
  await writeJson(config, { gatewayUrl, apiKey });
142
298
  } else {
143
299
  delete providers.subconscious;
144
300
  await fs.rm(extension, { force: true });
145
301
  await fs.rm(config, { force: true });
146
302
  }
147
- if (installing || await exists(modelsFile)) await writeJson(modelsFile, { ...document, providers });
303
+ if (installing || (await exists(modelsFile)))
304
+ await writeJson(modelsFile, { ...document, providers });
148
305
  log(`Pi: Windows integration ${installing ? 'installed' : 'removed'}.`);
149
306
  return 0;
150
307
  }
151
308
  if (id === 'claude-code' || id === 'opencode') {
152
309
  const dir = path.join(home, id === 'claude-code' ? '.claude' : '.opencode');
153
- const legacy = path.join(dir, id === 'claude-code' ? 'subconscious-gateway.env' : 'subconscious.env');
310
+ const legacy = path.join(
311
+ dir,
312
+ id === 'claude-code' ? 'subconscious-gateway.env' : 'subconscious.env',
313
+ );
154
314
  let present = await exists(legacy);
155
315
  if (id === 'opencode') {
156
316
  const file = path.join(dir, 'opencode.json');
157
- const document = object(await readJson(file, {}), 'OpenCode configuration');
158
- if (document.provider !== undefined) object(document.provider, 'OpenCode providers');
317
+ const document = object(
318
+ await readJson(file, {}),
319
+ 'OpenCode configuration',
320
+ );
321
+ if (document.provider !== undefined)
322
+ object(document.provider, 'OpenCode providers');
159
323
  present ||= Boolean(document.provider?.subconscious);
160
- if (removing && await exists(file)) {
324
+ if (removing && (await exists(file))) {
161
325
  if (document.provider) delete document.provider.subconscious;
162
326
  if (document.model?.startsWith('subconscious/')) delete document.model;
163
327
  await writeJson(file, document);
164
328
  }
165
- if (removing) await fs.rm(path.join(env.XDG_CONFIG_HOME || path.join(home, '.config'), 'opencode', 'plugins', 'subconscious-compaction.ts'), { force: true });
329
+ if (removing)
330
+ await fs.rm(
331
+ path.join(
332
+ env.XDG_CONFIG_HOME || path.join(home, '.config'),
333
+ 'opencode',
334
+ 'plugins',
335
+ 'subconscious-compaction.ts',
336
+ ),
337
+ { force: true },
338
+ );
166
339
  }
167
340
  if (removing) await fs.rm(legacy, { force: true });
168
- log(`${id}: ${removing ? 'legacy integration removed' : present ? 'legacy integration exists' : 'launch-only; no legacy integration'}.`);
341
+ log(
342
+ `${id}: ${removing ? 'legacy integration removed' : present ? 'legacy integration exists' : 'launch-only; no legacy integration'}.`,
343
+ );
169
344
  return 0;
170
345
  }
171
346
  throw new Error(`No native Windows setup is available for ${id}`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "subconscious-cli",
3
- "version": "5.0.3",
3
+ "version": "6.0.1",
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,8 +9,13 @@
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/model-capabilities.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/feedback.test.js test/release.test.js",
12
+ "test:windows": "node --test test/windows.test.js test/models.test.js test/model-capabilities.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/feedback.test.js test/release.test.js test/github-govuln-issue.test.js",
13
13
  "test:tui": "go -C tui test ./...",
14
+ "generate": "node scripts/generate-agents.js",
15
+ "lint": "biome ci .",
16
+ "format": "biome check --write .",
17
+ "typecheck": "tsc --noEmit",
18
+ "prepare": "node scripts/install-git-hooks.js",
14
19
  "prepack": "npm run build:tui",
15
20
  "prepublishOnly": "node scripts/require-publish-script.js"
16
21
  },
@@ -23,8 +28,7 @@
23
28
  },
24
29
  "repository": {
25
30
  "type": "git",
26
- "url": "git+https://github.com/subconscious-systems/subconscious.git",
27
- "directory": "cli"
31
+ "url": "git+https://github.com/subconscious-systems/subconscious.git"
28
32
  },
29
33
  "keywords": [
30
34
  "subconscious",
@@ -45,5 +49,16 @@
45
49
  "launcher"
46
50
  ],
47
51
  "author": "Subconscious Systems",
48
- "license": "MIT"
49
- }
52
+ "license": "MIT",
53
+ "simple-git-hooks": {
54
+ "commit-msg": "npx --no-install commitlint --edit $1"
55
+ },
56
+ "devDependencies": {
57
+ "@biomejs/biome": "^2.5.14",
58
+ "@commitlint/cli": "^21.2.3",
59
+ "@commitlint/config-conventional": "^21.2.3",
60
+ "@types/node": "^26.6.2",
61
+ "simple-git-hooks": "^2.14.0",
62
+ "typescript": "^7.0.2"
63
+ }
64
+ }