subconscious-cli 0.3.1 → 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,12 +31,17 @@ 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';
34
36
 
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.
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.
38
45
  const CALLBACK_ORIGINS = new Set([
39
46
  'https://www.subconscious.dev',
40
47
  'https://platform.subconscious.dev',
@@ -42,7 +49,7 @@ const CALLBACK_ORIGINS = new Set([
42
49
  'https://platform-dev.subconscious.dev',
43
50
  ]);
44
51
 
45
- export function isAllowedCallbackOrigin(origin, platformUrl = PLATFORM_URL) {
52
+ export function isAllowedCallbackOrigin(origin, platformUrl = getPlatformUrl()) {
46
53
  if (!origin) return false;
47
54
  if (origin === platformUrl || CALLBACK_ORIGINS.has(origin)) return true;
48
55
  try {
@@ -104,6 +111,24 @@ export async function getApiKey(profile) {
104
111
  return null;
105
112
  }
106
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
+
107
132
  // ── Browser opener ──────────────────────────────────────────────────────
108
133
 
109
134
  function openBrowser(url) {
@@ -141,7 +166,7 @@ function startCallbackServer(expectedState) {
141
166
  const allowed = isAllowedCallbackOrigin(origin);
142
167
  res.setHeader(
143
168
  'Access-Control-Allow-Origin',
144
- allowed ? origin : PLATFORM_URL,
169
+ allowed ? origin : getPlatformUrl(),
145
170
  );
146
171
  res.setHeader('Access-Control-Allow-Methods', 'GET, OPTIONS');
147
172
  res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
@@ -298,10 +323,18 @@ export async function loginCommand(_argv = [], options = {}) {
298
323
  );
299
324
  console.log();
300
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
+
301
334
  const state = crypto.randomBytes(16).toString('hex');
302
335
  const { port, promise } = await startCallbackServer(state);
303
336
 
304
- const authUrl = `${PLATFORM_URL}/cli/auth?port=${port}&state=${state}`;
337
+ const authUrl = `${platformUrl}/cli/auth?port=${port}&state=${state}`;
305
338
 
306
339
  console.log(` ${c.dim}Opening browser to sign in...${c.reset}`);
307
340
  console.log();
@@ -426,11 +459,16 @@ export async function whoamiCommand(_argv = [], options = {}) {
426
459
 
427
460
  // Validate the key against the server; falls back to offline display if unreachable
428
461
  try {
429
- const res = await fetch(`${PLATFORM_URL}/api/cli/whoami`, {
462
+ const res = await fetch(`${getPlatformUrl()}/api/cli/whoami`, {
430
463
  headers: { Authorization: `Bearer ${key}` },
431
464
  signal: AbortSignal.timeout(5000),
432
465
  });
433
466
 
467
+ if (res.status === 404) {
468
+ printLoginUpgradeWarning();
469
+ return;
470
+ }
471
+
434
472
  if (res.ok) {
435
473
  const data = await res.json();
436
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,6 +18,7 @@ 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,
@@ -57,6 +58,7 @@ function printHelp() {
57
58
  ${c.cyan}update-url${c.reset} Update the active profile's gateway URL automatically
58
59
  ${c.cyan}logout${c.reset} Remove saved credentials
59
60
  ${c.cyan}whoami${c.reset} Show current authentication status
61
+ ${c.cyan}upgrade${c.reset} Upgrade this CLI to the latest version
60
62
 
61
63
  ${c.bold}Profiles${c.reset}
62
64
  ${c.cyan}config${c.reset} List profiles, or show/edit one with ${c.dim}-p${c.reset}
@@ -73,6 +75,8 @@ ${agents}
73
75
 
74
76
  ${c.bold}Examples${c.reset}
75
77
  ${c.dim}$${c.reset} subc login
78
+ ${c.dim}$${c.reset} subc upgrade
79
+ ${c.dim}$${c.reset} subc upgrade --latest
76
80
  ${c.dim}$${c.reset} subc config
77
81
  ${c.dim}$${c.reset} subc config help
78
82
  ${c.dim}$${c.reset} subc -p staging config
@@ -112,6 +116,17 @@ Usage:
112
116
  subc whoami help
113
117
 
114
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
115
130
  `,
116
131
  'update-key': `
117
132
  Usage:
@@ -274,6 +289,15 @@ async function main() {
274
289
  return;
275
290
  }
276
291
 
292
+ if (command === 'upgrade') {
293
+ if (isHelpArg(args[1])) {
294
+ console.log(COMMAND_HELP.upgrade);
295
+ return;
296
+ }
297
+ await upgradeCommand(args.slice(1));
298
+ return;
299
+ }
300
+
277
301
  if (command === 'update-url') {
278
302
  if (isHelpArg(args[1])) {
279
303
  console.log(COMMAND_HELP['update-url']);
package/bin/upgrade.js ADDED
@@ -0,0 +1,164 @@
1
+ /**
2
+ * Self-update for the Subconscious CLI (`subc upgrade`).
3
+ *
4
+ * subc upgrade Interactive confirm, then install @latest
5
+ * subc upgrade --latest Skip the prompt and install @latest
6
+ */
7
+
8
+ import { spawn } from 'node:child_process';
9
+ import fs from 'node:fs/promises';
10
+ import readline from 'node:readline';
11
+ import { fileURLToPath } from 'node:url';
12
+ import { c } from './colors.js';
13
+
14
+ export const PACKAGE_NAME = 'subconscious-cli';
15
+ export const MIN_LOGIN_VERSION = '4.0';
16
+
17
+ export function parseUpgradeArgs(argv = []) {
18
+ const latest = argv.includes('--latest');
19
+ const unknown = argv.filter((arg) => arg !== '--latest');
20
+ if (unknown.length) {
21
+ throw new Error(`Unknown argument: ${unknown[0]}\nUsage: subc upgrade [--latest]`);
22
+ }
23
+ return { latest };
24
+ }
25
+
26
+ export function detectInstallCommand(here = fileURLToPath(import.meta.url)) {
27
+ const normalized = here.replace(/\\/g, '/');
28
+ if (normalized.includes('/.pnpm/') || normalized.includes('/pnpm/global/')) {
29
+ return `pnpm add -g ${PACKAGE_NAME}@latest`;
30
+ }
31
+ if (normalized.includes('/.yarn/') || normalized.includes('/yarn/global/')) {
32
+ return `yarn global add ${PACKAGE_NAME}@latest`;
33
+ }
34
+ if (normalized.includes('/.bun/install/global/')) {
35
+ return `bun add -g ${PACKAGE_NAME}@latest`;
36
+ }
37
+ return `npm install -g ${PACKAGE_NAME}@latest`;
38
+ }
39
+
40
+ export async function currentCliVersion() {
41
+ const pkg = JSON.parse(await fs.readFile(new URL('../package.json', import.meta.url), 'utf-8'));
42
+ return pkg.version;
43
+ }
44
+
45
+ export function compareVersions(a, b) {
46
+ const pa = String(a).split('.').map((part) => Number.parseInt(part, 10) || 0);
47
+ const pb = String(b).split('.').map((part) => Number.parseInt(part, 10) || 0);
48
+ const n = Math.max(pa.length, pb.length);
49
+ for (let i = 0; i < n; i++) {
50
+ const da = pa[i] || 0;
51
+ const db = pb[i] || 0;
52
+ if (da > db) return 1;
53
+ if (da < db) return -1;
54
+ }
55
+ return 0;
56
+ }
57
+
58
+ export async function fetchLatestVersion(fetchImpl = fetch) {
59
+ const res = await fetchImpl(`https://registry.npmjs.org/${PACKAGE_NAME}/latest`, {
60
+ signal: AbortSignal.timeout(10000),
61
+ });
62
+ if (!res.ok) {
63
+ throw new Error(`Could not look up ${PACKAGE_NAME} on npm (${res.status}).`);
64
+ }
65
+ const data = await res.json();
66
+ if (!data?.version) throw new Error(`npm did not return a version for ${PACKAGE_NAME}.`);
67
+ return data.version;
68
+ }
69
+
70
+ export function printLoginUpgradeWarning() {
71
+ console.error();
72
+ console.error(
73
+ ` ${c.yellow}Upgrade to version ${MIN_LOGIN_VERSION} to login.${c.reset}`,
74
+ );
75
+ console.error(` This login URL is no longer available (404).`);
76
+ console.error();
77
+ console.error(` Run ${c.cyan}subc upgrade --latest${c.reset}`);
78
+ console.error();
79
+ }
80
+
81
+ function askYesNo(question) {
82
+ return new Promise((resolve) => {
83
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
84
+ rl.question(question, (answer) => {
85
+ rl.close();
86
+ const a = answer.trim().toLowerCase();
87
+ resolve(a === '' || a === 'y' || a === 'yes');
88
+ });
89
+ });
90
+ }
91
+
92
+ function runInstall(command) {
93
+ return new Promise((resolve) => {
94
+ const child = spawn(command, { shell: true, stdio: 'inherit' });
95
+ child.on('error', () => resolve(false));
96
+ child.on('exit', (code) => resolve(code === 0));
97
+ });
98
+ }
99
+
100
+ export async function installLatest(options = {}) {
101
+ const install = options.install || runInstall;
102
+ const fetchLatest = options.fetchLatest || fetchLatestVersion;
103
+ const current = options.currentVersion || (await currentCliVersion());
104
+ const command = options.command || detectInstallCommand();
105
+
106
+ let latest;
107
+ try {
108
+ latest = await fetchLatest();
109
+ } catch (error) {
110
+ console.error(` ${c.yellow}${error.message}${c.reset}`);
111
+ console.error(` ${c.dim}Installing @latest anyway.${c.reset}\n`);
112
+ }
113
+
114
+ if (latest && compareVersions(current, latest) >= 0) {
115
+ console.log(`\n ${c.green}Already up to date${c.reset} ${c.dim}(${current}).${c.reset}\n`);
116
+ return true;
117
+ }
118
+
119
+ if (latest) {
120
+ console.log(
121
+ `\n Upgrading ${c.cyan}${PACKAGE_NAME}${c.reset} ${c.dim}${current}${c.reset} → ${c.cyan}${latest}${c.reset}`,
122
+ );
123
+ } else {
124
+ console.log(`\n Upgrading ${c.cyan}${PACKAGE_NAME}${c.reset} ${c.dim}${current}${c.reset} → latest`);
125
+ }
126
+ console.log(` ${c.dim}Running${c.reset} ${c.cyan}${command}${c.reset}\n`);
127
+
128
+ const ok = await install(command);
129
+ if (!ok) {
130
+ console.error(`\n ${c.red}Upgrade failed.${c.reset} Try it manually:\n`);
131
+ console.error(` ${c.cyan}${command}${c.reset}\n`);
132
+ return false;
133
+ }
134
+
135
+ console.log(`\n ${c.green}${c.bold}✓ Upgraded.${c.reset} Re-run ${c.cyan}subc login${c.reset} if you were signing in.\n`);
136
+ return true;
137
+ }
138
+
139
+ export async function upgradeCommand(argv = [], options = {}) {
140
+ const { latest } = parseUpgradeArgs(argv);
141
+ const interactive = options.interactive ?? (process.stdin.isTTY && process.stdout.isTTY);
142
+ const ask = options.ask || askYesNo;
143
+ const installLatestFn = options.installLatest || installLatest;
144
+
145
+ if (!latest) {
146
+ if (!interactive) {
147
+ console.error(
148
+ `\n ${c.yellow}Do you want to upgrade?${c.reset} requires a TTY.`,
149
+ );
150
+ console.error(` Run ${c.cyan}subc upgrade --latest${c.reset} instead.\n`);
151
+ process.exitCode = 1;
152
+ return;
153
+ }
154
+
155
+ const ok = await ask(` Do you want to upgrade? ${c.dim}[Y/n]${c.reset} `);
156
+ if (!ok) {
157
+ console.log(`\n ${c.dim}Upgrade cancelled.${c.reset}\n`);
158
+ return;
159
+ }
160
+ }
161
+
162
+ const succeeded = await installLatestFn();
163
+ if (!succeeded) process.exitCode = 1;
164
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "subconscious-cli",
3
- "version": "0.3.1",
3
+ "version": "4.0.0",
4
4
  "description": "CLI for Subconscious — run Claude Code, Codex, OpenCode, Cursor, Copilot, and Pi",
5
5
  "bin": {
6
6
  "subc": "./bin/cli.js"