@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
@@ -0,0 +1,131 @@
1
+ import {
2
+ extractHerdrAgentPanes,
3
+ fetchHerdrAgentPanes,
4
+ findMatchingHerdrPane,
5
+ } from "../../runtime/herdr/HerdrAgentDiscovery.js";
6
+ import type { HerdrCommandRunner } from "../../runtime/herdr/HerdrCliClient.js";
7
+
8
+ function jsonResponse(value: unknown): { stdout: string; stderr: string } {
9
+ return { stdout: JSON.stringify(value), stderr: "" };
10
+ }
11
+
12
+ describe("HerdrAgentDiscovery", () => {
13
+ it("extracts agent panes from pane list responses", () => {
14
+ const panes = extractHerdrAgentPanes({
15
+ result: {
16
+ panes: [
17
+ {
18
+ agent: "codex",
19
+ agent_session: { value: "session-1" },
20
+ cwd: "/repo",
21
+ pane_id: "w32:p1",
22
+ workspace_id: "w32",
23
+ tab_id: "w32:t1",
24
+ },
25
+ ],
26
+ },
27
+ });
28
+
29
+ expect(panes).toEqual([
30
+ {
31
+ agent: "codex",
32
+ agentSessionId: "session-1",
33
+ cwd: "/repo",
34
+ paneId: "w32:p1",
35
+ workspaceId: "w32",
36
+ tabId: "w32:t1",
37
+ },
38
+ ]);
39
+ });
40
+
41
+ it("matches by foreground PID before falling back to ambiguous cwd and type", () => {
42
+ const pane = findMatchingHerdrPane(
43
+ {
44
+ type: "codex",
45
+ projectPath: "/repo",
46
+ sessionId: "pid-64904",
47
+ pid: 64904,
48
+ },
49
+ [
50
+ { agent: "codex", cwd: "/repo", paneId: "w31:p1", foregroundPids: [44290] },
51
+ { agent: "codex", cwd: "/repo", paneId: "w32:p1", foregroundPids: [64904] },
52
+ ],
53
+ );
54
+
55
+ expect(pane?.paneId).toBe("w32:p1");
56
+ });
57
+
58
+ it("prefers a foreground PID match over a shell PID match", () => {
59
+ const pane = findMatchingHerdrPane(
60
+ {
61
+ type: "codex",
62
+ projectPath: "/repo",
63
+ sessionId: "pid-64904",
64
+ pid: 64904,
65
+ },
66
+ [
67
+ { agent: "codex", cwd: "/repo", paneId: "w31:p1", shellPid: 64904 },
68
+ { agent: "codex", cwd: "/repo", paneId: "w32:p1", foregroundPids: [64904] },
69
+ ],
70
+ );
71
+
72
+ expect(pane?.paneId).toBe("w32:p1");
73
+ });
74
+
75
+ it("enriches pane list output with foreground process IDs", async () => {
76
+ const runner = vi
77
+ .fn<HerdrCommandRunner>()
78
+ .mockResolvedValueOnce(
79
+ jsonResponse({
80
+ result: {
81
+ panes: [
82
+ { agent: "codex", cwd: "/repo", pane_id: "w31:p1" },
83
+ { agent: "codex", cwd: "/repo", pane_id: "w32:p1" },
84
+ ],
85
+ },
86
+ }),
87
+ )
88
+ .mockResolvedValueOnce(
89
+ jsonResponse({
90
+ result: {
91
+ process_info: {
92
+ pane_id: "w31:p1",
93
+ shell_pid: 111,
94
+ foreground_processes: [{ pid: 44290 }],
95
+ },
96
+ },
97
+ }),
98
+ )
99
+ .mockResolvedValueOnce(
100
+ jsonResponse({
101
+ result: {
102
+ process_info: {
103
+ pane_id: "w32:p1",
104
+ shell_pid: 222,
105
+ foreground_processes: [{ pid: 64904 }],
106
+ },
107
+ },
108
+ }),
109
+ );
110
+
111
+ const panes = await fetchHerdrAgentPanes(runner);
112
+
113
+ expect(runner).toHaveBeenNthCalledWith(1, "herdr", ["pane", "list"]);
114
+ expect(runner).toHaveBeenNthCalledWith(2, "herdr", [
115
+ "pane",
116
+ "process-info",
117
+ "--pane",
118
+ "w31:p1",
119
+ ]);
120
+ expect(runner).toHaveBeenNthCalledWith(3, "herdr", [
121
+ "pane",
122
+ "process-info",
123
+ "--pane",
124
+ "w32:p1",
125
+ ]);
126
+ expect(panes).toMatchObject([
127
+ { paneId: "w31:p1", foregroundPids: [44290], shellPid: 111 },
128
+ { paneId: "w32:p1", foregroundPids: [64904], shellPid: 222 },
129
+ ]);
130
+ });
131
+ });
@@ -186,11 +186,30 @@ describe("HerdrAgentRuntime", () => {
186
186
  agentResponse(),
187
187
  processInfoResponse({ shellPid: 12000 }),
188
188
  ]);
189
- const runtime = new HerdrAgentRuntime({ runner, env: {} });
189
+ const runtime = new HerdrAgentRuntime({ runner, env: {}, processInfoPollMs: 0 });
190
190
 
191
191
  await expect(runtime.startAgent(startInput)).resolves.toMatchObject({ pid: 12000 });
192
192
  });
193
193
 
194
+ it("waits for a foreground process pid before falling back to the pane shell pid", async () => {
195
+ const runner = createRunner([
196
+ workspaceResponse(),
197
+ agentResponse(),
198
+ processInfoResponse({ shellPid: 12000 }),
199
+ processInfoResponse({ shellPid: 12000, pid: 12345, processName: "node" }),
200
+ ]);
201
+ const runtime = new HerdrAgentRuntime({
202
+ runner,
203
+ env: {},
204
+ processInfoPollMs: 100,
205
+ processInfoPollIntervalMs: 0,
206
+ });
207
+
208
+ await expect(runtime.startAgent(startInput)).resolves.toMatchObject({ pid: 12345 });
209
+ expect(runner).toHaveBeenNthCalledWith(3, "herdr", ["pane", "process-info", "--pane", "w1:p1"]);
210
+ expect(runner).toHaveBeenNthCalledWith(4, "herdr", ["pane", "process-info", "--pane", "w1:p1"]);
211
+ });
212
+
194
213
  it("passes only explicit extra agent args after the Herdr start separator", async () => {
195
214
  const runner = createRunner([
196
215
  workspaceResponse(),
@@ -92,24 +92,34 @@ describe("AgentRegistry", () => {
92
92
  });
93
93
  });
94
94
 
95
- it("lets a Herdr managed start replace an existing same-pid name", () => {
95
+ it("preserves existing identity and routing fields for same-pid refreshes", () => {
96
96
  const runtimeRef = { session: "default", paneId: "w1:p2", agentName: "requested-name" };
97
97
 
98
- registry.register(makeEntry({ name: "old-name", pid: process.pid }));
98
+ registry.register(
99
+ makeEntry({
100
+ name: "old-name",
101
+ pid: process.pid,
102
+ runtime: "tmux",
103
+ runtimeRef: { session: "old-name" },
104
+ cwd: "/original",
105
+ }),
106
+ );
99
107
  registry.register(
100
108
  makeEntry({
101
109
  name: "requested-name",
102
110
  pid: process.pid,
103
111
  runtime: "herdr",
104
112
  runtimeRef,
113
+ cwd: "/incoming",
105
114
  }),
106
115
  );
107
116
 
108
- expect(registry.lookup("old-name")).toBeNull();
109
- expect(registry.lookup("requested-name")).toMatchObject({
117
+ expect(registry.lookup("requested-name")).toBeNull();
118
+ expect(registry.lookup("old-name")).toMatchObject({
110
119
  pid: process.pid,
111
- runtime: "herdr",
112
- runtimeRef,
120
+ runtime: "tmux",
121
+ runtimeRef: { session: "old-name" },
122
+ cwd: "/original",
113
123
  });
114
124
  });
115
125
 
@@ -127,11 +137,11 @@ describe("AgentRegistry", () => {
127
137
  expect(registry.lookup("a")?.runtimeRef).toBeNull();
128
138
  });
129
139
 
130
- it("lets a managed start entry replace a generated fallback for the same pid", () => {
140
+ it("preserves an existing generated fallback for the same pid", () => {
131
141
  registry.register(makeEntry({ name: `ai-devkit-${process.pid}`, runtimeRef: null }));
132
142
  registry.register(makeEntry({ name: "custom-name", runtimeRef: { session: "custom-name" } }));
133
- expect(registry.lookup("custom-name")?.runtimeRef).toEqual({ session: "custom-name" });
134
- expect(registry.lookup(`ai-devkit-${process.pid}`)).toBeNull();
143
+ expect(registry.lookup("custom-name")).toBeNull();
144
+ expect(registry.lookup(`ai-devkit-${process.pid}`)?.runtimeRef).toBeNull();
135
145
  expect(registry.list()).toHaveLength(1);
136
146
  });
137
147
 
@@ -186,7 +196,7 @@ describe("AgentRegistry", () => {
186
196
  registry.register(makeEntry({ name: `ai-devkit-${process.pid}`, runtimeRef: null }));
187
197
 
188
198
  expect(registry.list()).toHaveLength(1);
189
- expect(registry.lookup("custom-name")?.pid).toBe(process.pid);
199
+ expect(registry.lookup(`ai-devkit-${process.pid}`)?.pid).toBe(process.pid);
190
200
  });
191
201
 
192
202
  it("cleans up a cross-type row when its pid has been reused", () => {
@@ -65,17 +65,21 @@ export class OpenCodeAdapter implements AgentAdapter {
65
65
  private static readonly IDLE_THRESHOLD_MINUTES = 5;
66
66
 
67
67
  private readonly dbPath: string;
68
+ private readonly cleanup = (): void => this.close();
68
69
  private db: Database.Database | null = null;
69
70
 
70
71
  constructor() {
71
72
  this.dbPath = OpenCodeAdapter.resolveDbPath();
72
- const cleanup = (): void => this.close();
73
- process.once("exit", cleanup);
74
- process.once("SIGINT", cleanup);
75
- process.once("SIGTERM", cleanup);
73
+ process.once("exit", this.cleanup);
74
+ process.once("SIGINT", this.cleanup);
75
+ process.once("SIGTERM", this.cleanup);
76
76
  }
77
77
 
78
78
  close(): void {
79
+ process.off("exit", this.cleanup);
80
+ process.off("SIGINT", this.cleanup);
81
+ process.off("SIGTERM", this.cleanup);
82
+
79
83
  if (this.db) {
80
84
  try {
81
85
  this.db.close();
@@ -1,7 +1,7 @@
1
1
  export { ClaudeCodeAdapter } from "../providers/claude/ClaudeCodeAdapter.js";
2
2
  export { CodexAdapter } from "../providers/codex/CodexAdapter.js";
3
- export { CopilotAdapter } from "./CopilotAdapter.js";
4
- export { GeminiCliAdapter } from "./GeminiCliAdapter.js";
3
+ export { CopilotAdapter } from "../providers/copilot/CopilotAdapter.js";
4
+ export { GeminiCliAdapter } from "../providers/gemini/GeminiCliAdapter.js";
5
5
  export { GrokCliAdapter } from "./GrokCliAdapter.js";
6
6
  export { OpenCodeAdapter } from "./OpenCodeAdapter.js";
7
7
  export { PiAdapter } from "../providers/pi/PiAdapter.js";
package/src/index.ts CHANGED
@@ -22,8 +22,8 @@ export type {
22
22
 
23
23
  export { ClaudeCodeAdapter } from "./providers/claude/ClaudeCodeAdapter.js";
24
24
  export { CodexAdapter } from "./providers/codex/CodexAdapter.js";
25
- export { CopilotAdapter } from "./adapters/CopilotAdapter.js";
26
- export { GeminiCliAdapter } from "./adapters/GeminiCliAdapter.js";
25
+ export { CopilotAdapter } from "./providers/copilot/CopilotAdapter.js";
26
+ export { GeminiCliAdapter } from "./providers/gemini/GeminiCliAdapter.js";
27
27
  export { GrokCliAdapter } from "./adapters/GrokCliAdapter.js";
28
28
  export { OpenCodeAdapter } from "./adapters/OpenCodeAdapter.js";
29
29
  export { PiAdapter } from "./providers/pi/PiAdapter.js";
@@ -43,7 +43,14 @@ export { TerminalFocusManager, TerminalType } from "./terminal/TerminalFocusMana
43
43
  export type { TerminalLocation } from "./terminal/TerminalFocusManager.js";
44
44
  export { TtyWriter } from "./terminal/TtyWriter.js";
45
45
 
46
- export type { ListAgentsOptions } from "./AgentManager.js";
46
+ export type { AgentManagerOptions, ListAgentsOptions } from "./AgentManager.js";
47
+ export {
48
+ extractHerdrAgentPanes,
49
+ fetchHerdrAgentPanes,
50
+ findMatchingHerdrPane,
51
+ herdrPaneToRuntimeRef,
52
+ } from "./runtime/herdr/HerdrAgentDiscovery.js";
53
+ export type { HerdrAgentPane } from "./runtime/herdr/HerdrAgentDiscovery.js";
47
54
 
48
55
  export {
49
56
  AgentRegistry,
@@ -0,0 +1,145 @@
1
+ /**
2
+ * Copilot Adapter
3
+ *
4
+ * Detects running GitHub Copilot CLI agents by:
5
+ * 1. Filtering Copilot processes from a shared asynchronous process snapshot
6
+ * 2. Using snapshot CWD and start-time enrichment
7
+ * 3. Mapping active ~/.copilot/session-state/{sessionId}/inuse.{pid}.lock files to processes
8
+ * 4. Reading events.jsonl as the primary session/conversation source
9
+ * 5. Reading workspace.yaml as a flat fallback metadata source
10
+ */
11
+
12
+ import * as path from "path";
13
+ import type {
14
+ AgentAdapter,
15
+ AgentInfo,
16
+ ConversationMessage,
17
+ ListSessionsOptions,
18
+ ProcessInfo,
19
+ SessionSummary,
20
+ AgentDetectionContext,
21
+ } from "../../adapters/AgentAdapter.js";
22
+ import {
23
+ captureProcessSnapshot,
24
+ executableBasename,
25
+ filterByProcessNames,
26
+ findWrapperProcess,
27
+ findWrapperProcessPids,
28
+ } from "../../utils/process.js";
29
+ import { AgentRegistry, type RegistryEntry } from "../../utils/AgentRegistry.js";
30
+ import { CopilotAgentMapper } from "./CopilotAgentMapper.js";
31
+ import { CopilotSessionLocator } from "./CopilotSessionLocator.js";
32
+ import { CopilotSessionParser } from "./CopilotSessionParser.js";
33
+
34
+ export interface CopilotAdapterOptions {
35
+ sessionStateDir?: string;
36
+ }
37
+
38
+ export class CopilotAdapter implements AgentAdapter {
39
+ readonly type = "copilot" as const;
40
+ readonly processNames = ["copilot"] as const;
41
+
42
+ private readonly registry: AgentRegistry;
43
+ private readonly parser: CopilotSessionParser;
44
+ private readonly mapper: CopilotAgentMapper;
45
+ private readonly locator: CopilotSessionLocator;
46
+
47
+ constructor(
48
+ registry: AgentRegistry = AgentRegistry.default(),
49
+ options: CopilotAdapterOptions = {},
50
+ ) {
51
+ const homeDir = process.env.HOME || process.env.USERPROFILE || "";
52
+ this.registry = registry;
53
+ this.parser = new CopilotSessionParser();
54
+ this.mapper = new CopilotAgentMapper(this.parser);
55
+ this.locator = new CopilotSessionLocator({
56
+ sessionStateDir: options.sessionStateDir ?? path.join(homeDir, ".copilot", "session-state"),
57
+ });
58
+ }
59
+
60
+ canHandle(processInfo: ProcessInfo): boolean {
61
+ return this.isCopilotExecutable(processInfo.command);
62
+ }
63
+
64
+ async detectAgents(context?: AgentDetectionContext): Promise<AgentInfo[]> {
65
+ const snapshot = context?.processes ?? (await captureProcessSnapshot(this.processNames));
66
+ const relevant = filterByProcessNames(snapshot, this.processNames);
67
+ const processes = relevant.filter((process) => this.canHandle(process));
68
+ if (processes.length === 0) return [];
69
+
70
+ const processByPid = new Map(processes.map((proc) => [proc.pid, proc]));
71
+ const registryEntriesByPid = new Map(this.registry.list().map((entry) => [entry.pid, entry]));
72
+ const matchedPids = new Set<number>();
73
+ const matchedProcesses: ProcessInfo[] = [];
74
+ const agents: AgentInfo[] = [];
75
+
76
+ for (const lock of this.locator.discoverActiveLocks()) {
77
+ const proc = processByPid.get(lock.pid);
78
+ if (!proc) continue;
79
+
80
+ const session = this.parser.readSessionDir(lock.sessionDir, lock.sessionId);
81
+ if (!session) continue;
82
+
83
+ const agent = this.mapper.mapSessionToAgent(session, proc);
84
+ this.applyWrapperRegistryName(agent, proc, processes, registryEntriesByPid);
85
+ agents.push(agent);
86
+ matchedPids.add(proc.pid);
87
+ matchedProcesses.push(proc);
88
+ }
89
+
90
+ const wrapperPids = findWrapperProcessPids(processes, matchedProcesses);
91
+ for (const proc of processes) {
92
+ if (!matchedPids.has(proc.pid) && !wrapperPids.has(proc.pid)) {
93
+ const agent = this.mapper.mapProcessOnlyAgent(proc);
94
+ this.applyWrapperRegistryName(agent, proc, processes, registryEntriesByPid);
95
+ agents.push(agent);
96
+ }
97
+ }
98
+
99
+ return agents;
100
+ }
101
+
102
+ getConversation(sessionFilePath: string, options?: { verbose?: boolean }): ConversationMessage[] {
103
+ return this.parser.getConversation(sessionFilePath, options);
104
+ }
105
+
106
+ async listSessions(opts?: ListSessionsOptions): Promise<SessionSummary[]> {
107
+ const summaries: SessionSummary[] = [];
108
+
109
+ for (const { sessionDir, sessionId } of this.locator.listSessionDirs()) {
110
+ const session = this.parser.readSessionDir(sessionDir, sessionId);
111
+ if (!session) continue;
112
+ if (opts?.cwd !== undefined && session.projectPath !== opts.cwd) continue;
113
+
114
+ summaries.push({
115
+ type: this.type,
116
+ sessionId: session.sessionId,
117
+ cwd: session.projectPath,
118
+ firstUserMessage: session.firstUserMessage,
119
+ lastActive: session.lastActive,
120
+ startedAt: session.sessionStart,
121
+ sessionFilePath: session.eventsFilePath,
122
+ });
123
+ }
124
+
125
+ return summaries;
126
+ }
127
+
128
+ private applyWrapperRegistryName(
129
+ agent: AgentInfo,
130
+ processInfo: ProcessInfo,
131
+ processes: ProcessInfo[],
132
+ registryEntriesByPid: Map<number, RegistryEntry>,
133
+ ): void {
134
+ const wrapper = findWrapperProcess(processes, processInfo);
135
+ const wrapperEntry = wrapper ? registryEntriesByPid.get(wrapper.pid) : undefined;
136
+ if (wrapperEntry?.type === this.type) {
137
+ agent.name = wrapperEntry.name;
138
+ }
139
+ }
140
+
141
+ private isCopilotExecutable(command: string): boolean {
142
+ const base = executableBasename(command);
143
+ return base === "copilot" || base === "copilot.exe";
144
+ }
145
+ }
@@ -0,0 +1,36 @@
1
+ import type { AgentInfo, ProcessInfo } from "../../adapters/AgentAdapter.js";
2
+ import { AgentStatus } from "../../adapters/AgentAdapter.js";
3
+ import { generateAgentName } from "../../utils/matching.js";
4
+ import { CopilotSessionParser, type CopilotSession } from "./CopilotSessionParser.js";
5
+
6
+ export class CopilotAgentMapper {
7
+ constructor(private readonly parser: CopilotSessionParser = new CopilotSessionParser()) {}
8
+
9
+ mapSessionToAgent(session: CopilotSession, processInfo: ProcessInfo): AgentInfo {
10
+ const projectPath = session.projectPath || processInfo.cwd || "";
11
+ return {
12
+ name: generateAgentName(projectPath, processInfo.pid),
13
+ type: "copilot",
14
+ status: this.parser.determineStatus(session),
15
+ summary: session.summary || "Copilot session active",
16
+ pid: processInfo.pid,
17
+ projectPath,
18
+ sessionId: session.sessionId,
19
+ lastActive: session.lastActive,
20
+ sessionFilePath: session.eventsFilePath,
21
+ };
22
+ }
23
+
24
+ mapProcessOnlyAgent(processInfo: ProcessInfo): AgentInfo {
25
+ return {
26
+ name: generateAgentName(processInfo.cwd || "", processInfo.pid),
27
+ type: "copilot",
28
+ status: AgentStatus.RUNNING,
29
+ summary: "Copilot process running",
30
+ pid: processInfo.pid,
31
+ projectPath: processInfo.cwd || "",
32
+ sessionId: `pid-${processInfo.pid}`,
33
+ lastActive: new Date(),
34
+ };
35
+ }
36
+ }
@@ -0,0 +1,57 @@
1
+ import * as path from "path";
2
+ import { isDirectory, safeReaddir } from "../../utils/session.js";
3
+
4
+ export interface CopilotLock {
5
+ sessionDir: string;
6
+ sessionId: string;
7
+ pid: number;
8
+ }
9
+
10
+ export interface CopilotSessionDir {
11
+ sessionDir: string;
12
+ sessionId: string;
13
+ }
14
+
15
+ export interface CopilotSessionLocatorOptions {
16
+ sessionStateDir?: string;
17
+ }
18
+
19
+ export class CopilotSessionLocator {
20
+ private readonly sessionStateDir: string;
21
+
22
+ constructor(options: CopilotSessionLocatorOptions = {}) {
23
+ const homeDir = process.env.HOME || process.env.USERPROFILE || "";
24
+ this.sessionStateDir =
25
+ options.sessionStateDir ?? path.join(homeDir, ".copilot", "session-state");
26
+ }
27
+
28
+ listSessionDirs(): CopilotSessionDir[] {
29
+ if (!isDirectory(this.sessionStateDir)) return [];
30
+
31
+ const sessionDirs: CopilotSessionDir[] = [];
32
+ for (const sessionId of safeReaddir(this.sessionStateDir)) {
33
+ const sessionDir = path.join(this.sessionStateDir, sessionId);
34
+ if (!isDirectory(sessionDir)) continue;
35
+
36
+ sessionDirs.push({ sessionDir, sessionId });
37
+ }
38
+
39
+ return sessionDirs;
40
+ }
41
+
42
+ discoverActiveLocks(): CopilotLock[] {
43
+ const locks: CopilotLock[] = [];
44
+ for (const { sessionDir, sessionId } of this.listSessionDirs()) {
45
+ for (const entry of safeReaddir(sessionDir)) {
46
+ const match = entry.match(/^inuse\.(\d+)\.lock$/);
47
+ if (!match) continue;
48
+
49
+ const pid = Number.parseInt(match[1], 10);
50
+ if (!Number.isFinite(pid)) continue;
51
+ locks.push({ sessionDir, sessionId, pid });
52
+ }
53
+ }
54
+
55
+ return locks;
56
+ }
57
+ }