@ai-devkit/agent-manager 0.33.0 → 0.33.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.
Files changed (112) hide show
  1. package/dist/AgentManager.d.ts +11 -2
  2. package/dist/AgentManager.d.ts.map +1 -1
  3. package/dist/AgentManager.js +37 -6
  4. package/dist/AgentManager.js.map +1 -1
  5. package/dist/__tests__/AgentManager.test.js +212 -5
  6. package/dist/__tests__/AgentManager.test.js.map +1 -1
  7. package/dist/__tests__/adapters/OpenCodeAdapter.test.js +1 -0
  8. package/dist/__tests__/adapters/OpenCodeAdapter.test.js.map +1 -1
  9. package/dist/__tests__/{adapters → providers/copilot}/CopilotAdapter.test.js +19 -14
  10. package/dist/__tests__/providers/copilot/CopilotAdapter.test.js.map +1 -0
  11. package/dist/__tests__/{adapters → providers/gemini}/GeminiCliAdapter.test.js +37 -29
  12. package/dist/__tests__/providers/gemini/GeminiCliAdapter.test.js.map +1 -0
  13. package/dist/__tests__/runtime/HerdrAgentDiscovery.test.js +164 -0
  14. package/dist/__tests__/runtime/HerdrAgentDiscovery.test.js.map +1 -0
  15. package/dist/__tests__/runtime/HerdrAgentRuntime.test.js +37 -1
  16. package/dist/__tests__/runtime/HerdrAgentRuntime.test.js.map +1 -1
  17. package/dist/__tests__/utils/AgentRegistry.test.js +20 -13
  18. package/dist/__tests__/utils/AgentRegistry.test.js.map +1 -1
  19. package/dist/adapters/OpenCodeAdapter.d.ts +1 -0
  20. package/dist/adapters/OpenCodeAdapter.d.ts.map +1 -1
  21. package/dist/adapters/OpenCodeAdapter.js +7 -4
  22. package/dist/adapters/OpenCodeAdapter.js.map +1 -1
  23. package/dist/adapters/index.d.ts +2 -2
  24. package/dist/adapters/index.d.ts.map +1 -1
  25. package/dist/adapters/index.js +2 -2
  26. package/dist/adapters/index.js.map +1 -1
  27. package/dist/index.d.ts +5 -3
  28. package/dist/index.d.ts.map +1 -1
  29. package/dist/index.js +3 -2
  30. package/dist/index.js.map +1 -1
  31. package/dist/{adapters → providers/copilot}/CopilotAdapter.d.ts +11 -25
  32. package/dist/providers/copilot/CopilotAdapter.d.ts.map +1 -0
  33. package/dist/providers/copilot/CopilotAdapter.js +108 -0
  34. package/dist/providers/copilot/CopilotAdapter.js.map +1 -0
  35. package/dist/providers/copilot/CopilotAgentMapper.d.ts +9 -0
  36. package/dist/providers/copilot/CopilotAgentMapper.d.ts.map +1 -0
  37. package/dist/providers/copilot/CopilotAgentMapper.js +37 -0
  38. package/dist/providers/copilot/CopilotAgentMapper.js.map +1 -0
  39. package/dist/providers/copilot/CopilotSessionLocator.d.ts +19 -0
  40. package/dist/providers/copilot/CopilotSessionLocator.d.ts.map +1 -0
  41. package/dist/providers/copilot/CopilotSessionLocator.js +41 -0
  42. package/dist/providers/copilot/CopilotSessionLocator.js.map +1 -0
  43. package/dist/providers/copilot/CopilotSessionParser.d.ts +30 -0
  44. package/dist/providers/copilot/CopilotSessionParser.d.ts.map +1 -0
  45. package/dist/providers/copilot/CopilotSessionParser.js +181 -0
  46. package/dist/providers/copilot/CopilotSessionParser.js.map +1 -0
  47. package/dist/providers/gemini/GeminiAgentMapper.d.ts +9 -0
  48. package/dist/providers/gemini/GeminiAgentMapper.d.ts.map +1 -0
  49. package/dist/providers/gemini/GeminiAgentMapper.js +37 -0
  50. package/dist/providers/gemini/GeminiAgentMapper.js.map +1 -0
  51. package/dist/providers/gemini/GeminiCliAdapter.d.ts +52 -0
  52. package/dist/providers/gemini/GeminiCliAdapter.d.ts.map +1 -0
  53. package/dist/providers/gemini/GeminiCliAdapter.js +188 -0
  54. package/dist/providers/gemini/GeminiCliAdapter.js.map +1 -0
  55. package/dist/providers/gemini/GeminiSessionLocator.d.ts +30 -0
  56. package/dist/providers/gemini/GeminiSessionLocator.d.ts.map +1 -0
  57. package/dist/providers/gemini/GeminiSessionLocator.js +142 -0
  58. package/dist/providers/gemini/GeminiSessionLocator.js.map +1 -0
  59. package/dist/providers/gemini/GeminiSessionParser.d.ts +94 -0
  60. package/dist/providers/gemini/GeminiSessionParser.d.ts.map +1 -0
  61. package/dist/providers/gemini/GeminiSessionParser.js +166 -0
  62. package/dist/providers/gemini/GeminiSessionParser.js.map +1 -0
  63. package/dist/runtime/herdr/HerdrAgentDiscovery.d.ts +18 -0
  64. package/dist/runtime/herdr/HerdrAgentDiscovery.d.ts.map +1 -0
  65. package/dist/runtime/herdr/HerdrAgentDiscovery.js +183 -0
  66. package/dist/runtime/herdr/HerdrAgentDiscovery.js.map +1 -0
  67. package/dist/runtime/herdr/HerdrAgentRuntime.d.ts +5 -0
  68. package/dist/runtime/herdr/HerdrAgentRuntime.d.ts.map +1 -1
  69. package/dist/runtime/herdr/HerdrAgentRuntime.js +26 -8
  70. package/dist/runtime/herdr/HerdrAgentRuntime.js.map +1 -1
  71. package/dist/runtime/herdr/HerdrResponseParsers.d.ts +2 -0
  72. package/dist/runtime/herdr/HerdrResponseParsers.d.ts.map +1 -1
  73. package/dist/runtime/herdr/HerdrResponseParsers.js +12 -1
  74. package/dist/runtime/herdr/HerdrResponseParsers.js.map +1 -1
  75. package/dist/utils/AgentRegistry.d.ts.map +1 -1
  76. package/dist/utils/AgentRegistry.js +5 -5
  77. package/dist/utils/AgentRegistry.js.map +1 -1
  78. package/package.json +1 -1
  79. package/src/AgentManager.ts +67 -5
  80. package/src/__tests__/AgentManager.test.ts +231 -5
  81. package/src/__tests__/adapters/OpenCodeAdapter.test.ts +1 -0
  82. package/src/__tests__/{adapters → providers/copilot}/CopilotAdapter.test.ts +13 -16
  83. package/src/__tests__/{adapters → providers/gemini}/GeminiCliAdapter.test.ts +39 -31
  84. package/src/__tests__/runtime/HerdrAgentDiscovery.test.ts +131 -0
  85. package/src/__tests__/runtime/HerdrAgentRuntime.test.ts +20 -1
  86. package/src/__tests__/utils/AgentRegistry.test.ts +20 -10
  87. package/src/adapters/OpenCodeAdapter.ts +8 -4
  88. package/src/adapters/index.ts +2 -2
  89. package/src/index.ts +10 -3
  90. package/src/providers/copilot/CopilotAdapter.ts +145 -0
  91. package/src/providers/copilot/CopilotAgentMapper.ts +36 -0
  92. package/src/providers/copilot/CopilotSessionLocator.ts +57 -0
  93. package/src/providers/copilot/CopilotSessionParser.ts +289 -0
  94. package/src/providers/gemini/GeminiAgentMapper.ts +36 -0
  95. package/src/providers/gemini/GeminiCliAdapter.ts +266 -0
  96. package/src/providers/gemini/GeminiSessionLocator.ts +183 -0
  97. package/src/providers/gemini/GeminiSessionParser.ts +271 -0
  98. package/src/runtime/herdr/HerdrAgentDiscovery.ts +178 -0
  99. package/src/runtime/herdr/HerdrAgentRuntime.ts +33 -7
  100. package/src/runtime/herdr/HerdrResponseParsers.ts +11 -1
  101. package/src/utils/AgentRegistry.ts +8 -6
  102. package/dist/__tests__/adapters/CopilotAdapter.test.js.map +0 -1
  103. package/dist/__tests__/adapters/GeminiCliAdapter.test.js.map +0 -1
  104. package/dist/adapters/CopilotAdapter.d.ts.map +0 -1
  105. package/dist/adapters/CopilotAdapter.js +0 -329
  106. package/dist/adapters/CopilotAdapter.js.map +0 -1
  107. package/dist/adapters/GeminiCliAdapter.d.ts +0 -98
  108. package/dist/adapters/GeminiCliAdapter.d.ts.map +0 -1
  109. package/dist/adapters/GeminiCliAdapter.js +0 -513
  110. package/dist/adapters/GeminiCliAdapter.js.map +0 -1
  111. package/src/adapters/CopilotAdapter.ts +0 -483
  112. package/src/adapters/GeminiCliAdapter.ts +0 -699
@@ -16,6 +16,7 @@ import type {
16
16
  } from "../adapters/AgentAdapter.js";
17
17
  import { AgentStatus } from "../adapters/AgentAdapter.js";
18
18
  import { AgentRegistry, type RegistryEntry } from "../utils/AgentRegistry.js";
19
+ import type { HerdrAgentPane } from "../runtime/herdr/HerdrAgentDiscovery.js";
19
20
 
20
21
  // Mock adapter for testing
21
22
  class MockAdapter implements AgentAdapter {
@@ -360,6 +361,15 @@ describe("AgentManager", () => {
360
361
  fs.rmSync(tmpDir, { recursive: true, force: true });
361
362
  });
362
363
 
364
+ function useHerdrPanes(panes: readonly HerdrAgentPane[]): ReturnType<typeof vi.fn> {
365
+ const fetchHerdrPanes = vi.fn().mockResolvedValue(panes);
366
+ scopedManager = new AgentManager(registry, undefined, {
367
+ runtimeProvider: "herdr",
368
+ fetchHerdrAgentPanes: fetchHerdrPanes,
369
+ });
370
+ return fetchHerdrPanes;
371
+ }
372
+
363
373
  it("persists every detected agent to the registry", async () => {
364
374
  scopedManager.registerAdapter(
365
375
  new MockAdapter("claude", [
@@ -390,6 +400,223 @@ describe("AgentManager", () => {
390
400
  expect(entries[0].startedAt).toMatch(/^\d{4}-\d{2}-\d{2}T/);
391
401
  });
392
402
 
403
+ it("persists a Herdr runtime ref for manually discovered agents matched by session id", async () => {
404
+ useHerdrPanes([
405
+ {
406
+ agent: "codex",
407
+ agentSessionId: "01a0a05e-751f-7850-9fd7-247e297f9ac5",
408
+ cwd: "/cwd/ai-devkit",
409
+ paneId: "w31:p1",
410
+ workspaceId: "w31",
411
+ tabId: "w31:t1",
412
+ },
413
+ ]);
414
+ scopedManager.registerAdapter(
415
+ new MockAdapter("codex", [
416
+ createMockAgent({
417
+ name: "ai-devkit-44290",
418
+ type: "codex",
419
+ pid: process.pid,
420
+ sessionId: "01a0a05e-751f-7850-9fd7-247e297f9ac5",
421
+ sessionFilePath: "/path/codex.jsonl",
422
+ projectPath: "/cwd/ai-devkit",
423
+ }),
424
+ ]),
425
+ );
426
+
427
+ await scopedManager.listAgents();
428
+
429
+ expect(registry.list()[0]).toMatchObject({
430
+ name: "ai-devkit-44290",
431
+ type: "codex",
432
+ pid: process.pid,
433
+ runtime: "herdr",
434
+ runtimeRef: {
435
+ session: "default",
436
+ paneId: "w31:p1",
437
+ workspaceId: "w31",
438
+ tabId: "w31:t1",
439
+ agentName: "ai-devkit-44290",
440
+ },
441
+ });
442
+ });
443
+
444
+ it("upgrades an existing tmux fallback row when Herdr later matches by session id", async () => {
445
+ useHerdrPanes([
446
+ {
447
+ agent: "codex",
448
+ agentSessionId: "01a0a05e-751f-7850-9fd7-247e297f9ac5",
449
+ cwd: "/cwd/ai-devkit",
450
+ paneId: "w31:p1",
451
+ },
452
+ ]);
453
+ registry.register({
454
+ name: "ai-devkit-44290",
455
+ type: "codex",
456
+ pid: process.pid,
457
+ runtime: "tmux",
458
+ runtimeRef: null,
459
+ cwd: "/cwd/ai-devkit",
460
+ startedAt: "2026-09-14T00:00:00.000Z",
461
+ sessionId: "01a0a05e-751f-7850-9fd7-247e297f9ac5",
462
+ sessionFilePath: "/path/codex.jsonl",
463
+ });
464
+ scopedManager.registerAdapter(
465
+ new MockAdapter("codex", [
466
+ createMockAgent({
467
+ name: "ai-devkit-44290",
468
+ type: "codex",
469
+ pid: process.pid,
470
+ sessionId: "01a0a05e-751f-7850-9fd7-247e297f9ac5",
471
+ sessionFilePath: "/path/codex.jsonl",
472
+ projectPath: "/cwd/ai-devkit",
473
+ }),
474
+ ]),
475
+ );
476
+
477
+ await scopedManager.listAgents();
478
+
479
+ expect(registry.lookup("ai-devkit-44290")).toMatchObject({
480
+ runtime: "herdr",
481
+ runtimeRef: {
482
+ session: "default",
483
+ paneId: "w31:p1",
484
+ agentName: "ai-devkit-44290",
485
+ },
486
+ });
487
+ });
488
+
489
+ it("upgrades an existing tmux fallback row when Herdr matches by foreground pid", async () => {
490
+ useHerdrPanes([
491
+ {
492
+ agent: "codex",
493
+ cwd: "/cwd/ai-devkit",
494
+ paneId: "w31:p1",
495
+ foregroundPids: [44290],
496
+ },
497
+ {
498
+ agent: "codex",
499
+ cwd: "/cwd/ai-devkit",
500
+ paneId: "w32:p1",
501
+ foregroundPids: [process.pid],
502
+ },
503
+ ]);
504
+ registry.register({
505
+ name: "ai-devkit-64904",
506
+ type: "codex",
507
+ pid: process.pid,
508
+ runtime: "tmux",
509
+ runtimeRef: null,
510
+ cwd: "/cwd/ai-devkit",
511
+ startedAt: "2026-09-14T00:00:00.000Z",
512
+ sessionId: `pid-${process.pid}`,
513
+ sessionFilePath: "",
514
+ });
515
+ scopedManager.registerAdapter(
516
+ new MockAdapter("codex", [
517
+ createMockAgent({
518
+ name: "ai-devkit-64904",
519
+ type: "codex",
520
+ pid: process.pid,
521
+ sessionId: `pid-${process.pid}`,
522
+ sessionFilePath: undefined,
523
+ projectPath: "/cwd/ai-devkit",
524
+ }),
525
+ ]),
526
+ );
527
+
528
+ await scopedManager.listAgents();
529
+
530
+ expect(registry.lookup("ai-devkit-64904")).toMatchObject({
531
+ runtime: "herdr",
532
+ runtimeRef: {
533
+ session: "default",
534
+ paneId: "w32:p1",
535
+ agentName: "ai-devkit-64904",
536
+ },
537
+ });
538
+ });
539
+
540
+ it("fetches Herdr panes automatically when the configured runtime is Herdr", async () => {
541
+ const fetchHerdrPanes = useHerdrPanes([
542
+ {
543
+ agent: "codex",
544
+ cwd: "/cwd/ai-devkit",
545
+ paneId: "w32:p1",
546
+ foregroundPids: [process.pid],
547
+ },
548
+ ]);
549
+ scopedManager.registerAdapter(
550
+ new MockAdapter("codex", [
551
+ createMockAgent({
552
+ name: "ai-devkit-64904",
553
+ type: "codex",
554
+ pid: process.pid,
555
+ sessionId: `pid-${process.pid}`,
556
+ sessionFilePath: undefined,
557
+ projectPath: "/cwd/ai-devkit",
558
+ }),
559
+ ]),
560
+ );
561
+
562
+ await scopedManager.listAgents();
563
+
564
+ expect(fetchHerdrPanes).toHaveBeenCalledOnce();
565
+ expect(registry.lookup("ai-devkit-64904")).toMatchObject({
566
+ runtime: "herdr",
567
+ runtimeRef: {
568
+ session: "default",
569
+ paneId: "w32:p1",
570
+ agentName: "ai-devkit-64904",
571
+ },
572
+ });
573
+ });
574
+
575
+ it("upgrades an existing tmux row with a malformed runtime ref when Herdr matches", async () => {
576
+ useHerdrPanes([
577
+ {
578
+ agent: "codex",
579
+ cwd: "/cwd/ai-devkit",
580
+ paneId: "w32:p1",
581
+ foregroundPids: [process.pid],
582
+ },
583
+ ]);
584
+ registry.register({
585
+ name: "ai-devkit-64904",
586
+ type: "codex",
587
+ pid: process.pid,
588
+ runtime: "tmux",
589
+ runtimeRef: {},
590
+ cwd: "/cwd/ai-devkit",
591
+ startedAt: "2026-09-14T00:00:00.000Z",
592
+ sessionId: `pid-${process.pid}`,
593
+ sessionFilePath: "",
594
+ });
595
+ scopedManager.registerAdapter(
596
+ new MockAdapter("codex", [
597
+ createMockAgent({
598
+ name: "ai-devkit-64904",
599
+ type: "codex",
600
+ pid: process.pid,
601
+ sessionId: `pid-${process.pid}`,
602
+ sessionFilePath: undefined,
603
+ projectPath: "/cwd/ai-devkit",
604
+ }),
605
+ ]),
606
+ );
607
+
608
+ await scopedManager.listAgents();
609
+
610
+ expect(registry.lookup("ai-devkit-64904")).toMatchObject({
611
+ runtime: "herdr",
612
+ runtimeRef: {
613
+ session: "default",
614
+ paneId: "w32:p1",
615
+ agentName: "ai-devkit-64904",
616
+ },
617
+ });
618
+ });
619
+
393
620
  it("prunes entries for dead pids", async () => {
394
621
  registry.register({
395
622
  name: "dead",
@@ -620,7 +847,7 @@ describe("AgentManager", () => {
620
847
  expect(agents[0].lastActive.toISOString()).toBe("2026-01-01T00:00:00.000Z");
621
848
  });
622
849
 
623
- it("persists changed fields once in one write transaction", async () => {
850
+ it("does not persist passive cwd changes when an existing cwd is present", async () => {
624
851
  const adapter = new MockAdapter("claude", [
625
852
  createMockAgent({ name: "changing", pid: process.pid, projectPath: "/cwd/before" }),
626
853
  ]);
@@ -636,10 +863,9 @@ describe("AgentManager", () => {
636
863
 
637
864
  const upserts = databaseOperations.filter((sql) => /^\s*INSERT INTO agents/i.test(sql));
638
865
  const transactions = databaseOperations.filter((sql) => /^\s*(BEGIN|COMMIT)/i.test(sql));
639
- expect(upserts).toHaveLength(1);
640
- expect(upserts[0]).toContain("'2026-08-14T10:00:01.000Z'");
641
- expect(transactions).toHaveLength(2);
642
- expect(registry.lookup("changing")?.cwd).toBe("/cwd/after");
866
+ expect(upserts).toHaveLength(0);
867
+ expect(transactions).toHaveLength(0);
868
+ expect(registry.lookup("changing")?.cwd).toBe("/cwd/before");
643
869
  });
644
870
 
645
871
  it("prunes newly dead entries only when the passive cadence is due", async () => {
@@ -141,6 +141,7 @@ describe("OpenCodeAdapter", () => {
141
141
  });
142
142
 
143
143
  afterEach(() => {
144
+ adapter.close();
144
145
  fs.rmSync(tmpDir, { recursive: true, force: true });
145
146
  vi.restoreAllMocks();
146
147
  });
@@ -7,19 +7,19 @@ import * as fs from "fs";
7
7
  import * as os from "os";
8
8
  import * as path from "path";
9
9
 
10
- import { CopilotAdapter } from "../../adapters/CopilotAdapter.js";
11
- import type { ProcessInfo } from "../../adapters/AgentAdapter.js";
12
- import { AgentStatus } from "../../adapters/AgentAdapter.js";
10
+ import { CopilotAdapter } from "../../../providers/copilot/CopilotAdapter.js";
11
+ import type { ProcessInfo } from "../../../adapters/AgentAdapter.js";
12
+ import { AgentStatus } from "../../../adapters/AgentAdapter.js";
13
13
  import {
14
14
  listAgentProcesses,
15
15
  enrichProcesses,
16
16
  captureProcessSnapshot,
17
- } from "../../utils/process.js";
18
- import { generateAgentName } from "../../utils/matching.js";
19
- import { AgentRegistry } from "../../utils/AgentRegistry.js";
17
+ } from "../../../utils/process.js";
18
+ import { generateAgentName } from "../../../utils/matching.js";
19
+ import { AgentRegistry } from "../../../utils/AgentRegistry.js";
20
20
 
21
- vi.mock("../../utils/process.js", async (importOriginal) => {
22
- const actual = (await importOriginal()) as typeof import("../../utils/process.js");
21
+ vi.mock("../../../utils/process.js", async (importOriginal) => {
22
+ const actual = (await importOriginal()) as typeof import("../../../utils/process.js");
23
23
  return {
24
24
  ...actual,
25
25
  listAgentProcesses: vi.fn(),
@@ -28,7 +28,7 @@ vi.mock("../../utils/process.js", async (importOriginal) => {
28
28
  };
29
29
  });
30
30
 
31
- vi.mock("../../utils/matching.js", () => ({
31
+ vi.mock("../../../utils/matching.js", () => ({
32
32
  generateAgentName: vi.fn(),
33
33
  }));
34
34
 
@@ -45,11 +45,10 @@ describe("CopilotAdapter", () => {
45
45
  let sessionStateDir: string;
46
46
 
47
47
  beforeEach(() => {
48
- adapter = new CopilotAdapter();
49
48
  tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "copilot-test-"));
50
49
  sessionStateDir = path.join(tmpDir, "session-state");
51
50
  fs.mkdirSync(sessionStateDir, { recursive: true });
52
- (adapter as any).sessionStateDir = sessionStateDir;
51
+ adapter = new CopilotAdapter(undefined, { sessionStateDir });
53
52
 
54
53
  mockedListAgentProcesses.mockReset();
55
54
  mockedEnrichProcesses.mockReset();
@@ -296,8 +295,7 @@ describe("CopilotAdapter", () => {
296
295
 
297
296
  it("carries the managed wrapper name to a process-only child before the session lock exists", async () => {
298
297
  const registry = new AgentRegistry(path.join(tmpDir, "agents.json"));
299
- adapter = new CopilotAdapter(registry);
300
- (adapter as any).sessionStateDir = sessionStateDir;
298
+ adapter = new CopilotAdapter(registry, { sessionStateDir });
301
299
  const processes: ProcessInfo[] = [
302
300
  { pid: 86800, command: "copilot", cwd: "/repo", tty: "ttys001", ppid: 84174 },
303
301
  {
@@ -363,8 +361,7 @@ describe("CopilotAdapter", () => {
363
361
 
364
362
  it("carries the managed wrapper name to a lock-backed child process", async () => {
365
363
  const registry = new AgentRegistry(path.join(tmpDir, "agents.json"));
366
- adapter = new CopilotAdapter(registry);
367
- (adapter as any).sessionStateDir = sessionStateDir;
364
+ adapter = new CopilotAdapter(registry, { sessionStateDir });
368
365
  const processes: ProcessInfo[] = [
369
366
  { pid: 14095, command: "copilot", cwd: "/repo", tty: "ttys001", ppid: 84174 },
370
367
  {
@@ -597,7 +594,7 @@ describe("CopilotAdapter", () => {
597
594
  });
598
595
 
599
596
  it("returns empty when session-state directory does not exist", async () => {
600
- (adapter as any).sessionStateDir = path.join(tmpDir, "missing");
597
+ adapter = new CopilotAdapter(undefined, { sessionStateDir: path.join(tmpDir, "missing") });
601
598
 
602
599
  await expect(adapter.listSessions()).resolves.toEqual([]);
603
600
  });
@@ -7,20 +7,22 @@ import * as fs from "fs";
7
7
  import * as os from "os";
8
8
  import * as path from "path";
9
9
 
10
- import { GeminiCliAdapter } from "../../adapters/GeminiCliAdapter.js";
11
- import type { ProcessInfo } from "../../adapters/AgentAdapter.js";
12
- import { AgentStatus } from "../../adapters/AgentAdapter.js";
13
- import { AgentRegistry, type RegistryEntry } from "../../utils/AgentRegistry.js";
10
+ import { GeminiCliAdapter } from "../../../providers/gemini/GeminiCliAdapter.js";
11
+ import { GeminiSessionLocator } from "../../../providers/gemini/GeminiSessionLocator.js";
12
+ import { GeminiSessionParser } from "../../../providers/gemini/GeminiSessionParser.js";
13
+ import type { ProcessInfo } from "../../../adapters/AgentAdapter.js";
14
+ import { AgentStatus } from "../../../adapters/AgentAdapter.js";
15
+ import { AgentRegistry, type RegistryEntry } from "../../../utils/AgentRegistry.js";
14
16
  import {
15
17
  listAgentProcesses,
16
18
  enrichProcesses,
17
19
  captureProcessSnapshot,
18
- } from "../../utils/process.js";
19
- import { matchProcessesToSessions, generateAgentName } from "../../utils/matching.js";
20
+ } from "../../../utils/process.js";
21
+ import { matchProcessesToSessions, generateAgentName } from "../../../utils/matching.js";
20
22
  import * as crypto from "crypto";
21
23
 
22
- vi.mock("../../utils/process.js", async (importOriginal) => {
23
- const actual = (await importOriginal()) as typeof import("../../utils/process.js");
24
+ vi.mock("../../../utils/process.js", async (importOriginal) => {
25
+ const actual = (await importOriginal()) as typeof import("../../../utils/process.js");
24
26
  return {
25
27
  ...actual,
26
28
  listAgentProcesses: vi.fn(),
@@ -29,7 +31,7 @@ vi.mock("../../utils/process.js", async (importOriginal) => {
29
31
  };
30
32
  });
31
33
 
32
- vi.mock("../../utils/matching.js", () => ({
34
+ vi.mock("../../../utils/matching.js", () => ({
33
35
  matchProcessesToSessions: vi.fn(),
34
36
  generateAgentName: vi.fn(),
35
37
  }));
@@ -47,12 +49,18 @@ const mockedGenerateAgentName = generateAgentName as MockedFunction<typeof gener
47
49
  describe("GeminiCliAdapter", () => {
48
50
  let adapter: GeminiCliAdapter;
49
51
  let tmpHome: string;
52
+ let parser: GeminiSessionParser;
53
+ let locator: GeminiSessionLocator;
50
54
 
51
55
  beforeEach(() => {
52
56
  tmpHome = fs.mkdtempSync(path.join(os.tmpdir(), "gemini-adapter-test-"));
53
57
  process.env.HOME = tmpHome;
54
58
 
55
59
  adapter = new GeminiCliAdapter(new AgentRegistry(path.join(tmpHome, "agents.json")));
60
+ parser = new GeminiSessionParser();
61
+ locator = new GeminiSessionLocator({
62
+ geminiTmpDir: path.join(tmpHome, ".gemini", "tmp"),
63
+ });
56
64
  mockedListAgentProcesses.mockReset();
57
65
  mockedEnrichProcesses.mockReset();
58
66
  mockedCaptureProcessSnapshot.mockReset();
@@ -621,7 +629,7 @@ describe("GeminiCliAdapter", () => {
621
629
  startTime: new Date(),
622
630
  };
623
631
  // tmp dir absent by default
624
- const result = (adapter as any).discoverSessions([proc]);
632
+ const result = locator.discoverSessions([proc]);
625
633
  expect(result.sessions).toEqual([]);
626
634
  expect(result.contentCache.size).toBe(0);
627
635
  });
@@ -643,7 +651,7 @@ describe("GeminiCliAdapter", () => {
643
651
  messages: [],
644
652
  });
645
653
 
646
- const result = (adapter as any).discoverSessions([proc]);
654
+ const result = locator.discoverSessions([proc]);
647
655
  expect(result.sessions).toEqual([]);
648
656
  });
649
657
 
@@ -664,7 +672,7 @@ describe("GeminiCliAdapter", () => {
664
672
  messages: [],
665
673
  });
666
674
 
667
- const result = (adapter as any).discoverSessions([proc]);
675
+ const result = locator.discoverSessions([proc]);
668
676
  expect(result.sessions).toEqual([]);
669
677
  });
670
678
 
@@ -690,7 +698,7 @@ describe("GeminiCliAdapter", () => {
690
698
  messages: [],
691
699
  });
692
700
 
693
- const result = (adapter as any).discoverSessions([proc]);
701
+ const result = locator.discoverSessions([proc]);
694
702
  expect(result.sessions).toHaveLength(1);
695
703
  expect(result.sessions[0].sessionId).toBe("s-good");
696
704
  });
@@ -716,7 +724,7 @@ describe("GeminiCliAdapter", () => {
716
724
  messages: [],
717
725
  });
718
726
 
719
- const result = (adapter as any).discoverSessions([proc]);
727
+ const result = locator.discoverSessions([proc]);
720
728
  expect(result.sessions).toHaveLength(1);
721
729
  expect(result.sessions[0].resolvedCwd).toBe(procCwd);
722
730
  });
@@ -742,12 +750,12 @@ describe("GeminiCliAdapter", () => {
742
750
  }),
743
751
  );
744
752
 
745
- const result = (adapter as any).discoverSessions([proc]);
753
+ const result = locator.discoverSessions([proc]);
746
754
  expect(result.sessions).toEqual([]);
747
755
  });
748
756
  });
749
757
 
750
- describe("helper methods", () => {
758
+ describe("GeminiSessionParser", () => {
751
759
  describe("determineStatus", () => {
752
760
  it('should return "waiting" when the last message is from gemini', () => {
753
761
  const session = {
@@ -758,7 +766,7 @@ describe("GeminiCliAdapter", () => {
758
766
  lastActive: new Date(),
759
767
  lastMessageType: "gemini",
760
768
  };
761
- expect((adapter as any).determineStatus(session)).toBe(AgentStatus.WAITING);
769
+ expect(parser.determineStatus(session)).toBe(AgentStatus.WAITING);
762
770
  });
763
771
 
764
772
  it('should return "waiting" when the last message is from assistant', () => {
@@ -770,7 +778,7 @@ describe("GeminiCliAdapter", () => {
770
778
  lastActive: new Date(),
771
779
  lastMessageType: "assistant",
772
780
  };
773
- expect((adapter as any).determineStatus(session)).toBe(AgentStatus.WAITING);
781
+ expect(parser.determineStatus(session)).toBe(AgentStatus.WAITING);
774
782
  });
775
783
 
776
784
  it('should return "running" when the last message is from the user', () => {
@@ -782,7 +790,7 @@ describe("GeminiCliAdapter", () => {
782
790
  lastActive: new Date(),
783
791
  lastMessageType: "user",
784
792
  };
785
- expect((adapter as any).determineStatus(session)).toBe(AgentStatus.RUNNING);
793
+ expect(parser.determineStatus(session)).toBe(AgentStatus.RUNNING);
786
794
  });
787
795
 
788
796
  it('should return "idle" when last activity is older than the threshold', () => {
@@ -794,7 +802,7 @@ describe("GeminiCliAdapter", () => {
794
802
  lastActive: new Date(Date.now() - 10 * 60 * 1000),
795
803
  lastMessageType: "gemini",
796
804
  };
797
- expect((adapter as any).determineStatus(session)).toBe(AgentStatus.IDLE);
805
+ expect(parser.determineStatus(session)).toBe(AgentStatus.IDLE);
798
806
  });
799
807
  });
800
808
 
@@ -812,7 +820,7 @@ describe("GeminiCliAdapter", () => {
812
820
  ],
813
821
  });
814
822
 
815
- const result = (adapter as any).parseSession(undefined, filePath);
823
+ const result = parser.parseSession(undefined, filePath);
816
824
  expect(result).toMatchObject({
817
825
  sessionId: "s1",
818
826
  projectPath: "/repo",
@@ -829,24 +837,24 @@ describe("GeminiCliAdapter", () => {
829
837
  messages: [],
830
838
  });
831
839
 
832
- const result = (adapter as any).parseSession(content, "/does/not/exist.json");
840
+ const result = parser.parseSession(content, "/does/not/exist.json");
833
841
  expect(result?.sessionId).toBe("s2");
834
842
  });
835
843
 
836
844
  it("should return null for a missing file with no cached content", () => {
837
- expect((adapter as any).parseSession(undefined, "/missing.json")).toBeNull();
845
+ expect(parser.parseSession(undefined, "/missing.json")).toBeNull();
838
846
  });
839
847
 
840
848
  it("should return null when the file is not valid JSON", () => {
841
849
  const filePath = path.join(tmpHome, "broken.json");
842
850
  fs.writeFileSync(filePath, "not json");
843
- expect((adapter as any).parseSession(undefined, filePath)).toBeNull();
851
+ expect(parser.parseSession(undefined, filePath)).toBeNull();
844
852
  });
845
853
 
846
854
  it("should return null when sessionId is missing", () => {
847
855
  const filePath = path.join(tmpHome, "no-id.json");
848
856
  fs.writeFileSync(filePath, JSON.stringify({ messages: [] }));
849
- expect((adapter as any).parseSession(undefined, filePath)).toBeNull();
857
+ expect(parser.parseSession(undefined, filePath)).toBeNull();
850
858
  });
851
859
 
852
860
  it("should default the summary when no user message has content", () => {
@@ -865,7 +873,7 @@ describe("GeminiCliAdapter", () => {
865
873
  ],
866
874
  });
867
875
 
868
- const result = (adapter as any).parseSession(undefined, filePath);
876
+ const result = parser.parseSession(undefined, filePath);
869
877
  expect(result?.summary).toBe("Gemini CLI session active");
870
878
  });
871
879
 
@@ -881,7 +889,7 @@ describe("GeminiCliAdapter", () => {
881
889
  ],
882
890
  });
883
891
 
884
- const result = (adapter as any).parseSession(undefined, filePath);
892
+ const result = parser.parseSession(undefined, filePath);
885
893
  expect(result?.summary.length).toBe(120);
886
894
  expect(result?.summary.endsWith("...")).toBe(true);
887
895
  });
@@ -902,7 +910,7 @@ describe("GeminiCliAdapter", () => {
902
910
  ],
903
911
  });
904
912
 
905
- const result = (adapter as any).parseSession(undefined, filePath);
913
+ const result = parser.parseSession(undefined, filePath);
906
914
  expect(result?.summary).toBe("hello from part continued");
907
915
  });
908
916
 
@@ -922,7 +930,7 @@ describe("GeminiCliAdapter", () => {
922
930
  ],
923
931
  });
924
932
 
925
- expect(() => (adapter as any).parseSession(undefined, filePath)).not.toThrow();
933
+ expect(() => parser.parseSession(undefined, filePath)).not.toThrow();
926
934
  });
927
935
 
928
936
  it("should drop non-text parts (data/file) when resolving user content", () => {
@@ -945,7 +953,7 @@ describe("GeminiCliAdapter", () => {
945
953
  ],
946
954
  });
947
955
 
948
- const result = (adapter as any).parseSession(undefined, filePath);
956
+ const result = parser.parseSession(undefined, filePath);
949
957
  expect(result?.summary).toBe("readable text + more");
950
958
  });
951
959
 
@@ -958,7 +966,7 @@ describe("GeminiCliAdapter", () => {
958
966
  messages: [{ id: "m1", timestamp: "2026-04-18T00:00:01Z", type: "user", content: "hi" }],
959
967
  });
960
968
 
961
- const result = (adapter as any).parseSession(undefined, filePath);
969
+ const result = parser.parseSession(undefined, filePath);
962
970
  expect(result?.lastActive.toISOString()).toBe("2026-04-18T00:10:00.000Z");
963
971
  });
964
972
  });