subconscious-cli 4.0.0 → 4.0.1
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 +24 -9
- package/bin/agents.js +104 -13
- package/bin/cli.js +25 -1
- package/bin/colors.js +1 -0
- package/bin/models.js +104 -0
- package/bin/profiles.js +28 -5
- package/bin/runbook/codex/run.sh +2 -2
- package/bin/runbook/copilot/install.sh +8 -1
- package/bin/update-check.js +237 -0
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -10,6 +10,16 @@ subc login
|
|
|
10
10
|
subc claude
|
|
11
11
|
```
|
|
12
12
|
|
|
13
|
+
Every interactive `subc` command checks npm for a newer CLI release. When an
|
|
14
|
+
update is available, a notice shows the installed and latest versions and lets
|
|
15
|
+
you select **Update now** or **Skip for now** with the arrow keys and Enter.
|
|
16
|
+
Each option describes what it will do, and the active option is highlighted.
|
|
17
|
+
Update runs `npm install -g subconscious-cli@latest`; Skip continues the
|
|
18
|
+
requested command.
|
|
19
|
+
Non-interactive commands automatically skip, and registry errors and timeouts
|
|
20
|
+
never block the requested command. Set `SUBC_DISABLE_UPDATE_CHECK=1` to suppress
|
|
21
|
+
the check in offline automation.
|
|
22
|
+
|
|
13
23
|
Login creates both the saved credential and a ready-to-use `default` profile,
|
|
14
24
|
so Claude Code, Codex, and OpenCode can launch immediately. Persistent editor
|
|
15
25
|
and Pi integrations are installed per agent:
|
|
@@ -156,7 +166,9 @@ values. A command-line `--model` override has the highest model precedence.
|
|
|
156
166
|
|
|
157
167
|
## Models and endpoint overrides
|
|
158
168
|
|
|
159
|
-
List the available models with `subc models
|
|
169
|
+
List the available models with `subc models`. The command fetches the active
|
|
170
|
+
gateway's authenticated `/v1/models` catalog and falls back to the models
|
|
171
|
+
packaged with the CLI when discovery is unavailable:
|
|
160
172
|
|
|
161
173
|
```text
|
|
162
174
|
subconscious/glm-5.2 (default)
|
|
@@ -173,14 +185,17 @@ subc config --model subconscious/deepseek-v4-flash-marathon
|
|
|
173
185
|
export SUBCONSCIOUS_MODEL=subconscious/glm-5.2
|
|
174
186
|
```
|
|
175
187
|
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
188
|
+
Every launch and install fetches the same live catalog without caching. Codex,
|
|
189
|
+
OpenCode, Pi, Copilot, and Cursor receive the complete model list. The selected
|
|
190
|
+
profile model is listed first only while the gateway still advertises it.
|
|
191
|
+
If a saved profile default has been removed, launches use the first live model;
|
|
192
|
+
an explicit `--model` or `SUBCONSCIOUS_MODEL` override is always preserved.
|
|
193
|
+
Claude Code exposes four native Opus/Sonnet/Haiku/Fable picker slots plus one
|
|
194
|
+
custom model option, so the CLI maps the first five models into those slots; any other
|
|
195
|
+
model can still be selected with `subc claude --model MODEL`. Cursor requires adding
|
|
196
|
+
the printed model IDs in its OpenAI API Key Override settings. Use the printed
|
|
197
|
+
`/v1` Base URL in Cursor Settings; the profile itself stores the gateway origin
|
|
198
|
+
so correlation hooks can post to `/v1/agent-hooks`.
|
|
184
199
|
|
|
185
200
|
The default gateway is `https://api.subconscious.dev`. Profiles containing
|
|
186
201
|
the former exact default (`https://api.subconscious.dev`) migrate automatically;
|
package/bin/agents.js
CHANGED
|
@@ -21,13 +21,14 @@ 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
25
|
|
|
25
26
|
// --- Registry (single source of truth, generated copy shipped in the package).
|
|
26
27
|
const registry = JSON.parse(
|
|
27
28
|
readFileSync(new URL('./registry.generated.json', import.meta.url), 'utf-8'),
|
|
28
29
|
);
|
|
29
30
|
const DEFAULTS = registry.defaults;
|
|
30
|
-
const
|
|
31
|
+
const PACKAGED_MODELS =
|
|
31
32
|
Array.isArray(DEFAULTS.models) && DEFAULTS.models.length
|
|
32
33
|
? DEFAULTS.models
|
|
33
34
|
: [DEFAULTS.model];
|
|
@@ -312,8 +313,10 @@ function buildContext(apiKey, model, profile) {
|
|
|
312
313
|
* Falls back to SUBCONSCIOUS_MODEL, then the registry default.
|
|
313
314
|
*/
|
|
314
315
|
function extractModel(argv, profile) {
|
|
315
|
-
|
|
316
|
-
|
|
316
|
+
const environmentModel = process.env.SUBCONSCIOUS_MODEL?.trim();
|
|
317
|
+
const profileModel = profile?.values?.MODEL?.trim();
|
|
318
|
+
let model = environmentModel || profileModel || DEFAULTS.model;
|
|
319
|
+
let modelSource = environmentModel ? 'environment' : profileModel ? 'profile' : 'default';
|
|
317
320
|
const rest = [];
|
|
318
321
|
for (let i = 0; i < argv.length; i++) {
|
|
319
322
|
const a = argv[i];
|
|
@@ -321,17 +324,19 @@ function extractModel(argv, profile) {
|
|
|
321
324
|
const v = argv[i + 1];
|
|
322
325
|
if (v && !v.startsWith('-')) {
|
|
323
326
|
model = v;
|
|
327
|
+
modelSource = 'command';
|
|
324
328
|
i++;
|
|
325
329
|
}
|
|
326
330
|
continue;
|
|
327
331
|
}
|
|
328
332
|
if (a.startsWith('--model=')) {
|
|
329
333
|
model = a.slice('--model='.length);
|
|
334
|
+
modelSource = 'command';
|
|
330
335
|
continue;
|
|
331
336
|
}
|
|
332
337
|
rest.push(a);
|
|
333
338
|
}
|
|
334
|
-
return { model, rest };
|
|
339
|
+
return { model, modelSource, rest };
|
|
335
340
|
}
|
|
336
341
|
|
|
337
342
|
/**
|
|
@@ -611,34 +616,102 @@ const CLAUDE_MODEL_PICKER_KEYS = [
|
|
|
611
616
|
'ANTHROPIC_DEFAULT_HAIKU_MODEL',
|
|
612
617
|
'ANTHROPIC_DEFAULT_HAIKU_MODEL_NAME',
|
|
613
618
|
'ANTHROPIC_DEFAULT_HAIKU_MODEL_DESCRIPTION',
|
|
619
|
+
'ANTHROPIC_DEFAULT_FABLE_MODEL',
|
|
620
|
+
'ANTHROPIC_DEFAULT_FABLE_MODEL_NAME',
|
|
621
|
+
'ANTHROPIC_DEFAULT_FABLE_MODEL_DESCRIPTION',
|
|
622
|
+
'ANTHROPIC_CUSTOM_MODEL_OPTION',
|
|
623
|
+
'ANTHROPIC_CUSTOM_MODEL_OPTION_NAME',
|
|
624
|
+
'ANTHROPIC_CUSTOM_MODEL_OPTION_DESCRIPTION',
|
|
614
625
|
];
|
|
615
626
|
|
|
616
|
-
function claudeModelPickerEnv(agent, ctx) {
|
|
627
|
+
function claudeModelPickerEnv(agent, ctx, models) {
|
|
617
628
|
if (agent.id !== 'claude-code') return {};
|
|
618
629
|
const configured = substitute(agent.env || {}, ctx);
|
|
630
|
+
const configuredModels = [
|
|
631
|
+
configured.ANTHROPIC_DEFAULT_OPUS_MODEL,
|
|
632
|
+
configured.ANTHROPIC_DEFAULT_SONNET_MODEL,
|
|
633
|
+
configured.ANTHROPIC_DEFAULT_HAIKU_MODEL,
|
|
634
|
+
configured.ANTHROPIC_DEFAULT_FABLE_MODEL,
|
|
635
|
+
configured.ANTHROPIC_CUSTOM_MODEL_OPTION,
|
|
636
|
+
];
|
|
637
|
+
const pickerModels = [];
|
|
638
|
+
for (const model of [...models, ...configuredModels]) {
|
|
639
|
+
if (model && !pickerModels.includes(model)) pickerModels.push(model);
|
|
640
|
+
}
|
|
641
|
+
while (pickerModels.length < 3) pickerModels.push(pickerModels.at(-1) || ctx.model);
|
|
642
|
+
|
|
643
|
+
const roles = ['OPUS', 'SONNET', 'HAIKU', 'FABLE'];
|
|
644
|
+
const env = {};
|
|
645
|
+
for (let index = 0; index < roles.length; index++) {
|
|
646
|
+
const role = roles[index];
|
|
647
|
+
const model = pickerModels[index];
|
|
648
|
+
if (!model) continue;
|
|
649
|
+
env[`ANTHROPIC_DEFAULT_${role}_MODEL`] = model;
|
|
650
|
+
env[`ANTHROPIC_DEFAULT_${role}_MODEL_NAME`] = model;
|
|
651
|
+
env[`ANTHROPIC_DEFAULT_${role}_MODEL_DESCRIPTION`] = `Subconscious model ${model}`;
|
|
652
|
+
}
|
|
653
|
+
const customModel = pickerModels[4];
|
|
654
|
+
if (customModel) {
|
|
655
|
+
env.ANTHROPIC_CUSTOM_MODEL_OPTION = customModel;
|
|
656
|
+
env.ANTHROPIC_CUSTOM_MODEL_OPTION_NAME = customModel;
|
|
657
|
+
env.ANTHROPIC_CUSTOM_MODEL_OPTION_DESCRIPTION = `Subconscious model ${customModel}`;
|
|
658
|
+
}
|
|
619
659
|
return Object.fromEntries(
|
|
620
|
-
CLAUDE_MODEL_PICKER_KEYS.map((key) => [key,
|
|
660
|
+
CLAUDE_MODEL_PICKER_KEYS.map((key) => [key, env[key]]).filter(([, value]) => value),
|
|
621
661
|
);
|
|
622
662
|
}
|
|
623
663
|
|
|
624
|
-
export function runbookEnv(
|
|
664
|
+
export function runbookEnv(
|
|
665
|
+
apiKey,
|
|
666
|
+
model,
|
|
667
|
+
binDir,
|
|
668
|
+
profile,
|
|
669
|
+
agent,
|
|
670
|
+
models = PACKAGED_MODELS,
|
|
671
|
+
) {
|
|
625
672
|
const ctx = buildContext(apiKey, model, profile);
|
|
626
673
|
const extraDirs = [binDir, ...candidateBinDirs()].filter(Boolean);
|
|
627
674
|
const specificApiKey = agentApiKeySetting(agent)?.key;
|
|
628
675
|
return {
|
|
629
|
-
...claudeModelPickerEnv(agent, ctx),
|
|
676
|
+
...claudeModelPickerEnv(agent, ctx, models),
|
|
630
677
|
...(profile?.values || {}),
|
|
631
678
|
...process.env,
|
|
632
679
|
GATEWAY_URL: ctx.baseUrl,
|
|
633
680
|
API_KEY: apiKey,
|
|
634
681
|
...(specificApiKey ? { [specificApiKey]: apiKey } : {}),
|
|
635
682
|
MODEL: model,
|
|
636
|
-
SUBCONSCIOUS_MODELS:
|
|
683
|
+
SUBCONSCIOUS_MODELS: models.join('\n'),
|
|
637
684
|
SUBC_ENV_FILE: os.devNull,
|
|
638
685
|
PATH: augmentPath(extraDirs),
|
|
639
686
|
};
|
|
640
687
|
}
|
|
641
688
|
|
|
689
|
+
async function resolvedModelsForLaunch(profile, apiKey, selectedModel) {
|
|
690
|
+
const ctx = buildContext(apiKey, selectedModel, profile);
|
|
691
|
+
const catalog = await resolveModelCatalog({
|
|
692
|
+
baseUrl: ctx.baseUrl,
|
|
693
|
+
apiKey,
|
|
694
|
+
selectedModel,
|
|
695
|
+
fallbackModels: PACKAGED_MODELS,
|
|
696
|
+
});
|
|
697
|
+
if (catalog.error) {
|
|
698
|
+
console.error(
|
|
699
|
+
` ${c.yellow}Could not fetch the live model catalog; using packaged defaults.${c.reset}`,
|
|
700
|
+
);
|
|
701
|
+
console.error(` ${c.dim}${catalog.error.message}${c.reset}\n`);
|
|
702
|
+
}
|
|
703
|
+
return catalog;
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
export function selectLaunchModel(requestedModel, modelSource, catalog) {
|
|
707
|
+
const useLiveDefault =
|
|
708
|
+
catalog.source === 'gateway' &&
|
|
709
|
+
catalog.models.length > 0 &&
|
|
710
|
+
!catalog.models.includes(requestedModel) &&
|
|
711
|
+
(modelSource === 'profile' || modelSource === 'default');
|
|
712
|
+
return useLiveDefault ? catalog.models[0] : requestedModel;
|
|
713
|
+
}
|
|
714
|
+
|
|
642
715
|
async function runRunbookSetup(agent, argv, profile, relativeScript = agent.runbook.script) {
|
|
643
716
|
if (isSetupWithoutAuth(argv)) {
|
|
644
717
|
return spawnRunbook(agent, argv, {
|
|
@@ -648,9 +721,16 @@ async function runRunbookSetup(agent, argv, profile, relativeScript = agent.runb
|
|
|
648
721
|
}, relativeScript);
|
|
649
722
|
}
|
|
650
723
|
|
|
651
|
-
const { model, rest } = extractModel(argv, profile);
|
|
724
|
+
const { model: requestedModel, modelSource, rest } = extractModel(argv, profile);
|
|
652
725
|
const apiKey = optionValue(rest, '--api-key') || (await requireApiKey(profile, agent));
|
|
653
726
|
if (!apiKey) return 1;
|
|
727
|
+
const catalog = await resolvedModelsForLaunch(profile, apiKey, requestedModel);
|
|
728
|
+
const model = selectLaunchModel(requestedModel, modelSource, catalog);
|
|
729
|
+
if (model !== requestedModel) {
|
|
730
|
+
console.error(
|
|
731
|
+
` ${c.yellow}Configured model ${requestedModel} is not in the live catalog; using ${model}.${c.reset}\n`,
|
|
732
|
+
);
|
|
733
|
+
}
|
|
654
734
|
const ctx = buildContext(apiKey, model, profile);
|
|
655
735
|
const authArgs = substitute(agent.runbook.authArgs || [], ctx);
|
|
656
736
|
|
|
@@ -660,7 +740,7 @@ async function runRunbookSetup(agent, argv, profile, relativeScript = agent.runb
|
|
|
660
740
|
const code = await spawnRunbook(
|
|
661
741
|
agent,
|
|
662
742
|
[...authArgs, ...rest],
|
|
663
|
-
runbookEnv(apiKey, model, undefined, profile, agent),
|
|
743
|
+
runbookEnv(apiKey, model, undefined, profile, agent, catalog.models),
|
|
664
744
|
relativeScript,
|
|
665
745
|
);
|
|
666
746
|
const installed = !['status', 'uninstall'].includes(rest[0]);
|
|
@@ -708,17 +788,28 @@ export async function runAgent(agent, argv, options = {}) {
|
|
|
708
788
|
return code;
|
|
709
789
|
}
|
|
710
790
|
|
|
711
|
-
const { model, rest } = extractModel(argv, profile);
|
|
791
|
+
const { model: requestedModel, modelSource, rest } = extractModel(argv, profile);
|
|
712
792
|
const apiKey = await requireApiKey(profile, agent);
|
|
713
793
|
if (!apiKey) return 1;
|
|
714
794
|
|
|
715
795
|
const binDir = await ensureInstalled(agent);
|
|
796
|
+
const catalog = await resolvedModelsForLaunch(profile, apiKey, requestedModel);
|
|
797
|
+
const model = selectLaunchModel(requestedModel, modelSource, catalog);
|
|
798
|
+
if (model !== requestedModel) {
|
|
799
|
+
console.error(
|
|
800
|
+
` ${c.yellow}Configured model ${requestedModel} is not in the live catalog; using ${model}.${c.reset}\n`,
|
|
801
|
+
);
|
|
802
|
+
}
|
|
716
803
|
|
|
717
804
|
if (agent.runbook?.mode === 'launch') {
|
|
718
805
|
console.log(
|
|
719
806
|
` ${c.dim}Launching ${c.reset}${c.bold}${agent.name}${c.reset} ${c.dim}on Subconscious ${c.reset}${c.dim}(${model})${c.reset}\n`,
|
|
720
807
|
);
|
|
721
|
-
return spawnRunbook(
|
|
808
|
+
return spawnRunbook(
|
|
809
|
+
agent,
|
|
810
|
+
rest,
|
|
811
|
+
runbookEnv(apiKey, model, binDir, profile, agent, catalog.models),
|
|
812
|
+
);
|
|
722
813
|
}
|
|
723
814
|
|
|
724
815
|
const ctx = buildContext(apiKey, model, profile);
|
package/bin/cli.js
CHANGED
|
@@ -15,6 +15,7 @@ import { renderBanner } from './branding.js';
|
|
|
15
15
|
import {
|
|
16
16
|
loginCommand,
|
|
17
17
|
logoutCommand,
|
|
18
|
+
getApiKey,
|
|
18
19
|
updateApiKeyCommand,
|
|
19
20
|
whoamiCommand,
|
|
20
21
|
} from './auth.js';
|
|
@@ -33,9 +34,13 @@ import {
|
|
|
33
34
|
loadProfile,
|
|
34
35
|
modelsCommand,
|
|
35
36
|
printConfigHelp,
|
|
37
|
+
RUNBOOK_DEFAULTS,
|
|
38
|
+
SUPPORTED_MODELS as PACKAGED_MODELS,
|
|
36
39
|
updateUrlCommand,
|
|
37
40
|
validateProfileName,
|
|
38
41
|
} from './profiles.js';
|
|
42
|
+
import { resolveModelCatalog } from './models.js';
|
|
43
|
+
import { showUpdateNotice } from './update-check.js';
|
|
39
44
|
|
|
40
45
|
function isHelpArg(arg) {
|
|
41
46
|
return arg === 'help' || arg === '-h' || arg === '--help';
|
|
@@ -220,6 +225,9 @@ function requireNamedProfile(profile) {
|
|
|
220
225
|
}
|
|
221
226
|
|
|
222
227
|
async function main() {
|
|
228
|
+
const update = await showUpdateNotice();
|
|
229
|
+
if (update?.action === 'updated' || update?.action === 'cancel') return;
|
|
230
|
+
|
|
223
231
|
const parsed = extractProfile(process.argv.slice(2));
|
|
224
232
|
const { args, profileName, profileExplicit } = parsed;
|
|
225
233
|
const command = args[0];
|
|
@@ -285,7 +293,23 @@ async function main() {
|
|
|
285
293
|
console.log(COMMAND_HELP.models);
|
|
286
294
|
return;
|
|
287
295
|
}
|
|
288
|
-
|
|
296
|
+
const profile = await loadProfile(profileName);
|
|
297
|
+
const auth = await getApiKey(profile);
|
|
298
|
+
const selectedModel =
|
|
299
|
+
process.env.SUBCONSCIOUS_MODEL?.trim() ||
|
|
300
|
+
profile.values.MODEL?.trim() ||
|
|
301
|
+
RUNBOOK_DEFAULTS.MODEL;
|
|
302
|
+
const baseUrl =
|
|
303
|
+
process.env.SUBCONSCIOUS_BASE_URL?.trim() ||
|
|
304
|
+
profile.values.GATEWAY_URL?.trim() ||
|
|
305
|
+
RUNBOOK_DEFAULTS.GATEWAY_URL;
|
|
306
|
+
const catalog = await resolveModelCatalog({
|
|
307
|
+
baseUrl,
|
|
308
|
+
apiKey: auth?.key,
|
|
309
|
+
selectedModel,
|
|
310
|
+
fallbackModels: PACKAGED_MODELS,
|
|
311
|
+
});
|
|
312
|
+
modelsCommand(catalog.models, { selectedModel, error: catalog.error });
|
|
289
313
|
return;
|
|
290
314
|
}
|
|
291
315
|
|
package/bin/colors.js
CHANGED
package/bin/models.js
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolve the model catalog exposed by an OpenAI-compatible gateway.
|
|
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.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const MODEL_ID_PATTERN = /^[-A-Za-z0-9._:/+]+$/;
|
|
9
|
+
export const DEFAULT_MODEL_FETCH_TIMEOUT_MS = 3000;
|
|
10
|
+
|
|
11
|
+
export function normalizeModelIds(modelIds = [], selectedModel) {
|
|
12
|
+
const models = [];
|
|
13
|
+
const seen = new Set();
|
|
14
|
+
|
|
15
|
+
for (const value of modelIds) {
|
|
16
|
+
const model = typeof value === 'string' ? value.trim() : '';
|
|
17
|
+
if (!model || !MODEL_ID_PATTERN.test(model) || seen.has(model)) continue;
|
|
18
|
+
seen.add(model);
|
|
19
|
+
models.push(model);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const selectedIndex = models.indexOf(selectedModel?.trim());
|
|
23
|
+
if (selectedIndex > 0) {
|
|
24
|
+
models.unshift(models.splice(selectedIndex, 1)[0]);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return models;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export async function fetchGatewayModels({
|
|
31
|
+
baseUrl,
|
|
32
|
+
apiKey,
|
|
33
|
+
fetchImpl = globalThis.fetch,
|
|
34
|
+
timeoutMs = DEFAULT_MODEL_FETCH_TIMEOUT_MS,
|
|
35
|
+
}) {
|
|
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
|
+
if (typeof fetchImpl !== 'function') {
|
|
39
|
+
throw new Error('This Node.js version does not support fetch');
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const endpoint = `${baseUrl.trim().replace(/\/+$/, '')}/v1/models`;
|
|
43
|
+
const controller = new AbortController();
|
|
44
|
+
const timeout = setTimeout(() => controller.abort(), timeoutMs);
|
|
45
|
+
timeout.unref?.();
|
|
46
|
+
|
|
47
|
+
try {
|
|
48
|
+
const response = await fetchImpl(endpoint, {
|
|
49
|
+
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
|
+
},
|
|
56
|
+
cache: 'no-store',
|
|
57
|
+
signal: controller.signal,
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
if (!response.ok) {
|
|
61
|
+
throw new Error(`Model discovery returned HTTP ${response.status}`);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const payload = await response.json();
|
|
65
|
+
if (!Array.isArray(payload?.data)) {
|
|
66
|
+
throw new Error('Model discovery returned an invalid response');
|
|
67
|
+
}
|
|
68
|
+
|
|
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;
|
|
72
|
+
} catch (error) {
|
|
73
|
+
if (controller.signal.aborted) {
|
|
74
|
+
throw new Error(`Model discovery timed out after ${timeoutMs}ms`);
|
|
75
|
+
}
|
|
76
|
+
throw error;
|
|
77
|
+
} finally {
|
|
78
|
+
clearTimeout(timeout);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export async function resolveModelCatalog({
|
|
83
|
+
baseUrl,
|
|
84
|
+
apiKey,
|
|
85
|
+
selectedModel,
|
|
86
|
+
fallbackModels = [],
|
|
87
|
+
fetchImpl = globalThis.fetch,
|
|
88
|
+
timeoutMs = DEFAULT_MODEL_FETCH_TIMEOUT_MS,
|
|
89
|
+
}) {
|
|
90
|
+
try {
|
|
91
|
+
const discovered = await fetchGatewayModels({ baseUrl, apiKey, fetchImpl, timeoutMs });
|
|
92
|
+
return {
|
|
93
|
+
models: normalizeModelIds(discovered, selectedModel),
|
|
94
|
+
source: 'gateway',
|
|
95
|
+
error: null,
|
|
96
|
+
};
|
|
97
|
+
} catch (error) {
|
|
98
|
+
return {
|
|
99
|
+
models: normalizeModelIds([selectedModel, ...fallbackModels], selectedModel),
|
|
100
|
+
source: 'packaged',
|
|
101
|
+
error,
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
}
|
package/bin/profiles.js
CHANGED
|
@@ -415,6 +415,22 @@ function migrateDefaultGateway(text) {
|
|
|
415
415
|
: text;
|
|
416
416
|
}
|
|
417
417
|
|
|
418
|
+
function migrateCopilotTokenDefaults(text) {
|
|
419
|
+
const values = parseProfile(text);
|
|
420
|
+
const updates = {};
|
|
421
|
+
if (values.COPILOT_MAX_INPUT_TOKENS === '12288') {
|
|
422
|
+
updates.COPILOT_MAX_INPUT_TOKENS = RUNBOOK_DEFAULTS.COPILOT_MAX_INPUT_TOKENS;
|
|
423
|
+
}
|
|
424
|
+
if (values.COPILOT_MAX_OUTPUT_TOKENS === '4096') {
|
|
425
|
+
updates.COPILOT_MAX_OUTPUT_TOKENS = RUNBOOK_DEFAULTS.COPILOT_MAX_OUTPUT_TOKENS;
|
|
426
|
+
}
|
|
427
|
+
return Object.keys(updates).length ? upsertValues(text, updates) : text;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
function migrateDefaults(text) {
|
|
431
|
+
return migrateCopilotTokenDefaults(migrateDefaultGateway(text));
|
|
432
|
+
}
|
|
433
|
+
|
|
418
434
|
async function writeProfile(file, text) {
|
|
419
435
|
await fs.mkdir(PROFILES_DIR, { recursive: true });
|
|
420
436
|
await fs.writeFile(file, text, { encoding: 'utf-8', mode: 0o600 });
|
|
@@ -425,7 +441,7 @@ export async function loadProfile(name = DEFAULT_PROFILE) {
|
|
|
425
441
|
const file = profilePath(name);
|
|
426
442
|
try {
|
|
427
443
|
const text = await fs.readFile(file, 'utf-8');
|
|
428
|
-
const migratedText =
|
|
444
|
+
const migratedText = migrateDefaults(text);
|
|
429
445
|
if (migratedText !== text) await writeProfile(file, migratedText);
|
|
430
446
|
return { name, path: file, exists: true, values: parseProfile(migratedText) };
|
|
431
447
|
} catch (error) {
|
|
@@ -435,7 +451,7 @@ export async function loadProfile(name = DEFAULT_PROFILE) {
|
|
|
435
451
|
// Migrate an existing profile on first use, leaving the legacy copy intact.
|
|
436
452
|
if (LEGACY_PROFILES_DIR) {
|
|
437
453
|
try {
|
|
438
|
-
const text =
|
|
454
|
+
const text = migrateDefaults(
|
|
439
455
|
await fs.readFile(path.join(LEGACY_PROFILES_DIR, `${name}.env`), 'utf-8'),
|
|
440
456
|
);
|
|
441
457
|
await writeProfile(file, text);
|
|
@@ -729,12 +745,19 @@ export async function configCommand(argv, profileName = DEFAULT_PROFILE, options
|
|
|
729
745
|
await printProfile(await loadProfile(profileName));
|
|
730
746
|
}
|
|
731
747
|
|
|
732
|
-
export function modelsCommand() {
|
|
748
|
+
export function modelsCommand(models = SUPPORTED_MODELS, options = {}) {
|
|
749
|
+
const selectedModel = options.selectedModel || registry.defaults.model;
|
|
733
750
|
console.log(`\n ${c.bold}Available models${c.reset}\n`);
|
|
734
|
-
for (const model of
|
|
735
|
-
const suffix = model ===
|
|
751
|
+
for (const model of models) {
|
|
752
|
+
const suffix = model === selectedModel ? ` ${c.dim}(default)${c.reset}` : '';
|
|
736
753
|
console.log(` ${c.cyan}${model}${c.reset}${suffix}`);
|
|
737
754
|
}
|
|
755
|
+
if (options.error) {
|
|
756
|
+
console.error(
|
|
757
|
+
`\n ${c.yellow}Could not fetch the live model catalog; showing packaged defaults.${c.reset}`,
|
|
758
|
+
);
|
|
759
|
+
console.error(` ${c.dim}${options.error.message}${c.reset}`);
|
|
760
|
+
}
|
|
738
761
|
console.log();
|
|
739
762
|
}
|
|
740
763
|
|
package/bin/runbook/codex/run.sh
CHANGED
|
@@ -123,8 +123,8 @@ add_supported_model() {
|
|
|
123
123
|
}
|
|
124
124
|
|
|
125
125
|
# Keep the requested model first while exposing the complete Subconscious
|
|
126
|
-
# catalog in Codex's /model picker. The CLI supplies
|
|
127
|
-
# the fallback keeps the vendored runbook useful on its own.
|
|
126
|
+
# catalog in Codex's /model picker. The CLI supplies the live gateway catalog;
|
|
127
|
+
# the packaged fallback keeps the vendored runbook useful on its own.
|
|
128
128
|
add_supported_model "$MODEL"
|
|
129
129
|
while IFS= read -r model_id; do
|
|
130
130
|
add_supported_model "$model_id"
|
|
@@ -40,6 +40,7 @@
|
|
|
40
40
|
# "vision": false,
|
|
41
41
|
# "maxInputTokens": 5000000,
|
|
42
42
|
# "maxOutputTokens": 65536,
|
|
43
|
+
# "thinking": true,
|
|
43
44
|
# "streaming": true,
|
|
44
45
|
# "requestHeaders": { "x-subconscious-client": "copilot" }
|
|
45
46
|
# }
|
|
@@ -279,7 +280,12 @@ write_config() {
|
|
|
279
280
|
local user_dir="$1"
|
|
280
281
|
local models_json="${user_dir}/chatLanguageModels.json"
|
|
281
282
|
local base_url="${GATEWAY_URL%/}"
|
|
282
|
-
local chat_url
|
|
283
|
+
local chat_url
|
|
284
|
+
case "$base_url" in
|
|
285
|
+
*/v1/chat/completions) chat_url="$base_url" ;;
|
|
286
|
+
*/v1) chat_url="${base_url}/chat/completions" ;;
|
|
287
|
+
*) chat_url="${base_url}/v1/chat/completions" ;;
|
|
288
|
+
esac
|
|
283
289
|
|
|
284
290
|
mkdir -p "$user_dir"
|
|
285
291
|
local existing
|
|
@@ -302,6 +308,7 @@ write_config() {
|
|
|
302
308
|
vision: false,
|
|
303
309
|
maxInputTokens: $maxIn,
|
|
304
310
|
maxOutputTokens: $maxOut,
|
|
311
|
+
thinking: true,
|
|
305
312
|
streaming: true,
|
|
306
313
|
requestHeaders: { "x-subconscious-client": "copilot" }
|
|
307
314
|
}]')
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import { spawn } from 'node:child_process';
|
|
3
|
+
import readline from 'node:readline';
|
|
4
|
+
import { c } from './colors.js';
|
|
5
|
+
|
|
6
|
+
export const PACKAGE_NAME = 'subconscious-cli';
|
|
7
|
+
export const UPDATE_CHECK_TIMEOUT_MS = 1500;
|
|
8
|
+
const UPDATE_ACTIONS = ['Update now', 'Skip for now'];
|
|
9
|
+
|
|
10
|
+
function parseVersion(version) {
|
|
11
|
+
const match = String(version)
|
|
12
|
+
.trim()
|
|
13
|
+
.match(/^v?(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?(?:\+[0-9A-Za-z.-]+)?$/);
|
|
14
|
+
if (!match) return null;
|
|
15
|
+
return {
|
|
16
|
+
core: match.slice(1, 4).map(Number),
|
|
17
|
+
prerelease: match[4]?.split('.') || [],
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function comparePrerelease(a, b) {
|
|
22
|
+
if (!a.length && !b.length) return 0;
|
|
23
|
+
if (!a.length) return 1;
|
|
24
|
+
if (!b.length) return -1;
|
|
25
|
+
|
|
26
|
+
for (let index = 0; index < Math.max(a.length, b.length); index++) {
|
|
27
|
+
if (a[index] === undefined) return -1;
|
|
28
|
+
if (b[index] === undefined) return 1;
|
|
29
|
+
if (a[index] === b[index]) continue;
|
|
30
|
+
|
|
31
|
+
const aNumber = /^\d+$/.test(a[index]) ? Number(a[index]) : null;
|
|
32
|
+
const bNumber = /^\d+$/.test(b[index]) ? Number(b[index]) : null;
|
|
33
|
+
if (aNumber !== null && bNumber !== null) return Math.sign(aNumber - bNumber);
|
|
34
|
+
if (aNumber !== null) return -1;
|
|
35
|
+
if (bNumber !== null) return 1;
|
|
36
|
+
return a[index].localeCompare(b[index]);
|
|
37
|
+
}
|
|
38
|
+
return 0;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function compareVersions(a, b) {
|
|
42
|
+
const parsedA = parseVersion(a);
|
|
43
|
+
const parsedB = parseVersion(b);
|
|
44
|
+
if (!parsedA || !parsedB) return 0;
|
|
45
|
+
|
|
46
|
+
for (let index = 0; index < parsedA.core.length; index++) {
|
|
47
|
+
if (parsedA.core[index] !== parsedB.core[index]) {
|
|
48
|
+
return Math.sign(parsedA.core[index] - parsedB.core[index]);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return comparePrerelease(parsedA.prerelease, parsedB.prerelease);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export async function currentVersion() {
|
|
55
|
+
const pkg = JSON.parse(await fs.readFile(new URL('../package.json', import.meta.url), 'utf-8'));
|
|
56
|
+
return pkg.version;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export async function fetchLatestVersion(options = {}) {
|
|
60
|
+
const fetchImpl = options.fetchImpl || globalThis.fetch;
|
|
61
|
+
const timeoutMs = options.timeoutMs ?? UPDATE_CHECK_TIMEOUT_MS;
|
|
62
|
+
if (typeof fetchImpl !== 'function') throw new Error('fetch is unavailable');
|
|
63
|
+
|
|
64
|
+
const controller = new AbortController();
|
|
65
|
+
const timeout = setTimeout(() => controller.abort(), timeoutMs);
|
|
66
|
+
timeout.unref?.();
|
|
67
|
+
|
|
68
|
+
try {
|
|
69
|
+
const response = await fetchImpl(`https://registry.npmjs.org/${PACKAGE_NAME}/latest`, {
|
|
70
|
+
headers: {
|
|
71
|
+
Accept: 'application/json',
|
|
72
|
+
'Cache-Control': 'no-cache, no-store',
|
|
73
|
+
Pragma: 'no-cache',
|
|
74
|
+
},
|
|
75
|
+
cache: 'no-store',
|
|
76
|
+
signal: controller.signal,
|
|
77
|
+
});
|
|
78
|
+
if (!response.ok) throw new Error(`npm returned HTTP ${response.status}`);
|
|
79
|
+
const payload = await response.json();
|
|
80
|
+
if (!parseVersion(payload?.version)) throw new Error('npm returned an invalid version');
|
|
81
|
+
return payload.version;
|
|
82
|
+
} finally {
|
|
83
|
+
clearTimeout(timeout);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function renderUpdateNotice(installedVersion, latestVersion) {
|
|
88
|
+
const title = 'Subconscious CLI update available';
|
|
89
|
+
const versions = `${installedVersion} -> ${latestVersion}`;
|
|
90
|
+
const width = Math.max(title.length, versions.length);
|
|
91
|
+
const border = `+${'-'.repeat(width + 2)}+`;
|
|
92
|
+
const row = (text, style = '') =>
|
|
93
|
+
`${c.yellow}|${c.reset} ${style}${text.padEnd(width)}${c.reset} ${c.yellow}|${c.reset}`;
|
|
94
|
+
|
|
95
|
+
return [
|
|
96
|
+
` ${c.yellow}${border}${c.reset}`,
|
|
97
|
+
` ${row(title, c.bold)}`,
|
|
98
|
+
` ${row(versions, c.cyan)}`,
|
|
99
|
+
` ${c.yellow}${border}${c.reset}`,
|
|
100
|
+
].join('\n');
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export function renderUpdateOptions(selectedIndex = 0, versions = {}) {
|
|
104
|
+
const descriptions = [
|
|
105
|
+
`Runs \`npm install -g ${PACKAGE_NAME}@latest\``,
|
|
106
|
+
versions.installedVersion
|
|
107
|
+
? `Continue this command with version ${versions.installedVersion}`
|
|
108
|
+
: 'Continue this command without updating',
|
|
109
|
+
];
|
|
110
|
+
|
|
111
|
+
return UPDATE_ACTIONS.map((label, index) => {
|
|
112
|
+
const active = index === selectedIndex;
|
|
113
|
+
const pointer = active ? `${c.cyan}>${c.reset}` : ' ';
|
|
114
|
+
const option = active
|
|
115
|
+
? `${c.inverse}${c.bold} ${label} ${c.reset}`
|
|
116
|
+
: ` ${label} `;
|
|
117
|
+
const descriptionStyle = active ? c.cyan : c.dim;
|
|
118
|
+
return ` ${pointer} ${option}\n ${descriptionStyle}${descriptions[index]}${c.reset}`;
|
|
119
|
+
}).join('\n');
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export function moveUpdateSelection(selectedIndex, keyName) {
|
|
123
|
+
if (keyName === 'up') {
|
|
124
|
+
return (selectedIndex - 1 + UPDATE_ACTIONS.length) % UPDATE_ACTIONS.length;
|
|
125
|
+
}
|
|
126
|
+
if (keyName === 'down') return (selectedIndex + 1) % UPDATE_ACTIONS.length;
|
|
127
|
+
return selectedIndex;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export async function selectUpdateAction(options = {}) {
|
|
131
|
+
const input = options.input || process.stdin;
|
|
132
|
+
const output = options.output || process.stderr;
|
|
133
|
+
const initiallyRaw = input.isRaw === true;
|
|
134
|
+
let selectedIndex = options.selectedIndex ?? 0;
|
|
135
|
+
const instructions = ` ${c.dim}Use ↑/↓ to select • Enter to confirm${c.reset}`;
|
|
136
|
+
const render = () =>
|
|
137
|
+
`${renderUpdateOptions(selectedIndex, {
|
|
138
|
+
installedVersion: options.installedVersion,
|
|
139
|
+
latestVersion: options.latestVersion,
|
|
140
|
+
})}\n\n${instructions}`;
|
|
141
|
+
const renderedLineCount = render().split('\n').length;
|
|
142
|
+
|
|
143
|
+
readline.emitKeypressEvents(input);
|
|
144
|
+
input.setRawMode(true);
|
|
145
|
+
input.resume();
|
|
146
|
+
output.write(`\x1b[?25l${render()}`);
|
|
147
|
+
|
|
148
|
+
return new Promise((resolve) => {
|
|
149
|
+
const finish = (action) => {
|
|
150
|
+
input.off('keypress', onKeypress);
|
|
151
|
+
if (!initiallyRaw) input.setRawMode(false);
|
|
152
|
+
input.pause();
|
|
153
|
+
output.write('\n\x1b[?25h');
|
|
154
|
+
resolve(action);
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
const onKeypress = (_character, key = {}) => {
|
|
158
|
+
if (key.ctrl && key.name === 'c') {
|
|
159
|
+
process.exitCode = 130;
|
|
160
|
+
finish('cancel');
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
if (key.name === 'return' || key.name === 'enter') {
|
|
164
|
+
finish(selectedIndex === 0 ? 'update' : 'skip');
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const nextIndex = moveUpdateSelection(selectedIndex, key.name);
|
|
169
|
+
if (nextIndex === selectedIndex) return;
|
|
170
|
+
selectedIndex = nextIndex;
|
|
171
|
+
output.write(`\r\x1b[${renderedLineCount - 1}A\x1b[0J${render()}`);
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
input.on('keypress', onKeypress);
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
export async function installLatest(options = {}) {
|
|
179
|
+
const spawnImpl = options.spawnImpl || spawn;
|
|
180
|
+
return new Promise((resolve) => {
|
|
181
|
+
const child = spawnImpl('npm', ['install', '-g', `${PACKAGE_NAME}@latest`], {
|
|
182
|
+
stdio: 'inherit',
|
|
183
|
+
});
|
|
184
|
+
child.on('error', () => resolve(false));
|
|
185
|
+
child.on('exit', (code) => resolve(code === 0));
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
export async function showUpdateNotice(options = {}) {
|
|
190
|
+
const disabled =
|
|
191
|
+
options.disabled ?? process.env.SUBC_DISABLE_UPDATE_CHECK?.trim() === '1';
|
|
192
|
+
if (disabled) return null;
|
|
193
|
+
|
|
194
|
+
try {
|
|
195
|
+
const installedVersion = options.currentVersion || (await currentVersion());
|
|
196
|
+
const latestVersion =
|
|
197
|
+
options.latestVersion ||
|
|
198
|
+
(await fetchLatestVersion({
|
|
199
|
+
fetchImpl: options.fetchImpl,
|
|
200
|
+
timeoutMs: options.timeoutMs,
|
|
201
|
+
}));
|
|
202
|
+
if (compareVersions(latestVersion, installedVersion) <= 0) return null;
|
|
203
|
+
|
|
204
|
+
const notice = renderUpdateNotice(installedVersion, latestVersion);
|
|
205
|
+
const write = options.write || ((text) => process.stderr.write(text));
|
|
206
|
+
write(`\n${notice}\n\n`);
|
|
207
|
+
|
|
208
|
+
const interactive =
|
|
209
|
+
options.interactive ?? (process.stdin.isTTY === true && process.stderr.isTTY === true);
|
|
210
|
+
if (!interactive) return { installedVersion, latestVersion, action: 'skip' };
|
|
211
|
+
|
|
212
|
+
const select = options.select || selectUpdateAction;
|
|
213
|
+
const action = await select({ installedVersion, latestVersion });
|
|
214
|
+
if (action === 'cancel') return { installedVersion, latestVersion, action };
|
|
215
|
+
if (action === 'skip') {
|
|
216
|
+
write(`\n ${c.dim}Skipping update.${c.reset}\n\n`);
|
|
217
|
+
return { installedVersion, latestVersion, action };
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
write(`\n ${c.cyan}Updating ${PACKAGE_NAME} to ${latestVersion}...${c.reset}\n\n`);
|
|
221
|
+
const install = options.install || installLatest;
|
|
222
|
+
const installed = await install();
|
|
223
|
+
if (installed) {
|
|
224
|
+
write(
|
|
225
|
+
`\n ${c.green}${c.bold}Update complete.${c.reset} Re-run your subc command.\n\n`,
|
|
226
|
+
);
|
|
227
|
+
return { installedVersion, latestVersion, action: 'updated' };
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
write(`\n ${c.red}Update failed.${c.reset} Try it manually:\n\n`);
|
|
231
|
+
write(` ${c.cyan}npm install -g ${PACKAGE_NAME}@latest${c.reset}\n\n`);
|
|
232
|
+
return { installedVersion, latestVersion, action: 'failed' };
|
|
233
|
+
} catch {
|
|
234
|
+
// Update discovery must never prevent the requested command from running.
|
|
235
|
+
return null;
|
|
236
|
+
}
|
|
237
|
+
}
|
package/package.json
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "subconscious-cli",
|
|
3
|
-
"version": "4.0.
|
|
3
|
+
"version": "4.0.1",
|
|
4
4
|
"description": "CLI for Subconscious — run Claude Code, Codex, OpenCode, Cursor, Copilot, and Pi",
|
|
5
5
|
"bin": {
|
|
6
|
-
"subc": "
|
|
6
|
+
"subc": "bin/cli.js"
|
|
7
7
|
},
|
|
8
8
|
"scripts": {
|
|
9
9
|
"test": "node --test"
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
},
|
|
18
18
|
"repository": {
|
|
19
19
|
"type": "git",
|
|
20
|
-
"url": "https://github.com/subconscious-systems/subconscious.git",
|
|
20
|
+
"url": "git+https://github.com/subconscious-systems/subconscious.git",
|
|
21
21
|
"directory": "cli"
|
|
22
22
|
},
|
|
23
23
|
"keywords": [
|