subconscious-cli 6.0.0 → 6.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +135 -2
- package/bin/agent-data.js +242 -0
- package/bin/agent-help.js +106 -0
- package/bin/agent-install.js +354 -0
- package/bin/agent-launch.js +199 -0
- package/bin/agent-spawn.js +137 -0
- package/bin/agents.js +284 -840
- package/bin/auth.js +89 -33
- package/bin/branding.js +3 -1
- package/bin/claude-picker.js +143 -0
- package/bin/cli.js +87 -20
- package/bin/clipboard-image.js +24 -5
- package/bin/colors.js +1 -1
- package/bin/feedback.js +71 -21
- package/bin/harness-manifest.js +286 -0
- package/bin/headless-args.js +35 -0
- package/bin/launch-values.js +25 -0
- package/bin/model-capabilities.js +12 -8
- package/bin/models.js +35 -19
- package/bin/native/subc-tui-darwin-amd64 +0 -0
- package/bin/native/subc-tui-darwin-arm64 +0 -0
- package/bin/native/subc-tui-linux-amd64 +0 -0
- package/bin/native/subc-tui-linux-arm64 +0 -0
- package/bin/native/subc-tui-windows-amd64.exe +0 -0
- package/bin/native/subc-tui-windows-arm64.exe +0 -0
- package/bin/opencode-provider.js +48 -5
- package/bin/profile-settings.js +129 -0
- package/bin/profiles.js +136 -449
- package/bin/runbook/README.md +66 -23
- package/bin/runbook/claude-code/agent.json +300 -0
- package/bin/runbook/claude-code/install.sh +9 -252
- package/bin/runbook/codex/agent.json +461 -0
- package/bin/runbook/codex/hooks-lib.sh +17 -4
- package/bin/runbook/codex/install.sh +0 -4
- package/bin/runbook/codex/run.sh +16 -235
- package/bin/runbook/copilot/agent.json +185 -0
- package/bin/runbook/copilot/install.sh +45 -63
- package/bin/runbook/cursor/agent.json +137 -0
- package/bin/runbook/cursor/install.sh +8 -35
- package/bin/runbook/deepseek-harness/agent.json +163 -0
- package/bin/runbook/deepseek-harness/run.sh +20 -59
- package/bin/runbook/image-window/index.ts +14 -0
- package/bin/runbook/image-window/window.d.ts +19 -0
- package/bin/runbook/image-window/window.js +140 -0
- package/bin/runbook/lib.sh +41 -0
- package/bin/runbook/opencode/agent.json +158 -0
- package/bin/runbook/opencode/install.sh +9 -183
- package/bin/runbook/opencode/opencode-plugin.d.ts +16 -0
- package/bin/runbook/opencode/subconscious-image-window.ts +24 -0
- package/bin/runbook/pi/agent.json +216 -0
- package/bin/runbook/pi/install.sh +18 -44
- package/bin/runbook/pi/run.sh +6 -18
- package/bin/runbook/pi/subconscious-compaction.ts +71 -49
- package/bin/runbook/shared.json +58 -0
- package/bin/runbook/subconscious-code/agent.json +156 -0
- package/bin/sessions.js +173 -58
- package/bin/tui.js +104 -32
- package/bin/update-check.js +54 -21
- package/bin/upgrade.js +67 -43
- package/bin/usage.js +47 -24
- package/bin/windows/agents.js +141 -37
- package/bin/windows/common.js +36 -25
- package/bin/windows/install.js +116 -34
- package/bin/windows/launch.js +260 -77
- package/bin/windows/pi-compaction.ts +68 -13
- package/bin/windows/process.js +210 -57
- package/bin/windows/setup.js +251 -56
- package/package.json +9 -4
- package/bin/registry.generated.json +0 -406
- package/bin/runbook/claude-code/run.sh +0 -254
- package/bin/runbook/model-capabilities.generated.sh +0 -9
- package/bin/runbook/opencode/provider.sh +0 -93
- package/bin/runbook/opencode/run.sh +0 -99
- package/bin/runbook/opencode/subconscious-compaction.ts +0 -100
- package/bin/runbook/subconscious-code/run.sh +0 -21
- package/bin/windows/claude.js +0 -71
package/bin/auth.js
CHANGED
|
@@ -12,16 +12,23 @@
|
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
14
|
import { exec } from 'node:child_process';
|
|
15
|
-
import { openWindowsBrowser } from './windows/process.js';
|
|
16
15
|
import fs from 'node:fs/promises';
|
|
17
16
|
import os from 'node:os';
|
|
18
17
|
import path from 'node:path';
|
|
19
18
|
import { c } from './colors.js';
|
|
20
|
-
import {
|
|
19
|
+
import {
|
|
20
|
+
clearProfileApiKey,
|
|
21
|
+
DEFAULT_PROFILE,
|
|
22
|
+
ensureProfile,
|
|
23
|
+
} from './profiles.js';
|
|
21
24
|
import { printLoginUpgradeWarning } from './upgrade.js';
|
|
25
|
+
import { openWindowsBrowser } from './windows/process.js';
|
|
22
26
|
|
|
23
27
|
function configDir() {
|
|
24
|
-
return
|
|
28
|
+
return (
|
|
29
|
+
process.env.SUBC_CONFIG_DIR?.trim() ||
|
|
30
|
+
path.join(os.homedir(), '.subconscious')
|
|
31
|
+
);
|
|
25
32
|
}
|
|
26
33
|
|
|
27
34
|
function configFile() {
|
|
@@ -78,7 +85,10 @@ export async function getApiKey(profile) {
|
|
|
78
85
|
|
|
79
86
|
const config = await loadConfig();
|
|
80
87
|
if (config.subconscious_api_key) {
|
|
81
|
-
return {
|
|
88
|
+
return {
|
|
89
|
+
key: config.subconscious_api_key,
|
|
90
|
+
source: '~/.subconscious/config.json',
|
|
91
|
+
};
|
|
82
92
|
}
|
|
83
93
|
return null;
|
|
84
94
|
}
|
|
@@ -87,9 +97,11 @@ export async function getApiKey(profile) {
|
|
|
87
97
|
|
|
88
98
|
function openBrowser(url) {
|
|
89
99
|
if (process.platform === 'win32') {
|
|
90
|
-
void openWindowsBrowser(url)
|
|
91
|
-
|
|
92
|
-
|
|
100
|
+
void openWindowsBrowser(url)
|
|
101
|
+
.then((code) => {
|
|
102
|
+
if (code) throw new Error('Browser could not be opened');
|
|
103
|
+
})
|
|
104
|
+
.catch(() => console.log(`Please open this URL manually:\n\n ${url}\n`));
|
|
93
105
|
return;
|
|
94
106
|
}
|
|
95
107
|
const cmd =
|
|
@@ -131,7 +143,11 @@ export async function registerDeviceLogin(platformUrl, fetchImpl = fetch) {
|
|
|
131
143
|
return data;
|
|
132
144
|
}
|
|
133
145
|
|
|
134
|
-
export async function pollDeviceLogin(
|
|
146
|
+
export async function pollDeviceLogin(
|
|
147
|
+
platformUrl,
|
|
148
|
+
deviceCode,
|
|
149
|
+
fetchImpl = fetch,
|
|
150
|
+
) {
|
|
135
151
|
const res = await fetchImpl(`${platformUrl}/api/cli/device/token`, {
|
|
136
152
|
method: 'POST',
|
|
137
153
|
headers: { 'Content-Type': 'application/json' },
|
|
@@ -151,7 +167,9 @@ function printDeviceInstructions(platformUrl, userCode) {
|
|
|
151
167
|
console.log(` ${c.dim}Opening browser to sign in...${c.reset}`);
|
|
152
168
|
console.log();
|
|
153
169
|
console.log(` ${c.dim}If a window doesn't open, go to:${c.reset}`);
|
|
154
|
-
console.log(
|
|
170
|
+
console.log(
|
|
171
|
+
` ${c.underline}${c.cyan}${terminalLink(verificationUrl)}${c.reset}`,
|
|
172
|
+
);
|
|
155
173
|
console.log();
|
|
156
174
|
console.log(
|
|
157
175
|
` ${c.dim}Or open ${c.reset}${c.underline}${c.cyan}${terminalLink(deviceUrl)}${c.reset}${c.dim} and enter:${c.reset} ${c.bold}${userCode}${c.reset}`,
|
|
@@ -161,7 +179,9 @@ function printDeviceInstructions(platformUrl, userCode) {
|
|
|
161
179
|
}
|
|
162
180
|
|
|
163
181
|
function printLoginFallback() {
|
|
164
|
-
console.error(
|
|
182
|
+
console.error(
|
|
183
|
+
` ${c.dim}You can also copy an API key from the dashboard and run${c.reset} ${c.cyan}subc update-key <your-api-key>${c.reset}${c.dim}.${c.reset}`,
|
|
184
|
+
);
|
|
165
185
|
}
|
|
166
186
|
|
|
167
187
|
async function saveLoginKey(profileName, token) {
|
|
@@ -181,17 +201,23 @@ export async function loginCommand(_argv = [], options = {}) {
|
|
|
181
201
|
if (existing) {
|
|
182
202
|
const profile = await ensureProfile(profileName, existing.key);
|
|
183
203
|
const logout =
|
|
184
|
-
profileName === DEFAULT_PROFILE
|
|
185
|
-
|
|
204
|
+
profileName === DEFAULT_PROFILE
|
|
205
|
+
? 'subc logout'
|
|
206
|
+
: `subc --profile ${profileName} logout`;
|
|
207
|
+
const masked = `${existing.key.slice(0, 8)}...${existing.key.slice(-4)}`;
|
|
186
208
|
console.log(`\n${c.yellow}Already logged in.${c.reset}`);
|
|
187
209
|
console.log(` Key: ${c.dim}${masked}${c.reset}`);
|
|
188
210
|
console.log(` Profile: ${c.dim}${profile.path}${c.reset}`);
|
|
189
|
-
console.log(
|
|
211
|
+
console.log(
|
|
212
|
+
`\n Run ${c.cyan}${logout}${c.reset} first to switch accounts.\n`,
|
|
213
|
+
);
|
|
190
214
|
return;
|
|
191
215
|
}
|
|
192
216
|
|
|
193
217
|
console.log();
|
|
194
|
-
console.log(
|
|
218
|
+
console.log(
|
|
219
|
+
` ${c.magenta}${c.bold}Subconscious${c.reset} ${c.dim}— CLI Login${c.reset}`,
|
|
220
|
+
);
|
|
195
221
|
console.log();
|
|
196
222
|
|
|
197
223
|
const platformUrl = getPlatformUrl(options.profile);
|
|
@@ -206,7 +232,10 @@ export async function loginCommand(_argv = [], options = {}) {
|
|
|
206
232
|
return;
|
|
207
233
|
}
|
|
208
234
|
|
|
209
|
-
const verificationUrl = printDeviceInstructions(
|
|
235
|
+
const verificationUrl = printDeviceInstructions(
|
|
236
|
+
platformUrl,
|
|
237
|
+
registered.user_code,
|
|
238
|
+
);
|
|
210
239
|
(options.openBrowser || openBrowser)(verificationUrl);
|
|
211
240
|
|
|
212
241
|
const frames = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
|
|
@@ -229,35 +258,47 @@ export async function loginCommand(_argv = [], options = {}) {
|
|
|
229
258
|
if (Date.now() - started > DEVICE_POLL_TIMEOUT_MS) {
|
|
230
259
|
throw new Error('Authentication timed out (5 min). Please try again.');
|
|
231
260
|
}
|
|
232
|
-
const polled = await pollDeviceLogin(
|
|
261
|
+
const polled = await pollDeviceLogin(
|
|
262
|
+
platformUrl,
|
|
263
|
+
registered.device_code,
|
|
264
|
+
fetchImpl,
|
|
265
|
+
);
|
|
233
266
|
if (polled.status === 'approved') {
|
|
234
267
|
clearInterval(spinner);
|
|
235
|
-
process.stdout.write(
|
|
268
|
+
process.stdout.write(`\r${' '.repeat(50)}\r`);
|
|
236
269
|
const profile = await saveLoginKey(profileName, polled.key);
|
|
237
|
-
const masked = polled.key.slice(0, 8)
|
|
270
|
+
const masked = `${polled.key.slice(0, 8)}...${polled.key.slice(-4)}`;
|
|
238
271
|
console.log(` ${c.green}${c.bold}✓ Logged in successfully!${c.reset}`);
|
|
239
272
|
console.log(` ${c.dim}Key: ${masked}${c.reset}`);
|
|
240
273
|
if (profileName === DEFAULT_PROFILE) {
|
|
241
|
-
console.log(
|
|
274
|
+
console.log(
|
|
275
|
+
` ${c.dim}Saved to ~/.subconscious/config.json${c.reset}`,
|
|
276
|
+
);
|
|
242
277
|
}
|
|
243
278
|
console.log(` ${c.dim}Runbook profile: ${profile.path}${c.reset}`);
|
|
244
|
-
console.log(
|
|
245
|
-
|
|
279
|
+
console.log(
|
|
280
|
+
` ${c.dim}Launch a terminal agent with ${c.reset}${c.cyan}subc claude${c.reset}${c.dim}, or install editor hooks with ${c.reset}${c.cyan}subc cursor install${c.reset}${c.dim}.${c.reset}`,
|
|
281
|
+
);
|
|
282
|
+
console.log(
|
|
283
|
+
` ${c.dim}Pi needs ${c.reset}${c.cyan}subc pi install${c.reset}${c.dim} first. List profiles with ${c.reset}${c.cyan}subc config${c.reset}${c.dim}.${c.reset}`,
|
|
284
|
+
);
|
|
246
285
|
console.log();
|
|
247
286
|
return;
|
|
248
287
|
}
|
|
249
288
|
if (polled.status === 'expired') {
|
|
250
289
|
throw new Error('Login code expired. Please try again.');
|
|
251
290
|
}
|
|
252
|
-
await new Promise((resolve) =>
|
|
291
|
+
await new Promise((resolve) =>
|
|
292
|
+
setTimeout(resolve, DEVICE_POLL_INTERVAL_MS),
|
|
293
|
+
);
|
|
253
294
|
}
|
|
254
295
|
clearInterval(spinner);
|
|
255
|
-
process.stdout.write(
|
|
296
|
+
process.stdout.write(`\r${' '.repeat(50)}\r`);
|
|
256
297
|
console.log(`\n ${c.dim}Login cancelled.${c.reset}\n`);
|
|
257
298
|
process.exitCode = 1;
|
|
258
299
|
} catch (error) {
|
|
259
300
|
clearInterval(spinner);
|
|
260
|
-
process.stdout.write(
|
|
301
|
+
process.stdout.write(`\r${' '.repeat(50)}\r`);
|
|
261
302
|
console.error(` ${c.red}✗ ${error.message}${c.reset}`);
|
|
262
303
|
printLoginFallback();
|
|
263
304
|
console.log();
|
|
@@ -282,7 +323,8 @@ export async function updateApiKeyCommand(argv = [], options = {}) {
|
|
|
282
323
|
await saveConfig(config);
|
|
283
324
|
}
|
|
284
325
|
|
|
285
|
-
const masked =
|
|
326
|
+
const masked =
|
|
327
|
+
key.length <= 12 ? '********' : `${key.slice(0, 8)}...${key.slice(-4)}`;
|
|
286
328
|
console.log(`\n ${c.green}${c.bold}✓ API key updated.${c.reset}`);
|
|
287
329
|
console.log(` ${c.dim}Profile: ${profile.path}${c.reset}`);
|
|
288
330
|
console.log(` ${c.dim}Key: ${masked}${c.reset}`);
|
|
@@ -298,7 +340,8 @@ export async function logoutCommand(_argv = [], options = {}) {
|
|
|
298
340
|
const profileName = options.profileName || DEFAULT_PROFILE;
|
|
299
341
|
const config = await loadConfig();
|
|
300
342
|
const clearedProfile = await clearProfileApiKey(profileName);
|
|
301
|
-
const clearSavedConfig =
|
|
343
|
+
const clearSavedConfig =
|
|
344
|
+
profileName === DEFAULT_PROFILE && config.subconscious_api_key;
|
|
302
345
|
|
|
303
346
|
if (!clearSavedConfig && !clearedProfile) {
|
|
304
347
|
console.log(`\n ${c.dim}Not logged in.${c.reset}\n`);
|
|
@@ -331,16 +374,19 @@ export async function whoamiCommand(_argv = [], options = {}) {
|
|
|
331
374
|
}
|
|
332
375
|
|
|
333
376
|
const { key, source } = auth;
|
|
334
|
-
const masked = key.slice(0, 8)
|
|
377
|
+
const masked = `${key.slice(0, 8)}...${key.slice(-4)}`;
|
|
335
378
|
|
|
336
379
|
console.log();
|
|
337
380
|
|
|
338
381
|
// Validate the key against the server; falls back to offline display if unreachable
|
|
339
382
|
try {
|
|
340
|
-
const res = await fetch(
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
383
|
+
const res = await fetch(
|
|
384
|
+
`${getPlatformUrl(options.profile)}/api/cli/whoami`,
|
|
385
|
+
{
|
|
386
|
+
headers: { Authorization: `Bearer ${key}` },
|
|
387
|
+
signal: AbortSignal.timeout(5000),
|
|
388
|
+
},
|
|
389
|
+
);
|
|
344
390
|
|
|
345
391
|
if (res.status === 404) {
|
|
346
392
|
printLoginUpgradeWarning();
|
|
@@ -355,7 +401,7 @@ export async function whoamiCommand(_argv = [], options = {}) {
|
|
|
355
401
|
}
|
|
356
402
|
console.log(` ${c.dim}Key: ${masked}${c.reset}`);
|
|
357
403
|
console.log(` ${c.dim}Source: ${source}${c.reset}`);
|
|
358
|
-
} else {
|
|
404
|
+
} else if (res.status === 401) {
|
|
359
405
|
console.log(` ${c.red}✗ Key is invalid or revoked${c.reset}`);
|
|
360
406
|
console.log(` ${c.dim}Key: ${masked}${c.reset}`);
|
|
361
407
|
console.log(` ${c.dim}Source: ${source}${c.reset}`);
|
|
@@ -363,9 +409,19 @@ export async function whoamiCommand(_argv = [], options = {}) {
|
|
|
363
409
|
console.log(
|
|
364
410
|
` Run ${c.cyan}subc ${profileFlag}logout${c.reset} then ${c.cyan}subc ${profileFlag}login${c.reset} to re-authenticate.`,
|
|
365
411
|
);
|
|
412
|
+
} else {
|
|
413
|
+
// Only a 401 means the key was rejected. A 5xx or 429 says nothing
|
|
414
|
+
// about the key, so don't tell the user to log out.
|
|
415
|
+
console.log(
|
|
416
|
+
` ${c.yellow}Could not verify the key (platform returned HTTP ${res.status})${c.reset}`,
|
|
417
|
+
);
|
|
418
|
+
console.log(` ${c.dim}Key: ${masked}${c.reset}`);
|
|
419
|
+
console.log(` ${c.dim}Source: ${source}${c.reset}`);
|
|
366
420
|
}
|
|
367
421
|
} catch {
|
|
368
|
-
console.log(
|
|
422
|
+
console.log(
|
|
423
|
+
` ${c.green}✓ Authenticated${c.reset} ${c.dim}(offline — key not verified)${c.reset}`,
|
|
424
|
+
);
|
|
369
425
|
console.log(` ${c.dim}Key: ${masked}${c.reset}`);
|
|
370
426
|
console.log(` ${c.dim}Source: ${source}${c.reset}`);
|
|
371
427
|
}
|
package/bin/branding.js
CHANGED
|
@@ -28,6 +28,8 @@ export function renderBanner(options = {}) {
|
|
|
28
28
|
// still get the same logo without ANSI styling.
|
|
29
29
|
if (!isTTY || term === 'dumb') return ` ${title}`;
|
|
30
30
|
|
|
31
|
-
const logo = color
|
|
31
|
+
const logo = color
|
|
32
|
+
? `${BRAND_ORANGE}${LOGO_ART_SMALL}${RESET}`
|
|
33
|
+
: LOGO_ART_SMALL;
|
|
32
34
|
return `${logo}\n\n ${title}`;
|
|
33
35
|
}
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
// The env claudeModelPickerEnv sets, described for readers that cannot run it.
|
|
2
|
+
export const CLAUDE_PICKER_ENV_DESCRIPTION = [
|
|
3
|
+
...['OPUS', 'SONNET', 'HAIKU', 'FABLE'].flatMap((role, index) => {
|
|
4
|
+
const alias = role.toLowerCase();
|
|
5
|
+
return [
|
|
6
|
+
{
|
|
7
|
+
name: `ANTHROPIC_DEFAULT_${role}_MODEL`,
|
|
8
|
+
value: `{catalog[${index}]}`,
|
|
9
|
+
override: [`ANTHROPIC_DEFAULT_${role}_MODEL`],
|
|
10
|
+
description: `Model behind the ${alias} alias. Set from the catalog, padded to four entries; a profile or environment value is kept only when it is in the live catalog.`,
|
|
11
|
+
},
|
|
12
|
+
{
|
|
13
|
+
name: `ANTHROPIC_DEFAULT_${role}_MODEL_NAME`,
|
|
14
|
+
value: `{catalog[${index}]}`,
|
|
15
|
+
override: [`ANTHROPIC_DEFAULT_${role}_MODEL_NAME`],
|
|
16
|
+
description: `Picker label for the ${alias} alias.`,
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
name: `ANTHROPIC_DEFAULT_${role}_MODEL_DESCRIPTION`,
|
|
20
|
+
value: `Subconscious model {catalog[${index}]}`,
|
|
21
|
+
override: [`ANTHROPIC_DEFAULT_${role}_MODEL_DESCRIPTION`],
|
|
22
|
+
description: `Picker description for the ${alias} alias.`,
|
|
23
|
+
},
|
|
24
|
+
];
|
|
25
|
+
}),
|
|
26
|
+
{
|
|
27
|
+
name: 'ANTHROPIC_CUSTOM_MODEL_OPTION',
|
|
28
|
+
value: '{catalog[4]}',
|
|
29
|
+
description:
|
|
30
|
+
'Fifth picker entry, with _NAME and _DESCRIPTION. Only when the catalog has five or more models.',
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
name: 'SUBC_CLAUDE_SETTINGS',
|
|
34
|
+
value: '{claudeSettings}',
|
|
35
|
+
description: 'Picker settings JSON passed to --settings.',
|
|
36
|
+
},
|
|
37
|
+
];
|
|
38
|
+
|
|
39
|
+
const CLAUDE_MODEL_PICKER_KEYS = [
|
|
40
|
+
'ANTHROPIC_DEFAULT_OPUS_MODEL',
|
|
41
|
+
'ANTHROPIC_DEFAULT_OPUS_MODEL_NAME',
|
|
42
|
+
'ANTHROPIC_DEFAULT_OPUS_MODEL_DESCRIPTION',
|
|
43
|
+
'ANTHROPIC_DEFAULT_SONNET_MODEL',
|
|
44
|
+
'ANTHROPIC_DEFAULT_SONNET_MODEL_NAME',
|
|
45
|
+
'ANTHROPIC_DEFAULT_SONNET_MODEL_DESCRIPTION',
|
|
46
|
+
'ANTHROPIC_DEFAULT_HAIKU_MODEL',
|
|
47
|
+
'ANTHROPIC_DEFAULT_HAIKU_MODEL_NAME',
|
|
48
|
+
'ANTHROPIC_DEFAULT_HAIKU_MODEL_DESCRIPTION',
|
|
49
|
+
'ANTHROPIC_DEFAULT_FABLE_MODEL',
|
|
50
|
+
'ANTHROPIC_DEFAULT_FABLE_MODEL_NAME',
|
|
51
|
+
'ANTHROPIC_DEFAULT_FABLE_MODEL_DESCRIPTION',
|
|
52
|
+
'ANTHROPIC_CUSTOM_MODEL_OPTION',
|
|
53
|
+
'ANTHROPIC_CUSTOM_MODEL_OPTION_NAME',
|
|
54
|
+
'ANTHROPIC_CUSTOM_MODEL_OPTION_DESCRIPTION',
|
|
55
|
+
];
|
|
56
|
+
|
|
57
|
+
const CLAUDE_MODEL_PICKER_ID_KEYS = [
|
|
58
|
+
'ANTHROPIC_DEFAULT_OPUS_MODEL',
|
|
59
|
+
'ANTHROPIC_DEFAULT_SONNET_MODEL',
|
|
60
|
+
'ANTHROPIC_DEFAULT_HAIKU_MODEL',
|
|
61
|
+
'ANTHROPIC_DEFAULT_FABLE_MODEL',
|
|
62
|
+
'ANTHROPIC_CUSTOM_MODEL_OPTION',
|
|
63
|
+
];
|
|
64
|
+
|
|
65
|
+
function uniqueCatalogModels(models, fallbackModel) {
|
|
66
|
+
const unique = [];
|
|
67
|
+
for (const model of models) {
|
|
68
|
+
if (model && !unique.includes(model)) unique.push(model);
|
|
69
|
+
}
|
|
70
|
+
if (unique.length === 0 && fallbackModel) unique.push(fallbackModel);
|
|
71
|
+
return unique;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function claudePickerSettings(models, fallbackModel) {
|
|
75
|
+
const unique = uniqueCatalogModels(models, fallbackModel);
|
|
76
|
+
return {
|
|
77
|
+
availableModels: unique,
|
|
78
|
+
modelPicker: {
|
|
79
|
+
replaceBuiltInOptions: true,
|
|
80
|
+
options: unique.map((model) => ({
|
|
81
|
+
model,
|
|
82
|
+
label: model,
|
|
83
|
+
description: `Subconscious model ${model}`,
|
|
84
|
+
})),
|
|
85
|
+
},
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export function claudeModelPickerEnv(model, models) {
|
|
90
|
+
// Only the live catalog belongs in Claude's picker. Packaged registry slots
|
|
91
|
+
// (Haiku → DeepSeek, etc.) must not be unioned back in — that advertised
|
|
92
|
+
// models the key cannot call.
|
|
93
|
+
//
|
|
94
|
+
// Pad through Fable so the built-in `fable` alias cannot resolve to Anthropic
|
|
95
|
+
// Fable 5. The /model menu itself is replaced via claudePickerSettings.
|
|
96
|
+
const pickerModels = uniqueCatalogModels(models, model);
|
|
97
|
+
while (pickerModels.length < 4)
|
|
98
|
+
pickerModels.push(pickerModels.at(-1) || model);
|
|
99
|
+
|
|
100
|
+
const roles = ['OPUS', 'SONNET', 'HAIKU', 'FABLE'];
|
|
101
|
+
const env = {};
|
|
102
|
+
for (let index = 0; index < roles.length; index++) {
|
|
103
|
+
const role = roles[index];
|
|
104
|
+
const model = pickerModels[index];
|
|
105
|
+
if (!model) continue;
|
|
106
|
+
env[`ANTHROPIC_DEFAULT_${role}_MODEL`] = model;
|
|
107
|
+
env[`ANTHROPIC_DEFAULT_${role}_MODEL_NAME`] = model;
|
|
108
|
+
env[`ANTHROPIC_DEFAULT_${role}_MODEL_DESCRIPTION`] =
|
|
109
|
+
`Subconscious model ${model}`;
|
|
110
|
+
}
|
|
111
|
+
const customModel = pickerModels[4];
|
|
112
|
+
if (customModel) {
|
|
113
|
+
env.ANTHROPIC_CUSTOM_MODEL_OPTION = customModel;
|
|
114
|
+
env.ANTHROPIC_CUSTOM_MODEL_OPTION_NAME = customModel;
|
|
115
|
+
env.ANTHROPIC_CUSTOM_MODEL_OPTION_DESCRIPTION = `Subconscious model ${customModel}`;
|
|
116
|
+
}
|
|
117
|
+
return Object.fromEntries(
|
|
118
|
+
CLAUDE_MODEL_PICKER_KEYS.map((key) => [key, env[key]]).filter(
|
|
119
|
+
([, value]) => value,
|
|
120
|
+
),
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export function applyClaudePickerCatalog(env, picker, models) {
|
|
125
|
+
if (Object.keys(picker).length === 0) return env;
|
|
126
|
+
const allowed = new Set(models);
|
|
127
|
+
|
|
128
|
+
for (const key of CLAUDE_MODEL_PICKER_KEYS) {
|
|
129
|
+
if (!picker[key]) {
|
|
130
|
+
delete env[key];
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
for (const key of CLAUDE_MODEL_PICKER_ID_KEYS) {
|
|
135
|
+
if (!picker[key] || !env[key] || allowed.has(env[key])) continue;
|
|
136
|
+
env[key] = picker[key];
|
|
137
|
+
const nameKey = `${key}_NAME`;
|
|
138
|
+
const descriptionKey = `${key}_DESCRIPTION`;
|
|
139
|
+
if (picker[nameKey]) env[nameKey] = picker[nameKey];
|
|
140
|
+
if (picker[descriptionKey]) env[descriptionKey] = picker[descriptionKey];
|
|
141
|
+
}
|
|
142
|
+
return env;
|
|
143
|
+
}
|
package/bin/cli.js
CHANGED
|
@@ -10,43 +10,49 @@
|
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
12
|
import fs from 'node:fs/promises';
|
|
13
|
-
import { c } from './colors.js';
|
|
14
|
-
import { renderBanner } from './branding.js';
|
|
15
13
|
import {
|
|
14
|
+
agentCommandName,
|
|
15
|
+
agentList,
|
|
16
|
+
isAgentHelpRequest,
|
|
17
|
+
isHeadlessRequest,
|
|
18
|
+
parseAgentAction,
|
|
19
|
+
resolveAgent,
|
|
20
|
+
runAgent,
|
|
21
|
+
} from './agents.js';
|
|
22
|
+
import {
|
|
23
|
+
getApiKey,
|
|
16
24
|
loginCommand,
|
|
17
25
|
logoutCommand,
|
|
18
|
-
getApiKey,
|
|
19
26
|
updateApiKeyCommand,
|
|
20
27
|
whoamiCommand,
|
|
21
28
|
} from './auth.js';
|
|
22
|
-
import {
|
|
29
|
+
import { renderBanner } from './branding.js';
|
|
30
|
+
import { c } from './colors.js';
|
|
31
|
+
import { feedbackCommand, printFeedbackHelp } from './feedback.js';
|
|
23
32
|
import {
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
parseAgentAction,
|
|
30
|
-
} from './agents.js';
|
|
33
|
+
harnessManifestCommand,
|
|
34
|
+
printHarnessManifestHelp,
|
|
35
|
+
} from './harness-manifest.js';
|
|
36
|
+
import { headlessPromptIndex } from './headless-args.js';
|
|
37
|
+
import { resolveModelCatalog } from './models.js';
|
|
31
38
|
import {
|
|
32
39
|
configCommand,
|
|
33
40
|
DEFAULT_PROFILE,
|
|
34
41
|
loadProfile,
|
|
35
42
|
modelsCommand,
|
|
43
|
+
SUPPORTED_MODELS as PACKAGED_MODELS,
|
|
36
44
|
printConfigHelp,
|
|
37
|
-
resolvedModelSetting,
|
|
38
45
|
RUNBOOK_DEFAULTS,
|
|
39
|
-
|
|
46
|
+
resolvedModelSetting,
|
|
40
47
|
updatePlatformUrlCommand,
|
|
41
48
|
updateUrlCommand,
|
|
42
49
|
validateProfileName,
|
|
43
50
|
} from './profiles.js';
|
|
44
|
-
import { resolveModelCatalog } from './models.js';
|
|
45
|
-
import { showUpdateNotice } from './update-check.js';
|
|
46
|
-
import { runTui } from './tui.js';
|
|
47
51
|
import { sessionsCommand } from './sessions.js';
|
|
52
|
+
import { runTui } from './tui.js';
|
|
53
|
+
import { showUpdateNotice } from './update-check.js';
|
|
54
|
+
import { upgradeCommand } from './upgrade.js';
|
|
48
55
|
import { printUsageHelp, usageCommand } from './usage.js';
|
|
49
|
-
import { feedbackCommand, printFeedbackHelp } from './feedback.js';
|
|
50
56
|
|
|
51
57
|
function isHelpArg(arg) {
|
|
52
58
|
return arg === 'help' || arg === '-h' || arg === '--help';
|
|
@@ -54,7 +60,10 @@ function isHelpArg(arg) {
|
|
|
54
60
|
|
|
55
61
|
function printHelp() {
|
|
56
62
|
const agents = agentList()
|
|
57
|
-
.map(
|
|
63
|
+
.map(
|
|
64
|
+
({ name, alias, action }) =>
|
|
65
|
+
` ${c.cyan}${alias.padEnd(13)}${c.reset}${c.dim}${action} ${name}${c.reset}`,
|
|
66
|
+
)
|
|
58
67
|
.join('\n');
|
|
59
68
|
|
|
60
69
|
console.log(`${renderBanner()}
|
|
@@ -83,6 +92,7 @@ function printHelp() {
|
|
|
83
92
|
|
|
84
93
|
${c.bold}Coding agents${c.reset}
|
|
85
94
|
${agents}
|
|
95
|
+
${c.cyan}harness-manifest${c.reset} Print how each agent is installed and launched (JSON)
|
|
86
96
|
|
|
87
97
|
${c.bold}Options${c.reset}
|
|
88
98
|
${c.dim}--model <id>${c.reset} Model to use (profile MODEL, or first live catalog model if UNSET)
|
|
@@ -108,6 +118,8 @@ ${agents}
|
|
|
108
118
|
${c.dim}$${c.reset} subc cursor install
|
|
109
119
|
${c.dim}$${c.reset} subc cursor uninstall
|
|
110
120
|
${c.dim}$${c.reset} subc pi install
|
|
121
|
+
${c.dim}$${c.reset} subc harness-manifest --json
|
|
122
|
+
${c.dim}$${c.reset} subc codex headless "fix the failing test"
|
|
111
123
|
${c.dim}$${c.reset} subc -p staging codex
|
|
112
124
|
|
|
113
125
|
${c.dim}Use subc <command> help for command-specific usage.${c.reset}
|
|
@@ -242,12 +254,45 @@ const authCommands = {
|
|
|
242
254
|
whoami: whoamiCommand,
|
|
243
255
|
};
|
|
244
256
|
|
|
257
|
+
/**
|
|
258
|
+
* Position of the headless prompt in the full argv, or -1. It is decided on
|
|
259
|
+
* the words left once profile flags are removed, which is what the agent
|
|
260
|
+
* launcher sees, so both agree on whether the run is headless.
|
|
261
|
+
*/
|
|
262
|
+
function headlessPromptPosition(argv) {
|
|
263
|
+
const kept = [];
|
|
264
|
+
for (let i = 0; i < argv.length; i++) {
|
|
265
|
+
const arg = argv[i];
|
|
266
|
+
const afterCommand = kept.slice(1).map((index) => argv[index]);
|
|
267
|
+
if (
|
|
268
|
+
kept.length > 0 &&
|
|
269
|
+
headlessPromptIndex(afterCommand) === afterCommand.length
|
|
270
|
+
) {
|
|
271
|
+
return i;
|
|
272
|
+
}
|
|
273
|
+
if (arg === '--') return -1;
|
|
274
|
+
if (arg === '--profile' || arg === '-p') {
|
|
275
|
+
i++;
|
|
276
|
+
continue;
|
|
277
|
+
}
|
|
278
|
+
if (arg.startsWith('--profile=')) continue;
|
|
279
|
+
kept.push(i);
|
|
280
|
+
}
|
|
281
|
+
return -1;
|
|
282
|
+
}
|
|
283
|
+
|
|
245
284
|
function extractProfile(argv) {
|
|
246
285
|
let profileName = process.env.SUBC_PROFILE?.trim() || DEFAULT_PROFILE;
|
|
247
286
|
let profileExplicit = false;
|
|
248
287
|
const args = [];
|
|
288
|
+
// The headless prompt is opaque, even when it looks like --profile.
|
|
289
|
+
const promptIndex = headlessPromptPosition(argv);
|
|
249
290
|
for (let i = 0; i < argv.length; i++) {
|
|
250
291
|
const arg = argv[i];
|
|
292
|
+
if (i === promptIndex) {
|
|
293
|
+
args.push(arg);
|
|
294
|
+
continue;
|
|
295
|
+
}
|
|
251
296
|
if (arg === '--') {
|
|
252
297
|
args.push(...argv.slice(i));
|
|
253
298
|
break;
|
|
@@ -293,15 +338,32 @@ async function main() {
|
|
|
293
338
|
return;
|
|
294
339
|
}
|
|
295
340
|
|
|
341
|
+
// Machine-readable output: skip the update check so nothing else reaches
|
|
342
|
+
// stdout and no npm request is made.
|
|
343
|
+
if (command === 'harness-manifest') {
|
|
344
|
+
if (isHelpArg(args[1])) {
|
|
345
|
+
printHarnessManifestHelp();
|
|
346
|
+
return;
|
|
347
|
+
}
|
|
348
|
+
harnessManifestCommand(args.slice(1));
|
|
349
|
+
return;
|
|
350
|
+
}
|
|
351
|
+
|
|
296
352
|
const launchTui =
|
|
297
353
|
!command &&
|
|
298
354
|
process.stdin.isTTY === true &&
|
|
299
355
|
process.stdout.isTTY === true &&
|
|
300
356
|
process.env.TERM !== 'dumb';
|
|
301
357
|
|
|
358
|
+
// Headless runs are scripted: no npm request and no update prompt.
|
|
359
|
+
const headlessAgent = resolveAgent(command);
|
|
360
|
+
const headless = Boolean(
|
|
361
|
+
headlessAgent && isHeadlessRequest(headlessAgent, args.slice(1)),
|
|
362
|
+
);
|
|
363
|
+
|
|
302
364
|
// Explicit commands can wait on npm. The TUI opens immediately and prompts
|
|
303
365
|
// only after a newer version is found.
|
|
304
|
-
if (!launchTui) {
|
|
366
|
+
if (!launchTui && !headless) {
|
|
305
367
|
const update = await showUpdateNotice();
|
|
306
368
|
if (update?.action === 'updated' || update?.action === 'cancel') return;
|
|
307
369
|
}
|
|
@@ -330,7 +392,12 @@ async function main() {
|
|
|
330
392
|
}
|
|
331
393
|
}
|
|
332
394
|
|
|
333
|
-
if (
|
|
395
|
+
if (
|
|
396
|
+
!command ||
|
|
397
|
+
command === '--help' ||
|
|
398
|
+
command === '-h' ||
|
|
399
|
+
(command === 'help' && !args[1])
|
|
400
|
+
) {
|
|
334
401
|
printHelp();
|
|
335
402
|
return;
|
|
336
403
|
}
|
package/bin/clipboard-image.js
CHANGED
|
@@ -8,13 +8,23 @@ import { promisify } from 'node:util';
|
|
|
8
8
|
|
|
9
9
|
const execFileAsync = promisify(execFile);
|
|
10
10
|
|
|
11
|
-
const PNG_SIGNATURE = Buffer.from([
|
|
11
|
+
const PNG_SIGNATURE = Buffer.from([
|
|
12
|
+
0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a,
|
|
13
|
+
]);
|
|
12
14
|
|
|
13
15
|
export function imageContentType(buffer) {
|
|
14
|
-
if (
|
|
16
|
+
if (
|
|
17
|
+
buffer.length >= 3 &&
|
|
18
|
+
buffer[0] === 0xff &&
|
|
19
|
+
buffer[1] === 0xd8 &&
|
|
20
|
+
buffer[2] === 0xff
|
|
21
|
+
) {
|
|
15
22
|
return 'image/jpeg';
|
|
16
23
|
}
|
|
17
|
-
if (
|
|
24
|
+
if (
|
|
25
|
+
buffer.length >= PNG_SIGNATURE.length &&
|
|
26
|
+
buffer.subarray(0, PNG_SIGNATURE.length).equals(PNG_SIGNATURE)
|
|
27
|
+
) {
|
|
18
28
|
return 'image/png';
|
|
19
29
|
}
|
|
20
30
|
return null;
|
|
@@ -48,11 +58,16 @@ async function readDarwinClipboard() {
|
|
|
48
58
|
async function readLinuxClipboard() {
|
|
49
59
|
const commands = [
|
|
50
60
|
['wl-paste', ['--type', 'image/png']],
|
|
61
|
+
['wl-paste', ['--type', 'image/jpeg']],
|
|
51
62
|
['xclip', ['-selection', 'clipboard', '-t', 'image/png', '-o']],
|
|
63
|
+
['xclip', ['-selection', 'clipboard', '-t', 'image/jpeg', '-o']],
|
|
52
64
|
];
|
|
53
65
|
for (const [command, args] of commands) {
|
|
54
66
|
try {
|
|
55
|
-
const { stdout } = await execFileAsync(command, args, {
|
|
67
|
+
const { stdout } = await execFileAsync(command, args, {
|
|
68
|
+
encoding: 'buffer',
|
|
69
|
+
maxBuffer: 8 * 1024 * 1024,
|
|
70
|
+
});
|
|
56
71
|
if (stdout?.length) return stdout;
|
|
57
72
|
} catch {
|
|
58
73
|
// Try the next clipboard tool.
|
|
@@ -72,7 +87,11 @@ async function readWindowsClipboard() {
|
|
|
72
87
|
Write-Output $path
|
|
73
88
|
`;
|
|
74
89
|
try {
|
|
75
|
-
const { stdout } = await execFileAsync('powershell', [
|
|
90
|
+
const { stdout } = await execFileAsync('powershell', [
|
|
91
|
+
'-NoProfile',
|
|
92
|
+
'-Command',
|
|
93
|
+
script,
|
|
94
|
+
]);
|
|
76
95
|
const outPath = String(stdout).trim();
|
|
77
96
|
if (!outPath) return null;
|
|
78
97
|
const { readFile, rm } = await import('node:fs/promises');
|
package/bin/colors.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// ANSI color helpers shared across the CLI. Keep redirected output machine
|
|
2
2
|
// readable and honor the standard NO_COLOR opt-out.
|
|
3
|
-
const noColor = Object.
|
|
3
|
+
const noColor = Object.hasOwn(process.env, 'NO_COLOR');
|
|
4
4
|
const forceColor =
|
|
5
5
|
process.env.FORCE_COLOR !== undefined && process.env.FORCE_COLOR !== '0';
|
|
6
6
|
|