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.
- package/README.md +135 -2
- package/bin/agent-data.js +242 -0
- package/bin/agent-help.js +106 -0
- package/bin/agent-install.js +354 -0
- package/bin/agent-launch.js +199 -0
- package/bin/agent-spawn.js +137 -0
- package/bin/agents.js +284 -840
- package/bin/auth.js +89 -33
- package/bin/branding.js +3 -1
- package/bin/claude-picker.js +143 -0
- package/bin/cli.js +87 -20
- package/bin/clipboard-image.js +24 -5
- package/bin/colors.js +1 -1
- package/bin/feedback.js +71 -21
- package/bin/harness-manifest.js +286 -0
- package/bin/headless-args.js +35 -0
- package/bin/launch-values.js +25 -0
- package/bin/model-capabilities.js +12 -8
- package/bin/models.js +35 -19
- 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 +48 -5
- package/bin/profile-settings.js +129 -0
- package/bin/profiles.js +136 -449
- package/bin/runbook/README.md +66 -23
- package/bin/runbook/claude-code/agent.json +300 -0
- package/bin/runbook/claude-code/install.sh +9 -252
- package/bin/runbook/codex/agent.json +461 -0
- package/bin/runbook/codex/hooks-lib.sh +17 -4
- package/bin/runbook/codex/install.sh +0 -4
- package/bin/runbook/codex/run.sh +16 -235
- package/bin/runbook/copilot/agent.json +185 -0
- package/bin/runbook/copilot/install.sh +45 -63
- package/bin/runbook/cursor/agent.json +137 -0
- package/bin/runbook/cursor/install.sh +8 -35
- package/bin/runbook/deepseek-harness/agent.json +163 -0
- package/bin/runbook/deepseek-harness/run.sh +20 -59
- package/bin/runbook/image-window/index.ts +14 -0
- package/bin/runbook/image-window/window.d.ts +19 -0
- package/bin/runbook/image-window/window.js +140 -0
- package/bin/runbook/lib.sh +41 -0
- package/bin/runbook/opencode/agent.json +158 -0
- package/bin/runbook/opencode/install.sh +9 -183
- package/bin/runbook/opencode/opencode-plugin.d.ts +16 -0
- package/bin/runbook/opencode/subconscious-image-window.ts +24 -0
- package/bin/runbook/pi/agent.json +216 -0
- package/bin/runbook/pi/install.sh +18 -44
- package/bin/runbook/pi/run.sh +6 -18
- package/bin/runbook/pi/subconscious-compaction.ts +71 -49
- package/bin/runbook/shared.json +58 -0
- package/bin/runbook/subconscious-code/agent.json +156 -0
- package/bin/sessions.js +173 -58
- package/bin/tui.js +104 -32
- package/bin/update-check.js +54 -21
- package/bin/upgrade.js +67 -43
- package/bin/usage.js +47 -24
- package/bin/windows/agents.js +141 -37
- package/bin/windows/common.js +36 -25
- package/bin/windows/install.js +116 -34
- package/bin/windows/launch.js +260 -77
- package/bin/windows/pi-compaction.ts +68 -13
- package/bin/windows/process.js +210 -57
- package/bin/windows/setup.js +251 -56
- package/package.json +9 -4
- package/bin/registry.generated.json +0 -406
- package/bin/runbook/claude-code/run.sh +0 -254
- package/bin/runbook/model-capabilities.generated.sh +0 -9
- package/bin/runbook/opencode/provider.sh +0 -93
- package/bin/runbook/opencode/run.sh +0 -99
- package/bin/runbook/opencode/subconscious-compaction.ts +0 -100
- package/bin/runbook/subconscious-code/run.sh +0 -21
- 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 {
|
|
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 =
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
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
|
-
.
|
|
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
|
-
|
|
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 =
|
|
198
|
-
|
|
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 =
|
|
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]
|
|
233
|
-
|
|
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:
|
|
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 =
|
|
275
|
+
const meta =
|
|
276
|
+
records.find((record) => record.type === 'session_meta')?.payload || {};
|
|
245
277
|
const messages = codexMessages(records);
|
|
246
|
-
const id =
|
|
247
|
-
|
|
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(
|
|
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 (
|
|
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
|
|
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, {
|
|
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()]
|
|
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', [
|
|
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 =
|
|
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', [
|
|
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(
|
|
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(
|
|
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)
|
|
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 =
|
|
499
|
-
|
|
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(
|
|
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
|
-
|
|
540
|
-
if (
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
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
|
-
|
|
549
|
-
if (
|
|
550
|
-
|
|
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 || ''))
|
|
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(
|
|
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(
|
|
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')
|
|
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)
|
|
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 =
|
|
596
|
-
|
|
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(
|
|
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)
|
|
715
|
+
if (!agent)
|
|
716
|
+
throw new Error(`${target.name} is not registered with this CLI.`);
|
|
611
717
|
if (targetHarness === session.harness) {
|
|
612
|
-
console.log(
|
|
613
|
-
|
|
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)
|
|
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), {
|
|
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 {
|
|
10
|
-
|
|
11
|
-
|
|
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(
|
|
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) &&
|
|
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 ||
|
|
40
|
-
|
|
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(
|
|
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([
|
|
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 {
|
|
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 (
|
|
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(
|
|
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(
|
|
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(
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
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 =
|
|
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(
|
|
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(/\/+$/, '') ||
|
|
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 ??
|
|
312
|
+
options.disableUpdateCheck ??
|
|
313
|
+
process.env.SUBC_DISABLE_UPDATE_CHECK?.trim() === '1';
|
|
278
314
|
|
|
279
315
|
const tasks = [
|
|
280
316
|
(async () => {
|
|
281
|
-
const profile =
|
|
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({
|
|
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(
|
|
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 =
|
|
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, {
|
|
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`, {
|
|
472
|
+
await fs.writeFile(statePath, `${JSON.stringify(state)}\n`, {
|
|
473
|
+
mode: 0o600,
|
|
474
|
+
});
|
|
412
475
|
const code = await spawnAndWait(
|
|
413
476
|
executable.command,
|
|
414
|
-
[
|
|
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)
|
|
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'));
|