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.
package/README.md CHANGED
@@ -351,8 +351,11 @@ subc opencode uninstall
351
351
  ```
352
352
 
353
353
  `subc claude uninstall` and `subc opencode uninstall` only clean leftover files
354
- from older overwrite-style setup. Launch those agents with `subc claude` /
355
- `subc opencode`; they do not need install.
354
+ from older overwrite-style setup. `subc claude` writes nothing persistent: it
355
+ passes routing, credentials and the model picker through the launch env and
356
+ `--settings`, and sessions use your own Claude config (~/.claude, or your
357
+ CLAUDE_CONFIG_DIR). Launch those agents with `subc claude` / `subc opencode`;
358
+ they do not need install.
356
359
 
357
360
  Inspect a persistent integration with `subc <agent> status`. A one-off
358
361
  `--api-key` passed to install takes precedence for that command but is not
@@ -1,7 +1,6 @@
1
- import path from 'node:path';
2
1
  import { agentInputs, agentKeyName } from './agent-data.js';
3
2
  import { openCodeConfigFromEnv } from './opencode-provider.js';
4
- import { configDir, isUnsetSetting } from './profiles.js';
3
+ import { isUnsetSetting } from './profiles.js';
5
4
 
6
5
  // One pass, so a substituted value (a prompt, a settings document) is never
7
6
  // scanned again for placeholders.
@@ -9,9 +8,6 @@ const PLACEHOLDER = /\$\{([A-Z_][A-Z0-9_]*)\}(\/)?|\{([A-Za-z][A-Za-z0-9]*)\}/g;
9
8
  const present = (value) =>
10
9
  value !== undefined && value !== null && value !== '';
11
10
 
12
- // CLAUDE_CODE_ISOLATE values that stop Claude Code config isolation.
13
- const ISOLATE_OFF = new Set(['off', '0', 'false', 'no']);
14
-
15
11
  function tokensFor(agent, env, extra) {
16
12
  const baseUrl = String(env.GATEWAY_URL ?? '').replace(/\/+$/, '');
17
13
  const keyName = agentKeyName(agent);
@@ -23,26 +19,6 @@ function tokensFor(agent, env, extra) {
23
19
  // Left for the runbook, which writes the file and substitutes its path.
24
20
  tempFile: '{tempFile}',
25
21
  opencodeConfig: () => JSON.stringify(openCodeConfigFromEnv(env)),
26
- // Claude Code config isolation: subc sessions keep their Claude config
27
- // (settings, sessions, plugins, credentials) under the subc config dir
28
- // instead of ~/.claude, so session state cannot bleed into a plain
29
- // `claude` installation. Empty when CLAUDE_CODE_ISOLATE opts out.
30
- claudeConfigDir: () => {
31
- const setting = String(env.CLAUDE_CODE_ISOLATE ?? '')
32
- .trim()
33
- .toLowerCase();
34
- // Opted out: the user's own folder, as plain `claude` would use it.
35
- if (ISOLATE_OFF.has(setting)) {
36
- return String(
37
- env.SUBC_CLAUDE_CONFIG_SOURCE || env.CLAUDE_CONFIG_DIR || '',
38
- ).trim();
39
- }
40
- const base = String(env.SUBC_CONFIG_DIR ?? '').trim() || configDir();
41
- // A relative base would put the config, MCP tokens included, in the
42
- // current directory; use ~/.claude unchanged instead.
43
- if (!path.isAbsolute(base)) return '';
44
- return path.join(base, 'claude-code');
45
- },
46
22
  ...extra,
47
23
  };
48
24
  }
@@ -3,7 +3,7 @@ import os from 'node:os';
3
3
  import path from 'node:path';
4
4
  import { RUNBOOK_DIR } from './agent-data.js';
5
5
  import { modelIds } from './launch-values.js';
6
- import { visionModelList } from './model-capabilities.js';
6
+ import { visionModelListFromMetadata } from './model-capabilities.js';
7
7
 
8
8
  /** Resolve a script inside the agent's own runbook directory. */
9
9
  export function runbookScriptPath(agent, relativeScript) {
@@ -95,7 +95,7 @@ export function scriptEnv(env, { strict = true } = {}) {
95
95
  return {
96
96
  ...env,
97
97
  ...(ids === undefined ? {} : { SUBC_MODEL_IDS: ids }),
98
- SUBC_VISION_MODELS: visionModelList(),
98
+ SUBC_VISION_MODELS: visionModelListFromMetadata(env.SUBC_MODEL_METADATA),
99
99
  };
100
100
  }
101
101
 
package/bin/agents.js CHANGED
@@ -19,7 +19,6 @@ import {
19
19
  import { launchPlan, resolveInputs } from './agent-launch.js';
20
20
  import { planCommand, runCommand, scriptCommand } from './agent-spawn.js';
21
21
  import { getApiKey } from './auth.js';
22
- import { prepareClaudeLaunch } from './claude-config.js';
23
22
  import {
24
23
  applyClaudePickerCatalog,
25
24
  CLAUDE_PICKER_ENV_DESCRIPTION,
@@ -37,6 +36,7 @@ import {
37
36
  PUBLIC_CATALOG_FALLBACK_MESSAGE,
38
37
  resolveModelCatalog,
39
38
  } from './models.js';
39
+ import { describeFailure } from './network.js';
40
40
  import { isUnsetSetting, resolvedModelSetting } from './profiles.js';
41
41
  import { runWindowsAgent } from './windows/agents.js';
42
42
 
@@ -327,6 +327,7 @@ export function runbookEnv(
327
327
  profile,
328
328
  agent,
329
329
  models = PACKAGED_MODELS,
330
+ entries = [],
330
331
  ) {
331
332
  const ctx = buildContext(apiKey, model, profile);
332
333
  const extraDirs = [binDir, ...candidateBinDirs()].filter(Boolean);
@@ -343,6 +344,7 @@ export function runbookEnv(
343
344
  ...(keyName ? { [keyName]: apiKey } : {}),
344
345
  MODEL: model,
345
346
  SUBCONSCIOUS_MODELS: models.join('\n'),
347
+ SUBC_MODEL_METADATA: JSON.stringify(entries),
346
348
  ...(claude
347
349
  ? {
348
350
  SUBC_CLAUDE_SETTINGS: JSON.stringify(
@@ -372,13 +374,17 @@ async function resolvedModelsForLaunch(profile, apiKey, selectedModel) {
372
374
  console.error(
373
375
  ` ${c.yellow}Could not fetch the live model catalog; using packaged defaults.${c.reset}`,
374
376
  );
375
- console.error(` ${c.dim}${catalog.error.message}${c.reset}\n`);
377
+ console.error(` ${c.dim}${describeFailure(catalog.error)}${c.reset}\n`);
376
378
  }
377
379
  return catalog;
378
380
  }
379
381
 
380
382
  export function selectLaunchModel(requestedModel, modelSource, catalog) {
381
- const first = catalog.models[0] || requestedModel || DEFAULTS.model;
383
+ const first =
384
+ catalog.defaultModels?.[0] ||
385
+ catalog.models[0] ||
386
+ requestedModel ||
387
+ DEFAULTS.model;
382
388
  if (modelSource === 'catalog' || !requestedModel) {
383
389
  return first;
384
390
  }
@@ -427,7 +433,15 @@ async function runRunbookSetup(agent, argv, profile) {
427
433
  // Setup scripts read no strict-choice setting; only the model list matters.
428
434
  const env = resolveInputs(
429
435
  agent,
430
- runbookEnv(apiKey, model, undefined, profile, agent, catalog.models),
436
+ runbookEnv(
437
+ apiKey,
438
+ model,
439
+ undefined,
440
+ profile,
441
+ agent,
442
+ catalog.defaultModels,
443
+ catalog.entries,
444
+ ),
431
445
  {},
432
446
  { strict: false },
433
447
  );
@@ -542,26 +556,17 @@ async function launchAgent(agent, argv, profile, headless) {
542
556
  (headless ? console.error : console.log)(
543
557
  ` ${c.dim}Launching ${c.reset}${c.bold}${agent.name}${c.reset} ${c.dim}on Subconscious ${c.reset}${c.dim}(${model})${c.reset}\n`,
544
558
  );
545
- const env = runbookEnv(apiKey, model, binDir, profile, agent, catalog.models);
559
+ const env = runbookEnv(
560
+ apiKey,
561
+ model,
562
+ binDir,
563
+ profile,
564
+ agent,
565
+ catalog.defaultModels,
566
+ catalog.entries,
567
+ );
546
568
  // The headless endpoint is the one endpoint for every harness, Claude too.
547
569
  const launchEnv = headless ? { ...env, CLAUDE_GATEWAY_URL: '' } : env;
548
570
  const plan = launchPlan(agent, { env: launchEnv, ...headlessParts(rest) });
549
- if (agent.id === CLAUDE_CODE) {
550
- const {
551
- warnings,
552
- refused,
553
- env: sourceEnv,
554
- } = prepareClaudeLaunch(agent, plan.env, launchEnv);
555
- const color = refused ? c.red : c.yellow;
556
- for (const warning of warnings) {
557
- console.error(` ${color}${warning}${c.reset}`);
558
- }
559
- if (refused) {
560
- process.exitCode = 1;
561
- return 1;
562
- }
563
- const prepared = { ...plan, env: { ...plan.env, ...sourceEnv } };
564
- return runCommand(planCommand(agent, prepared, binDir));
565
- }
566
571
  return runCommand(planCommand(agent, plan, binDir));
567
572
  }
package/bin/auth.js CHANGED
@@ -16,6 +16,12 @@ import fs from 'node:fs/promises';
16
16
  import os from 'node:os';
17
17
  import path from 'node:path';
18
18
  import { c } from './colors.js';
19
+ import {
20
+ describeFetchError,
21
+ proxyAwareFetch,
22
+ proxyHint,
23
+ tlsTrustHint,
24
+ } from './network.js';
19
25
  import {
20
26
  clearProfileApiKey,
21
27
  DEFAULT_PROFILE,
@@ -128,7 +134,10 @@ function terminalLink(url) {
128
134
  return `\u001b]8;;${url}\u0007${url}\u001b]8;;\u0007`;
129
135
  }
130
136
 
131
- export async function registerDeviceLogin(platformUrl, fetchImpl = fetch) {
137
+ export async function registerDeviceLogin(
138
+ platformUrl,
139
+ fetchImpl = proxyAwareFetch,
140
+ ) {
132
141
  const res = await fetchImpl(`${platformUrl}/api/cli/device/register`, {
133
142
  method: 'POST',
134
143
  signal: AbortSignal.timeout(8000),
@@ -146,7 +155,7 @@ export async function registerDeviceLogin(platformUrl, fetchImpl = fetch) {
146
155
  export async function pollDeviceLogin(
147
156
  platformUrl,
148
157
  deviceCode,
149
- fetchImpl = fetch,
158
+ fetchImpl = proxyAwareFetch,
150
159
  ) {
151
160
  const res = await fetchImpl(`${platformUrl}/api/cli/device/token`, {
152
161
  method: 'POST',
@@ -180,10 +189,25 @@ function printDeviceInstructions(platformUrl, userCode) {
180
189
 
181
190
  function printLoginFallback() {
182
191
  console.error(
183
- ` ${c.dim}You can also copy an API key from the dashboard and run${c.reset} ${c.cyan}subc update-key <your-api-key>${c.reset}${c.dim}.${c.reset}`,
192
+ ` ${c.dim}You can also copy an API key from the dashboard and run${c.reset} ${c.cyan}subc update-key <your-api-key>${c.reset}${c.dim}, which needs no network.${c.reset}`,
184
193
  );
185
194
  }
186
195
 
196
+ // Render a login failure with the real underlying network cause. Node's fetch
197
+ // reports every connection problem as a bare "fetch failed"; the diagnostic
198
+ // (`ENOTFOUND`, `ECONNREFUSED`, `ETIMEDOUT`, TLS, proxy) lives on `cause`.
199
+ function printLoginFailure(platformUrl, error) {
200
+ const message = describeFetchError(error);
201
+ console.error(` ${c.red}✗ ${message}${c.reset}`);
202
+ const hint = tlsTrustHint(error);
203
+ if (hint) {
204
+ console.error(` ${c.yellow}${hint}${c.reset}`);
205
+ return;
206
+ }
207
+ const proxy = proxyHint(error, platformUrl);
208
+ if (proxy) console.error(` ${c.dim}${proxy}${c.reset}`);
209
+ }
210
+
187
211
  async function saveLoginKey(profileName, token) {
188
212
  if (profileName === DEFAULT_PROFILE) {
189
213
  const config = await loadConfig();
@@ -196,7 +220,7 @@ async function saveLoginKey(profileName, token) {
196
220
  export async function loginCommand(_argv = [], options = {}) {
197
221
  const profileName = options.profileName || DEFAULT_PROFILE;
198
222
  const existing = await getApiKey(options.profile);
199
- const fetchImpl = options.fetchImpl || fetch;
223
+ const fetchImpl = options.fetchImpl || proxyAwareFetch;
200
224
 
201
225
  if (existing) {
202
226
  const profile = await ensureProfile(profileName, existing.key);
@@ -225,7 +249,7 @@ export async function loginCommand(_argv = [], options = {}) {
225
249
  try {
226
250
  registered = await registerDeviceLogin(platformUrl, fetchImpl);
227
251
  } catch (error) {
228
- console.error(` ${c.red}✗ ${error.message}${c.reset}`);
252
+ printLoginFailure(platformUrl, error);
229
253
  printLoginFallback();
230
254
  console.log();
231
255
  process.exitCode = 1;
@@ -299,7 +323,7 @@ export async function loginCommand(_argv = [], options = {}) {
299
323
  } catch (error) {
300
324
  clearInterval(spinner);
301
325
  process.stdout.write(`\r${' '.repeat(50)}\r`);
302
- console.error(` ${c.red}✗ ${error.message}${c.reset}`);
326
+ printLoginFailure(platformUrl, error);
303
327
  printLoginFallback();
304
328
  console.log();
305
329
  process.exitCode = 1;
@@ -380,7 +404,7 @@ export async function whoamiCommand(_argv = [], options = {}) {
380
404
 
381
405
  // Validate the key against the server; falls back to offline display if unreachable
382
406
  try {
383
- const res = await fetch(
407
+ const res = await proxyAwareFetch(
384
408
  `${getPlatformUrl(options.profile)}/api/cli/whoami`,
385
409
  {
386
410
  headers: { Authorization: `Bearer ${key}` },