subconscious-cli 0.2.1 → 0.3.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.
- package/README.md +200 -76
- package/bin/agents.js +430 -23
- package/bin/auth.js +116 -24
- package/bin/branding.js +39 -0
- package/bin/cli.js +238 -18
- package/bin/colors.js +22 -10
- package/bin/profiles.js +782 -0
- package/bin/registry.generated.json +182 -44
- package/bin/runbook/README.md +22 -0
- package/bin/runbook/claude-code/install.sh +290 -0
- package/bin/runbook/claude-code/run.sh +164 -0
- package/bin/runbook/codex/hook.sh +112 -0
- package/bin/runbook/codex/hooks-lib.sh +139 -0
- package/bin/runbook/codex/hooks.json +29 -0
- package/bin/runbook/codex/install.sh +82 -0
- package/bin/runbook/codex/run.sh +248 -0
- package/bin/runbook/copilot/hook.sh +173 -0
- package/bin/runbook/copilot/hooks.json +19 -0
- package/bin/runbook/copilot/install.sh +470 -0
- package/bin/runbook/cursor/hook.sh +145 -0
- package/bin/runbook/cursor/hooks.json +17 -0
- package/bin/runbook/cursor/install.sh +284 -0
- package/bin/runbook/opencode/install.sh +298 -0
- package/bin/runbook/opencode/run.sh +106 -0
- package/bin/runbook/opencode/subconscious-compaction.ts +100 -0
- package/bin/runbook/pi/install.sh +278 -0
- package/bin/runbook/pi/run.sh +25 -0
- package/bin/runbook/pi/subconscious-compaction.ts +151 -0
- package/package.json +10 -5
package/bin/auth.js
CHANGED
|
@@ -8,7 +8,8 @@
|
|
|
8
8
|
* 3. The web app authenticates the user, generates an API key, and
|
|
9
9
|
* delivers it back to the CLI via a cross-origin fetch to
|
|
10
10
|
* localhost:{port}/callback?token=...&state=...
|
|
11
|
-
* 4. CLI verifies the `state
|
|
11
|
+
* 4. CLI verifies the `state`, saves the key to ~/.subconscious/config.json,
|
|
12
|
+
* and creates a coding-agent profile under ~/.subconscious/profiles/.
|
|
12
13
|
*
|
|
13
14
|
* Override SUBCONSCIOUS_URL env var for local development.
|
|
14
15
|
*/
|
|
@@ -20,18 +21,55 @@ import fs from 'node:fs/promises';
|
|
|
20
21
|
import os from 'node:os';
|
|
21
22
|
import path from 'node:path';
|
|
22
23
|
import { c } from './colors.js';
|
|
24
|
+
import { clearProfileApiKey, DEFAULT_PROFILE, ensureProfile } from './profiles.js';
|
|
23
25
|
|
|
24
|
-
const
|
|
26
|
+
const CONFIG_OVERRIDE = process.env.SUBC_CONFIG_DIR?.trim();
|
|
27
|
+
const CONFIG_DIR = CONFIG_OVERRIDE || path.join(os.homedir(), '.subconscious');
|
|
25
28
|
const CONFIG_FILE = path.join(CONFIG_DIR, 'config.json');
|
|
29
|
+
const LEGACY_CONFIG_FILE = CONFIG_OVERRIDE
|
|
30
|
+
? null
|
|
31
|
+
: path.join(os.homedir(), '.subcon', 'config.json');
|
|
26
32
|
// Defaults to production. Developers set SUBCONSCIOUS_URL=http://localhost:3000 for local dev.
|
|
27
33
|
const PLATFORM_URL = process.env.SUBCONSCIOUS_URL || 'https://www.subconscious.dev';
|
|
28
34
|
|
|
35
|
+
// Login callback CORS. After the marketing/platform split, www 307s /cli/auth
|
|
36
|
+
// to the platform host, so the browser Origin is platform even when the CLI
|
|
37
|
+
// still opened www. Keep www so a non-redirected tab still works.
|
|
38
|
+
const CALLBACK_ORIGINS = new Set([
|
|
39
|
+
'https://www.subconscious.dev',
|
|
40
|
+
'https://platform.subconscious.dev',
|
|
41
|
+
'https://dev.subconscious.dev',
|
|
42
|
+
'https://platform-dev.subconscious.dev',
|
|
43
|
+
]);
|
|
44
|
+
|
|
45
|
+
export function isAllowedCallbackOrigin(origin, platformUrl = PLATFORM_URL) {
|
|
46
|
+
if (!origin) return false;
|
|
47
|
+
if (origin === platformUrl || CALLBACK_ORIGINS.has(origin)) return true;
|
|
48
|
+
try {
|
|
49
|
+
const { protocol, hostname } = new URL(origin);
|
|
50
|
+
return protocol === 'http:' && (hostname === 'localhost' || hostname === '127.0.0.1');
|
|
51
|
+
} catch {
|
|
52
|
+
return false;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
29
56
|
// ── Config helpers ──────────────────────────────────────────────────────
|
|
30
57
|
|
|
31
58
|
async function loadConfig() {
|
|
32
59
|
try {
|
|
33
60
|
const content = await fs.readFile(CONFIG_FILE, 'utf-8');
|
|
34
61
|
return JSON.parse(content);
|
|
62
|
+
} catch (error) {
|
|
63
|
+
if (error.code !== 'ENOENT' || !LEGACY_CONFIG_FILE) return {};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// One-way compatibility migration. Keep the old file untouched so this is
|
|
67
|
+
// recoverable, but all future writes go to ~/.subconscious.
|
|
68
|
+
try {
|
|
69
|
+
const content = await fs.readFile(LEGACY_CONFIG_FILE, 'utf-8');
|
|
70
|
+
const config = JSON.parse(content);
|
|
71
|
+
await saveConfig(config);
|
|
72
|
+
return config;
|
|
35
73
|
} catch {
|
|
36
74
|
return {};
|
|
37
75
|
}
|
|
@@ -48,13 +86,20 @@ async function saveConfig(config) {
|
|
|
48
86
|
* Resolve the active API key. The env var takes precedence over the saved
|
|
49
87
|
* config so CI and per-shell overrides win. Returns null when unauthenticated.
|
|
50
88
|
*/
|
|
51
|
-
export async function getApiKey() {
|
|
89
|
+
export async function getApiKey(profile) {
|
|
52
90
|
const envKey = process.env.SUBCONSCIOUS_API_KEY?.trim();
|
|
53
91
|
if (envKey) return { key: envKey, source: 'SUBCONSCIOUS_API_KEY env var' };
|
|
54
92
|
|
|
93
|
+
const profileKey = profile?.values?.API_KEY?.trim();
|
|
94
|
+
if (profileKey) return { key: profileKey, source: profile.path };
|
|
95
|
+
|
|
96
|
+
// Named profiles are isolated: an empty/missing key must not silently fall
|
|
97
|
+
// back to the default credential and send traffic to the wrong account.
|
|
98
|
+
if (profile?.name && profile.name !== DEFAULT_PROFILE) return null;
|
|
99
|
+
|
|
55
100
|
const config = await loadConfig();
|
|
56
101
|
if (config.subconscious_api_key) {
|
|
57
|
-
return { key: config.subconscious_api_key, source: '~/.
|
|
102
|
+
return { key: config.subconscious_api_key, source: '~/.subconscious/config.json' };
|
|
58
103
|
}
|
|
59
104
|
return null;
|
|
60
105
|
}
|
|
@@ -90,12 +135,10 @@ function startCallbackServer(expectedState) {
|
|
|
90
135
|
});
|
|
91
136
|
|
|
92
137
|
const server = http.createServer((req, res) => {
|
|
93
|
-
// CORS: only allow
|
|
138
|
+
// CORS: only allow known web-app origins (or localhost dev).
|
|
94
139
|
// This prevents arbitrary websites from hitting this callback.
|
|
95
140
|
const origin = req.headers.origin || '';
|
|
96
|
-
const allowed =
|
|
97
|
-
origin === PLATFORM_URL ||
|
|
98
|
-
origin.startsWith('http://localhost:');
|
|
141
|
+
const allowed = isAllowedCallbackOrigin(origin);
|
|
99
142
|
res.setHeader(
|
|
100
143
|
'Access-Control-Allow-Origin',
|
|
101
144
|
allowed ? origin : PLATFORM_URL,
|
|
@@ -231,15 +274,20 @@ h1{font-size:15px;font-weight:600;margin-bottom:4px;letter-spacing:-.01em}
|
|
|
231
274
|
|
|
232
275
|
// ── Commands ────────────────────────────────────────────────────────────
|
|
233
276
|
|
|
234
|
-
export async function loginCommand() {
|
|
235
|
-
const
|
|
277
|
+
export async function loginCommand(_argv = [], options = {}) {
|
|
278
|
+
const profileName = options.profileName || DEFAULT_PROFILE;
|
|
279
|
+
const existing = await getApiKey(options.profile);
|
|
236
280
|
|
|
237
281
|
if (existing) {
|
|
282
|
+
const profile = await ensureProfile(profileName, existing.key);
|
|
283
|
+
const logout =
|
|
284
|
+
profileName === DEFAULT_PROFILE ? 'subc logout' : `subc --profile ${profileName} logout`;
|
|
238
285
|
const masked = existing.key.slice(0, 8) + '...' + existing.key.slice(-4);
|
|
239
286
|
console.log(`\n${c.yellow}Already logged in.${c.reset}`);
|
|
240
287
|
console.log(` Key: ${c.dim}${masked}${c.reset}`);
|
|
288
|
+
console.log(` Profile: ${c.dim}${profile.path}${c.reset}`);
|
|
241
289
|
console.log(
|
|
242
|
-
`\n Run ${c.cyan}
|
|
290
|
+
`\n Run ${c.cyan}${logout}${c.reset} first to switch accounts.\n`,
|
|
243
291
|
);
|
|
244
292
|
return;
|
|
245
293
|
}
|
|
@@ -283,14 +331,22 @@ export async function loginCommand() {
|
|
|
283
331
|
clearInterval(spinner);
|
|
284
332
|
process.stdout.write('\r' + ' '.repeat(50) + '\r');
|
|
285
333
|
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
334
|
+
if (profileName === DEFAULT_PROFILE) {
|
|
335
|
+
const config = await loadConfig();
|
|
336
|
+
config.subconscious_api_key = result.token;
|
|
337
|
+
await saveConfig(config);
|
|
338
|
+
}
|
|
339
|
+
const profile = await ensureProfile(profileName, result.token);
|
|
289
340
|
|
|
290
341
|
const masked = result.token.slice(0, 8) + '...' + result.token.slice(-4);
|
|
291
342
|
console.log(` ${c.green}${c.bold}✓ Logged in successfully!${c.reset}`);
|
|
292
343
|
console.log(` ${c.dim}Key: ${masked}${c.reset}`);
|
|
293
|
-
|
|
344
|
+
if (profileName === DEFAULT_PROFILE) {
|
|
345
|
+
console.log(` ${c.dim}Saved to ~/.subconscious/config.json${c.reset}`);
|
|
346
|
+
}
|
|
347
|
+
console.log(` ${c.dim}Runbook profile: ${profile.path}${c.reset}`);
|
|
348
|
+
console.log(` ${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}`);
|
|
349
|
+
console.log(` ${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}`);
|
|
294
350
|
console.log();
|
|
295
351
|
} catch (error) {
|
|
296
352
|
clearInterval(spinner);
|
|
@@ -300,29 +356,65 @@ export async function loginCommand() {
|
|
|
300
356
|
}
|
|
301
357
|
}
|
|
302
358
|
|
|
303
|
-
export async function
|
|
359
|
+
export async function updateApiKeyCommand(argv = [], options = {}) {
|
|
360
|
+
if (argv.length !== 1 || !argv[0]?.trim()) {
|
|
361
|
+
throw new Error('Usage: subc update-key <api-key>');
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
const key = argv[0].trim();
|
|
365
|
+
const profileName = options.profileName || DEFAULT_PROFILE;
|
|
366
|
+
const profile = await ensureProfile(profileName, key);
|
|
367
|
+
|
|
368
|
+
if (profileName === DEFAULT_PROFILE) {
|
|
369
|
+
const config = await loadConfig();
|
|
370
|
+
config.subconscious_api_key = key;
|
|
371
|
+
await saveConfig(config);
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
const masked = key.length <= 12 ? '********' : `${key.slice(0, 8)}...${key.slice(-4)}`;
|
|
375
|
+
console.log(`\n ${c.green}${c.bold}✓ API key updated.${c.reset}`);
|
|
376
|
+
console.log(` ${c.dim}Profile: ${profile.path}${c.reset}`);
|
|
377
|
+
console.log(` ${c.dim}Key: ${masked}${c.reset}`);
|
|
378
|
+
if (process.env.SUBCONSCIOUS_API_KEY?.trim()) {
|
|
379
|
+
console.log(
|
|
380
|
+
`\n ${c.yellow}SUBCONSCIOUS_API_KEY is set and will override this saved key.${c.reset}`,
|
|
381
|
+
);
|
|
382
|
+
}
|
|
383
|
+
console.log();
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
export async function logoutCommand(_argv = [], options = {}) {
|
|
387
|
+
const profileName = options.profileName || DEFAULT_PROFILE;
|
|
304
388
|
const config = await loadConfig();
|
|
389
|
+
const clearedProfile = await clearProfileApiKey(profileName);
|
|
390
|
+
const clearSavedConfig = profileName === DEFAULT_PROFILE && config.subconscious_api_key;
|
|
305
391
|
|
|
306
|
-
if (!
|
|
392
|
+
if (!clearSavedConfig && !clearedProfile) {
|
|
307
393
|
console.log(`\n ${c.dim}Not logged in.${c.reset}\n`);
|
|
308
394
|
return;
|
|
309
395
|
}
|
|
310
396
|
|
|
311
|
-
|
|
312
|
-
|
|
397
|
+
if (clearSavedConfig) {
|
|
398
|
+
delete config.subconscious_api_key;
|
|
399
|
+
await saveConfig(config);
|
|
400
|
+
}
|
|
313
401
|
|
|
314
402
|
console.log(
|
|
315
|
-
`\n ${c.green}✓${c.reset} Logged out
|
|
403
|
+
`\n ${c.green}✓${c.reset} Logged out of profile '${profileName}'.${c.reset}\n`,
|
|
316
404
|
);
|
|
317
405
|
}
|
|
318
406
|
|
|
319
|
-
export async function whoamiCommand() {
|
|
320
|
-
const auth = await getApiKey();
|
|
407
|
+
export async function whoamiCommand(_argv = [], options = {}) {
|
|
408
|
+
const auth = await getApiKey(options.profile);
|
|
409
|
+
const profileFlag =
|
|
410
|
+
options.profileName && options.profileName !== DEFAULT_PROFILE
|
|
411
|
+
? `--profile ${options.profileName} `
|
|
412
|
+
: '';
|
|
321
413
|
|
|
322
414
|
if (!auth) {
|
|
323
415
|
console.log(`\n ${c.dim}Not logged in.${c.reset}`);
|
|
324
416
|
console.log(
|
|
325
|
-
` Run ${c.cyan}
|
|
417
|
+
` Run ${c.cyan}subc ${profileFlag}login${c.reset} to get started.\n`,
|
|
326
418
|
);
|
|
327
419
|
return;
|
|
328
420
|
}
|
|
@@ -353,7 +445,7 @@ export async function whoamiCommand() {
|
|
|
353
445
|
console.log(` ${c.dim}Source: ${source}${c.reset}`);
|
|
354
446
|
console.log();
|
|
355
447
|
console.log(
|
|
356
|
-
` Run ${c.cyan}
|
|
448
|
+
` Run ${c.cyan}subc ${profileFlag}logout${c.reset} then ${c.cyan}subc ${profileFlag}login${c.reset} to re-authenticate.`,
|
|
357
449
|
);
|
|
358
450
|
}
|
|
359
451
|
} catch {
|
package/bin/branding.js
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { colorEnabled } from './colors.js';
|
|
2
|
+
|
|
3
|
+
// Pre-rendered from assets/imgs/logo.png as portable, 7-bit ASCII art.
|
|
4
|
+
const LOGO = [
|
|
5
|
+
' ##### #####',
|
|
6
|
+
' ####### #######',
|
|
7
|
+
' ######## ########',
|
|
8
|
+
' ######## ########',
|
|
9
|
+
' ######',
|
|
10
|
+
' ##',
|
|
11
|
+
' ###### #### ######',
|
|
12
|
+
'########## ######## ##########',
|
|
13
|
+
'########## ######## ##########',
|
|
14
|
+
' ###### #### ######',
|
|
15
|
+
' ##',
|
|
16
|
+
' ######',
|
|
17
|
+
' ####### #######',
|
|
18
|
+
' ####### #######',
|
|
19
|
+
' ####### #######',
|
|
20
|
+
' ###### #####',
|
|
21
|
+
].join('\n');
|
|
22
|
+
|
|
23
|
+
const ORANGE = '\x1b[38;2;255;92;40m';
|
|
24
|
+
const BOLD = '\x1b[1m';
|
|
25
|
+
const RESET = '\x1b[0m';
|
|
26
|
+
|
|
27
|
+
export function renderBanner(options = {}) {
|
|
28
|
+
const isTTY = options.isTTY ?? process.stdout.isTTY === true;
|
|
29
|
+
const term = options.term ?? process.env.TERM;
|
|
30
|
+
const color = options.color ?? colorEnabled;
|
|
31
|
+
const title = color ? `${BOLD}Subconscious CLI${RESET}` : 'Subconscious CLI';
|
|
32
|
+
|
|
33
|
+
// Avoid multi-line art in logs and pipes. Interactive NO_COLOR sessions
|
|
34
|
+
// still get the same logo without ANSI styling.
|
|
35
|
+
if (!isTTY || term === 'dumb') return ` ${title}`;
|
|
36
|
+
|
|
37
|
+
const logo = color ? `${ORANGE}${LOGO}${RESET}` : LOGO;
|
|
38
|
+
return `${logo}\n\n ${title}`;
|
|
39
|
+
}
|
package/bin/cli.js
CHANGED
|
@@ -3,61 +3,229 @@
|
|
|
3
3
|
/**
|
|
4
4
|
* Subconscious CLI — log in, then launch coding agents on your hosted models.
|
|
5
5
|
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
6
|
+
* subc login | update-key | logout | whoami — manage your API key
|
|
7
|
+
* subc <agent> [...args] — launch or configure a coding agent
|
|
8
8
|
*
|
|
9
9
|
* Auth lives in ./auth.js, the agent launcher + registry in ./agents.js.
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
12
|
import fs from 'node:fs/promises';
|
|
13
13
|
import { c } from './colors.js';
|
|
14
|
-
import {
|
|
15
|
-
import {
|
|
14
|
+
import { renderBanner } from './branding.js';
|
|
15
|
+
import {
|
|
16
|
+
loginCommand,
|
|
17
|
+
logoutCommand,
|
|
18
|
+
updateApiKeyCommand,
|
|
19
|
+
whoamiCommand,
|
|
20
|
+
} from './auth.js';
|
|
21
|
+
import {
|
|
22
|
+
resolveAgent,
|
|
23
|
+
runAgent,
|
|
24
|
+
agentList,
|
|
25
|
+
agentCommandName,
|
|
26
|
+
isAgentHelpRequest,
|
|
27
|
+
parseAgentAction,
|
|
28
|
+
} from './agents.js';
|
|
29
|
+
import {
|
|
30
|
+
configCommand,
|
|
31
|
+
DEFAULT_PROFILE,
|
|
32
|
+
loadProfile,
|
|
33
|
+
modelsCommand,
|
|
34
|
+
printConfigHelp,
|
|
35
|
+
updateUrlCommand,
|
|
36
|
+
validateProfileName,
|
|
37
|
+
} from './profiles.js';
|
|
38
|
+
|
|
39
|
+
function isHelpArg(arg) {
|
|
40
|
+
return arg === 'help' || arg === '-h' || arg === '--help';
|
|
41
|
+
}
|
|
16
42
|
|
|
17
43
|
function printHelp() {
|
|
18
44
|
const agents = agentList()
|
|
19
|
-
.map(({ name, alias }) => ` ${c.cyan}${alias.padEnd(13)}${c.reset}${c.dim}
|
|
45
|
+
.map(({ name, alias, action }) => ` ${c.cyan}${alias.padEnd(13)}${c.reset}${c.dim}${action} ${name}${c.reset}`)
|
|
20
46
|
.join('\n');
|
|
21
47
|
|
|
22
|
-
console.log(
|
|
23
|
-
${c.magenta}${c.bold}Subconscious CLI${c.reset}
|
|
48
|
+
console.log(`${renderBanner()}
|
|
24
49
|
|
|
25
50
|
${c.bold}Usage${c.reset}
|
|
26
|
-
${c.cyan}
|
|
51
|
+
${c.cyan}subc${c.reset} <command> [...args]
|
|
52
|
+
${c.cyan}subc${c.reset} <command> help
|
|
27
53
|
|
|
28
54
|
${c.bold}Auth${c.reset}
|
|
29
55
|
${c.cyan}login${c.reset} Authenticate and save your API key
|
|
56
|
+
${c.cyan}update-key${c.reset} Replace the selected profile's API key
|
|
57
|
+
${c.cyan}update-url${c.reset} Update the active profile's gateway URL automatically
|
|
30
58
|
${c.cyan}logout${c.reset} Remove saved credentials
|
|
31
59
|
${c.cyan}whoami${c.reset} Show current authentication status
|
|
32
60
|
|
|
61
|
+
${c.bold}Profiles${c.reset}
|
|
62
|
+
${c.cyan}config${c.reset} List profiles, or show/edit one with ${c.dim}-p${c.reset}
|
|
63
|
+
${c.cyan}models${c.reset} List available Subconscious models
|
|
64
|
+
|
|
33
65
|
${c.bold}Coding agents${c.reset}
|
|
34
66
|
${agents}
|
|
35
67
|
|
|
36
68
|
${c.bold}Options${c.reset}
|
|
37
|
-
${c.dim}--model <id>${c.reset} Model to use (default subconscious/
|
|
69
|
+
${c.dim}--model <id>${c.reset} Model to use (default subconscious/glm-5.2)
|
|
70
|
+
${c.dim}-p, --profile${c.reset} Select a profile (default: default)
|
|
38
71
|
${c.dim}-h, --help${c.reset} Show this help
|
|
39
72
|
${c.dim}-v, --version${c.reset} Show version
|
|
40
73
|
|
|
41
74
|
${c.bold}Examples${c.reset}
|
|
42
|
-
${c.dim}$${c.reset}
|
|
43
|
-
${c.dim}$${c.reset}
|
|
44
|
-
${c.dim}$${c.reset}
|
|
75
|
+
${c.dim}$${c.reset} subc login
|
|
76
|
+
${c.dim}$${c.reset} subc config
|
|
77
|
+
${c.dim}$${c.reset} subc config help
|
|
78
|
+
${c.dim}$${c.reset} subc -p staging config
|
|
79
|
+
${c.dim}$${c.reset} subc config edit vim
|
|
80
|
+
${c.dim}$${c.reset} subc claude
|
|
81
|
+
${c.dim}$${c.reset} subc claude help
|
|
82
|
+
${c.dim}$${c.reset} subc cursor install
|
|
83
|
+
${c.dim}$${c.reset} subc cursor uninstall
|
|
84
|
+
${c.dim}$${c.reset} subc pi install
|
|
85
|
+
${c.dim}$${c.reset} subc -p staging codex
|
|
45
86
|
|
|
46
|
-
${c.dim}
|
|
87
|
+
${c.dim}Use subc <command> help for command-specific usage.${c.reset}
|
|
47
88
|
`);
|
|
48
89
|
}
|
|
49
90
|
|
|
50
|
-
const
|
|
91
|
+
const COMMAND_HELP = {
|
|
92
|
+
login: `
|
|
93
|
+
Usage:
|
|
94
|
+
subc login
|
|
95
|
+
subc -p NAME login
|
|
96
|
+
subc login help
|
|
97
|
+
|
|
98
|
+
Authenticate and save an API key to the selected profile.
|
|
99
|
+
`,
|
|
100
|
+
logout: `
|
|
101
|
+
Usage:
|
|
102
|
+
subc logout
|
|
103
|
+
subc -p NAME logout
|
|
104
|
+
subc logout help
|
|
105
|
+
|
|
106
|
+
Remove the selected profile's saved API key. Non-secret settings are kept.
|
|
107
|
+
`,
|
|
108
|
+
whoami: `
|
|
109
|
+
Usage:
|
|
110
|
+
subc whoami
|
|
111
|
+
subc -p NAME whoami
|
|
112
|
+
subc whoami help
|
|
113
|
+
|
|
114
|
+
Show the current authentication status for the selected profile.
|
|
115
|
+
`,
|
|
116
|
+
'update-key': `
|
|
117
|
+
Usage:
|
|
118
|
+
subc update-key <api-key>
|
|
119
|
+
subc -p NAME update-key <api-key>
|
|
120
|
+
subc update-key help
|
|
121
|
+
|
|
122
|
+
Replace the selected profile's shared API key.
|
|
123
|
+
`,
|
|
124
|
+
'update-url': `
|
|
125
|
+
Usage:
|
|
126
|
+
subc update-url <gateway-url>
|
|
127
|
+
subc update-url help
|
|
128
|
+
|
|
129
|
+
Update the active profile's gateway URL.
|
|
130
|
+
`,
|
|
131
|
+
models: `
|
|
132
|
+
Usage:
|
|
133
|
+
subc models
|
|
134
|
+
subc models help
|
|
135
|
+
|
|
136
|
+
List available Subconscious models.
|
|
137
|
+
`,
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
function printRetiredSetup(args = []) {
|
|
141
|
+
const maybeAgent = resolveAgent(args[0]);
|
|
142
|
+
const command = maybeAgent ? agentCommandName(maybeAgent) : 'cursor';
|
|
143
|
+
console.error(`
|
|
144
|
+
${c.red}subc setup is no longer used.${c.reset} Use the agent command instead:
|
|
145
|
+
|
|
146
|
+
${c.cyan}subc ${command} install${c.reset}
|
|
147
|
+
${c.cyan}subc ${command} uninstall${c.reset}
|
|
148
|
+
${c.cyan}subc ${command} help${c.reset}
|
|
149
|
+
`);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function printRetiredSettings() {
|
|
153
|
+
console.error(`
|
|
154
|
+
${c.red}subc settings is no longer used.${c.reset} Use:
|
|
155
|
+
|
|
156
|
+
${c.cyan}subc config${c.reset} List profiles
|
|
157
|
+
${c.cyan}subc -p NAME config${c.reset} Show a profile
|
|
158
|
+
${c.cyan}subc -p NAME config edit${c.reset} Open the profile env file
|
|
159
|
+
${c.cyan}subc config help${c.reset}
|
|
160
|
+
`);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const authCommands = {
|
|
164
|
+
login: loginCommand,
|
|
165
|
+
'update-key': updateApiKeyCommand,
|
|
166
|
+
logout: logoutCommand,
|
|
167
|
+
whoami: whoamiCommand,
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
function extractProfile(argv) {
|
|
171
|
+
let profileName = process.env.SUBC_PROFILE?.trim() || DEFAULT_PROFILE;
|
|
172
|
+
let profileExplicit = false;
|
|
173
|
+
const args = [];
|
|
174
|
+
for (let i = 0; i < argv.length; i++) {
|
|
175
|
+
const arg = argv[i];
|
|
176
|
+
if (arg === '--') {
|
|
177
|
+
args.push(...argv.slice(i));
|
|
178
|
+
break;
|
|
179
|
+
}
|
|
180
|
+
if (arg === '--profile' || arg === '-p') {
|
|
181
|
+
const value = argv[++i];
|
|
182
|
+
if (!value) throw new Error(`${arg} requires a profile name`);
|
|
183
|
+
profileName = value;
|
|
184
|
+
profileExplicit = true;
|
|
185
|
+
continue;
|
|
186
|
+
}
|
|
187
|
+
if (arg.startsWith('--profile=')) {
|
|
188
|
+
profileName = arg.slice('--profile='.length);
|
|
189
|
+
profileExplicit = true;
|
|
190
|
+
continue;
|
|
191
|
+
}
|
|
192
|
+
args.push(arg);
|
|
193
|
+
}
|
|
194
|
+
validateProfileName(profileName);
|
|
195
|
+
return { args, profileName, profileExplicit };
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function requireNamedProfile(profile) {
|
|
199
|
+
if (profile.name !== DEFAULT_PROFILE && !profile.exists) {
|
|
200
|
+
throw new Error(
|
|
201
|
+
`Profile '${profile.name}' does not exist. Create it with ` +
|
|
202
|
+
`subc -p ${profile.name} config --api-key KEY`,
|
|
203
|
+
);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
51
206
|
|
|
52
207
|
async function main() {
|
|
53
|
-
const
|
|
208
|
+
const parsed = extractProfile(process.argv.slice(2));
|
|
209
|
+
const { args, profileName, profileExplicit } = parsed;
|
|
54
210
|
const command = args[0];
|
|
55
211
|
|
|
56
|
-
if (!command || command === '--help' || command === '-h') {
|
|
212
|
+
if (!command || command === '--help' || command === '-h' || (command === 'help' && !args[1])) {
|
|
57
213
|
printHelp();
|
|
58
214
|
return;
|
|
59
215
|
}
|
|
60
216
|
|
|
217
|
+
if (command === 'help') {
|
|
218
|
+
if (!args[1] || isHelpArg(args[1])) {
|
|
219
|
+
printHelp();
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
const agent = resolveAgent(args[1]);
|
|
223
|
+
if (!agent) throw new Error(`Unknown coding agent: ${args[1]}`);
|
|
224
|
+
const profile = await loadProfile(profileName);
|
|
225
|
+
await runAgent(agent, ['help'], { profile });
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
|
|
61
229
|
if (command === '--version' || command === '-v') {
|
|
62
230
|
const pkgPath = new URL('../package.json', import.meta.url);
|
|
63
231
|
const pkg = JSON.parse(await fs.readFile(pkgPath, 'utf-8'));
|
|
@@ -65,15 +233,67 @@ async function main() {
|
|
|
65
233
|
return;
|
|
66
234
|
}
|
|
67
235
|
|
|
236
|
+
if (command === 'setup') {
|
|
237
|
+
printRetiredSetup(args.slice(1));
|
|
238
|
+
process.exitCode = 1;
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
if (command === 'settings') {
|
|
243
|
+
printRetiredSettings();
|
|
244
|
+
process.exitCode = 1;
|
|
245
|
+
return;
|
|
246
|
+
}
|
|
247
|
+
|
|
68
248
|
const authHandler = authCommands[command];
|
|
69
249
|
if (authHandler) {
|
|
70
|
-
|
|
250
|
+
if (isHelpArg(args[1])) {
|
|
251
|
+
console.log(COMMAND_HELP[command]);
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
254
|
+
const profile = await loadProfile(profileName);
|
|
255
|
+
await authHandler(args.slice(1), { profile, profileName });
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
if (command === 'config') {
|
|
260
|
+
if (isHelpArg(args[1])) {
|
|
261
|
+
printConfigHelp();
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
|
+
await configCommand(args.slice(1), profileName, { profileExplicit });
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
if (command === 'models') {
|
|
269
|
+
if (isHelpArg(args[1])) {
|
|
270
|
+
console.log(COMMAND_HELP.models);
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
273
|
+
modelsCommand();
|
|
274
|
+
return;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
if (command === 'update-url') {
|
|
278
|
+
if (isHelpArg(args[1])) {
|
|
279
|
+
console.log(COMMAND_HELP['update-url']);
|
|
280
|
+
return;
|
|
281
|
+
}
|
|
282
|
+
await updateUrlCommand(args.slice(1), { profileName });
|
|
71
283
|
return;
|
|
72
284
|
}
|
|
73
285
|
|
|
74
286
|
const agent = resolveAgent(command);
|
|
75
287
|
if (agent) {
|
|
76
|
-
|
|
288
|
+
const agentArgs = args.slice(1);
|
|
289
|
+
const profile = await loadProfile(profileName);
|
|
290
|
+
if (!isAgentHelpRequest(agentArgs)) {
|
|
291
|
+
const action = parseAgentAction(agent, agentArgs);
|
|
292
|
+
if (action.action !== 'status' && action.action !== 'uninstall') {
|
|
293
|
+
requireNamedProfile(profile);
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
await runAgent(agent, agentArgs, { profile });
|
|
77
297
|
return;
|
|
78
298
|
}
|
|
79
299
|
|
package/bin/colors.js
CHANGED
|
@@ -1,12 +1,24 @@
|
|
|
1
|
-
// ANSI color helpers shared across the CLI.
|
|
1
|
+
// ANSI color helpers shared across the CLI. Keep redirected output machine
|
|
2
|
+
// readable and honor the standard NO_COLOR opt-out.
|
|
3
|
+
const noColor = Object.prototype.hasOwnProperty.call(process.env, 'NO_COLOR');
|
|
4
|
+
const forceColor =
|
|
5
|
+
process.env.FORCE_COLOR !== undefined && process.env.FORCE_COLOR !== '0';
|
|
6
|
+
|
|
7
|
+
export const colorEnabled =
|
|
8
|
+
!noColor &&
|
|
9
|
+
process.env.TERM !== 'dumb' &&
|
|
10
|
+
(forceColor || process.stdout.isTTY === true);
|
|
11
|
+
|
|
12
|
+
const ansi = (code) => (colorEnabled ? `\x1b[${code}m` : '');
|
|
13
|
+
|
|
2
14
|
export const c = {
|
|
3
|
-
reset:
|
|
4
|
-
bold:
|
|
5
|
-
dim:
|
|
6
|
-
cyan:
|
|
7
|
-
green:
|
|
8
|
-
red:
|
|
9
|
-
yellow:
|
|
10
|
-
magenta:
|
|
11
|
-
underline:
|
|
15
|
+
reset: ansi(0),
|
|
16
|
+
bold: ansi(1),
|
|
17
|
+
dim: ansi(2),
|
|
18
|
+
cyan: ansi(36),
|
|
19
|
+
green: ansi(32),
|
|
20
|
+
red: ansi(31),
|
|
21
|
+
yellow: ansi(33),
|
|
22
|
+
magenta: ansi(35),
|
|
23
|
+
underline: ansi(4),
|
|
12
24
|
};
|