subconscious-cli 4.0.13 → 4.0.14

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
@@ -153,8 +153,9 @@ subc config # list every profile and its file path
153
153
  subc -p staging config create # create a profile with default settings
154
154
  subc -p staging config # print that path and env file
155
155
  subc -p staging config --model subconscious/glm-5.2
156
+ subc -p staging config --model UNSET
156
157
  subc -p staging config --subagent-model subconscious/deepseek-v4-flash-marathon
157
- subc -p staging config --subagent-model follow-default
158
+ subc -p staging config --subagent-model UNSET
158
159
  subc config --gateway-url https://gateway.example
159
160
  subc config path
160
161
  subc config edit # open the selected profile in $VISUAL, $EDITOR, vim, or nano
@@ -170,9 +171,12 @@ Subconscious-injected defaults; resolved login identity (`GATEWAY_URL`,
170
171
  In scripts and CI, keep using `subc config --gateway-url`, `--api-key`,
171
172
  `--model`, and `--subagent-model`.
172
173
 
174
+ Use `UNSET` to clear the profile default so launches use the first model in
175
+ the live catalog (gateway priority order).
176
+
173
177
  The subagent setting controls `CLAUDE_CODE_SUBAGENT_MODEL` for Claude Code.
174
- Use `follow-default` to clear the override so subagents automatically follow
175
- the profile's default model.
178
+ Use `UNSET` to clear the override so subagents automatically follow the
179
+ profile's default model.
176
180
 
177
181
  Each agent section can hold its own API key. An agent-specific key takes
178
182
  precedence over the shared profile key and can be used on its own, so profiles
@@ -224,11 +228,15 @@ list. The selected profile model is listed first only while the gateway still
224
228
  advertises it.
225
229
  OpenCode rebuilds its isolated runtime provider on every `subc opencode` launch,
226
230
  so models left in older OpenCode configuration files do not leak into the list.
227
- If a saved profile default has been removed, launches use the first live model;
228
- an explicit `--model` or `SUBCONSCIOUS_MODEL` override is always preserved.
231
+ If a saved profile default is UNSET or has been removed, launches use the first
232
+ live model; an explicit `--model` or `SUBCONSCIOUS_MODEL` override is always
233
+ preserved.
229
234
  Claude Code exposes four native Opus/Sonnet/Haiku/Fable picker slots plus one
230
- custom model option, so the CLI maps the first five models into those slots; any other
231
- model can still be selected with `subc claude --model MODEL`. Cursor requires adding
235
+ custom model option. The CLI remaps those aliases to the live catalog, then
236
+ replaces the built-in `/model` lineup (`modelPicker.replaceBuiltInOptions`) and
237
+ allowlists only those models (`availableModels`) so Anthropic Fable and
238
+ ungranted slugs do not appear. Any other model can still be selected with
239
+ `subc claude --model MODEL`. Cursor requires adding
232
240
  the printed model IDs in its OpenAI API Key Override settings. Use the printed
233
241
  `/v1` Base URL in Cursor Settings; the profile itself stores the gateway origin
234
242
  so correlation hooks can post to `/v1/agent-hooks`.
package/bin/agents.js CHANGED
@@ -20,8 +20,16 @@ import readline from 'node:readline';
20
20
  import { fileURLToPath } from 'node:url';
21
21
  import { c } from './colors.js';
22
22
  import { getApiKey } from './auth.js';
23
- import { profileSettingsForAgent, resolvedProfileValues } from './profiles.js';
23
+ import {
24
+ isUnsetSetting,
25
+ profileSettingsForAgent,
26
+ resolvedModelSetting,
27
+ resolvedProfileValues,
28
+ } from './profiles.js';
24
29
  import { isLiveModelSource, PUBLIC_CATALOG_FALLBACK_MESSAGE, resolveModelCatalog } from './models.js';
30
+ import { compareVersions } from './update-check.js';
31
+
32
+ export const MIN_CLAUDE_CODE_VERSION = '2.1.242';
25
33
 
26
34
  // --- Registry (single source of truth, generated copy shipped in the package).
27
35
  const registry = JSON.parse(
@@ -258,6 +266,12 @@ function displayProfileValue(setting, value, values) {
258
266
  if (value) return '(set)';
259
267
  return setting.key !== 'API_KEY' && values.API_KEY ? '(shared key)' : '(not set)';
260
268
  }
269
+ if (
270
+ !value &&
271
+ (setting.key === 'MODEL' || setting.key === 'CLAUDE_CODE_SUBAGENT_MODEL')
272
+ ) {
273
+ return 'UNSET';
274
+ }
261
275
  return value || '(auto)';
262
276
  }
263
277
 
@@ -325,28 +339,40 @@ function buildContext(apiKey, model, profile) {
325
339
  /**
326
340
  * Pull a `--model <value>` / `--model=<value>` flag out of the passthrough
327
341
  * args (so it sets the Subconscious model rather than reaching the agent).
328
- * Falls back to SUBCONSCIOUS_MODEL, then the registry default.
342
+ * Falls back to SUBCONSCIOUS_MODEL, then the profile MODEL. UNSET or a blank
343
+ * profile model means "use the first live catalog entry" (gateway priority).
329
344
  */
330
- function extractModel(argv, profile) {
331
- const environmentModel = process.env.SUBCONSCIOUS_MODEL?.trim();
332
- const profileModel = profile?.values?.MODEL?.trim();
333
- let model = environmentModel || profileModel || DEFAULTS.model;
334
- let modelSource = environmentModel ? 'environment' : profileModel ? 'profile' : 'default';
345
+ export function extractModel(argv, profile) {
346
+ const environmentModel = resolvedModelSetting(process.env.SUBCONSCIOUS_MODEL);
347
+ const profileModel = resolvedModelSetting(profile?.values?.MODEL);
348
+ let model = environmentModel || profileModel || '';
349
+ let modelSource = environmentModel ? 'environment' : profileModel ? 'profile' : 'catalog';
335
350
  const rest = [];
336
351
  for (let i = 0; i < argv.length; i++) {
337
352
  const a = argv[i];
338
353
  if (a === '--model') {
339
354
  const v = argv[i + 1];
340
355
  if (v && !v.startsWith('-')) {
341
- model = v;
342
- modelSource = 'command';
356
+ if (isUnsetSetting(v)) {
357
+ model = '';
358
+ modelSource = 'catalog';
359
+ } else {
360
+ model = v;
361
+ modelSource = 'command';
362
+ }
343
363
  i++;
344
364
  }
345
365
  continue;
346
366
  }
347
367
  if (a.startsWith('--model=')) {
348
- model = a.slice('--model='.length);
349
- modelSource = 'command';
368
+ const v = a.slice('--model='.length);
369
+ if (isUnsetSetting(v)) {
370
+ model = '';
371
+ modelSource = 'catalog';
372
+ } else {
373
+ model = v;
374
+ modelSource = 'command';
375
+ }
350
376
  continue;
351
377
  }
352
378
  rest.push(a);
@@ -445,6 +471,16 @@ function askYesNo(question) {
445
471
  });
446
472
  }
447
473
 
474
+ function waitForEnter(prompt) {
475
+ return new Promise((resolve) => {
476
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
477
+ rl.question(prompt, () => {
478
+ rl.close();
479
+ resolve();
480
+ });
481
+ });
482
+ }
483
+
448
484
  /** Run the agent's install command (may contain `&&`, so shell:true). */
449
485
  function runInstaller(install) {
450
486
  return new Promise((resolve) => {
@@ -536,6 +572,51 @@ async function ensureInstalled(agent) {
536
572
  process.exit(0);
537
573
  }
538
574
 
575
+ export function parseClaudeVersion(text) {
576
+ const match = String(text ?? '').match(/v?(\d+\.\d+\.\d+)/);
577
+ return match?.[1] ?? null;
578
+ }
579
+
580
+ export function claudeVersionNeedsUpgrade(installed, minimum = MIN_CLAUDE_CODE_VERSION) {
581
+ return Boolean(installed && minimum && compareVersions(installed, minimum) < 0);
582
+ }
583
+
584
+ export function readClaudeVersion(bin, binDir, options = {}) {
585
+ const execFile = options.execFileSync || execFileSync;
586
+ try {
587
+ const stdout = execFile(bin, ['--version'], {
588
+ encoding: 'utf-8',
589
+ stdio: ['ignore', 'pipe', 'pipe'],
590
+ env: { ...process.env, PATH: augmentPath([binDir, ...candidateBinDirs()].filter(Boolean)) },
591
+ timeout: options.timeoutMs ?? 3000,
592
+ });
593
+ return parseClaudeVersion(typeof stdout === 'string' ? stdout : stdout?.toString?.());
594
+ } catch {
595
+ return null;
596
+ }
597
+ }
598
+
599
+ async function ensureClaudeCompatible(agent, binDir) {
600
+ if (agent.id !== 'claude-code') return;
601
+ const version = readClaudeVersion(agent.bin, binDir);
602
+ if (!claudeVersionNeedsUpgrade(version)) return;
603
+
604
+ console.error(
605
+ `\n Minimum supported Claude Code version is ${MIN_CLAUDE_CODE_VERSION}. Your version is ${version}. Upgrade to get the best experience.\n`,
606
+ );
607
+ console.error(` Upgrade it with:`);
608
+ console.error(` ${c.cyan}${agent.install}${c.reset}`);
609
+ if (agent.installFallback) {
610
+ console.error(` or`);
611
+ console.error(` ${c.cyan}${agent.installFallback}${c.reset}`);
612
+ }
613
+ console.error('');
614
+ if (process.stdin.isTTY === true && process.stdout.isTTY === true) {
615
+ await waitForEnter(` ${c.dim}Press Enter to continue.${c.reset} `);
616
+ console.error('');
617
+ }
618
+ }
619
+
539
620
  /** Resolve and validate a script inside the packaged runbook directory. */
540
621
  function runbookScriptPath(agent, relativeScript = agent.runbook.script) {
541
622
  const script = path.resolve(RUNBOOK_DIR, relativeScript);
@@ -639,21 +720,48 @@ const CLAUDE_MODEL_PICKER_KEYS = [
639
720
  'ANTHROPIC_CUSTOM_MODEL_OPTION_DESCRIPTION',
640
721
  ];
641
722
 
723
+ const CLAUDE_MODEL_PICKER_ID_KEYS = [
724
+ 'ANTHROPIC_DEFAULT_OPUS_MODEL',
725
+ 'ANTHROPIC_DEFAULT_SONNET_MODEL',
726
+ 'ANTHROPIC_DEFAULT_HAIKU_MODEL',
727
+ 'ANTHROPIC_DEFAULT_FABLE_MODEL',
728
+ 'ANTHROPIC_CUSTOM_MODEL_OPTION',
729
+ ];
730
+
731
+ function uniqueCatalogModels(models, fallbackModel) {
732
+ const unique = [];
733
+ for (const model of models) {
734
+ if (model && !unique.includes(model)) unique.push(model);
735
+ }
736
+ if (unique.length === 0 && fallbackModel) unique.push(fallbackModel);
737
+ return unique;
738
+ }
739
+
740
+ export function claudePickerSettings(models, fallbackModel) {
741
+ const unique = uniqueCatalogModels(models, fallbackModel);
742
+ return {
743
+ availableModels: unique,
744
+ modelPicker: {
745
+ replaceBuiltInOptions: true,
746
+ options: unique.map((model) => ({
747
+ model,
748
+ label: model,
749
+ description: `Subconscious model ${model}`,
750
+ })),
751
+ },
752
+ };
753
+ }
754
+
642
755
  function claudeModelPickerEnv(agent, ctx, models) {
643
756
  if (agent.id !== 'claude-code') return {};
644
- const configured = substitute(agent.env || {}, ctx);
645
- const configuredModels = [
646
- configured.ANTHROPIC_DEFAULT_OPUS_MODEL,
647
- configured.ANTHROPIC_DEFAULT_SONNET_MODEL,
648
- configured.ANTHROPIC_DEFAULT_HAIKU_MODEL,
649
- configured.ANTHROPIC_DEFAULT_FABLE_MODEL,
650
- configured.ANTHROPIC_CUSTOM_MODEL_OPTION,
651
- ];
652
- const pickerModels = [];
653
- for (const model of [...models, ...configuredModels]) {
654
- if (model && !pickerModels.includes(model)) pickerModels.push(model);
655
- }
656
- while (pickerModels.length < 3) pickerModels.push(pickerModels.at(-1) || ctx.model);
757
+ // Only the live catalog belongs in Claude's picker. Packaged registry slots
758
+ // (Haiku → DeepSeek, etc.) must not be unioned back in — that advertised
759
+ // models the key cannot call.
760
+ //
761
+ // Pad through Fable so the built-in `fable` alias cannot resolve to Anthropic
762
+ // Fable 5. The /model menu itself is replaced via claudePickerSettings.
763
+ const pickerModels = uniqueCatalogModels(models, ctx.model);
764
+ while (pickerModels.length < 4) pickerModels.push(pickerModels.at(-1) || ctx.model);
657
765
 
658
766
  const roles = ['OPUS', 'SONNET', 'HAIKU', 'FABLE'];
659
767
  const env = {};
@@ -676,6 +784,27 @@ function claudeModelPickerEnv(agent, ctx, models) {
676
784
  );
677
785
  }
678
786
 
787
+ function applyClaudePickerCatalog(env, picker, models) {
788
+ if (Object.keys(picker).length === 0) return env;
789
+ const allowed = new Set(models);
790
+
791
+ for (const key of CLAUDE_MODEL_PICKER_KEYS) {
792
+ if (!picker[key]) {
793
+ delete env[key];
794
+ }
795
+ }
796
+
797
+ for (const key of CLAUDE_MODEL_PICKER_ID_KEYS) {
798
+ if (!picker[key] || !env[key] || allowed.has(env[key])) continue;
799
+ env[key] = picker[key];
800
+ const nameKey = `${key}_NAME`;
801
+ const descriptionKey = `${key}_DESCRIPTION`;
802
+ if (picker[nameKey]) env[nameKey] = picker[nameKey];
803
+ if (picker[descriptionKey]) env[descriptionKey] = picker[descriptionKey];
804
+ }
805
+ return env;
806
+ }
807
+
679
808
  export function runbookEnv(
680
809
  apiKey,
681
810
  model,
@@ -687,18 +816,29 @@ export function runbookEnv(
687
816
  const ctx = buildContext(apiKey, model, profile);
688
817
  const extraDirs = [binDir, ...candidateBinDirs()].filter(Boolean);
689
818
  const specificApiKey = agentApiKeySetting(agent)?.key;
690
- return {
691
- ...claudeModelPickerEnv(agent, ctx, models),
692
- ...(profile?.values || {}),
693
- ...process.env,
694
- GATEWAY_URL: ctx.baseUrl,
695
- API_KEY: apiKey,
696
- ...(specificApiKey ? { [specificApiKey]: apiKey } : {}),
697
- MODEL: model,
698
- SUBCONSCIOUS_MODELS: models.join('\n'),
699
- SUBC_ENV_FILE: os.devNull,
700
- PATH: augmentPath(extraDirs),
701
- };
819
+ const picker = claudeModelPickerEnv(agent, ctx, models);
820
+ return applyClaudePickerCatalog(
821
+ {
822
+ ...picker,
823
+ ...(profile?.values || {}),
824
+ ...process.env,
825
+ GATEWAY_URL: ctx.baseUrl,
826
+ API_KEY: apiKey,
827
+ ...(specificApiKey ? { [specificApiKey]: apiKey } : {}),
828
+ MODEL: model,
829
+ SUBCONSCIOUS_MODELS: models.join('\n'),
830
+ ...(agent.id === 'claude-code'
831
+ ? {
832
+ SUBC_CLAUDE_SETTINGS: JSON.stringify(claudePickerSettings(models, model)),
833
+ CLAUDE_CODE_ENABLE_GATEWAY_MODEL_DISCOVERY: '0',
834
+ }
835
+ : {}),
836
+ SUBC_ENV_FILE: os.devNull,
837
+ PATH: augmentPath(extraDirs),
838
+ },
839
+ picker,
840
+ models,
841
+ );
702
842
  }
703
843
 
704
844
  async function resolvedModelsForLaunch(profile, apiKey, selectedModel) {
@@ -721,12 +861,16 @@ async function resolvedModelsForLaunch(profile, apiKey, selectedModel) {
721
861
  }
722
862
 
723
863
  export function selectLaunchModel(requestedModel, modelSource, catalog) {
864
+ const first = catalog.models[0] || requestedModel || DEFAULTS.model;
865
+ if (modelSource === 'catalog' || !requestedModel) {
866
+ return first;
867
+ }
724
868
  const useLiveDefault =
725
869
  isLiveModelSource(catalog.source) &&
726
870
  catalog.models.length > 0 &&
727
871
  !catalog.models.includes(requestedModel) &&
728
872
  (modelSource === 'profile' || modelSource === 'default');
729
- return useLiveDefault ? catalog.models[0] : requestedModel;
873
+ return useLiveDefault ? first : requestedModel;
730
874
  }
731
875
 
732
876
  async function runRunbookSetup(agent, argv, profile, relativeScript = agent.runbook.script) {
@@ -743,7 +887,7 @@ async function runRunbookSetup(agent, argv, profile, relativeScript = agent.runb
743
887
  if (!apiKey) return 1;
744
888
  const catalog = await resolvedModelsForLaunch(profile, apiKey, requestedModel);
745
889
  const model = selectLaunchModel(requestedModel, modelSource, catalog);
746
- if (model !== requestedModel) {
890
+ if (requestedModel && model !== requestedModel) {
747
891
  console.error(
748
892
  ` ${c.yellow}Configured model ${requestedModel} is not in the live catalog; using ${model}.${c.reset}\n`,
749
893
  );
@@ -810,9 +954,10 @@ export async function runAgent(agent, argv, options = {}) {
810
954
  if (!apiKey) return 1;
811
955
 
812
956
  const binDir = await ensureInstalled(agent);
957
+ await ensureClaudeCompatible(agent, binDir);
813
958
  const catalog = await resolvedModelsForLaunch(profile, apiKey, requestedModel);
814
959
  const model = selectLaunchModel(requestedModel, modelSource, catalog);
815
- if (model !== requestedModel) {
960
+ if (requestedModel && model !== requestedModel) {
816
961
  console.error(
817
962
  ` ${c.yellow}Configured model ${requestedModel} is not in the live catalog; using ${model}.${c.reset}\n`,
818
963
  );
package/bin/cli.js CHANGED
@@ -34,6 +34,7 @@ import {
34
34
  loadProfile,
35
35
  modelsCommand,
36
36
  printConfigHelp,
37
+ resolvedModelSetting,
37
38
  RUNBOOK_DEFAULTS,
38
39
  SUPPORTED_MODELS as PACKAGED_MODELS,
39
40
  updateUrlCommand,
@@ -74,7 +75,7 @@ function printHelp() {
74
75
  ${agents}
75
76
 
76
77
  ${c.bold}Options${c.reset}
77
- ${c.dim}--model <id>${c.reset} Model to use (default subconscious/glm-5.2)
78
+ ${c.dim}--model <id>${c.reset} Model to use (profile MODEL, or first live catalog model if UNSET)
78
79
  ${c.dim}-p, --profile${c.reset} Select a profile (default: default)
79
80
  ${c.dim}-h, --help${c.reset} Show this help
80
81
  ${c.dim}-v, --version${c.reset} Show version
@@ -154,7 +155,8 @@ Usage:
154
155
  subc models
155
156
  subc models help
156
157
 
157
- List available Subconscious models.
158
+ List available Subconscious models. The default is the profile MODEL,
159
+ or the first live catalog model if MODEL is UNSET.
158
160
  `,
159
161
  };
160
162
 
@@ -309,10 +311,9 @@ async function main() {
309
311
  }
310
312
  const profile = await loadProfile(profileName);
311
313
  const auth = await getApiKey(profile);
312
- const selectedModel =
313
- process.env.SUBCONSCIOUS_MODEL?.trim() ||
314
- profile.values.MODEL?.trim() ||
315
- RUNBOOK_DEFAULTS.MODEL;
314
+ const selectedModel = resolvedModelSetting(
315
+ process.env.SUBCONSCIOUS_MODEL || profile.values.MODEL,
316
+ );
316
317
  const baseUrl =
317
318
  process.env.SUBCONSCIOUS_BASE_URL?.trim() ||
318
319
  profile.values.GATEWAY_URL?.trim() ||
Binary file
Binary file
Binary file
Binary file
package/bin/profiles.js CHANGED
@@ -84,14 +84,15 @@ const SETTINGS = {
84
84
  MODEL: {
85
85
  key: 'MODEL',
86
86
  label: 'Default model',
87
- description: 'Initial model used by coding-agent launches and setup.',
87
+ description:
88
+ 'Initial model used by coding-agent launches and setup. UNSET uses the first live catalog model.',
88
89
  type: 'choice',
89
90
  choices: SUPPORTED_MODELS,
90
91
  },
91
92
  CLAUDE_CODE_SUBAGENT_MODEL: {
92
93
  key: 'CLAUDE_CODE_SUBAGENT_MODEL',
93
94
  label: 'Subagent model',
94
- description: 'Model used by Claude Code subagents; blank follows the default model.',
95
+ description: 'Model used by Claude Code subagents. UNSET follows the default model.',
95
96
  type: 'choice',
96
97
  choices: SUPPORTED_MODELS,
97
98
  },
@@ -340,7 +341,7 @@ function profileTemplate() {
340
341
 
341
342
  GATEWAY_URL={GATEWAY_URL}
342
343
  API_KEY={API_KEY}
343
- # Available models:
344
+ # Available models (MODEL=UNSET uses the first live catalog model):
344
345
  ${modelComments}
345
346
  MODEL={MODEL}
346
347
 
@@ -356,7 +357,7 @@ DEEPSEEK_HARNESS_API_KEY={DEEPSEEK_HARNESS_API_KEY}
356
357
  # Claude Code
357
358
  # Leave CLAUDE_GATEWAY_URL blank to use GATEWAY_URL above.
358
359
  CLAUDE_GATEWAY_URL={CLAUDE_GATEWAY_URL}
359
- # Leave blank to use MODEL above for Claude Code subagents.
360
+ # CLAUDE_CODE_SUBAGENT_MODEL=UNSET follows MODEL above.
360
361
  CLAUDE_CODE_SUBAGENT_MODEL={CLAUDE_CODE_SUBAGENT_MODEL}
361
362
  CLAUDE_CODE_AUTO_COMPACT_WINDOW={CLAUDE_CODE_AUTO_COMPACT_WINDOW}
362
363
  CLAUDE_CODE_MAX_CONTEXT_TOKENS={CLAUDE_CODE_MAX_CONTEXT_TOKENS}
@@ -600,6 +601,16 @@ async function printProfile(profile) {
600
601
  console.log();
601
602
  }
602
603
 
604
+ export function isUnsetSetting(value) {
605
+ const normalized = String(value ?? '').trim().toUpperCase();
606
+ return normalized === 'UNSET' || normalized === 'FOLLOW-GATEWAY' || normalized === 'FOLLOW-DEFAULT';
607
+ }
608
+
609
+ export function resolvedModelSetting(value) {
610
+ const trimmed = String(value ?? '').trim();
611
+ return isUnsetSetting(trimmed) ? '' : trimmed;
612
+ }
613
+
603
614
  export function validateSettingValue(setting, value) {
604
615
  if (!value) {
605
616
  if (setting.required) return `${setting.label} cannot be blank`;
@@ -691,8 +702,9 @@ Usage:
691
702
  subc config edit [vim|nano]
692
703
  subc -p NAME config edit [vim|nano]
693
704
  subc config [show|path|list|create|delete]
694
- [--gateway-url URL] [--api-key KEY] [--model MODEL]
695
- [--subagent-model MODEL|follow-default]
705
+ [--gateway-url URL] [--api-key KEY]
706
+ [--model MODEL|UNSET]
707
+ [--subagent-model MODEL|UNSET]
696
708
 
697
709
  subc config List every profile and its file path
698
710
  subc -p NAME config Print that profile's path and env file
@@ -702,6 +714,9 @@ Usage:
702
714
  subc config path Print the selected profile path
703
715
  subc -p NAME config create Create a new profile with default settings
704
716
  subc -p NAME config delete Delete a non-default profile
717
+
718
+ --model UNSET Clear MODEL so launches use the first live catalog model
719
+ --subagent-model UNSET Clear the Claude subagent override so it follows MODEL
705
720
  `);
706
721
  }
707
722
 
@@ -745,7 +760,8 @@ export async function configCommand(argv, profileName = DEFAULT_PROFILE, options
745
760
  '--model': 'MODEL',
746
761
  '--subagent-model': 'CLAUDE_CODE_SUBAGENT_MODEL',
747
762
  }[arg];
748
- updates[key] = arg === '--subagent-model' && value === 'follow-default' ? '' : value;
763
+ updates[key] =
764
+ (arg === '--model' || arg === '--subagent-model') && isUnsetSetting(value) ? '' : value;
749
765
  } else if (action === 'edit' && ALLOWED_EDITORS.has(arg)) {
750
766
  if (editor) throw new Error('Specify only one editor (vim or nano)');
751
767
  editor = arg;
@@ -813,7 +829,7 @@ export async function configCommand(argv, profileName = DEFAULT_PROFILE, options
813
829
  }
814
830
 
815
831
  export function modelsCommand(models = SUPPORTED_MODELS, options = {}) {
816
- const selectedModel = options.selectedModel || registry.defaults.model;
832
+ const selectedModel = options.selectedModel || models[0] || registry.defaults.model;
817
833
  console.log(`\n ${c.bold}Available models${c.reset}\n`);
818
834
  for (const model of models) {
819
835
  const suffix = model === selectedModel ? ` ${c.dim}(default)${c.reset}` : '';
@@ -62,9 +62,6 @@ DEFAULT_COMPACT_WINDOW="1000000"
62
62
  DEFAULT_MAX_CONTEXT_TOKENS="3000000"
63
63
  DEFAULT_MAX_CONCURRENT_SUBAGENTS="4"
64
64
  DEFAULT_MAX_SUBAGENT_SPAWN_DEPTH="1"
65
- DEFAULT_OPUS_MODEL="subconscious/glm-5.2"
66
- DEFAULT_SONNET_MODEL="subconscious/tim-qwen3.6-27b"
67
- DEFAULT_HAIKU_MODEL="subconscious/deepseek-v4-flash-marathon"
68
65
 
69
66
  GATEWAY_URL="${GATEWAY_URL:-}"
70
67
  API_KEY="${CLAUDE_CODE_API_KEY:-${API_KEY:-}}"
@@ -73,15 +70,55 @@ COMPACT_WINDOW="${CLAUDE_CODE_AUTO_COMPACT_WINDOW:-${COMPACT_WINDOW:-$DEFAULT_CO
73
70
  MAX_CONTEXT_TOKENS="${CLAUDE_CODE_MAX_CONTEXT_TOKENS:-${MAX_CONTEXT_TOKENS:-$DEFAULT_MAX_CONTEXT_TOKENS}}"
74
71
  MAX_CONCURRENT_SUBAGENTS="${MAX_CONCURRENT_SUBAGENTS:-$DEFAULT_MAX_CONCURRENT_SUBAGENTS}"
75
72
  MAX_SUBAGENT_SPAWN_DEPTH="${MAX_SUBAGENT_SPAWN_DEPTH:-$DEFAULT_MAX_SUBAGENT_SPAWN_DEPTH}"
73
+
74
+ PICKER_MODELS=()
75
+ if [[ -n "${SUBCONSCIOUS_MODELS:-}" ]]; then
76
+ while IFS= read -r picker_model; do
77
+ [[ -n "$picker_model" ]] && PICKER_MODELS+=("$picker_model")
78
+ done <<< "$SUBCONSCIOUS_MODELS"
79
+ fi
80
+
81
+ picker_model_at() {
82
+ local index="$1"
83
+ local count="${#PICKER_MODELS[@]}"
84
+ if [[ "$count" -eq 0 ]]; then
85
+ printf '%s' "$MODEL"
86
+ return
87
+ fi
88
+ if [[ "$index" -ge "$count" ]]; then
89
+ index=$((count - 1))
90
+ fi
91
+ printf '%s' "${PICKER_MODELS[$index]}"
92
+ }
93
+
94
+ in_picker_catalog() {
95
+ local candidate="$1" existing
96
+ [[ -z "$candidate" ]] && return 1
97
+ [[ "${#PICKER_MODELS[@]}" -eq 0 ]] && return 0
98
+ for existing in "${PICKER_MODELS[@]}"; do
99
+ [[ "$existing" == "$candidate" ]] && return 0
100
+ done
101
+ return 1
102
+ }
103
+
104
+ if [[ "${#PICKER_MODELS[@]}" -gt 0 ]]; then
105
+ in_picker_catalog "${ANTHROPIC_DEFAULT_OPUS_MODEL:-}" || ANTHROPIC_DEFAULT_OPUS_MODEL=""
106
+ in_picker_catalog "${ANTHROPIC_DEFAULT_SONNET_MODEL:-}" || ANTHROPIC_DEFAULT_SONNET_MODEL=""
107
+ in_picker_catalog "${ANTHROPIC_DEFAULT_HAIKU_MODEL:-}" || ANTHROPIC_DEFAULT_HAIKU_MODEL=""
108
+ fi
109
+
110
+ DEFAULT_OPUS_MODEL="$(picker_model_at 0)"
111
+ DEFAULT_SONNET_MODEL="$(picker_model_at 1)"
112
+ DEFAULT_HAIKU_MODEL="$(picker_model_at 2)"
76
113
  OPUS_MODEL="${ANTHROPIC_DEFAULT_OPUS_MODEL:-$DEFAULT_OPUS_MODEL}"
77
114
  OPUS_MODEL_NAME="${ANTHROPIC_DEFAULT_OPUS_MODEL_NAME:-$OPUS_MODEL}"
78
- OPUS_MODEL_DESCRIPTION="${ANTHROPIC_DEFAULT_OPUS_MODEL_DESCRIPTION:-Subconscious default model}"
115
+ OPUS_MODEL_DESCRIPTION="${ANTHROPIC_DEFAULT_OPUS_MODEL_DESCRIPTION:-Subconscious model ${OPUS_MODEL}}"
79
116
  SONNET_MODEL="${ANTHROPIC_DEFAULT_SONNET_MODEL:-$DEFAULT_SONNET_MODEL}"
80
117
  SONNET_MODEL_NAME="${ANTHROPIC_DEFAULT_SONNET_MODEL_NAME:-$SONNET_MODEL}"
81
- SONNET_MODEL_DESCRIPTION="${ANTHROPIC_DEFAULT_SONNET_MODEL_DESCRIPTION:-Subconscious TIM Qwen 3.6 27B}"
118
+ SONNET_MODEL_DESCRIPTION="${ANTHROPIC_DEFAULT_SONNET_MODEL_DESCRIPTION:-Subconscious model ${SONNET_MODEL}}"
82
119
  HAIKU_MODEL="${ANTHROPIC_DEFAULT_HAIKU_MODEL:-$DEFAULT_HAIKU_MODEL}"
83
120
  HAIKU_MODEL_NAME="${ANTHROPIC_DEFAULT_HAIKU_MODEL_NAME:-$HAIKU_MODEL}"
84
- HAIKU_MODEL_DESCRIPTION="${ANTHROPIC_DEFAULT_HAIKU_MODEL_DESCRIPTION:-Subconscious DeepSeek V4 Flash Marathon}"
121
+ HAIKU_MODEL_DESCRIPTION="${ANTHROPIC_DEFAULT_HAIKU_MODEL_DESCRIPTION:-Subconscious model ${HAIKU_MODEL}}"
85
122
  COMMAND="install"
86
123
 
87
124
  usage() {
@@ -30,9 +30,6 @@ DEFAULT_COMPACT_WINDOW="1000000"
30
30
  DEFAULT_MAX_CONTEXT_TOKENS="3000000"
31
31
  DEFAULT_MAX_CONCURRENT_SUBAGENTS="4"
32
32
  DEFAULT_MAX_SUBAGENT_SPAWN_DEPTH="1"
33
- DEFAULT_OPUS_MODEL="subconscious/glm-5.2"
34
- DEFAULT_SONNET_MODEL="subconscious/tim-qwen3.6-27b"
35
- DEFAULT_HAIKU_MODEL="subconscious/deepseek-v4-flash-marathon"
36
33
 
37
34
  GATEWAY_URL="${GATEWAY_URL:-}"
38
35
  API_KEY="${CLAUDE_CODE_API_KEY:-${API_KEY:-}}"
@@ -41,15 +38,94 @@ COMPACT_WINDOW="${CLAUDE_CODE_AUTO_COMPACT_WINDOW:-${COMPACT_WINDOW:-$DEFAULT_CO
41
38
  MAX_CONTEXT_TOKENS="${CLAUDE_CODE_MAX_CONTEXT_TOKENS:-${MAX_CONTEXT_TOKENS:-$DEFAULT_MAX_CONTEXT_TOKENS}}"
42
39
  MAX_CONCURRENT_SUBAGENTS="${MAX_CONCURRENT_SUBAGENTS:-$DEFAULT_MAX_CONCURRENT_SUBAGENTS}"
43
40
  MAX_SUBAGENT_SPAWN_DEPTH="${MAX_SUBAGENT_SPAWN_DEPTH:-$DEFAULT_MAX_SUBAGENT_SPAWN_DEPTH}"
41
+
42
+ # Claude's Opus/Sonnet/Haiku/Fable/custom slots must follow the live catalog
43
+ # (SUBCONSCIOUS_MODELS). Packaged DeepSeek/GLM defaults are not a capability.
44
+ PICKER_MODELS=()
45
+ if [[ -n "${SUBCONSCIOUS_MODELS:-}" ]]; then
46
+ while IFS= read -r picker_model; do
47
+ [[ -n "$picker_model" ]] && PICKER_MODELS+=("$picker_model")
48
+ done <<< "$SUBCONSCIOUS_MODELS"
49
+ fi
50
+
51
+ picker_model_at() {
52
+ local index="$1"
53
+ local count="${#PICKER_MODELS[@]}"
54
+ if [[ "$count" -eq 0 ]]; then
55
+ printf '%s' "$MODEL"
56
+ return
57
+ fi
58
+ if [[ "$index" -ge "$count" ]]; then
59
+ index=$((count - 1))
60
+ fi
61
+ printf '%s' "${PICKER_MODELS[$index]}"
62
+ }
63
+
64
+ in_picker_catalog() {
65
+ local candidate="$1" existing
66
+ [[ -z "$candidate" ]] && return 1
67
+ [[ "${#PICKER_MODELS[@]}" -eq 0 ]] && return 0
68
+ for existing in "${PICKER_MODELS[@]}"; do
69
+ [[ "$existing" == "$candidate" ]] && return 0
70
+ done
71
+ return 1
72
+ }
73
+
74
+ if [[ "${#PICKER_MODELS[@]}" -gt 0 ]]; then
75
+ in_picker_catalog "${ANTHROPIC_DEFAULT_OPUS_MODEL:-}" || ANTHROPIC_DEFAULT_OPUS_MODEL=""
76
+ in_picker_catalog "${ANTHROPIC_DEFAULT_SONNET_MODEL:-}" || ANTHROPIC_DEFAULT_SONNET_MODEL=""
77
+ in_picker_catalog "${ANTHROPIC_DEFAULT_HAIKU_MODEL:-}" || ANTHROPIC_DEFAULT_HAIKU_MODEL=""
78
+ in_picker_catalog "${ANTHROPIC_DEFAULT_FABLE_MODEL:-}" || unset ANTHROPIC_DEFAULT_FABLE_MODEL ANTHROPIC_DEFAULT_FABLE_MODEL_NAME ANTHROPIC_DEFAULT_FABLE_MODEL_DESCRIPTION
79
+ if [[ "${#PICKER_MODELS[@]}" -lt 5 ]] || ! in_picker_catalog "${ANTHROPIC_CUSTOM_MODEL_OPTION:-}"; then
80
+ unset ANTHROPIC_CUSTOM_MODEL_OPTION ANTHROPIC_CUSTOM_MODEL_OPTION_NAME ANTHROPIC_CUSTOM_MODEL_OPTION_DESCRIPTION
81
+ fi
82
+ fi
83
+
84
+ DEFAULT_OPUS_MODEL="$(picker_model_at 0)"
85
+ DEFAULT_SONNET_MODEL="$(picker_model_at 1)"
86
+ DEFAULT_HAIKU_MODEL="$(picker_model_at 2)"
87
+ DEFAULT_FABLE_MODEL="$(picker_model_at 3)"
44
88
  OPUS_MODEL="${ANTHROPIC_DEFAULT_OPUS_MODEL:-$DEFAULT_OPUS_MODEL}"
45
89
  OPUS_MODEL_NAME="${ANTHROPIC_DEFAULT_OPUS_MODEL_NAME:-$OPUS_MODEL}"
46
- OPUS_MODEL_DESCRIPTION="${ANTHROPIC_DEFAULT_OPUS_MODEL_DESCRIPTION:-Subconscious default model}"
90
+ OPUS_MODEL_DESCRIPTION="${ANTHROPIC_DEFAULT_OPUS_MODEL_DESCRIPTION:-Subconscious model ${OPUS_MODEL}}"
47
91
  SONNET_MODEL="${ANTHROPIC_DEFAULT_SONNET_MODEL:-$DEFAULT_SONNET_MODEL}"
48
92
  SONNET_MODEL_NAME="${ANTHROPIC_DEFAULT_SONNET_MODEL_NAME:-$SONNET_MODEL}"
49
- SONNET_MODEL_DESCRIPTION="${ANTHROPIC_DEFAULT_SONNET_MODEL_DESCRIPTION:-Subconscious TIM Qwen 3.6 27B}"
93
+ SONNET_MODEL_DESCRIPTION="${ANTHROPIC_DEFAULT_SONNET_MODEL_DESCRIPTION:-Subconscious model ${SONNET_MODEL}}"
50
94
  HAIKU_MODEL="${ANTHROPIC_DEFAULT_HAIKU_MODEL:-$DEFAULT_HAIKU_MODEL}"
51
95
  HAIKU_MODEL_NAME="${ANTHROPIC_DEFAULT_HAIKU_MODEL_NAME:-$HAIKU_MODEL}"
52
- HAIKU_MODEL_DESCRIPTION="${ANTHROPIC_DEFAULT_HAIKU_MODEL_DESCRIPTION:-Subconscious DeepSeek V4 Flash Marathon}"
96
+ HAIKU_MODEL_DESCRIPTION="${ANTHROPIC_DEFAULT_HAIKU_MODEL_DESCRIPTION:-Subconscious model ${HAIKU_MODEL}}"
97
+ FABLE_MODEL="${ANTHROPIC_DEFAULT_FABLE_MODEL:-$DEFAULT_FABLE_MODEL}"
98
+ FABLE_MODEL_NAME="${ANTHROPIC_DEFAULT_FABLE_MODEL_NAME:-$FABLE_MODEL}"
99
+ FABLE_MODEL_DESCRIPTION="${ANTHROPIC_DEFAULT_FABLE_MODEL_DESCRIPTION:-Subconscious model ${FABLE_MODEL}}"
100
+
101
+ # Model IDs are already restricted to [-A-Za-z0-9._:/+], so they are JSON-safe.
102
+ build_claude_picker_settings() {
103
+ local ids=() model_id first options allowlist
104
+ if [[ "${#PICKER_MODELS[@]}" -gt 0 ]]; then
105
+ ids=("${PICKER_MODELS[@]}")
106
+ else
107
+ ids=("$MODEL")
108
+ fi
109
+ options=""
110
+ allowlist=""
111
+ first=1
112
+ for model_id in "${ids[@]}"; do
113
+ [[ -n "$model_id" ]] || continue
114
+ if [[ "$first" -eq 1 ]]; then
115
+ first=0
116
+ else
117
+ options="${options},"
118
+ allowlist="${allowlist},"
119
+ fi
120
+ options="${options}{\"model\":\"${model_id}\",\"label\":\"${model_id}\",\"description\":\"Subconscious model ${model_id}\"}"
121
+ allowlist="${allowlist}\"${model_id}\""
122
+ done
123
+ printf '{"availableModels":[%s],"modelPicker":{"replaceBuiltInOptions":true,"options":[%s]}}' "$allowlist" "$options"
124
+ }
125
+
126
+ if [[ -z "${SUBC_CLAUDE_SETTINGS:-}" ]]; then
127
+ SUBC_CLAUDE_SETTINGS="$(build_claude_picker_settings)"
128
+ fi
53
129
 
54
130
  usage() {
55
131
  cat <<'EOF'
@@ -136,6 +212,10 @@ export ANTHROPIC_DEFAULT_SONNET_MODEL_DESCRIPTION="${ANTHROPIC_DEFAULT_SONNET_MO
136
212
  export ANTHROPIC_DEFAULT_HAIKU_MODEL="${ANTHROPIC_DEFAULT_HAIKU_MODEL:-$HAIKU_MODEL}"
137
213
  export ANTHROPIC_DEFAULT_HAIKU_MODEL_NAME="${ANTHROPIC_DEFAULT_HAIKU_MODEL_NAME:-$HAIKU_MODEL_NAME}"
138
214
  export ANTHROPIC_DEFAULT_HAIKU_MODEL_DESCRIPTION="${ANTHROPIC_DEFAULT_HAIKU_MODEL_DESCRIPTION:-$HAIKU_MODEL_DESCRIPTION}"
215
+ export ANTHROPIC_DEFAULT_FABLE_MODEL="${ANTHROPIC_DEFAULT_FABLE_MODEL:-$FABLE_MODEL}"
216
+ export ANTHROPIC_DEFAULT_FABLE_MODEL_NAME="${ANTHROPIC_DEFAULT_FABLE_MODEL_NAME:-$FABLE_MODEL_NAME}"
217
+ export ANTHROPIC_DEFAULT_FABLE_MODEL_DESCRIPTION="${ANTHROPIC_DEFAULT_FABLE_MODEL_DESCRIPTION:-$FABLE_MODEL_DESCRIPTION}"
218
+ export CLAUDE_CODE_ENABLE_GATEWAY_MODEL_DISCOVERY="${CLAUDE_CODE_ENABLE_GATEWAY_MODEL_DISCOVERY:-0}"
139
219
  export CLAUDE_CODE_SUBAGENT_MODEL="${CLAUDE_CODE_SUBAGENT_MODEL:-$MODEL}"
140
220
  export CLAUDE_CODE_MAX_CONCURRENT_SUBAGENTS="${MAX_CONCURRENT_SUBAGENTS}"
141
221
  export CLAUDE_CODE_MAX_SUBAGENT_SPAWN_DEPTH="${MAX_SUBAGENT_SPAWN_DEPTH}"
@@ -164,4 +244,6 @@ if [[ "${BASH_SOURCE[0]:-$0}" != "${0}" ]]; then
164
244
  fi
165
245
 
166
246
  # If executed, launch claude with any passed-through args.
167
- exec claude ${PASSTHRU[@]+"${PASSTHRU[@]}"}
247
+ # --settings is additional JSON: it replaces the built-in Fable/Opus lineup
248
+ # with the live catalog and allowlists only those models.
249
+ exec claude --settings "${SUBC_CLAUDE_SETTINGS}" ${PASSTHRU[@]+"${PASSTHRU[@]}"}
package/bin/tui.js CHANGED
@@ -11,6 +11,7 @@ import {
11
11
  DEFAULT_PROFILE,
12
12
  listProfiles,
13
13
  loadProfile,
14
+ resolvedModelSetting,
14
15
  RUNBOOK_DEFAULTS,
15
16
  SUPPORTED_MODELS as PACKAGED_MODELS,
16
17
  } from './profiles.js';
@@ -70,15 +71,11 @@ export async function resolveTuiExecutable(options = {}) {
70
71
  }
71
72
 
72
73
  function selectedModelFor(profile) {
73
- return (
74
- process.env.SUBCONSCIOUS_MODEL?.trim() ||
75
- profile.values.MODEL?.trim() ||
76
- RUNBOOK_DEFAULTS.MODEL
77
- );
74
+ return resolvedModelSetting(process.env.SUBCONSCIOUS_MODEL || profile.values.MODEL);
78
75
  }
79
76
 
80
77
  function subagentModelFor(profile) {
81
- return profile.values.CLAUDE_CODE_SUBAGENT_MODEL?.trim() || '';
78
+ return resolvedModelSetting(profile.values.CLAUDE_CODE_SUBAGENT_MODEL);
82
79
  }
83
80
 
84
81
  function gatewayFor(profile) {
@@ -117,14 +114,15 @@ export async function createTuiState(profileName = DEFAULT_PROFILE, options = {}
117
114
  );
118
115
 
119
116
  const auth = await getApiKey(activeProfile);
120
- const selectedModel = selectedModelFor(activeProfile);
117
+ const requestedModel = selectedModelFor(activeProfile);
121
118
  const gatewayUrl = gatewayFor(activeProfile);
122
119
  const catalog = await resolveModelCatalog({
123
120
  baseUrl: gatewayUrl,
124
121
  apiKey: auth?.key,
125
- selectedModel,
122
+ selectedModel: requestedModel,
126
123
  fallbackModels: PACKAGED_MODELS,
127
124
  });
125
+ const selectedModel = requestedModel;
128
126
 
129
127
  return {
130
128
  version: await packageVersion(),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "subconscious-cli",
3
- "version": "4.0.13",
3
+ "version": "4.0.14",
4
4
  "description": "CLI for Subconscious — run Claude Code, Codex, OpenCode, DeepSeek Harness, Cursor, Copilot, and Pi",
5
5
  "bin": {
6
6
  "subc": "bin/cli.js"