subconscious-cli 5.0.3 → 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.
@@ -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(['gpt', 'oss', 'api', 'gguf', 'ggml', 'nomic', 'vl', 'it', 'mlx']);
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)) return token.toUpperCase();
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/') ? text.slice('subconscious/'.length) : text;
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
- ? { attachment: true, modalities: { input: ['text', 'image'], output: ['text'] } }
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({ baseUrl, model, modelIds, context, output }) {
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 { spawn, execFileSync } from 'node:child_process';
10
- import { windowsEditor, runWindows } from './windows/process.js';
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: 'Model used by Claude Code subagents. UNSET follows the default model.',
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: 'Enable Codex apps/plugins; may exceed the gateway tool limit.',
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: ['OPENCODE_API_KEY', 'OPENCODE_CONTEXT_LIMIT', 'OPENCODE_OUTPUT_LIMIT'],
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
- { id: 'shared', label: 'Shared settings', keys: ['GATEWAY_URL', 'API_KEY', 'MODEL'] },
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('\n');
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(`Invalid profile name '${name}' (use letters, digits, _ or -)`);
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(/^(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*)=(.*)$/);
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) => encodeValue(values[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) output.push(`${key}=${encodeValue(value)}`);
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 = RUNBOOK_DEFAULTS.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 = RUNBOOK_DEFAULTS.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 { name, path: file, exists: true, values: parseProfile(migratedText) };
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(path.join(LEGACY_PROFILES_DIR, `${name}.env`), 'utf-8'),
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 || (key === 'API_KEY' && apiKey !== 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 ? '********' : `${value.slice(0, 4)}…${value.slice(-4)}`;
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(/^(\s*(?:export\s+)?)([A-Za-z_][A-Za-z0-9_]*)=(.*)$/);
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 ?? '').trim().toUpperCase();
647
- return normalized === 'UNSET' || normalized === 'FOLLOW-GATEWAY' || normalized === 'FOLLOW-DEFAULT';
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(process.platform === 'win32' ? ['notepad', 'code', 'code-insiders', 'vim', 'nano'] : ['vim', 'nano']);
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('No editor found. Install vim or nano, or set $VISUAL / $EDITOR.');
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(` ${c.cyan}${name}${c.reset}${active} ${c.dim}${profilePath(name)}${c.reset}`);
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(argv, profileName = DEFAULT_PROFILE, options = {}) {
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 (['show', 'path', 'list', 'create', 'delete', 'edit', 'interactive'].includes(arg)) {
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') && isUnsetSetting(value) ? '' : value;
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 (vim or nano)');
876
+ if (editor) throw new Error('Specify only one editor');
816
877
  editor = arg;
817
878
  } else if (action === 'edit') {
818
- throw new Error(`Unknown editor '${arg}' (use vim or nano)`);
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 (action === 'list' || (action === 'default' && !options.profileExplicit && !Object.keys(updates).length)) {
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) throw new Error(`Profile '${profileName}' already 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('Refusing to delete the default profile; use subc logout to clear its key');
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 = options.selectedModel || models[0] || registry.defaults.model;
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 = model === selectedModel ? ` ${c.dim}(default)${c.reset}` : '';
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 ?? '').trim().replace(/\/+$/, '');
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(` ${c.dim}Updated profile automatically: ${profile.path}${c.reset}`);
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() || profile.values.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, { PLATFORM_URL: platformUrl });
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(` ${c.dim}Updated profile automatically: ${profile.path}${c.reset}`);
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/platform"
15
+ "platformUrl": "https://platform.subconscious.dev"
16
16
  },
17
17
  "modelCapabilities": {
18
18
  "subconscious/deepseek-v4.1-flash-marathon": {
@@ -6,7 +6,7 @@ Subconscious gateway.
6
6
 
7
7
  `subc` reads `~/.subconscious/profiles/<name>.env` and injects those values into
8
8
  the scripts, so users do not need a sibling `.env` file. When a script is run
9
- directly, `SUBC_ENV_FILE` (or `cli/bin/runbook/.env`) can supply the same keys.
9
+ directly, `SUBC_ENV_FILE` (or `bin/runbook/.env`) can supply the same keys.
10
10
 
11
11
  Included integrations:
12
12
 
@@ -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 "@opencode-ai/plugin"
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(/\/+$/, "") : undefined
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: "start" | "end",
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: "POST",
59
+ method: 'POST',
60
60
  headers: {
61
- "content-type": "application/json",
61
+ 'content-type': 'application/json',
62
62
  authorization: `Bearer ${key}`,
63
- "x-subconscious-client": "opencode",
63
+ 'x-subconscious-client': 'opencode',
64
64
  },
65
65
  body: JSON.stringify({
66
- event: "conversation_compaction",
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
- "experimental.session.compacting": async (input) => {
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, "start", "session.compacting")
86
+ await report(input.sessionID, 'start', 'session.compacting');
87
87
  },
88
88
  event: async ({ event }) => {
89
- if (event.type === "session.compacted") {
90
- const sessionID = (event as { properties?: { sessionID?: string } }).properties
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, "end", "session.compacted")
95
+ await report(sessionID, 'end', 'session.compacted');
96
96
  }
97
97
  }
98
98
  },
99
- }
100
- }
99
+ };
100
+ };