copilot-agent 0.6.0 → 0.8.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/dist/index.js CHANGED
@@ -8,9 +8,11 @@ import {
8
8
  readFileSync,
9
9
  statSync
10
10
  } from "fs";
11
- import { join, resolve } from "path";
11
+ import { join, resolve, basename } from "path";
12
12
  import { homedir } from "os";
13
13
  var SESSION_DIR = join(homedir(), ".copilot", "session-state");
14
+ var CLAUDE_DIR = join(homedir(), ".claude");
15
+ var CLAUDE_PROJECTS_DIR = join(CLAUDE_DIR, "projects");
14
16
  function validateSession(sid) {
15
17
  const events = join(SESSION_DIR, sid, "events.jsonl");
16
18
  try {
@@ -41,7 +43,8 @@ function listSessions(limit = 20) {
41
43
  premiumRequests: getSessionPremium(s.id),
42
44
  summary: getSessionSummary(s.id),
43
45
  cwd: getSessionCwd(s.id),
44
- complete: hasTaskComplete(s.id)
46
+ complete: hasTaskComplete(s.id),
47
+ agent: "copilot"
45
48
  }));
46
49
  }
47
50
  function getLatestSessionId() {
@@ -123,13 +126,6 @@ function getSessionSummary(sid) {
123
126
  function getSessionCwd(sid) {
124
127
  return readWorkspace(sid).cwd ?? "";
125
128
  }
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
129
  function getSessionReport(sid) {
134
130
  if (!validateSession(sid)) return null;
135
131
  const ws = readWorkspace(sid);
@@ -156,7 +152,8 @@ function getSessionReport(sid) {
156
152
  filesCreated: [],
157
153
  filesEdited: [],
158
154
  errors: [],
159
- taskCompletions: []
155
+ taskCompletions: [],
156
+ agent: "copilot"
160
157
  };
161
158
  for (const line of lines) {
162
159
  let event;
@@ -226,6 +223,245 @@ function getSessionReport(sid) {
226
223
  }
227
224
  return report;
228
225
  }
226
+ function decodeClaudePath(encoded) {
227
+ return encoded.replace(/^-/, "/").replace(/-/g, "/");
228
+ }
229
+ function encodeClaudePath(fsPath) {
230
+ return fsPath.replace(/\//g, "-");
231
+ }
232
+ function listClaudeSessions(limit = 20) {
233
+ if (!existsSync(CLAUDE_PROJECTS_DIR)) return [];
234
+ const sessions = [];
235
+ try {
236
+ const projectDirs = readdirSync(CLAUDE_PROJECTS_DIR, { withFileTypes: true }).filter((d) => d.isDirectory());
237
+ for (const projDir of projectDirs) {
238
+ const projPath = join(CLAUDE_PROJECTS_DIR, projDir.name);
239
+ const cwd = decodeClaudePath(projDir.name);
240
+ const files = readdirSync(projPath).filter((f) => f.endsWith(".jsonl"));
241
+ for (const file of files) {
242
+ const filePath = join(projPath, file);
243
+ const sid = basename(file, ".jsonl");
244
+ try {
245
+ const stat = statSync(filePath);
246
+ const { lastEvent, complete, summary } = parseClaudeSessionMeta(filePath);
247
+ sessions.push({
248
+ id: sid,
249
+ dir: projPath,
250
+ mtime: stat.mtimeMs,
251
+ lastEvent,
252
+ premiumRequests: 0,
253
+ summary,
254
+ cwd,
255
+ complete,
256
+ agent: "claude"
257
+ });
258
+ } catch {
259
+ }
260
+ }
261
+ }
262
+ } catch {
263
+ }
264
+ sessions.sort((a, b) => b.mtime - a.mtime);
265
+ return sessions.slice(0, limit);
266
+ }
267
+ function parseClaudeSessionMeta(filePath) {
268
+ try {
269
+ const content = readFileSync(filePath, "utf-8").trimEnd();
270
+ const lines = content.split("\n");
271
+ let lastEvent = "unknown";
272
+ let complete = false;
273
+ let summary = "";
274
+ for (let i = lines.length - 1; i >= Math.max(0, lines.length - 5); i--) {
275
+ try {
276
+ const event = JSON.parse(lines[i]);
277
+ if (i === lines.length - 1) {
278
+ lastEvent = event.type ?? event.role ?? "unknown";
279
+ }
280
+ if (event.type === "result" || event.type === "assistant" && event.stop_reason === "end_turn") {
281
+ complete = true;
282
+ }
283
+ } catch {
284
+ }
285
+ }
286
+ for (const line of lines.slice(0, 10)) {
287
+ try {
288
+ const event = JSON.parse(line);
289
+ if ((event.type === "human" || event.role === "human") && event.message) {
290
+ summary = typeof event.message === "string" ? event.message.slice(0, 100) : JSON.stringify(event.message).slice(0, 100);
291
+ break;
292
+ }
293
+ } catch {
294
+ }
295
+ }
296
+ return { lastEvent, complete, summary };
297
+ } catch {
298
+ return { lastEvent: "error", complete: false, summary: "" };
299
+ }
300
+ }
301
+ function getLatestClaudeSessionId(projectDir) {
302
+ if (!existsSync(CLAUDE_PROJECTS_DIR)) return null;
303
+ let searchDirs;
304
+ if (projectDir) {
305
+ const encoded = encodeClaudePath(resolve(projectDir));
306
+ const projPath = join(CLAUDE_PROJECTS_DIR, encoded);
307
+ searchDirs = existsSync(projPath) ? [projPath] : [];
308
+ } else {
309
+ try {
310
+ searchDirs = readdirSync(CLAUDE_PROJECTS_DIR, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => join(CLAUDE_PROJECTS_DIR, d.name));
311
+ } catch {
312
+ return null;
313
+ }
314
+ }
315
+ let latest = null;
316
+ for (const dir of searchDirs) {
317
+ try {
318
+ const files = readdirSync(dir).filter((f) => f.endsWith(".jsonl"));
319
+ for (const file of files) {
320
+ const stat = statSync(join(dir, file));
321
+ if (!latest || stat.mtimeMs > latest.mtime) {
322
+ latest = { id: basename(file, ".jsonl"), mtime: stat.mtimeMs };
323
+ }
324
+ }
325
+ } catch {
326
+ }
327
+ }
328
+ return latest?.id ?? null;
329
+ }
330
+ function getClaudeSessionCwd(sid) {
331
+ if (!existsSync(CLAUDE_PROJECTS_DIR)) return "";
332
+ try {
333
+ const projectDirs = readdirSync(CLAUDE_PROJECTS_DIR, { withFileTypes: true }).filter((d) => d.isDirectory());
334
+ for (const projDir of projectDirs) {
335
+ const filePath = join(CLAUDE_PROJECTS_DIR, projDir.name, `${sid}.jsonl`);
336
+ if (existsSync(filePath)) {
337
+ return decodeClaudePath(projDir.name);
338
+ }
339
+ }
340
+ } catch {
341
+ }
342
+ return "";
343
+ }
344
+ function getClaudeSessionReport(sid) {
345
+ if (!existsSync(CLAUDE_PROJECTS_DIR)) return null;
346
+ let filePath = null;
347
+ let cwd = "";
348
+ try {
349
+ const projectDirs = readdirSync(CLAUDE_PROJECTS_DIR, { withFileTypes: true }).filter((d) => d.isDirectory());
350
+ for (const projDir of projectDirs) {
351
+ const candidate = join(CLAUDE_PROJECTS_DIR, projDir.name, `${sid}.jsonl`);
352
+ if (existsSync(candidate)) {
353
+ filePath = candidate;
354
+ cwd = decodeClaudePath(projDir.name);
355
+ break;
356
+ }
357
+ }
358
+ } catch {
359
+ }
360
+ if (!filePath) return null;
361
+ let lines;
362
+ try {
363
+ lines = readFileSync(filePath, "utf-8").trimEnd().split("\n");
364
+ } catch {
365
+ return null;
366
+ }
367
+ const report = {
368
+ id: sid,
369
+ cwd,
370
+ summary: "",
371
+ startTime: "",
372
+ endTime: "",
373
+ durationMs: 0,
374
+ complete: false,
375
+ userMessages: 0,
376
+ assistantTurns: 0,
377
+ outputTokens: 0,
378
+ premiumRequests: 0,
379
+ toolUsage: {},
380
+ gitCommits: [],
381
+ filesCreated: [],
382
+ filesEdited: [],
383
+ errors: [],
384
+ taskCompletions: [],
385
+ agent: "claude"
386
+ };
387
+ for (const line of lines) {
388
+ let event;
389
+ try {
390
+ event = JSON.parse(line);
391
+ } catch {
392
+ continue;
393
+ }
394
+ const type = event.type ?? event.role ?? "";
395
+ const ts = event.timestamp;
396
+ if (ts && !report.startTime) report.startTime = ts;
397
+ if (ts) report.endTime = ts;
398
+ if (type === "human" || type === "user") {
399
+ report.userMessages++;
400
+ if (!report.summary) {
401
+ const msg = event.message ?? event.content;
402
+ report.summary = (typeof msg === "string" ? msg : JSON.stringify(msg ?? "")).slice(0, 100);
403
+ }
404
+ }
405
+ if (type === "assistant") {
406
+ report.assistantTurns++;
407
+ const usage = event.usage;
408
+ if (usage?.output_tokens) report.outputTokens += usage.output_tokens;
409
+ if (event.stop_reason === "end_turn") report.complete = true;
410
+ }
411
+ if (type === "tool_use") {
412
+ const toolName = event.name ?? event.tool ?? "unknown";
413
+ report.toolUsage[toolName] = (report.toolUsage[toolName] ?? 0) + 1;
414
+ if (toolName === "Bash" || toolName === "bash") {
415
+ const input = event.input ?? "";
416
+ const cmd = typeof input === "string" ? input : input?.command ?? "";
417
+ if (cmd.includes("git") && cmd.includes("commit") && cmd.includes("-m")) {
418
+ const msgMatch = cmd.match(/-m\s+"([^"]{1,120})/);
419
+ if (msgMatch) report.gitCommits.push(msgMatch[1]);
420
+ }
421
+ }
422
+ if (toolName === "Write" || toolName === "Create") {
423
+ const path = event.input?.file_path ?? event.input?.path;
424
+ if (path) report.filesCreated.push(path);
425
+ }
426
+ if (toolName === "Edit") {
427
+ const path = event.input?.file_path ?? event.input?.path;
428
+ if (path && !report.filesEdited.includes(path)) report.filesEdited.push(path);
429
+ }
430
+ }
431
+ if (type === "result") {
432
+ report.complete = true;
433
+ const result = event.result ?? event.content ?? "";
434
+ if (result) report.taskCompletions.push(typeof result === "string" ? result.slice(0, 200) : "(completed)");
435
+ }
436
+ if (type === "error") {
437
+ const msg = event.error ?? event.message ?? "unknown error";
438
+ report.errors.push(typeof msg === "string" ? msg : JSON.stringify(msg));
439
+ }
440
+ }
441
+ if (report.startTime && report.endTime) {
442
+ report.durationMs = new Date(report.endTime).getTime() - new Date(report.startTime).getTime();
443
+ }
444
+ return report;
445
+ }
446
+ function listAllSessions(limit = 20, agentFilter) {
447
+ const copilot = agentFilter === "claude" ? [] : listSessions(limit);
448
+ const claude = agentFilter === "copilot" ? [] : listClaudeSessions(limit);
449
+ const all = [...copilot, ...claude];
450
+ all.sort((a, b) => b.mtime - a.mtime);
451
+ return all.slice(0, limit);
452
+ }
453
+ function getAgentSessionReport(sid, agent) {
454
+ if (agent === "claude") return getClaudeSessionReport(sid);
455
+ if (agent === "copilot") return getSessionReport(sid);
456
+ return getSessionReport(sid) ?? getClaudeSessionReport(sid);
457
+ }
458
+ function findLatestIncompleteForAgent(agent) {
459
+ const sessions = listAllSessions(50, agent);
460
+ for (const s of sessions) {
461
+ if (!s.complete) return { id: s.id, agent: s.agent };
462
+ }
463
+ return null;
464
+ }
229
465
 
230
466
  // src/lib/process.ts
231
467
  import { execSync as execSync2, spawn } from "child_process";
@@ -308,21 +544,7 @@ function notify(message, title = "copilot-agent") {
308
544
  }
309
545
 
310
546
  // 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() {
547
+ function findAgentProcesses(agentFilter) {
326
548
  try {
327
549
  const output = execSync2("ps -eo pid,command", { encoding: "utf-8" });
328
550
  const results = [];
@@ -331,52 +553,56 @@ function findCopilotProcesses() {
331
553
  for (const line of output.split("\n")) {
332
554
  const trimmed = line.trim();
333
555
  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
- }
556
+ const isCopilot = (trimmed.includes("copilot") || trimmed.includes("@githubnext/copilot")) && !trimmed.includes("copilot-agent") && !trimmed.includes("copilot-api");
557
+ const isClaude = trimmed.includes("claude") && !trimmed.includes("claude-code") && !trimmed.includes("copilot-agent");
558
+ if (!isCopilot && !isClaude) continue;
559
+ if (trimmed.includes("ps -eo") || trimmed.includes("grep")) continue;
560
+ const agent = isClaude ? "claude" : "copilot";
561
+ if (agentFilter && agent !== agentFilter) continue;
562
+ const match = trimmed.match(/^(\d+)\s+(.+)$/);
563
+ if (!match) continue;
564
+ const pid = parseInt(match[1], 10);
565
+ if (pid === myPid || pid === parentPid) continue;
566
+ const cmd = match[2];
567
+ const sidMatch = agent === "copilot" ? cmd.match(/resume[= ]+([a-f0-9-]{36})/) : cmd.match(/(?:--resume|--session-id)[= ]+([a-f0-9-]{36})/);
568
+ let cwd;
569
+ try {
570
+ cwd = execSync2(`lsof -p ${pid} -Fn 2>/dev/null | grep '^n/' | head -1`, {
571
+ encoding: "utf-8",
572
+ stdio: ["pipe", "pipe", "pipe"]
573
+ }).trim().slice(1) || void 0;
574
+ } catch {
575
+ }
576
+ const sid = sidMatch?.[1];
577
+ if (!cwd && sid) {
578
+ cwd = (agent === "copilot" ? getSessionCwd(sid) : getClaudeSessionCwd(sid)) || void 0;
355
579
  }
580
+ results.push({ pid, command: cmd, sessionId: sid, cwd, agent });
356
581
  }
357
582
  return results;
358
583
  } catch {
359
584
  return [];
360
585
  }
361
586
  }
362
- function findPidForSession(sid) {
363
- const procs = findCopilotProcesses();
587
+ function findPidForSession(sid, agent) {
588
+ const procs = findAgentProcesses(agent);
364
589
  const matching = procs.filter((p) => p.command.includes(sid)).sort((a, b) => b.pid - a.pid);
365
590
  return matching[0]?.pid ?? null;
366
591
  }
367
- async function waitForCopilotInDir(dir, timeoutMs = 144e5, pollMs = 1e4) {
592
+ async function waitForAgentInDir(dir, agent, timeoutMs = 144e5, pollMs = 1e4) {
368
593
  const targetDir = resolve2(dir);
369
594
  const start = Date.now();
370
595
  let warned = false;
596
+ const label = agent ?? "agent";
371
597
  while (Date.now() - start < timeoutMs) {
372
- const procs = findCopilotProcesses();
598
+ const procs = findAgentProcesses(agent);
373
599
  const conflicting = procs.filter((p) => {
374
600
  if (!p.cwd) return false;
375
601
  return resolve2(p.cwd) === targetDir;
376
602
  });
377
603
  if (conflicting.length === 0) return;
378
604
  if (!warned) {
379
- warn(`Waiting for copilot in ${targetDir} to finish...`);
605
+ warn(`Waiting for ${label} in ${targetDir} to finish...`);
380
606
  for (const p of conflicting) {
381
607
  log(` PID ${p.pid}: ${p.command.slice(0, 80)}`);
382
608
  }
@@ -384,12 +610,13 @@ async function waitForCopilotInDir(dir, timeoutMs = 144e5, pollMs = 1e4) {
384
610
  }
385
611
  await sleep(pollMs);
386
612
  }
387
- warn("Timeout waiting for copilot to finish in directory");
613
+ warn(`Timeout waiting for ${label} to finish in directory`);
388
614
  }
389
- function assertSessionNotRunning(sid) {
390
- const pid = findPidForSession(sid);
615
+ function assertSessionNotRunning(sid, agent) {
616
+ const pid = findPidForSession(sid, agent);
391
617
  if (pid) {
392
- fail(`Session ${sid.slice(0, 8)}\u2026 already has copilot running (PID ${pid}). Cannot resume \u2014 would corrupt the session.`);
618
+ const label = agent ?? "agent";
619
+ fail(`Session ${sid.slice(0, 8)}\u2026 already has ${label} running (PID ${pid}). Cannot resume.`);
393
620
  process.exit(1);
394
621
  }
395
622
  }
@@ -407,9 +634,8 @@ async function waitForExit(pid, timeoutMs = 144e5) {
407
634
  }
408
635
  async function runCopilot(args, options) {
409
636
  const dir = options?.cwd ?? process.cwd();
410
- if (options?.useWorktree) {
411
- } else {
412
- await waitForCopilotInDir(dir);
637
+ if (!options?.useWorktree) {
638
+ await waitForAgentInDir(dir, "copilot");
413
639
  }
414
640
  return new Promise((resolve7) => {
415
641
  const child = spawn("copilot", args, {
@@ -421,11 +647,7 @@ async function runCopilot(args, options) {
421
647
  await sleep(3e3);
422
648
  const sid = getLatestSessionId();
423
649
  const premium = sid ? getSessionPremium(sid) : 0;
424
- resolve7({
425
- exitCode: code ?? 1,
426
- sessionId: sid,
427
- premium
428
- });
650
+ resolve7({ exitCode: code ?? 1, sessionId: sid, premium });
429
651
  });
430
652
  child.on("error", () => {
431
653
  resolve7({ exitCode: 1, sessionId: null, premium: 0 });
@@ -433,7 +655,7 @@ async function runCopilot(args, options) {
433
655
  });
434
656
  }
435
657
  function runCopilotResume(sid, steps, message, cwd) {
436
- assertSessionNotRunning(sid);
658
+ assertSessionNotRunning(sid, "copilot");
437
659
  const args = [
438
660
  `--resume=${sid}`,
439
661
  "--autopilot",
@@ -456,38 +678,82 @@ function runCopilotTask(prompt, steps, cwd, useWorktree) {
456
678
  "--no-ask-user"
457
679
  ], { cwd, useWorktree });
458
680
  }
681
+ async function runClaude(args, options) {
682
+ const dir = options?.cwd ?? process.cwd();
683
+ if (!options?.useWorktree) {
684
+ await waitForAgentInDir(dir, "claude");
685
+ }
686
+ return new Promise((resolve7) => {
687
+ const child = spawn("claude", args, {
688
+ cwd: options?.cwd,
689
+ stdio: "inherit",
690
+ env: { ...process.env }
691
+ });
692
+ child.on("close", async (code) => {
693
+ await sleep(2e3);
694
+ const sid = getLatestClaudeSessionId(options?.cwd);
695
+ resolve7({ exitCode: code ?? 1, sessionId: sid, premium: 0 });
696
+ });
697
+ child.on("error", () => {
698
+ resolve7({ exitCode: 1, sessionId: null, premium: 0 });
699
+ });
700
+ });
701
+ }
702
+ function runClaudeResume(sid, _steps, message, cwd) {
703
+ assertSessionNotRunning(sid, "claude");
704
+ const args = ["--resume", sid, "--dangerously-skip-permissions"];
705
+ if (message) args.push(message);
706
+ return runClaude(args, { cwd });
707
+ }
708
+ function runClaudeTask(prompt, _steps, cwd, useWorktree) {
709
+ return runClaude([
710
+ "--print",
711
+ "--dangerously-skip-permissions",
712
+ "--output-format",
713
+ "text",
714
+ prompt
715
+ ], { cwd, useWorktree });
716
+ }
717
+ function runAgentTask(agent, prompt, steps, cwd, useWorktree) {
718
+ return agent === "claude" ? runClaudeTask(prompt, steps, cwd, useWorktree) : runCopilotTask(prompt, steps, cwd, useWorktree);
719
+ }
720
+ function runAgentResume(agent, sid, steps, message, cwd) {
721
+ return agent === "claude" ? runClaudeResume(sid, steps, message, cwd) : runCopilotResume(sid, steps, message, cwd);
722
+ }
459
723
  function sleep(ms) {
460
724
  return new Promise((r) => setTimeout(r, ms));
461
725
  }
462
726
 
463
727
  // src/commands/status.ts
464
728
  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) => {
729
+ 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) => {
730
+ const agentFilter = opts.agent;
466
731
  if (opts.active) {
467
- showActive();
732
+ showActive(agentFilter);
468
733
  } else {
469
- showRecent(parseInt(opts.limit, 10), opts.incomplete ?? false);
734
+ showRecent(parseInt(opts.limit, 10), opts.incomplete ?? false, agentFilter);
470
735
  }
471
736
  });
472
737
  }
473
- function showActive() {
474
- const procs = findCopilotProcesses();
738
+ function showActive(agentFilter) {
739
+ const procs = findAgentProcesses(agentFilter);
475
740
  if (procs.length === 0) {
476
- log(`${DIM}No active copilot processes.${RESET}`);
741
+ log(`${DIM}No active agent processes.${RESET}`);
477
742
  return;
478
743
  }
479
744
  log(`
480
- ${BOLD}${"PID".padEnd(8)} ${"Session".padEnd(40)} Command${RESET}`);
481
- log("\u2500".repeat(108));
745
+ ${BOLD}${"Agent".padEnd(9)} ${"PID".padEnd(8)} ${"Session".padEnd(40)} Command${RESET}`);
746
+ log("\u2500".repeat(118));
482
747
  for (const p of procs) {
748
+ const agentLabel = p.agent === "claude" ? `${CYAN}claude${RESET} ` : `${GREEN}copilot${RESET}`;
483
749
  log(
484
- `${CYAN}${String(p.pid).padEnd(8)}${RESET} ${(p.sessionId ?? "\u2014").padEnd(40)} ${truncate(p.command, 58)}`
750
+ `${agentLabel.padEnd(9 + 9)} ${String(p.pid).padEnd(8)} ${(p.sessionId ?? "\u2014").padEnd(40)} ${truncate(p.command, 50)}`
485
751
  );
486
752
  }
487
753
  log("");
488
754
  }
489
- function showRecent(limit, incompleteOnly) {
490
- let sessions = listSessions(limit);
755
+ function showRecent(limit, incompleteOnly, agentFilter) {
756
+ let sessions = listAllSessions(limit, agentFilter);
491
757
  if (incompleteOnly) {
492
758
  sessions = sessions.filter((s) => !s.complete);
493
759
  }
@@ -497,15 +763,16 @@ function showRecent(limit, incompleteOnly) {
497
763
  }
498
764
  log(
499
765
  `
500
- ${BOLD}${"Status".padEnd(10)} ${"Premium".padEnd(10)} ${"Last Event".padEnd(25)} ${"Summary".padEnd(40)} ID${RESET}`
766
+ ${BOLD}${"Agent".padEnd(9)} ${"Status".padEnd(10)} ${"Premium".padEnd(10)} ${"Last Event".padEnd(22)} ${"Summary".padEnd(35)} ID${RESET}`
501
767
  );
502
- log("\u2500".repeat(120));
768
+ log("\u2500".repeat(130));
503
769
  for (const s of sessions) {
770
+ const agentLabel = s.agent === "claude" ? `${CYAN}claude${RESET} ` : `${GREEN}copilot${RESET}`;
504
771
  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);
772
+ const premium = s.agent === "claude" ? "\u2014" : String(s.premiumRequests);
773
+ const summary = truncate(s.summary || "\u2014", 33);
507
774
  log(
508
- `${status.padEnd(10 + 9)} ${premium.padEnd(10)} ${s.lastEvent.padEnd(25)} ${summary.padEnd(40)} ${DIM}${s.id}${RESET}`
775
+ `${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
776
  );
510
777
  }
511
778
  log(`
@@ -516,15 +783,55 @@ function truncate(s, max) {
516
783
  return s.substring(0, max - 1) + "\u2026";
517
784
  }
518
785
 
786
+ // src/lib/provider.ts
787
+ import { execSync as execSync3 } from "child_process";
788
+ function detectAvailableAgents() {
789
+ const agents = [];
790
+ try {
791
+ execSync3("which copilot", { stdio: "pipe" });
792
+ agents.push("copilot");
793
+ } catch {
794
+ }
795
+ try {
796
+ execSync3("which claude", { stdio: "pipe" });
797
+ agents.push("claude");
798
+ } catch {
799
+ }
800
+ return agents;
801
+ }
802
+ function resolveAgent(explicit) {
803
+ if (explicit === "copilot" || explicit === "claude") return explicit;
804
+ const available = detectAvailableAgents();
805
+ if (available.includes("copilot")) return "copilot";
806
+ if (available.includes("claude")) return "claude";
807
+ return "copilot";
808
+ }
809
+ function isAgentInstalled(agent) {
810
+ try {
811
+ execSync3(`which ${agent}`, { stdio: "pipe" });
812
+ return true;
813
+ } catch {
814
+ return false;
815
+ }
816
+ }
817
+ function assertAgent(agent) {
818
+ if (!isAgentInstalled(agent)) {
819
+ const installHint = agent === "copilot" ? "npm i -g @githubnext/copilot" : "npm i -g @anthropic-ai/claude-code";
820
+ console.error(`\u2716 ${agent} CLI not found. Install with: ${installHint}`);
821
+ process.exit(1);
822
+ }
823
+ }
824
+
519
825
  // src/commands/watch.ts
520
826
  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) => {
827
+ 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
828
  try {
523
829
  await watchCommand(sid, {
524
830
  steps: parseInt(opts.steps, 10),
525
831
  maxResumes: parseInt(opts.maxResumes, 10),
526
832
  cooldown: parseInt(opts.cooldown, 10),
527
- message: opts.message
833
+ message: opts.message,
834
+ agent: resolveAgent(opts.agent)
528
835
  });
529
836
  } catch (err) {
530
837
  fail(`Watch error: ${err instanceof Error ? err.message : err}`);
@@ -533,14 +840,15 @@ function registerWatchCommand(program2) {
533
840
  });
534
841
  }
535
842
  async function watchCommand(sid, opts) {
536
- assertCopilot();
843
+ assertAgent(opts.agent);
537
844
  if (!sid) {
538
- sid = findLatestIncomplete() ?? void 0;
845
+ const result = findLatestIncompleteForAgent(opts.agent);
846
+ sid = result?.id;
539
847
  if (!sid) {
540
848
  fail("No incomplete session found.");
541
849
  process.exit(1);
542
850
  }
543
- info(`Auto-detected incomplete session: ${CYAN}${sid}${RESET}`);
851
+ info(`Auto-detected incomplete ${opts.agent} session: ${CYAN}${sid}${RESET}`);
544
852
  }
545
853
  if (!validateSession(sid)) {
546
854
  fail(`Invalid session: ${sid}`);
@@ -552,9 +860,9 @@ async function watchCommand(sid, opts) {
552
860
  }
553
861
  let resumes = 0;
554
862
  while (resumes < opts.maxResumes) {
555
- const pid = findPidForSession(sid);
863
+ const pid = findPidForSession(sid, opts.agent);
556
864
  if (pid) {
557
- info(`Watching PID ${pid} for session ${CYAN}${sid.slice(0, 8)}${RESET}\u2026`);
865
+ info(`Watching PID ${pid} for ${opts.agent} session ${CYAN}${sid.slice(0, 8)}${RESET}\u2026`);
558
866
  const exited = await waitForExit(pid);
559
867
  if (!exited) {
560
868
  warn("Timeout waiting for process exit.");
@@ -574,7 +882,8 @@ async function watchCommand(sid, opts) {
574
882
  await sleep2(opts.cooldown * 1e3);
575
883
  }
576
884
  const cwd = getSessionCwd(sid) || void 0;
577
- const result = await runCopilotResume(
885
+ const result = await runAgentResume(
886
+ opts.agent,
578
887
  sid,
579
888
  opts.steps,
580
889
  opts.message ?? "Continue remaining work. Pick up where you left off and complete the task.",
@@ -594,8 +903,8 @@ function sleep2(ms) {
594
903
 
595
904
  // src/lib/detect.ts
596
905
  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";
906
+ import { join as join2, basename as basename2, resolve as resolve3 } from "path";
907
+ import { execSync as execSync4 } from "child_process";
599
908
  function detectProjectType(dir) {
600
909
  const exists = (f) => existsSync2(join2(dir, f));
601
910
  if (exists("build.gradle.kts") || exists("build.gradle")) {
@@ -639,11 +948,11 @@ function detectProjectName(dir) {
639
948
  if (pkg.name) return pkg.name;
640
949
  } catch {
641
950
  }
642
- return basename(resolve3(dir));
951
+ return basename2(resolve3(dir));
643
952
  }
644
953
  function detectMainBranch(dir) {
645
954
  try {
646
- const ref = execSync3("git symbolic-ref refs/remotes/origin/HEAD", {
955
+ const ref = execSync4("git symbolic-ref refs/remotes/origin/HEAD", {
647
956
  cwd: dir,
648
957
  encoding: "utf-8",
649
958
  stdio: ["pipe", "pipe", "pipe"]
@@ -652,7 +961,7 @@ function detectMainBranch(dir) {
652
961
  } catch {
653
962
  }
654
963
  try {
655
- const branch = execSync3("git branch --show-current", {
964
+ const branch = execSync4("git branch --show-current", {
656
965
  cwd: dir,
657
966
  encoding: "utf-8",
658
967
  stdio: ["pipe", "pipe", "pipe"]
@@ -818,10 +1127,10 @@ async function withLock(name, fn) {
818
1127
  // src/lib/git.ts
819
1128
  import { existsSync as existsSync4 } from "fs";
820
1129
  import { join as join4, resolve as resolve4 } from "path";
821
- import { execSync as execSync4 } from "child_process";
1130
+ import { execSync as execSync5 } from "child_process";
822
1131
  function gitExec(dir, cmd) {
823
1132
  try {
824
- return execSync4(cmd, {
1133
+ return execSync5(cmd, {
825
1134
  cwd: dir,
826
1135
  encoding: "utf-8",
827
1136
  stdio: ["pipe", "pipe", "pipe"]
@@ -873,14 +1182,15 @@ function removeWorktree(repoDir, worktreePath) {
873
1182
 
874
1183
  // src/commands/run.ts
875
1184
  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) => {
1185
+ 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
1186
  try {
878
1187
  await runCommand(dir ?? process.cwd(), {
879
1188
  steps: parseInt(opts.steps, 10),
880
1189
  maxTasks: parseInt(opts.maxTasks, 10),
881
1190
  maxPremium: parseInt(opts.maxPremium, 10),
882
1191
  dryRun: opts.dryRun ?? false,
883
- useWorktree: opts.worktree ?? false
1192
+ useWorktree: opts.worktree ?? false,
1193
+ agent: resolveAgent(opts.agent)
884
1194
  });
885
1195
  } catch (err) {
886
1196
  fail(`Run error: ${err instanceof Error ? err.message : err}`);
@@ -889,11 +1199,11 @@ function registerRunCommand(program2) {
889
1199
  });
890
1200
  }
891
1201
  async function runCommand(dir, opts) {
892
- assertCopilot();
1202
+ assertAgent(opts.agent);
893
1203
  const projectType = detectProjectType(dir);
894
1204
  const name = detectProjectName(dir);
895
1205
  const mainBranch = isGitRepo(dir) ? detectMainBranch(dir) : null;
896
- info(`Project: ${CYAN}${name}${RESET} (${projectType})`);
1206
+ info(`Project: ${CYAN}${name}${RESET} (${projectType}) \u2014 agent: ${opts.agent}`);
897
1207
  if (mainBranch) info(`Main branch: ${mainBranch}`);
898
1208
  const tasks = getTasksForProject(projectType).slice(0, opts.maxTasks);
899
1209
  if (tasks.length === 0) {
@@ -935,9 +1245,14 @@ ${"\u2550".repeat(60)}`);
935
1245
  let worktreeCreated = false;
936
1246
  if (opts.useWorktree && isGitRepo(dir)) {
937
1247
  try {
938
- taskDir = createWorktree(dir, branchName, mainBranch ?? void 0);
939
- worktreeCreated = true;
940
- info(`Created worktree: ${taskDir}`);
1248
+ const wt = createWorktree(dir, branchName);
1249
+ if (wt) {
1250
+ taskDir = wt;
1251
+ worktreeCreated = true;
1252
+ info(`Created worktree: ${taskDir}`);
1253
+ } else {
1254
+ warn("Worktree creation returned null, falling back to main dir");
1255
+ }
941
1256
  } catch (err) {
942
1257
  warn(`Worktree creation failed, falling back to main dir: ${err}`);
943
1258
  taskDir = dir;
@@ -945,7 +1260,7 @@ ${"\u2550".repeat(60)}`);
945
1260
  }
946
1261
  const result = await withLock(
947
1262
  "copilot-run",
948
- () => runCopilotTask(task.prompt, opts.steps, taskDir, opts.useWorktree)
1263
+ () => runAgentTask(opts.agent, task.prompt, opts.steps, taskDir, opts.useWorktree)
949
1264
  );
950
1265
  const commitRef = worktreeCreated ? taskDir : dir;
951
1266
  const commits = mainBranch ? gitCountCommits(commitRef, mainBranch, "HEAD") : 0;
@@ -973,7 +1288,7 @@ ${BOLD}\u2550\u2550\u2550 Run Summary \u2550\u2550\u2550${RESET}`);
973
1288
  import { join as join5 } from "path";
974
1289
  import { homedir as homedir3 } from "os";
975
1290
  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) => {
1291
+ 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
1292
  try {
978
1293
  await overnightCommand(dir ?? process.cwd(), {
979
1294
  until: parseInt(opts.until, 10),
@@ -981,7 +1296,8 @@ function registerOvernightCommand(program2) {
981
1296
  cooldown: parseInt(opts.cooldown, 10),
982
1297
  maxPremium: parseInt(opts.maxPremium, 10),
983
1298
  dryRun: opts.dryRun ?? false,
984
- useWorktree: opts.worktree ?? false
1299
+ useWorktree: opts.worktree ?? false,
1300
+ agent: resolveAgent(opts.agent)
985
1301
  });
986
1302
  } catch (err) {
987
1303
  fail(`Overnight error: ${err instanceof Error ? err.message : err}`);
@@ -994,14 +1310,14 @@ function isPastDeadline(untilHour) {
994
1310
  return hour >= untilHour && hour < 20;
995
1311
  }
996
1312
  async function overnightCommand(dir, opts) {
997
- assertCopilot();
1313
+ assertAgent(opts.agent);
998
1314
  const ts = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "").slice(0, 15);
999
1315
  const logPath = join5(homedir3(), ".copilot", "auto-resume-logs", `overnight-${ts}.log`);
1000
1316
  setLogFile(logPath);
1001
1317
  const name = detectProjectName(dir);
1002
1318
  const projectType = detectProjectType(dir);
1003
1319
  const mainBranch = isGitRepo(dir) ? detectMainBranch(dir) : null;
1004
- info(`Overnight runner for ${CYAN}${name}${RESET} (${projectType})`);
1320
+ info(`Overnight runner for ${CYAN}${name}${RESET} (${projectType}) \u2014 agent: ${opts.agent}`);
1005
1321
  info(`Deadline: ${String(opts.until).padStart(2, "0")}:00`);
1006
1322
  info(`Max premium: ${opts.maxPremium}, Steps: ${opts.steps}`);
1007
1323
  info(`Log: ${logPath}`);
@@ -1012,18 +1328,20 @@ Would run ${tasks.length} tasks:`);
1012
1328
  for (const t of tasks) log(` ${DIM}\u2022${RESET} ${t.title}`);
1013
1329
  return;
1014
1330
  }
1015
- const existingSession = findLatestIncomplete();
1331
+ const existingResult = findLatestIncompleteForAgent(opts.agent);
1332
+ const existingSession = existingResult?.id;
1016
1333
  if (existingSession && validateSession(existingSession)) {
1017
1334
  info(`Found incomplete session: ${existingSession}`);
1018
- const pid = findPidForSession(existingSession);
1335
+ const pid = findPidForSession(existingSession, opts.agent);
1019
1336
  if (pid) {
1020
- info(`Waiting for running copilot (PID ${pid})...`);
1337
+ info(`Waiting for running ${opts.agent} (PID ${pid})...`);
1021
1338
  await waitForExit(pid);
1022
1339
  }
1023
1340
  if (!hasTaskComplete(existingSession) && !isPastDeadline(opts.until)) {
1024
1341
  info("Resuming incomplete session...");
1025
1342
  const cwd = getSessionCwd(existingSession) || dir;
1026
- await runCopilotResume(
1343
+ await runAgentResume(
1344
+ opts.agent,
1027
1345
  existingSession,
1028
1346
  opts.steps,
1029
1347
  "Continue remaining work. Complete the task.",
@@ -1060,9 +1378,14 @@ ${"\u2550".repeat(60)}`);
1060
1378
  let worktreeCreated = false;
1061
1379
  if (opts.useWorktree && isGitRepo(dir)) {
1062
1380
  try {
1063
- taskDir = createWorktree(dir, branchName, mainBranch ?? void 0);
1064
- worktreeCreated = true;
1065
- info(`Created worktree: ${taskDir}`);
1381
+ const wt = createWorktree(dir, branchName);
1382
+ if (wt) {
1383
+ taskDir = wt;
1384
+ worktreeCreated = true;
1385
+ info(`Created worktree: ${taskDir}`);
1386
+ } else {
1387
+ warn("Worktree creation returned null, falling back to main dir");
1388
+ }
1066
1389
  } catch (err) {
1067
1390
  warn(`Worktree creation failed, falling back to main dir: ${err}`);
1068
1391
  taskDir = dir;
@@ -1071,7 +1394,7 @@ ${"\u2550".repeat(60)}`);
1071
1394
  try {
1072
1395
  const result = await withLock(
1073
1396
  "copilot-overnight",
1074
- () => runCopilotTask(task.prompt, opts.steps, taskDir, opts.useWorktree)
1397
+ () => runAgentTask(opts.agent, task.prompt, opts.steps, taskDir, opts.useWorktree)
1075
1398
  );
1076
1399
  const commitRef = worktreeCreated ? taskDir : dir;
1077
1400
  const commits = mainBranch ? gitCountCommits(commitRef, mainBranch, "HEAD") : 0;
@@ -1117,10 +1440,12 @@ import { existsSync as existsSync5, copyFileSync, mkdirSync as mkdirSync3 } from
1117
1440
  import { join as join6, resolve as resolve5 } from "path";
1118
1441
  import { homedir as homedir4 } from "os";
1119
1442
  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) => {
1443
+ 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
1444
  try {
1445
+ const agent = resolveAgent(opts.agent);
1122
1446
  await researchCommand(project ?? process.cwd(), {
1123
- steps: parseInt(opts.steps, 10)
1447
+ steps: parseInt(opts.steps, 10),
1448
+ agent
1124
1449
  });
1125
1450
  } catch (err) {
1126
1451
  fail(`Research error: ${err instanceof Error ? err.message : err}`);
@@ -1149,15 +1474,15 @@ For each proposal, include:
1149
1474
  Write RESEARCH-PROPOSALS.md in the project root.`;
1150
1475
  }
1151
1476
  async function researchCommand(dir, opts) {
1152
- assertCopilot();
1477
+ assertAgent(opts.agent);
1153
1478
  const projectDir = resolve5(dir);
1154
1479
  const projectType = detectProjectType(projectDir);
1155
1480
  const projectName = detectProjectName(projectDir);
1156
- info(`Researching: ${CYAN}${projectName}${RESET} (${projectType})`);
1481
+ info(`Researching: ${CYAN}${projectName}${RESET} (${projectType}) \u2014 agent: ${opts.agent}`);
1157
1482
  const prompt = buildResearchPrompt(projectType, projectName);
1158
1483
  const result = await withLock(
1159
1484
  "copilot-research",
1160
- () => runCopilotTask(prompt, opts.steps, projectDir)
1485
+ () => runAgentTask(opts.agent, prompt, opts.steps, projectDir)
1161
1486
  );
1162
1487
  log(`Copilot exited with code ${result.exitCode}`);
1163
1488
  const proposalsFile = join6(projectDir, "RESEARCH-PROPOSALS.md");
@@ -1178,12 +1503,12 @@ async function researchCommand(dir, opts) {
1178
1503
  // src/commands/report.ts
1179
1504
  import { resolve as resolve6 } from "path";
1180
1505
  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) => {
1506
+ 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
1507
  try {
1183
1508
  if (sessionId) {
1184
- reportSingle(sessionId, opts.json ?? false);
1509
+ reportSingle(sessionId, opts.json ?? false, opts.agent);
1185
1510
  } else {
1186
- reportRecent(parseInt(opts.limit, 10), opts.project, opts.json ?? false);
1511
+ reportRecent(parseInt(opts.limit, 10), opts.project, opts.json ?? false, opts.agent);
1187
1512
  }
1188
1513
  } catch (err) {
1189
1514
  fail(`Report error: ${err instanceof Error ? err.message : err}`);
@@ -1191,8 +1516,8 @@ function registerReportCommand(program2) {
1191
1516
  }
1192
1517
  });
1193
1518
  }
1194
- function reportRecent(limit, projectDir, json = false) {
1195
- let sessions = listSessions(limit * 3);
1519
+ function reportRecent(limit, projectDir, json = false, agentFilter) {
1520
+ let sessions = listAllSessions(limit * 3, agentFilter);
1196
1521
  if (projectDir) {
1197
1522
  const target = resolve6(projectDir);
1198
1523
  sessions = sessions.filter((s) => s.cwd && resolve6(s.cwd) === target);
@@ -1203,11 +1528,11 @@ function reportRecent(limit, projectDir, json = false) {
1203
1528
  return;
1204
1529
  }
1205
1530
  for (const s of sessions) {
1206
- reportSingle(s.id, json);
1531
+ reportSingle(s.id, json, s.agent);
1207
1532
  }
1208
1533
  }
1209
- function reportSingle(sid, json = false) {
1210
- const report = getSessionReport(sid);
1534
+ function reportSingle(sid, json = false, agent) {
1535
+ const report = getAgentSessionReport(sid, agent);
1211
1536
  if (!report) {
1212
1537
  warn(`Session ${sid} not found or invalid.`);
1213
1538
  return;
@@ -1244,7 +1569,7 @@ function renderReport(r) {
1244
1569
  const projectName = r.cwd.split("/").pop() ?? r.cwd;
1245
1570
  log("");
1246
1571
  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}`);
1572
+ 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
1573
  log(`${BOLD}\u255A${"\u2550".repeat(62)}\u255D${RESET}`);
1249
1574
  log("");
1250
1575
  log(`${BOLD} Overview${RESET}`);
@@ -1331,14 +1656,567 @@ function renderReport(r) {
1331
1656
  log("");
1332
1657
  }
1333
1658
 
1659
+ // src/commands/dashboard.ts
1660
+ var ESC = "\x1B";
1661
+ var CLEAR = `${ESC}[2J${ESC}[H`;
1662
+ var HIDE_CURSOR = `${ESC}[?25l`;
1663
+ var SHOW_CURSOR = `${ESC}[?25h`;
1664
+ var SAVE_CURSOR = `${ESC}7`;
1665
+ var RESTORE_CURSOR = `${ESC}8`;
1666
+ function registerDashboardCommand(program2) {
1667
+ program2.command("dashboard").alias("tui").description("Real-time terminal dashboard for copilot sessions").option("-r, --refresh <n>", "Refresh interval in seconds", "5").option("-l, --limit <n>", "Number of sessions to show", "8").action((opts) => {
1668
+ runDashboard(parseInt(opts.refresh, 10), parseInt(opts.limit, 10));
1669
+ });
1670
+ }
1671
+ function runDashboard(refreshSec, limit) {
1672
+ process.stdout.write(HIDE_CURSOR);
1673
+ const cleanup = () => {
1674
+ process.stdout.write(SHOW_CURSOR);
1675
+ process.stdout.write(CLEAR);
1676
+ process.exit(0);
1677
+ };
1678
+ process.on("SIGINT", cleanup);
1679
+ process.on("SIGTERM", cleanup);
1680
+ const render = () => {
1681
+ try {
1682
+ const output = buildScreen(limit);
1683
+ process.stdout.write(CLEAR + output);
1684
+ } catch {
1685
+ }
1686
+ };
1687
+ render();
1688
+ const timer = setInterval(render, refreshSec * 1e3);
1689
+ process.stdout.on("resize", render);
1690
+ process.stdin.setRawMode?.(true);
1691
+ process.stdin.resume();
1692
+ process.stdin.on("data", (data) => {
1693
+ const key = data.toString();
1694
+ if (key === "q" || key === "") {
1695
+ clearInterval(timer);
1696
+ cleanup();
1697
+ }
1698
+ if (key === "r") render();
1699
+ });
1700
+ }
1701
+ function buildScreen(limit) {
1702
+ const cols = process.stdout.columns || 80;
1703
+ const rows = process.stdout.rows || 40;
1704
+ const lines = [];
1705
+ const now = /* @__PURE__ */ new Date();
1706
+ const timeStr = now.toLocaleTimeString("en-GB");
1707
+ lines.push("");
1708
+ lines.push(` ${BOLD}${CYAN}\u250C${"\u2500".repeat(cols - 6)}\u2510${RESET}`);
1709
+ 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}`);
1710
+ lines.push(` ${BOLD}${CYAN}\u2514${"\u2500".repeat(cols - 6)}\u2518${RESET}`);
1711
+ lines.push("");
1712
+ const procs = findAgentProcesses();
1713
+ lines.push(` ${BOLD}${GREEN}\u25CF Active Processes (${procs.length})${RESET}`);
1714
+ lines.push(` ${"\u2500".repeat(Math.min(cols - 4, 70))}`);
1715
+ if (procs.length === 0) {
1716
+ lines.push(` ${DIM}No agent processes running${RESET}`);
1717
+ } else {
1718
+ for (const p of procs) {
1719
+ const agentTag = p.agent === "claude" ? `${CYAN}[claude]${RESET}` : `${GREEN}[copilot]${RESET}`;
1720
+ const sid = p.sessionId ? p.sessionId.slice(0, 8) + "\u2026" : "\u2014";
1721
+ const cwdShort = p.cwd ? "~/" + p.cwd.split("/").slice(-2).join("/") : "\u2014";
1722
+ lines.push(` ${GREEN}\u2B24${RESET} ${agentTag} PID ${BOLD}${p.pid}${RESET} ${CYAN}${sid}${RESET} ${DIM}${cwdShort}${RESET}`);
1723
+ }
1724
+ }
1725
+ lines.push("");
1726
+ const sessions = listAllSessions(limit);
1727
+ lines.push(` ${BOLD}${CYAN}\u25CF Recent Sessions (${sessions.length})${RESET}`);
1728
+ lines.push(` ${"\u2500".repeat(Math.min(cols - 4, 70))}`);
1729
+ const headerCols = [
1730
+ pad("Status", 10),
1731
+ pad("Premium", 8),
1732
+ pad("Duration", 10),
1733
+ pad("Project", 18),
1734
+ pad("Last Activity", 20)
1735
+ ];
1736
+ lines.push(` ${DIM}${headerCols.join(" ")}${RESET}`);
1737
+ for (const s of sessions) {
1738
+ const report = getAgentSessionReport(s.id, s.agent);
1739
+ const statusIcon = s.complete ? `${GREEN}\u2714 done ${RESET}` : `${YELLOW}\u23F8 stop ${RESET}`;
1740
+ const premium = pad(String(s.premiumRequests), 8);
1741
+ const duration = report ? pad(formatDuration2(report.durationMs), 10) : pad("\u2014", 10);
1742
+ const project = pad(s.cwd.split("/").pop() ?? "\u2014", 18);
1743
+ const lastAct = pad(formatTimeAgo(s.mtime), 20);
1744
+ lines.push(` ${statusIcon}${premium}${duration}${project}${lastAct}`);
1745
+ }
1746
+ lines.push("");
1747
+ if (sessions.length > 0) {
1748
+ const latest = getAgentSessionReport(sessions[0].id, sessions[0].agent);
1749
+ if (latest) {
1750
+ lines.push(` ${BOLD}${CYAN}\u25CF Latest Session Detail${RESET} ${DIM}${latest.id.slice(0, 8)}\u2026${RESET}`);
1751
+ lines.push(` ${"\u2500".repeat(Math.min(cols - 4, 70))}`);
1752
+ lines.push(` Turns: ${BOLD}${latest.assistantTurns}${RESET} Tokens: ${BOLD}${latest.outputTokens.toLocaleString()}${RESET} Premium: ${BOLD}${latest.premiumRequests}${RESET} Commits: ${BOLD}${latest.gitCommits.length}${RESET} Files: ${BOLD}${latest.filesEdited.length}${RESET} edited, ${BOLD}${latest.filesCreated.length}${RESET} created`);
1753
+ lines.push("");
1754
+ const toolEntries = Object.entries(latest.toolUsage).sort((a, b) => b[1] - a[1]).slice(0, 5);
1755
+ if (toolEntries.length > 0) {
1756
+ const maxVal = toolEntries[0][1];
1757
+ const barW = Math.min(20, Math.floor((cols - 30) / 2));
1758
+ lines.push(` ${DIM}Tools:${RESET}`);
1759
+ for (const [tool, count] of toolEntries) {
1760
+ const filled = Math.round(count / maxVal * barW);
1761
+ const b = `${CYAN}${"\u2588".repeat(filled)}${DIM}${"\u2591".repeat(barW - filled)}${RESET}`;
1762
+ lines.push(` ${b} ${String(count).padStart(4)} ${tool}`);
1763
+ }
1764
+ lines.push("");
1765
+ }
1766
+ if (latest.gitCommits.length > 0) {
1767
+ const maxCommits = Math.min(3, latest.gitCommits.length);
1768
+ lines.push(` ${DIM}Recent commits:${RESET}`);
1769
+ for (let i = 0; i < maxCommits; i++) {
1770
+ const msg = latest.gitCommits[i].split("\n")[0].slice(0, cols - 10);
1771
+ lines.push(` ${GREEN}\u25CF${RESET} ${msg}`);
1772
+ }
1773
+ if (latest.gitCommits.length > maxCommits) {
1774
+ lines.push(` ${DIM} \u2026 +${latest.gitCommits.length - maxCommits} more${RESET}`);
1775
+ }
1776
+ lines.push("");
1777
+ }
1778
+ if (latest.taskCompletions.length > 0) {
1779
+ const maxTasks = Math.min(3, latest.taskCompletions.length);
1780
+ lines.push(` ${DIM}Completed tasks:${RESET}`);
1781
+ for (let i = 0; i < maxTasks; i++) {
1782
+ const msg = latest.taskCompletions[i].split("\n")[0].slice(0, cols - 10);
1783
+ lines.push(` ${GREEN}\u2714${RESET} ${msg}`);
1784
+ }
1785
+ if (latest.taskCompletions.length > maxTasks) {
1786
+ lines.push(` ${DIM} \u2026 +${latest.taskCompletions.length - maxTasks} more${RESET}`);
1787
+ }
1788
+ }
1789
+ }
1790
+ }
1791
+ lines.push("");
1792
+ lines.push(` ${DIM}Press ${BOLD}q${RESET}${DIM} to quit, ${BOLD}r${RESET}${DIM} to refresh${RESET}`);
1793
+ return lines.join("\n");
1794
+ }
1795
+ function pad(s, n) {
1796
+ if (s.length >= n) return s.slice(0, n);
1797
+ return s + " ".repeat(n - s.length);
1798
+ }
1799
+ function formatDuration2(ms) {
1800
+ if (ms < 6e4) return `${Math.round(ms / 1e3)}s`;
1801
+ if (ms < 36e5) return `${Math.round(ms / 6e4)}m`;
1802
+ const h = Math.floor(ms / 36e5);
1803
+ const m = Math.round(ms % 36e5 / 6e4);
1804
+ return `${h}h ${m}m`;
1805
+ }
1806
+ function formatTimeAgo(mtimeMs) {
1807
+ const diff = Date.now() - mtimeMs;
1808
+ if (diff < 6e4) return "just now";
1809
+ if (diff < 36e5) return `${Math.round(diff / 6e4)}m ago`;
1810
+ if (diff < 864e5) return `${Math.round(diff / 36e5)}h ago`;
1811
+ return `${Math.round(diff / 864e5)}d ago`;
1812
+ }
1813
+
1814
+ // src/commands/web.ts
1815
+ import { Hono } from "hono";
1816
+ import { streamSSE } from "hono/streaming";
1817
+ import { serve } from "@hono/node-server";
1818
+ import { spawn as spawn2 } from "child_process";
1819
+
1820
+ // src/web/layout.ts
1821
+ var cssStyles = `
1822
+ *{margin:0;padding:0;box-sizing:border-box}
1823
+ :root{
1824
+ --bg:#0d1117;--bg2:#161b22;--bg3:#21262d;--bg4:#292e36;
1825
+ --border:#30363d;--border2:#3d444d;
1826
+ --text:#e6edf3;--text2:#8b949e;--text3:#484f58;
1827
+ --cyan:#58a6ff;--green:#3fb950;--yellow:#d29922;--red:#f85149;--purple:#bc8cff;--orange:#f0883e;
1828
+ --font-sans:'Inter',system-ui,sans-serif;
1829
+ --font-mono:'JetBrains Mono','SF Mono',monospace;
1830
+ --radius:8px;
1831
+ }
1832
+ body{background:var(--bg);color:var(--text);font-family:var(--font-sans);font-size:14px;line-height:1.5;-webkit-font-smoothing:antialiased}
1833
+ .header{background:var(--bg2);border-bottom:1px solid var(--border);padding:12px 24px;display:flex;align-items:center;justify-content:space-between;position:sticky;top:0;z-index:10;backdrop-filter:blur(12px)}
1834
+ .header-left{display:flex;align-items:center;gap:10px}
1835
+ .header-left h1{font-size:16px;font-weight:600}
1836
+ .header-left h1 span{color:var(--cyan)}
1837
+ .live-badge{display:flex;align-items:center;gap:6px;font-size:12px;color:var(--green);background:rgba(63,185,80,.1);padding:3px 10px;border-radius:12px;font-weight:500}
1838
+ .live-dot{width:6px;height:6px;border-radius:50%;background:var(--green);animation:pulse 2s infinite}
1839
+ @keyframes pulse{0%,100%{opacity:1}50%{opacity:.3}}
1840
+ .clock{font-family:var(--font-mono);font-size:13px;color:var(--text2)}
1841
+ .container{max-width:1280px;margin:0 auto;padding:16px 20px}
1842
+ .stats{display:grid;grid-template-columns:repeat(auto-fit,minmax(160px,1fr));gap:10px;margin-bottom:16px}
1843
+ .stat{background:var(--bg2);border:1px solid var(--border);border-radius:var(--radius);padding:14px 16px;transition:border-color .2s}
1844
+ .stat:hover{border-color:var(--border2)}
1845
+ .stat-label{font-size:11px;font-weight:500;color:var(--text2);text-transform:uppercase;letter-spacing:.4px}
1846
+ .stat-value{font-size:22px;font-weight:700;font-family:var(--font-mono);margin-top:2px}
1847
+ .stat-value.green{color:var(--green)}.stat-value.cyan{color:var(--cyan)}
1848
+ .stat-value.yellow{color:var(--yellow)}.stat-value.purple{color:var(--purple)}
1849
+ .procs{background:var(--bg2);border:1px solid var(--border);border-radius:var(--radius);margin-bottom:16px;overflow:hidden}
1850
+ .procs-header{padding:10px 16px;font-size:13px;font-weight:600;border-bottom:1px solid var(--border);display:flex;align-items:center;gap:8px}
1851
+ .procs-body{padding:4px 0}
1852
+ .proc{padding:6px 16px;display:flex;align-items:center;gap:10px;font-size:13px}
1853
+ .proc-dot{width:6px;height:6px;border-radius:50%;background:var(--green);flex-shrink:0}
1854
+ .proc-pid{font-family:var(--font-mono);color:var(--cyan);font-weight:500;min-width:70px}
1855
+ .proc-sid{color:var(--text2);font-family:var(--font-mono);font-size:12px}
1856
+ .proc-cwd{color:var(--text3);font-size:12px}
1857
+ .empty{padding:14px 16px;color:var(--text3);font-size:13px}
1858
+ .main{display:grid;grid-template-columns:360px 1fr;gap:0;background:var(--bg2);border:1px solid var(--border);border-radius:var(--radius);overflow:hidden;min-height:calc(100vh - 260px)}
1859
+ .sidebar{border-right:1px solid var(--border);overflow-y:auto}
1860
+ .sidebar-header{padding:10px 16px;font-size:13px;font-weight:600;border-bottom:1px solid var(--border);display:flex;align-items:center;gap:8px;position:sticky;top:0;background:var(--bg2);z-index:2}
1861
+ .count{background:var(--bg3);color:var(--text2);font-size:11px;padding:1px 7px;border-radius:10px;font-weight:500}
1862
+ .s-item{padding:10px 16px;border-bottom:1px solid var(--border);cursor:pointer;transition:background .15s}
1863
+ .s-item:last-child{border-bottom:none}
1864
+ .s-item:hover{background:var(--bg3)}
1865
+ .s-item.active{background:rgba(88,166,255,.06);border-left:3px solid var(--cyan);padding-left:13px}
1866
+ .s-row{display:flex;align-items:center;gap:10px}
1867
+ .s-icon{font-size:14px;flex-shrink:0}
1868
+ .s-info{flex:1;min-width:0}
1869
+ .s-title{font-size:13px;font-weight:600;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
1870
+ .s-meta{font-size:11px;color:var(--text2);display:flex;gap:10px;margin-top:2px;flex-wrap:wrap}
1871
+ .badge{font-size:10px;padding:1px 6px;border-radius:4px;font-weight:600;font-family:var(--font-mono);text-transform:uppercase}
1872
+ .badge-done{background:rgba(63,185,80,.12);color:var(--green)}
1873
+ .badge-stop{background:rgba(210,153,34,.12);color:var(--yellow)}
1874
+ .badge-claude{background:rgba(217,119,6,.15);color:#f59e0b}
1875
+ .badge-copilot{background:rgba(56,189,248,.12);color:#38bdf8}
1876
+ .detail{padding:20px;overflow-y:auto}
1877
+ .detail-head{display:flex;align-items:center;justify-content:space-between;margin-bottom:16px}
1878
+ .detail-title{font-size:18px;font-weight:700}
1879
+ .detail-id{font-family:var(--font-mono);font-size:11px;color:var(--text3);background:var(--bg);padding:3px 8px;border-radius:4px}
1880
+ .detail-time{font-size:12px;color:var(--text2);margin-bottom:16px}
1881
+ .detail-stats{display:grid;grid-template-columns:repeat(auto-fit,minmax(120px,1fr));gap:8px;margin-bottom:20px}
1882
+ .d-stat{background:var(--bg);border-radius:6px;padding:10px}
1883
+ .d-stat-label{font-size:10px;color:var(--text2);text-transform:uppercase;letter-spacing:.3px}
1884
+ .d-stat-val{font-size:16px;font-weight:700;font-family:var(--font-mono);margin-top:1px}
1885
+ .sub{margin-top:20px}
1886
+ .sub-title{font-size:12px;font-weight:600;color:var(--text2);text-transform:uppercase;letter-spacing:.5px;margin-bottom:8px;display:flex;align-items:center;gap:6px}
1887
+ .tool-row{display:flex;align-items:center;gap:8px;padding:3px 0;font-size:12px}
1888
+ .tool-name{color:var(--text2);min-width:110px;text-align:right;font-family:var(--font-mono);font-size:11px}
1889
+ .tool-bar-bg{flex:1;background:var(--bg);border-radius:3px;height:14px;overflow:hidden}
1890
+ .tool-bar{height:100%;border-radius:3px;background:linear-gradient(90deg,var(--cyan),var(--purple));opacity:.7;transition:width .4s ease}
1891
+ .tool-count{font-family:var(--font-mono);color:var(--text2);min-width:40px;font-size:11px}
1892
+ .commit-list,.task-list,.file-list,.error-list{list-style:none}
1893
+ .commit-list li,.task-list li{padding:5px 0;font-size:13px;display:flex;align-items:flex-start;gap:6px;border-bottom:1px solid var(--border)}
1894
+ .commit-list li:last-child,.task-list li:last-child{border-bottom:none}
1895
+ .c-dot{color:var(--green);flex-shrink:0;margin-top:2px}
1896
+ .t-check{color:var(--green);flex-shrink:0;margin-top:2px}
1897
+ .file-list li{padding:2px 0;font-size:11px;font-family:var(--font-mono)}
1898
+ .file-created{color:var(--green)}
1899
+ .file-edited{color:var(--yellow)}
1900
+ .error-list li{padding:5px 0;font-size:12px;color:var(--red)}
1901
+ .more{font-size:11px;color:var(--text3);padding:4px 0}
1902
+ .empty-detail{display:flex;align-items:center;justify-content:center;height:100%;color:var(--text3)}
1903
+ @media(max-width:768px){.main{grid-template-columns:1fr}.stats{grid-template-columns:repeat(2,1fr)}}
1904
+ `;
1905
+ var layoutHead = `<!DOCTYPE html>
1906
+ <html lang="en">
1907
+ <head>
1908
+ <meta charset="utf-8">
1909
+ <meta name="viewport" content="width=device-width, initial-scale=1">
1910
+ <title>Copilot Agent Dashboard</title>
1911
+ <script src="https://unpkg.com/htmx.org@2.0.4" integrity="sha384-HGfztofotfshcF7+8n44JQL2oJmowVChPTg48S+jvZoztPfvwD79OC/LTtG6dMp+" crossorigin="anonymous"></script>
1912
+ <script src="https://unpkg.com/htmx-ext-sse@2.2.2/sse.js"></script>
1913
+ <script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js"></script>
1914
+ <link rel="preconnect" href="https://fonts.googleapis.com">
1915
+ <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
1916
+ <style>
1917
+ ${cssStyles}
1918
+ </style>
1919
+ </head>`;
1920
+ var layoutFoot = `</html>`;
1921
+
1922
+ // src/web/views.ts
1923
+ function esc(s) {
1924
+ return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
1925
+ }
1926
+ function fmt(n) {
1927
+ return n.toLocaleString();
1928
+ }
1929
+ function fmtDur(ms) {
1930
+ if (!ms) return "\u2014";
1931
+ if (ms < 6e4) return `${Math.round(ms / 1e3)}s`;
1932
+ if (ms < 36e5) return `${Math.round(ms / 6e4)}m`;
1933
+ const h = Math.floor(ms / 36e5);
1934
+ const m = Math.round(ms % 36e5 / 6e4);
1935
+ return `${h}h ${m}m`;
1936
+ }
1937
+ function fmtTime(iso) {
1938
+ if (!iso) return "\u2014";
1939
+ return new Date(iso).toLocaleString("en-GB", { month: "short", day: "numeric", hour: "2-digit", minute: "2-digit" });
1940
+ }
1941
+ function fmtAgo(iso) {
1942
+ if (!iso) return "\u2014";
1943
+ const diff = Date.now() - new Date(iso).getTime();
1944
+ if (diff < 6e4) return "just now";
1945
+ if (diff < 36e5) return `${Math.round(diff / 6e4)}m ago`;
1946
+ if (diff < 864e5) return `${Math.round(diff / 36e5)}h ago`;
1947
+ return `${Math.round(diff / 864e5)}d ago`;
1948
+ }
1949
+ function shortPath(p, proj) {
1950
+ if (proj && p.includes(proj + "/")) return p.split(proj + "/").pop() ?? p;
1951
+ return p.split("/").slice(-3).join("/");
1952
+ }
1953
+ function renderStats(sessions) {
1954
+ const totalPremium = sessions.reduce((a, s) => a + (s.premiumRequests ?? 0), 0);
1955
+ const totalTokens = sessions.reduce((a, s) => a + (s.outputTokens ?? 0), 0);
1956
+ const totalCommits = sessions.reduce((a, s) => a + (s.gitCommits?.length ?? 0), 0);
1957
+ const totalTasks = sessions.reduce((a, s) => a + (s.taskCompletions?.length ?? 0), 0);
1958
+ const completed = sessions.filter((s) => s.complete).length;
1959
+ const items = [
1960
+ { label: "Sessions", value: String(sessions.length), cls: "cyan" },
1961
+ { label: "Completed", value: `${completed}/${sessions.length}`, cls: "green" },
1962
+ { label: "Premium", value: fmt(totalPremium), cls: "yellow" },
1963
+ { label: "Tokens", value: fmt(totalTokens), cls: "purple" },
1964
+ { label: "Commits", value: String(totalCommits), cls: "green" },
1965
+ { label: "Tasks Done", value: String(totalTasks), cls: "cyan" }
1966
+ ];
1967
+ return items.map(
1968
+ (i) => `<div class="stat"><div class="stat-label">${i.label}</div><div class="stat-value ${i.cls}">${i.value}</div></div>`
1969
+ ).join("");
1970
+ }
1971
+ function renderProcesses(procs) {
1972
+ if (procs.length === 0) return '<div class="empty">No active agent processes</div>';
1973
+ return procs.map((p) => {
1974
+ const sid = p.sessionId ? p.sessionId.slice(0, 8) + "\u2026" : "\u2014";
1975
+ const agentBadge = p.agent === "claude" ? '<span class="badge badge-claude">claude</span>' : '<span class="badge badge-copilot">copilot</span>';
1976
+ return `<div class="proc">
1977
+ <div class="proc-dot"></div>
1978
+ ${agentBadge}
1979
+ <span class="proc-pid">PID ${p.pid}</span>
1980
+ <span class="proc-sid">${esc(sid)}</span>
1981
+ <span class="proc-cwd">${esc(p.cwd ?? "")}</span>
1982
+ </div>`;
1983
+ }).join("");
1984
+ }
1985
+ function renderSessionList(sessions, selectedId) {
1986
+ return sessions.map((s) => {
1987
+ const proj = (s.cwd ?? "").split("/").pop() ?? "\u2014";
1988
+ const isActive = s.id === selectedId;
1989
+ const agentBadge = s.agent === "claude" ? '<span class="badge badge-claude">claude</span>' : '<span class="badge badge-copilot">copilot</span>';
1990
+ return `<div class="s-item${isActive ? " active" : ""}"
1991
+ hx-get="/partial/detail/${s.id}" hx-target="#detail" hx-swap="innerHTML"
1992
+ onclick="document.querySelectorAll('.s-item').forEach(e=>e.classList.remove('active'));this.classList.add('active')">
1993
+ <div class="s-row">
1994
+ <span class="s-icon">${s.complete ? "\u2705" : "\u23F8\uFE0F"}</span>
1995
+ <div class="s-info">
1996
+ <div class="s-title">${agentBadge} ${esc(proj)} \u2014 ${esc(s.summary || "(no summary)")}</div>
1997
+ <div class="s-meta">
1998
+ <span>${fmtDur(s.durationMs)}</span>
1999
+ <span>${s.agent === "claude" ? "" : fmt(s.premiumRequests) + " premium"}</span>
2000
+ <span>${fmtAgo(s.endTime)}</span>
2001
+ <span class="badge ${s.complete ? "badge-done" : "badge-stop"}">${s.complete ? "done" : "stopped"}</span>
2002
+ </div>
2003
+ </div>
2004
+ </div>
2005
+ </div>`;
2006
+ }).join("");
2007
+ }
2008
+ function renderDetail(s) {
2009
+ const proj = (s.cwd ?? "").split("/").pop() ?? "\u2014";
2010
+ const totalTools = Object.values(s.toolUsage ?? {}).reduce((a, b) => a + b, 0);
2011
+ const toolEntries = Object.entries(s.toolUsage ?? {}).sort((a, b) => b[1] - a[1]);
2012
+ const maxTool = toolEntries[0]?.[1] ?? 1;
2013
+ let html = "";
2014
+ 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>';
2015
+ html += `<div class="detail-head">
2016
+ <div class="detail-title">${esc(proj)}${agentBadge}</div>
2017
+ <div class="detail-id">${s.id}</div>
2018
+ </div>`;
2019
+ html += `<div class="detail-time">${fmtTime(s.startTime)} \u2192 ${fmtTime(s.endTime)}</div>`;
2020
+ html += `<div class="detail-stats">
2021
+ <div class="d-stat"><div class="d-stat-label">Duration</div><div class="d-stat-val">${fmtDur(s.durationMs)}</div></div>
2022
+ <div class="d-stat"><div class="d-stat-label">User Msgs</div><div class="d-stat-val">${s.userMessages}</div></div>
2023
+ <div class="d-stat"><div class="d-stat-label">Turns</div><div class="d-stat-val">${fmt(s.assistantTurns)}</div></div>
2024
+ <div class="d-stat"><div class="d-stat-label">Tokens</div><div class="d-stat-val">${fmt(s.outputTokens)}</div></div>
2025
+ <div class="d-stat"><div class="d-stat-label">Premium</div><div class="d-stat-val">${fmt(s.premiumRequests)}</div></div>
2026
+ <div class="d-stat"><div class="d-stat-label">Tool Calls</div><div class="d-stat-val">${fmt(totalTools)}</div></div>
2027
+ </div>`;
2028
+ if (toolEntries.length > 0) {
2029
+ html += `<div class="sub"><div class="sub-title">\u{1F527} Tools Used</div>`;
2030
+ for (const [tool, count] of toolEntries.slice(0, 12)) {
2031
+ const pct = Math.round(count / maxTool * 100);
2032
+ html += `<div class="tool-row">
2033
+ <span class="tool-name">${esc(tool)}</span>
2034
+ <div class="tool-bar-bg"><div class="tool-bar" style="width:${pct}%"></div></div>
2035
+ <span class="tool-count">${count}</span>
2036
+ </div>`;
2037
+ }
2038
+ if (toolEntries.length > 12) html += `<div class="more">\u2026 +${toolEntries.length - 12} more</div>`;
2039
+ html += `</div>`;
2040
+ }
2041
+ if (s.gitCommits.length > 0) {
2042
+ html += `<div class="sub"><div class="sub-title">\u{1F500} Git Commits <span class="count">${s.gitCommits.length}</span></div><ul class="commit-list">`;
2043
+ for (const msg of s.gitCommits.slice(0, 12)) {
2044
+ const first = msg.split("\n")[0].slice(0, 80);
2045
+ html += `<li><span class="c-dot">\u25CF</span><span>${esc(first)}</span></li>`;
2046
+ }
2047
+ if (s.gitCommits.length > 12) html += `<li class="more">\u2026 +${s.gitCommits.length - 12} more</li>`;
2048
+ html += `</ul></div>`;
2049
+ }
2050
+ const files = [
2051
+ ...s.filesCreated.map((f) => ({ path: f, type: "created" })),
2052
+ ...s.filesEdited.map((f) => ({ path: f, type: "edited" }))
2053
+ ];
2054
+ if (files.length > 0) {
2055
+ html += `<div class="sub"><div class="sub-title">\u{1F4C1} Files Changed <span class="count">${files.length}</span></div><ul class="file-list">`;
2056
+ for (const f of files.slice(0, 25)) {
2057
+ const cls = f.type === "created" ? "file-created" : "file-edited";
2058
+ const icon = f.type === "created" ? "+" : "~";
2059
+ html += `<li><span class="${cls}">${icon}</span> ${esc(shortPath(f.path, proj))}</li>`;
2060
+ }
2061
+ if (files.length > 25) html += `<li class="more">\u2026 +${files.length - 25} more</li>`;
2062
+ html += `</ul></div>`;
2063
+ }
2064
+ if (s.taskCompletions.length > 0) {
2065
+ html += `<div class="sub"><div class="sub-title">\u2705 Tasks Completed <span class="count">${s.taskCompletions.length}</span></div><ul class="task-list">`;
2066
+ for (const t of s.taskCompletions.slice(0, 10)) {
2067
+ const first = t.split("\n")[0].slice(0, 80);
2068
+ html += `<li><span class="t-check">\u2714</span><span>${esc(first)}</span></li>`;
2069
+ }
2070
+ if (s.taskCompletions.length > 10) html += `<li class="more">\u2026 +${s.taskCompletions.length - 10} more</li>`;
2071
+ html += `</ul></div>`;
2072
+ }
2073
+ if (s.errors.length > 0) {
2074
+ html += `<div class="sub"><div class="sub-title" style="color:var(--red)">\u26A0\uFE0F Errors <span class="count">${s.errors.length}</span></div><ul class="error-list">`;
2075
+ for (const e of s.errors.slice(0, 5)) {
2076
+ html += `<li>${esc(e.slice(0, 100))}</li>`;
2077
+ }
2078
+ if (s.errors.length > 5) html += `<li class="more">\u2026 +${s.errors.length - 5} more</li>`;
2079
+ html += `</ul></div>`;
2080
+ }
2081
+ return html;
2082
+ }
2083
+
2084
+ // src/commands/web.ts
2085
+ function registerWebCommand(program2) {
2086
+ program2.command("web").description("Launch web dashboard in browser").option("-p, --port <n>", "Port number", "3847").option("--no-open", "Do not auto-open browser").action((opts) => {
2087
+ startWebServer(parseInt(opts.port, 10), opts.open !== false);
2088
+ });
2089
+ }
2090
+ function getData() {
2091
+ const sessions = listAllSessions(20);
2092
+ const reports = sessions.map((s) => getAgentSessionReport(s.id, s.agent)).filter((r) => r !== null);
2093
+ const processes = findAgentProcesses();
2094
+ return { sessions: reports, processes };
2095
+ }
2096
+ function simpleHash(s) {
2097
+ let h = 0;
2098
+ for (let i = 0; i < s.length; i++) {
2099
+ h = (h << 5) - h + s.charCodeAt(i) | 0;
2100
+ }
2101
+ return h.toString(36);
2102
+ }
2103
+ function startWebServer(port, autoOpen) {
2104
+ const app = new Hono();
2105
+ app.get("/api/sessions", (c) => c.json(getData()));
2106
+ app.get("/api/session/:id", (c) => {
2107
+ const report = getAgentSessionReport(c.req.param("id"));
2108
+ if (!report) return c.json({ error: "Not found" }, 404);
2109
+ return c.json(report);
2110
+ });
2111
+ app.get("/events", (c) => {
2112
+ return streamSSE(c, async (stream) => {
2113
+ let prevStatsHash = "";
2114
+ let prevProcsHash = "";
2115
+ while (true) {
2116
+ const { sessions, processes } = getData();
2117
+ const statsHtml = renderStats(sessions);
2118
+ const procsHtml = renderProcesses(processes);
2119
+ const statsHash = simpleHash(statsHtml);
2120
+ const procsHash = simpleHash(procsHtml);
2121
+ if (statsHash !== prevStatsHash) {
2122
+ await stream.writeSSE({ event: "stats", data: statsHtml });
2123
+ prevStatsHash = statsHash;
2124
+ }
2125
+ if (procsHash !== prevProcsHash) {
2126
+ await stream.writeSSE({ event: "procs", data: procsHtml });
2127
+ await stream.writeSSE({ event: "proc-count", data: String(processes.length) });
2128
+ prevProcsHash = procsHash;
2129
+ }
2130
+ await stream.sleep(5e3);
2131
+ }
2132
+ });
2133
+ });
2134
+ app.get("/partial/detail/:id", (c) => {
2135
+ const report = getAgentSessionReport(c.req.param("id"));
2136
+ if (!report) return c.html('<div class="empty-detail">Session not found</div>');
2137
+ return c.html(renderDetail(report));
2138
+ });
2139
+ app.get("/", (c) => {
2140
+ const { sessions, processes } = getData();
2141
+ const firstId = sessions[0]?.id;
2142
+ return c.html(`${layoutHead}
2143
+ <body>
2144
+ <div class="header">
2145
+ <div class="header-left">
2146
+ <h1>\u{1F916} <span>Copilot Agent</span></h1>
2147
+ <div class="live-badge"><div class="live-dot"></div> Live</div>
2148
+ </div>
2149
+ <div class="clock" id="clock"></div>
2150
+ </div>
2151
+
2152
+ <div class="container"
2153
+ hx-ext="sse"
2154
+ sse-connect="/events">
2155
+
2156
+ <div class="stats"
2157
+ sse-swap="stats"
2158
+ hx-swap="innerHTML settle:0s swap:0s">
2159
+ ${renderStats(sessions)}
2160
+ </div>
2161
+
2162
+ <div class="procs">
2163
+ <div class="procs-header">
2164
+ \u2B24 Active Processes <span class="count" sse-swap="proc-count" hx-swap="innerHTML settle:0s swap:0s">${processes.length}</span>
2165
+ </div>
2166
+ <div class="procs-body"
2167
+ sse-swap="procs"
2168
+ hx-swap="innerHTML settle:0s swap:0s">
2169
+ ${renderProcesses(processes)}
2170
+ </div>
2171
+ </div>
2172
+
2173
+ <div class="main">
2174
+ <div class="sidebar">
2175
+ <div class="sidebar-header">
2176
+ \u{1F4CB} Sessions <span class="count">${sessions.length}</span>
2177
+ </div>
2178
+ ${renderSessionList(sessions, firstId)}
2179
+ </div>
2180
+ <div class="detail" id="detail">
2181
+ ${firstId ? renderDetail(sessions[0]) : '<div class="empty-detail">No sessions</div>'}
2182
+ </div>
2183
+ </div>
2184
+ </div>
2185
+
2186
+ <script>
2187
+ setInterval(() => {
2188
+ document.getElementById('clock').textContent = new Date().toLocaleTimeString('en-GB');
2189
+ }, 1000);
2190
+ document.getElementById('clock').textContent = new Date().toLocaleTimeString('en-GB');
2191
+ </script>
2192
+ </body>
2193
+ ${layoutFoot}`);
2194
+ });
2195
+ try {
2196
+ serve({ fetch: app.fetch, port }, () => {
2197
+ const url = `http://localhost:${port}`;
2198
+ ok(`Web dashboard \u2192 ${url}`);
2199
+ info("Press Ctrl+C to stop");
2200
+ if (autoOpen) {
2201
+ spawn2("open", [url], { detached: true, stdio: "ignore" }).unref();
2202
+ }
2203
+ });
2204
+ } catch (err) {
2205
+ fail(`Server error: ${err instanceof Error ? err.message : err}`);
2206
+ process.exit(1);
2207
+ }
2208
+ }
2209
+
1334
2210
  // src/index.ts
1335
2211
  var program = new Command();
1336
- program.name("copilot-agent").version("0.6.0").description("Autonomous GitHub Copilot CLI agent \u2014 auto-resume, task discovery, overnight runs");
2212
+ 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.");
1337
2213
  registerStatusCommand(program);
1338
2214
  registerWatchCommand(program);
1339
2215
  registerRunCommand(program);
1340
2216
  registerOvernightCommand(program);
1341
2217
  registerResearchCommand(program);
1342
2218
  registerReportCommand(program);
2219
+ registerDashboardCommand(program);
2220
+ registerWebCommand(program);
1343
2221
  program.parse();
1344
2222
  //# sourceMappingURL=index.js.map