subconscious-cli 6.0.0 → 6.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 +16 -2
- package/bin/agents.js +210 -70
- package/bin/auth.js +80 -32
- package/bin/branding.js +3 -1
- package/bin/cli.js +28 -20
- package/bin/clipboard-image.js +22 -5
- package/bin/colors.js +1 -1
- package/bin/feedback.js +57 -18
- package/bin/model-capabilities.js +4 -1
- package/bin/models.js +22 -6
- package/bin/native/subc-tui-darwin-amd64 +0 -0
- package/bin/native/subc-tui-darwin-arm64 +0 -0
- package/bin/native/subc-tui-linux-amd64 +0 -0
- package/bin/native/subc-tui-linux-arm64 +0 -0
- package/bin/native/subc-tui-windows-amd64.exe +0 -0
- package/bin/native/subc-tui-windows-arm64.exe +0 -0
- package/bin/opencode-provider.js +27 -5
- package/bin/profiles.js +126 -44
- package/bin/registry.generated.json +1 -1
- package/bin/runbook/opencode/opencode-plugin.d.ts +9 -0
- package/bin/runbook/opencode/subconscious-compaction.ts +26 -26
- package/bin/runbook/pi/subconscious-compaction.ts +69 -49
- package/bin/sessions.js +126 -41
- package/bin/tui.js +91 -32
- package/bin/update-check.js +49 -20
- package/bin/upgrade.js +49 -18
- package/bin/usage.js +26 -14
- package/bin/windows/agents.js +135 -32
- package/bin/windows/claude.js +43 -14
- package/bin/windows/common.js +56 -19
- package/bin/windows/install.js +120 -32
- package/bin/windows/launch.js +211 -49
- package/bin/windows/pi-compaction.ts +68 -13
- package/bin/windows/process.js +198 -56
- package/bin/windows/setup.js +231 -56
- package/package.json +9 -3
package/bin/opencode-provider.js
CHANGED
|
@@ -3,7 +3,17 @@ import { modelSupportsVision } from './model-capabilities.js';
|
|
|
3
3
|
export const OPENCODE_PROVIDER_ID = 'subconscious';
|
|
4
4
|
export const OPENCODE_PROVIDER_NAME = 'Subconscious Gateway';
|
|
5
5
|
|
|
6
|
-
const ACRONYMS = new Set([
|
|
6
|
+
const ACRONYMS = new Set([
|
|
7
|
+
'gpt',
|
|
8
|
+
'oss',
|
|
9
|
+
'api',
|
|
10
|
+
'gguf',
|
|
11
|
+
'ggml',
|
|
12
|
+
'nomic',
|
|
13
|
+
'vl',
|
|
14
|
+
'it',
|
|
15
|
+
'mlx',
|
|
16
|
+
]);
|
|
7
17
|
|
|
8
18
|
function formatToken(token) {
|
|
9
19
|
const lower = token.toLowerCase();
|
|
@@ -11,14 +21,17 @@ function formatToken(token) {
|
|
|
11
21
|
if (/^\d+[bkmg]$/i.test(token)) return token.toUpperCase();
|
|
12
22
|
if (/^q\d+$/i.test(token)) return token.toUpperCase();
|
|
13
23
|
if (/^\d+\.\d+/.test(token)) return token;
|
|
14
|
-
if (/^[a-z]\d+[a-z]$/i.test(token) || /^\d+[a-z]$/i.test(token))
|
|
24
|
+
if (/^[a-z]\d+[a-z]$/i.test(token) || /^\d+[a-z]$/i.test(token))
|
|
25
|
+
return token.toUpperCase();
|
|
15
26
|
return token.charAt(0).toUpperCase() + token.slice(1).toLowerCase();
|
|
16
27
|
}
|
|
17
28
|
|
|
18
29
|
/** Drop the subconscious/ prefix, then capitalize each dash-separated token. */
|
|
19
30
|
export function opencodeModelDisplayName(modelId) {
|
|
20
31
|
const text = String(modelId ?? '').trim();
|
|
21
|
-
const slug = text.startsWith('subconscious/')
|
|
32
|
+
const slug = text.startsWith('subconscious/')
|
|
33
|
+
? text.slice('subconscious/'.length)
|
|
34
|
+
: text;
|
|
22
35
|
return slug.split(/[-_]/).filter(Boolean).map(formatToken).join(' ');
|
|
23
36
|
}
|
|
24
37
|
|
|
@@ -31,14 +44,23 @@ export function buildOpenCodeModels(modelIds, { context, output }) {
|
|
|
31
44
|
tools: true,
|
|
32
45
|
limit: { context, output },
|
|
33
46
|
...(modelSupportsVision(id)
|
|
34
|
-
? {
|
|
47
|
+
? {
|
|
48
|
+
attachment: true,
|
|
49
|
+
modalities: { input: ['text', 'image'], output: ['text'] },
|
|
50
|
+
}
|
|
35
51
|
: {}),
|
|
36
52
|
},
|
|
37
53
|
]),
|
|
38
54
|
);
|
|
39
55
|
}
|
|
40
56
|
|
|
41
|
-
export function buildOpenCodeConfig({
|
|
57
|
+
export function buildOpenCodeConfig({
|
|
58
|
+
baseUrl,
|
|
59
|
+
model,
|
|
60
|
+
modelIds,
|
|
61
|
+
context,
|
|
62
|
+
output,
|
|
63
|
+
}) {
|
|
42
64
|
return {
|
|
43
65
|
$schema: 'https://opencode.ai/config.json',
|
|
44
66
|
disabled_providers: ['subconscious-cli'],
|
package/bin/profiles.js
CHANGED
|
@@ -6,14 +6,14 @@
|
|
|
6
6
|
* ~/.subconscious/profiles/<name>.env
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
9
|
+
import { execFileSync, spawn } from 'node:child_process';
|
|
10
|
+
import { readFileSync } from 'node:fs';
|
|
11
11
|
import fs from 'node:fs/promises';
|
|
12
12
|
import os from 'node:os';
|
|
13
13
|
import path from 'node:path';
|
|
14
|
-
import { readFileSync } from 'node:fs';
|
|
15
14
|
import { c } from './colors.js';
|
|
16
15
|
import { PUBLIC_CATALOG_FALLBACK_MESSAGE } from './models.js';
|
|
16
|
+
import { runWindows, windowsEditor } from './windows/process.js';
|
|
17
17
|
|
|
18
18
|
const registry = JSON.parse(
|
|
19
19
|
readFileSync(new URL('./registry.generated.json', import.meta.url), 'utf-8'),
|
|
@@ -106,7 +106,8 @@ const SETTINGS = {
|
|
|
106
106
|
CLAUDE_CODE_SUBAGENT_MODEL: {
|
|
107
107
|
key: 'CLAUDE_CODE_SUBAGENT_MODEL',
|
|
108
108
|
label: 'Subagent model',
|
|
109
|
-
description:
|
|
109
|
+
description:
|
|
110
|
+
'Model used by Claude Code subagents. UNSET follows the default model.',
|
|
110
111
|
type: 'choice',
|
|
111
112
|
choices: SUPPORTED_MODELS,
|
|
112
113
|
},
|
|
@@ -210,7 +211,8 @@ const SETTINGS = {
|
|
|
210
211
|
CODEX_EXTERNAL_TOOLS: {
|
|
211
212
|
key: 'CODEX_EXTERNAL_TOOLS',
|
|
212
213
|
label: 'External tools',
|
|
213
|
-
description:
|
|
214
|
+
description:
|
|
215
|
+
'Enable Codex apps/plugins; may exceed the gateway tool limit.',
|
|
214
216
|
type: 'choice',
|
|
215
217
|
choices: ['false', 'true'],
|
|
216
218
|
},
|
|
@@ -332,7 +334,11 @@ const AGENT_SETTING_KEYS = {
|
|
|
332
334
|
'CODEX_SUBAGENT_REASONING_EFFORT',
|
|
333
335
|
'MAX_CONCURRENT_SUBAGENTS',
|
|
334
336
|
],
|
|
335
|
-
opencode: [
|
|
337
|
+
opencode: [
|
|
338
|
+
'OPENCODE_API_KEY',
|
|
339
|
+
'OPENCODE_CONTEXT_LIMIT',
|
|
340
|
+
'OPENCODE_OUTPUT_LIMIT',
|
|
341
|
+
],
|
|
336
342
|
cursor: ['CURSOR_API_KEY'],
|
|
337
343
|
copilot: [
|
|
338
344
|
'COPILOT_API_KEY',
|
|
@@ -349,7 +355,11 @@ const AGENT_SETTING_KEYS = {
|
|
|
349
355
|
};
|
|
350
356
|
|
|
351
357
|
const GROUP_KEYS = [
|
|
352
|
-
{
|
|
358
|
+
{
|
|
359
|
+
id: 'shared',
|
|
360
|
+
label: 'Shared settings',
|
|
361
|
+
keys: ['GATEWAY_URL', 'API_KEY', 'MODEL'],
|
|
362
|
+
},
|
|
353
363
|
...registry.agents
|
|
354
364
|
.filter((agent) => agent.cli !== false)
|
|
355
365
|
.map((agent) => ({
|
|
@@ -375,7 +385,9 @@ export function resolvedProfileValues(profile) {
|
|
|
375
385
|
}
|
|
376
386
|
|
|
377
387
|
function profileTemplate() {
|
|
378
|
-
const modelComments = SUPPORTED_MODELS.map((model) => `# ${model}`).join(
|
|
388
|
+
const modelComments = SUPPORTED_MODELS.map((model) => `# ${model}`).join(
|
|
389
|
+
'\n',
|
|
390
|
+
);
|
|
379
391
|
return `# Subconscious coding-agent profile
|
|
380
392
|
# Generated by subc. Extra KEY=value lines are passed through to launches.
|
|
381
393
|
|
|
@@ -435,7 +447,9 @@ VSCODE_APP={VSCODE_APP}
|
|
|
435
447
|
|
|
436
448
|
export function validateProfileName(name) {
|
|
437
449
|
if (!/^[A-Za-z0-9][A-Za-z0-9_-]*$/.test(name)) {
|
|
438
|
-
throw new Error(
|
|
450
|
+
throw new Error(
|
|
451
|
+
`Invalid profile name '${name}' (use letters, digits, _ or -)`,
|
|
452
|
+
);
|
|
439
453
|
}
|
|
440
454
|
return name;
|
|
441
455
|
}
|
|
@@ -462,7 +476,9 @@ export function parseProfile(text) {
|
|
|
462
476
|
for (const line of text.split('\n')) {
|
|
463
477
|
const trimmed = line.trim();
|
|
464
478
|
if (!trimmed || trimmed.startsWith('#')) continue;
|
|
465
|
-
const match = trimmed.match(
|
|
479
|
+
const match = trimmed.match(
|
|
480
|
+
/^(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*)=(.*)$/,
|
|
481
|
+
);
|
|
466
482
|
if (!match) continue;
|
|
467
483
|
values[match[1]] = decodeValue(match[2]);
|
|
468
484
|
}
|
|
@@ -478,7 +494,9 @@ function encodeValue(value) {
|
|
|
478
494
|
}
|
|
479
495
|
|
|
480
496
|
function renderTemplate(values) {
|
|
481
|
-
return profileTemplate().replace(/\{([A-Z0-9_]+)\}/g, (_, key) =>
|
|
497
|
+
return profileTemplate().replace(/\{([A-Z0-9_]+)\}/g, (_, key) =>
|
|
498
|
+
encodeValue(values[key]),
|
|
499
|
+
);
|
|
482
500
|
}
|
|
483
501
|
|
|
484
502
|
function upsertValues(text, updates) {
|
|
@@ -491,7 +509,8 @@ function upsertValues(text, updates) {
|
|
|
491
509
|
remaining.delete(match[2]);
|
|
492
510
|
return `${match[1]}${match[2]}=${encodeValue(value)}`;
|
|
493
511
|
});
|
|
494
|
-
for (const [key, value] of remaining)
|
|
512
|
+
for (const [key, value] of remaining)
|
|
513
|
+
output.push(`${key}=${encodeValue(value)}`);
|
|
495
514
|
return `${output.join('\n')}\n`;
|
|
496
515
|
}
|
|
497
516
|
|
|
@@ -506,10 +525,12 @@ function migrateCopilotTokenDefaults(text) {
|
|
|
506
525
|
const values = parseProfile(text);
|
|
507
526
|
const updates = {};
|
|
508
527
|
if (values.COPILOT_MAX_INPUT_TOKENS === '12288') {
|
|
509
|
-
updates.COPILOT_MAX_INPUT_TOKENS =
|
|
528
|
+
updates.COPILOT_MAX_INPUT_TOKENS =
|
|
529
|
+
RUNBOOK_DEFAULTS.COPILOT_MAX_INPUT_TOKENS;
|
|
510
530
|
}
|
|
511
531
|
if (values.COPILOT_MAX_OUTPUT_TOKENS === '4096') {
|
|
512
|
-
updates.COPILOT_MAX_OUTPUT_TOKENS =
|
|
532
|
+
updates.COPILOT_MAX_OUTPUT_TOKENS =
|
|
533
|
+
RUNBOOK_DEFAULTS.COPILOT_MAX_OUTPUT_TOKENS;
|
|
513
534
|
}
|
|
514
535
|
return Object.keys(updates).length ? upsertValues(text, updates) : text;
|
|
515
536
|
}
|
|
@@ -530,7 +551,12 @@ export async function loadProfile(name = DEFAULT_PROFILE) {
|
|
|
530
551
|
const text = await fs.readFile(file, 'utf-8');
|
|
531
552
|
const migratedText = migrateDefaults(text);
|
|
532
553
|
if (migratedText !== text) await writeProfile(file, migratedText);
|
|
533
|
-
return {
|
|
554
|
+
return {
|
|
555
|
+
name,
|
|
556
|
+
path: file,
|
|
557
|
+
exists: true,
|
|
558
|
+
values: parseProfile(migratedText),
|
|
559
|
+
};
|
|
534
560
|
} catch (error) {
|
|
535
561
|
if (error.code !== 'ENOENT') throw error;
|
|
536
562
|
}
|
|
@@ -539,7 +565,10 @@ export async function loadProfile(name = DEFAULT_PROFILE) {
|
|
|
539
565
|
if (LEGACY_PROFILES_DIR) {
|
|
540
566
|
try {
|
|
541
567
|
const text = migrateDefaults(
|
|
542
|
-
await fs.readFile(
|
|
568
|
+
await fs.readFile(
|
|
569
|
+
path.join(LEGACY_PROFILES_DIR, `${name}.env`),
|
|
570
|
+
'utf-8',
|
|
571
|
+
),
|
|
543
572
|
);
|
|
544
573
|
await writeProfile(file, text);
|
|
545
574
|
return { name, path: file, exists: true, values: parseProfile(text) };
|
|
@@ -562,7 +591,8 @@ export async function ensureProfile(name = DEFAULT_PROFILE, apiKey) {
|
|
|
562
591
|
Object.fromEntries(
|
|
563
592
|
Object.entries(values).filter(
|
|
564
593
|
([key]) =>
|
|
565
|
-
profile.values[key] === undefined ||
|
|
594
|
+
profile.values[key] === undefined ||
|
|
595
|
+
(key === 'API_KEY' && apiKey !== undefined),
|
|
566
596
|
),
|
|
567
597
|
),
|
|
568
598
|
)
|
|
@@ -599,11 +629,15 @@ function isSecretKey(key) {
|
|
|
599
629
|
|
|
600
630
|
function redact(key, value) {
|
|
601
631
|
if (!isSecretKey(key) || !value) return value;
|
|
602
|
-
return value.length <= 8
|
|
632
|
+
return value.length <= 8
|
|
633
|
+
? '********'
|
|
634
|
+
: `${value.slice(0, 4)}…${value.slice(-4)}`;
|
|
603
635
|
}
|
|
604
636
|
|
|
605
637
|
function redactEnvLine(line) {
|
|
606
|
-
const match = line.match(
|
|
638
|
+
const match = line.match(
|
|
639
|
+
/^(\s*(?:export\s+)?)([A-Za-z_][A-Za-z0-9_]*)=(.*)$/,
|
|
640
|
+
);
|
|
607
641
|
if (!match) return line;
|
|
608
642
|
const [, prefix, key, raw] = match;
|
|
609
643
|
const value = decodeValue(raw);
|
|
@@ -633,18 +667,20 @@ async function printProfile(profile) {
|
|
|
633
667
|
}
|
|
634
668
|
const text = await fs.readFile(profile.path, 'utf-8');
|
|
635
669
|
console.log(
|
|
636
|
-
text
|
|
637
|
-
.replace(/\n$/, '')
|
|
638
|
-
.split('\n')
|
|
639
|
-
.map(redactEnvLine)
|
|
640
|
-
.join('\n'),
|
|
670
|
+
text.replace(/\n$/, '').split('\n').map(redactEnvLine).join('\n'),
|
|
641
671
|
);
|
|
642
672
|
console.log();
|
|
643
673
|
}
|
|
644
674
|
|
|
645
675
|
export function isUnsetSetting(value) {
|
|
646
|
-
const normalized = String(value ?? '')
|
|
647
|
-
|
|
676
|
+
const normalized = String(value ?? '')
|
|
677
|
+
.trim()
|
|
678
|
+
.toUpperCase();
|
|
679
|
+
return (
|
|
680
|
+
normalized === 'UNSET' ||
|
|
681
|
+
normalized === 'FOLLOW-GATEWAY' ||
|
|
682
|
+
normalized === 'FOLLOW-DEFAULT'
|
|
683
|
+
);
|
|
648
684
|
}
|
|
649
685
|
|
|
650
686
|
export function resolvedModelSetting(value) {
|
|
@@ -684,7 +720,11 @@ export function validateSettingValue(setting, value) {
|
|
|
684
720
|
return null;
|
|
685
721
|
}
|
|
686
722
|
|
|
687
|
-
const ALLOWED_EDITORS = new Set(
|
|
723
|
+
const ALLOWED_EDITORS = new Set(
|
|
724
|
+
process.platform === 'win32'
|
|
725
|
+
? ['notepad', 'code', 'code-insiders', 'vim', 'nano']
|
|
726
|
+
: ['vim', 'nano'],
|
|
727
|
+
);
|
|
688
728
|
|
|
689
729
|
function commandExists(bin) {
|
|
690
730
|
try {
|
|
@@ -709,7 +749,9 @@ function resolveEditor(requested) {
|
|
|
709
749
|
if (fromEnv) return parseEditorCommand(fromEnv);
|
|
710
750
|
if (commandExists('vim')) return { cmd: 'vim', args: [] };
|
|
711
751
|
if (commandExists('nano')) return { cmd: 'nano', args: [] };
|
|
712
|
-
throw new Error(
|
|
752
|
+
throw new Error(
|
|
753
|
+
'No editor found. Install vim or nano, or set $VISUAL / $EDITOR.',
|
|
754
|
+
);
|
|
713
755
|
}
|
|
714
756
|
|
|
715
757
|
async function editProfile(profileName, requestedEditor) {
|
|
@@ -776,12 +818,18 @@ async function printProfileList(activeName) {
|
|
|
776
818
|
console.log(`\n ${c.bold}Profiles${c.reset}`);
|
|
777
819
|
for (const name of profiles) {
|
|
778
820
|
const active = name === activeName ? ` ${c.green}(active)${c.reset}` : '';
|
|
779
|
-
console.log(
|
|
821
|
+
console.log(
|
|
822
|
+
` ${c.cyan}${name}${c.reset}${active} ${c.dim}${profilePath(name)}${c.reset}`,
|
|
823
|
+
);
|
|
780
824
|
}
|
|
781
825
|
console.log();
|
|
782
826
|
}
|
|
783
827
|
|
|
784
|
-
export async function configCommand(
|
|
828
|
+
export async function configCommand(
|
|
829
|
+
argv,
|
|
830
|
+
profileName = DEFAULT_PROFILE,
|
|
831
|
+
options = {},
|
|
832
|
+
) {
|
|
785
833
|
let action = 'default';
|
|
786
834
|
let editor;
|
|
787
835
|
const updates = {};
|
|
@@ -791,7 +839,17 @@ export async function configCommand(argv, profileName = DEFAULT_PROFILE, options
|
|
|
791
839
|
if (arg === 'help' || arg === '-h' || arg === '--help') {
|
|
792
840
|
printConfigHelp();
|
|
793
841
|
return;
|
|
794
|
-
} else if (
|
|
842
|
+
} else if (
|
|
843
|
+
[
|
|
844
|
+
'show',
|
|
845
|
+
'path',
|
|
846
|
+
'list',
|
|
847
|
+
'create',
|
|
848
|
+
'delete',
|
|
849
|
+
'edit',
|
|
850
|
+
'interactive',
|
|
851
|
+
].includes(arg)
|
|
852
|
+
) {
|
|
795
853
|
action = arg === 'interactive' ? 'edit' : arg;
|
|
796
854
|
} else if (
|
|
797
855
|
arg === '--gateway-url' ||
|
|
@@ -810,12 +868,17 @@ export async function configCommand(argv, profileName = DEFAULT_PROFILE, options
|
|
|
810
868
|
'--subagent-model': 'CLAUDE_CODE_SUBAGENT_MODEL',
|
|
811
869
|
}[arg];
|
|
812
870
|
updates[key] =
|
|
813
|
-
(arg === '--model' || arg === '--subagent-model') &&
|
|
871
|
+
(arg === '--model' || arg === '--subagent-model') &&
|
|
872
|
+
isUnsetSetting(value)
|
|
873
|
+
? ''
|
|
874
|
+
: value;
|
|
814
875
|
} else if (action === 'edit' && ALLOWED_EDITORS.has(arg)) {
|
|
815
|
-
if (editor) throw new Error('Specify only one editor
|
|
876
|
+
if (editor) throw new Error('Specify only one editor');
|
|
816
877
|
editor = arg;
|
|
817
878
|
} else if (action === 'edit') {
|
|
818
|
-
throw new Error(
|
|
879
|
+
throw new Error(
|
|
880
|
+
`Unknown editor '${arg}' (use ${[...ALLOWED_EDITORS].join(', ')})`,
|
|
881
|
+
);
|
|
819
882
|
} else {
|
|
820
883
|
throw new Error(`Unknown config argument: ${arg}`);
|
|
821
884
|
}
|
|
@@ -831,7 +894,12 @@ export async function configCommand(argv, profileName = DEFAULT_PROFILE, options
|
|
|
831
894
|
return;
|
|
832
895
|
}
|
|
833
896
|
|
|
834
|
-
if (
|
|
897
|
+
if (
|
|
898
|
+
action === 'list' ||
|
|
899
|
+
(action === 'default' &&
|
|
900
|
+
!options.profileExplicit &&
|
|
901
|
+
!Object.keys(updates).length)
|
|
902
|
+
) {
|
|
835
903
|
await printProfileList(profileName);
|
|
836
904
|
return;
|
|
837
905
|
}
|
|
@@ -846,7 +914,8 @@ export async function configCommand(argv, profileName = DEFAULT_PROFILE, options
|
|
|
846
914
|
throw new Error('config create cannot be combined with profile updates');
|
|
847
915
|
}
|
|
848
916
|
const existing = await loadProfile(profileName);
|
|
849
|
-
if (existing.exists)
|
|
917
|
+
if (existing.exists)
|
|
918
|
+
throw new Error(`Profile '${profileName}' already exists`);
|
|
850
919
|
const profile = await ensureProfile(profileName);
|
|
851
920
|
console.log(`Created profile '${profileName}'.`);
|
|
852
921
|
await printProfile(profile);
|
|
@@ -855,7 +924,9 @@ export async function configCommand(argv, profileName = DEFAULT_PROFILE, options
|
|
|
855
924
|
|
|
856
925
|
if (action === 'delete') {
|
|
857
926
|
if (profileName === DEFAULT_PROFILE) {
|
|
858
|
-
throw new Error(
|
|
927
|
+
throw new Error(
|
|
928
|
+
'Refusing to delete the default profile; use subc logout to clear its key',
|
|
929
|
+
);
|
|
859
930
|
}
|
|
860
931
|
const profile = await loadProfile(profileName);
|
|
861
932
|
if (!profile.exists) {
|
|
@@ -878,10 +949,12 @@ export async function configCommand(argv, profileName = DEFAULT_PROFILE, options
|
|
|
878
949
|
}
|
|
879
950
|
|
|
880
951
|
export function modelsCommand(models = SUPPORTED_MODELS, options = {}) {
|
|
881
|
-
const selectedModel =
|
|
952
|
+
const selectedModel =
|
|
953
|
+
options.selectedModel || models[0] || registry.defaults.model;
|
|
882
954
|
console.log(`\n ${c.bold}Available models${c.reset}\n`);
|
|
883
955
|
for (const model of models) {
|
|
884
|
-
const suffix =
|
|
956
|
+
const suffix =
|
|
957
|
+
model === selectedModel ? ` ${c.dim}(default)${c.reset}` : '';
|
|
885
958
|
console.log(` ${c.cyan}${model}${c.reset}${suffix}`);
|
|
886
959
|
}
|
|
887
960
|
if (options.source === 'public' && options.hasApiKey) {
|
|
@@ -896,7 +969,9 @@ export function modelsCommand(models = SUPPORTED_MODELS, options = {}) {
|
|
|
896
969
|
}
|
|
897
970
|
|
|
898
971
|
export function validateOriginUrl(rawUrl, label = 'URL') {
|
|
899
|
-
const normalized = String(rawUrl ?? '')
|
|
972
|
+
const normalized = String(rawUrl ?? '')
|
|
973
|
+
.trim()
|
|
974
|
+
.replace(/\/+$/, '');
|
|
900
975
|
if (!normalized) {
|
|
901
976
|
throw new Error(`${label} cannot be blank`);
|
|
902
977
|
}
|
|
@@ -929,7 +1004,9 @@ export async function updateUrlCommand(argv = [], options = {}) {
|
|
|
929
1004
|
const profile = await updateProfile(profileName, { GATEWAY_URL: gatewayUrl });
|
|
930
1005
|
|
|
931
1006
|
console.log(`\n ${c.green}${c.bold}✓ Gateway URL updated.${c.reset}`);
|
|
932
|
-
console.log(
|
|
1007
|
+
console.log(
|
|
1008
|
+
` ${c.dim}Updated profile automatically: ${profile.path}${c.reset}`,
|
|
1009
|
+
);
|
|
933
1010
|
console.log(` ${c.dim}URL: ${gatewayUrl}${c.reset}`);
|
|
934
1011
|
if (process.env.SUBCONSCIOUS_BASE_URL?.trim()) {
|
|
935
1012
|
console.log(
|
|
@@ -937,7 +1014,8 @@ export async function updateUrlCommand(argv = [], options = {}) {
|
|
|
937
1014
|
);
|
|
938
1015
|
}
|
|
939
1016
|
const claudeOverride =
|
|
940
|
-
process.env.CLAUDE_GATEWAY_URL?.trim() ||
|
|
1017
|
+
process.env.CLAUDE_GATEWAY_URL?.trim() ||
|
|
1018
|
+
profile.values.CLAUDE_GATEWAY_URL?.trim();
|
|
941
1019
|
if (claudeOverride) {
|
|
942
1020
|
console.log(
|
|
943
1021
|
` ${c.yellow}CLAUDE_GATEWAY_URL is set, so Claude Code will continue using ${claudeOverride}.${c.reset}`,
|
|
@@ -954,10 +1032,14 @@ export async function updatePlatformUrlCommand(argv = [], options = {}) {
|
|
|
954
1032
|
const platformUrl = validateOriginUrl(argv[0], 'Platform URL');
|
|
955
1033
|
|
|
956
1034
|
const profileName = options.profileName || DEFAULT_PROFILE;
|
|
957
|
-
const profile = await updateProfile(profileName, {
|
|
1035
|
+
const profile = await updateProfile(profileName, {
|
|
1036
|
+
PLATFORM_URL: platformUrl,
|
|
1037
|
+
});
|
|
958
1038
|
|
|
959
1039
|
console.log(`\n ${c.green}${c.bold}✓ Platform URL updated.${c.reset}`);
|
|
960
|
-
console.log(
|
|
1040
|
+
console.log(
|
|
1041
|
+
` ${c.dim}Updated profile automatically: ${profile.path}${c.reset}`,
|
|
1042
|
+
);
|
|
961
1043
|
console.log(` ${c.dim}URL: ${platformUrl}${c.reset}`);
|
|
962
1044
|
if (process.env.SUBCONSCIOUS_URL?.trim()) {
|
|
963
1045
|
console.log(
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
"baseUrl": "https://api.subconscious.dev",
|
|
13
13
|
"baseUrlV1": "https://api.subconscious.dev/v1",
|
|
14
14
|
"keyPlaceholder": "your_key",
|
|
15
|
-
"platformUrl": "https://subconscious.dev
|
|
15
|
+
"platformUrl": "https://platform.subconscious.dev"
|
|
16
16
|
},
|
|
17
17
|
"modelCapabilities": {
|
|
18
18
|
"subconscious/deepseek-v4.1-flash-marathon": {
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
// OpenCode loads this plugin. The host package is not a dependency of the CLI.
|
|
2
|
+
declare module '@opencode-ai/plugin' {
|
|
3
|
+
export type Plugin = () => Promise<{
|
|
4
|
+
'experimental.session.compacting': (input: {
|
|
5
|
+
sessionID: string;
|
|
6
|
+
}) => Promise<void>;
|
|
7
|
+
event: (args: { event: { type: string } }) => Promise<void>;
|
|
8
|
+
}>;
|
|
9
|
+
}
|
|
@@ -22,27 +22,27 @@
|
|
|
22
22
|
* `opencode/install.sh` does this for you.
|
|
23
23
|
*/
|
|
24
24
|
|
|
25
|
-
import type { Plugin } from
|
|
25
|
+
import type { Plugin } from '@opencode-ai/plugin';
|
|
26
26
|
|
|
27
|
-
const TIMEOUT_MS = 2000
|
|
27
|
+
const TIMEOUT_MS = 2000;
|
|
28
28
|
|
|
29
29
|
function gatewayUrl(): string | undefined {
|
|
30
|
-
const raw = process.env.SUBCONSCIOUS_GATEWAY_URL ?? process.env.GATEWAY_URL
|
|
31
|
-
return raw ? raw.replace(/\/+$/,
|
|
30
|
+
const raw = process.env.SUBCONSCIOUS_GATEWAY_URL ?? process.env.GATEWAY_URL;
|
|
31
|
+
return raw ? raw.replace(/\/+$/, '') : undefined;
|
|
32
32
|
}
|
|
33
33
|
|
|
34
34
|
function apiKey(): string | undefined {
|
|
35
|
-
return process.env.SUBCONSCIOUS_API_KEY ?? process.env.API_KEY
|
|
35
|
+
return process.env.SUBCONSCIOUS_API_KEY ?? process.env.API_KEY;
|
|
36
36
|
}
|
|
37
37
|
|
|
38
38
|
async function report(
|
|
39
39
|
sessionID: string,
|
|
40
|
-
phase:
|
|
40
|
+
phase: 'start' | 'end',
|
|
41
41
|
hookEventName: string,
|
|
42
42
|
): Promise<void> {
|
|
43
|
-
const url = gatewayUrl()
|
|
44
|
-
const key = apiKey()
|
|
45
|
-
if (!url || !key || !sessionID) return
|
|
43
|
+
const url = gatewayUrl();
|
|
44
|
+
const key = apiKey();
|
|
45
|
+
if (!url || !key || !sessionID) return;
|
|
46
46
|
|
|
47
47
|
// Idempotency key for this one event. It must be unique per compaction but stable for
|
|
48
48
|
// a redelivery of this same POST, so it is derived from the wall clock rather than any
|
|
@@ -50,51 +50,51 @@ async function report(
|
|
|
50
50
|
// compacting again would then reuse a key the gateway has already seen and silently
|
|
51
51
|
// drop the signal. Computed once here, before the request, so a transport-level retry
|
|
52
52
|
// carries the identical body.
|
|
53
|
-
const dedupeKey = `${sessionID}:${phase}:${Date.now()}
|
|
53
|
+
const dedupeKey = `${sessionID}:${phase}:${Date.now()}`;
|
|
54
54
|
|
|
55
|
-
const controller = new AbortController()
|
|
56
|
-
const timer = setTimeout(() => controller.abort(), TIMEOUT_MS)
|
|
55
|
+
const controller = new AbortController();
|
|
56
|
+
const timer = setTimeout(() => controller.abort(), TIMEOUT_MS);
|
|
57
57
|
try {
|
|
58
58
|
await fetch(`${url}/v1/agent-hooks`, {
|
|
59
|
-
method:
|
|
59
|
+
method: 'POST',
|
|
60
60
|
headers: {
|
|
61
|
-
|
|
61
|
+
'content-type': 'application/json',
|
|
62
62
|
authorization: `Bearer ${key}`,
|
|
63
|
-
|
|
63
|
+
'x-subconscious-client': 'opencode',
|
|
64
64
|
},
|
|
65
65
|
body: JSON.stringify({
|
|
66
|
-
event:
|
|
66
|
+
event: 'conversation_compaction',
|
|
67
67
|
conversation_id: sessionID,
|
|
68
68
|
phase,
|
|
69
69
|
hook_event_name: hookEventName,
|
|
70
70
|
dedupe_key: dedupeKey,
|
|
71
71
|
}),
|
|
72
72
|
signal: controller.signal,
|
|
73
|
-
})
|
|
73
|
+
});
|
|
74
74
|
} catch {
|
|
75
75
|
// Fail open on timeout, offline gateway, or auth failure.
|
|
76
76
|
} finally {
|
|
77
|
-
clearTimeout(timer)
|
|
77
|
+
clearTimeout(timer);
|
|
78
78
|
}
|
|
79
79
|
}
|
|
80
80
|
|
|
81
81
|
export const SubconsciousCompaction: Plugin = async () => {
|
|
82
82
|
return {
|
|
83
|
-
|
|
83
|
+
'experimental.session.compacting': async (input) => {
|
|
84
84
|
// Deliberately does not touch `output`: mutating `context` would change the
|
|
85
85
|
// customer's compaction prompt, and setting `prompt` would replace it entirely.
|
|
86
|
-
await report(input.sessionID,
|
|
86
|
+
await report(input.sessionID, 'start', 'session.compacting');
|
|
87
87
|
},
|
|
88
88
|
event: async ({ event }) => {
|
|
89
|
-
if (event.type ===
|
|
90
|
-
const sessionID = (event as { properties?: { sessionID?: string } })
|
|
91
|
-
?.sessionID
|
|
89
|
+
if (event.type === 'session.compacted') {
|
|
90
|
+
const sessionID = (event as { properties?: { sessionID?: string } })
|
|
91
|
+
.properties?.sessionID;
|
|
92
92
|
if (sessionID) {
|
|
93
93
|
// No shared id with the matching `start` is needed: the gateway pairs a start
|
|
94
94
|
// with the next end by server-stamped time, not by key.
|
|
95
|
-
await report(sessionID,
|
|
95
|
+
await report(sessionID, 'end', 'session.compacted');
|
|
96
96
|
}
|
|
97
97
|
}
|
|
98
98
|
},
|
|
99
|
-
}
|
|
100
|
-
}
|
|
99
|
+
};
|
|
100
|
+
};
|