@ai-devkit/agent-manager 0.1.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.
Files changed (57) hide show
  1. package/.eslintrc.json +31 -0
  2. package/dist/AgentManager.d.ts +104 -0
  3. package/dist/AgentManager.d.ts.map +1 -0
  4. package/dist/AgentManager.js +185 -0
  5. package/dist/AgentManager.js.map +1 -0
  6. package/dist/adapters/AgentAdapter.d.ts +76 -0
  7. package/dist/adapters/AgentAdapter.d.ts.map +1 -0
  8. package/dist/adapters/AgentAdapter.js +20 -0
  9. package/dist/adapters/AgentAdapter.js.map +1 -0
  10. package/dist/adapters/ClaudeCodeAdapter.d.ts +58 -0
  11. package/dist/adapters/ClaudeCodeAdapter.d.ts.map +1 -0
  12. package/dist/adapters/ClaudeCodeAdapter.js +274 -0
  13. package/dist/adapters/ClaudeCodeAdapter.js.map +1 -0
  14. package/dist/adapters/index.d.ts +4 -0
  15. package/dist/adapters/index.d.ts.map +1 -0
  16. package/dist/adapters/index.js +8 -0
  17. package/dist/adapters/index.js.map +1 -0
  18. package/dist/index.d.ts +10 -0
  19. package/dist/index.d.ts.map +1 -0
  20. package/dist/index.js +23 -0
  21. package/dist/index.js.map +1 -0
  22. package/dist/terminal/TerminalFocusManager.d.ts +22 -0
  23. package/dist/terminal/TerminalFocusManager.d.ts.map +1 -0
  24. package/dist/terminal/TerminalFocusManager.js +196 -0
  25. package/dist/terminal/TerminalFocusManager.js.map +1 -0
  26. package/dist/terminal/index.d.ts +3 -0
  27. package/dist/terminal/index.d.ts.map +1 -0
  28. package/dist/terminal/index.js +6 -0
  29. package/dist/terminal/index.js.map +1 -0
  30. package/dist/utils/file.d.ts +52 -0
  31. package/dist/utils/file.d.ts.map +1 -0
  32. package/dist/utils/file.js +135 -0
  33. package/dist/utils/file.js.map +1 -0
  34. package/dist/utils/index.d.ts +4 -0
  35. package/dist/utils/index.d.ts.map +1 -0
  36. package/dist/utils/index.js +15 -0
  37. package/dist/utils/index.js.map +1 -0
  38. package/dist/utils/process.d.ts +61 -0
  39. package/dist/utils/process.d.ts.map +1 -0
  40. package/dist/utils/process.js +166 -0
  41. package/dist/utils/process.js.map +1 -0
  42. package/jest.config.js +21 -0
  43. package/package.json +42 -0
  44. package/project.json +29 -0
  45. package/src/AgentManager.ts +198 -0
  46. package/src/__tests__/AgentManager.test.ts +308 -0
  47. package/src/__tests__/adapters/ClaudeCodeAdapter.test.ts +286 -0
  48. package/src/adapters/AgentAdapter.ts +94 -0
  49. package/src/adapters/ClaudeCodeAdapter.ts +344 -0
  50. package/src/adapters/index.ts +3 -0
  51. package/src/index.ts +12 -0
  52. package/src/terminal/TerminalFocusManager.ts +206 -0
  53. package/src/terminal/index.ts +2 -0
  54. package/src/utils/file.ts +100 -0
  55. package/src/utils/index.ts +3 -0
  56. package/src/utils/process.ts +184 -0
  57. package/tsconfig.json +17 -0
@@ -0,0 +1,286 @@
1
+ /**
2
+ * Tests for ClaudeCodeAdapter
3
+ */
4
+
5
+ import { describe, it, expect, beforeEach, jest } from '@jest/globals';
6
+ import { ClaudeCodeAdapter } from '../../adapters/ClaudeCodeAdapter';
7
+ import type { AgentInfo, ProcessInfo } from '../../adapters/AgentAdapter';
8
+ import { AgentStatus } from '../../adapters/AgentAdapter';
9
+ import { listProcesses } from '../../utils/process';
10
+
11
+ jest.mock('../../utils/process', () => ({
12
+ listProcesses: jest.fn(),
13
+ }));
14
+
15
+ const mockedListProcesses = listProcesses as jest.MockedFunction<typeof listProcesses>;
16
+
17
+ type PrivateMethod<T extends (...args: never[]) => unknown> = T;
18
+
19
+ interface AdapterPrivates {
20
+ readSessions: PrivateMethod<() => unknown[]>;
21
+ readHistory: PrivateMethod<() => unknown[]>;
22
+ }
23
+
24
+ describe('ClaudeCodeAdapter', () => {
25
+ let adapter: ClaudeCodeAdapter;
26
+
27
+ beforeEach(() => {
28
+ adapter = new ClaudeCodeAdapter();
29
+ mockedListProcesses.mockReset();
30
+ });
31
+
32
+ describe('initialization', () => {
33
+ it('should create adapter with correct type', () => {
34
+ expect(adapter.type).toBe('claude');
35
+ });
36
+ });
37
+
38
+ describe('canHandle', () => {
39
+ it('should return true for claude processes', () => {
40
+ const processInfo = {
41
+ pid: 12345,
42
+ command: 'claude',
43
+ cwd: '/test',
44
+ tty: 'ttys001',
45
+ };
46
+
47
+ expect(adapter.canHandle(processInfo)).toBe(true);
48
+ });
49
+
50
+ it('should return true for processes with "claude" in command (case-insensitive)', () => {
51
+ const processInfo = {
52
+ pid: 12345,
53
+ command: '/usr/local/bin/CLAUDE --some-flag',
54
+ cwd: '/test',
55
+ tty: 'ttys001',
56
+ };
57
+
58
+ expect(adapter.canHandle(processInfo)).toBe(true);
59
+ });
60
+
61
+ it('should return false for non-claude processes', () => {
62
+ const processInfo = {
63
+ pid: 12345,
64
+ command: 'node',
65
+ cwd: '/test',
66
+ tty: 'ttys001',
67
+ };
68
+
69
+ expect(adapter.canHandle(processInfo)).toBe(false);
70
+ });
71
+ });
72
+
73
+ describe('detectAgents', () => {
74
+ it('should return empty array if no claude processes running', async () => {
75
+ mockedListProcesses.mockReturnValue([]);
76
+
77
+ const agents = await adapter.detectAgents();
78
+ expect(agents).toEqual([]);
79
+ });
80
+
81
+ it('should detect agents using mocked process/session/history data', async () => {
82
+ const processData: ProcessInfo[] = [
83
+ {
84
+ pid: 12345,
85
+ command: 'claude --continue',
86
+ cwd: '/Users/test/my-project',
87
+ tty: 'ttys001',
88
+ },
89
+ ];
90
+
91
+ const sessionData = [
92
+ {
93
+ sessionId: 'session-1',
94
+ projectPath: '/Users/test/my-project',
95
+ sessionLogPath: '/mock/path/session-1.jsonl',
96
+ slug: 'merry-dog',
97
+ lastEntry: { type: 'assistant' },
98
+ lastActive: new Date(),
99
+ },
100
+ ];
101
+
102
+ const historyData = [
103
+ {
104
+ display: 'Investigate failing tests in package',
105
+ timestamp: Date.now(),
106
+ project: '/Users/test/my-project',
107
+ sessionId: 'session-1',
108
+ },
109
+ ];
110
+
111
+ mockedListProcesses.mockReturnValue(processData);
112
+ jest.spyOn(adapter as unknown as AdapterPrivates, 'readSessions').mockReturnValue(sessionData);
113
+ jest.spyOn(adapter as unknown as AdapterPrivates, 'readHistory').mockReturnValue(historyData);
114
+
115
+ const agents = await adapter.detectAgents();
116
+
117
+ expect(agents).toHaveLength(1);
118
+ expect(agents[0]).toMatchObject({
119
+ name: 'my-project',
120
+ type: 'claude',
121
+ status: AgentStatus.WAITING,
122
+ pid: 12345,
123
+ projectPath: '/Users/test/my-project',
124
+ sessionId: 'session-1',
125
+ slug: 'merry-dog',
126
+ });
127
+ expect(agents[0].summary).toContain('Investigate failing tests in package');
128
+ });
129
+
130
+ it('should return empty list when process cwd has no matching session', async () => {
131
+ mockedListProcesses.mockReturnValue([
132
+ {
133
+ pid: 777,
134
+ command: 'claude',
135
+ cwd: '/project/without-session',
136
+ tty: 'ttys008',
137
+ },
138
+ ]);
139
+ jest.spyOn(adapter as unknown as AdapterPrivates, 'readSessions').mockReturnValue([
140
+ {
141
+ sessionId: 'session-2',
142
+ projectPath: '/other/project',
143
+ sessionLogPath: '/mock/path/session-2.jsonl',
144
+ lastEntry: { type: 'assistant' },
145
+ lastActive: new Date(),
146
+ },
147
+ ]);
148
+ jest.spyOn(adapter as unknown as AdapterPrivates, 'readHistory').mockReturnValue([]);
149
+
150
+ const agents = await adapter.detectAgents();
151
+ expect(agents).toEqual([]);
152
+ });
153
+ });
154
+
155
+ describe('helper methods', () => {
156
+ describe('determineStatus', () => {
157
+ it('should return "unknown" for sessions with no last entry', () => {
158
+ const adapter = new ClaudeCodeAdapter();
159
+ const determineStatus = (adapter as any).determineStatus.bind(adapter);
160
+
161
+ const session = {
162
+ sessionId: 'test',
163
+ projectPath: '/test',
164
+ sessionLogPath: '/test/log',
165
+ };
166
+
167
+ const status = determineStatus(session);
168
+ expect(status).toBe(AgentStatus.UNKNOWN);
169
+ });
170
+
171
+ it('should return "waiting" for assistant entries', () => {
172
+ const adapter = new ClaudeCodeAdapter();
173
+ const determineStatus = (adapter as any).determineStatus.bind(adapter);
174
+
175
+ const session = {
176
+ sessionId: 'test',
177
+ projectPath: '/test',
178
+ sessionLogPath: '/test/log',
179
+ lastEntry: { type: 'assistant' },
180
+ lastActive: new Date(),
181
+ };
182
+
183
+ const status = determineStatus(session);
184
+ expect(status).toBe(AgentStatus.WAITING);
185
+ });
186
+
187
+ it('should return "waiting" for user interruption', () => {
188
+ const adapter = new ClaudeCodeAdapter();
189
+ const determineStatus = (adapter as any).determineStatus.bind(adapter);
190
+
191
+ const session = {
192
+ sessionId: 'test',
193
+ projectPath: '/test',
194
+ sessionLogPath: '/test/log',
195
+ lastEntry: {
196
+ type: 'user',
197
+ message: {
198
+ content: [{ type: 'text', text: '[Request interrupted by user for tool use]' }],
199
+ },
200
+ },
201
+ lastActive: new Date(),
202
+ };
203
+
204
+ const status = determineStatus(session);
205
+ expect(status).toBe(AgentStatus.WAITING);
206
+ });
207
+
208
+ it('should return "running" for user/progress entries', () => {
209
+ const adapter = new ClaudeCodeAdapter();
210
+ const determineStatus = (adapter as any).determineStatus.bind(adapter);
211
+
212
+ const session = {
213
+ sessionId: 'test',
214
+ projectPath: '/test',
215
+ sessionLogPath: '/test/log',
216
+ lastEntry: { type: 'user' },
217
+ lastActive: new Date(),
218
+ };
219
+
220
+ const status = determineStatus(session);
221
+ expect(status).toBe(AgentStatus.RUNNING);
222
+ });
223
+
224
+ it('should return "idle" for old sessions', () => {
225
+ const adapter = new ClaudeCodeAdapter();
226
+ const determineStatus = (adapter as any).determineStatus.bind(adapter);
227
+
228
+ const oldDate = new Date(Date.now() - 10 * 60 * 1000);
229
+
230
+ const session = {
231
+ sessionId: 'test',
232
+ projectPath: '/test',
233
+ sessionLogPath: '/test/log',
234
+ lastEntry: { type: 'assistant' },
235
+ lastActive: oldDate,
236
+ };
237
+
238
+ const status = determineStatus(session);
239
+ expect(status).toBe(AgentStatus.IDLE);
240
+ });
241
+ });
242
+
243
+ describe('generateAgentName', () => {
244
+ it('should use project name for first session', () => {
245
+ const adapter = new ClaudeCodeAdapter();
246
+ const generateAgentName = (adapter as any).generateAgentName.bind(adapter);
247
+
248
+ const session = {
249
+ sessionId: 'test-123',
250
+ projectPath: '/Users/test/my-project',
251
+ sessionLogPath: '/test/log',
252
+ };
253
+
254
+ const name = generateAgentName(session, []);
255
+ expect(name).toBe('my-project');
256
+ });
257
+
258
+ it('should append slug for duplicate projects', () => {
259
+ const adapter = new ClaudeCodeAdapter();
260
+ const generateAgentName = (adapter as any).generateAgentName.bind(adapter);
261
+
262
+ const existingAgent: AgentInfo = {
263
+ name: 'my-project',
264
+ projectPath: '/Users/test/my-project',
265
+ type: 'claude',
266
+ status: AgentStatus.RUNNING,
267
+ summary: 'Test',
268
+ pid: 123,
269
+ sessionId: 'existing-123',
270
+ slug: 'happy-cat',
271
+ lastActive: new Date(),
272
+ };
273
+
274
+ const session = {
275
+ sessionId: 'test-456',
276
+ projectPath: '/Users/test/my-project',
277
+ sessionLogPath: '/test/log',
278
+ slug: 'merry-dog',
279
+ };
280
+
281
+ const name = generateAgentName(session, [existingAgent]);
282
+ expect(name).toBe('my-project (merry)');
283
+ });
284
+ });
285
+ });
286
+ });
@@ -0,0 +1,94 @@
1
+ /**
2
+ * Agent Adapter Interface
3
+ *
4
+ * Defines the contract for detecting and managing different types of AI agents.
5
+ * Each adapter is responsible for detecting agents of a specific type (e.g., claude).
6
+ */
7
+
8
+ /**
9
+ * Type of AI agent
10
+ */
11
+ export type AgentType = 'claude' | 'gemini_cli' | 'codex' | 'other';
12
+
13
+ /**
14
+ * Current status of an agent
15
+ */
16
+ export enum AgentStatus {
17
+ RUNNING = 'running',
18
+ WAITING = 'waiting',
19
+ IDLE = 'idle',
20
+ UNKNOWN = 'unknown'
21
+ }
22
+
23
+ /**
24
+ * Information about a detected agent
25
+ */
26
+ export interface AgentInfo {
27
+ /** Project-based name (e.g., "ai-devkit" or "ai-devkit (merry)") */
28
+ name: string;
29
+
30
+ /** Type of agent */
31
+ type: AgentType;
32
+
33
+ /** Current status */
34
+ status: AgentStatus;
35
+
36
+ /** Last user prompt from history */
37
+ summary: string;
38
+
39
+ /** Process ID */
40
+ pid: number;
41
+
42
+ /** Working directory/project path */
43
+ projectPath: string;
44
+
45
+ /** Session UUID */
46
+ sessionId: string;
47
+
48
+ /** Human-readable session name (e.g., "merry-wobbling-starlight"), may be undefined for new sessions */
49
+ slug?: string;
50
+
51
+ /** Timestamp of last activity */
52
+ lastActive: Date;
53
+
54
+ }
55
+
56
+ /**
57
+ * Information about a running process
58
+ */
59
+ export interface ProcessInfo {
60
+ /** Process ID */
61
+ pid: number;
62
+
63
+ /** Process command */
64
+ command: string;
65
+
66
+ /** Working directory */
67
+ cwd: string;
68
+
69
+ /** Terminal TTY (e.g., "ttys030") */
70
+ tty: string;
71
+ }
72
+
73
+ /**
74
+ * Agent Adapter Interface
75
+ *
76
+ * Implementations must provide detection logic for a specific agent type.
77
+ */
78
+ export interface AgentAdapter {
79
+ /** Type of agent this adapter handles */
80
+ readonly type: AgentType;
81
+
82
+ /**
83
+ * Detect running agents of this type
84
+ * @returns List of detected agents
85
+ */
86
+ detectAgents(): Promise<AgentInfo[]>;
87
+
88
+ /**
89
+ * Check if this adapter can handle the given process
90
+ * @param processInfo Process information
91
+ * @returns True if this adapter can handle the process
92
+ */
93
+ canHandle(processInfo: ProcessInfo): boolean;
94
+ }