copilot-agent 0.7.0 → 0.8.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/dist/index.js CHANGED
@@ -1,3 +1,5 @@
1
+ #!/usr/bin/env node
2
+
1
3
  // src/index.ts
2
4
  import { Command } from "commander";
3
5
 
@@ -8,9 +10,11 @@ import {
8
10
  readFileSync,
9
11
  statSync
10
12
  } from "fs";
11
- import { join, resolve } from "path";
13
+ import { join, resolve, basename } from "path";
12
14
  import { homedir } from "os";
13
15
  var SESSION_DIR = join(homedir(), ".copilot", "session-state");
16
+ var CLAUDE_DIR = join(homedir(), ".claude");
17
+ var CLAUDE_PROJECTS_DIR = join(CLAUDE_DIR, "projects");
14
18
  function validateSession(sid) {
15
19
  const events = join(SESSION_DIR, sid, "events.jsonl");
16
20
  try {
@@ -41,7 +45,8 @@ function listSessions(limit = 20) {
41
45
  premiumRequests: getSessionPremium(s.id),
42
46
  summary: getSessionSummary(s.id),
43
47
  cwd: getSessionCwd(s.id),
44
- complete: hasTaskComplete(s.id)
48
+ complete: hasTaskComplete(s.id),
49
+ agent: "copilot"
45
50
  }));
46
51
  }
47
52
  function getLatestSessionId() {
@@ -123,13 +128,6 @@ function getSessionSummary(sid) {
123
128
  function getSessionCwd(sid) {
124
129
  return readWorkspace(sid).cwd ?? "";
125
130
  }
126
- function findLatestIncomplete() {
127
- const sessions = listSessions(50);
128
- for (const s of sessions) {
129
- if (!s.complete) return s.id;
130
- }
131
- return null;
132
- }
133
131
  function getSessionReport(sid) {
134
132
  if (!validateSession(sid)) return null;
135
133
  const ws = readWorkspace(sid);
@@ -156,7 +154,8 @@ function getSessionReport(sid) {
156
154
  filesCreated: [],
157
155
  filesEdited: [],
158
156
  errors: [],
159
- taskCompletions: []
157
+ taskCompletions: [],
158
+ agent: "copilot"
160
159
  };
161
160
  for (const line of lines) {
162
161
  let event;
@@ -226,6 +225,245 @@ function getSessionReport(sid) {
226
225
  }
227
226
  return report;
228
227
  }
228
+ function decodeClaudePath(encoded) {
229
+ return encoded.replace(/^-/, "/").replace(/-/g, "/");
230
+ }
231
+ function encodeClaudePath(fsPath) {
232
+ return fsPath.replace(/\//g, "-");
233
+ }
234
+ function listClaudeSessions(limit = 20) {
235
+ if (!existsSync(CLAUDE_PROJECTS_DIR)) return [];
236
+ const sessions = [];
237
+ try {
238
+ const projectDirs = readdirSync(CLAUDE_PROJECTS_DIR, { withFileTypes: true }).filter((d) => d.isDirectory());
239
+ for (const projDir of projectDirs) {
240
+ const projPath = join(CLAUDE_PROJECTS_DIR, projDir.name);
241
+ const cwd = decodeClaudePath(projDir.name);
242
+ const files = readdirSync(projPath).filter((f) => f.endsWith(".jsonl"));
243
+ for (const file of files) {
244
+ const filePath = join(projPath, file);
245
+ const sid = basename(file, ".jsonl");
246
+ try {
247
+ const stat = statSync(filePath);
248
+ const { lastEvent, complete, summary } = parseClaudeSessionMeta(filePath);
249
+ sessions.push({
250
+ id: sid,
251
+ dir: projPath,
252
+ mtime: stat.mtimeMs,
253
+ lastEvent,
254
+ premiumRequests: 0,
255
+ summary,
256
+ cwd,
257
+ complete,
258
+ agent: "claude"
259
+ });
260
+ } catch {
261
+ }
262
+ }
263
+ }
264
+ } catch {
265
+ }
266
+ sessions.sort((a, b) => b.mtime - a.mtime);
267
+ return sessions.slice(0, limit);
268
+ }
269
+ function parseClaudeSessionMeta(filePath) {
270
+ try {
271
+ const content = readFileSync(filePath, "utf-8").trimEnd();
272
+ const lines = content.split("\n");
273
+ let lastEvent = "unknown";
274
+ let complete = false;
275
+ let summary = "";
276
+ for (let i = lines.length - 1; i >= Math.max(0, lines.length - 5); i--) {
277
+ try {
278
+ const event = JSON.parse(lines[i]);
279
+ if (i === lines.length - 1) {
280
+ lastEvent = event.type ?? event.role ?? "unknown";
281
+ }
282
+ if (event.type === "result" || event.type === "assistant" && event.stop_reason === "end_turn") {
283
+ complete = true;
284
+ }
285
+ } catch {
286
+ }
287
+ }
288
+ for (const line of lines.slice(0, 10)) {
289
+ try {
290
+ const event = JSON.parse(line);
291
+ if ((event.type === "human" || event.role === "human") && event.message) {
292
+ summary = typeof event.message === "string" ? event.message.slice(0, 100) : JSON.stringify(event.message).slice(0, 100);
293
+ break;
294
+ }
295
+ } catch {
296
+ }
297
+ }
298
+ return { lastEvent, complete, summary };
299
+ } catch {
300
+ return { lastEvent: "error", complete: false, summary: "" };
301
+ }
302
+ }
303
+ function getLatestClaudeSessionId(projectDir) {
304
+ if (!existsSync(CLAUDE_PROJECTS_DIR)) return null;
305
+ let searchDirs;
306
+ if (projectDir) {
307
+ const encoded = encodeClaudePath(resolve(projectDir));
308
+ const projPath = join(CLAUDE_PROJECTS_DIR, encoded);
309
+ searchDirs = existsSync(projPath) ? [projPath] : [];
310
+ } else {
311
+ try {
312
+ searchDirs = readdirSync(CLAUDE_PROJECTS_DIR, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => join(CLAUDE_PROJECTS_DIR, d.name));
313
+ } catch {
314
+ return null;
315
+ }
316
+ }
317
+ let latest = null;
318
+ for (const dir of searchDirs) {
319
+ try {
320
+ const files = readdirSync(dir).filter((f) => f.endsWith(".jsonl"));
321
+ for (const file of files) {
322
+ const stat = statSync(join(dir, file));
323
+ if (!latest || stat.mtimeMs > latest.mtime) {
324
+ latest = { id: basename(file, ".jsonl"), mtime: stat.mtimeMs };
325
+ }
326
+ }
327
+ } catch {
328
+ }
329
+ }
330
+ return latest?.id ?? null;
331
+ }
332
+ function getClaudeSessionCwd(sid) {
333
+ if (!existsSync(CLAUDE_PROJECTS_DIR)) return "";
334
+ try {
335
+ const projectDirs = readdirSync(CLAUDE_PROJECTS_DIR, { withFileTypes: true }).filter((d) => d.isDirectory());
336
+ for (const projDir of projectDirs) {
337
+ const filePath = join(CLAUDE_PROJECTS_DIR, projDir.name, `${sid}.jsonl`);
338
+ if (existsSync(filePath)) {
339
+ return decodeClaudePath(projDir.name);
340
+ }
341
+ }
342
+ } catch {
343
+ }
344
+ return "";
345
+ }
346
+ function getClaudeSessionReport(sid) {
347
+ if (!existsSync(CLAUDE_PROJECTS_DIR)) return null;
348
+ let filePath = null;
349
+ let cwd = "";
350
+ try {
351
+ const projectDirs = readdirSync(CLAUDE_PROJECTS_DIR, { withFileTypes: true }).filter((d) => d.isDirectory());
352
+ for (const projDir of projectDirs) {
353
+ const candidate = join(CLAUDE_PROJECTS_DIR, projDir.name, `${sid}.jsonl`);
354
+ if (existsSync(candidate)) {
355
+ filePath = candidate;
356
+ cwd = decodeClaudePath(projDir.name);
357
+ break;
358
+ }
359
+ }
360
+ } catch {
361
+ }
362
+ if (!filePath) return null;
363
+ let lines;
364
+ try {
365
+ lines = readFileSync(filePath, "utf-8").trimEnd().split("\n");
366
+ } catch {
367
+ return null;
368
+ }
369
+ const report = {
370
+ id: sid,
371
+ cwd,
372
+ summary: "",
373
+ startTime: "",
374
+ endTime: "",
375
+ durationMs: 0,
376
+ complete: false,
377
+ userMessages: 0,
378
+ assistantTurns: 0,
379
+ outputTokens: 0,
380
+ premiumRequests: 0,
381
+ toolUsage: {},
382
+ gitCommits: [],
383
+ filesCreated: [],
384
+ filesEdited: [],
385
+ errors: [],
386
+ taskCompletions: [],
387
+ agent: "claude"
388
+ };
389
+ for (const line of lines) {
390
+ let event;
391
+ try {
392
+ event = JSON.parse(line);
393
+ } catch {
394
+ continue;
395
+ }
396
+ const type = event.type ?? event.role ?? "";
397
+ const ts = event.timestamp;
398
+ if (ts && !report.startTime) report.startTime = ts;
399
+ if (ts) report.endTime = ts;
400
+ if (type === "human" || type === "user") {
401
+ report.userMessages++;
402
+ if (!report.summary) {
403
+ const msg = event.message ?? event.content;
404
+ report.summary = (typeof msg === "string" ? msg : JSON.stringify(msg ?? "")).slice(0, 100);
405
+ }
406
+ }
407
+ if (type === "assistant") {
408
+ report.assistantTurns++;
409
+ const usage = event.usage;
410
+ if (usage?.output_tokens) report.outputTokens += usage.output_tokens;
411
+ if (event.stop_reason === "end_turn") report.complete = true;
412
+ }
413
+ if (type === "tool_use") {
414
+ const toolName = event.name ?? event.tool ?? "unknown";
415
+ report.toolUsage[toolName] = (report.toolUsage[toolName] ?? 0) + 1;
416
+ if (toolName === "Bash" || toolName === "bash") {
417
+ const input = event.input ?? "";
418
+ const cmd = typeof input === "string" ? input : input?.command ?? "";
419
+ if (cmd.includes("git") && cmd.includes("commit") && cmd.includes("-m")) {
420
+ const msgMatch = cmd.match(/-m\s+"([^"]{1,120})/);
421
+ if (msgMatch) report.gitCommits.push(msgMatch[1]);
422
+ }
423
+ }
424
+ if (toolName === "Write" || toolName === "Create") {
425
+ const path = event.input?.file_path ?? event.input?.path;
426
+ if (path) report.filesCreated.push(path);
427
+ }
428
+ if (toolName === "Edit") {
429
+ const path = event.input?.file_path ?? event.input?.path;
430
+ if (path && !report.filesEdited.includes(path)) report.filesEdited.push(path);
431
+ }
432
+ }
433
+ if (type === "result") {
434
+ report.complete = true;
435
+ const result = event.result ?? event.content ?? "";
436
+ if (result) report.taskCompletions.push(typeof result === "string" ? result.slice(0, 200) : "(completed)");
437
+ }
438
+ if (type === "error") {
439
+ const msg = event.error ?? event.message ?? "unknown error";
440
+ report.errors.push(typeof msg === "string" ? msg : JSON.stringify(msg));
441
+ }
442
+ }
443
+ if (report.startTime && report.endTime) {
444
+ report.durationMs = new Date(report.endTime).getTime() - new Date(report.startTime).getTime();
445
+ }
446
+ return report;
447
+ }
448
+ function listAllSessions(limit = 20, agentFilter) {
449
+ const copilot = agentFilter === "claude" ? [] : listSessions(limit);
450
+ const claude = agentFilter === "copilot" ? [] : listClaudeSessions(limit);
451
+ const all = [...copilot, ...claude];
452
+ all.sort((a, b) => b.mtime - a.mtime);
453
+ return all.slice(0, limit);
454
+ }
455
+ function getAgentSessionReport(sid, agent) {
456
+ if (agent === "claude") return getClaudeSessionReport(sid);
457
+ if (agent === "copilot") return getSessionReport(sid);
458
+ return getSessionReport(sid) ?? getClaudeSessionReport(sid);
459
+ }
460
+ function findLatestIncompleteForAgent(agent) {
461
+ const sessions = listAllSessions(50, agent);
462
+ for (const s of sessions) {
463
+ if (!s.complete) return { id: s.id, agent: s.agent };
464
+ }
465
+ return null;
466
+ }
229
467
 
230
468
  // src/lib/process.ts
231
469
  import { execSync as execSync2, spawn } from "child_process";
@@ -308,21 +546,7 @@ function notify(message, title = "copilot-agent") {
308
546
  }
309
547
 
310
548
  // src/lib/process.ts
311
- function isCopilotInstalled() {
312
- try {
313
- execSync2("which copilot", { stdio: "pipe", encoding: "utf-8" });
314
- return true;
315
- } catch {
316
- return false;
317
- }
318
- }
319
- function assertCopilot() {
320
- if (!isCopilotInstalled()) {
321
- fail("copilot CLI not found. Install with: npm i -g @githubnext/copilot");
322
- process.exit(1);
323
- }
324
- }
325
- function findCopilotProcesses() {
549
+ function findAgentProcesses(agentFilter) {
326
550
  try {
327
551
  const output = execSync2("ps -eo pid,command", { encoding: "utf-8" });
328
552
  const results = [];
@@ -331,52 +555,56 @@ function findCopilotProcesses() {
331
555
  for (const line of output.split("\n")) {
332
556
  const trimmed = line.trim();
333
557
  if (!trimmed) continue;
334
- if ((trimmed.includes("copilot") || trimmed.includes("@githubnext/copilot")) && !trimmed.includes("ps -eo") && !trimmed.includes("copilot-agent") && !trimmed.includes("grep")) {
335
- const match = trimmed.match(/^(\d+)\s+(.+)$/);
336
- if (match) {
337
- const pid = parseInt(match[1], 10);
338
- if (pid === myPid || pid === parentPid) continue;
339
- const cmd = match[2];
340
- const sidMatch = cmd.match(/resume[= ]+([a-f0-9-]{36})/);
341
- let cwd;
342
- try {
343
- cwd = execSync2(`lsof -p ${pid} -Fn 2>/dev/null | grep '^n/' | head -1`, {
344
- encoding: "utf-8",
345
- stdio: ["pipe", "pipe", "pipe"]
346
- }).trim().slice(1) || void 0;
347
- } catch {
348
- }
349
- const sid = sidMatch?.[1];
350
- if (!cwd && sid) {
351
- cwd = getSessionCwd(sid) || void 0;
352
- }
353
- results.push({ pid, command: cmd, sessionId: sid, cwd });
354
- }
558
+ const isCopilot = (trimmed.includes("copilot") || trimmed.includes("@githubnext/copilot")) && !trimmed.includes("copilot-agent") && !trimmed.includes("copilot-api");
559
+ const isClaude = trimmed.includes("claude") && !trimmed.includes("claude-code") && !trimmed.includes("copilot-agent");
560
+ if (!isCopilot && !isClaude) continue;
561
+ if (trimmed.includes("ps -eo") || trimmed.includes("grep")) continue;
562
+ const agent = isClaude ? "claude" : "copilot";
563
+ if (agentFilter && agent !== agentFilter) continue;
564
+ const match = trimmed.match(/^(\d+)\s+(.+)$/);
565
+ if (!match) continue;
566
+ const pid = parseInt(match[1], 10);
567
+ if (pid === myPid || pid === parentPid) continue;
568
+ const cmd = match[2];
569
+ const sidMatch = agent === "copilot" ? cmd.match(/resume[= ]+([a-f0-9-]{36})/) : cmd.match(/(?:--resume|--session-id)[= ]+([a-f0-9-]{36})/);
570
+ let cwd;
571
+ try {
572
+ cwd = execSync2(`lsof -p ${pid} -Fn 2>/dev/null | grep '^n/' | head -1`, {
573
+ encoding: "utf-8",
574
+ stdio: ["pipe", "pipe", "pipe"]
575
+ }).trim().slice(1) || void 0;
576
+ } catch {
577
+ }
578
+ const sid = sidMatch?.[1];
579
+ if (!cwd && sid) {
580
+ cwd = (agent === "copilot" ? getSessionCwd(sid) : getClaudeSessionCwd(sid)) || void 0;
355
581
  }
582
+ results.push({ pid, command: cmd, sessionId: sid, cwd, agent });
356
583
  }
357
584
  return results;
358
585
  } catch {
359
586
  return [];
360
587
  }
361
588
  }
362
- function findPidForSession(sid) {
363
- const procs = findCopilotProcesses();
589
+ function findPidForSession(sid, agent) {
590
+ const procs = findAgentProcesses(agent);
364
591
  const matching = procs.filter((p) => p.command.includes(sid)).sort((a, b) => b.pid - a.pid);
365
592
  return matching[0]?.pid ?? null;
366
593
  }
367
- async function waitForCopilotInDir(dir, timeoutMs = 144e5, pollMs = 1e4) {
594
+ async function waitForAgentInDir(dir, agent, timeoutMs = 144e5, pollMs = 1e4) {
368
595
  const targetDir = resolve2(dir);
369
596
  const start = Date.now();
370
597
  let warned = false;
598
+ const label = agent ?? "agent";
371
599
  while (Date.now() - start < timeoutMs) {
372
- const procs = findCopilotProcesses();
600
+ const procs = findAgentProcesses(agent);
373
601
  const conflicting = procs.filter((p) => {
374
602
  if (!p.cwd) return false;
375
603
  return resolve2(p.cwd) === targetDir;
376
604
  });
377
605
  if (conflicting.length === 0) return;
378
606
  if (!warned) {
379
- warn(`Waiting for copilot in ${targetDir} to finish...`);
607
+ warn(`Waiting for ${label} in ${targetDir} to finish...`);
380
608
  for (const p of conflicting) {
381
609
  log(` PID ${p.pid}: ${p.command.slice(0, 80)}`);
382
610
  }
@@ -384,12 +612,13 @@ async function waitForCopilotInDir(dir, timeoutMs = 144e5, pollMs = 1e4) {
384
612
  }
385
613
  await sleep(pollMs);
386
614
  }
387
- warn("Timeout waiting for copilot to finish in directory");
615
+ warn(`Timeout waiting for ${label} to finish in directory`);
388
616
  }
389
- function assertSessionNotRunning(sid) {
390
- const pid = findPidForSession(sid);
617
+ function assertSessionNotRunning(sid, agent) {
618
+ const pid = findPidForSession(sid, agent);
391
619
  if (pid) {
392
- fail(`Session ${sid.slice(0, 8)}\u2026 already has copilot running (PID ${pid}). Cannot resume \u2014 would corrupt the session.`);
620
+ const label = agent ?? "agent";
621
+ fail(`Session ${sid.slice(0, 8)}\u2026 already has ${label} running (PID ${pid}). Cannot resume.`);
393
622
  process.exit(1);
394
623
  }
395
624
  }
@@ -407,9 +636,8 @@ async function waitForExit(pid, timeoutMs = 144e5) {
407
636
  }
408
637
  async function runCopilot(args, options) {
409
638
  const dir = options?.cwd ?? process.cwd();
410
- if (options?.useWorktree) {
411
- } else {
412
- await waitForCopilotInDir(dir);
639
+ if (!options?.useWorktree) {
640
+ await waitForAgentInDir(dir, "copilot");
413
641
  }
414
642
  return new Promise((resolve7) => {
415
643
  const child = spawn("copilot", args, {
@@ -421,11 +649,7 @@ async function runCopilot(args, options) {
421
649
  await sleep(3e3);
422
650
  const sid = getLatestSessionId();
423
651
  const premium = sid ? getSessionPremium(sid) : 0;
424
- resolve7({
425
- exitCode: code ?? 1,
426
- sessionId: sid,
427
- premium
428
- });
652
+ resolve7({ exitCode: code ?? 1, sessionId: sid, premium });
429
653
  });
430
654
  child.on("error", () => {
431
655
  resolve7({ exitCode: 1, sessionId: null, premium: 0 });
@@ -433,7 +657,7 @@ async function runCopilot(args, options) {
433
657
  });
434
658
  }
435
659
  function runCopilotResume(sid, steps, message, cwd) {
436
- assertSessionNotRunning(sid);
660
+ assertSessionNotRunning(sid, "copilot");
437
661
  const args = [
438
662
  `--resume=${sid}`,
439
663
  "--autopilot",
@@ -456,38 +680,82 @@ function runCopilotTask(prompt, steps, cwd, useWorktree) {
456
680
  "--no-ask-user"
457
681
  ], { cwd, useWorktree });
458
682
  }
683
+ async function runClaude(args, options) {
684
+ const dir = options?.cwd ?? process.cwd();
685
+ if (!options?.useWorktree) {
686
+ await waitForAgentInDir(dir, "claude");
687
+ }
688
+ return new Promise((resolve7) => {
689
+ const child = spawn("claude", args, {
690
+ cwd: options?.cwd,
691
+ stdio: "inherit",
692
+ env: { ...process.env }
693
+ });
694
+ child.on("close", async (code) => {
695
+ await sleep(2e3);
696
+ const sid = getLatestClaudeSessionId(options?.cwd);
697
+ resolve7({ exitCode: code ?? 1, sessionId: sid, premium: 0 });
698
+ });
699
+ child.on("error", () => {
700
+ resolve7({ exitCode: 1, sessionId: null, premium: 0 });
701
+ });
702
+ });
703
+ }
704
+ function runClaudeResume(sid, _steps, message, cwd) {
705
+ assertSessionNotRunning(sid, "claude");
706
+ const args = ["--resume", sid, "--dangerously-skip-permissions"];
707
+ if (message) args.push(message);
708
+ return runClaude(args, { cwd });
709
+ }
710
+ function runClaudeTask(prompt, _steps, cwd, useWorktree) {
711
+ return runClaude([
712
+ "--print",
713
+ "--dangerously-skip-permissions",
714
+ "--output-format",
715
+ "text",
716
+ prompt
717
+ ], { cwd, useWorktree });
718
+ }
719
+ function runAgentTask(agent, prompt, steps, cwd, useWorktree) {
720
+ return agent === "claude" ? runClaudeTask(prompt, steps, cwd, useWorktree) : runCopilotTask(prompt, steps, cwd, useWorktree);
721
+ }
722
+ function runAgentResume(agent, sid, steps, message, cwd) {
723
+ return agent === "claude" ? runClaudeResume(sid, steps, message, cwd) : runCopilotResume(sid, steps, message, cwd);
724
+ }
459
725
  function sleep(ms) {
460
726
  return new Promise((r) => setTimeout(r, ms));
461
727
  }
462
728
 
463
729
  // src/commands/status.ts
464
730
  function registerStatusCommand(program2) {
465
- program2.command("status").description("Show copilot session status").option("-l, --limit <n>", "Number of sessions to show", "10").option("-a, --active", "Show only active (running) processes").option("-i, --incomplete", "Only show incomplete sessions").action((opts) => {
731
+ program2.command("status").description("Show agent session status (copilot + claude)").option("-l, --limit <n>", "Number of sessions to show", "10").option("-a, --active", "Show only active (running) processes").option("-i, --incomplete", "Only show incomplete sessions").option("--agent <type>", "Filter by agent: copilot or claude").action((opts) => {
732
+ const agentFilter = opts.agent;
466
733
  if (opts.active) {
467
- showActive();
734
+ showActive(agentFilter);
468
735
  } else {
469
- showRecent(parseInt(opts.limit, 10), opts.incomplete ?? false);
736
+ showRecent(parseInt(opts.limit, 10), opts.incomplete ?? false, agentFilter);
470
737
  }
471
738
  });
472
739
  }
473
- function showActive() {
474
- const procs = findCopilotProcesses();
740
+ function showActive(agentFilter) {
741
+ const procs = findAgentProcesses(agentFilter);
475
742
  if (procs.length === 0) {
476
- log(`${DIM}No active copilot processes.${RESET}`);
743
+ log(`${DIM}No active agent processes.${RESET}`);
477
744
  return;
478
745
  }
479
746
  log(`
480
- ${BOLD}${"PID".padEnd(8)} ${"Session".padEnd(40)} Command${RESET}`);
481
- log("\u2500".repeat(108));
747
+ ${BOLD}${"Agent".padEnd(9)} ${"PID".padEnd(8)} ${"Session".padEnd(40)} Command${RESET}`);
748
+ log("\u2500".repeat(118));
482
749
  for (const p of procs) {
750
+ const agentLabel = p.agent === "claude" ? `${CYAN}claude${RESET} ` : `${GREEN}copilot${RESET}`;
483
751
  log(
484
- `${CYAN}${String(p.pid).padEnd(8)}${RESET} ${(p.sessionId ?? "\u2014").padEnd(40)} ${truncate(p.command, 58)}`
752
+ `${agentLabel.padEnd(9 + 9)} ${String(p.pid).padEnd(8)} ${(p.sessionId ?? "\u2014").padEnd(40)} ${truncate(p.command, 50)}`
485
753
  );
486
754
  }
487
755
  log("");
488
756
  }
489
- function showRecent(limit, incompleteOnly) {
490
- let sessions = listSessions(limit);
757
+ function showRecent(limit, incompleteOnly, agentFilter) {
758
+ let sessions = listAllSessions(limit, agentFilter);
491
759
  if (incompleteOnly) {
492
760
  sessions = sessions.filter((s) => !s.complete);
493
761
  }
@@ -497,15 +765,16 @@ function showRecent(limit, incompleteOnly) {
497
765
  }
498
766
  log(
499
767
  `
500
- ${BOLD}${"Status".padEnd(10)} ${"Premium".padEnd(10)} ${"Last Event".padEnd(25)} ${"Summary".padEnd(40)} ID${RESET}`
768
+ ${BOLD}${"Agent".padEnd(9)} ${"Status".padEnd(10)} ${"Premium".padEnd(10)} ${"Last Event".padEnd(22)} ${"Summary".padEnd(35)} ID${RESET}`
501
769
  );
502
- log("\u2500".repeat(120));
770
+ log("\u2500".repeat(130));
503
771
  for (const s of sessions) {
772
+ const agentLabel = s.agent === "claude" ? `${CYAN}claude${RESET} ` : `${GREEN}copilot${RESET}`;
504
773
  const status = s.complete ? `${GREEN}\u2714 done${RESET}` : `${YELLOW}\u23F8 stop${RESET}`;
505
- const premium = String(s.premiumRequests);
506
- const summary = truncate(s.summary || "\u2014", 38);
774
+ const premium = s.agent === "claude" ? "\u2014" : String(s.premiumRequests);
775
+ const summary = truncate(s.summary || "\u2014", 33);
507
776
  log(
508
- `${status.padEnd(10 + 9)} ${premium.padEnd(10)} ${s.lastEvent.padEnd(25)} ${summary.padEnd(40)} ${DIM}${s.id}${RESET}`
777
+ `${agentLabel.padEnd(9 + 9)} ${status.padEnd(10 + 9)} ${premium.padEnd(10)} ${s.lastEvent.padEnd(22)} ${summary.padEnd(35)} ${DIM}${s.id.slice(0, 12)}\u2026${RESET}`
509
778
  );
510
779
  }
511
780
  log(`
@@ -516,15 +785,55 @@ function truncate(s, max) {
516
785
  return s.substring(0, max - 1) + "\u2026";
517
786
  }
518
787
 
788
+ // src/lib/provider.ts
789
+ import { execSync as execSync3 } from "child_process";
790
+ function detectAvailableAgents() {
791
+ const agents = [];
792
+ try {
793
+ execSync3("which copilot", { stdio: "pipe" });
794
+ agents.push("copilot");
795
+ } catch {
796
+ }
797
+ try {
798
+ execSync3("which claude", { stdio: "pipe" });
799
+ agents.push("claude");
800
+ } catch {
801
+ }
802
+ return agents;
803
+ }
804
+ function resolveAgent(explicit) {
805
+ if (explicit === "copilot" || explicit === "claude") return explicit;
806
+ const available = detectAvailableAgents();
807
+ if (available.includes("copilot")) return "copilot";
808
+ if (available.includes("claude")) return "claude";
809
+ return "copilot";
810
+ }
811
+ function isAgentInstalled(agent) {
812
+ try {
813
+ execSync3(`which ${agent}`, { stdio: "pipe" });
814
+ return true;
815
+ } catch {
816
+ return false;
817
+ }
818
+ }
819
+ function assertAgent(agent) {
820
+ if (!isAgentInstalled(agent)) {
821
+ const installHint = agent === "copilot" ? "npm i -g @githubnext/copilot" : "npm i -g @anthropic-ai/claude-code";
822
+ console.error(`\u2716 ${agent} CLI not found. Install with: ${installHint}`);
823
+ process.exit(1);
824
+ }
825
+ }
826
+
519
827
  // src/commands/watch.ts
520
828
  function registerWatchCommand(program2) {
521
- program2.command("watch [session-id]").description("Watch a session and auto-resume when it stops").option("-s, --steps <n>", "Max autopilot continues per resume", "30").option("-r, --max-resumes <n>", "Max number of resumes", "10").option("-c, --cooldown <n>", "Seconds between resumes", "10").option("-m, --message <msg>", "Message to send on resume").action(async (sid, opts) => {
829
+ program2.command("watch [session-id]").description("Watch a session and auto-resume when it stops").option("-s, --steps <n>", "Max autopilot continues per resume", "30").option("-r, --max-resumes <n>", "Max number of resumes", "10").option("-c, --cooldown <n>", "Seconds between resumes", "10").option("-m, --message <msg>", "Message to send on resume").option("-a, --agent <type>", "Agent to use: copilot or claude").action(async (sid, opts) => {
522
830
  try {
523
831
  await watchCommand(sid, {
524
832
  steps: parseInt(opts.steps, 10),
525
833
  maxResumes: parseInt(opts.maxResumes, 10),
526
834
  cooldown: parseInt(opts.cooldown, 10),
527
- message: opts.message
835
+ message: opts.message,
836
+ agent: resolveAgent(opts.agent)
528
837
  });
529
838
  } catch (err) {
530
839
  fail(`Watch error: ${err instanceof Error ? err.message : err}`);
@@ -533,14 +842,15 @@ function registerWatchCommand(program2) {
533
842
  });
534
843
  }
535
844
  async function watchCommand(sid, opts) {
536
- assertCopilot();
845
+ assertAgent(opts.agent);
537
846
  if (!sid) {
538
- sid = findLatestIncomplete() ?? void 0;
847
+ const result = findLatestIncompleteForAgent(opts.agent);
848
+ sid = result?.id;
539
849
  if (!sid) {
540
850
  fail("No incomplete session found.");
541
851
  process.exit(1);
542
852
  }
543
- info(`Auto-detected incomplete session: ${CYAN}${sid}${RESET}`);
853
+ info(`Auto-detected incomplete ${opts.agent} session: ${CYAN}${sid}${RESET}`);
544
854
  }
545
855
  if (!validateSession(sid)) {
546
856
  fail(`Invalid session: ${sid}`);
@@ -552,9 +862,9 @@ async function watchCommand(sid, opts) {
552
862
  }
553
863
  let resumes = 0;
554
864
  while (resumes < opts.maxResumes) {
555
- const pid = findPidForSession(sid);
865
+ const pid = findPidForSession(sid, opts.agent);
556
866
  if (pid) {
557
- info(`Watching PID ${pid} for session ${CYAN}${sid.slice(0, 8)}${RESET}\u2026`);
867
+ info(`Watching PID ${pid} for ${opts.agent} session ${CYAN}${sid.slice(0, 8)}${RESET}\u2026`);
558
868
  const exited = await waitForExit(pid);
559
869
  if (!exited) {
560
870
  warn("Timeout waiting for process exit.");
@@ -574,7 +884,8 @@ async function watchCommand(sid, opts) {
574
884
  await sleep2(opts.cooldown * 1e3);
575
885
  }
576
886
  const cwd = getSessionCwd(sid) || void 0;
577
- const result = await runCopilotResume(
887
+ const result = await runAgentResume(
888
+ opts.agent,
578
889
  sid,
579
890
  opts.steps,
580
891
  opts.message ?? "Continue remaining work. Pick up where you left off and complete the task.",
@@ -594,8 +905,8 @@ function sleep2(ms) {
594
905
 
595
906
  // src/lib/detect.ts
596
907
  import { existsSync as existsSync2, readFileSync as readFileSync2, readdirSync as readdirSync2 } from "fs";
597
- import { join as join2, basename, resolve as resolve3 } from "path";
598
- import { execSync as execSync3 } from "child_process";
908
+ import { join as join2, basename as basename2, resolve as resolve3 } from "path";
909
+ import { execSync as execSync4 } from "child_process";
599
910
  function detectProjectType(dir) {
600
911
  const exists = (f) => existsSync2(join2(dir, f));
601
912
  if (exists("build.gradle.kts") || exists("build.gradle")) {
@@ -639,11 +950,11 @@ function detectProjectName(dir) {
639
950
  if (pkg.name) return pkg.name;
640
951
  } catch {
641
952
  }
642
- return basename(resolve3(dir));
953
+ return basename2(resolve3(dir));
643
954
  }
644
955
  function detectMainBranch(dir) {
645
956
  try {
646
- const ref = execSync3("git symbolic-ref refs/remotes/origin/HEAD", {
957
+ const ref = execSync4("git symbolic-ref refs/remotes/origin/HEAD", {
647
958
  cwd: dir,
648
959
  encoding: "utf-8",
649
960
  stdio: ["pipe", "pipe", "pipe"]
@@ -652,7 +963,7 @@ function detectMainBranch(dir) {
652
963
  } catch {
653
964
  }
654
965
  try {
655
- const branch = execSync3("git branch --show-current", {
966
+ const branch = execSync4("git branch --show-current", {
656
967
  cwd: dir,
657
968
  encoding: "utf-8",
658
969
  stdio: ["pipe", "pipe", "pipe"]
@@ -818,10 +1129,10 @@ async function withLock(name, fn) {
818
1129
  // src/lib/git.ts
819
1130
  import { existsSync as existsSync4 } from "fs";
820
1131
  import { join as join4, resolve as resolve4 } from "path";
821
- import { execSync as execSync4 } from "child_process";
1132
+ import { execSync as execSync5 } from "child_process";
822
1133
  function gitExec(dir, cmd) {
823
1134
  try {
824
- return execSync4(cmd, {
1135
+ return execSync5(cmd, {
825
1136
  cwd: dir,
826
1137
  encoding: "utf-8",
827
1138
  stdio: ["pipe", "pipe", "pipe"]
@@ -873,14 +1184,15 @@ function removeWorktree(repoDir, worktreePath) {
873
1184
 
874
1185
  // src/commands/run.ts
875
1186
  function registerRunCommand(program2) {
876
- program2.command("run [dir]").description("Discover and fix issues in a project").option("-s, --steps <n>", "Max autopilot continues per task", "30").option("-t, --max-tasks <n>", "Max number of tasks to run", "5").option("-p, --max-premium <n>", "Max total premium requests", "50").option("--dry-run", "Show tasks without executing").option("--worktree", "Use git worktree for parallel execution (default: wait for idle)").action(async (dir, opts) => {
1187
+ program2.command("run [dir]").description("Discover and fix issues in a project").option("-s, --steps <n>", "Max autopilot continues per task", "30").option("-t, --max-tasks <n>", "Max number of tasks to run", "5").option("-p, --max-premium <n>", "Max total premium requests", "50").option("--dry-run", "Show tasks without executing").option("--worktree", "Use git worktree for parallel execution (default: wait for idle)").option("-a, --agent <type>", "Agent to use: copilot or claude").action(async (dir, opts) => {
877
1188
  try {
878
1189
  await runCommand(dir ?? process.cwd(), {
879
1190
  steps: parseInt(opts.steps, 10),
880
1191
  maxTasks: parseInt(opts.maxTasks, 10),
881
1192
  maxPremium: parseInt(opts.maxPremium, 10),
882
1193
  dryRun: opts.dryRun ?? false,
883
- useWorktree: opts.worktree ?? false
1194
+ useWorktree: opts.worktree ?? false,
1195
+ agent: resolveAgent(opts.agent)
884
1196
  });
885
1197
  } catch (err) {
886
1198
  fail(`Run error: ${err instanceof Error ? err.message : err}`);
@@ -889,11 +1201,11 @@ function registerRunCommand(program2) {
889
1201
  });
890
1202
  }
891
1203
  async function runCommand(dir, opts) {
892
- assertCopilot();
1204
+ assertAgent(opts.agent);
893
1205
  const projectType = detectProjectType(dir);
894
1206
  const name = detectProjectName(dir);
895
1207
  const mainBranch = isGitRepo(dir) ? detectMainBranch(dir) : null;
896
- info(`Project: ${CYAN}${name}${RESET} (${projectType})`);
1208
+ info(`Project: ${CYAN}${name}${RESET} (${projectType}) \u2014 agent: ${opts.agent}`);
897
1209
  if (mainBranch) info(`Main branch: ${mainBranch}`);
898
1210
  const tasks = getTasksForProject(projectType).slice(0, opts.maxTasks);
899
1211
  if (tasks.length === 0) {
@@ -935,9 +1247,14 @@ ${"\u2550".repeat(60)}`);
935
1247
  let worktreeCreated = false;
936
1248
  if (opts.useWorktree && isGitRepo(dir)) {
937
1249
  try {
938
- taskDir = createWorktree(dir, branchName, mainBranch ?? void 0);
939
- worktreeCreated = true;
940
- info(`Created worktree: ${taskDir}`);
1250
+ const wt = createWorktree(dir, branchName);
1251
+ if (wt) {
1252
+ taskDir = wt;
1253
+ worktreeCreated = true;
1254
+ info(`Created worktree: ${taskDir}`);
1255
+ } else {
1256
+ warn("Worktree creation returned null, falling back to main dir");
1257
+ }
941
1258
  } catch (err) {
942
1259
  warn(`Worktree creation failed, falling back to main dir: ${err}`);
943
1260
  taskDir = dir;
@@ -945,7 +1262,7 @@ ${"\u2550".repeat(60)}`);
945
1262
  }
946
1263
  const result = await withLock(
947
1264
  "copilot-run",
948
- () => runCopilotTask(task.prompt, opts.steps, taskDir, opts.useWorktree)
1265
+ () => runAgentTask(opts.agent, task.prompt, opts.steps, taskDir, opts.useWorktree)
949
1266
  );
950
1267
  const commitRef = worktreeCreated ? taskDir : dir;
951
1268
  const commits = mainBranch ? gitCountCommits(commitRef, mainBranch, "HEAD") : 0;
@@ -973,7 +1290,7 @@ ${BOLD}\u2550\u2550\u2550 Run Summary \u2550\u2550\u2550${RESET}`);
973
1290
  import { join as join5 } from "path";
974
1291
  import { homedir as homedir3 } from "os";
975
1292
  function registerOvernightCommand(program2) {
976
- program2.command("overnight [dir]").description("Run tasks continuously until a deadline").option("-u, --until <HH>", "Stop at this hour (24h format)", "07").option("-s, --steps <n>", "Max autopilot continues per task", "50").option("-c, --cooldown <n>", "Seconds between tasks", "15").option("-p, --max-premium <n>", "Max premium requests budget", "300").option("--dry-run", "Show plan without executing").option("--worktree", "Use git worktree for parallel execution (default: wait for idle)").action(async (dir, opts) => {
1293
+ program2.command("overnight [dir]").description("Run tasks continuously until a deadline").option("-u, --until <HH>", "Stop at this hour (24h format)", "07").option("-s, --steps <n>", "Max autopilot continues per task", "50").option("-c, --cooldown <n>", "Seconds between tasks", "15").option("-p, --max-premium <n>", "Max premium requests budget", "300").option("--dry-run", "Show plan without executing").option("--worktree", "Use git worktree for parallel execution (default: wait for idle)").option("-a, --agent <type>", "Agent to use: copilot or claude").action(async (dir, opts) => {
977
1294
  try {
978
1295
  await overnightCommand(dir ?? process.cwd(), {
979
1296
  until: parseInt(opts.until, 10),
@@ -981,7 +1298,8 @@ function registerOvernightCommand(program2) {
981
1298
  cooldown: parseInt(opts.cooldown, 10),
982
1299
  maxPremium: parseInt(opts.maxPremium, 10),
983
1300
  dryRun: opts.dryRun ?? false,
984
- useWorktree: opts.worktree ?? false
1301
+ useWorktree: opts.worktree ?? false,
1302
+ agent: resolveAgent(opts.agent)
985
1303
  });
986
1304
  } catch (err) {
987
1305
  fail(`Overnight error: ${err instanceof Error ? err.message : err}`);
@@ -994,14 +1312,14 @@ function isPastDeadline(untilHour) {
994
1312
  return hour >= untilHour && hour < 20;
995
1313
  }
996
1314
  async function overnightCommand(dir, opts) {
997
- assertCopilot();
1315
+ assertAgent(opts.agent);
998
1316
  const ts = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "").slice(0, 15);
999
1317
  const logPath = join5(homedir3(), ".copilot", "auto-resume-logs", `overnight-${ts}.log`);
1000
1318
  setLogFile(logPath);
1001
1319
  const name = detectProjectName(dir);
1002
1320
  const projectType = detectProjectType(dir);
1003
1321
  const mainBranch = isGitRepo(dir) ? detectMainBranch(dir) : null;
1004
- info(`Overnight runner for ${CYAN}${name}${RESET} (${projectType})`);
1322
+ info(`Overnight runner for ${CYAN}${name}${RESET} (${projectType}) \u2014 agent: ${opts.agent}`);
1005
1323
  info(`Deadline: ${String(opts.until).padStart(2, "0")}:00`);
1006
1324
  info(`Max premium: ${opts.maxPremium}, Steps: ${opts.steps}`);
1007
1325
  info(`Log: ${logPath}`);
@@ -1012,18 +1330,20 @@ Would run ${tasks.length} tasks:`);
1012
1330
  for (const t of tasks) log(` ${DIM}\u2022${RESET} ${t.title}`);
1013
1331
  return;
1014
1332
  }
1015
- const existingSession = findLatestIncomplete();
1333
+ const existingResult = findLatestIncompleteForAgent(opts.agent);
1334
+ const existingSession = existingResult?.id;
1016
1335
  if (existingSession && validateSession(existingSession)) {
1017
1336
  info(`Found incomplete session: ${existingSession}`);
1018
- const pid = findPidForSession(existingSession);
1337
+ const pid = findPidForSession(existingSession, opts.agent);
1019
1338
  if (pid) {
1020
- info(`Waiting for running copilot (PID ${pid})...`);
1339
+ info(`Waiting for running ${opts.agent} (PID ${pid})...`);
1021
1340
  await waitForExit(pid);
1022
1341
  }
1023
1342
  if (!hasTaskComplete(existingSession) && !isPastDeadline(opts.until)) {
1024
1343
  info("Resuming incomplete session...");
1025
1344
  const cwd = getSessionCwd(existingSession) || dir;
1026
- await runCopilotResume(
1345
+ await runAgentResume(
1346
+ opts.agent,
1027
1347
  existingSession,
1028
1348
  opts.steps,
1029
1349
  "Continue remaining work. Complete the task.",
@@ -1060,9 +1380,14 @@ ${"\u2550".repeat(60)}`);
1060
1380
  let worktreeCreated = false;
1061
1381
  if (opts.useWorktree && isGitRepo(dir)) {
1062
1382
  try {
1063
- taskDir = createWorktree(dir, branchName, mainBranch ?? void 0);
1064
- worktreeCreated = true;
1065
- info(`Created worktree: ${taskDir}`);
1383
+ const wt = createWorktree(dir, branchName);
1384
+ if (wt) {
1385
+ taskDir = wt;
1386
+ worktreeCreated = true;
1387
+ info(`Created worktree: ${taskDir}`);
1388
+ } else {
1389
+ warn("Worktree creation returned null, falling back to main dir");
1390
+ }
1066
1391
  } catch (err) {
1067
1392
  warn(`Worktree creation failed, falling back to main dir: ${err}`);
1068
1393
  taskDir = dir;
@@ -1071,7 +1396,7 @@ ${"\u2550".repeat(60)}`);
1071
1396
  try {
1072
1397
  const result = await withLock(
1073
1398
  "copilot-overnight",
1074
- () => runCopilotTask(task.prompt, opts.steps, taskDir, opts.useWorktree)
1399
+ () => runAgentTask(opts.agent, task.prompt, opts.steps, taskDir, opts.useWorktree)
1075
1400
  );
1076
1401
  const commitRef = worktreeCreated ? taskDir : dir;
1077
1402
  const commits = mainBranch ? gitCountCommits(commitRef, mainBranch, "HEAD") : 0;
@@ -1117,10 +1442,12 @@ import { existsSync as existsSync5, copyFileSync, mkdirSync as mkdirSync3 } from
1117
1442
  import { join as join6, resolve as resolve5 } from "path";
1118
1443
  import { homedir as homedir4 } from "os";
1119
1444
  function registerResearchCommand(program2) {
1120
- program2.command("research [project]").description("Research improvements or a specific topic").option("-s, --steps <n>", "Max autopilot continues", "50").action(async (project, opts) => {
1445
+ program2.command("research [project]").description("Research improvements or a specific topic").option("-s, --steps <n>", "Max autopilot continues", "50").option("-a, --agent <type>", "Agent to use: copilot or claude").action(async (project, opts) => {
1121
1446
  try {
1447
+ const agent = resolveAgent(opts.agent);
1122
1448
  await researchCommand(project ?? process.cwd(), {
1123
- steps: parseInt(opts.steps, 10)
1449
+ steps: parseInt(opts.steps, 10),
1450
+ agent
1124
1451
  });
1125
1452
  } catch (err) {
1126
1453
  fail(`Research error: ${err instanceof Error ? err.message : err}`);
@@ -1149,15 +1476,15 @@ For each proposal, include:
1149
1476
  Write RESEARCH-PROPOSALS.md in the project root.`;
1150
1477
  }
1151
1478
  async function researchCommand(dir, opts) {
1152
- assertCopilot();
1479
+ assertAgent(opts.agent);
1153
1480
  const projectDir = resolve5(dir);
1154
1481
  const projectType = detectProjectType(projectDir);
1155
1482
  const projectName = detectProjectName(projectDir);
1156
- info(`Researching: ${CYAN}${projectName}${RESET} (${projectType})`);
1483
+ info(`Researching: ${CYAN}${projectName}${RESET} (${projectType}) \u2014 agent: ${opts.agent}`);
1157
1484
  const prompt = buildResearchPrompt(projectType, projectName);
1158
1485
  const result = await withLock(
1159
1486
  "copilot-research",
1160
- () => runCopilotTask(prompt, opts.steps, projectDir)
1487
+ () => runAgentTask(opts.agent, prompt, opts.steps, projectDir)
1161
1488
  );
1162
1489
  log(`Copilot exited with code ${result.exitCode}`);
1163
1490
  const proposalsFile = join6(projectDir, "RESEARCH-PROPOSALS.md");
@@ -1178,12 +1505,12 @@ async function researchCommand(dir, opts) {
1178
1505
  // src/commands/report.ts
1179
1506
  import { resolve as resolve6 } from "path";
1180
1507
  function registerReportCommand(program2) {
1181
- program2.command("report [session-id]").description("Show what copilot did \u2014 timeline, tools, commits, files changed").option("-l, --limit <n>", "Number of recent sessions to report (when no ID given)", "1").option("--project <dir>", "Filter sessions by project directory").option("--json", "Output raw JSON").action((sessionId, opts) => {
1508
+ program2.command("report [session-id]").description("Show what an agent did \u2014 timeline, tools, commits, files changed").option("-l, --limit <n>", "Number of recent sessions to report (when no ID given)", "1").option("--project <dir>", "Filter sessions by project directory").option("--json", "Output raw JSON").option("-a, --agent <type>", "Filter by agent: copilot or claude").action((sessionId, opts) => {
1182
1509
  try {
1183
1510
  if (sessionId) {
1184
- reportSingle(sessionId, opts.json ?? false);
1511
+ reportSingle(sessionId, opts.json ?? false, opts.agent);
1185
1512
  } else {
1186
- reportRecent(parseInt(opts.limit, 10), opts.project, opts.json ?? false);
1513
+ reportRecent(parseInt(opts.limit, 10), opts.project, opts.json ?? false, opts.agent);
1187
1514
  }
1188
1515
  } catch (err) {
1189
1516
  fail(`Report error: ${err instanceof Error ? err.message : err}`);
@@ -1191,8 +1518,8 @@ function registerReportCommand(program2) {
1191
1518
  }
1192
1519
  });
1193
1520
  }
1194
- function reportRecent(limit, projectDir, json = false) {
1195
- let sessions = listSessions(limit * 3);
1521
+ function reportRecent(limit, projectDir, json = false, agentFilter) {
1522
+ let sessions = listAllSessions(limit * 3, agentFilter);
1196
1523
  if (projectDir) {
1197
1524
  const target = resolve6(projectDir);
1198
1525
  sessions = sessions.filter((s) => s.cwd && resolve6(s.cwd) === target);
@@ -1203,11 +1530,11 @@ function reportRecent(limit, projectDir, json = false) {
1203
1530
  return;
1204
1531
  }
1205
1532
  for (const s of sessions) {
1206
- reportSingle(s.id, json);
1533
+ reportSingle(s.id, json, s.agent);
1207
1534
  }
1208
1535
  }
1209
- function reportSingle(sid, json = false) {
1210
- const report = getSessionReport(sid);
1536
+ function reportSingle(sid, json = false, agent) {
1537
+ const report = getAgentSessionReport(sid, agent);
1211
1538
  if (!report) {
1212
1539
  warn(`Session ${sid} not found or invalid.`);
1213
1540
  return;
@@ -1244,7 +1571,7 @@ function renderReport(r) {
1244
1571
  const projectName = r.cwd.split("/").pop() ?? r.cwd;
1245
1572
  log("");
1246
1573
  log(`${BOLD}\u2554${"\u2550".repeat(62)}\u2557${RESET}`);
1247
- log(`${BOLD}\u2551${RESET} \u{1F4CB} Session Report: ${CYAN}${r.id.slice(0, 8)}\u2026${RESET}${" ".repeat(62 - 28 - r.id.slice(0, 8).length)}${BOLD}\u2551${RESET}`);
1574
+ log(`${BOLD}\u2551${RESET} \u{1F4CB} Session Report: ${CYAN}${r.id.slice(0, 8)}\u2026${RESET} (${r.agent})${" ".repeat(Math.max(0, 62 - 32 - r.id.slice(0, 8).length - r.agent.length))}${BOLD}\u2551${RESET}`);
1248
1575
  log(`${BOLD}\u255A${"\u2550".repeat(62)}\u255D${RESET}`);
1249
1576
  log("");
1250
1577
  log(`${BOLD} Overview${RESET}`);
@@ -1384,20 +1711,21 @@ function buildScreen(limit) {
1384
1711
  lines.push(` ${BOLD}${CYAN}\u2502${RESET} \u{1F916} ${BOLD}Copilot Agent Dashboard${RESET}${" ".repeat(Math.max(0, cols - 37 - timeStr.length))}${DIM}${timeStr}${RESET} ${BOLD}${CYAN}\u2502${RESET}`);
1385
1712
  lines.push(` ${BOLD}${CYAN}\u2514${"\u2500".repeat(cols - 6)}\u2518${RESET}`);
1386
1713
  lines.push("");
1387
- const procs = findCopilotProcesses();
1714
+ const procs = findAgentProcesses();
1388
1715
  lines.push(` ${BOLD}${GREEN}\u25CF Active Processes (${procs.length})${RESET}`);
1389
1716
  lines.push(` ${"\u2500".repeat(Math.min(cols - 4, 70))}`);
1390
1717
  if (procs.length === 0) {
1391
- lines.push(` ${DIM}No copilot processes running${RESET}`);
1718
+ lines.push(` ${DIM}No agent processes running${RESET}`);
1392
1719
  } else {
1393
1720
  for (const p of procs) {
1721
+ const agentTag = p.agent === "claude" ? `${CYAN}[claude]${RESET}` : `${GREEN}[copilot]${RESET}`;
1394
1722
  const sid = p.sessionId ? p.sessionId.slice(0, 8) + "\u2026" : "\u2014";
1395
1723
  const cwdShort = p.cwd ? "~/" + p.cwd.split("/").slice(-2).join("/") : "\u2014";
1396
- lines.push(` ${GREEN}\u2B24${RESET} PID ${BOLD}${p.pid}${RESET} ${CYAN}${sid}${RESET} ${DIM}${cwdShort}${RESET}`);
1724
+ lines.push(` ${GREEN}\u2B24${RESET} ${agentTag} PID ${BOLD}${p.pid}${RESET} ${CYAN}${sid}${RESET} ${DIM}${cwdShort}${RESET}`);
1397
1725
  }
1398
1726
  }
1399
1727
  lines.push("");
1400
- const sessions = listSessions(limit);
1728
+ const sessions = listAllSessions(limit);
1401
1729
  lines.push(` ${BOLD}${CYAN}\u25CF Recent Sessions (${sessions.length})${RESET}`);
1402
1730
  lines.push(` ${"\u2500".repeat(Math.min(cols - 4, 70))}`);
1403
1731
  const headerCols = [
@@ -1409,7 +1737,7 @@ function buildScreen(limit) {
1409
1737
  ];
1410
1738
  lines.push(` ${DIM}${headerCols.join(" ")}${RESET}`);
1411
1739
  for (const s of sessions) {
1412
- const report = getSessionReport(s.id);
1740
+ const report = getAgentSessionReport(s.id, s.agent);
1413
1741
  const statusIcon = s.complete ? `${GREEN}\u2714 done ${RESET}` : `${YELLOW}\u23F8 stop ${RESET}`;
1414
1742
  const premium = pad(String(s.premiumRequests), 8);
1415
1743
  const duration = report ? pad(formatDuration2(report.durationMs), 10) : pad("\u2014", 10);
@@ -1419,7 +1747,7 @@ function buildScreen(limit) {
1419
1747
  }
1420
1748
  lines.push("");
1421
1749
  if (sessions.length > 0) {
1422
- const latest = getSessionReport(sessions[0].id);
1750
+ const latest = getAgentSessionReport(sessions[0].id, sessions[0].agent);
1423
1751
  if (latest) {
1424
1752
  lines.push(` ${BOLD}${CYAN}\u25CF Latest Session Detail${RESET} ${DIM}${latest.id.slice(0, 8)}\u2026${RESET}`);
1425
1753
  lines.push(` ${"\u2500".repeat(Math.min(cols - 4, 70))}`);
@@ -1545,6 +1873,8 @@ body{background:var(--bg);color:var(--text);font-family:var(--font-sans);font-si
1545
1873
  .badge{font-size:10px;padding:1px 6px;border-radius:4px;font-weight:600;font-family:var(--font-mono);text-transform:uppercase}
1546
1874
  .badge-done{background:rgba(63,185,80,.12);color:var(--green)}
1547
1875
  .badge-stop{background:rgba(210,153,34,.12);color:var(--yellow)}
1876
+ .badge-claude{background:rgba(217,119,6,.15);color:#f59e0b}
1877
+ .badge-copilot{background:rgba(56,189,248,.12);color:#38bdf8}
1548
1878
  .detail{padding:20px;overflow-y:auto}
1549
1879
  .detail-head{display:flex;align-items:center;justify-content:space-between;margin-bottom:16px}
1550
1880
  .detail-title{font-size:18px;font-weight:700}
@@ -1572,8 +1902,6 @@ body{background:var(--bg);color:var(--text);font-family:var(--font-sans);font-si
1572
1902
  .error-list li{padding:5px 0;font-size:12px;color:var(--red)}
1573
1903
  .more{font-size:11px;color:var(--text3);padding:4px 0}
1574
1904
  .empty-detail{display:flex;align-items:center;justify-content:center;height:100%;color:var(--text3)}
1575
- .htmx-settling{opacity:0}
1576
- .htmx-added{opacity:0;transition:opacity .3s ease}
1577
1905
  @media(max-width:768px){.main{grid-template-columns:1fr}.stats{grid-template-columns:repeat(2,1fr)}}
1578
1906
  `;
1579
1907
  var layoutHead = `<!DOCTYPE html>
@@ -1643,11 +1971,13 @@ function renderStats(sessions) {
1643
1971
  ).join("");
1644
1972
  }
1645
1973
  function renderProcesses(procs) {
1646
- if (procs.length === 0) return '<div class="empty">No active copilot processes</div>';
1974
+ if (procs.length === 0) return '<div class="empty">No active agent processes</div>';
1647
1975
  return procs.map((p) => {
1648
1976
  const sid = p.sessionId ? p.sessionId.slice(0, 8) + "\u2026" : "\u2014";
1977
+ const agentBadge = p.agent === "claude" ? '<span class="badge badge-claude">claude</span>' : '<span class="badge badge-copilot">copilot</span>';
1649
1978
  return `<div class="proc">
1650
1979
  <div class="proc-dot"></div>
1980
+ ${agentBadge}
1651
1981
  <span class="proc-pid">PID ${p.pid}</span>
1652
1982
  <span class="proc-sid">${esc(sid)}</span>
1653
1983
  <span class="proc-cwd">${esc(p.cwd ?? "")}</span>
@@ -1658,16 +1988,17 @@ function renderSessionList(sessions, selectedId) {
1658
1988
  return sessions.map((s) => {
1659
1989
  const proj = (s.cwd ?? "").split("/").pop() ?? "\u2014";
1660
1990
  const isActive = s.id === selectedId;
1991
+ const agentBadge = s.agent === "claude" ? '<span class="badge badge-claude">claude</span>' : '<span class="badge badge-copilot">copilot</span>';
1661
1992
  return `<div class="s-item${isActive ? " active" : ""}"
1662
1993
  hx-get="/partial/detail/${s.id}" hx-target="#detail" hx-swap="innerHTML"
1663
1994
  onclick="document.querySelectorAll('.s-item').forEach(e=>e.classList.remove('active'));this.classList.add('active')">
1664
1995
  <div class="s-row">
1665
1996
  <span class="s-icon">${s.complete ? "\u2705" : "\u23F8\uFE0F"}</span>
1666
1997
  <div class="s-info">
1667
- <div class="s-title">${esc(proj)} \u2014 ${esc(s.summary || "(no summary)")}</div>
1998
+ <div class="s-title">${agentBadge} ${esc(proj)} \u2014 ${esc(s.summary || "(no summary)")}</div>
1668
1999
  <div class="s-meta">
1669
2000
  <span>${fmtDur(s.durationMs)}</span>
1670
- <span>${fmt(s.premiumRequests)} premium</span>
2001
+ <span>${s.agent === "claude" ? "" : fmt(s.premiumRequests) + " premium"}</span>
1671
2002
  <span>${fmtAgo(s.endTime)}</span>
1672
2003
  <span class="badge ${s.complete ? "badge-done" : "badge-stop"}">${s.complete ? "done" : "stopped"}</span>
1673
2004
  </div>
@@ -1682,8 +2013,9 @@ function renderDetail(s) {
1682
2013
  const toolEntries = Object.entries(s.toolUsage ?? {}).sort((a, b) => b[1] - a[1]);
1683
2014
  const maxTool = toolEntries[0]?.[1] ?? 1;
1684
2015
  let html = "";
2016
+ const agentBadge = s.agent === "claude" ? '<span class="badge badge-claude" style="margin-left:8px">claude</span>' : '<span class="badge badge-copilot" style="margin-left:8px">copilot</span>';
1685
2017
  html += `<div class="detail-head">
1686
- <div class="detail-title">${esc(proj)}</div>
2018
+ <div class="detail-title">${esc(proj)}${agentBadge}</div>
1687
2019
  <div class="detail-id">${s.id}</div>
1688
2020
  </div>`;
1689
2021
  html += `<div class="detail-time">${fmtTime(s.startTime)} \u2192 ${fmtTime(s.endTime)}</div>`;
@@ -1758,41 +2090,51 @@ function registerWebCommand(program2) {
1758
2090
  });
1759
2091
  }
1760
2092
  function getData() {
1761
- const sessions = listSessions(20);
1762
- const reports = sessions.map((s) => getSessionReport(s.id)).filter((r) => r !== null);
1763
- const processes = findCopilotProcesses();
2093
+ const sessions = listAllSessions(20);
2094
+ const reports = sessions.map((s) => getAgentSessionReport(s.id, s.agent)).filter((r) => r !== null);
2095
+ const processes = findAgentProcesses();
1764
2096
  return { sessions: reports, processes };
1765
2097
  }
2098
+ function simpleHash(s) {
2099
+ let h = 0;
2100
+ for (let i = 0; i < s.length; i++) {
2101
+ h = (h << 5) - h + s.charCodeAt(i) | 0;
2102
+ }
2103
+ return h.toString(36);
2104
+ }
1766
2105
  function startWebServer(port, autoOpen) {
1767
2106
  const app = new Hono();
1768
2107
  app.get("/api/sessions", (c) => c.json(getData()));
1769
2108
  app.get("/api/session/:id", (c) => {
1770
- const report = getSessionReport(c.req.param("id"));
2109
+ const report = getAgentSessionReport(c.req.param("id"));
1771
2110
  if (!report) return c.json({ error: "Not found" }, 404);
1772
2111
  return c.json(report);
1773
2112
  });
1774
2113
  app.get("/events", (c) => {
1775
2114
  return streamSSE(c, async (stream) => {
2115
+ let prevStatsHash = "";
2116
+ let prevProcsHash = "";
1776
2117
  while (true) {
1777
2118
  const { sessions, processes } = getData();
1778
- await stream.writeSSE({
1779
- event: "stats",
1780
- data: renderStats(sessions)
1781
- });
1782
- await stream.writeSSE({
1783
- event: "procs",
1784
- data: renderProcesses(processes)
1785
- });
1786
- await stream.writeSSE({
1787
- event: "proc-count",
1788
- data: String(processes.length)
1789
- });
2119
+ const statsHtml = renderStats(sessions);
2120
+ const procsHtml = renderProcesses(processes);
2121
+ const statsHash = simpleHash(statsHtml);
2122
+ const procsHash = simpleHash(procsHtml);
2123
+ if (statsHash !== prevStatsHash) {
2124
+ await stream.writeSSE({ event: "stats", data: statsHtml });
2125
+ prevStatsHash = statsHash;
2126
+ }
2127
+ if (procsHash !== prevProcsHash) {
2128
+ await stream.writeSSE({ event: "procs", data: procsHtml });
2129
+ await stream.writeSSE({ event: "proc-count", data: String(processes.length) });
2130
+ prevProcsHash = procsHash;
2131
+ }
1790
2132
  await stream.sleep(5e3);
1791
2133
  }
1792
2134
  });
1793
2135
  });
1794
2136
  app.get("/partial/detail/:id", (c) => {
1795
- const report = getSessionReport(c.req.param("id"));
2137
+ const report = getAgentSessionReport(c.req.param("id"));
1796
2138
  if (!report) return c.html('<div class="empty-detail">Session not found</div>');
1797
2139
  return c.html(renderDetail(report));
1798
2140
  });
@@ -1815,17 +2157,17 @@ function startWebServer(port, autoOpen) {
1815
2157
 
1816
2158
  <div class="stats"
1817
2159
  sse-swap="stats"
1818
- hx-swap="innerHTML">
2160
+ hx-swap="innerHTML settle:0s swap:0s">
1819
2161
  ${renderStats(sessions)}
1820
2162
  </div>
1821
2163
 
1822
2164
  <div class="procs">
1823
2165
  <div class="procs-header">
1824
- \u2B24 Active Processes <span class="count" sse-swap="proc-count" hx-swap="innerHTML">${processes.length}</span>
2166
+ \u2B24 Active Processes <span class="count" sse-swap="proc-count" hx-swap="innerHTML settle:0s swap:0s">${processes.length}</span>
1825
2167
  </div>
1826
2168
  <div class="procs-body"
1827
2169
  sse-swap="procs"
1828
- hx-swap="innerHTML">
2170
+ hx-swap="innerHTML settle:0s swap:0s">
1829
2171
  ${renderProcesses(processes)}
1830
2172
  </div>
1831
2173
  </div>
@@ -1869,7 +2211,7 @@ ${layoutFoot}`);
1869
2211
 
1870
2212
  // src/index.ts
1871
2213
  var program = new Command();
1872
- program.name("copilot-agent").version("0.7.0").description("Autonomous GitHub Copilot CLI agent \u2014 auto-resume, task discovery, overnight runs");
2214
+ program.name("copilot-agent").version("0.8.0").description("Autonomous AI agent manager \u2014 auto-resume, task discovery, overnight runs. Supports GitHub Copilot CLI + Claude Code.");
1873
2215
  registerStatusCommand(program);
1874
2216
  registerWatchCommand(program);
1875
2217
  registerRunCommand(program);