subconscious-cli 6.3.1 → 8.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.
@@ -1,6 +1,9 @@
1
1
  import { fileURLToPath } from 'node:url';
2
2
  import { modelIds, positiveInteger } from './launch-values.js';
3
- import { modelSupportsVision } from './model-capabilities.js';
3
+ import {
4
+ catalogEntriesFromMetadata,
5
+ modelSupportsVision,
6
+ } from './model-capabilities.js';
4
7
 
5
8
  export const OPENCODE_PROVIDER_ID = 'subconscious';
6
9
  const OPENCODE_IMAGE_WINDOW_PLUGIN = fileURLToPath(
@@ -40,15 +43,22 @@ export function opencodeModelDisplayName(modelId) {
40
43
  return slug.split(/[-_]/).filter(Boolean).map(formatToken).join(' ');
41
44
  }
42
45
 
43
- export function buildOpenCodeModels(modelIds, { context, output }) {
46
+ export function buildOpenCodeModels(
47
+ modelIds,
48
+ { context, output, entries = [] },
49
+ ) {
50
+ const entryById = new Map(entries.map((entry) => [entry.id, entry]));
44
51
  return Object.fromEntries(
45
52
  modelIds.map((id) => [
46
53
  id,
47
54
  {
48
55
  name: opencodeModelDisplayName(id),
49
56
  tools: true,
50
- limit: { context, output },
51
- ...(modelSupportsVision(id)
57
+ limit: {
58
+ context: entryById.get(id)?.limits?.context_length ?? context,
59
+ output: entryById.get(id)?.limits?.max_output_tokens ?? output,
60
+ },
61
+ ...(modelSupportsVision(id, entries)
52
62
  ? {
53
63
  attachment: true,
54
64
  modalities: { input: ['text', 'image'], output: ['text'] },
@@ -65,6 +75,7 @@ export function buildOpenCodeConfig({
65
75
  modelIds,
66
76
  context,
67
77
  output,
78
+ entries = [],
68
79
  }) {
69
80
  return {
70
81
  $schema: 'https://opencode.ai/config.json',
@@ -82,7 +93,7 @@ export function buildOpenCodeConfig({
82
93
  headers: { 'x-subconscious-client': 'opencode' },
83
94
  modelsDiscovery: { enabled: false },
84
95
  },
85
- models: buildOpenCodeModels(modelIds, { context, output }),
96
+ models: buildOpenCodeModels(modelIds, { context, output, entries }),
86
97
  },
87
98
  },
88
99
  model: `${OPENCODE_PROVIDER_ID}/${model}`,
@@ -100,5 +111,6 @@ export function openCodeConfigFromEnv(env) {
100
111
  'OPENCODE_CONTEXT_LIMIT',
101
112
  ),
102
113
  output: positiveInteger(env.OPENCODE_OUTPUT_LIMIT, 'OPENCODE_OUTPUT_LIMIT'),
114
+ entries: catalogEntriesFromMetadata(env.SUBC_MODEL_METADATA),
103
115
  });
104
116
  }
package/bin/profiles.js CHANGED
@@ -12,9 +12,11 @@ import os from 'node:os';
12
12
  import path from 'node:path';
13
13
  import { c } from './colors.js';
14
14
  import { PUBLIC_CATALOG_FALLBACK_MESSAGE } from './models.js';
15
+ import { describeFailure } from './network.js';
15
16
  import {
16
17
  profileTemplate,
17
18
  RUNBOOK_DEFAULTS,
19
+ resolvedProfileValues,
18
20
  SUPPORTED_MODELS,
19
21
  } from './profile-settings.js';
20
22
 
@@ -26,6 +28,10 @@ export {
26
28
  SUPPORTED_MODELS,
27
29
  } from './profile-settings.js';
28
30
 
31
+ export function systemCaEnabledFor(profile) {
32
+ return String(resolvedProfileValues(profile).SYSTEM_CA ?? '').trim() === 'on';
33
+ }
34
+
29
35
  import { runWindows, windowsEditor } from './windows/process.js';
30
36
 
31
37
  export const DEFAULT_PROFILE = 'default';
@@ -560,11 +566,49 @@ export async function configCommand(
560
566
  export function modelsCommand(models = SUPPORTED_MODELS, options = {}) {
561
567
  const selectedModel =
562
568
  options.selectedModel || models[0] || RUNBOOK_DEFAULTS.MODEL;
569
+ const entryById = new Map(
570
+ (options.entries ?? []).map((entry) => [entry.id, entry]),
571
+ );
572
+ const lifecycleNote = (model) => {
573
+ const lifecycle = entryById.get(model)?.lifecycle;
574
+ if (!lifecycle) return '';
575
+ const date = lifecycle.deprecation_date
576
+ ? ` until ${lifecycle.deprecation_date}`
577
+ : '';
578
+ const replacement = lifecycle.replacement
579
+ ? ` ${c.dim}-> ${lifecycle.replacement}${c.reset}`
580
+ : '';
581
+ if (lifecycle.status === 'deprecated') {
582
+ return ` ${c.yellow}[deprecated${date}]${c.reset}${replacement}`;
583
+ }
584
+ if (lifecycle.status === 'retired') {
585
+ return ` ${c.red}[retired${date}]${c.reset}${replacement}`;
586
+ }
587
+ return '';
588
+ };
563
589
  console.log(`\n ${c.bold}Available models${c.reset}\n`);
564
590
  for (const model of models) {
565
591
  const suffix =
566
592
  model === selectedModel ? ` ${c.dim}(default)${c.reset}` : '';
567
- console.log(` ${c.cyan}${model}${c.reset}${suffix}`);
593
+ console.log(
594
+ ` ${c.cyan}${model}${c.reset}${suffix}${lifecycleNote(model)}`,
595
+ );
596
+ }
597
+ const selectedLifecycle = entryById.get(selectedModel)?.lifecycle;
598
+ if (selectedLifecycle?.status === 'deprecated') {
599
+ const when = selectedLifecycle.deprecation_date
600
+ ? ` on ${selectedLifecycle.deprecation_date}`
601
+ : '';
602
+ const to = selectedLifecycle.replacement
603
+ ? ` Migrate to ${selectedLifecycle.replacement}.`
604
+ : '';
605
+ console.error(
606
+ `\n ${c.yellow}Selected model ${selectedModel} is deprecated${when}.${to}${c.reset}`,
607
+ );
608
+ } else if (selectedLifecycle?.status === 'retired') {
609
+ console.error(
610
+ `\n ${c.yellow}Selected model ${selectedModel} is retired and may stop serving without notice.${c.reset}`,
611
+ );
568
612
  }
569
613
  if (options.source === 'public' && options.hasApiKey) {
570
614
  console.error(`\n ${c.dim}${PUBLIC_CATALOG_FALLBACK_MESSAGE}${c.reset}`);
@@ -572,7 +616,7 @@ export function modelsCommand(models = SUPPORTED_MODELS, options = {}) {
572
616
  console.error(
573
617
  `\n ${c.yellow}Could not fetch the live model catalog; showing packaged defaults.${c.reset}`,
574
618
  );
575
- console.error(` ${c.dim}${options.error.message}${c.reset}`);
619
+ console.error(` ${c.dim}${describeFailure(options.error)}${c.reset}`);
576
620
  }
577
621
  console.log();
578
622
  }
@@ -28,11 +28,14 @@
28
28
  },
29
29
  "help": {
30
30
  "usage": "subc [-p NAME] claude [help|status|uninstall] [Claude arguments...]",
31
- "behavior": "Launches Claude Code with the active Subconscious profile and model picker. Sessions run with CLAUDE_CONFIG_DIR=~/.subconscious/claude-code, which shares the setup your plain claude uses (~/.claude, or your CLAUDE_CONFIG_DIR): CLAUDE.md, skills, plugins, hooks, MCP servers and session history. Only the model pick is kept apart, so a /model choice in subc does not change plain Claude. Persistent files are leftover-only: subc claude uninstall.",
31
+ "behavior": "Launches Claude Code with the active Subconscious profile and model picker, using your own Claude config (~/.claude, or your CLAUDE_CONFIG_DIR) unchanged - plans, sessions, memory, skills and settings stay where plain claude keeps them. Subconscious routing, credentials and model pinning ride the launch env and --settings, which persist nothing. A /model pick inside a subc session changes your Claude default, as it does in plain claude. Persistent files are leftover-only: subc claude uninstall.",
32
32
  "options": [
33
33
  ["help", "Show this help"],
34
- ["status", "Inspect leftover ~/.claude/subconscious-gateway.env"],
35
- ["uninstall", "Remove leftover ~/.claude/subconscious-gateway.env"],
34
+ ["status", "Inspect leftovers: ~/.claude/subconscious-gateway.env"],
35
+ [
36
+ "uninstall",
37
+ "Remove leftover ~/.claude/subconscious-gateway.env from older persistent setup"
38
+ ],
36
39
  ["--model MODEL", "Override the profile model for this launch"]
37
40
  ],
38
41
  "args": ["-- ARGS...", "Pass remaining arguments to Claude Code"]
@@ -113,18 +116,6 @@
113
116
  "type": "integer",
114
117
  "min": 1
115
118
  },
116
- {
117
- "name": "CLAUDE_CODE_ISOLATE",
118
- "flag": "--no-isolate",
119
- "flag_value": "off",
120
- "default": "on",
121
- "strict": true,
122
- "choices": ["on", "off"],
123
- "help": "Use ~/.claude directly, so a /model pick in subc also changes plain Claude Code",
124
- "label": "Isolated Claude config",
125
- "description": "When on, subc claude sessions use ~/.subconscious/claude-code as the Claude config dir. It links back to ~/.claude for CLAUDE.md, skills, hooks and session history, copies the plugin index, rebuilds settings.json from ~/.claude/settings.json on each launch, and keeps only the subc model pick apart.",
126
- "comment": "CLAUDE_CODE_ISOLATE=off shares ~/.claude with plain Claude Code."
127
- },
128
119
  { "name": "MAX_CONCURRENT_SUBAGENTS" },
129
120
  {
130
121
  "name": "MAX_SUBAGENT_SPAWN_DEPTH",
@@ -159,12 +150,6 @@
159
150
  "override": ["ANTHROPIC_SMALL_FAST_MODEL"],
160
151
  "description": "Small/fast background model."
161
152
  },
162
- {
163
- "name": "CLAUDE_CONFIG_DIR",
164
- "value": "{claudeConfigDir}",
165
- "optional": true,
166
- "description": "Claude Code config dir for subc sessions, prepared before each launch to share the user's own Claude config (~/.claude, or a CLAUDE_CONFIG_DIR they set, which becomes the source rather than the launch dir). When CLAUDE_CODE_ISOLATE=off, the user's own CLAUDE_CONFIG_DIR is kept, or none is set, so sessions use it directly."
167
- },
168
153
  {
169
154
  "name": "CLAUDE_CODE_ENABLE_GATEWAY_MODEL_DISCOVERY",
170
155
  "value": "0",
@@ -251,12 +236,6 @@
251
236
  },
252
237
  "description": "Replaces the built-in model picker with the live catalog and allowlists only those models."
253
238
  },
254
- {
255
- "kind": "file",
256
- "path": "~/.subconscious/claude-code",
257
- "lifetime": "persistent",
258
- "description": "Claude Code config dir for subc sessions (~/.subconscious/claude-code, honoring $SUBC_CONFIG_DIR), set via CLAUDE_CONFIG_DIR. Each launch links CLAUDE.md, history.jsonl and keybindings.json (when present) and the rules, agents, commands, skills, output-styles, hooks, projects, file-history, todos, plans and ide folders (created in ~/.claude when missing) back to ~/.claude, copies the plugin index files (plugin changes made inside subc stay in subc and reset on the next launch), rebuilds settings.json from ~/.claude/settings.json (keeping the subc model pick, dropping apiKeyHelper and env names subc sets), and copies MCP servers, trust and onboarding state from ~/.claude.json (never login state). Settings or MCP servers changed inside a subc session are replaced on the next launch; that launch names them and saves them in settings.discarded.json or mcp.discarded.json in this dir. subc never deletes it and never writes ~/.claude settings; it refuses to launch when this dir resolves to your own config. To remove it, delete the links first: rm -r with a trailing slash, or PowerShell Remove-Item -Recurse, follows them into ~/.claude. Opt out with CLAUDE_CODE_ISOLATE=off or subc claude --no-isolate."
259
- },
260
239
  {
261
240
  "kind": "file",
262
241
  "path": "~/.claude/subconscious-gateway.env",
@@ -1,8 +1,9 @@
1
1
  #!/usr/bin/env bash
2
2
  # ── Subconscious API Gateway — Claude Code leftovers ──────────────────────────
3
3
  # Claude Code is launch-only: `subc claude` passes everything as env and flags
4
- # and writes nothing. Older persistent setups wrote
5
- # ~/.claude/subconscious-gateway.env; this script reports and removes it.
4
+ # and writes nothing persistent. One leftover remains from older releases:
5
+ # the ~/.claude/subconscious-gateway.env file from the first persistent setup.
6
+ # This script reports and removes it.
6
7
  #
7
8
  # subc claude status
8
9
  # subc claude uninstall
@@ -51,6 +51,16 @@
51
51
  "description": "Maximum concurrent Claude/Codex subagent threads.",
52
52
  "type": "integer",
53
53
  "min": 1
54
+ },
55
+ {
56
+ "name": "SYSTEM_CA",
57
+ "label": "Trust the OS certificate store",
58
+ "description": "Trust certificates from the operating system store, for a TLS-inspecting corporate proxy. Off by default.",
59
+ "type": "choice",
60
+ "strict": true,
61
+ "choices": ["off", "on"],
62
+ "default": "off",
63
+ "template": false
54
64
  }
55
65
  ]
56
66
  }
package/bin/tui.js CHANGED
@@ -330,7 +330,7 @@ export async function loadRemoteTuiUpdates(state, options = {}) {
330
330
  });
331
331
  if (signal?.aborted) return;
332
332
  await enqueuePatch({
333
- models: catalog.models,
333
+ models: catalog.defaultModels,
334
334
  modelError: catalog.error?.message || '',
335
335
  modelSource: catalog.source,
336
336
  modelsLoading: false,
@@ -6,6 +6,7 @@ import readline from 'node:readline';
6
6
  import { fileURLToPath } from 'node:url';
7
7
  import { LOGO_ART_SMALL_LINES } from './branding.js';
8
8
  import { c, colorEnabled } from './colors.js';
9
+ import { proxyAwareFetch } from './network.js';
9
10
  import { spawnWindows } from './windows/process.js';
10
11
 
11
12
  export const PACKAGE_NAME = 'subconscious-cli';
@@ -162,7 +163,7 @@ export async function currentVersion() {
162
163
  }
163
164
 
164
165
  export async function fetchLatestVersion(options = {}) {
165
- const fetchImpl = options.fetchImpl || globalThis.fetch;
166
+ const fetchImpl = options.fetchImpl || proxyAwareFetch;
166
167
  const timeoutMs = options.timeoutMs ?? UPDATE_CHECK_TIMEOUT_MS;
167
168
  const signal = options.signal;
168
169
  if (typeof fetchImpl !== 'function') throw new Error('fetch is unavailable');
package/bin/upgrade.js CHANGED
@@ -10,6 +10,7 @@ import fs from 'node:fs/promises';
10
10
  import readline from 'node:readline';
11
11
  import { fileURLToPath } from 'node:url';
12
12
  import { c } from './colors.js';
13
+ import { proxyAwareFetch } from './network.js';
13
14
  import {
14
15
  compareVersions,
15
16
  detectInstallTarget,
@@ -42,7 +43,7 @@ export async function currentCliVersion() {
42
43
  return pkg.version;
43
44
  }
44
45
 
45
- export async function fetchLatestVersion(fetchImpl = fetch) {
46
+ export async function fetchLatestVersion(fetchImpl = proxyAwareFetch) {
46
47
  const res = await fetchImpl(
47
48
  `https://registry.npmjs.org/${PACKAGE_NAME}/latest`,
48
49
  {
package/bin/usage.js CHANGED
@@ -4,6 +4,12 @@
4
4
 
5
5
  import { getApiKey, getPlatformUrl } from './auth.js';
6
6
  import { c } from './colors.js';
7
+ import {
8
+ describeFetchError,
9
+ proxyAwareFetch,
10
+ proxyHint,
11
+ tlsTrustHint,
12
+ } from './network.js';
7
13
  import { DEFAULT_PROFILE } from './profiles.js';
8
14
  import { printLoginUpgradeWarning } from './upgrade.js';
9
15
 
@@ -49,15 +55,6 @@ export function shortModelSlug(slug) {
49
55
  : text;
50
56
  }
51
57
 
52
- export function isLegacyUsagePayload(data) {
53
- if (!data || typeof data !== 'object') return true;
54
- if ('credits' in data && data.plan && typeof data.plan === 'object')
55
- return false;
56
- return (
57
- 'dailyTokenCeiling' in data || 'consumedToday' in data || !('plan' in data)
58
- );
59
- }
60
-
61
58
  export function meterColor(percentage) {
62
59
  if (percentage >= 100) return c.red;
63
60
  if (percentage >= 80) return c.yellow;
@@ -85,27 +82,25 @@ export function formatAllowancePercent(percent) {
85
82
  return `${whole}%`;
86
83
  }
87
84
 
88
- /** New percent payloads, or the pre-basis token shape (consumed / amount). */
85
+ /** Percent-only allowance payloads from the current platform. */
89
86
  export function describeDailyAllowance(allowance) {
90
87
  if (!allowance || typeof allowance !== 'object') return null;
91
88
  if (allowance.basis === 'unavailable') return { kind: 'unavailable' };
92
89
  if (allowance.basis === 'credit' || allowance.basis === 'tokens') {
93
90
  return { kind: allowance.basis, percent: Number(allowance.percent) || 0 };
94
91
  }
95
-
96
- const consumed = Number(allowance.consumed) || 0;
97
- const amount = Number(allowance.amount) || 0;
98
- if (amount <= 0) return null;
99
- return { kind: 'tokens', percent: (consumed / amount) * 100 };
92
+ return null;
100
93
  }
101
94
 
102
95
  export function formatUsageDisplay(data) {
103
96
  const lines = [];
104
97
  lines.push(`\n ${c.bold}Subconscious Usage${c.reset}\n`);
105
98
 
106
- const billingMode = data.billingMode ?? null;
99
+ const billingTerms = data.billingTerms ?? null;
100
+ const invoiced = billingTerms === 'invoiced';
107
101
  const plan = data.plan ?? {};
108
102
  const credits = data.credits ?? {};
103
+ const invoice = invoiced ? (data.invoice ?? {}) : {};
109
104
  const models = Array.isArray(data.models) ? data.models : [];
110
105
 
111
106
  const planLabel = plan.label || 'No plan';
@@ -113,55 +108,81 @@ export function formatUsageDisplay(data) {
113
108
  const planLine = planStatus ? `${planLabel} · ${planStatus}` : planLabel;
114
109
 
115
110
  lines.push(` ${c.dim}Plan${c.reset} ${planLine}`);
116
- if (billingMode) {
117
- lines.push(` ${c.dim}Billing${c.reset} ${billingMode}`);
111
+ if (invoiced) {
112
+ lines.push(` ${c.dim}Billing${c.reset} Invoiced monthly (net 30)`);
118
113
  }
119
114
 
120
115
  const allowance = plan.dailyAllowance;
121
- const unlimitedComp = Boolean(plan.isUnlimitedComp);
122
116
 
123
- if (unlimitedComp) {
117
+ // Invoiced orgs bill against the spend limit, not a daily grant, so the
118
+ // daily allowance does not pertain to them and is never shown.
119
+ const described = invoiced ? null : describeDailyAllowance(allowance);
120
+ if (described) {
124
121
  lines.push('');
125
122
  lines.push(` ${c.dim}Daily allowance${c.reset}`);
126
- lines.push(` ${c.green}Unlimited${c.reset} ${c.dim}(comp)${c.reset}`);
127
- } else {
128
- const described = describeDailyAllowance(allowance);
129
- if (described) {
130
- lines.push('');
131
- lines.push(` ${c.dim}Daily allowance${c.reset}`);
132
- if (described.kind === 'unavailable') {
133
- lines.push(` ${c.dim}Daily credit unavailable${c.reset}`);
134
- lines.push(
135
- ` ${c.dim}Today's credit usage could not be read just now. Your plan and access are unaffected, and the figure returns on its own.${c.reset}`,
136
- );
137
- } else {
138
- const label =
139
- described.kind === 'credit'
140
- ? 'of daily credit used'
141
- : 'of daily tokens used';
123
+ if (described.kind === 'unavailable') {
124
+ lines.push(` ${c.dim}Daily credit unavailable${c.reset}`);
125
+ lines.push(
126
+ ` ${c.dim}Today's credit usage could not be read just now. Your plan and access are unaffected, and the figure returns on its own.${c.reset}`,
127
+ );
128
+ } else {
129
+ const label =
130
+ described.kind === 'credit'
131
+ ? 'of daily credit used'
132
+ : 'of daily tokens used';
133
+ lines.push(
134
+ ` ${renderProgressBar(described.percent)} ${formatAllowancePercent(described.percent)} ${label}`,
135
+ );
136
+ lines.push(` ${c.dim}Resets midnight UTC${c.reset}`);
137
+ if (described.kind === 'credit' && described.percent >= 100) {
142
138
  lines.push(
143
- ` ${renderProgressBar(described.percent)} ${formatAllowancePercent(described.percent)} ${label}`,
139
+ ` ${c.yellow}Past today's allowance - further usage bills from your balance.${c.reset}`,
144
140
  );
145
- lines.push(` ${c.dim}Resets midnight UTC${c.reset}`);
146
- if (described.kind === 'credit' && described.percent >= 100) {
147
- lines.push(
148
- ` ${c.yellow}Past today's allowance - further usage bills from your balance.${c.reset}`,
149
- );
150
- }
151
141
  }
152
142
  }
153
143
  }
154
144
 
155
- lines.push('');
156
- lines.push(
157
- ` ${c.dim}Credits${c.reset} ${formatCurrency(credits.balanceDollars ?? 0)}`,
158
- );
159
- if (billingMode === 'subscription' || billingMode === 'comp' || allowance) {
160
- const overage = Number(credits.overageThisPeriodDollars) || 0;
161
- const overageColor = overage > 0 ? c.yellow : c.reset;
145
+ if (invoiced) {
146
+ const accrued = invoice.accruedThisPeriodDollars;
147
+ const limit = invoice.limitDollars;
148
+ const periodEnd = invoice.periodEnd ? new Date(invoice.periodEnd) : null;
149
+
150
+ lines.push('');
151
+ lines.push(` ${c.dim}Spend this period${c.reset}`);
152
+ if (accrued == null) {
153
+ lines.push(` ${c.dim}Spend could not be read just now.${c.reset}`);
154
+ lines.push(
155
+ ` ${c.dim}Your plan and access are unaffected, and the figure returns on its own.${c.reset}`,
156
+ );
157
+ } else {
158
+ const limitText =
159
+ limit != null ? ` of ${formatCurrency(limit)} limit` : '';
160
+ const spendColor = limit != null && accrued >= limit ? c.red : c.reset;
161
+ lines.push(
162
+ ` ${spendColor}${formatCurrency(accrued)}${c.reset}${c.dim}${limitText}${c.reset}`,
163
+ );
164
+ if (periodEnd && !Number.isNaN(periodEnd.getTime())) {
165
+ const isoDay = periodEnd.toISOString().slice(0, 10);
166
+ lines.push(` ${c.dim}Period ends ${isoDay}${c.reset}`);
167
+ }
168
+ if (limit != null && accrued >= limit) {
169
+ lines.push(
170
+ ` ${c.red}At the spend limit - further requests follow your limit policy.${c.reset}`,
171
+ );
172
+ }
173
+ }
174
+ } else if (credits.balanceDollars != null || data.credits) {
175
+ lines.push('');
162
176
  lines.push(
163
- ` ${c.dim}Overage${c.reset} ${overageColor}${formatCurrency(overage)} this period${c.reset}`,
177
+ ` ${c.dim}Credits${c.reset} ${formatCurrency(credits.balanceDollars ?? 0)}`,
164
178
  );
179
+ const overage = Number(credits.overageThisPeriodDollars) || 0;
180
+ if (overage > 0 || allowance) {
181
+ const overageColor = overage > 0 ? c.yellow : c.reset;
182
+ lines.push(
183
+ ` ${c.dim}Overage${c.reset} ${overageColor}${formatCurrency(overage)} this period${c.reset}`,
184
+ );
185
+ }
165
186
  }
166
187
 
167
188
  if (models.length > 0) {
@@ -181,7 +202,11 @@ export function formatUsageDisplay(data) {
181
202
  return lines.join('\n');
182
203
  }
183
204
 
184
- export async function fetchUsage(apiKey, platformUrl, fetchImpl = fetch) {
205
+ export async function fetchUsage(
206
+ apiKey,
207
+ platformUrl,
208
+ fetchImpl = proxyAwareFetch,
209
+ ) {
185
210
  const res = await fetchImpl(
186
211
  `${platformUrl.replace(/\/$/, '')}${USAGE_API_PATH}`,
187
212
  {
@@ -241,14 +266,6 @@ export async function usageCommand(argv = [], options = {}) {
241
266
  throw new Error(message);
242
267
  }
243
268
 
244
- if (isLegacyUsagePayload(data)) {
245
- console.log(`\n ${c.yellow}Platform upgrade required.${c.reset}`);
246
- console.log(
247
- ` ${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`,
248
- );
249
- return;
250
- }
251
-
252
269
  if (jsonOutput) {
253
270
  console.log(JSON.stringify(data, null, 2));
254
271
  return;
@@ -260,10 +277,16 @@ export async function usageCommand(argv = [], options = {}) {
260
277
  console.log(
261
278
  `\n ${c.yellow}Could not reach the platform (timed out).${c.reset}`,
262
279
  );
263
- console.log(` ${c.dim}Host: ${platformUrl}${c.reset}\n`);
280
+ console.log(` ${c.dim}Host: ${platformUrl}${c.reset}`);
281
+ const proxy = proxyHint(error, platformUrl);
282
+ if (proxy) console.log(` ${c.dim}${proxy}${c.reset}`);
283
+ console.log();
264
284
  return;
265
285
  }
266
- console.error(`\n ${c.red}${error.message}${c.reset}\n`);
286
+ console.error(`\n ${c.red}${describeFetchError(error)}${c.reset}`);
287
+ const hint = tlsTrustHint(error) || proxyHint(error, platformUrl);
288
+ if (hint) console.error(` ${c.yellow}${hint}${c.reset}`);
289
+ console.error();
267
290
  process.exitCode = 1;
268
291
  }
269
292
  }
@@ -9,7 +9,6 @@ import {
9
9
  resolveInputs,
10
10
  takeFlags,
11
11
  } from '../agent-launch.js';
12
- import { prepareClaudeLaunch } from '../claude-config.js';
13
12
  import { claudePickerSettings } from '../claude-picker.js';
14
13
  import { modelIds, positiveInteger } from '../launch-values.js';
15
14
  import { modelSupportsVision } from '../model-capabilities.js';
@@ -62,13 +61,7 @@ function plannedLaunch(agent, argv, env) {
62
61
  return { command: plan.argv[0], args: plan.argv.slice(1), env: plan.env };
63
62
  }
64
63
  const plan = claudeLaunch(agent, argv, env);
65
- return {
66
- command: plan.argv[0],
67
- args: plan.argv.slice(1),
68
- env: plan.env,
69
- // Runs only when the launch executes, so planning never touches disk.
70
- prepare: (options) => prepareClaudeLaunch(agent, plan.env, env, options),
71
- };
64
+ return { command: plan.argv[0], args: plan.argv.slice(1), env: plan.env };
72
65
  }
73
66
 
74
67
  function codexCatalog(env, context, maxContext, compact) {
@@ -274,17 +267,12 @@ export async function windowsLaunch(
274
267
  throw new Error(`No native Windows launcher is available for ${id}`);
275
268
  }
276
269
 
277
- export async function executeWindowsLaunch(spec, run, prepareOptions) {
270
+ export async function executeWindowsLaunch(spec, run) {
278
271
  try {
279
- const prepared = spec.prepare?.(prepareOptions);
280
- for (const warning of prepared?.warnings ?? []) {
281
- console.error(` ${warning}`);
282
- }
283
- // Claude Code would write into the user's own config there.
284
- if (prepared?.refused) return 1;
285
- const env = { ...spec.env, ...prepared?.env };
286
272
  const options =
287
- spec.input === undefined ? { env } : { env, input: spec.input };
273
+ spec.input === undefined
274
+ ? { env: spec.env }
275
+ : { env: spec.env, input: spec.input };
288
276
  return await run(spec.command, spec.args, options);
289
277
  } finally {
290
278
  await spec.cleanup?.();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "subconscious-cli",
3
- "version": "6.3.1",
3
+ "version": "8.0.0",
4
4
  "description": "CLI for Subconscious — run Subconscious Code, Claude Code, Codex, OpenCode, DeepSeek Harness, Copilot, and Pi",
5
5
  "bin": {
6
6
  "subc": "bin/cli.js"
@@ -1,58 +0,0 @@
1
- import { isDeepStrictEqual } from 'node:util';
2
-
3
- // Each launch rebuilds the subc copies of settings.json and the MCP servers
4
- // in .claude.json from the user's own files, so anything a subc session
5
- // changed in those copies is replaced. These helpers find what a session
6
- // changed, by comparing the copies with what the last launch wrote, so the
7
- // launch can save it aside and name it instead of losing it silently.
8
-
9
- const isObject = (value) =>
10
- value !== null && typeof value === 'object' && !Array.isArray(value);
11
-
12
- /**
13
- * Keys of `current` that a session added or changed since `written`.
14
- * Removed keys are ignored: Claude Code drops keys on its own.
15
- *
16
- * @param {Record<string, unknown>} written what the last launch wrote
17
- * @param {Record<string, unknown>} current what the copy holds now
18
- * @param {string[]} [ignored] keys subc keeps across launches
19
- * @returns {string[]}
20
- */
21
- export function changedKeys(written, current, ignored = []) {
22
- if (!isObject(written) || !isObject(current)) return [];
23
- return Object.keys(current).filter(
24
- (key) =>
25
- !ignored.includes(key) && !isDeepStrictEqual(current[key], written[key]),
26
- );
27
- }
28
-
29
- /** The MCP servers of a .claude.json: user scope and each project. */
30
- export function mcpServersOf(state) {
31
- const projects = isObject(state?.projects) ? state.projects : {};
32
- return {
33
- mcpServers: isObject(state?.mcpServers) ? state.mcpServers : {},
34
- projects: Object.fromEntries(
35
- Object.entries(projects)
36
- .filter(([, project]) => isObject(project?.mcpServers))
37
- .map(([name, project]) => [name, project.mcpServers]),
38
- ),
39
- };
40
- }
41
-
42
- /**
43
- * MCP server names a session added or changed since `written`, as
44
- * `name` for user scope and `name (project)` for a project.
45
- *
46
- * @param {ReturnType<typeof mcpServersOf>} written
47
- * @param {ReturnType<typeof mcpServersOf>} current
48
- * @returns {string[]}
49
- */
50
- export function changedMcpServers(written, current) {
51
- const changed = changedKeys(written.mcpServers, current.mcpServers);
52
- for (const [project, servers] of Object.entries(current.projects)) {
53
- for (const name of changedKeys(written.projects[project] ?? {}, servers)) {
54
- changed.push(`${name} (${project})`);
55
- }
56
- }
57
- return changed;
58
- }