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
|
@@ -31,40 +31,43 @@
|
|
|
31
31
|
* https://pi.dev/docs/latest/extensions
|
|
32
32
|
*/
|
|
33
33
|
|
|
34
|
-
import { readFileSync } from
|
|
35
|
-
import { homedir } from
|
|
36
|
-
import { join } from
|
|
34
|
+
import { readFileSync } from 'node:fs';
|
|
35
|
+
import { homedir } from 'node:os';
|
|
36
|
+
import { join } from 'node:path';
|
|
37
37
|
|
|
38
|
-
const TIMEOUT_MS = 2000
|
|
38
|
+
const TIMEOUT_MS = 2000;
|
|
39
39
|
|
|
40
|
-
type CompactPhase =
|
|
40
|
+
type CompactPhase = 'start' | 'end';
|
|
41
41
|
|
|
42
|
-
let envFileLoaded = false
|
|
42
|
+
let envFileLoaded = false;
|
|
43
43
|
|
|
44
44
|
function loadSubconsciousEnvFile(): void {
|
|
45
|
-
if (envFileLoaded) return
|
|
46
|
-
envFileLoaded = true
|
|
47
|
-
if (
|
|
48
|
-
|
|
45
|
+
if (envFileLoaded) return;
|
|
46
|
+
envFileLoaded = true;
|
|
47
|
+
if (
|
|
48
|
+
process.env.SUBCONSCIOUS_GATEWAY_URL &&
|
|
49
|
+
process.env.SUBCONSCIOUS_API_KEY
|
|
50
|
+
) {
|
|
51
|
+
return;
|
|
49
52
|
}
|
|
50
53
|
try {
|
|
51
|
-
const path = join(homedir(),
|
|
52
|
-
const text = readFileSync(path,
|
|
53
|
-
for (const line of text.split(
|
|
54
|
-
const trimmed = line.trim()
|
|
55
|
-
if (!trimmed || trimmed.startsWith(
|
|
56
|
-
const m = trimmed.match(/^(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*)=(.*)$/)
|
|
57
|
-
if (!m) continue
|
|
58
|
-
const key = m[1]
|
|
59
|
-
let val = m[2]
|
|
54
|
+
const path = join(homedir(), '.pi', 'agent', 'subconscious.env');
|
|
55
|
+
const text = readFileSync(path, 'utf8');
|
|
56
|
+
for (const line of text.split('\n')) {
|
|
57
|
+
const trimmed = line.trim();
|
|
58
|
+
if (!trimmed || trimmed.startsWith('#')) continue;
|
|
59
|
+
const m = trimmed.match(/^(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*)=(.*)$/);
|
|
60
|
+
if (!m) continue;
|
|
61
|
+
const key = m[1];
|
|
62
|
+
let val = m[2];
|
|
60
63
|
if (
|
|
61
64
|
(val.startsWith('"') && val.endsWith('"')) ||
|
|
62
65
|
(val.startsWith("'") && val.endsWith("'"))
|
|
63
66
|
) {
|
|
64
|
-
val = val.slice(1, -1)
|
|
67
|
+
val = val.slice(1, -1);
|
|
65
68
|
}
|
|
66
69
|
if (process.env[key] === undefined) {
|
|
67
|
-
process.env[key] = val
|
|
70
|
+
process.env[key] = val;
|
|
68
71
|
}
|
|
69
72
|
}
|
|
70
73
|
} catch {
|
|
@@ -73,14 +76,18 @@ function loadSubconsciousEnvFile(): void {
|
|
|
73
76
|
}
|
|
74
77
|
|
|
75
78
|
function gatewayUrl(): string | undefined {
|
|
76
|
-
loadSubconsciousEnvFile()
|
|
77
|
-
const raw = process.env.SUBCONSCIOUS_GATEWAY_URL ?? process.env.GATEWAY_URL
|
|
78
|
-
return raw ? raw.replace(/\/+$/,
|
|
79
|
+
loadSubconsciousEnvFile();
|
|
80
|
+
const raw = process.env.SUBCONSCIOUS_GATEWAY_URL ?? process.env.GATEWAY_URL;
|
|
81
|
+
return raw ? raw.replace(/\/+$/, '') : undefined;
|
|
79
82
|
}
|
|
80
83
|
|
|
81
84
|
function apiKey(): string | undefined {
|
|
82
|
-
loadSubconsciousEnvFile()
|
|
83
|
-
return
|
|
85
|
+
loadSubconsciousEnvFile();
|
|
86
|
+
return (
|
|
87
|
+
process.env.SUBCONSCIOUS_API_KEY ??
|
|
88
|
+
process.env.API_KEY ??
|
|
89
|
+
process.env.PI_API_KEY
|
|
90
|
+
);
|
|
84
91
|
}
|
|
85
92
|
|
|
86
93
|
async function report(
|
|
@@ -89,25 +96,25 @@ async function report(
|
|
|
89
96
|
hookEventName: string,
|
|
90
97
|
metadata?: Record<string, unknown>,
|
|
91
98
|
): Promise<void> {
|
|
92
|
-
const url = gatewayUrl()
|
|
93
|
-
const key = apiKey()
|
|
94
|
-
if (!url || !key || !sessionID) return
|
|
99
|
+
const url = gatewayUrl();
|
|
100
|
+
const key = apiKey();
|
|
101
|
+
if (!url || !key || !sessionID) return;
|
|
95
102
|
|
|
96
103
|
// Unique per event, stable for a transport-level retry of this same POST.
|
|
97
|
-
const dedupeKey = `${sessionID}:${phase}:${Date.now()}
|
|
104
|
+
const dedupeKey = `${sessionID}:${phase}:${Date.now()}`;
|
|
98
105
|
|
|
99
|
-
const controller = new AbortController()
|
|
100
|
-
const timer = setTimeout(() => controller.abort(), TIMEOUT_MS)
|
|
106
|
+
const controller = new AbortController();
|
|
107
|
+
const timer = setTimeout(() => controller.abort(), TIMEOUT_MS);
|
|
101
108
|
try {
|
|
102
109
|
await fetch(`${url}/v1/agent-hooks`, {
|
|
103
|
-
method:
|
|
110
|
+
method: 'POST',
|
|
104
111
|
headers: {
|
|
105
|
-
|
|
112
|
+
'content-type': 'application/json',
|
|
106
113
|
authorization: `Bearer ${key}`,
|
|
107
|
-
|
|
114
|
+
'x-subconscious-client': 'pi',
|
|
108
115
|
},
|
|
109
116
|
body: JSON.stringify({
|
|
110
|
-
event:
|
|
117
|
+
event: 'conversation_compaction',
|
|
111
118
|
conversation_id: sessionID,
|
|
112
119
|
phase,
|
|
113
120
|
hook_event_name: hookEventName,
|
|
@@ -115,37 +122,50 @@ async function report(
|
|
|
115
122
|
metadata: metadata ?? undefined,
|
|
116
123
|
}),
|
|
117
124
|
signal: controller.signal,
|
|
118
|
-
})
|
|
125
|
+
});
|
|
119
126
|
} catch {
|
|
120
127
|
// Fail open on timeout, offline gateway, or auth failure.
|
|
121
128
|
} finally {
|
|
122
|
-
clearTimeout(timer)
|
|
129
|
+
clearTimeout(timer);
|
|
123
130
|
}
|
|
124
131
|
}
|
|
125
132
|
|
|
133
|
+
type PiCompactEvent = {
|
|
134
|
+
reason?: unknown;
|
|
135
|
+
preparation?: { tokensBefore?: unknown };
|
|
136
|
+
fromExtension?: unknown;
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
type PiContext = {
|
|
140
|
+
sessionManager?: { getSessionId?: () => string | undefined };
|
|
141
|
+
};
|
|
142
|
+
|
|
126
143
|
// Pi auto-discovers default-exported extension factories from
|
|
127
144
|
// ~/.pi/agent/extensions/*.ts. See https://pi.dev/docs/latest/extensions
|
|
128
145
|
export default function (pi: {
|
|
129
|
-
on: (
|
|
146
|
+
on: (
|
|
147
|
+
event: string,
|
|
148
|
+
handler: (event: PiCompactEvent, ctx: PiContext) => Promise<void>,
|
|
149
|
+
) => void;
|
|
130
150
|
}) {
|
|
131
|
-
pi.on(
|
|
132
|
-
const sessionID = ctx?.sessionManager?.getSessionId?.()
|
|
151
|
+
pi.on('session_before_compact', async (event, ctx) => {
|
|
152
|
+
const sessionID = ctx?.sessionManager?.getSessionId?.();
|
|
133
153
|
if (sessionID) {
|
|
134
|
-
await report(sessionID,
|
|
154
|
+
await report(sessionID, 'start', 'session_before_compact', {
|
|
135
155
|
reason: event?.reason ?? null,
|
|
136
156
|
tokens_before: event?.preparation?.tokensBefore ?? null,
|
|
137
|
-
})
|
|
157
|
+
});
|
|
138
158
|
}
|
|
139
159
|
// Deliberately return nothing: do not cancel or replace summarization.
|
|
140
|
-
})
|
|
160
|
+
});
|
|
141
161
|
|
|
142
|
-
pi.on(
|
|
143
|
-
const sessionID = ctx?.sessionManager?.getSessionId?.()
|
|
162
|
+
pi.on('session_compact', async (event, ctx) => {
|
|
163
|
+
const sessionID = ctx?.sessionManager?.getSessionId?.();
|
|
144
164
|
if (sessionID) {
|
|
145
|
-
await report(sessionID,
|
|
165
|
+
await report(sessionID, 'end', 'session_compact', {
|
|
146
166
|
reason: event?.reason ?? null,
|
|
147
167
|
from_extension: event?.fromExtension ?? null,
|
|
148
|
-
})
|
|
168
|
+
});
|
|
149
169
|
}
|
|
150
|
-
})
|
|
170
|
+
});
|
|
151
171
|
}
|
package/bin/sessions.js
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
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 { resolveAgent, runAgent } from './agents.js';
|
|
6
6
|
|
|
7
7
|
import { c } from './colors.js';
|
|
8
|
-
import {
|
|
8
|
+
import { spawnWindowsSync } from './windows/process.js';
|
|
9
9
|
|
|
10
10
|
const MAX_CANDIDATE_FILES = 160;
|
|
11
11
|
const MAX_READ_BYTES = 2 * 1024 * 1024;
|
|
@@ -26,7 +26,7 @@ export const SESSION_HARNESSES = Object.freeze({
|
|
|
26
26
|
|
|
27
27
|
function cleanText(value) {
|
|
28
28
|
return String(value || '')
|
|
29
|
-
.
|
|
29
|
+
.replaceAll('\0', '')
|
|
30
30
|
.replace(/\r\n/g, '\n')
|
|
31
31
|
.trim();
|
|
32
32
|
}
|
|
@@ -194,8 +194,15 @@ function codexMessages(records) {
|
|
|
194
194
|
return records.flatMap((record) => {
|
|
195
195
|
if (record.type !== 'event_msg') return [];
|
|
196
196
|
const eventType = record.payload?.type;
|
|
197
|
-
const role =
|
|
198
|
-
|
|
197
|
+
const role =
|
|
198
|
+
eventType === 'user_message'
|
|
199
|
+
? 'user'
|
|
200
|
+
: eventType === 'agent_message'
|
|
201
|
+
? 'assistant'
|
|
202
|
+
: '';
|
|
203
|
+
const text = cleanText(
|
|
204
|
+
record.payload?.message || record.payload?.last_agent_message,
|
|
205
|
+
);
|
|
199
206
|
return role && text ? [{ role, text }] : [];
|
|
200
207
|
});
|
|
201
208
|
}
|
|
@@ -227,12 +234,22 @@ async function parseFileSession(entry, harness) {
|
|
|
227
234
|
const messages = claudeMessages(records);
|
|
228
235
|
const metadata = records.find((record) => record.cwd) || {};
|
|
229
236
|
const identity = records.find((record) => record.sessionId) || {};
|
|
230
|
-
const id =
|
|
237
|
+
const id =
|
|
238
|
+
metadata.sessionId ||
|
|
239
|
+
identity.sessionId ||
|
|
240
|
+
filename.split('.orphaned-')[0];
|
|
231
241
|
const titleRecord = records.find((record) => record.type === 'ai-title');
|
|
232
|
-
const lastPrompt = [...records]
|
|
233
|
-
|
|
242
|
+
const lastPrompt = [...records]
|
|
243
|
+
.reverse()
|
|
244
|
+
.find((record) => record.type === 'last-prompt');
|
|
245
|
+
const assistant = records.find(
|
|
246
|
+
(record) => record.message?.role === 'assistant',
|
|
247
|
+
);
|
|
234
248
|
return sessionRecord(harness, id, {
|
|
235
|
-
title:
|
|
249
|
+
title:
|
|
250
|
+
titleRecord?.aiTitle ||
|
|
251
|
+
messages.find((message) => message.role === 'user')?.text ||
|
|
252
|
+
lastPrompt?.lastPrompt,
|
|
236
253
|
cwd: metadata.cwd,
|
|
237
254
|
model: assistant?.message?.model,
|
|
238
255
|
updated: entry.updated,
|
|
@@ -241,10 +258,14 @@ async function parseFileSession(entry, harness) {
|
|
|
241
258
|
}
|
|
242
259
|
|
|
243
260
|
if (harness === 'codex') {
|
|
244
|
-
const meta =
|
|
261
|
+
const meta =
|
|
262
|
+
records.find((record) => record.type === 'session_meta')?.payload || {};
|
|
245
263
|
const messages = codexMessages(records);
|
|
246
|
-
const id =
|
|
247
|
-
|
|
264
|
+
const id =
|
|
265
|
+
meta.id ||
|
|
266
|
+
filename.match(/([0-9a-f]{8}(?:-[0-9a-f]{4}){3}-[0-9a-f]{12})$/i)?.[1];
|
|
267
|
+
const context =
|
|
268
|
+
records.find((record) => record.type === 'turn_context')?.payload || {};
|
|
248
269
|
return sessionRecord(harness, id, {
|
|
249
270
|
title: messages.find((message) => message.role === 'user')?.text,
|
|
250
271
|
cwd: meta.cwd,
|
|
@@ -257,7 +278,9 @@ async function parseFileSession(entry, harness) {
|
|
|
257
278
|
if (harness === 'pi') {
|
|
258
279
|
const meta = records.find((record) => record.type === 'session') || {};
|
|
259
280
|
const messages = piMessages(records);
|
|
260
|
-
const assistant = records.find(
|
|
281
|
+
const assistant = records.find(
|
|
282
|
+
(record) => record.message?.role === 'assistant',
|
|
283
|
+
);
|
|
261
284
|
return sessionRecord(harness, meta.id || filename.split('_').at(-1), {
|
|
262
285
|
title: messages.find((message) => message.role === 'user')?.text,
|
|
263
286
|
cwd: meta.cwd,
|
|
@@ -294,11 +317,17 @@ async function discoverFileSessions(root, harness, limit) {
|
|
|
294
317
|
function sessionIdFromFile(file, harness) {
|
|
295
318
|
const name = path.basename(file, '.jsonl');
|
|
296
319
|
if (harness === 'claude') {
|
|
297
|
-
if (
|
|
320
|
+
if (
|
|
321
|
+
name.includes('.orphaned-') ||
|
|
322
|
+
file.split(path.sep).includes('subagents')
|
|
323
|
+
)
|
|
324
|
+
return '';
|
|
298
325
|
return name;
|
|
299
326
|
}
|
|
300
327
|
if (harness === 'codex') {
|
|
301
|
-
return
|
|
328
|
+
return (
|
|
329
|
+
name.match(/([0-9a-f]{8}(?:-[0-9a-f]{4}){3}-[0-9a-f]{12})$/i)?.[1] || ''
|
|
330
|
+
);
|
|
302
331
|
}
|
|
303
332
|
return '';
|
|
304
333
|
}
|
|
@@ -359,10 +388,16 @@ async function discoverCodexIndex(root, indexFile, max) {
|
|
|
359
388
|
const updated = Date.parse(record.updated_at || '') || 0;
|
|
360
389
|
const current = byID.get(record.id);
|
|
361
390
|
if (!current || updated >= current.updated) {
|
|
362
|
-
byID.set(record.id, {
|
|
391
|
+
byID.set(record.id, {
|
|
392
|
+
id: record.id,
|
|
393
|
+
title: record.thread_name,
|
|
394
|
+
updated,
|
|
395
|
+
});
|
|
363
396
|
}
|
|
364
397
|
}
|
|
365
|
-
const selected = [...byID.values()]
|
|
398
|
+
const selected = [...byID.values()]
|
|
399
|
+
.sort((a, b) => b.updated - a.updated)
|
|
400
|
+
.slice(0, max);
|
|
366
401
|
return selected.map((record) =>
|
|
367
402
|
sessionRecord('codex', record.id, {
|
|
368
403
|
...record,
|
|
@@ -400,7 +435,12 @@ function discoverOpenCodeSessions(execute, max) {
|
|
|
400
435
|
'from session where time_archived is null',
|
|
401
436
|
`order by time_updated desc limit ${Math.max(1, Math.min(500, max))}`,
|
|
402
437
|
].join(' ');
|
|
403
|
-
const output = commandResult(execute, 'opencode', [
|
|
438
|
+
const output = commandResult(execute, 'opencode', [
|
|
439
|
+
'db',
|
|
440
|
+
'--format',
|
|
441
|
+
'json',
|
|
442
|
+
query,
|
|
443
|
+
]);
|
|
404
444
|
if (!output) return [];
|
|
405
445
|
try {
|
|
406
446
|
return JSON.parse(output).map((record) =>
|
|
@@ -419,7 +459,9 @@ function discoverOpenCodeSessions(execute, max) {
|
|
|
419
459
|
export async function discoverSessions(options = {}) {
|
|
420
460
|
const home = options.home || os.homedir();
|
|
421
461
|
const max = options.max || 500;
|
|
422
|
-
const execute =
|
|
462
|
+
const execute =
|
|
463
|
+
options.execute ||
|
|
464
|
+
(process.platform === 'win32' ? spawnWindowsSync : spawnSync);
|
|
423
465
|
const roots = options.roots || {
|
|
424
466
|
claude: path.join(home, '.claude', 'projects'),
|
|
425
467
|
codex: path.join(home, '.codex', 'sessions'),
|
|
@@ -467,11 +509,19 @@ async function openCodeMessages(session, execute) {
|
|
|
467
509
|
"and json_extract(part.data, '$.type') = 'text'",
|
|
468
510
|
'order by message.time_created asc, part.time_created asc',
|
|
469
511
|
].join(' ');
|
|
470
|
-
const output = commandResult(execute, 'opencode', [
|
|
512
|
+
const output = commandResult(execute, 'opencode', [
|
|
513
|
+
'db',
|
|
514
|
+
'--format',
|
|
515
|
+
'json',
|
|
516
|
+
query,
|
|
517
|
+
]);
|
|
471
518
|
if (!output) return [];
|
|
472
519
|
try {
|
|
473
520
|
return JSON.parse(output)
|
|
474
|
-
.filter(
|
|
521
|
+
.filter(
|
|
522
|
+
(row) =>
|
|
523
|
+
['user', 'assistant'].includes(row.role) && cleanText(row.text),
|
|
524
|
+
)
|
|
475
525
|
.map((row) => ({ role: row.role, text: cleanText(row.text) }));
|
|
476
526
|
} catch {
|
|
477
527
|
return [];
|
|
@@ -480,7 +530,11 @@ async function openCodeMessages(session, execute) {
|
|
|
480
530
|
|
|
481
531
|
export async function readSessionMessages(session, options = {}) {
|
|
482
532
|
if (session.harness === 'opencode') {
|
|
483
|
-
return openCodeMessages(
|
|
533
|
+
return openCodeMessages(
|
|
534
|
+
session,
|
|
535
|
+
options.execute ||
|
|
536
|
+
(process.platform === 'win32' ? spawnWindowsSync : spawnSync),
|
|
537
|
+
);
|
|
484
538
|
}
|
|
485
539
|
if (!session.sourcePath) return [];
|
|
486
540
|
const records = parseLines(await readBounded(session.sourcePath));
|
|
@@ -492,11 +546,14 @@ export async function readSessionMessages(session, options = {}) {
|
|
|
492
546
|
}
|
|
493
547
|
|
|
494
548
|
async function hydrateSession(session) {
|
|
495
|
-
if (session.harness !== 'codex' || !session.sourcePath || session.cwd)
|
|
549
|
+
if (session.harness !== 'codex' || !session.sourcePath || session.cwd)
|
|
550
|
+
return session;
|
|
496
551
|
try {
|
|
497
552
|
const head = parseLines(await readHead(session.sourcePath));
|
|
498
|
-
const meta =
|
|
499
|
-
|
|
553
|
+
const meta =
|
|
554
|
+
head.find((item) => item.type === 'session_meta')?.payload || {};
|
|
555
|
+
const context =
|
|
556
|
+
head.find((item) => item.type === 'turn_context')?.payload || {};
|
|
500
557
|
return { ...session, cwd: meta.cwd || '', model: context.model || '' };
|
|
501
558
|
} catch {
|
|
502
559
|
return session;
|
|
@@ -518,7 +575,9 @@ export function buildHandoffPrompt(session, messages) {
|
|
|
518
575
|
}
|
|
519
576
|
selected.reverse();
|
|
520
577
|
if (!selected.length) {
|
|
521
|
-
throw new Error(
|
|
578
|
+
throw new Error(
|
|
579
|
+
`No portable user/assistant messages were found in ${session.harnessName}.`,
|
|
580
|
+
);
|
|
522
581
|
}
|
|
523
582
|
|
|
524
583
|
return [
|
|
@@ -539,7 +598,8 @@ export function nativeResumeArgs(session) {
|
|
|
539
598
|
if (session.harness === 'claude') return ['--resume', session.id];
|
|
540
599
|
if (session.harness === 'codex') return ['resume', session.id];
|
|
541
600
|
if (session.harness === 'opencode') return ['--session', session.id];
|
|
542
|
-
if (session.harness === 'pi')
|
|
601
|
+
if (session.harness === 'pi')
|
|
602
|
+
return ['--session', session.sourcePath || session.id];
|
|
543
603
|
if (session.harness === 'sc') return ['--resume', session.sourcePath];
|
|
544
604
|
throw new Error(`Native resume is not supported for ${session.harnessName}.`);
|
|
545
605
|
}
|
|
@@ -547,11 +607,14 @@ export function nativeResumeArgs(session) {
|
|
|
547
607
|
export function handoffLaunchArgs(targetHarness, prompt) {
|
|
548
608
|
if (targetHarness === 'opencode') return ['--prompt', prompt];
|
|
549
609
|
if (['claude', 'codex', 'pi'].includes(targetHarness)) return [prompt];
|
|
550
|
-
throw new Error(
|
|
610
|
+
throw new Error(
|
|
611
|
+
`${SESSION_HARNESSES[targetHarness]?.name || targetHarness} cannot start an interactive handoff yet.`,
|
|
612
|
+
);
|
|
551
613
|
}
|
|
552
614
|
|
|
553
615
|
function sessionByKey(sessions, key) {
|
|
554
|
-
if (!SESSION_KEY_PATTERN.test(key || ''))
|
|
616
|
+
if (!SESSION_KEY_PATTERN.test(key || ''))
|
|
617
|
+
throw new Error('Invalid session key.');
|
|
555
618
|
return sessions.find((session) => session.key === key) || null;
|
|
556
619
|
}
|
|
557
620
|
|
|
@@ -564,11 +627,15 @@ function formatWhen(timestamp) {
|
|
|
564
627
|
export function printSessions(sessions) {
|
|
565
628
|
console.log(`\n ${c.bold}Coding sessions${c.reset}\n`);
|
|
566
629
|
if (!sessions.length) {
|
|
567
|
-
console.log(
|
|
630
|
+
console.log(
|
|
631
|
+
` ${c.dim}No supported local coding sessions were found.${c.reset}\n`,
|
|
632
|
+
);
|
|
568
633
|
return;
|
|
569
634
|
}
|
|
570
635
|
for (const session of sessions) {
|
|
571
|
-
console.log(
|
|
636
|
+
console.log(
|
|
637
|
+
` ${c.cyan}${session.key}${c.reset} ${c.bold}${session.title}${c.reset}`,
|
|
638
|
+
);
|
|
572
639
|
console.log(
|
|
573
640
|
` ${c.dim}${session.harnessName} · ${formatWhen(session.updatedAt)}${session.cwd ? ` · ${session.cwd}` : ''}${c.reset}`,
|
|
574
641
|
);
|
|
@@ -585,15 +652,21 @@ export async function sessionsCommand(argv, options = {}) {
|
|
|
585
652
|
printSessions(sessions);
|
|
586
653
|
return 0;
|
|
587
654
|
}
|
|
588
|
-
if (action !== 'resume')
|
|
655
|
+
if (action !== 'resume')
|
|
656
|
+
throw new Error(`Unknown sessions action: ${action}`);
|
|
589
657
|
|
|
590
658
|
const key = argv[1];
|
|
591
659
|
let session = sessionByKey(sessions, key);
|
|
592
|
-
if (!session)
|
|
660
|
+
if (!session)
|
|
661
|
+
throw new Error(
|
|
662
|
+
`Session '${key}' was not found. Run subc sessions to refresh the list.`,
|
|
663
|
+
);
|
|
593
664
|
session = await hydrateSession(session);
|
|
594
665
|
const harnessIndex = argv.indexOf('--harness');
|
|
595
|
-
const requestedHarness =
|
|
596
|
-
|
|
666
|
+
const requestedHarness =
|
|
667
|
+
harnessIndex >= 0 ? argv[harnessIndex + 1] : session.harness;
|
|
668
|
+
const targetHarness =
|
|
669
|
+
requestedHarness === 'marathon' ? 'sc' : requestedHarness;
|
|
597
670
|
const target = SESSION_HARNESSES[targetHarness];
|
|
598
671
|
if (!target) throw new Error(`Unknown destination harness: ${targetHarness}`);
|
|
599
672
|
|
|
@@ -602,22 +675,34 @@ export async function sessionsCommand(argv, options = {}) {
|
|
|
602
675
|
const stat = await fs.stat(session.cwd);
|
|
603
676
|
if (stat.isDirectory()) process.chdir(session.cwd);
|
|
604
677
|
} catch {
|
|
605
|
-
console.error(
|
|
678
|
+
console.error(
|
|
679
|
+
` ${c.yellow}The original directory is unavailable; resuming in ${process.cwd()}.${c.reset}\n`,
|
|
680
|
+
);
|
|
606
681
|
}
|
|
607
682
|
}
|
|
608
683
|
|
|
609
684
|
const agent = resolveAgent(target.command);
|
|
610
|
-
if (!agent)
|
|
685
|
+
if (!agent)
|
|
686
|
+
throw new Error(`${target.name} is not registered with this CLI.`);
|
|
611
687
|
if (targetHarness === session.harness) {
|
|
612
|
-
console.log(
|
|
613
|
-
|
|
688
|
+
console.log(
|
|
689
|
+
`\n ${c.dim}Resuming in ${c.reset}${c.bold}${session.harnessName}${c.reset}${c.dim}: ${session.title}${c.reset}\n`,
|
|
690
|
+
);
|
|
691
|
+
return runAgent(agent, nativeResumeArgs(session), {
|
|
692
|
+
profile: options.profile,
|
|
693
|
+
});
|
|
614
694
|
}
|
|
615
|
-
if (!target.portable)
|
|
695
|
+
if (!target.portable)
|
|
696
|
+
throw new Error(
|
|
697
|
+
`${target.name} cannot receive cross-harness sessions yet.`,
|
|
698
|
+
);
|
|
616
699
|
|
|
617
700
|
const messages = await readSessionMessages(session, options);
|
|
618
701
|
const prompt = buildHandoffPrompt(session, messages);
|
|
619
702
|
console.log(
|
|
620
703
|
`\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
704
|
);
|
|
622
|
-
return runAgent(agent, handoffLaunchArgs(targetHarness, prompt), {
|
|
705
|
+
return runAgent(agent, handoffLaunchArgs(targetHarness, prompt), {
|
|
706
|
+
profile: options.profile,
|
|
707
|
+
});
|
|
623
708
|
}
|