@ai-devkit/agent-manager 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,3 +1,4 @@
1
1
  export { TerminalFocusManager } from './TerminalFocusManager';
2
+ export { TerminalType } from './TerminalFocusManager';
2
3
  export type { TerminalLocation } from './TerminalFocusManager';
3
4
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/terminal/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,oBAAoB,EAAE,MAAM,wBAAwB,CAAC;AAC9D,YAAY,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/terminal/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,oBAAoB,EAAE,MAAM,wBAAwB,CAAC;AAC9D,OAAO,EAAE,YAAY,EAAE,MAAM,wBAAwB,CAAC;AACtD,YAAY,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAC"}
@@ -1,6 +1,8 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.TerminalFocusManager = void 0;
3
+ exports.TerminalType = exports.TerminalFocusManager = void 0;
4
4
  var TerminalFocusManager_1 = require("./TerminalFocusManager");
5
5
  Object.defineProperty(exports, "TerminalFocusManager", { enumerable: true, get: function () { return TerminalFocusManager_1.TerminalFocusManager; } });
6
+ var TerminalFocusManager_2 = require("./TerminalFocusManager");
7
+ Object.defineProperty(exports, "TerminalType", { enumerable: true, get: function () { return TerminalFocusManager_2.TerminalType; } });
6
8
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/terminal/index.ts"],"names":[],"mappings":";;;AAAA,+DAA8D;AAArD,4HAAA,oBAAoB,OAAA"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/terminal/index.ts"],"names":[],"mappings":";;;AAAA,+DAA8D;AAArD,4HAAA,oBAAoB,OAAA;AAC7B,+DAAsD;AAA7C,oHAAA,YAAY,OAAA"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ai-devkit/agent-manager",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "Standalone agent detection and management utilities for AI DevKit",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -127,7 +127,7 @@ describe('ClaudeCodeAdapter', () => {
127
127
  expect(agents[0].summary).toContain('Investigate failing tests in package');
128
128
  });
129
129
 
130
- it('should return empty list when process cwd has no matching session', async () => {
130
+ it('should include process-only entry when process cwd has no matching session', async () => {
131
131
  mockedListProcesses.mockReturnValue([
132
132
  {
133
133
  pid: 777,
@@ -148,7 +148,125 @@ describe('ClaudeCodeAdapter', () => {
148
148
  jest.spyOn(adapter as unknown as AdapterPrivates, 'readHistory').mockReturnValue([]);
149
149
 
150
150
  const agents = await adapter.detectAgents();
151
- expect(agents).toEqual([]);
151
+ expect(agents).toHaveLength(1);
152
+ expect(agents[0]).toMatchObject({
153
+ type: 'claude',
154
+ status: AgentStatus.RUNNING,
155
+ pid: 777,
156
+ projectPath: '/project/without-session',
157
+ sessionId: 'pid-777',
158
+ summary: 'Claude process running',
159
+ });
160
+ });
161
+
162
+ it('should match process in subdirectory to project-root session', async () => {
163
+ mockedListProcesses.mockReturnValue([
164
+ {
165
+ pid: 888,
166
+ command: 'claude',
167
+ cwd: '/Users/test/my-project/packages/cli',
168
+ tty: 'ttys009',
169
+ },
170
+ ]);
171
+ jest.spyOn(adapter as unknown as AdapterPrivates, 'readSessions').mockReturnValue([
172
+ {
173
+ sessionId: 'session-3',
174
+ projectPath: '/Users/test/my-project',
175
+ sessionLogPath: '/mock/path/session-3.jsonl',
176
+ slug: 'gentle-otter',
177
+ lastEntry: { type: 'assistant' },
178
+ lastActive: new Date(),
179
+ },
180
+ ]);
181
+ jest.spyOn(adapter as unknown as AdapterPrivates, 'readHistory').mockReturnValue([
182
+ {
183
+ display: 'Refactor CLI command flow',
184
+ timestamp: Date.now(),
185
+ project: '/Users/test/my-project',
186
+ sessionId: 'session-3',
187
+ },
188
+ ]);
189
+
190
+ const agents = await adapter.detectAgents();
191
+ expect(agents).toHaveLength(1);
192
+ expect(agents[0]).toMatchObject({
193
+ type: 'claude',
194
+ pid: 888,
195
+ sessionId: 'session-3',
196
+ projectPath: '/Users/test/my-project',
197
+ summary: 'Refactor CLI command flow',
198
+ });
199
+ });
200
+
201
+ it('should use latest history entry for process-only fallback session id', async () => {
202
+ mockedListProcesses.mockReturnValue([
203
+ {
204
+ pid: 97529,
205
+ command: 'claude',
206
+ cwd: '/Users/test/my-project/packages/cli',
207
+ tty: 'ttys021',
208
+ },
209
+ ]);
210
+ jest.spyOn(adapter as unknown as AdapterPrivates, 'readSessions').mockReturnValue([]);
211
+ jest.spyOn(adapter as unknown as AdapterPrivates, 'readHistory').mockReturnValue([
212
+ {
213
+ display: '/status',
214
+ timestamp: 1772122701536,
215
+ project: '/Users/test/my-project/packages/cli',
216
+ sessionId: '69237415-b0c3-4990-ba53-15882616509e',
217
+ },
218
+ ]);
219
+
220
+ const agents = await adapter.detectAgents();
221
+ expect(agents).toHaveLength(1);
222
+ expect(agents[0]).toMatchObject({
223
+ type: 'claude',
224
+ pid: 97529,
225
+ projectPath: '/Users/test/my-project/packages/cli',
226
+ sessionId: '69237415-b0c3-4990-ba53-15882616509e',
227
+ summary: '/status',
228
+ status: AgentStatus.RUNNING,
229
+ });
230
+ expect(agents[0].lastActive.toISOString()).toBe('2026-02-26T16:18:21.536Z');
231
+ });
232
+
233
+ it('should prefer exact-cwd history session over parent-project session match', async () => {
234
+ mockedListProcesses.mockReturnValue([
235
+ {
236
+ pid: 97529,
237
+ command: 'claude',
238
+ cwd: '/Users/test/my-project/packages/cli',
239
+ tty: 'ttys021',
240
+ },
241
+ ]);
242
+ jest.spyOn(adapter as unknown as AdapterPrivates, 'readSessions').mockReturnValue([
243
+ {
244
+ sessionId: 'old-parent-session',
245
+ projectPath: '/Users/test/my-project',
246
+ sessionLogPath: '/mock/path/old-parent-session.jsonl',
247
+ slug: 'fluffy-brewing-kazoo',
248
+ lastEntry: { type: 'assistant' },
249
+ lastActive: new Date('2026-02-23T17:24:50.996Z'),
250
+ },
251
+ ]);
252
+ jest.spyOn(adapter as unknown as AdapterPrivates, 'readHistory').mockReturnValue([
253
+ {
254
+ display: '/status',
255
+ timestamp: 1772122701536,
256
+ project: '/Users/test/my-project/packages/cli',
257
+ sessionId: '69237415-b0c3-4990-ba53-15882616509e',
258
+ },
259
+ ]);
260
+
261
+ const agents = await adapter.detectAgents();
262
+ expect(agents).toHaveLength(1);
263
+ expect(agents[0]).toMatchObject({
264
+ type: 'claude',
265
+ pid: 97529,
266
+ sessionId: '69237415-b0c3-4990-ba53-15882616509e',
267
+ projectPath: '/Users/test/my-project/packages/cli',
268
+ summary: '/status',
269
+ });
152
270
  });
153
271
  });
154
272
 
@@ -0,0 +1,319 @@
1
+ import { beforeEach, describe, expect, it, jest } from '@jest/globals';
2
+ import { CodexAdapter } from '../../adapters/CodexAdapter';
3
+ import type { ProcessInfo } from '../../adapters/AgentAdapter';
4
+ import { AgentStatus } from '../../adapters/AgentAdapter';
5
+ import { listProcesses } from '../../utils/process';
6
+
7
+ jest.mock('../../utils/process', () => ({
8
+ listProcesses: jest.fn(),
9
+ }));
10
+
11
+ const mockedListProcesses = listProcesses as jest.MockedFunction<typeof listProcesses>;
12
+
13
+ interface MockSession {
14
+ sessionId: string;
15
+ projectPath: string;
16
+ summary: string;
17
+ sessionStart?: Date;
18
+ lastActive: Date;
19
+ lastPayloadType?: string;
20
+ }
21
+
22
+ describe('CodexAdapter', () => {
23
+ let adapter: CodexAdapter;
24
+
25
+ beforeEach(() => {
26
+ adapter = new CodexAdapter();
27
+ mockedListProcesses.mockReset();
28
+ });
29
+
30
+ it('should expose codex type', () => {
31
+ expect(adapter.type).toBe('codex');
32
+ });
33
+
34
+ it('should match codex commands in canHandle', () => {
35
+ expect(
36
+ adapter.canHandle({
37
+ pid: 1,
38
+ command: 'codex',
39
+ cwd: '/repo',
40
+ tty: 'ttys001',
41
+ }),
42
+ ).toBe(true);
43
+
44
+ expect(
45
+ adapter.canHandle({
46
+ pid: 2,
47
+ command: '/usr/local/bin/CODEX --sandbox workspace-write',
48
+ cwd: '/repo',
49
+ tty: 'ttys002',
50
+ }),
51
+ ).toBe(true);
52
+
53
+ expect(
54
+ adapter.canHandle({
55
+ pid: 4,
56
+ command: 'node /worktrees/feature-codex-adapter-agent-manager-package/node_modules/nx/src/daemon/server/start.js',
57
+ cwd: '/repo',
58
+ tty: 'ttys004',
59
+ }),
60
+ ).toBe(false);
61
+
62
+ expect(
63
+ adapter.canHandle({
64
+ pid: 3,
65
+ command: 'node app.js',
66
+ cwd: '/repo',
67
+ tty: 'ttys003',
68
+ }),
69
+ ).toBe(false);
70
+ });
71
+
72
+ it('should return empty list when no codex process is running', async () => {
73
+ mockedListProcesses.mockReturnValue([]);
74
+
75
+ const agents = await adapter.detectAgents();
76
+ expect(agents).toEqual([]);
77
+ });
78
+
79
+ it('should map active codex sessions to matching processes by cwd', async () => {
80
+ mockedListProcesses.mockReturnValue([
81
+ { pid: 100, command: 'codex', cwd: '/repo-a', tty: 'ttys001' },
82
+ ] as ProcessInfo[]);
83
+
84
+ jest.spyOn(adapter as any, 'readSessions').mockReturnValue([
85
+ {
86
+ sessionId: 'abc12345-session',
87
+ projectPath: '/repo-a',
88
+ summary: 'Implement adapter flow',
89
+ sessionStart: new Date('2026-02-26T15:00:00.000Z'),
90
+ lastActive: new Date(),
91
+ lastPayloadType: 'token_count',
92
+ } as MockSession,
93
+ ]);
94
+
95
+ const agents = await adapter.detectAgents();
96
+ expect(agents).toHaveLength(1);
97
+ expect(agents[0]).toMatchObject({
98
+ name: 'repo-a',
99
+ type: 'codex',
100
+ status: AgentStatus.RUNNING,
101
+ summary: 'Implement adapter flow',
102
+ pid: 100,
103
+ projectPath: '/repo-a',
104
+ sessionId: 'abc12345-session',
105
+ });
106
+ });
107
+
108
+ it('should still map sessions with task_complete as waiting when process is running', async () => {
109
+ mockedListProcesses.mockReturnValue([
110
+ { pid: 101, command: 'codex', cwd: '/repo-b', tty: 'ttys001' },
111
+ ] as ProcessInfo[]);
112
+
113
+ jest.spyOn(adapter as any, 'readSessions').mockReturnValue([
114
+ {
115
+ sessionId: 'ended-1111',
116
+ projectPath: '/repo-b',
117
+ summary: 'Ended turn but process still alive',
118
+ sessionStart: new Date('2026-02-26T15:00:00.000Z'),
119
+ lastActive: new Date(),
120
+ lastPayloadType: 'task_complete',
121
+ } as MockSession,
122
+ ]);
123
+
124
+ const agents = await adapter.detectAgents();
125
+ expect(agents).toHaveLength(1);
126
+ expect(agents[0].sessionId).toBe('ended-1111');
127
+ expect(agents[0].status).toBe(AgentStatus.WAITING);
128
+ });
129
+
130
+ it('should use codex-session-id-prefix fallback name when cwd is missing', async () => {
131
+ mockedListProcesses.mockReturnValue([
132
+ { pid: 102, command: 'codex', cwd: '', tty: 'ttys009' },
133
+ ] as ProcessInfo[]);
134
+
135
+ jest.spyOn(adapter as any, 'readSessions').mockReturnValue([
136
+ {
137
+ sessionId: 'abcdef123456',
138
+ projectPath: '',
139
+ summary: 'No cwd available',
140
+ sessionStart: new Date('2026-02-26T15:00:00.000Z'),
141
+ lastActive: new Date(),
142
+ lastPayloadType: 'agent_reasoning',
143
+ } as MockSession,
144
+ ]);
145
+
146
+ const agents = await adapter.detectAgents();
147
+ expect(agents).toHaveLength(1);
148
+ expect(agents[0].name).toBe('codex-abcdef12');
149
+ });
150
+
151
+ it('should report waiting status for recent agent_message events', async () => {
152
+ mockedListProcesses.mockReturnValue([
153
+ { pid: 103, command: 'codex', cwd: '/repo-c', tty: 'ttys010' },
154
+ ] as ProcessInfo[]);
155
+
156
+ jest.spyOn(adapter as any, 'readSessions').mockReturnValue([
157
+ {
158
+ sessionId: 'waiting-1234',
159
+ projectPath: '/repo-c',
160
+ summary: 'Waiting',
161
+ sessionStart: new Date('2026-02-26T15:00:00.000Z'),
162
+ lastActive: new Date(),
163
+ lastPayloadType: 'agent_message',
164
+ } as MockSession,
165
+ ]);
166
+
167
+ const agents = await adapter.detectAgents();
168
+ expect(agents).toHaveLength(1);
169
+ expect(agents[0].status).toBe(AgentStatus.WAITING);
170
+ });
171
+
172
+ it('should report idle status when session exceeds shared threshold', async () => {
173
+ mockedListProcesses.mockReturnValue([
174
+ { pid: 104, command: 'codex', cwd: '/repo-d', tty: 'ttys011' },
175
+ ] as ProcessInfo[]);
176
+
177
+ jest.spyOn(adapter as any, 'readSessions').mockReturnValue([
178
+ {
179
+ sessionId: 'idle-5678',
180
+ projectPath: '/repo-d',
181
+ summary: 'Idle',
182
+ sessionStart: new Date('2026-02-26T15:00:00.000Z'),
183
+ lastActive: new Date(Date.now() - 10 * 60 * 1000),
184
+ lastPayloadType: 'token_count',
185
+ } as MockSession,
186
+ ]);
187
+
188
+ const agents = await adapter.detectAgents();
189
+ expect(agents).toHaveLength(1);
190
+ expect(agents[0].status).toBe(AgentStatus.IDLE);
191
+ });
192
+
193
+ it('should list unmatched running codex process even when no session matches', async () => {
194
+ mockedListProcesses.mockReturnValue([
195
+ { pid: 105, command: 'codex', cwd: '/repo-x', tty: 'ttys012' },
196
+ ] as ProcessInfo[]);
197
+
198
+ jest.spyOn(adapter as any, 'readSessions').mockReturnValue([
199
+ {
200
+ sessionId: 'other-session',
201
+ projectPath: '/repo-y',
202
+ summary: 'Other repo',
203
+ sessionStart: new Date('2026-02-26T15:00:00.000Z'),
204
+ lastActive: new Date(),
205
+ lastPayloadType: 'agent_message',
206
+ } as MockSession,
207
+ ]);
208
+
209
+ const agents = await adapter.detectAgents();
210
+ expect(agents).toHaveLength(1);
211
+ expect(agents[0].pid).toBe(105);
212
+ });
213
+
214
+ it('should list process when session metadata is unavailable', async () => {
215
+ mockedListProcesses.mockReturnValue([
216
+ { pid: 106, command: 'codex', cwd: '/repo-z', tty: 'ttys013' },
217
+ ] as ProcessInfo[]);
218
+ jest.spyOn(adapter as any, 'readSessions').mockReturnValue([]);
219
+
220
+ const agents = await adapter.detectAgents();
221
+ expect(agents).toHaveLength(1);
222
+ expect(agents[0].pid).toBe(106);
223
+ expect(agents[0].summary).toContain('No Codex session metadata');
224
+ });
225
+
226
+ it('should choose same-cwd session closest to process start time', async () => {
227
+ mockedListProcesses.mockReturnValue([
228
+ { pid: 107, command: 'codex', cwd: '/repo-time', tty: 'ttys014' },
229
+ ] as ProcessInfo[]);
230
+
231
+ jest.spyOn(adapter as any, 'readSessions').mockReturnValue([
232
+ {
233
+ sessionId: 'far-session',
234
+ projectPath: '/repo-time',
235
+ summary: 'Far start time',
236
+ sessionStart: new Date('2026-02-26T14:00:00.000Z'),
237
+ lastActive: new Date('2026-02-26T15:10:00.000Z'),
238
+ lastPayloadType: 'agent_message',
239
+ } as MockSession,
240
+ {
241
+ sessionId: 'near-session',
242
+ projectPath: '/repo-time',
243
+ summary: 'Near start time',
244
+ sessionStart: new Date('2026-02-26T15:00:20.000Z'),
245
+ lastActive: new Date('2026-02-26T15:11:00.000Z'),
246
+ lastPayloadType: 'agent_message',
247
+ } as MockSession,
248
+ ]);
249
+ jest.spyOn(adapter as any, 'getProcessStartTimes').mockReturnValue(
250
+ new Map([[107, new Date('2026-02-26T15:00:00.000Z')]]),
251
+ );
252
+
253
+ const agents = await adapter.detectAgents();
254
+ expect(agents).toHaveLength(1);
255
+ expect(agents[0].sessionId).toBe('near-session');
256
+ });
257
+
258
+ it('should prefer missing-cwd session before any-session fallback for unmatched process', async () => {
259
+ mockedListProcesses.mockReturnValue([
260
+ { pid: 108, command: 'codex', cwd: '/repo-missing-cwd', tty: 'ttys015' },
261
+ ] as ProcessInfo[]);
262
+
263
+ jest.spyOn(adapter as any, 'readSessions').mockReturnValue([
264
+ {
265
+ sessionId: 'any-session',
266
+ projectPath: '/another-repo',
267
+ summary: 'Any session fallback',
268
+ sessionStart: new Date('2026-02-26T15:00:00.000Z'),
269
+ lastActive: new Date('2026-02-26T15:12:00.000Z'),
270
+ lastPayloadType: 'agent_message',
271
+ } as MockSession,
272
+ {
273
+ sessionId: 'missing-cwd-session',
274
+ projectPath: '',
275
+ summary: 'Missing cwd session',
276
+ sessionStart: new Date('2026-02-26T15:00:10.000Z'),
277
+ lastActive: new Date('2026-02-26T15:11:00.000Z'),
278
+ lastPayloadType: 'agent_message',
279
+ } as MockSession,
280
+ ]);
281
+ jest.spyOn(adapter as any, 'getProcessStartTimes').mockReturnValue(
282
+ new Map([[108, new Date('2026-02-26T15:00:00.000Z')]]),
283
+ );
284
+
285
+ const agents = await adapter.detectAgents();
286
+ expect(agents).toHaveLength(1);
287
+ expect(agents[0].sessionId).toBe('missing-cwd-session');
288
+ });
289
+
290
+ it('should not reuse the same session for multiple running processes', async () => {
291
+ mockedListProcesses.mockReturnValue([
292
+ { pid: 109, command: 'codex', cwd: '/repo-shared', tty: 'ttys016' },
293
+ { pid: 110, command: 'codex', cwd: '/repo-shared', tty: 'ttys017' },
294
+ ] as ProcessInfo[]);
295
+
296
+ jest.spyOn(adapter as any, 'readSessions').mockReturnValue([
297
+ {
298
+ sessionId: 'shared-session',
299
+ projectPath: '/repo-shared',
300
+ summary: 'Only one session exists',
301
+ sessionStart: new Date('2026-02-26T15:00:00.000Z'),
302
+ lastActive: new Date('2026-02-26T15:11:00.000Z'),
303
+ lastPayloadType: 'agent_message',
304
+ } as MockSession,
305
+ ]);
306
+ jest.spyOn(adapter as any, 'getProcessStartTimes').mockReturnValue(
307
+ new Map([
308
+ [109, new Date('2026-02-26T15:00:00.000Z')],
309
+ [110, new Date('2026-02-26T15:00:30.000Z')],
310
+ ]),
311
+ );
312
+
313
+ const agents = await adapter.detectAgents();
314
+ expect(agents).toHaveLength(2);
315
+ const mappedAgents = agents.filter((agent) => agent.sessionId === 'shared-session');
316
+ expect(mappedAgents).toHaveLength(1);
317
+ expect(agents.some((agent) => agent.sessionId.startsWith('pid-'))).toBe(true);
318
+ });
319
+ });