subconscious-cli 4.0.10 → 4.0.12

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
@@ -40,6 +40,13 @@ leaving the TUI.
40
40
  `subc help` and `subc --help` continue to print script-friendly command help.
41
41
  Non-interactive `subc` also prints regular help.
42
42
 
43
+ Print the installed CLI version without performing an update or gateway check:
44
+
45
+ ```bash
46
+ subc --version
47
+ subc -v
48
+ ```
49
+
43
50
  Every command accepts `help` as a subcommand. These only read the selected
44
51
  profile; they do not authenticate, install, configure, or launch anything:
45
52
 
@@ -121,7 +128,8 @@ also require `jq`; Cursor and Copilot also require `curl`.
121
128
 
122
129
  `subc dsh` defaults to the DeepSeek Harness Web UI. It injects an ephemeral
123
130
  Cordis overlay containing the active gateway URL, selected model, token limits,
124
- client header, and every model returned by `/v1/models`; the API key remains in
131
+ client header, and every model from the live catalog (`/v1/models/available`
132
+ when a profile key is present, otherwise public `/v1/models`); the API key remains in
125
133
  the process environment and is never written into the overlay. The temporary
126
134
  file is removed when Harness exits, and existing `$DSH_HOME` settings are not
127
135
  rewritten. Run a one-shot task with `subc dsh headless "PROMPT"`.
@@ -187,9 +195,10 @@ values. A command-line `--model` override has the highest model precedence.
187
195
 
188
196
  ## Models and endpoint overrides
189
197
 
190
- List the available models with `subc models`. The command fetches the active
191
- gateway's authenticated `/v1/models` catalog and falls back to the models
192
- packaged with the CLI when discovery is unavailable:
198
+ List the available models with `subc models`. When the selected profile has an
199
+ API key, the command fetches the key-scoped `/v1/models/available` catalog. If
200
+ that request fails, it falls back to the public `/v1/models` fleet list, then
201
+ to the models packaged with the CLI:
193
202
 
194
203
  ```text
195
204
  subconscious/glm-5.2 (default)
package/bin/agents.js CHANGED
@@ -21,7 +21,7 @@ import { fileURLToPath } from 'node:url';
21
21
  import { c } from './colors.js';
22
22
  import { getApiKey } from './auth.js';
23
23
  import { profileSettingsForAgent, resolvedProfileValues } from './profiles.js';
24
- import { resolveModelCatalog } from './models.js';
24
+ import { isLiveModelSource, PUBLIC_CATALOG_FALLBACK_MESSAGE, resolveModelCatalog } from './models.js';
25
25
 
26
26
  // --- Registry (single source of truth, generated copy shipped in the package).
27
27
  const registry = JSON.parse(
@@ -709,7 +709,9 @@ async function resolvedModelsForLaunch(profile, apiKey, selectedModel) {
709
709
  selectedModel,
710
710
  fallbackModels: PACKAGED_MODELS,
711
711
  });
712
- if (catalog.error) {
712
+ if (catalog.source === 'public' && apiKey) {
713
+ console.error(` ${c.dim}${PUBLIC_CATALOG_FALLBACK_MESSAGE}${c.reset}\n`);
714
+ } else if (catalog.error) {
713
715
  console.error(
714
716
  ` ${c.yellow}Could not fetch the live model catalog; using packaged defaults.${c.reset}`,
715
717
  );
@@ -720,7 +722,7 @@ async function resolvedModelsForLaunch(profile, apiKey, selectedModel) {
720
722
 
721
723
  export function selectLaunchModel(requestedModel, modelSource, catalog) {
722
724
  const useLiveDefault =
723
- catalog.source === 'gateway' &&
725
+ isLiveModelSource(catalog.source) &&
724
726
  catalog.models.length > 0 &&
725
727
  !catalog.models.includes(requestedModel) &&
726
728
  (modelSource === 'profile' || modelSource === 'default');
package/bin/cli.js CHANGED
@@ -226,13 +226,22 @@ function requireNamedProfile(profile) {
226
226
  }
227
227
 
228
228
  async function main() {
229
- const update = await showUpdateNotice();
230
- if (update?.action === 'updated' || update?.action === 'cancel') return;
231
-
232
229
  let parsed = extractProfile(process.argv.slice(2));
233
230
  let { args, profileName, profileExplicit } = parsed;
234
231
  let command = args[0];
235
232
 
233
+ // Version output must be fast and reliable even when npm or the gateway is
234
+ // unavailable, so handle it before the automatic update check.
235
+ if (command === '--version' || command === '-v') {
236
+ const pkgPath = new URL('../package.json', import.meta.url);
237
+ const pkg = JSON.parse(await fs.readFile(pkgPath, 'utf-8'));
238
+ console.log(pkg.version);
239
+ return;
240
+ }
241
+
242
+ const update = await showUpdateNotice();
243
+ if (update?.action === 'updated' || update?.action === 'cancel') return;
244
+
236
245
  if (!command && process.stdin.isTTY === true && process.stdout.isTTY === true && process.env.TERM !== 'dumb') {
237
246
  const selection = await runTui({ profileName });
238
247
  if (!selection?.args?.length) return;
@@ -261,13 +270,6 @@ async function main() {
261
270
  return;
262
271
  }
263
272
 
264
- if (command === '--version' || command === '-v') {
265
- const pkgPath = new URL('../package.json', import.meta.url);
266
- const pkg = JSON.parse(await fs.readFile(pkgPath, 'utf-8'));
267
- console.log(pkg.version);
268
- return;
269
- }
270
-
271
273
  if (command === 'setup') {
272
274
  printRetiredSetup(args.slice(1));
273
275
  process.exitCode = 1;
@@ -321,7 +323,12 @@ async function main() {
321
323
  selectedModel,
322
324
  fallbackModels: PACKAGED_MODELS,
323
325
  });
324
- modelsCommand(catalog.models, { selectedModel, error: catalog.error });
326
+ modelsCommand(catalog.models, {
327
+ selectedModel,
328
+ error: catalog.error,
329
+ source: catalog.source,
330
+ hasApiKey: Boolean(auth?.key),
331
+ });
325
332
  return;
326
333
  }
327
334
 
package/bin/models.js CHANGED
@@ -1,12 +1,22 @@
1
1
  /**
2
2
  * Resolve the model catalog exposed by an OpenAI-compatible gateway.
3
3
  *
4
- * Live discovery is best-effort: launches should keep working with the
5
- * packaged registry when the gateway is offline or does not expose /v1/models.
4
+ * When a profile (or agent) API key is present, prefer the key-scoped
5
+ * /v1/models/available list. If that request fails, fall back to the public
6
+ * /v1/models fleet catalog. Launches keep working with the packaged registry
7
+ * when the gateway is offline or returns nothing usable.
6
8
  */
7
9
 
8
10
  const MODEL_ID_PATTERN = /^[-A-Za-z0-9._:/+]+$/;
9
11
  export const DEFAULT_MODEL_FETCH_TIMEOUT_MS = 3000;
12
+ export const AVAILABLE_MODELS_PATH = '/v1/models/available';
13
+ export const PUBLIC_MODELS_PATH = '/v1/models';
14
+ export const PUBLIC_CATALOG_FALLBACK_MESSAGE =
15
+ 'Provisioned catalog unavailable; showing the public model list.';
16
+
17
+ export function isLiveModelSource(source) {
18
+ return source === 'available' || source === 'public';
19
+ }
10
20
 
11
21
  export function normalizeModelIds(modelIds = [], selectedModel) {
12
22
  const models = [];
@@ -27,19 +37,37 @@ export function normalizeModelIds(modelIds = [], selectedModel) {
27
37
  return models;
28
38
  }
29
39
 
40
+ function gatewayOrigin(baseUrl) {
41
+ if (!baseUrl?.trim()) throw new Error('Gateway URL is not configured');
42
+ return baseUrl.trim().replace(/\/+$/, '');
43
+ }
44
+
45
+ function modelsEndpoint(baseUrl, path) {
46
+ const normalizedPath = path.startsWith('/') ? path : `/${path}`;
47
+ return `${gatewayOrigin(baseUrl)}${normalizedPath}`;
48
+ }
49
+
30
50
  export async function fetchGatewayModels({
31
51
  baseUrl,
32
52
  apiKey,
53
+ path = PUBLIC_MODELS_PATH,
33
54
  fetchImpl = globalThis.fetch,
34
55
  timeoutMs = DEFAULT_MODEL_FETCH_TIMEOUT_MS,
35
56
  }) {
36
- if (!baseUrl?.trim()) throw new Error('Gateway URL is not configured');
37
- if (!apiKey?.trim()) throw new Error('No API key is available for model discovery');
38
57
  if (typeof fetchImpl !== 'function') {
39
58
  throw new Error('This Node.js version does not support fetch');
40
59
  }
41
60
 
42
- const endpoint = `${baseUrl.trim().replace(/\/+$/, '')}/v1/models`;
61
+ const endpoint = modelsEndpoint(baseUrl, path);
62
+ const headers = {
63
+ Accept: 'application/json',
64
+ 'Cache-Control': 'no-cache, no-store',
65
+ Pragma: 'no-cache',
66
+ };
67
+ if (apiKey?.trim()) {
68
+ headers.Authorization = `Bearer ${apiKey.trim()}`;
69
+ }
70
+
43
71
  const controller = new AbortController();
44
72
  const timeout = setTimeout(() => controller.abort(), timeoutMs);
45
73
  timeout.unref?.();
@@ -47,12 +75,7 @@ export async function fetchGatewayModels({
47
75
  try {
48
76
  const response = await fetchImpl(endpoint, {
49
77
  method: 'GET',
50
- headers: {
51
- Accept: 'application/json',
52
- Authorization: `Bearer ${apiKey.trim()}`,
53
- 'Cache-Control': 'no-cache, no-store',
54
- Pragma: 'no-cache',
55
- },
78
+ headers,
56
79
  cache: 'no-store',
57
80
  signal: controller.signal,
58
81
  });
@@ -66,9 +89,7 @@ export async function fetchGatewayModels({
66
89
  throw new Error('Model discovery returned an invalid response');
67
90
  }
68
91
 
69
- const models = normalizeModelIds(payload.data.map((model) => model?.id));
70
- if (!models.length) throw new Error('Model discovery returned no usable model IDs');
71
- return models;
92
+ return normalizeModelIds(payload.data.map((model) => model?.id));
72
93
  } catch (error) {
73
94
  if (controller.signal.aborted) {
74
95
  throw new Error(`Model discovery timed out after ${timeoutMs}ms`);
@@ -79,6 +100,33 @@ export async function fetchGatewayModels({
79
100
  }
80
101
  }
81
102
 
103
+ async function fetchPublicModels({ baseUrl, fetchImpl, timeoutMs }) {
104
+ const models = await fetchGatewayModels({
105
+ baseUrl,
106
+ path: PUBLIC_MODELS_PATH,
107
+ fetchImpl,
108
+ timeoutMs,
109
+ });
110
+ if (!models.length) throw new Error('Model discovery returned no usable model IDs');
111
+ return models;
112
+ }
113
+
114
+ function packagedCatalog(selectedModel, fallbackModels, error) {
115
+ return {
116
+ models: normalizeModelIds([selectedModel, ...fallbackModels], selectedModel),
117
+ source: 'packaged',
118
+ error,
119
+ };
120
+ }
121
+
122
+ function liveCatalog(models, selectedModel, source) {
123
+ return {
124
+ models: normalizeModelIds(models, selectedModel),
125
+ source,
126
+ error: null,
127
+ };
128
+ }
129
+
82
130
  export async function resolveModelCatalog({
83
131
  baseUrl,
84
132
  apiKey,
@@ -87,18 +135,32 @@ export async function resolveModelCatalog({
87
135
  fetchImpl = globalThis.fetch,
88
136
  timeoutMs = DEFAULT_MODEL_FETCH_TIMEOUT_MS,
89
137
  }) {
138
+ const key = apiKey?.trim() || '';
139
+
140
+ if (key) {
141
+ try {
142
+ const discovered = await fetchGatewayModels({
143
+ baseUrl,
144
+ apiKey: key,
145
+ path: AVAILABLE_MODELS_PATH,
146
+ fetchImpl,
147
+ timeoutMs,
148
+ });
149
+ return liveCatalog(discovered, selectedModel, 'available');
150
+ } catch (availableError) {
151
+ try {
152
+ const discovered = await fetchPublicModels({ baseUrl, fetchImpl, timeoutMs });
153
+ return liveCatalog(discovered, selectedModel, 'public');
154
+ } catch {
155
+ return packagedCatalog(selectedModel, fallbackModels, availableError);
156
+ }
157
+ }
158
+ }
159
+
90
160
  try {
91
- const discovered = await fetchGatewayModels({ baseUrl, apiKey, fetchImpl, timeoutMs });
92
- return {
93
- models: normalizeModelIds(discovered, selectedModel),
94
- source: 'gateway',
95
- error: null,
96
- };
161
+ const discovered = await fetchPublicModels({ baseUrl, fetchImpl, timeoutMs });
162
+ return liveCatalog(discovered, selectedModel, 'public');
97
163
  } catch (error) {
98
- return {
99
- models: normalizeModelIds([selectedModel, ...fallbackModels], selectedModel),
100
- source: 'packaged',
101
- error,
102
- };
164
+ return packagedCatalog(selectedModel, fallbackModels, error);
103
165
  }
104
166
  }
Binary file
Binary file
Binary file
Binary file
package/bin/profiles.js CHANGED
@@ -12,6 +12,7 @@ import os from 'node:os';
12
12
  import path from 'node:path';
13
13
  import { readFileSync } from 'node:fs';
14
14
  import { c } from './colors.js';
15
+ import { PUBLIC_CATALOG_FALLBACK_MESSAGE } from './models.js';
15
16
 
16
17
  const registry = JSON.parse(
17
18
  readFileSync(new URL('./registry.generated.json', import.meta.url), 'utf-8'),
@@ -818,7 +819,9 @@ export function modelsCommand(models = SUPPORTED_MODELS, options = {}) {
818
819
  const suffix = model === selectedModel ? ` ${c.dim}(default)${c.reset}` : '';
819
820
  console.log(` ${c.cyan}${model}${c.reset}${suffix}`);
820
821
  }
821
- if (options.error) {
822
+ if (options.source === 'public' && options.hasApiKey) {
823
+ console.error(`\n ${c.dim}${PUBLIC_CATALOG_FALLBACK_MESSAGE}${c.reset}`);
824
+ } else if (options.error) {
822
825
  console.error(
823
826
  `\n ${c.yellow}Could not fetch the live model catalog; showing packaged defaults.${c.reset}`,
824
827
  );
package/bin/tui.js CHANGED
@@ -140,6 +140,7 @@ export async function createTuiState(profileName = DEFAULT_PROFILE, options = {}
140
140
  RUNBOOK_DEFAULTS.GATEWAY_URL,
141
141
  gatewayOverridden: Boolean(process.env.SUBCONSCIOUS_BASE_URL?.trim()),
142
142
  modelError: catalog.error?.message || '',
143
+ modelSource: catalog.source,
143
144
  agents: agentList().map((agent) => ({
144
145
  command: agent.alias,
145
146
  name: agent.name,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "subconscious-cli",
3
- "version": "4.0.10",
3
+ "version": "4.0.12",
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"
@@ -10,7 +10,8 @@
10
10
  "build:tui:host": "node scripts/build-tui.js --host",
11
11
  "test": "node --test",
12
12
  "test:tui": "go -C tui test ./...",
13
- "prepack": "npm run build:tui"
13
+ "prepack": "npm run build:tui",
14
+ "prepublishOnly": "node scripts/require-publish-script.js"
14
15
  },
15
16
  "files": [
16
17
  "bin"