subconscious-cli 4.0.17 → 4.1.0-windows.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 +125 -7
- package/bin/agents.js +10 -1
- package/bin/auth.js +15 -5
- package/bin/cli.js +69 -0
- 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/profiles.js +60 -16
- package/bin/registry.generated.json +14 -4
- package/bin/runbook/claude-code/install.sh +4 -4
- package/bin/runbook/claude-code/run.sh +3 -3
- package/bin/runbook/codex/run.sh +3 -2
- package/bin/runbook/copilot/install.sh +6 -5
- package/bin/runbook/cursor/install.sh +3 -2
- package/bin/runbook/deepseek-harness/run.sh +3 -2
- package/bin/runbook/opencode/install.sh +6 -5
- package/bin/runbook/opencode/run.sh +3 -2
- package/bin/runbook/pi/install.sh +4 -3
- package/bin/runbook/pi/run.sh +1 -1
- package/bin/runbook/subconscious-code/run.sh +1 -1
- package/bin/sessions.js +622 -0
- package/bin/tui.js +27 -1
- package/bin/update-check.js +90 -17
- package/bin/upgrade.js +27 -11
- package/bin/usage.js +229 -0
- package/bin/windows/agents.js +87 -0
- package/bin/windows/claude.js +71 -0
- package/bin/windows/common.js +58 -0
- package/bin/windows/hook.cjs +71 -0
- package/bin/windows/install.js +97 -0
- package/bin/windows/launch.js +97 -0
- package/bin/windows/pi-compaction.ts +29 -0
- package/bin/windows/process.js +157 -0
- package/bin/windows/setup.js +168 -0
- package/package.json +2 -1
package/bin/update-check.js
CHANGED
|
@@ -1,16 +1,69 @@
|
|
|
1
1
|
import fs from 'node:fs/promises';
|
|
2
2
|
import { spawn } from 'node:child_process';
|
|
3
|
+
import { spawnWindows } from './windows/process.js';
|
|
3
4
|
import readline from 'node:readline';
|
|
4
5
|
import { fileURLToPath } from 'node:url';
|
|
5
6
|
import { LOGO_ART_SMALL_LINES } from './branding.js';
|
|
6
7
|
import { c, colorEnabled } from './colors.js';
|
|
7
|
-
import { detectInstallCommand } from './upgrade.js';
|
|
8
8
|
|
|
9
9
|
export const PACKAGE_NAME = 'subconscious-cli';
|
|
10
10
|
export const UPDATE_CHECK_TIMEOUT_MS = 1500;
|
|
11
11
|
const UPDATE_ACTIONS = ['Update now', 'Skip for now'];
|
|
12
12
|
const UPDATE_LOGO = LOGO_ART_SMALL_LINES;
|
|
13
13
|
|
|
14
|
+
function shellQuote(value) {
|
|
15
|
+
const text = String(value);
|
|
16
|
+
return /^[-/@A-Za-z0-9._:]+$/.test(text)
|
|
17
|
+
? text
|
|
18
|
+
: `'${text.replaceAll("'", `'"'"'`)}'`;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function detectInstallTarget(
|
|
22
|
+
here = fileURLToPath(import.meta.url),
|
|
23
|
+
platform = process.platform,
|
|
24
|
+
) {
|
|
25
|
+
const normalized = here.replace(/\\/g, '/');
|
|
26
|
+
if (normalized.includes('/.pnpm/') || normalized.includes('/pnpm/global/')) {
|
|
27
|
+
return {
|
|
28
|
+
command: 'pnpm',
|
|
29
|
+
args: ['add', '-g', `${PACKAGE_NAME}@latest`],
|
|
30
|
+
display: `pnpm add -g ${PACKAGE_NAME}@latest`,
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
if (normalized.includes('/.yarn/') || normalized.includes('/yarn/global/')) {
|
|
34
|
+
return {
|
|
35
|
+
command: 'yarn',
|
|
36
|
+
args: ['global', 'add', `${PACKAGE_NAME}@latest`],
|
|
37
|
+
display: `yarn global add ${PACKAGE_NAME}@latest`,
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
if (normalized.includes('/.bun/install/global/')) {
|
|
41
|
+
return {
|
|
42
|
+
command: 'bun',
|
|
43
|
+
args: ['add', '-g', `${PACKAGE_NAME}@latest`],
|
|
44
|
+
display: `bun add -g ${PACKAGE_NAME}@latest`,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const marker = `/lib/node_modules/${PACKAGE_NAME}/`;
|
|
49
|
+
let prefix = normalized.includes(marker) ? normalized.slice(0, normalized.indexOf(marker)) : '';
|
|
50
|
+
if (!prefix && platform === 'win32') {
|
|
51
|
+
const windowsMarker = `/node_modules/${PACKAGE_NAME}/`;
|
|
52
|
+
if (normalized.includes(windowsMarker)) {
|
|
53
|
+
prefix = normalized.slice(0, normalized.indexOf(windowsMarker));
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
const args = ['install', '-g'];
|
|
57
|
+
if (prefix) args.push('--prefix', prefix);
|
|
58
|
+
args.push(`${PACKAGE_NAME}@latest`);
|
|
59
|
+
return {
|
|
60
|
+
command: 'npm',
|
|
61
|
+
args,
|
|
62
|
+
display: ['npm', ...args].map(platform === 'win32' ? value => /^[-/@A-Za-z0-9._:]+$/.test(value) ? value : `"${value}"` : shellQuote).join(' '),
|
|
63
|
+
prefix,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
14
67
|
function parseVersion(version) {
|
|
15
68
|
const match = String(version)
|
|
16
69
|
.trim()
|
|
@@ -123,13 +176,8 @@ export function renderUpdateNotice(installedVersion, latestVersion) {
|
|
|
123
176
|
].join('\n');
|
|
124
177
|
}
|
|
125
178
|
|
|
126
|
-
export function resolveInstallCommand(here = fileURLToPath(import.meta.url)) {
|
|
127
|
-
return detectInstallCommand(here);
|
|
128
|
-
}
|
|
129
|
-
|
|
130
179
|
export function renderUpdateOptions(selectedIndex = 0, versions = {}) {
|
|
131
|
-
const installCommand =
|
|
132
|
-
versions.installCommand || `npm install -g ${PACKAGE_NAME}@latest`;
|
|
180
|
+
const installCommand = versions.installCommand || detectInstallTarget().display;
|
|
133
181
|
const descriptions = [
|
|
134
182
|
`Runs \`${installCommand}\``,
|
|
135
183
|
versions.installedVersion
|
|
@@ -209,12 +257,30 @@ export async function selectUpdateAction(options = {}) {
|
|
|
209
257
|
}
|
|
210
258
|
|
|
211
259
|
export async function installLatest(options = {}) {
|
|
212
|
-
const spawnImpl = options.spawnImpl || spawn;
|
|
213
|
-
const
|
|
260
|
+
const spawnImpl = options.spawnImpl || (process.platform === 'win32' ? spawnWindows : spawn);
|
|
261
|
+
const target = options.target || detectInstallTarget();
|
|
214
262
|
return new Promise((resolve) => {
|
|
215
|
-
const child = spawnImpl(command,
|
|
263
|
+
const child = spawnImpl(target.command, target.args, {
|
|
264
|
+
stdio: 'inherit',
|
|
265
|
+
});
|
|
216
266
|
child.on('error', () => resolve(false));
|
|
217
|
-
child.on('exit', (code) =>
|
|
267
|
+
child.on('exit', async (code) => {
|
|
268
|
+
if (code !== 0) {
|
|
269
|
+
resolve(false);
|
|
270
|
+
return;
|
|
271
|
+
}
|
|
272
|
+
if (!options.expectedVersion) {
|
|
273
|
+
resolve(true);
|
|
274
|
+
return;
|
|
275
|
+
}
|
|
276
|
+
try {
|
|
277
|
+
const readVersion = options.readVersion || currentVersion;
|
|
278
|
+
const actualVersion = await readVersion();
|
|
279
|
+
resolve(compareVersions(actualVersion, options.expectedVersion) >= 0);
|
|
280
|
+
} catch {
|
|
281
|
+
resolve(false);
|
|
282
|
+
}
|
|
283
|
+
});
|
|
218
284
|
});
|
|
219
285
|
}
|
|
220
286
|
|
|
@@ -241,9 +307,13 @@ export async function showUpdateNotice(options = {}) {
|
|
|
241
307
|
options.interactive ?? (process.stdin.isTTY === true && process.stderr.isTTY === true);
|
|
242
308
|
if (!interactive) return { installedVersion, latestVersion, action: 'skip' };
|
|
243
309
|
|
|
244
|
-
const installCommand = options.installCommand || resolveInstallCommand();
|
|
245
310
|
const select = options.select || selectUpdateAction;
|
|
246
|
-
const
|
|
311
|
+
const installTarget = options.installTarget || detectInstallTarget();
|
|
312
|
+
const action = await select({
|
|
313
|
+
installedVersion,
|
|
314
|
+
latestVersion,
|
|
315
|
+
installCommand: installTarget.display,
|
|
316
|
+
});
|
|
247
317
|
if (action === 'cancel') return { installedVersion, latestVersion, action };
|
|
248
318
|
if (action === 'skip') {
|
|
249
319
|
write(`\n ${c.dim}Skipping update.${c.reset}\n\n`);
|
|
@@ -251,8 +321,10 @@ export async function showUpdateNotice(options = {}) {
|
|
|
251
321
|
}
|
|
252
322
|
|
|
253
323
|
write(`\n ${c.cyan}Updating ${PACKAGE_NAME} to ${latestVersion}...${c.reset}\n\n`);
|
|
254
|
-
const install =
|
|
255
|
-
|
|
324
|
+
const install =
|
|
325
|
+
options.install ||
|
|
326
|
+
((expectedVersion) => installLatest({ expectedVersion, target: installTarget }));
|
|
327
|
+
const installed = await install(latestVersion);
|
|
256
328
|
if (installed) {
|
|
257
329
|
write(
|
|
258
330
|
`\n ${c.green}${c.bold}Update complete.${c.reset} Re-run your subc command.\n\n`,
|
|
@@ -260,8 +332,9 @@ export async function showUpdateNotice(options = {}) {
|
|
|
260
332
|
return { installedVersion, latestVersion, action: 'updated' };
|
|
261
333
|
}
|
|
262
334
|
|
|
263
|
-
write(`\n ${c.red}Update failed.${c.reset}
|
|
264
|
-
write(`
|
|
335
|
+
write(`\n ${c.red}Update failed or the running installation stayed unchanged.${c.reset}\n`);
|
|
336
|
+
write(` Try the exact detected install target manually:\n\n`);
|
|
337
|
+
write(` ${c.cyan}${installTarget.display}${c.reset}\n\n`);
|
|
265
338
|
return { installedVersion, latestVersion, action: 'failed' };
|
|
266
339
|
} catch {
|
|
267
340
|
// Update discovery must never prevent the requested command from running.
|
package/bin/upgrade.js
CHANGED
|
@@ -6,10 +6,12 @@
|
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import { spawn } from 'node:child_process';
|
|
9
|
+
import { runWindows } from './windows/process.js';
|
|
9
10
|
import fs from 'node:fs/promises';
|
|
10
11
|
import readline from 'node:readline';
|
|
11
12
|
import { fileURLToPath } from 'node:url';
|
|
12
13
|
import { c } from './colors.js';
|
|
14
|
+
import { detectInstallTarget } from './update-check.js';
|
|
13
15
|
|
|
14
16
|
export const PACKAGE_NAME = 'subconscious-cli';
|
|
15
17
|
export const MIN_LOGIN_VERSION = '4.0';
|
|
@@ -24,17 +26,7 @@ export function parseUpgradeArgs(argv = []) {
|
|
|
24
26
|
}
|
|
25
27
|
|
|
26
28
|
export function detectInstallCommand(here = fileURLToPath(import.meta.url)) {
|
|
27
|
-
|
|
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`;
|
|
29
|
+
return detectInstallTarget(here).display;
|
|
38
30
|
}
|
|
39
31
|
|
|
40
32
|
export async function currentCliVersion() {
|
|
@@ -90,6 +82,10 @@ function askYesNo(question) {
|
|
|
90
82
|
}
|
|
91
83
|
|
|
92
84
|
function runInstall(command) {
|
|
85
|
+
if (process.platform === 'win32') {
|
|
86
|
+
const target = detectInstallTarget();
|
|
87
|
+
return runWindows(target.command, target.args).then(code => code === 0, () => false);
|
|
88
|
+
}
|
|
93
89
|
return new Promise((resolve) => {
|
|
94
90
|
const child = spawn(command, { shell: true, stdio: 'inherit' });
|
|
95
91
|
child.on('error', () => resolve(false));
|
|
@@ -132,6 +128,26 @@ export async function installLatest(options = {}) {
|
|
|
132
128
|
return false;
|
|
133
129
|
}
|
|
134
130
|
|
|
131
|
+
if (latest) {
|
|
132
|
+
try {
|
|
133
|
+
const readVersion = options.readVersion || currentCliVersion;
|
|
134
|
+
const installedVersion = await readVersion();
|
|
135
|
+
if (compareVersions(installedVersion, latest) < 0) {
|
|
136
|
+
console.error(
|
|
137
|
+
`\n ${c.red}Upgrade command completed, but this installation is still ${installedVersion}.${c.reset}`,
|
|
138
|
+
);
|
|
139
|
+
console.error(` Run the exact detected command manually:\n`);
|
|
140
|
+
console.error(` ${c.cyan}${command}${c.reset}\n`);
|
|
141
|
+
return false;
|
|
142
|
+
}
|
|
143
|
+
} catch {
|
|
144
|
+
console.error(`\n ${c.red}Could not verify the updated CLI installation.${c.reset}`);
|
|
145
|
+
console.error(` Run the exact detected command manually:\n`);
|
|
146
|
+
console.error(` ${c.cyan}${command}${c.reset}\n`);
|
|
147
|
+
return false;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
135
151
|
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
152
|
return true;
|
|
137
153
|
}
|
package/bin/usage.js
ADDED
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Usage display for `subc usage` — fetches billing/quota from the platform API.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { c } from './colors.js';
|
|
6
|
+
import { getApiKey, getPlatformUrl } from './auth.js';
|
|
7
|
+
import { DEFAULT_PROFILE } from './profiles.js';
|
|
8
|
+
import { printLoginUpgradeWarning } from './upgrade.js';
|
|
9
|
+
|
|
10
|
+
export const USAGE_API_PATH = '/api/v1/usage';
|
|
11
|
+
|
|
12
|
+
export function formatTokens(value) {
|
|
13
|
+
const n = Number(value);
|
|
14
|
+
if (!Number.isFinite(n) || n < 0) return '0';
|
|
15
|
+
if (n >= 1_000_000_000) {
|
|
16
|
+
const scaled = n / 1_000_000_000;
|
|
17
|
+
return `${scaled.toFixed(n % 1_000_000_000 ? 1 : 0)}B`;
|
|
18
|
+
}
|
|
19
|
+
if (n >= 1_000_000) {
|
|
20
|
+
const scaled = n / 1_000_000;
|
|
21
|
+
return `${scaled.toFixed(n % 1_000_000 ? 1 : 0)}M`;
|
|
22
|
+
}
|
|
23
|
+
if (n >= 1_000) return `${Math.round(n / 1_000)}K`;
|
|
24
|
+
return n.toLocaleString('en-US');
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function formatCurrency(value) {
|
|
28
|
+
const n = Number(value);
|
|
29
|
+
if (!Number.isFinite(n)) return '$0.00';
|
|
30
|
+
const sign = n < 0 ? '-' : '';
|
|
31
|
+
return `${sign}$${Math.abs(n).toFixed(2)}`;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function shortModelSlug(slug) {
|
|
35
|
+
const text = String(slug ?? '').trim();
|
|
36
|
+
return text.startsWith('subconscious/') ? text.slice('subconscious/'.length) : text;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function isLegacyUsagePayload(data) {
|
|
40
|
+
if (!data || typeof data !== 'object') return true;
|
|
41
|
+
if ('credits' in data && data.plan && typeof data.plan === 'object') return false;
|
|
42
|
+
return 'dailyTokenCeiling' in data || 'consumedToday' in data || !('plan' in data);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function meterColor(percentage) {
|
|
46
|
+
if (percentage >= 100) return c.red;
|
|
47
|
+
if (percentage >= 80) return c.yellow;
|
|
48
|
+
return c.green;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function renderProgressBar(percentage, width = 20) {
|
|
52
|
+
const pct = Math.max(0, Math.min(100, percentage));
|
|
53
|
+
const filled = Math.round((pct / 100) * width);
|
|
54
|
+
const empty = Math.max(0, width - filled);
|
|
55
|
+
const color = meterColor(pct);
|
|
56
|
+
return `${color}${'█'.repeat(filled)}${c.dim}${'░'.repeat(empty)}${c.reset}`;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function formatUsageDisplay(data) {
|
|
60
|
+
const lines = [];
|
|
61
|
+
lines.push(`\n ${c.bold}Subconscious Usage${c.reset}\n`);
|
|
62
|
+
|
|
63
|
+
const billingMode = data.billingMode ?? null;
|
|
64
|
+
const plan = data.plan ?? {};
|
|
65
|
+
const credits = data.credits ?? {};
|
|
66
|
+
const models = Array.isArray(data.models) ? data.models : [];
|
|
67
|
+
|
|
68
|
+
const planLabel = plan.label || 'No plan';
|
|
69
|
+
const planStatus = plan.status ? plan.status.replace(/_/g, ' ') : null;
|
|
70
|
+
const planLine = planStatus ? `${planLabel} · ${planStatus}` : planLabel;
|
|
71
|
+
|
|
72
|
+
lines.push(` ${c.dim}Plan${c.reset} ${planLine}`);
|
|
73
|
+
if (billingMode) {
|
|
74
|
+
lines.push(` ${c.dim}Billing${c.reset} ${billingMode}`);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const allowance = plan.dailyAllowance;
|
|
78
|
+
const unlimitedComp = Boolean(plan.isUnlimitedComp);
|
|
79
|
+
|
|
80
|
+
if (unlimitedComp) {
|
|
81
|
+
lines.push('');
|
|
82
|
+
lines.push(` ${c.dim}Daily allowance${c.reset}`);
|
|
83
|
+
lines.push(` ${c.green}Unlimited${c.reset} ${c.dim}(comp)${c.reset}`);
|
|
84
|
+
} else if (allowance && allowance.amount > 0) {
|
|
85
|
+
const consumed = Number(allowance.consumed) || 0;
|
|
86
|
+
const amount = Number(allowance.amount) || 0;
|
|
87
|
+
const remaining = Number(allowance.remaining) || 0;
|
|
88
|
+
const pct = amount > 0 ? Math.min((consumed / amount) * 100, 100) : 0;
|
|
89
|
+
const overage = Math.max(0, consumed - amount);
|
|
90
|
+
|
|
91
|
+
lines.push('');
|
|
92
|
+
lines.push(` ${c.dim}Daily allowance${c.reset}`);
|
|
93
|
+
lines.push(
|
|
94
|
+
` ${renderProgressBar(pct)} ${formatTokens(consumed)} / ${formatTokens(amount)} tokens`,
|
|
95
|
+
);
|
|
96
|
+
const resetNote = allowance.resetAt
|
|
97
|
+
? `${formatTokens(remaining)} remaining · resets midnight UTC`
|
|
98
|
+
: `${formatTokens(remaining)} remaining`;
|
|
99
|
+
lines.push(` ${c.dim}${resetNote}${c.reset}`);
|
|
100
|
+
if (overage > 0) {
|
|
101
|
+
lines.push(` ${c.yellow}+${formatTokens(overage)} over daily allowance${c.reset}`);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
lines.push('');
|
|
106
|
+
lines.push(
|
|
107
|
+
` ${c.dim}Credits${c.reset} ${formatCurrency(credits.balanceDollars ?? 0)}`,
|
|
108
|
+
);
|
|
109
|
+
if (billingMode === 'subscription' || billingMode === 'comp' || allowance) {
|
|
110
|
+
const overage = Number(credits.overageThisPeriodDollars) || 0;
|
|
111
|
+
const overageColor = overage > 0 ? c.yellow : c.reset;
|
|
112
|
+
lines.push(
|
|
113
|
+
` ${c.dim}Overage${c.reset} ${overageColor}${formatCurrency(overage)} this period${c.reset}`,
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (models.length > 0) {
|
|
118
|
+
lines.push('');
|
|
119
|
+
lines.push(` ${c.dim}Models today${c.reset}`);
|
|
120
|
+
const slugWidth = Math.max(
|
|
121
|
+
12,
|
|
122
|
+
...models.map((model) => shortModelSlug(model.slug).length),
|
|
123
|
+
);
|
|
124
|
+
for (const model of models) {
|
|
125
|
+
const label = shortModelSlug(model.slug).padEnd(slugWidth);
|
|
126
|
+
lines.push(
|
|
127
|
+
` ${label} ${formatTokens(model.consumedToday ?? 0)}`,
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
lines.push('');
|
|
133
|
+
return lines.join('\n');
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export async function fetchUsage(apiKey, platformUrl, fetchImpl = fetch) {
|
|
137
|
+
const res = await fetchImpl(`${platformUrl.replace(/\/$/, '')}${USAGE_API_PATH}`, {
|
|
138
|
+
headers: { Authorization: `Bearer ${apiKey}` },
|
|
139
|
+
signal: AbortSignal.timeout(5000),
|
|
140
|
+
});
|
|
141
|
+
return res;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export async function usageCommand(argv = [], options = {}) {
|
|
145
|
+
const jsonOutput = argv.includes('--json');
|
|
146
|
+
const profileFlag =
|
|
147
|
+
options.profileName && options.profileName !== DEFAULT_PROFILE
|
|
148
|
+
? `--profile ${options.profileName} `
|
|
149
|
+
: '';
|
|
150
|
+
|
|
151
|
+
const auth = await getApiKey(options.profile);
|
|
152
|
+
if (!auth) {
|
|
153
|
+
console.log(`\n ${c.dim}Not logged in.${c.reset}`);
|
|
154
|
+
console.log(` Run ${c.cyan}subc ${profileFlag}login${c.reset} to get started.\n`);
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const platformUrl = getPlatformUrl(options.profile);
|
|
159
|
+
|
|
160
|
+
try {
|
|
161
|
+
const res = await fetchUsage(auth.key, platformUrl);
|
|
162
|
+
|
|
163
|
+
if (res.status === 404) {
|
|
164
|
+
printLoginUpgradeWarning();
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
let data;
|
|
169
|
+
try {
|
|
170
|
+
data = await res.json();
|
|
171
|
+
} catch {
|
|
172
|
+
throw new Error('Platform returned an invalid response');
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
if (!res.ok) {
|
|
176
|
+
const message = data?.error || `Request failed (${res.status})`;
|
|
177
|
+
if (res.status === 401) {
|
|
178
|
+
console.log(`\n ${c.red}✗ ${message}${c.reset}`);
|
|
179
|
+
console.log(
|
|
180
|
+
` Run ${c.cyan}subc ${profileFlag}logout${c.reset} then ${c.cyan}subc ${profileFlag}login${c.reset} to re-authenticate.\n`,
|
|
181
|
+
);
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
if (res.status === 403) {
|
|
185
|
+
console.log(`\n ${c.red}✗ ${message}${c.reset}\n`);
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
throw new Error(message);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
if (isLegacyUsagePayload(data)) {
|
|
192
|
+
console.log(`\n ${c.yellow}Platform upgrade required.${c.reset}`);
|
|
193
|
+
console.log(
|
|
194
|
+
` ${c.dim}This platform host returns a legacy usage shape. Deploy the latest platform or point PLATFORM_URL at a current host.${c.reset}\n`,
|
|
195
|
+
);
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
if (jsonOutput) {
|
|
200
|
+
console.log(JSON.stringify(data, null, 2));
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
console.log(formatUsageDisplay(data));
|
|
205
|
+
} catch (error) {
|
|
206
|
+
if (error.name === 'TimeoutError' || error.name === 'AbortError') {
|
|
207
|
+
console.log(`\n ${c.yellow}Could not reach the platform (timed out).${c.reset}`);
|
|
208
|
+
console.log(` ${c.dim}Host: ${platformUrl}${c.reset}\n`);
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
console.error(`\n ${c.red}${error.message}${c.reset}\n`);
|
|
212
|
+
process.exitCode = 1;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
export function printUsageHelp() {
|
|
217
|
+
console.log(`
|
|
218
|
+
Usage:
|
|
219
|
+
subc usage
|
|
220
|
+
subc -p NAME usage
|
|
221
|
+
subc usage --json
|
|
222
|
+
subc usage help
|
|
223
|
+
|
|
224
|
+
Show billing mode, daily token allowance, credit balance, and per-model usage
|
|
225
|
+
for the authenticated organization.
|
|
226
|
+
|
|
227
|
+
--json Print the raw platform JSON response
|
|
228
|
+
`);
|
|
229
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import os from 'node:os';
|
|
3
|
+
import readline from 'node:readline';
|
|
4
|
+
import { windowsEnv, windowsBinDirs, windowsPath, resolveWindowsExecutable, readWindowsVersion, runWindows } from './process.js';
|
|
5
|
+
import { windowsInstallSpec, installWindowsAgent } from './install.js';
|
|
6
|
+
import { windowsSetup } from './setup.js';
|
|
7
|
+
import { windowsLaunch, executeWindowsLaunch } from './launch.js';
|
|
8
|
+
import { parseOptions } from './common.js';
|
|
9
|
+
|
|
10
|
+
const askInstall = name => new Promise(resolve => {
|
|
11
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
12
|
+
rl.question(` ${name} isn't installed. Install it now? [Y/n] `, answer => { rl.close(); resolve(['', 'y', 'yes'].includes(answer.trim().toLowerCase())); });
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
export async function runWindowsAgent(agent, argv, dependencies) {
|
|
16
|
+
const { profile, parseAgentAction, extractModel, requireApiKey, resolvedModelsForLaunch, selectLaunchModel, runbookEnv } = dependencies;
|
|
17
|
+
const parsed = parseAgentAction(agent, argv);
|
|
18
|
+
let args = parsed.action !== 'launch' && parsed.action === argv[0] ? argv.slice(1) : argv;
|
|
19
|
+
const environment = windowsEnv(profile?.values, process.env);
|
|
20
|
+
const preferredDirs = agent.runbook?.installDir
|
|
21
|
+
? [path.resolve(environment[agent.runbook.installDirEnv] || path.join(os.homedir(), agent.runbook.installDir))] : [];
|
|
22
|
+
environment.PATH = windowsPath([...preferredDirs, ...(environment.PATH || '').split(';'), ...windowsBinDirs(environment)]);
|
|
23
|
+
if (agent.id === 'subconscious-code' && parsed.action === 'install') {
|
|
24
|
+
if (args.length) throw new Error('Usage: subc sc install');
|
|
25
|
+
return finish(await installWindowsAgent(agent.id, environment));
|
|
26
|
+
}
|
|
27
|
+
if (['status', 'uninstall'].includes(parsed.action)) {
|
|
28
|
+
return finish(await windowsSetup(agent.id, parsed.action, args, environment));
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Honor -- after which all arguments belong to the underlying agent.
|
|
32
|
+
const boundary = args.indexOf('--');
|
|
33
|
+
const tail = boundary < 0 ? [] : args.slice(boundary);
|
|
34
|
+
const extracted = extractModel(boundary < 0 ? args : args.slice(0, boundary), profile);
|
|
35
|
+
args = [...extracted.rest, ...tail];
|
|
36
|
+
let explicit = {};
|
|
37
|
+
if (parsed.action === 'install' || agent.id === 'claude-code') {
|
|
38
|
+
const handled = parseOptions(boundary < 0 ? args : args.slice(0, args.indexOf('--')), { '--api-key': 1, '--gateway-url': 1 });
|
|
39
|
+
explicit = handled.options;
|
|
40
|
+
}
|
|
41
|
+
const key = explicit['--api-key'] || await requireApiKey(profile, agent);
|
|
42
|
+
if (!key) return finish(1);
|
|
43
|
+
const catalogProfile = explicit['--gateway-url'] ? { ...profile, values: { ...profile?.values, GATEWAY_URL: explicit['--gateway-url'] } } : profile;
|
|
44
|
+
const catalog = await resolvedModelsForLaunch(catalogProfile, key, extracted.model);
|
|
45
|
+
const model = selectLaunchModel(extracted.model, extracted.modelSource, catalog);
|
|
46
|
+
if (extracted.model && model !== extracted.model) console.error(`Configured model ${extracted.model} is not in the live catalog; using ${model}.`);
|
|
47
|
+
let executable;
|
|
48
|
+
if (parsed.action === 'launch') {
|
|
49
|
+
executable = resolveWindowsExecutable(agent.bin, { env: environment, preferredDirs });
|
|
50
|
+
if (!executable) {
|
|
51
|
+
const installer = windowsInstallSpec(agent.id, environment);
|
|
52
|
+
if (!installer || !process.stdin.isTTY || !process.stdout.isTTY || !await askInstall(agent.name)) {
|
|
53
|
+
console.error(`${agent.name} isn't installed. ${installer ? `Install it with: ${installer.display}` : 'Install it separately, then rerun subc.'}`);
|
|
54
|
+
return finish(127);
|
|
55
|
+
}
|
|
56
|
+
const installed = await installWindowsAgent(agent.id, environment);
|
|
57
|
+
if (installed) return finish(installed);
|
|
58
|
+
executable = resolveWindowsExecutable(agent.bin, { env: environment, preferredDirs });
|
|
59
|
+
if (!executable) {
|
|
60
|
+
console.error(`Installed ${agent.name}, but ${agent.bin} was not found. Add its installation directory to PATH or restart your terminal.`);
|
|
61
|
+
return finish(127);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
if (agent.id === 'claude-code') {
|
|
65
|
+
const version = dependencies.parseClaudeVersion(readWindowsVersion(executable, environment));
|
|
66
|
+
if (dependencies.claudeVersionNeedsUpgrade(version)) console.error(`Minimum supported Claude Code version is ${dependencies.minimumClaudeVersion}; installed version is ${version}. Please upgrade Claude Code.`);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
// Reuse profile/catalog semantics, but use Windows path and environment rules.
|
|
70
|
+
const env = windowsEnv(runbookEnv(key, model, executable && path.dirname(executable), catalogProfile, agent, catalog.models), { PATH: windowsPath(executable ? [path.dirname(executable)] : [], environment.PATH) });
|
|
71
|
+
if (explicit['--gateway-url']) env.GATEWAY_URL = explicit['--gateway-url'];
|
|
72
|
+
if (parsed.action === 'install') return finish(await windowsSetup(agent.id, 'install', args, env));
|
|
73
|
+
if (agent.id === 'pi') await windowsSetup('pi', 'install', [], env);
|
|
74
|
+
if (agent.id === 'codex') {
|
|
75
|
+
try { await windowsSetup('codex', 'install', [], env, { log: () => {} }); }
|
|
76
|
+
catch (error) { console.error(`Could not refresh Codex hooks (launch will continue): ${error.message}`); }
|
|
77
|
+
}
|
|
78
|
+
const spec = await windowsLaunch(agent.id, args, env);
|
|
79
|
+
if (spec.command === agent.bin) spec.command = executable;
|
|
80
|
+
console.log(` Launching ${agent.name} on Subconscious (${model})\n`);
|
|
81
|
+
return finish(await executeWindowsLaunch(spec, runWindows));
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function finish(code) {
|
|
85
|
+
if (code) process.exitCode = code;
|
|
86
|
+
return code;
|
|
87
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
// Windows-only equivalent of the existing Claude shell launch.
|
|
2
|
+
import { readFileSync } from 'node:fs';
|
|
3
|
+
import { resolvedModelSetting } from '../profiles.js';
|
|
4
|
+
import { positiveInteger, parseOptions } from './common.js';
|
|
5
|
+
const DEFAULTS = JSON.parse(readFileSync(new URL('../registry.generated.json', import.meta.url), 'utf8')).defaults;
|
|
6
|
+
function claudePickerSettings(models) {
|
|
7
|
+
return { availableModels: models, modelPicker: { replaceBuiltInOptions: true, options: models.map(model => ({model, label: model, description: 'Subconscious model ' + model})) } };
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function claudeNativeLaunch(argv, environment) {
|
|
11
|
+
const { options, rest: passthrough } = parseOptions(argv, {
|
|
12
|
+
'--gateway-url': 1, '--api-key': 1, '--model': 1,
|
|
13
|
+
'--compact-window': 1, '--max-context-tokens': 1,
|
|
14
|
+
});
|
|
15
|
+
const gatewayUrl = options['--gateway-url'] || environment.GATEWAY_URL?.trim() || '';
|
|
16
|
+
const apiKey = options['--api-key'] || environment.CLAUDE_CODE_API_KEY?.trim() || environment.API_KEY?.trim() || '';
|
|
17
|
+
const model = options['--model'] || environment.MODEL?.trim() || DEFAULTS.model;
|
|
18
|
+
const compactWindow = options['--compact-window'] ||
|
|
19
|
+
environment.CLAUDE_CODE_AUTO_COMPACT_WINDOW?.trim() ||
|
|
20
|
+
environment.COMPACT_WINDOW?.trim() ||
|
|
21
|
+
'1000000';
|
|
22
|
+
const maxContextTokens = options['--max-context-tokens'] ||
|
|
23
|
+
environment.CLAUDE_CODE_MAX_CONTEXT_TOKENS?.trim() ||
|
|
24
|
+
environment.MAX_CONTEXT_TOKENS?.trim() ||
|
|
25
|
+
'3000000';
|
|
26
|
+
|
|
27
|
+
if (!gatewayUrl || !apiKey) {
|
|
28
|
+
throw new Error('GATEWAY_URL and API_KEY are required to launch Claude Code');
|
|
29
|
+
}
|
|
30
|
+
positiveInteger(compactWindow, 'compact-window');
|
|
31
|
+
positiveInteger(maxContextTokens, 'max-context-tokens');
|
|
32
|
+
|
|
33
|
+
const effectiveGatewayUrl = environment.CLAUDE_GATEWAY_URL?.trim() || gatewayUrl;
|
|
34
|
+
const subagentModel = resolvedModelSetting(environment.CLAUDE_CODE_SUBAGENT_MODEL) || model;
|
|
35
|
+
const settings =
|
|
36
|
+
environment.SUBC_CLAUDE_SETTINGS?.trim() ||
|
|
37
|
+
JSON.stringify(claudePickerSettings([model], model));
|
|
38
|
+
const withoutTrailingSlash = effectiveGatewayUrl.replace(/\/+$/, '');
|
|
39
|
+
const env = {
|
|
40
|
+
...environment,
|
|
41
|
+
ANTHROPIC_BASE_URL: effectiveGatewayUrl,
|
|
42
|
+
ANTHROPIC_AUTH_TOKEN: apiKey,
|
|
43
|
+
ANTHROPIC_MODEL: environment.ANTHROPIC_MODEL?.trim() || model,
|
|
44
|
+
ANTHROPIC_SMALL_FAST_MODEL: environment.ANTHROPIC_SMALL_FAST_MODEL?.trim() || model,
|
|
45
|
+
CLAUDE_CODE_ENABLE_GATEWAY_MODEL_DISCOVERY:
|
|
46
|
+
environment.CLAUDE_CODE_ENABLE_GATEWAY_MODEL_DISCOVERY?.trim() || '0',
|
|
47
|
+
CLAUDE_CODE_SUBAGENT_MODEL: subagentModel,
|
|
48
|
+
CLAUDE_CODE_MAX_CONCURRENT_SUBAGENTS:
|
|
49
|
+
environment.MAX_CONCURRENT_SUBAGENTS?.trim() || '4',
|
|
50
|
+
CLAUDE_CODE_MAX_SUBAGENT_SPAWN_DEPTH:
|
|
51
|
+
environment.MAX_SUBAGENT_SPAWN_DEPTH?.trim() || '1',
|
|
52
|
+
CLAUDE_CODE_AUTO_COMPACT_WINDOW: compactWindow,
|
|
53
|
+
CLAUDE_CODE_MAX_CONTEXT_TOKENS: maxContextTokens,
|
|
54
|
+
CLAUDE_CODE_ENABLE_TELEMETRY:
|
|
55
|
+
environment.CLAUDE_CODE_ENABLE_TELEMETRY?.trim() || '1',
|
|
56
|
+
CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC:
|
|
57
|
+
environment.CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC?.trim() || '1',
|
|
58
|
+
ENABLE_CLAUDEAI_MCP_SERVERS: 'false',
|
|
59
|
+
OTEL_LOGS_EXPORTER: environment.OTEL_LOGS_EXPORTER?.trim() || 'otlp',
|
|
60
|
+
OTEL_EXPORTER_OTLP_PROTOCOL:
|
|
61
|
+
environment.OTEL_EXPORTER_OTLP_PROTOCOL?.trim() || 'http/json',
|
|
62
|
+
OTEL_EXPORTER_OTLP_LOGS_ENDPOINT: `${withoutTrailingSlash}/v1/logs`,
|
|
63
|
+
OTEL_EXPORTER_OTLP_HEADERS: `x-api-key=${apiKey}`,
|
|
64
|
+
OTEL_EXPORTER_OTLP_TIMEOUT:
|
|
65
|
+
environment.OTEL_EXPORTER_OTLP_TIMEOUT?.trim() || '2000',
|
|
66
|
+
OTEL_LOGS_EXPORT_INTERVAL:
|
|
67
|
+
environment.OTEL_LOGS_EXPORT_INTERVAL?.trim() || '2000',
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
return { args: ['--settings', settings, ...passthrough], env };
|
|
71
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { randomUUID } from 'node:crypto';
|
|
4
|
+
import { readFileSync } from 'node:fs';
|
|
5
|
+
|
|
6
|
+
export const defaults = JSON.parse(readFileSync(new URL('../registry.generated.json', import.meta.url), 'utf8')).defaults;
|
|
7
|
+
export const value = (env, key, fallback = '') => env[key]?.trim() || fallback;
|
|
8
|
+
export const origin = url => url.replace(/\/+$/, '').replace(/\/v1(?:\/(?:messages|responses|chat\/completions))?$/, '');
|
|
9
|
+
export function positiveInteger(input, name) {
|
|
10
|
+
if (!/^[1-9][0-9]*$/.test(String(input)) || !Number.isSafeInteger(Number(input))) throw new Error(`${name} must be a positive integer`);
|
|
11
|
+
return Number(input);
|
|
12
|
+
}
|
|
13
|
+
export function modelIds(env) {
|
|
14
|
+
const models = [...new Set([value(env, 'MODEL', defaults.model), ...value(env, 'SUBCONSCIOUS_MODELS', defaults.models.join('\n')).split(/\r?\n/)].filter(Boolean))];
|
|
15
|
+
for (const model of models) if (!/^[-A-Za-z0-9._:/+]+$/.test(model)) throw new Error(`Invalid model id: ${model}`);
|
|
16
|
+
return models;
|
|
17
|
+
}
|
|
18
|
+
export function parseOptions(argv, schema, { strict = false } = {}) {
|
|
19
|
+
const options = {}, rest = [];
|
|
20
|
+
for (let i = 0; i < argv.length; i++) {
|
|
21
|
+
if (argv[i] === '--') {
|
|
22
|
+
if (strict && i + 1 < argv.length) throw new Error(`Unexpected setup argument: ${argv[i + 1]}`);
|
|
23
|
+
rest.push(...argv.slice(i + 1)); break;
|
|
24
|
+
}
|
|
25
|
+
const [flag, ...equals] = argv[i].split('=');
|
|
26
|
+
if (!Object.hasOwn(schema, flag)) {
|
|
27
|
+
if (strict) throw new Error(`Unknown setup option: ${argv[i]}`);
|
|
28
|
+
rest.push(argv[i]); continue;
|
|
29
|
+
}
|
|
30
|
+
if (schema[flag] === true) {
|
|
31
|
+
if (equals.length) throw new Error(`${flag} does not take a value`);
|
|
32
|
+
options[flag] = true; continue;
|
|
33
|
+
}
|
|
34
|
+
const next = equals.length ? equals.join('=') : argv[++i];
|
|
35
|
+
if (!next || next.startsWith('--')) throw new Error(`${flag} requires a value`);
|
|
36
|
+
options[flag] = next;
|
|
37
|
+
}
|
|
38
|
+
return { options, rest };
|
|
39
|
+
}
|
|
40
|
+
export async function readJson(file, fallback) {
|
|
41
|
+
try { return JSON.parse((await fs.readFile(file, 'utf8')).replace(/^\uFEFF/, '')); }
|
|
42
|
+
catch (error) {
|
|
43
|
+
if (error.code === 'ENOENT') return fallback;
|
|
44
|
+
throw new Error(`Cannot read ${file}; leaving it unchanged: ${error.message}`);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
export function object(value, description) {
|
|
48
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) throw new Error(`${description} must be a JSON object; leaving it unchanged`);
|
|
49
|
+
return value;
|
|
50
|
+
}
|
|
51
|
+
export async function writeJson(file, data) {
|
|
52
|
+
await fs.mkdir(path.dirname(file), { recursive: true });
|
|
53
|
+
const temp = `${file}.${randomUUID()}.tmp`;
|
|
54
|
+
try {
|
|
55
|
+
await fs.writeFile(temp, JSON.stringify(data, null, 2) + '\n', { mode: 0o600, flag: 'wx' });
|
|
56
|
+
await fs.rename(temp, file);
|
|
57
|
+
} finally { await fs.rm(temp, { force: true }); }
|
|
58
|
+
}
|