@orgloop/agentctl 1.1.0 → 1.2.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 +145 -3
- package/dist/adapters/claude-code.js +9 -9
- package/dist/adapters/codex.d.ts +72 -0
- package/dist/adapters/codex.js +692 -0
- package/dist/adapters/openclaw.d.ts +60 -9
- package/dist/adapters/openclaw.js +195 -38
- package/dist/adapters/opencode.d.ts +143 -0
- package/dist/adapters/opencode.js +672 -0
- package/dist/adapters/pi-rust.d.ts +89 -0
- package/dist/adapters/pi-rust.js +743 -0
- package/dist/adapters/pi.d.ts +96 -0
- package/dist/adapters/pi.js +855 -0
- package/dist/cli.js +277 -59
- package/dist/core/types.d.ts +1 -0
- package/dist/daemon/server.js +34 -4
- package/dist/daemon/session-tracker.d.ts +11 -0
- package/dist/daemon/session-tracker.js +76 -4
- package/dist/daemon/state.d.ts +1 -0
- package/dist/hooks.d.ts +2 -0
- package/dist/hooks.js +4 -0
- package/dist/launch-orchestrator.d.ts +60 -0
- package/dist/launch-orchestrator.js +198 -0
- package/dist/matrix-parser.d.ts +40 -0
- package/dist/matrix-parser.js +69 -0
- package/dist/utils/partial-read.d.ts +20 -0
- package/dist/utils/partial-read.js +66 -0
- package/dist/worktree.d.ts +22 -0
- package/dist/worktree.js +68 -0
- package/package.json +3 -2
package/dist/cli.js
CHANGED
|
@@ -9,21 +9,34 @@ import path from "node:path";
|
|
|
9
9
|
import { fileURLToPath } from "node:url";
|
|
10
10
|
import { Command } from "commander";
|
|
11
11
|
import { ClaudeCodeAdapter } from "./adapters/claude-code.js";
|
|
12
|
+
import { CodexAdapter } from "./adapters/codex.js";
|
|
12
13
|
import { OpenClawAdapter } from "./adapters/openclaw.js";
|
|
14
|
+
import { OpenCodeAdapter } from "./adapters/opencode.js";
|
|
15
|
+
import { PiAdapter } from "./adapters/pi.js";
|
|
16
|
+
import { PiRustAdapter } from "./adapters/pi-rust.js";
|
|
13
17
|
import { DaemonClient } from "./client/daemon-client.js";
|
|
14
18
|
import { runHook } from "./hooks.js";
|
|
19
|
+
import { orchestrateLaunch, parseAdapterSlots, } from "./launch-orchestrator.js";
|
|
20
|
+
import { expandMatrix, parseMatrixFile } from "./matrix-parser.js";
|
|
15
21
|
import { mergeSession } from "./merge.js";
|
|
16
22
|
import { createWorktree } from "./worktree.js";
|
|
17
23
|
const adapters = {
|
|
18
24
|
"claude-code": new ClaudeCodeAdapter(),
|
|
25
|
+
codex: new CodexAdapter(),
|
|
19
26
|
openclaw: new OpenClawAdapter(),
|
|
27
|
+
opencode: new OpenCodeAdapter(),
|
|
28
|
+
pi: new PiAdapter(),
|
|
29
|
+
"pi-rust": new PiRustAdapter(),
|
|
20
30
|
};
|
|
21
31
|
const client = new DaemonClient();
|
|
22
32
|
/**
|
|
23
33
|
* Ensure the daemon is running. Auto-starts it if not.
|
|
24
34
|
* Returns true if daemon is available after the call.
|
|
35
|
+
* Set AGENTCTL_NO_DAEMON=1 to skip daemon and use direct adapter mode.
|
|
25
36
|
*/
|
|
26
37
|
async function ensureDaemon() {
|
|
38
|
+
if (process.env.AGENTCTL_NO_DAEMON === "1")
|
|
39
|
+
return false;
|
|
27
40
|
if (await client.isRunning())
|
|
28
41
|
return true;
|
|
29
42
|
// Auto-start daemon in background
|
|
@@ -67,27 +80,33 @@ function getAllAdapters() {
|
|
|
67
80
|
return Object.values(adapters);
|
|
68
81
|
}
|
|
69
82
|
// --- Formatters ---
|
|
70
|
-
function formatSession(s) {
|
|
71
|
-
|
|
83
|
+
function formatSession(s, showGroup) {
|
|
84
|
+
const row = {
|
|
72
85
|
ID: s.id.slice(0, 8),
|
|
73
86
|
Status: s.status,
|
|
74
87
|
Model: s.model || "-",
|
|
75
|
-
CWD: s.cwd ? shortenPath(s.cwd) : "-",
|
|
76
|
-
PID: s.pid?.toString() || "-",
|
|
77
|
-
Started: timeAgo(s.startedAt),
|
|
78
|
-
Prompt: (s.prompt || "-").slice(0, 60),
|
|
79
88
|
};
|
|
89
|
+
if (showGroup)
|
|
90
|
+
row.Group = s.group || "-";
|
|
91
|
+
row.CWD = s.cwd ? shortenPath(s.cwd) : "-";
|
|
92
|
+
row.PID = s.pid?.toString() || "-";
|
|
93
|
+
row.Started = timeAgo(s.startedAt);
|
|
94
|
+
row.Prompt = (s.prompt || "-").slice(0, 60);
|
|
95
|
+
return row;
|
|
80
96
|
}
|
|
81
|
-
function formatRecord(s) {
|
|
82
|
-
|
|
97
|
+
function formatRecord(s, showGroup) {
|
|
98
|
+
const row = {
|
|
83
99
|
ID: s.id.slice(0, 8),
|
|
84
100
|
Status: s.status,
|
|
85
101
|
Model: s.model || "-",
|
|
86
|
-
CWD: s.cwd ? shortenPath(s.cwd) : "-",
|
|
87
|
-
PID: s.pid?.toString() || "-",
|
|
88
|
-
Started: timeAgo(new Date(s.startedAt)),
|
|
89
|
-
Prompt: (s.prompt || "-").slice(0, 60),
|
|
90
102
|
};
|
|
103
|
+
if (showGroup)
|
|
104
|
+
row.Group = s.group || "-";
|
|
105
|
+
row.CWD = s.cwd ? shortenPath(s.cwd) : "-";
|
|
106
|
+
row.PID = s.pid?.toString() || "-";
|
|
107
|
+
row.Started = timeAgo(new Date(s.startedAt));
|
|
108
|
+
row.Prompt = (s.prompt || "-").slice(0, 60);
|
|
109
|
+
return row;
|
|
91
110
|
}
|
|
92
111
|
function shortenPath(p) {
|
|
93
112
|
const home = process.env.HOME || "";
|
|
@@ -153,6 +172,7 @@ function sessionToJson(s) {
|
|
|
153
172
|
tokens: s.tokens,
|
|
154
173
|
cost: s.cost,
|
|
155
174
|
pid: s.pid,
|
|
175
|
+
group: s.group,
|
|
156
176
|
meta: s.meta,
|
|
157
177
|
};
|
|
158
178
|
}
|
|
@@ -168,20 +188,27 @@ program
|
|
|
168
188
|
.description("List agent sessions")
|
|
169
189
|
.option("--adapter <name>", "Filter by adapter")
|
|
170
190
|
.option("--status <status>", "Filter by status (running|stopped|idle|error)")
|
|
191
|
+
.option("--group <id>", "Filter by launch group (e.g. g-a1b2c3)")
|
|
171
192
|
.option("-a, --all", "Include stopped sessions (last 7 days)")
|
|
172
193
|
.option("--json", "Output as JSON")
|
|
173
194
|
.action(async (opts) => {
|
|
174
195
|
const daemonRunning = await ensureDaemon();
|
|
175
196
|
if (daemonRunning) {
|
|
176
|
-
|
|
197
|
+
let sessions = await client.call("session.list", {
|
|
177
198
|
status: opts.status,
|
|
178
199
|
all: opts.all,
|
|
200
|
+
adapter: opts.adapter,
|
|
201
|
+
group: opts.group,
|
|
179
202
|
});
|
|
203
|
+
if (opts.adapter) {
|
|
204
|
+
sessions = sessions.filter((s) => s.adapter === opts.adapter);
|
|
205
|
+
}
|
|
180
206
|
if (opts.json) {
|
|
181
207
|
printJson(sessions);
|
|
182
208
|
}
|
|
183
209
|
else {
|
|
184
|
-
|
|
210
|
+
const hasGroups = sessions.some((s) => s.group);
|
|
211
|
+
printTable(sessions.map((s) => formatRecord(s, hasGroups)));
|
|
185
212
|
}
|
|
186
213
|
return;
|
|
187
214
|
}
|
|
@@ -202,7 +229,8 @@ program
|
|
|
202
229
|
printJson(sessions.map(sessionToJson));
|
|
203
230
|
}
|
|
204
231
|
else {
|
|
205
|
-
|
|
232
|
+
const hasGroups = sessions.some((s) => s.group);
|
|
233
|
+
printTable(sessions.map((s) => formatSession(s, hasGroups)));
|
|
206
234
|
}
|
|
207
235
|
});
|
|
208
236
|
// status
|
|
@@ -222,7 +250,7 @@ program
|
|
|
222
250
|
printJson(session);
|
|
223
251
|
}
|
|
224
252
|
else {
|
|
225
|
-
const fmt = formatRecord(session);
|
|
253
|
+
const fmt = formatRecord(session, !!session.group);
|
|
226
254
|
for (const [k, v] of Object.entries(fmt)) {
|
|
227
255
|
console.log(`${k.padEnd(10)} ${v}`);
|
|
228
256
|
}
|
|
@@ -232,32 +260,37 @@ program
|
|
|
232
260
|
}
|
|
233
261
|
return;
|
|
234
262
|
}
|
|
235
|
-
catch
|
|
236
|
-
|
|
237
|
-
process.exit(1);
|
|
263
|
+
catch {
|
|
264
|
+
// Daemon failed — fall through to direct adapter lookup
|
|
238
265
|
}
|
|
239
266
|
}
|
|
240
|
-
// Direct fallback
|
|
241
|
-
const
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
for (const [k, v] of Object.entries(fmt)) {
|
|
250
|
-
console.log(`${k.padEnd(10)} ${v}`);
|
|
267
|
+
// Direct fallback: try specified adapter, or search all adapters
|
|
268
|
+
const statusAdapters = opts.adapter
|
|
269
|
+
? [getAdapter(opts.adapter)]
|
|
270
|
+
: getAllAdapters();
|
|
271
|
+
for (const adapter of statusAdapters) {
|
|
272
|
+
try {
|
|
273
|
+
const session = await adapter.status(id);
|
|
274
|
+
if (opts.json) {
|
|
275
|
+
printJson(sessionToJson(session));
|
|
251
276
|
}
|
|
252
|
-
|
|
253
|
-
|
|
277
|
+
else {
|
|
278
|
+
const fmt = formatSession(session, !!session.group);
|
|
279
|
+
for (const [k, v] of Object.entries(fmt)) {
|
|
280
|
+
console.log(`${k.padEnd(10)} ${v}`);
|
|
281
|
+
}
|
|
282
|
+
if (session.tokens) {
|
|
283
|
+
console.log(`Tokens in: ${session.tokens.in}, out: ${session.tokens.out}`);
|
|
284
|
+
}
|
|
254
285
|
}
|
|
286
|
+
return;
|
|
287
|
+
}
|
|
288
|
+
catch {
|
|
289
|
+
// Try next adapter
|
|
255
290
|
}
|
|
256
291
|
}
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
process.exit(1);
|
|
260
|
-
}
|
|
292
|
+
console.error(`Session not found: ${id}`);
|
|
293
|
+
process.exit(1);
|
|
261
294
|
});
|
|
262
295
|
// peek
|
|
263
296
|
program
|
|
@@ -276,22 +309,39 @@ program
|
|
|
276
309
|
console.log(output);
|
|
277
310
|
return;
|
|
278
311
|
}
|
|
312
|
+
catch {
|
|
313
|
+
// Daemon failed — fall through to direct adapter lookup
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
// Direct fallback: try specified adapter, or search all adapters
|
|
317
|
+
if (opts.adapter) {
|
|
318
|
+
const adapter = getAdapter(opts.adapter);
|
|
319
|
+
try {
|
|
320
|
+
const output = await adapter.peek(id, {
|
|
321
|
+
lines: Number.parseInt(opts.lines, 10),
|
|
322
|
+
});
|
|
323
|
+
console.log(output);
|
|
324
|
+
return;
|
|
325
|
+
}
|
|
279
326
|
catch (err) {
|
|
280
327
|
console.error(err.message);
|
|
281
328
|
process.exit(1);
|
|
282
329
|
}
|
|
283
330
|
}
|
|
284
|
-
const adapter
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
331
|
+
for (const adapter of getAllAdapters()) {
|
|
332
|
+
try {
|
|
333
|
+
const output = await adapter.peek(id, {
|
|
334
|
+
lines: Number.parseInt(opts.lines, 10),
|
|
335
|
+
});
|
|
336
|
+
console.log(output);
|
|
337
|
+
return;
|
|
338
|
+
}
|
|
339
|
+
catch {
|
|
340
|
+
// Try next adapter
|
|
341
|
+
}
|
|
294
342
|
}
|
|
343
|
+
console.error(`Session not found: ${id}`);
|
|
344
|
+
process.exit(1);
|
|
295
345
|
});
|
|
296
346
|
// stop
|
|
297
347
|
program
|
|
@@ -356,7 +406,7 @@ program
|
|
|
356
406
|
// launch
|
|
357
407
|
program
|
|
358
408
|
.command("launch [adapter]")
|
|
359
|
-
.description("Launch a new agent session")
|
|
409
|
+
.description("Launch a new agent session (or multiple with --adapter flags)")
|
|
360
410
|
.requiredOption("-p, --prompt <text>", "Prompt to send")
|
|
361
411
|
.option("--spec <path>", "Spec file path")
|
|
362
412
|
.option("--cwd <dir>", "Working directory")
|
|
@@ -364,13 +414,108 @@ program
|
|
|
364
414
|
.option("--force", "Override directory locks")
|
|
365
415
|
.option("--worktree <repo>", "Auto-create git worktree from this repo before launch")
|
|
366
416
|
.option("--branch <name>", "Branch name for --worktree")
|
|
417
|
+
.option("--adapter <name>", "Adapter to launch (repeatable for parallel launch)", collectAdapter, [])
|
|
418
|
+
.option("--matrix <file>", "YAML matrix file for advanced sweep launch")
|
|
367
419
|
.option("--on-create <script>", "Hook: run after session is created")
|
|
368
420
|
.option("--on-complete <script>", "Hook: run after session completes")
|
|
369
421
|
.option("--pre-merge <script>", "Hook: run before merge")
|
|
370
422
|
.option("--post-merge <script>", "Hook: run after merge")
|
|
423
|
+
.allowUnknownOption() // Allow interleaved --adapter/--model for parseAdapterSlots
|
|
371
424
|
.action(async (adapterName, opts) => {
|
|
372
425
|
let cwd = opts.cwd ? path.resolve(opts.cwd) : process.cwd();
|
|
373
|
-
|
|
426
|
+
// Collect hooks
|
|
427
|
+
const hooks = opts.onCreate || opts.onComplete || opts.preMerge || opts.postMerge
|
|
428
|
+
? {
|
|
429
|
+
onCreate: opts.onCreate,
|
|
430
|
+
onComplete: opts.onComplete,
|
|
431
|
+
preMerge: opts.preMerge,
|
|
432
|
+
postMerge: opts.postMerge,
|
|
433
|
+
}
|
|
434
|
+
: undefined;
|
|
435
|
+
// --- Multi-adapter / matrix detection ---
|
|
436
|
+
let slots = [];
|
|
437
|
+
if (opts.matrix) {
|
|
438
|
+
// Matrix file mode
|
|
439
|
+
try {
|
|
440
|
+
const matrixFile = await parseMatrixFile(opts.matrix);
|
|
441
|
+
slots = expandMatrix(matrixFile);
|
|
442
|
+
// Matrix can override cwd and prompt
|
|
443
|
+
if (matrixFile.cwd)
|
|
444
|
+
cwd = path.resolve(matrixFile.cwd);
|
|
445
|
+
}
|
|
446
|
+
catch (err) {
|
|
447
|
+
console.error(`Failed to parse matrix file: ${err.message}`);
|
|
448
|
+
process.exit(1);
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
else {
|
|
452
|
+
// Check for multi-adapter via raw argv parsing
|
|
453
|
+
// We need raw argv because commander can't handle interleaved
|
|
454
|
+
// --adapter A --model M1 --adapter B --model M2
|
|
455
|
+
const rawArgs = process.argv.slice(2);
|
|
456
|
+
const adapterCount = rawArgs.filter((a) => a === "--adapter" || a === "-A").length;
|
|
457
|
+
if (adapterCount > 1) {
|
|
458
|
+
// Multi-adapter mode: parse from raw args
|
|
459
|
+
slots = parseAdapterSlots(rawArgs);
|
|
460
|
+
}
|
|
461
|
+
else if (adapterCount === 1 && opts.adapter?.length === 1) {
|
|
462
|
+
// Single --adapter flag — could still be multi if model is specified
|
|
463
|
+
// but this is the normal single-adapter path via --adapter flag
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
// --- Parallel launch path ---
|
|
467
|
+
if (slots.length > 1) {
|
|
468
|
+
const daemonRunning = await ensureDaemon();
|
|
469
|
+
try {
|
|
470
|
+
let groupId = "";
|
|
471
|
+
const result = await orchestrateLaunch({
|
|
472
|
+
slots,
|
|
473
|
+
prompt: opts.prompt,
|
|
474
|
+
spec: opts.spec,
|
|
475
|
+
cwd,
|
|
476
|
+
hooks,
|
|
477
|
+
adapters,
|
|
478
|
+
onSessionLaunched: (slotResult) => {
|
|
479
|
+
// Track in daemon if available
|
|
480
|
+
if (daemonRunning && !slotResult.error) {
|
|
481
|
+
client
|
|
482
|
+
.call("session.launch.track", {
|
|
483
|
+
id: slotResult.sessionId,
|
|
484
|
+
adapter: slotResult.slot.adapter,
|
|
485
|
+
cwd: slotResult.cwd,
|
|
486
|
+
group: groupId,
|
|
487
|
+
})
|
|
488
|
+
.catch(() => {
|
|
489
|
+
// Best effort — session will be picked up by poll
|
|
490
|
+
});
|
|
491
|
+
}
|
|
492
|
+
},
|
|
493
|
+
onGroupCreated: (id) => {
|
|
494
|
+
groupId = id;
|
|
495
|
+
},
|
|
496
|
+
});
|
|
497
|
+
console.log(`\nLaunched ${result.results.length} sessions (group: ${result.groupId}):`);
|
|
498
|
+
for (const r of result.results) {
|
|
499
|
+
const label = r.slot.model
|
|
500
|
+
? `${r.slot.adapter} (${r.slot.model})`
|
|
501
|
+
: r.slot.adapter;
|
|
502
|
+
if (r.error) {
|
|
503
|
+
console.log(` ✗ ${label} — ${r.error}`);
|
|
504
|
+
}
|
|
505
|
+
else {
|
|
506
|
+
console.log(` ${label} → ${shortenPath(r.cwd)} (${r.sessionId.slice(0, 8)})`);
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
catch (err) {
|
|
511
|
+
console.error(`Parallel launch failed: ${err.message}`);
|
|
512
|
+
process.exit(1);
|
|
513
|
+
}
|
|
514
|
+
return;
|
|
515
|
+
}
|
|
516
|
+
// --- Single adapter launch path (original behavior) ---
|
|
517
|
+
const name = slots.length === 1 ? slots[0].adapter : adapterName || "claude-code";
|
|
518
|
+
const model = slots.length === 1 && slots[0].model ? slots[0].model : opts.model;
|
|
374
519
|
// FEAT-1: Worktree lifecycle
|
|
375
520
|
let worktreeInfo;
|
|
376
521
|
if (opts.worktree) {
|
|
@@ -391,15 +536,6 @@ program
|
|
|
391
536
|
process.exit(1);
|
|
392
537
|
}
|
|
393
538
|
}
|
|
394
|
-
// Collect hooks
|
|
395
|
-
const hooks = opts.onCreate || opts.onComplete || opts.preMerge || opts.postMerge
|
|
396
|
-
? {
|
|
397
|
-
onCreate: opts.onCreate,
|
|
398
|
-
onComplete: opts.onComplete,
|
|
399
|
-
preMerge: opts.preMerge,
|
|
400
|
-
postMerge: opts.postMerge,
|
|
401
|
-
}
|
|
402
|
-
: undefined;
|
|
403
539
|
const daemonRunning = await ensureDaemon();
|
|
404
540
|
if (daemonRunning) {
|
|
405
541
|
try {
|
|
@@ -408,7 +544,7 @@ program
|
|
|
408
544
|
prompt: opts.prompt,
|
|
409
545
|
cwd,
|
|
410
546
|
spec: opts.spec,
|
|
411
|
-
model
|
|
547
|
+
model,
|
|
412
548
|
force: opts.force,
|
|
413
549
|
worktree: worktreeInfo
|
|
414
550
|
? { repo: worktreeInfo.repo, branch: worktreeInfo.branch }
|
|
@@ -443,7 +579,7 @@ program
|
|
|
443
579
|
prompt: opts.prompt,
|
|
444
580
|
spec: opts.spec,
|
|
445
581
|
cwd,
|
|
446
|
-
model
|
|
582
|
+
model,
|
|
447
583
|
hooks,
|
|
448
584
|
});
|
|
449
585
|
console.log(`Launched session ${session.id.slice(0, 8)} (PID: ${session.pid})`);
|
|
@@ -462,6 +598,10 @@ program
|
|
|
462
598
|
process.exit(1);
|
|
463
599
|
}
|
|
464
600
|
});
|
|
601
|
+
/** Commander collect callback for repeatable --adapter */
|
|
602
|
+
function collectAdapter(value, previous) {
|
|
603
|
+
return previous.concat([value]);
|
|
604
|
+
}
|
|
465
605
|
// events
|
|
466
606
|
program
|
|
467
607
|
.command("events")
|
|
@@ -560,6 +700,84 @@ program
|
|
|
560
700
|
});
|
|
561
701
|
}
|
|
562
702
|
});
|
|
703
|
+
// --- Worktree subcommand ---
|
|
704
|
+
const worktreeCmd = new Command("worktree").description("Manage agentctl-created worktrees");
|
|
705
|
+
worktreeCmd
|
|
706
|
+
.command("list")
|
|
707
|
+
.description("List git worktrees for a repo")
|
|
708
|
+
.argument("<repo>", "Path to the main repo")
|
|
709
|
+
.option("--json", "Output as JSON")
|
|
710
|
+
.action(async (repo, opts) => {
|
|
711
|
+
const { listWorktrees } = await import("./worktree.js");
|
|
712
|
+
try {
|
|
713
|
+
const entries = await listWorktrees(repo);
|
|
714
|
+
// Filter to only non-bare worktrees (exclude the main worktree)
|
|
715
|
+
const worktrees = entries.filter((e) => !e.bare);
|
|
716
|
+
if (opts.json) {
|
|
717
|
+
printJson(worktrees);
|
|
718
|
+
return;
|
|
719
|
+
}
|
|
720
|
+
if (worktrees.length === 0) {
|
|
721
|
+
console.log("No worktrees found.");
|
|
722
|
+
return;
|
|
723
|
+
}
|
|
724
|
+
printTable(worktrees.map((e) => ({
|
|
725
|
+
Path: shortenPath(e.path),
|
|
726
|
+
Branch: e.branch || "-",
|
|
727
|
+
HEAD: e.head?.slice(0, 8) || "-",
|
|
728
|
+
})));
|
|
729
|
+
}
|
|
730
|
+
catch (err) {
|
|
731
|
+
console.error(err.message);
|
|
732
|
+
process.exit(1);
|
|
733
|
+
}
|
|
734
|
+
});
|
|
735
|
+
worktreeCmd
|
|
736
|
+
.command("clean")
|
|
737
|
+
.description("Remove a worktree and optionally its branch")
|
|
738
|
+
.argument("<path>", "Path to the worktree to remove")
|
|
739
|
+
.option("--repo <path>", "Main repo path (auto-detected if omitted)")
|
|
740
|
+
.option("--delete-branch", "Also delete the worktree's branch")
|
|
741
|
+
.action(async (worktreePath, opts) => {
|
|
742
|
+
const { cleanWorktree } = await import("./worktree.js");
|
|
743
|
+
const absPath = path.resolve(worktreePath);
|
|
744
|
+
let repo = opts.repo;
|
|
745
|
+
// Auto-detect repo from the worktree's .git file
|
|
746
|
+
if (!repo) {
|
|
747
|
+
try {
|
|
748
|
+
const gitFile = await fs.readFile(path.join(absPath, ".git"), "utf-8");
|
|
749
|
+
// .git file contains: gitdir: /path/to/repo/.git/worktrees/<name>
|
|
750
|
+
const match = gitFile.match(/gitdir:\s*(.+)/);
|
|
751
|
+
if (match) {
|
|
752
|
+
const gitDir = match[1].trim();
|
|
753
|
+
// Navigate up from .git/worktrees/<name> to the repo root
|
|
754
|
+
repo = path.resolve(gitDir, "..", "..", "..");
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
catch {
|
|
758
|
+
console.error("Cannot auto-detect repo. Use --repo to specify the main repository.");
|
|
759
|
+
process.exit(1);
|
|
760
|
+
}
|
|
761
|
+
}
|
|
762
|
+
if (!repo) {
|
|
763
|
+
console.error("Cannot determine repo path. Use --repo.");
|
|
764
|
+
process.exit(1);
|
|
765
|
+
}
|
|
766
|
+
try {
|
|
767
|
+
const result = await cleanWorktree(repo, absPath, {
|
|
768
|
+
deleteBranch: opts.deleteBranch,
|
|
769
|
+
});
|
|
770
|
+
console.log(`Removed worktree: ${shortenPath(result.removedPath)}`);
|
|
771
|
+
if (result.deletedBranch) {
|
|
772
|
+
console.log(`Deleted branch: ${result.deletedBranch}`);
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
catch (err) {
|
|
776
|
+
console.error(err.message);
|
|
777
|
+
process.exit(1);
|
|
778
|
+
}
|
|
779
|
+
});
|
|
780
|
+
program.addCommand(worktreeCmd);
|
|
563
781
|
// --- Lock commands ---
|
|
564
782
|
program
|
|
565
783
|
.command("lock <directory>")
|
package/dist/core/types.d.ts
CHANGED
package/dist/daemon/server.js
CHANGED
|
@@ -5,7 +5,11 @@ import net from "node:net";
|
|
|
5
5
|
import os from "node:os";
|
|
6
6
|
import path from "node:path";
|
|
7
7
|
import { ClaudeCodeAdapter } from "../adapters/claude-code.js";
|
|
8
|
+
import { CodexAdapter } from "../adapters/codex.js";
|
|
8
9
|
import { OpenClawAdapter } from "../adapters/openclaw.js";
|
|
10
|
+
import { OpenCodeAdapter } from "../adapters/opencode.js";
|
|
11
|
+
import { PiAdapter } from "../adapters/pi.js";
|
|
12
|
+
import { PiRustAdapter } from "../adapters/pi-rust.js";
|
|
9
13
|
import { migrateLocks } from "../migration/migrate-locks.js";
|
|
10
14
|
import { FuseEngine } from "./fuse-engine.js";
|
|
11
15
|
import { LockManager } from "./lock-manager.js";
|
|
@@ -32,7 +36,11 @@ export async function startDaemon(opts = {}) {
|
|
|
32
36
|
// 5. Initialize subsystems
|
|
33
37
|
const adapters = opts.adapters || {
|
|
34
38
|
"claude-code": new ClaudeCodeAdapter(),
|
|
39
|
+
codex: new CodexAdapter(),
|
|
35
40
|
openclaw: new OpenClawAdapter(),
|
|
41
|
+
opencode: new OpenCodeAdapter(),
|
|
42
|
+
pi: new PiAdapter(),
|
|
43
|
+
"pi-rust": new PiRustAdapter(),
|
|
36
44
|
};
|
|
37
45
|
const lockManager = new LockManager(state);
|
|
38
46
|
const emitter = new EventEmitter();
|
|
@@ -145,11 +153,16 @@ function createRequestHandler(ctx) {
|
|
|
145
153
|
return async (req) => {
|
|
146
154
|
const params = (req.params || {});
|
|
147
155
|
switch (req.method) {
|
|
148
|
-
case "session.list":
|
|
149
|
-
|
|
156
|
+
case "session.list": {
|
|
157
|
+
let sessions = ctx.sessionTracker.listSessions({
|
|
150
158
|
status: params.status,
|
|
151
159
|
all: params.all,
|
|
152
160
|
});
|
|
161
|
+
if (params.group) {
|
|
162
|
+
sessions = sessions.filter((s) => s.group === params.group);
|
|
163
|
+
}
|
|
164
|
+
return sessions;
|
|
165
|
+
}
|
|
153
166
|
case "session.status": {
|
|
154
167
|
const session = ctx.sessionTracker.getSession(params.id);
|
|
155
168
|
if (!session)
|
|
@@ -157,11 +170,15 @@ function createRequestHandler(ctx) {
|
|
|
157
170
|
return session;
|
|
158
171
|
}
|
|
159
172
|
case "session.peek": {
|
|
160
|
-
|
|
173
|
+
// Auto-detect adapter from tracked session, fall back to param or claude-code
|
|
174
|
+
const tracked = ctx.sessionTracker.getSession(params.id);
|
|
175
|
+
const adapterName = params.adapter || tracked?.adapter || "claude-code";
|
|
161
176
|
const adapter = ctx.adapters[adapterName];
|
|
162
177
|
if (!adapter)
|
|
163
178
|
throw new Error(`Unknown adapter: ${adapterName}`);
|
|
164
|
-
|
|
179
|
+
// Use the full session ID if we resolved it from the tracker
|
|
180
|
+
const peekId = tracked?.id || params.id;
|
|
181
|
+
return adapter.peek(peekId, {
|
|
165
182
|
lines: params.lines,
|
|
166
183
|
});
|
|
167
184
|
}
|
|
@@ -193,6 +210,10 @@ function createRequestHandler(ctx) {
|
|
|
193
210
|
env: params.env,
|
|
194
211
|
adapterOpts: params.adapterOpts,
|
|
195
212
|
});
|
|
213
|
+
// Propagate group tag if provided
|
|
214
|
+
if (params.group) {
|
|
215
|
+
session.group = params.group;
|
|
216
|
+
}
|
|
196
217
|
const record = ctx.sessionTracker.track(session, adapterName);
|
|
197
218
|
// Auto-lock
|
|
198
219
|
if (cwd) {
|
|
@@ -204,6 +225,15 @@ function createRequestHandler(ctx) {
|
|
|
204
225
|
const session = ctx.sessionTracker.getSession(params.id);
|
|
205
226
|
if (!session)
|
|
206
227
|
throw new Error(`Session not found: ${params.id}`);
|
|
228
|
+
// Ghost pending entry with dead PID: remove from state with --force
|
|
229
|
+
if (session.id.startsWith("pending-") &&
|
|
230
|
+
params.force &&
|
|
231
|
+
session.pid &&
|
|
232
|
+
!isProcessAlive(session.pid)) {
|
|
233
|
+
ctx.lockManager.autoUnlock(session.id);
|
|
234
|
+
ctx.sessionTracker.removeSession(session.id);
|
|
235
|
+
return null;
|
|
236
|
+
}
|
|
207
237
|
const adapter = ctx.adapters[session.adapter];
|
|
208
238
|
if (!adapter)
|
|
209
239
|
throw new Error(`Unknown adapter: ${session.adapter}`);
|
|
@@ -11,9 +11,12 @@ export declare class SessionTracker {
|
|
|
11
11
|
private adapters;
|
|
12
12
|
private pollIntervalMs;
|
|
13
13
|
private pollHandle;
|
|
14
|
+
private polling;
|
|
14
15
|
private readonly isProcessAlive;
|
|
15
16
|
constructor(state: StateManager, opts: SessionTrackerOpts);
|
|
16
17
|
startPolling(): void;
|
|
18
|
+
/** Run poll() with a guard to skip if the previous cycle is still running */
|
|
19
|
+
private guardedPoll;
|
|
17
20
|
stopPolling(): void;
|
|
18
21
|
private poll;
|
|
19
22
|
/**
|
|
@@ -22,6 +25,11 @@ export declare class SessionTracker {
|
|
|
22
25
|
* - Any "running"/"idle" session in state whose PID is dead → mark stopped
|
|
23
26
|
*/
|
|
24
27
|
private reapStaleEntries;
|
|
28
|
+
/**
|
|
29
|
+
* Remove stopped sessions from state that have been stopped for more than 7 days.
|
|
30
|
+
* This reduces overhead from accumulating hundreds of historical sessions.
|
|
31
|
+
*/
|
|
32
|
+
private pruneOldSessions;
|
|
25
33
|
/** Track a newly launched session */
|
|
26
34
|
track(session: AgentSession, adapterName: string): SessionRecord;
|
|
27
35
|
/** Get session record by id (exact or prefix) */
|
|
@@ -30,8 +38,11 @@ export declare class SessionTracker {
|
|
|
30
38
|
listSessions(opts?: {
|
|
31
39
|
status?: string;
|
|
32
40
|
all?: boolean;
|
|
41
|
+
adapter?: string;
|
|
33
42
|
}): SessionRecord[];
|
|
34
43
|
activeCount(): number;
|
|
44
|
+
/** Remove a session from state entirely (used for ghost cleanup) */
|
|
45
|
+
removeSession(sessionId: string): void;
|
|
35
46
|
/** Called when a session stops — returns the cwd for fuse/lock processing */
|
|
36
47
|
onSessionExit(sessionId: string): SessionRecord | undefined;
|
|
37
48
|
}
|