subconscious-cli 6.0.0 → 6.1.0

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.
Files changed (76) hide show
  1. package/README.md +135 -2
  2. package/bin/agent-data.js +242 -0
  3. package/bin/agent-help.js +106 -0
  4. package/bin/agent-install.js +354 -0
  5. package/bin/agent-launch.js +199 -0
  6. package/bin/agent-spawn.js +137 -0
  7. package/bin/agents.js +284 -840
  8. package/bin/auth.js +89 -33
  9. package/bin/branding.js +3 -1
  10. package/bin/claude-picker.js +143 -0
  11. package/bin/cli.js +87 -20
  12. package/bin/clipboard-image.js +24 -5
  13. package/bin/colors.js +1 -1
  14. package/bin/feedback.js +71 -21
  15. package/bin/harness-manifest.js +286 -0
  16. package/bin/headless-args.js +35 -0
  17. package/bin/launch-values.js +25 -0
  18. package/bin/model-capabilities.js +12 -8
  19. package/bin/models.js +35 -19
  20. package/bin/native/subc-tui-darwin-amd64 +0 -0
  21. package/bin/native/subc-tui-darwin-arm64 +0 -0
  22. package/bin/native/subc-tui-linux-amd64 +0 -0
  23. package/bin/native/subc-tui-linux-arm64 +0 -0
  24. package/bin/native/subc-tui-windows-amd64.exe +0 -0
  25. package/bin/native/subc-tui-windows-arm64.exe +0 -0
  26. package/bin/opencode-provider.js +48 -5
  27. package/bin/profile-settings.js +129 -0
  28. package/bin/profiles.js +136 -449
  29. package/bin/runbook/README.md +66 -23
  30. package/bin/runbook/claude-code/agent.json +300 -0
  31. package/bin/runbook/claude-code/install.sh +9 -252
  32. package/bin/runbook/codex/agent.json +461 -0
  33. package/bin/runbook/codex/hooks-lib.sh +17 -4
  34. package/bin/runbook/codex/install.sh +0 -4
  35. package/bin/runbook/codex/run.sh +16 -235
  36. package/bin/runbook/copilot/agent.json +185 -0
  37. package/bin/runbook/copilot/install.sh +45 -63
  38. package/bin/runbook/cursor/agent.json +137 -0
  39. package/bin/runbook/cursor/install.sh +8 -35
  40. package/bin/runbook/deepseek-harness/agent.json +163 -0
  41. package/bin/runbook/deepseek-harness/run.sh +20 -59
  42. package/bin/runbook/image-window/index.ts +14 -0
  43. package/bin/runbook/image-window/window.d.ts +19 -0
  44. package/bin/runbook/image-window/window.js +140 -0
  45. package/bin/runbook/lib.sh +41 -0
  46. package/bin/runbook/opencode/agent.json +158 -0
  47. package/bin/runbook/opencode/install.sh +9 -183
  48. package/bin/runbook/opencode/opencode-plugin.d.ts +16 -0
  49. package/bin/runbook/opencode/subconscious-image-window.ts +24 -0
  50. package/bin/runbook/pi/agent.json +216 -0
  51. package/bin/runbook/pi/install.sh +18 -44
  52. package/bin/runbook/pi/run.sh +6 -18
  53. package/bin/runbook/pi/subconscious-compaction.ts +71 -49
  54. package/bin/runbook/shared.json +58 -0
  55. package/bin/runbook/subconscious-code/agent.json +156 -0
  56. package/bin/sessions.js +173 -58
  57. package/bin/tui.js +104 -32
  58. package/bin/update-check.js +54 -21
  59. package/bin/upgrade.js +67 -43
  60. package/bin/usage.js +47 -24
  61. package/bin/windows/agents.js +141 -37
  62. package/bin/windows/common.js +36 -25
  63. package/bin/windows/install.js +116 -34
  64. package/bin/windows/launch.js +260 -77
  65. package/bin/windows/pi-compaction.ts +68 -13
  66. package/bin/windows/process.js +210 -57
  67. package/bin/windows/setup.js +251 -56
  68. package/package.json +9 -4
  69. package/bin/registry.generated.json +0 -406
  70. package/bin/runbook/claude-code/run.sh +0 -254
  71. package/bin/runbook/model-capabilities.generated.sh +0 -9
  72. package/bin/runbook/opencode/provider.sh +0 -93
  73. package/bin/runbook/opencode/run.sh +0 -99
  74. package/bin/runbook/opencode/subconscious-compaction.ts +0 -100
  75. package/bin/runbook/subconscious-code/run.sh +0 -21
  76. package/bin/windows/claude.js +0 -71
package/bin/sessions.js CHANGED
@@ -1,11 +1,12 @@
1
1
  import { spawnSync } from 'node:child_process';
2
- import { spawnWindowsSync } from './windows/process.js';
3
2
  import fs from 'node:fs/promises';
4
3
  import os from 'node:os';
5
4
  import path from 'node:path';
5
+ import { AGENTS, agentById, agentCommandName } from './agent-data.js';
6
+ import { resolveAgent, runAgent } from './agents.js';
6
7
 
7
8
  import { c } from './colors.js';
8
- import { resolveAgent, runAgent } from './agents.js';
9
+ import { spawnWindowsSync } from './windows/process.js';
9
10
 
10
11
  const MAX_CANDIDATE_FILES = 160;
11
12
  const MAX_READ_BYTES = 2 * 1024 * 1024;
@@ -14,19 +15,29 @@ const MAX_INDEX_BYTES = 4 * 1024 * 1024;
14
15
  const MAX_HANDOFF_MESSAGES = 24;
15
16
  const MAX_HANDOFF_CHARS = 24_000;
16
17
  const MAX_MESSAGE_CHARS = 4_000;
17
- const SESSION_KEY_PATTERN = /^(claude|codex|opencode|pi|sc):[-A-Za-z0-9._]+$/;
18
-
19
- export const SESSION_HARNESSES = Object.freeze({
20
- claude: { name: 'Claude Code', command: 'claude', portable: true },
21
- codex: { name: 'Codex CLI', command: 'codex', portable: true },
22
- opencode: { name: 'OpenCode', command: 'opencode', portable: true },
23
- pi: { name: 'Pi', command: 'pi', portable: true },
24
- sc: { name: 'Marathon', command: 'marathon', portable: false },
25
- });
18
+ const SESSION_KEY_PATTERN = new RegExp(
19
+ `^(${AGENTS.filter((agent) => agent.sessions)
20
+ .map((agent) => agent.sessions.key)
21
+ .join('|')}):[-A-Za-z0-9._]+$`,
22
+ );
23
+
24
+ export const SESSION_HARNESSES = Object.freeze(
25
+ Object.fromEntries(
26
+ AGENTS.filter((agent) => agent.sessions).map((agent) => [
27
+ agent.sessions.key,
28
+ Object.freeze({
29
+ name: agent.name,
30
+ command: agentCommandName(agent),
31
+ portable: agent.sessions.portable,
32
+ agentId: agent.id,
33
+ }),
34
+ ]),
35
+ ),
36
+ );
26
37
 
27
38
  function cleanText(value) {
28
39
  return String(value || '')
29
- .replace(/\u0000/g, '')
40
+ .replaceAll('\0', '')
30
41
  .replace(/\r\n/g, '\n')
31
42
  .trim();
32
43
  }
@@ -56,7 +67,10 @@ function parseLines(text) {
56
67
  for (const line of text.split('\n')) {
57
68
  if (!line.trim()) continue;
58
69
  try {
59
- records.push(JSON.parse(line));
70
+ const record = JSON.parse(line);
71
+ if (record && typeof record === 'object' && !Array.isArray(record)) {
72
+ records.push(record);
73
+ }
60
74
  } catch {
61
75
  // Session writers can leave a partial final line after an interrupted run.
62
76
  }
@@ -194,8 +208,15 @@ function codexMessages(records) {
194
208
  return records.flatMap((record) => {
195
209
  if (record.type !== 'event_msg') return [];
196
210
  const eventType = record.payload?.type;
197
- const role = eventType === 'user_message' ? 'user' : eventType === 'agent_message' ? 'assistant' : '';
198
- const text = cleanText(record.payload?.message || record.payload?.last_agent_message);
211
+ const role =
212
+ eventType === 'user_message'
213
+ ? 'user'
214
+ : eventType === 'agent_message'
215
+ ? 'assistant'
216
+ : '';
217
+ const text = cleanText(
218
+ record.payload?.message || record.payload?.last_agent_message,
219
+ );
199
220
  return role && text ? [{ role, text }] : [];
200
221
  });
201
222
  }
@@ -227,12 +248,22 @@ async function parseFileSession(entry, harness) {
227
248
  const messages = claudeMessages(records);
228
249
  const metadata = records.find((record) => record.cwd) || {};
229
250
  const identity = records.find((record) => record.sessionId) || {};
230
- const id = metadata.sessionId || identity.sessionId || filename.split('.orphaned-')[0];
251
+ const id =
252
+ metadata.sessionId ||
253
+ identity.sessionId ||
254
+ filename.split('.orphaned-')[0];
231
255
  const titleRecord = records.find((record) => record.type === 'ai-title');
232
- const lastPrompt = [...records].reverse().find((record) => record.type === 'last-prompt');
233
- const assistant = records.find((record) => record.message?.role === 'assistant');
256
+ const lastPrompt = [...records]
257
+ .reverse()
258
+ .find((record) => record.type === 'last-prompt');
259
+ const assistant = records.find(
260
+ (record) => record.message?.role === 'assistant',
261
+ );
234
262
  return sessionRecord(harness, id, {
235
- title: titleRecord?.aiTitle || messages.find((message) => message.role === 'user')?.text || lastPrompt?.lastPrompt,
263
+ title:
264
+ titleRecord?.aiTitle ||
265
+ messages.find((message) => message.role === 'user')?.text ||
266
+ lastPrompt?.lastPrompt,
236
267
  cwd: metadata.cwd,
237
268
  model: assistant?.message?.model,
238
269
  updated: entry.updated,
@@ -241,10 +272,14 @@ async function parseFileSession(entry, harness) {
241
272
  }
242
273
 
243
274
  if (harness === 'codex') {
244
- const meta = records.find((record) => record.type === 'session_meta')?.payload || {};
275
+ const meta =
276
+ records.find((record) => record.type === 'session_meta')?.payload || {};
245
277
  const messages = codexMessages(records);
246
- const id = meta.id || filename.match(/([0-9a-f]{8}(?:-[0-9a-f]{4}){3}-[0-9a-f]{12})$/i)?.[1];
247
- const context = records.find((record) => record.type === 'turn_context')?.payload || {};
278
+ const id =
279
+ meta.id ||
280
+ filename.match(/([0-9a-f]{8}(?:-[0-9a-f]{4}){3}-[0-9a-f]{12})$/i)?.[1];
281
+ const context =
282
+ records.find((record) => record.type === 'turn_context')?.payload || {};
248
283
  return sessionRecord(harness, id, {
249
284
  title: messages.find((message) => message.role === 'user')?.text,
250
285
  cwd: meta.cwd,
@@ -257,7 +292,9 @@ async function parseFileSession(entry, harness) {
257
292
  if (harness === 'pi') {
258
293
  const meta = records.find((record) => record.type === 'session') || {};
259
294
  const messages = piMessages(records);
260
- const assistant = records.find((record) => record.message?.role === 'assistant');
295
+ const assistant = records.find(
296
+ (record) => record.message?.role === 'assistant',
297
+ );
261
298
  return sessionRecord(harness, meta.id || filename.split('_').at(-1), {
262
299
  title: messages.find((message) => message.role === 'user')?.text,
263
300
  cwd: meta.cwd,
@@ -294,11 +331,17 @@ async function discoverFileSessions(root, harness, limit) {
294
331
  function sessionIdFromFile(file, harness) {
295
332
  const name = path.basename(file, '.jsonl');
296
333
  if (harness === 'claude') {
297
- if (name.includes('.orphaned-') || file.split(path.sep).includes('subagents')) return '';
334
+ if (
335
+ name.includes('.orphaned-') ||
336
+ file.split(path.sep).includes('subagents')
337
+ )
338
+ return '';
298
339
  return name;
299
340
  }
300
341
  if (harness === 'codex') {
301
- return name.match(/([0-9a-f]{8}(?:-[0-9a-f]{4}){3}-[0-9a-f]{12})$/i)?.[1] || '';
342
+ return (
343
+ name.match(/([0-9a-f]{8}(?:-[0-9a-f]{4}){3}-[0-9a-f]{12})$/i)?.[1] || ''
344
+ );
302
345
  }
303
346
  return '';
304
347
  }
@@ -359,10 +402,16 @@ async function discoverCodexIndex(root, indexFile, max) {
359
402
  const updated = Date.parse(record.updated_at || '') || 0;
360
403
  const current = byID.get(record.id);
361
404
  if (!current || updated >= current.updated) {
362
- byID.set(record.id, { id: record.id, title: record.thread_name, updated });
405
+ byID.set(record.id, {
406
+ id: record.id,
407
+ title: record.thread_name,
408
+ updated,
409
+ });
363
410
  }
364
411
  }
365
- const selected = [...byID.values()].sort((a, b) => b.updated - a.updated).slice(0, max);
412
+ const selected = [...byID.values()]
413
+ .sort((a, b) => b.updated - a.updated)
414
+ .slice(0, max);
366
415
  return selected.map((record) =>
367
416
  sessionRecord('codex', record.id, {
368
417
  ...record,
@@ -400,7 +449,12 @@ function discoverOpenCodeSessions(execute, max) {
400
449
  'from session where time_archived is null',
401
450
  `order by time_updated desc limit ${Math.max(1, Math.min(500, max))}`,
402
451
  ].join(' ');
403
- const output = commandResult(execute, 'opencode', ['db', '--format', 'json', query]);
452
+ const output = commandResult(execute, 'opencode', [
453
+ 'db',
454
+ '--format',
455
+ 'json',
456
+ query,
457
+ ]);
404
458
  if (!output) return [];
405
459
  try {
406
460
  return JSON.parse(output).map((record) =>
@@ -419,7 +473,9 @@ function discoverOpenCodeSessions(execute, max) {
419
473
  export async function discoverSessions(options = {}) {
420
474
  const home = options.home || os.homedir();
421
475
  const max = options.max || 500;
422
- const execute = options.execute || (process.platform === 'win32' ? spawnWindowsSync : spawnSync);
476
+ const execute =
477
+ options.execute ||
478
+ (process.platform === 'win32' ? spawnWindowsSync : spawnSync);
423
479
  const roots = options.roots || {
424
480
  claude: path.join(home, '.claude', 'projects'),
425
481
  codex: path.join(home, '.codex', 'sessions'),
@@ -467,11 +523,19 @@ async function openCodeMessages(session, execute) {
467
523
  "and json_extract(part.data, '$.type') = 'text'",
468
524
  'order by message.time_created asc, part.time_created asc',
469
525
  ].join(' ');
470
- const output = commandResult(execute, 'opencode', ['db', '--format', 'json', query]);
526
+ const output = commandResult(execute, 'opencode', [
527
+ 'db',
528
+ '--format',
529
+ 'json',
530
+ query,
531
+ ]);
471
532
  if (!output) return [];
472
533
  try {
473
534
  return JSON.parse(output)
474
- .filter((row) => ['user', 'assistant'].includes(row.role) && cleanText(row.text))
535
+ .filter(
536
+ (row) =>
537
+ ['user', 'assistant'].includes(row.role) && cleanText(row.text),
538
+ )
475
539
  .map((row) => ({ role: row.role, text: cleanText(row.text) }));
476
540
  } catch {
477
541
  return [];
@@ -480,7 +544,11 @@ async function openCodeMessages(session, execute) {
480
544
 
481
545
  export async function readSessionMessages(session, options = {}) {
482
546
  if (session.harness === 'opencode') {
483
- return openCodeMessages(session, options.execute || (process.platform === 'win32' ? spawnWindowsSync : spawnSync));
547
+ return openCodeMessages(
548
+ session,
549
+ options.execute ||
550
+ (process.platform === 'win32' ? spawnWindowsSync : spawnSync),
551
+ );
484
552
  }
485
553
  if (!session.sourcePath) return [];
486
554
  const records = parseLines(await readBounded(session.sourcePath));
@@ -492,11 +560,14 @@ export async function readSessionMessages(session, options = {}) {
492
560
  }
493
561
 
494
562
  async function hydrateSession(session) {
495
- if (session.harness !== 'codex' || !session.sourcePath || session.cwd) return session;
563
+ if (session.harness !== 'codex' || !session.sourcePath || session.cwd)
564
+ return session;
496
565
  try {
497
566
  const head = parseLines(await readHead(session.sourcePath));
498
- const meta = head.find((item) => item.type === 'session_meta')?.payload || {};
499
- const context = head.find((item) => item.type === 'turn_context')?.payload || {};
567
+ const meta =
568
+ head.find((item) => item.type === 'session_meta')?.payload || {};
569
+ const context =
570
+ head.find((item) => item.type === 'turn_context')?.payload || {};
500
571
  return { ...session, cwd: meta.cwd || '', model: context.model || '' };
501
572
  } catch {
502
573
  return session;
@@ -518,7 +589,9 @@ export function buildHandoffPrompt(session, messages) {
518
589
  }
519
590
  selected.reverse();
520
591
  if (!selected.length) {
521
- throw new Error(`No portable user/assistant messages were found in ${session.harnessName}.`);
592
+ throw new Error(
593
+ `No portable user/assistant messages were found in ${session.harnessName}.`,
594
+ );
522
595
  }
523
596
 
524
597
  return [
@@ -535,23 +608,43 @@ export function buildHandoffPrompt(session, messages) {
535
608
  .join('\n\n');
536
609
  }
537
610
 
611
+ function sessionLaunch(harness) {
612
+ return agentById(SESSION_HARNESSES[harness]?.agentId)?.launch ?? {};
613
+ }
614
+
615
+ // One pass, so a prompt that mentions a placeholder is passed unchanged.
616
+ function fillWords(words, values) {
617
+ return words.map((word) =>
618
+ word.replace(/\{(sessionId|sessionFile|prompt)\}/g, (_, name) =>
619
+ String(values[name]),
620
+ ),
621
+ );
622
+ }
623
+
538
624
  export function nativeResumeArgs(session) {
539
- if (session.harness === 'claude') return ['--resume', session.id];
540
- if (session.harness === 'codex') return ['resume', session.id];
541
- if (session.harness === 'opencode') return ['--session', session.id];
542
- if (session.harness === 'pi') return ['--session', session.sourcePath || session.id];
543
- if (session.harness === 'sc') return ['--resume', session.sourcePath];
544
- throw new Error(`Native resume is not supported for ${session.harnessName}.`);
625
+ const { resume } = sessionLaunch(session.harness);
626
+ if (!resume)
627
+ throw new Error(
628
+ `Native resume is not supported for ${session.harnessName}.`,
629
+ );
630
+ return fillWords(resume, {
631
+ sessionId: session.id,
632
+ sessionFile: session.sourcePath || session.id,
633
+ });
545
634
  }
546
635
 
547
636
  export function handoffLaunchArgs(targetHarness, prompt) {
548
- if (targetHarness === 'opencode') return ['--prompt', prompt];
549
- if (['claude', 'codex', 'pi'].includes(targetHarness)) return [prompt];
550
- throw new Error(`${SESSION_HARNESSES[targetHarness]?.name || targetHarness} cannot start an interactive handoff yet.`);
637
+ const { handoff } = sessionLaunch(targetHarness);
638
+ if (!handoff)
639
+ throw new Error(
640
+ `${SESSION_HARNESSES[targetHarness]?.name || targetHarness} cannot start an interactive handoff yet.`,
641
+ );
642
+ return fillWords(handoff, { prompt });
551
643
  }
552
644
 
553
645
  function sessionByKey(sessions, key) {
554
- if (!SESSION_KEY_PATTERN.test(key || '')) throw new Error('Invalid session key.');
646
+ if (!SESSION_KEY_PATTERN.test(key || ''))
647
+ throw new Error('Invalid session key.');
555
648
  return sessions.find((session) => session.key === key) || null;
556
649
  }
557
650
 
@@ -564,11 +657,15 @@ function formatWhen(timestamp) {
564
657
  export function printSessions(sessions) {
565
658
  console.log(`\n ${c.bold}Coding sessions${c.reset}\n`);
566
659
  if (!sessions.length) {
567
- console.log(` ${c.dim}No supported local coding sessions were found.${c.reset}\n`);
660
+ console.log(
661
+ ` ${c.dim}No supported local coding sessions were found.${c.reset}\n`,
662
+ );
568
663
  return;
569
664
  }
570
665
  for (const session of sessions) {
571
- console.log(` ${c.cyan}${session.key}${c.reset} ${c.bold}${session.title}${c.reset}`);
666
+ console.log(
667
+ ` ${c.cyan}${session.key}${c.reset} ${c.bold}${session.title}${c.reset}`,
668
+ );
572
669
  console.log(
573
670
  ` ${c.dim}${session.harnessName} · ${formatWhen(session.updatedAt)}${session.cwd ? ` · ${session.cwd}` : ''}${c.reset}`,
574
671
  );
@@ -585,15 +682,21 @@ export async function sessionsCommand(argv, options = {}) {
585
682
  printSessions(sessions);
586
683
  return 0;
587
684
  }
588
- if (action !== 'resume') throw new Error(`Unknown sessions action: ${action}`);
685
+ if (action !== 'resume')
686
+ throw new Error(`Unknown sessions action: ${action}`);
589
687
 
590
688
  const key = argv[1];
591
689
  let session = sessionByKey(sessions, key);
592
- if (!session) throw new Error(`Session '${key}' was not found. Run subc sessions to refresh the list.`);
690
+ if (!session)
691
+ throw new Error(
692
+ `Session '${key}' was not found. Run subc sessions to refresh the list.`,
693
+ );
593
694
  session = await hydrateSession(session);
594
695
  const harnessIndex = argv.indexOf('--harness');
595
- const requestedHarness = harnessIndex >= 0 ? argv[harnessIndex + 1] : session.harness;
596
- const targetHarness = requestedHarness === 'marathon' ? 'sc' : requestedHarness;
696
+ const requestedHarness =
697
+ harnessIndex >= 0 ? argv[harnessIndex + 1] : session.harness;
698
+ const targetHarness =
699
+ requestedHarness === 'marathon' ? 'sc' : requestedHarness;
597
700
  const target = SESSION_HARNESSES[targetHarness];
598
701
  if (!target) throw new Error(`Unknown destination harness: ${targetHarness}`);
599
702
 
@@ -602,22 +705,34 @@ export async function sessionsCommand(argv, options = {}) {
602
705
  const stat = await fs.stat(session.cwd);
603
706
  if (stat.isDirectory()) process.chdir(session.cwd);
604
707
  } catch {
605
- console.error(` ${c.yellow}The original directory is unavailable; resuming in ${process.cwd()}.${c.reset}\n`);
708
+ console.error(
709
+ ` ${c.yellow}The original directory is unavailable; resuming in ${process.cwd()}.${c.reset}\n`,
710
+ );
606
711
  }
607
712
  }
608
713
 
609
714
  const agent = resolveAgent(target.command);
610
- if (!agent) throw new Error(`${target.name} is not registered with this CLI.`);
715
+ if (!agent)
716
+ throw new Error(`${target.name} is not registered with this CLI.`);
611
717
  if (targetHarness === session.harness) {
612
- console.log(`\n ${c.dim}Resuming in ${c.reset}${c.bold}${session.harnessName}${c.reset}${c.dim}: ${session.title}${c.reset}\n`);
613
- return runAgent(agent, nativeResumeArgs(session), { profile: options.profile });
718
+ console.log(
719
+ `\n ${c.dim}Resuming in ${c.reset}${c.bold}${session.harnessName}${c.reset}${c.dim}: ${session.title}${c.reset}\n`,
720
+ );
721
+ return runAgent(agent, nativeResumeArgs(session), {
722
+ profile: options.profile,
723
+ });
614
724
  }
615
- if (!target.portable) throw new Error(`${target.name} cannot receive cross-harness sessions yet.`);
725
+ if (!target.portable)
726
+ throw new Error(
727
+ `${target.name} cannot receive cross-harness sessions yet.`,
728
+ );
616
729
 
617
730
  const messages = await readSessionMessages(session, options);
618
731
  const prompt = buildHandoffPrompt(session, messages);
619
732
  console.log(
620
733
  `\n ${c.dim}Handing off ${c.reset}${c.bold}${session.harnessName}${c.reset}${c.dim} → ${c.reset}${c.bold}${target.name}${c.reset}${c.dim}: ${session.title}${c.reset}\n`,
621
734
  );
622
- return runAgent(agent, handoffLaunchArgs(targetHarness, prompt), { profile: options.profile });
735
+ return runAgent(agent, handoffLaunchArgs(targetHarness, prompt), {
736
+ profile: options.profile,
737
+ });
623
738
  }
package/bin/tui.js CHANGED
@@ -6,22 +6,29 @@ import { fileURLToPath } from 'node:url';
6
6
 
7
7
  import { agentList } from './agents.js';
8
8
  import { DEFAULT_PLATFORM_URL, getApiKey, getPlatformUrl } from './auth.js';
9
- import { isAbortError, normalizeModelIds, resolveModelCatalog } from './models.js';
10
- import { discoverSessions, SESSION_HARNESSES } from './sessions.js';
11
- import { compareVersions, fetchLatestVersion } from './update-check.js';
9
+ import {
10
+ isAbortError,
11
+ normalizeModelIds,
12
+ resolveModelCatalog,
13
+ } from './models.js';
12
14
  import {
13
15
  DEFAULT_PROFILE,
14
16
  listProfiles,
15
17
  loadProfile,
16
- resolvedModelSetting,
17
- RUNBOOK_DEFAULTS,
18
18
  SUPPORTED_MODELS as PACKAGED_MODELS,
19
+ RUNBOOK_DEFAULTS,
20
+ resolvedModelSetting,
19
21
  } from './profiles.js';
22
+ import { discoverSessions, SESSION_HARNESSES } from './sessions.js';
23
+ import { compareVersions, fetchLatestVersion } from './update-check.js';
20
24
 
21
25
  const BIN_DIR = path.dirname(fileURLToPath(import.meta.url));
22
26
  const TUI_SOURCE_DIR = path.resolve(BIN_DIR, '../tui');
23
27
 
24
- export function nativeTargetName(platform = process.platform, arch = process.arch) {
28
+ export function nativeTargetName(
29
+ platform = process.platform,
30
+ arch = process.arch,
31
+ ) {
25
32
  const goArch = { x64: 'amd64', arm64: 'arm64' }[arch];
26
33
  if (!goArch || !['darwin', 'linux', 'win32'].includes(platform)) return null;
27
34
  const goOS = platform === 'win32' ? 'windows' : platform;
@@ -34,10 +41,13 @@ export function isTuiResult(result) {
34
41
  result !== null &&
35
42
  typeof result === 'object' &&
36
43
  (result.updatePrompt === true ||
37
- (Array.isArray(result.args) && result.args.every((arg) => typeof arg === 'string'))) &&
44
+ (Array.isArray(result.args) &&
45
+ result.args.every((arg) => typeof arg === 'string'))) &&
38
46
  (result.baseUrl === undefined || typeof result.baseUrl === 'string') &&
39
- (result.installedVersion === undefined || typeof result.installedVersion === 'string') &&
40
- (result.latestVersion === undefined || typeof result.latestVersion === 'string')
47
+ (result.installedVersion === undefined ||
48
+ typeof result.installedVersion === 'string') &&
49
+ (result.latestVersion === undefined ||
50
+ typeof result.latestVersion === 'string')
41
51
  );
42
52
  }
43
53
 
@@ -50,9 +60,15 @@ async function pathExists(file) {
50
60
  }
51
61
  }
52
62
 
53
- export async function tuiSourceIsNewerThan(binary, sourceFile = path.join(TUI_SOURCE_DIR, 'cmd/subc-tui/main.go')) {
63
+ export async function tuiSourceIsNewerThan(
64
+ binary,
65
+ sourceFile = path.join(TUI_SOURCE_DIR, 'cmd/subc-tui/main.go'),
66
+ ) {
54
67
  try {
55
- const [binaryStat, sourceStat] = await Promise.all([fs.stat(binary), fs.stat(sourceFile)]);
68
+ const [binaryStat, sourceStat] = await Promise.all([
69
+ fs.stat(binary),
70
+ fs.stat(sourceFile),
71
+ ]);
56
72
  return sourceStat.mtimeMs > binaryStat.mtimeMs;
57
73
  } catch {
58
74
  return false;
@@ -70,7 +86,11 @@ function sourceTuiCommand() {
70
86
  export async function resolveTuiExecutable(options = {}) {
71
87
  const override = options.binary || process.env.SUBC_TUI_BIN?.trim();
72
88
  if (override) {
73
- return { command: override, args: options.binaryArgs || [], cwd: undefined };
89
+ return {
90
+ command: override,
91
+ args: options.binaryArgs || [],
92
+ cwd: undefined,
93
+ };
74
94
  }
75
95
 
76
96
  const hasSource = await pathExists(path.join(TUI_SOURCE_DIR, 'go.mod'));
@@ -79,7 +99,10 @@ export async function resolveTuiExecutable(options = {}) {
79
99
  const packaged = path.join(BIN_DIR, 'native', target);
80
100
  // Source checkouts keep a cached host binary for speed. If TUI source is
81
101
  // newer, that cache is stale (for example it will reject --updates).
82
- if (await pathExists(packaged) && !(hasSource && (await tuiSourceIsNewerThan(packaged)))) {
102
+ if (
103
+ (await pathExists(packaged)) &&
104
+ !(hasSource && (await tuiSourceIsNewerThan(packaged)))
105
+ ) {
83
106
  return { command: packaged, args: [], cwd: undefined };
84
107
  }
85
108
  }
@@ -91,7 +114,9 @@ export async function resolveTuiExecutable(options = {}) {
91
114
  }
92
115
 
93
116
  function selectedModelFor(profile) {
94
- return resolvedModelSetting(process.env.SUBCONSCIOUS_MODEL || profile.values.MODEL);
117
+ return resolvedModelSetting(
118
+ process.env.SUBCONSCIOUS_MODEL || profile.values.MODEL,
119
+ );
95
120
  }
96
121
 
97
122
  function subagentModelFor(profile) {
@@ -181,16 +206,22 @@ export async function writeAtomicJson(file, value) {
181
206
  }
182
207
  }
183
208
 
184
- export async function createLocalTuiState(profileName = DEFAULT_PROFILE, options = {}) {
209
+ export async function createLocalTuiState(
210
+ profileName = DEFAULT_PROFILE,
211
+ options = {},
212
+ ) {
185
213
  const activeProfile = options.profile || (await loadProfile(profileName));
186
- const names = [...new Set([profileName, ...(await listProfiles())])].sort((a, b) => {
187
- if (a === profileName) return -1;
188
- if (b === profileName) return 1;
189
- return a.localeCompare(b);
190
- });
214
+ const names = [...new Set([profileName, ...(await listProfiles())])].sort(
215
+ (a, b) => {
216
+ if (a === profileName) return -1;
217
+ if (b === profileName) return 1;
218
+ return a.localeCompare(b);
219
+ },
220
+ );
191
221
  const profiles = await Promise.all(
192
222
  names.map(async (name) => {
193
- const profile = name === profileName ? activeProfile : await loadProfile(name);
223
+ const profile =
224
+ name === profileName ? activeProfile : await loadProfile(name);
194
225
  const auth = await getApiKey(profile);
195
226
  return {
196
227
  name,
@@ -210,7 +241,10 @@ export async function createLocalTuiState(profileName = DEFAULT_PROFILE, options
210
241
  activeProfile: profileName,
211
242
  profilePath: activeProfile.path,
212
243
  profiles,
213
- models: normalizeModelIds([requestedModel, ...PACKAGED_MODELS], requestedModel),
244
+ models: normalizeModelIds(
245
+ [requestedModel, ...PACKAGED_MODELS],
246
+ requestedModel,
247
+ ),
214
248
  selectedModel: requestedModel,
215
249
  subagentModel: subagentModelFor(activeProfile),
216
250
  gatewayUrl,
@@ -220,7 +254,8 @@ export async function createLocalTuiState(profileName = DEFAULT_PROFILE, options
220
254
  gatewayOverridden: Boolean(process.env.SUBCONSCIOUS_BASE_URL?.trim()),
221
255
  platformUrl,
222
256
  savedPlatformUrl:
223
- activeProfile.values.PLATFORM_URL?.trim().replace(/\/+$/, '') || DEFAULT_PLATFORM_URL,
257
+ activeProfile.values.PLATFORM_URL?.trim().replace(/\/+$/, '') ||
258
+ DEFAULT_PLATFORM_URL,
224
259
  platformOverridden: Boolean(process.env.SUBCONSCIOUS_URL?.trim()),
225
260
  modelError: '',
226
261
  modelSource: 'packaged',
@@ -274,11 +309,13 @@ export async function loadRemoteTuiUpdates(state, options = {}) {
274
309
  const fetchLatest = options.fetchLatestVersion || fetchLatestVersion;
275
310
  const readApiKey = options.getApiKey || getApiKey;
276
311
  const updateDisabled =
277
- options.disableUpdateCheck ?? process.env.SUBC_DISABLE_UPDATE_CHECK?.trim() === '1';
312
+ options.disableUpdateCheck ??
313
+ process.env.SUBC_DISABLE_UPDATE_CHECK?.trim() === '1';
278
314
 
279
315
  const tasks = [
280
316
  (async () => {
281
- const profile = options.profile || (await loadProfile(state.activeProfile));
317
+ const profile =
318
+ options.profile || (await loadProfile(state.activeProfile));
282
319
  if (signal?.aborted) return;
283
320
  const auth = await readApiKey(profile);
284
321
  if (signal?.aborted) return;
@@ -330,7 +367,10 @@ export async function loadRemoteTuiUpdates(state, options = {}) {
330
367
  });
331
368
  if (signal?.aborted || !latest) return;
332
369
  if (compareVersions(latest, state.version) > 0) {
333
- await enqueuePatch({ updateAvailable: true, latestVersion: latest });
370
+ await enqueuePatch({
371
+ updateAvailable: true,
372
+ latestVersion: latest,
373
+ });
334
374
  }
335
375
  } catch (error) {
336
376
  if (isAbortError(error) || signal?.aborted) return;
@@ -347,7 +387,10 @@ export async function loadRemoteTuiUpdates(state, options = {}) {
347
387
  }
348
388
  }
349
389
 
350
- export async function createTuiState(profileName = DEFAULT_PROFILE, options = {}) {
390
+ export async function createTuiState(
391
+ profileName = DEFAULT_PROFILE,
392
+ options = {},
393
+ ) {
351
394
  const state = await createLocalTuiState(profileName, options);
352
395
  let next = { ...state };
353
396
  await loadRemoteTuiUpdates(state, {
@@ -379,11 +422,25 @@ function spawnAndWait(command, args, options = {}) {
379
422
  }
380
423
 
381
424
  export async function runTui(options = {}) {
425
+ for (;;) {
426
+ const result = await runTuiOnce(options);
427
+ if (result?.args?.length !== 2 || result.args[0] !== '-p') return result;
428
+ options = {
429
+ ...options,
430
+ profileName: result.args[1],
431
+ profile: undefined,
432
+ state: undefined,
433
+ };
434
+ }
435
+ }
436
+
437
+ async function runTuiOnce(options) {
382
438
  const executable = await resolveTuiExecutable(options);
383
439
  if (!executable) return null;
384
440
 
385
441
  const providedState = options.state;
386
- const state = providedState || (await createLocalTuiState(options.profileName, options));
442
+ const state =
443
+ providedState || (await createLocalTuiState(options.profileName, options));
387
444
  const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'subc-tui-'));
388
445
  const statePath = path.join(tempDir, 'state.json');
389
446
  const resultPath = path.join(tempDir, 'result.json');
@@ -403,20 +460,35 @@ export async function runTui(options = {}) {
403
460
 
404
461
  const remote = providedState
405
462
  ? Promise.resolve()
406
- : loadRemoteTuiUpdates(state, { ...options, signal: controller.signal, writePatch });
463
+ : loadRemoteTuiUpdates(state, {
464
+ ...options,
465
+ signal: controller.signal,
466
+ writePatch,
467
+ });
407
468
 
408
469
  try {
409
470
  // This state intentionally contains only display data. API keys are used
410
471
  // by the Node command engine and are never passed into the TUI process.
411
- await fs.writeFile(statePath, `${JSON.stringify(state)}\n`, { mode: 0o600 });
472
+ await fs.writeFile(statePath, `${JSON.stringify(state)}\n`, {
473
+ mode: 0o600,
474
+ });
412
475
  const code = await spawnAndWait(
413
476
  executable.command,
414
- [...executable.args, '--state', statePath, '--result', resultPath, '--updates', updatesPath],
477
+ [
478
+ ...executable.args,
479
+ '--state',
480
+ statePath,
481
+ '--result',
482
+ resultPath,
483
+ '--updates',
484
+ updatesPath,
485
+ ],
415
486
  { cwd: executable.cwd, spawn: options.spawn, stdio: options.stdio },
416
487
  );
417
488
  controller.abort();
418
489
  await remote.catch(() => {});
419
- if (code !== 0) throw new Error(`Subconscious TUI exited with status ${code}`);
490
+ if (code !== 0)
491
+ throw new Error(`Subconscious TUI exited with status ${code}`);
420
492
 
421
493
  try {
422
494
  const result = JSON.parse(await fs.readFile(resultPath, 'utf-8'));