subconscious-cli 0.3.0 → 4.0.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/bin/auth.js CHANGED
@@ -4,14 +4,15 @@
4
4
  * Login flow (localhost callback pattern, similar to Vercel/Supabase CLIs):
5
5
  * 1. CLI generates a random `state` token (CSRF protection) and starts
6
6
  * an ephemeral HTTP server on a random port bound to 127.0.0.1.
7
- * 2. Opens the browser to {PLATFORM_URL}/cli/auth?port=...&state=...
7
+ * 2. Opens the browser to {platformUrl}/cli/auth?port=...&state=...
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
11
  * 4. CLI verifies the `state`, saves the key to ~/.subconscious/config.json,
12
12
  * and creates a coding-agent profile under ~/.subconscious/profiles/.
13
13
  *
14
- * Override SUBCONSCIOUS_URL env var for local development.
14
+ * Override SUBCONSCIOUS_URL env var for local development
15
+ * (e.g. http://localhost:3000). Production defaults to platform.subconscious.dev.
15
16
  */
16
17
 
17
18
  import http from 'node:http';
@@ -22,6 +23,7 @@ import os from 'node:os';
22
23
  import path from 'node:path';
23
24
  import { c } from './colors.js';
24
25
  import { clearProfileApiKey, DEFAULT_PROFILE, ensureProfile } from './profiles.js';
26
+ import { printLoginUpgradeWarning } from './upgrade.js';
25
27
 
26
28
  const CONFIG_OVERRIDE = process.env.SUBC_CONFIG_DIR?.trim();
27
29
  const CONFIG_DIR = CONFIG_OVERRIDE || path.join(os.homedir(), '.subconscious');
@@ -29,8 +31,34 @@ const CONFIG_FILE = path.join(CONFIG_DIR, 'config.json');
29
31
  const LEGACY_CONFIG_FILE = CONFIG_OVERRIDE
30
32
  ? null
31
33
  : path.join(os.homedir(), '.subcon', 'config.json');
32
- // Defaults to production. Developers set SUBCONSCIOUS_URL=http://localhost:3000 for local dev.
33
- const PLATFORM_URL = process.env.SUBCONSCIOUS_URL || 'https://www.subconscious.dev';
34
+ // Defaults to the platform host. Developers set SUBCONSCIOUS_URL for local dev.
35
+ export const DEFAULT_PLATFORM_URL = 'https://platform.subconscious.dev';
36
+
37
+ export function getPlatformUrl() {
38
+ const raw = process.env.SUBCONSCIOUS_URL?.trim() || DEFAULT_PLATFORM_URL;
39
+ return raw.replace(/\/$/, '');
40
+ }
41
+
42
+ // Login callback CORS. After the marketing/platform split, /cli/auth lives on
43
+ // platform. www may still 307 there for older CLIs; keep www so a redirected
44
+ // or leftover tab can complete the callback.
45
+ const CALLBACK_ORIGINS = new Set([
46
+ 'https://www.subconscious.dev',
47
+ 'https://platform.subconscious.dev',
48
+ 'https://dev.subconscious.dev',
49
+ 'https://platform-dev.subconscious.dev',
50
+ ]);
51
+
52
+ export function isAllowedCallbackOrigin(origin, platformUrl = getPlatformUrl()) {
53
+ if (!origin) return false;
54
+ if (origin === platformUrl || CALLBACK_ORIGINS.has(origin)) return true;
55
+ try {
56
+ const { protocol, hostname } = new URL(origin);
57
+ return protocol === 'http:' && (hostname === 'localhost' || hostname === '127.0.0.1');
58
+ } catch {
59
+ return false;
60
+ }
61
+ }
34
62
 
35
63
  // ── Config helpers ──────────────────────────────────────────────────────
36
64
 
@@ -83,6 +111,24 @@ export async function getApiKey(profile) {
83
111
  return null;
84
112
  }
85
113
 
114
+ export async function probeLoginPage(platformUrl = getPlatformUrl(), fetchImpl = fetch) {
115
+ const url = `${platformUrl.replace(/\/$/, '')}/cli/auth`;
116
+ try {
117
+ const res = await fetchImpl(url, {
118
+ method: 'GET',
119
+ redirect: 'manual',
120
+ signal: AbortSignal.timeout(8000),
121
+ });
122
+ return res.status;
123
+ } catch {
124
+ return null;
125
+ }
126
+ }
127
+
128
+ export function isLoginMissing(status) {
129
+ return status === 404;
130
+ }
131
+
86
132
  // ── Browser opener ──────────────────────────────────────────────────────
87
133
 
88
134
  function openBrowser(url) {
@@ -114,15 +160,13 @@ function startCallbackServer(expectedState) {
114
160
  });
115
161
 
116
162
  const server = http.createServer((req, res) => {
117
- // CORS: only allow the web app's origin (production or localhost dev).
163
+ // CORS: only allow known web-app origins (or localhost dev).
118
164
  // This prevents arbitrary websites from hitting this callback.
119
165
  const origin = req.headers.origin || '';
120
- const allowed =
121
- origin === PLATFORM_URL ||
122
- origin.startsWith('http://localhost:');
166
+ const allowed = isAllowedCallbackOrigin(origin);
123
167
  res.setHeader(
124
168
  'Access-Control-Allow-Origin',
125
- allowed ? origin : PLATFORM_URL,
169
+ allowed ? origin : getPlatformUrl(),
126
170
  );
127
171
  res.setHeader('Access-Control-Allow-Methods', 'GET, OPTIONS');
128
172
  res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
@@ -279,10 +323,18 @@ export async function loginCommand(_argv = [], options = {}) {
279
323
  );
280
324
  console.log();
281
325
 
326
+ const platformUrl = getPlatformUrl();
327
+ const loginStatus = await probeLoginPage(platformUrl);
328
+ if (isLoginMissing(loginStatus)) {
329
+ printLoginUpgradeWarning();
330
+ process.exitCode = 1;
331
+ return;
332
+ }
333
+
282
334
  const state = crypto.randomBytes(16).toString('hex');
283
335
  const { port, promise } = await startCallbackServer(state);
284
336
 
285
- const authUrl = `${PLATFORM_URL}/cli/auth?port=${port}&state=${state}`;
337
+ const authUrl = `${platformUrl}/cli/auth?port=${port}&state=${state}`;
286
338
 
287
339
  console.log(` ${c.dim}Opening browser to sign in...${c.reset}`);
288
340
  console.log();
@@ -326,9 +378,8 @@ export async function loginCommand(_argv = [], options = {}) {
326
378
  console.log(` ${c.dim}Saved to ~/.subconscious/config.json${c.reset}`);
327
379
  }
328
380
  console.log(` ${c.dim}Runbook profile: ${profile.path}${c.reset}`);
329
- const setup =
330
- profileName === DEFAULT_PROFILE ? 'subc setup' : `subc --profile ${profileName} setup`;
331
- console.log(` ${c.dim}Run ${setup} once to configure all coding agents.${c.reset}`);
381
+ 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}`);
382
+ 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}`);
332
383
  console.log();
333
384
  } catch (error) {
334
385
  clearInterval(spinner);
@@ -408,11 +459,16 @@ export async function whoamiCommand(_argv = [], options = {}) {
408
459
 
409
460
  // Validate the key against the server; falls back to offline display if unreachable
410
461
  try {
411
- const res = await fetch(`${PLATFORM_URL}/api/cli/whoami`, {
462
+ const res = await fetch(`${getPlatformUrl()}/api/cli/whoami`, {
412
463
  headers: { Authorization: `Bearer ${key}` },
413
464
  signal: AbortSignal.timeout(5000),
414
465
  });
415
466
 
467
+ if (res.status === 404) {
468
+ printLoginUpgradeWarning();
469
+ return;
470
+ }
471
+
416
472
  if (res.ok) {
417
473
  const data = await res.json();
418
474
  console.log(` ${c.green}✓ Authenticated${c.reset}`);
package/bin/cli.js CHANGED
@@ -3,7 +3,7 @@
3
3
  /**
4
4
  * Subconscious CLI — log in, then launch coding agents on your hosted models.
5
5
  *
6
- * subc login | update-key | logout | whoami — manage your API key
6
+ * subc login | update-key | logout | whoami | upgrade — manage your API key
7
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.
@@ -18,22 +18,29 @@ import {
18
18
  updateApiKeyCommand,
19
19
  whoamiCommand,
20
20
  } from './auth.js';
21
+ import { upgradeCommand } from './upgrade.js';
21
22
  import {
22
23
  resolveAgent,
23
24
  runAgent,
24
25
  agentList,
25
- parseSetupRequest,
26
+ agentCommandName,
26
27
  isAgentHelpRequest,
28
+ parseAgentAction,
27
29
  } from './agents.js';
28
30
  import {
29
31
  configCommand,
30
32
  DEFAULT_PROFILE,
31
33
  loadProfile,
32
34
  modelsCommand,
35
+ printConfigHelp,
33
36
  updateUrlCommand,
34
37
  validateProfileName,
35
38
  } from './profiles.js';
36
39
 
40
+ function isHelpArg(arg) {
41
+ return arg === 'help' || arg === '-h' || arg === '--help';
42
+ }
43
+
37
44
  function printHelp() {
38
45
  const agents = agentList()
39
46
  .map(({ name, alias, action }) => ` ${c.cyan}${alias.padEnd(13)}${c.reset}${c.dim}${action} ${name}${c.reset}`)
@@ -43,6 +50,7 @@ function printHelp() {
43
50
 
44
51
  ${c.bold}Usage${c.reset}
45
52
  ${c.cyan}subc${c.reset} <command> [...args]
53
+ ${c.cyan}subc${c.reset} <command> help
46
54
 
47
55
  ${c.bold}Auth${c.reset}
48
56
  ${c.cyan}login${c.reset} Authenticate and save your API key
@@ -50,12 +58,10 @@ function printHelp() {
50
58
  ${c.cyan}update-url${c.reset} Update the active profile's gateway URL automatically
51
59
  ${c.cyan}logout${c.reset} Remove saved credentials
52
60
  ${c.cyan}whoami${c.reset} Show current authentication status
61
+ ${c.cyan}upgrade${c.reset} Upgrade this CLI to the latest version
53
62
 
54
- ${c.bold}Setup and profiles${c.reset}
55
- ${c.cyan}help <agent>${c.reset} Show coding-agent integration help and settings
56
- ${c.cyan}setup${c.reset} Configure all or one coding-agent integration
57
- ${c.cyan}config${c.reset} Show or update the selected runbook profile
58
- ${c.cyan}settings${c.reset} Edit profile and per-agent settings interactively
63
+ ${c.bold}Profiles${c.reset}
64
+ ${c.cyan}config${c.reset} List profiles, or show/edit one with ${c.dim}-p${c.reset}
59
65
  ${c.cyan}models${c.reset} List available Subconscious models
60
66
 
61
67
  ${c.bold}Coding agents${c.reset}
@@ -69,17 +75,103 @@ ${agents}
69
75
 
70
76
  ${c.bold}Examples${c.reset}
71
77
  ${c.dim}$${c.reset} subc login
72
- ${c.dim}$${c.reset} subc update-key sk-...
73
- ${c.dim}$${c.reset} subc update-url https://api.subconscious.dev
74
- ${c.dim}$${c.reset} subc setup
75
- ${c.dim}$${c.reset} subc settings
76
- ${c.dim}$${c.reset} subc models
77
- ${c.dim}$${c.reset} subc help codex
78
+ ${c.dim}$${c.reset} subc upgrade
79
+ ${c.dim}$${c.reset} subc upgrade --latest
80
+ ${c.dim}$${c.reset} subc config
81
+ ${c.dim}$${c.reset} subc config help
82
+ ${c.dim}$${c.reset} subc -p staging config
83
+ ${c.dim}$${c.reset} subc config edit vim
78
84
  ${c.dim}$${c.reset} subc claude
79
- ${c.dim}$${c.reset} subc --profile staging codex
80
- ${c.dim}$${c.reset} subc codex --model subconscious/glm-5.2
85
+ ${c.dim}$${c.reset} subc claude help
86
+ ${c.dim}$${c.reset} subc cursor install
87
+ ${c.dim}$${c.reset} subc cursor uninstall
88
+ ${c.dim}$${c.reset} subc pi install
89
+ ${c.dim}$${c.reset} subc -p staging codex
90
+
91
+ ${c.dim}Use subc <command> help for command-specific usage.${c.reset}
92
+ `);
93
+ }
94
+
95
+ const COMMAND_HELP = {
96
+ login: `
97
+ Usage:
98
+ subc login
99
+ subc -p NAME login
100
+ subc login help
101
+
102
+ Authenticate and save an API key to the selected profile.
103
+ `,
104
+ logout: `
105
+ Usage:
106
+ subc logout
107
+ subc -p NAME logout
108
+ subc logout help
109
+
110
+ Remove the selected profile's saved API key. Non-secret settings are kept.
111
+ `,
112
+ whoami: `
113
+ Usage:
114
+ subc whoami
115
+ subc -p NAME whoami
116
+ subc whoami help
117
+
118
+ Show the current authentication status for the selected profile.
119
+ `,
120
+ upgrade: `
121
+ Usage:
122
+ subc upgrade
123
+ subc upgrade --latest
124
+ subc upgrade help
125
+
126
+ Upgrade subconscious-cli to the latest published version.
127
+
128
+ subc upgrade Prompt "Do you want to upgrade?" then install @latest
129
+ subc upgrade --latest Skip the prompt and install @latest
130
+ `,
131
+ 'update-key': `
132
+ Usage:
133
+ subc update-key <api-key>
134
+ subc -p NAME update-key <api-key>
135
+ subc update-key help
136
+
137
+ Replace the selected profile's shared API key.
138
+ `,
139
+ 'update-url': `
140
+ Usage:
141
+ subc update-url <gateway-url>
142
+ subc update-url help
143
+
144
+ Update the active profile's gateway URL.
145
+ `,
146
+ models: `
147
+ Usage:
148
+ subc models
149
+ subc models help
150
+
151
+ List available Subconscious models.
152
+ `,
153
+ };
154
+
155
+ function printRetiredSetup(args = []) {
156
+ const maybeAgent = resolveAgent(args[0]);
157
+ const command = maybeAgent ? agentCommandName(maybeAgent) : 'cursor';
158
+ console.error(`
159
+ ${c.red}subc setup is no longer used.${c.reset} Use the agent command instead:
81
160
 
82
- ${c.dim}Arguments are forwarded to terminal agents or their runbook setup.${c.reset}
161
+ ${c.cyan}subc ${command} install${c.reset}
162
+ ${c.cyan}subc ${command} uninstall${c.reset}
163
+ ${c.cyan}subc ${command} help${c.reset}
164
+ `);
165
+ }
166
+
167
+ function printRetiredSettings() {
168
+ console.error(`
169
+ ${c.red}subc settings is no longer used.${c.reset} Use:
170
+
171
+ ${c.cyan}subc config${c.reset} List profiles
172
+ ${c.cyan}subc -p NAME config${c.reset} Show a profile
173
+ ${c.cyan}subc -p NAME config edit${c.reset} Open the profile env file
174
+ ${c.cyan}subc config help${c.reset}
83
175
  `);
84
176
  }
85
177
 
@@ -91,10 +183,8 @@ const authCommands = {
91
183
  };
92
184
 
93
185
  function extractProfile(argv) {
94
- let profileName =
95
- process.env.SUBC_PROFILE?.trim() ||
96
- process.env.MBTA_PROFILE?.trim() ||
97
- DEFAULT_PROFILE;
186
+ let profileName = process.env.SUBC_PROFILE?.trim() || DEFAULT_PROFILE;
187
+ let profileExplicit = false;
98
188
  const args = [];
99
189
  for (let i = 0; i < argv.length; i++) {
100
190
  const arg = argv[i];
@@ -106,39 +196,41 @@ function extractProfile(argv) {
106
196
  const value = argv[++i];
107
197
  if (!value) throw new Error(`${arg} requires a profile name`);
108
198
  profileName = value;
199
+ profileExplicit = true;
109
200
  continue;
110
201
  }
111
202
  if (arg.startsWith('--profile=')) {
112
203
  profileName = arg.slice('--profile='.length);
204
+ profileExplicit = true;
113
205
  continue;
114
206
  }
115
207
  args.push(arg);
116
208
  }
117
209
  validateProfileName(profileName);
118
- return { args, profileName };
210
+ return { args, profileName, profileExplicit };
119
211
  }
120
212
 
121
213
  function requireNamedProfile(profile) {
122
214
  if (profile.name !== DEFAULT_PROFILE && !profile.exists) {
123
215
  throw new Error(
124
216
  `Profile '${profile.name}' does not exist. Create it with ` +
125
- `subc --profile ${profile.name} config --api-key KEY`,
217
+ `subc -p ${profile.name} config --api-key KEY`,
126
218
  );
127
219
  }
128
220
  }
129
221
 
130
222
  async function main() {
131
223
  const parsed = extractProfile(process.argv.slice(2));
132
- const { args, profileName } = parsed;
224
+ const { args, profileName, profileExplicit } = parsed;
133
225
  const command = args[0];
134
226
 
135
- if (!command || command === '--help' || command === '-h') {
227
+ if (!command || command === '--help' || command === '-h' || (command === 'help' && !args[1])) {
136
228
  printHelp();
137
229
  return;
138
230
  }
139
231
 
140
232
  if (command === 'help') {
141
- if (!args[1]) {
233
+ if (!args[1] || isHelpArg(args[1])) {
142
234
  printHelp();
143
235
  return;
144
236
  }
@@ -156,94 +248,76 @@ async function main() {
156
248
  return;
157
249
  }
158
250
 
251
+ if (command === 'setup') {
252
+ printRetiredSetup(args.slice(1));
253
+ process.exitCode = 1;
254
+ return;
255
+ }
256
+
257
+ if (command === 'settings') {
258
+ printRetiredSettings();
259
+ process.exitCode = 1;
260
+ return;
261
+ }
262
+
159
263
  const authHandler = authCommands[command];
160
264
  if (authHandler) {
265
+ if (isHelpArg(args[1])) {
266
+ console.log(COMMAND_HELP[command]);
267
+ return;
268
+ }
161
269
  const profile = await loadProfile(profileName);
162
270
  await authHandler(args.slice(1), { profile, profileName });
163
271
  return;
164
272
  }
165
273
 
166
274
  if (command === 'config') {
167
- await configCommand(args.slice(1), profileName);
168
- return;
169
- }
170
-
171
- if (command === 'settings') {
172
- if (['help', '-h', '--help'].includes(args[1])) {
173
- console.log(`
174
- Usage:
175
- subc settings
176
- subc --profile NAME settings
177
- subc --profile NAME config interactive
178
-
179
- Interactively choose or create a profile, then edit shared or per-agent settings.
180
- `);
275
+ if (isHelpArg(args[1])) {
276
+ printConfigHelp();
181
277
  return;
182
278
  }
183
- if (args.length > 1) throw new Error('Usage: subc [--profile NAME] settings');
184
- await configCommand(['interactive'], profileName);
279
+ await configCommand(args.slice(1), profileName, { profileExplicit });
185
280
  return;
186
281
  }
187
282
 
188
283
  if (command === 'models') {
284
+ if (isHelpArg(args[1])) {
285
+ console.log(COMMAND_HELP.models);
286
+ return;
287
+ }
189
288
  modelsCommand();
190
289
  return;
191
290
  }
192
291
 
193
- if (command === 'update-url') {
194
- await updateUrlCommand(args.slice(1), { profileName });
292
+ if (command === 'upgrade') {
293
+ if (isHelpArg(args[1])) {
294
+ console.log(COMMAND_HELP.upgrade);
295
+ return;
296
+ }
297
+ await upgradeCommand(args.slice(1));
195
298
  return;
196
299
  }
197
300
 
198
- if (command === 'setup') {
199
- const setupArgs = args.slice(1);
200
- if (setupArgs[0] === '-h' || setupArgs[0] === '--help') {
201
- console.log(`
202
- Usage:
203
- subc setup [install|status|uninstall]
204
- subc setup AGENT [install|status|uninstall] [agent options]
205
-
206
- Examples:
207
- subc setup Configure every coding-agent integration
208
- subc setup status Show every integration's setup status
209
- subc setup codex Configure only Codex
210
- subc setup codex status Show only Codex's setup status
211
- subc setup codex --subagents Configure Codex's legacy subagent mode
212
- subc setup codex env Print persistent Codex exports for sourcing
213
- `);
301
+ if (command === 'update-url') {
302
+ if (isHelpArg(args[1])) {
303
+ console.log(COMMAND_HELP['update-url']);
214
304
  return;
215
305
  }
216
- const request = parseSetupRequest(setupArgs);
217
- const profile = await loadProfile(profileName);
218
- const targetHelp = request.targeted && isAgentHelpRequest(request.args);
219
- const persistentHelper = ['use', 'env', 'unset'].includes(request.action);
220
- const oneOffApiKey = request.targeted && request.args.includes('--api-key');
221
- if (!targetHelp && !persistentHelper && !oneOffApiKey) requireNamedProfile(profile);
222
- const failures = [];
223
- for (const agent of request.agents) {
224
- const code = await runAgent(agent, request.args, { profile, setup: true });
225
- if (code) failures.push(agent.name);
226
- }
227
- if (failures.length) {
228
- throw new Error(`Setup failed for: ${failures.join(', ')}`);
229
- }
230
- if (targetHelp || persistentHelper) return;
231
- const subject = request.targeted ? request.agents[0].name : 'Coding-agent';
232
- const message =
233
- request.action === 'status'
234
- ? `${subject} status check complete.`
235
- : request.action === 'uninstall'
236
- ? `${subject} integration${request.targeted ? '' : 's'} removed.`
237
- : `${subject} setup complete.`;
238
- console.log(`\n ${c.green}${c.bold}✓ ${message}${c.reset}\n`);
306
+ await updateUrlCommand(args.slice(1), { profileName });
239
307
  return;
240
308
  }
241
309
 
242
310
  const agent = resolveAgent(command);
243
311
  if (agent) {
312
+ const agentArgs = args.slice(1);
244
313
  const profile = await loadProfile(profileName);
245
- if (!isAgentHelpRequest(args.slice(1))) requireNamedProfile(profile);
246
- await runAgent(agent, args.slice(1), { profile });
314
+ if (!isAgentHelpRequest(agentArgs)) {
315
+ const action = parseAgentAction(agent, agentArgs);
316
+ if (action.action !== 'status' && action.action !== 'uninstall') {
317
+ requireNamedProfile(profile);
318
+ }
319
+ }
320
+ await runAgent(agent, agentArgs, { profile });
247
321
  return;
248
322
  }
249
323