@ai-devkit/agent-manager 0.9.0 → 0.11.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 (63) hide show
  1. package/dist/AgentManager.d.ts +21 -1
  2. package/dist/AgentManager.d.ts.map +1 -1
  3. package/dist/AgentManager.js +47 -0
  4. package/dist/AgentManager.js.map +1 -1
  5. package/dist/adapters/AgentAdapter.d.ts +66 -0
  6. package/dist/adapters/AgentAdapter.d.ts.map +1 -1
  7. package/dist/adapters/ClaudeCodeAdapter.d.ts +14 -43
  8. package/dist/adapters/ClaudeCodeAdapter.d.ts.map +1 -1
  9. package/dist/adapters/ClaudeCodeAdapter.js +60 -275
  10. package/dist/adapters/ClaudeCodeAdapter.js.map +1 -1
  11. package/dist/adapters/CodexAdapter.d.ts +14 -1
  12. package/dist/adapters/CodexAdapter.d.ts.map +1 -1
  13. package/dist/adapters/CodexAdapter.js +105 -6
  14. package/dist/adapters/CodexAdapter.js.map +1 -1
  15. package/dist/adapters/GeminiCliAdapter.d.ts +9 -1
  16. package/dist/adapters/GeminiCliAdapter.d.ts.map +1 -1
  17. package/dist/adapters/GeminiCliAdapter.js +77 -6
  18. package/dist/adapters/GeminiCliAdapter.js.map +1 -1
  19. package/dist/index.d.ts +1 -1
  20. package/dist/index.d.ts.map +1 -1
  21. package/dist/index.js.map +1 -1
  22. package/dist/terminal/TerminalFocusManager.d.ts.map +1 -1
  23. package/dist/terminal/TerminalFocusManager.js +36 -27
  24. package/dist/terminal/TerminalFocusManager.js.map +1 -1
  25. package/dist/terminal/TtyWriter.d.ts.map +1 -1
  26. package/dist/terminal/TtyWriter.js +3 -12
  27. package/dist/terminal/TtyWriter.js.map +1 -1
  28. package/dist/utils/ClaudeSessionParser.d.ts +114 -0
  29. package/dist/utils/ClaudeSessionParser.d.ts.map +1 -0
  30. package/dist/utils/ClaudeSessionParser.js +377 -0
  31. package/dist/utils/ClaudeSessionParser.js.map +1 -0
  32. package/dist/utils/applescript.d.ts +6 -0
  33. package/dist/utils/applescript.d.ts.map +1 -0
  34. package/dist/utils/applescript.js +14 -0
  35. package/dist/utils/applescript.js.map +1 -0
  36. package/dist/utils/process.d.ts +3 -4
  37. package/dist/utils/process.d.ts.map +1 -1
  38. package/dist/utils/process.js +11 -15
  39. package/dist/utils/process.js.map +1 -1
  40. package/dist/utils/session.d.ts +34 -5
  41. package/dist/utils/session.d.ts.map +1 -1
  42. package/dist/utils/session.js +90 -44
  43. package/dist/utils/session.js.map +1 -1
  44. package/package.json +1 -1
  45. package/src/AgentManager.ts +66 -4
  46. package/src/__tests__/AgentManager.test.ts +134 -2
  47. package/src/__tests__/adapters/ClaudeCodeAdapter.test.ts +188 -32
  48. package/src/__tests__/adapters/CodexAdapter.test.ts +123 -3
  49. package/src/__tests__/adapters/GeminiCliAdapter.test.ts +133 -0
  50. package/src/__tests__/utils/ClaudeSessionParser.test.ts +195 -0
  51. package/src/__tests__/utils/process.test.ts +27 -27
  52. package/src/__tests__/utils/session.test.ts +79 -43
  53. package/src/adapters/AgentAdapter.ts +76 -0
  54. package/src/adapters/ClaudeCodeAdapter.ts +82 -356
  55. package/src/adapters/CodexAdapter.ts +126 -8
  56. package/src/adapters/GeminiCliAdapter.ts +102 -7
  57. package/src/index.ts +9 -1
  58. package/src/terminal/TerminalFocusManager.ts +35 -26
  59. package/src/terminal/TtyWriter.ts +1 -11
  60. package/src/utils/ClaudeSessionParser.ts +437 -0
  61. package/src/utils/applescript.ts +10 -0
  62. package/src/utils/process.ts +21 -24
  63. package/src/utils/session.ts +86 -45
@@ -3,25 +3,31 @@
3
3
  */
4
4
 
5
5
  import { describe, it, expect, jest, beforeEach } from '@jest/globals';
6
- import { execSync } from 'child_process';
6
+ import * as fs from 'fs';
7
7
  import { batchGetSessionFileBirthtimes } from '../../utils/session';
8
8
 
9
- jest.mock('child_process', () => ({
10
- execSync: jest.fn(),
9
+ jest.mock('fs', () => ({
10
+ readdirSync: jest.fn(),
11
+ statSync: jest.fn(),
11
12
  }));
12
13
 
13
- const mockedExecSync = execSync as jest.MockedFunction<typeof execSync>;
14
+ const mockedReaddirSync = fs.readdirSync as jest.MockedFunction<typeof fs.readdirSync>;
15
+ const mockedStatSync = fs.statSync as jest.MockedFunction<typeof fs.statSync>;
14
16
 
15
17
  describe('batchGetSessionFileBirthtimes', () => {
16
18
  beforeEach(() => {
17
- mockedExecSync.mockReset();
19
+ mockedReaddirSync.mockReset();
20
+ mockedStatSync.mockReset();
18
21
  });
19
22
 
20
- it('should parse stat output correctly', () => {
21
- mockedExecSync.mockReturnValue(
22
- '1710800324 /home/.claude/projects/my-app/abc123.jsonl\n' +
23
- '1710800500 /home/.claude/projects/my-app/def456.jsonl\n',
24
- );
23
+ it('should parse session files correctly', () => {
24
+ mockedReaddirSync.mockReturnValue([
25
+ 'abc123.jsonl',
26
+ 'def456.jsonl',
27
+ ] as unknown as ReturnType<typeof fs.readdirSync>);
28
+ mockedStatSync
29
+ .mockReturnValueOnce({ birthtimeMs: 1710800324000 } as fs.Stats)
30
+ .mockReturnValueOnce({ birthtimeMs: 1710800500000 } as fs.Stats);
25
31
 
26
32
  const results = batchGetSessionFileBirthtimes(['/home/.claude/projects/my-app']);
27
33
 
@@ -39,49 +45,57 @@ describe('batchGetSessionFileBirthtimes', () => {
39
45
 
40
46
  it('should return empty array for empty dirs list', () => {
41
47
  expect(batchGetSessionFileBirthtimes([])).toEqual([]);
42
- expect(mockedExecSync).not.toHaveBeenCalled();
48
+ expect(mockedReaddirSync).not.toHaveBeenCalled();
43
49
  });
44
50
 
45
- it('should return empty array on command failure', () => {
46
- mockedExecSync.mockImplementation(() => {
47
- throw new Error('Command failed');
51
+ it('should return empty array on readdir failure', () => {
52
+ mockedReaddirSync.mockImplementation(() => {
53
+ throw new Error('ENOENT');
48
54
  });
49
55
 
50
56
  expect(batchGetSessionFileBirthtimes(['/some/dir'])).toEqual([]);
51
57
  });
52
58
 
53
- it('should skip lines with invalid epoch (0 or negative)', () => {
54
- mockedExecSync.mockReturnValue(
55
- '0 /dir/bad.jsonl\n' +
56
- '-1 /dir/negative.jsonl\n' +
57
- '1710800324 /dir/good.jsonl\n',
58
- );
59
+ it('should skip files with invalid birthtime (0 or negative)', () => {
60
+ mockedReaddirSync.mockReturnValue([
61
+ 'bad.jsonl',
62
+ 'negative.jsonl',
63
+ 'good.jsonl',
64
+ ] as unknown as ReturnType<typeof fs.readdirSync>);
65
+ mockedStatSync
66
+ .mockReturnValueOnce({ birthtimeMs: 0 } as fs.Stats)
67
+ .mockReturnValueOnce({ birthtimeMs: -1 } as fs.Stats)
68
+ .mockReturnValueOnce({ birthtimeMs: 1710800324000 } as fs.Stats);
59
69
 
60
70
  const results = batchGetSessionFileBirthtimes(['/dir']);
61
71
  expect(results).toHaveLength(1);
62
72
  expect(results[0].sessionId).toBe('good');
63
73
  });
64
74
 
65
- it('should skip non-jsonl files in output', () => {
66
- mockedExecSync.mockReturnValue(
67
- '1710800324 /dir/sessions-index.json\n' +
68
- '1710800500 /dir/abc123.jsonl\n',
69
- );
75
+ it('should skip non-jsonl files', () => {
76
+ mockedReaddirSync.mockReturnValue([
77
+ 'sessions-index.json',
78
+ 'abc123.jsonl',
79
+ ] as unknown as ReturnType<typeof fs.readdirSync>);
80
+ mockedStatSync
81
+ .mockReturnValueOnce({ birthtimeMs: 1710800500000 } as fs.Stats);
70
82
 
71
83
  const results = batchGetSessionFileBirthtimes(['/dir']);
72
84
  expect(results).toHaveLength(1);
73
85
  expect(results[0].sessionId).toBe('abc123');
74
86
  });
75
87
 
76
- it('should handle empty output', () => {
77
- mockedExecSync.mockReturnValue('');
88
+ it('should handle empty directory', () => {
89
+ mockedReaddirSync.mockReturnValue([] as unknown as ReturnType<typeof fs.readdirSync>);
78
90
  expect(batchGetSessionFileBirthtimes(['/dir'])).toEqual([]);
79
91
  });
80
92
 
81
93
  it('should handle UUID session IDs', () => {
82
- mockedExecSync.mockReturnValue(
83
- '1710800324 /dir/068e7b1f-cff5-4c94-bf69-b9acd32d765c.jsonl\n',
84
- );
94
+ mockedReaddirSync.mockReturnValue([
95
+ '068e7b1f-cff5-4c94-bf69-b9acd32d765c.jsonl',
96
+ ] as unknown as ReturnType<typeof fs.readdirSync>);
97
+ mockedStatSync
98
+ .mockReturnValueOnce({ birthtimeMs: 1710800324000 } as fs.Stats);
85
99
 
86
100
  const results = batchGetSessionFileBirthtimes(['/dir']);
87
101
  expect(results).toHaveLength(1);
@@ -89,29 +103,51 @@ describe('batchGetSessionFileBirthtimes', () => {
89
103
  });
90
104
 
91
105
  it('should leave resolvedCwd empty', () => {
92
- mockedExecSync.mockReturnValue('1710800324 /dir/abc.jsonl\n');
106
+ mockedReaddirSync.mockReturnValue([
107
+ 'abc.jsonl',
108
+ ] as unknown as ReturnType<typeof fs.readdirSync>);
109
+ mockedStatSync
110
+ .mockReturnValueOnce({ birthtimeMs: 1710800324000 } as fs.Stats);
93
111
 
94
112
  const results = batchGetSessionFileBirthtimes(['/dir']);
95
113
  expect(results[0].resolvedCwd).toBe('');
96
114
  });
97
115
 
98
- it('should combine multiple directories into a single stat call', () => {
99
- mockedExecSync.mockReturnValue(
100
- '1710800324 /projects/app-a/sess1.jsonl\n' +
101
- '1710800400 /projects/app-b/sess2.jsonl\n' +
102
- '1710800500 /projects/app-a/sess3.jsonl\n',
103
- );
116
+ it('should enumerate multiple directories', () => {
117
+ mockedReaddirSync
118
+ .mockReturnValueOnce([
119
+ 'sess1.jsonl',
120
+ 'sess3.jsonl',
121
+ ] as unknown as ReturnType<typeof fs.readdirSync>)
122
+ .mockReturnValueOnce([
123
+ 'sess2.jsonl',
124
+ ] as unknown as ReturnType<typeof fs.readdirSync>);
125
+ mockedStatSync
126
+ .mockReturnValueOnce({ birthtimeMs: 1710800324000 } as fs.Stats)
127
+ .mockReturnValueOnce({ birthtimeMs: 1710800500000 } as fs.Stats)
128
+ .mockReturnValueOnce({ birthtimeMs: 1710800400000 } as fs.Stats);
104
129
 
105
130
  const results = batchGetSessionFileBirthtimes(['/projects/app-a', '/projects/app-b']);
106
131
 
107
- expect(mockedExecSync).toHaveBeenCalledTimes(1);
108
- const cmd = mockedExecSync.mock.calls[0][0] as string;
109
- expect(cmd).toContain('"/projects/app-a"/*.jsonl');
110
- expect(cmd).toContain('"/projects/app-b"/*.jsonl');
132
+ expect(mockedReaddirSync).toHaveBeenCalledTimes(2);
111
133
 
112
134
  expect(results).toHaveLength(3);
113
135
  expect(results[0].projectDir).toBe('/projects/app-a');
114
- expect(results[1].projectDir).toBe('/projects/app-b');
115
- expect(results[2].projectDir).toBe('/projects/app-a');
136
+ expect(results[1].projectDir).toBe('/projects/app-a');
137
+ expect(results[2].projectDir).toBe('/projects/app-b');
138
+ });
139
+
140
+ it('should skip files where statSync fails', () => {
141
+ mockedReaddirSync.mockReturnValue([
142
+ 'good.jsonl',
143
+ 'gone.jsonl',
144
+ ] as unknown as ReturnType<typeof fs.readdirSync>);
145
+ mockedStatSync
146
+ .mockReturnValueOnce({ birthtimeMs: 1710800324000 } as fs.Stats)
147
+ .mockImplementationOnce(() => { throw new Error('ENOENT'); });
148
+
149
+ const results = batchGetSessionFileBirthtimes(['/dir']);
150
+ expect(results).toHaveLength(1);
151
+ expect(results[0].sessionId).toBe('good');
116
152
  });
117
153
  });
@@ -81,6 +81,70 @@ export interface ConversationMessage {
81
81
  timestamp?: string;
82
82
  }
83
83
 
84
+ /**
85
+ * A historical session discovered on disk (running or not).
86
+ *
87
+ * Used by `listSessions` to surface enough context for a user to identify
88
+ * a session and resume it via the originating tool's resume command.
89
+ */
90
+ export interface SessionSummary {
91
+ /** Tool that produced this session */
92
+ type: AgentType;
93
+
94
+ /**
95
+ * ID accepted by the tool's resume command. Adapters MUST pass this
96
+ * through verbatim — no normalization, no encoding/decoding — so it
97
+ * round-trips into `claude --resume <id>` (and equivalents).
98
+ */
99
+ sessionId: string;
100
+
101
+ /** Working directory the session was started in (best-known value) */
102
+ cwd: string;
103
+
104
+ /**
105
+ * Trimmed first user message; empty string if none. Adapters apply
106
+ * the same noise-filter their existing parsers use (skip tool_result
107
+ * blocks, request-interruption notices, system-injected skill
108
+ * content). The CLI table renderer substitutes a placeholder for
109
+ * empty values; JSON output keeps the empty string raw.
110
+ */
111
+ firstUserMessage: string;
112
+
113
+ /** Last activity timestamp (from session content; falls back to file mtime) */
114
+ lastActive: Date;
115
+
116
+ /** Session start time (from session content; falls back to file birthtime/mtime) */
117
+ startedAt: Date;
118
+
119
+ /** Absolute path to the session file on disk (debug/diagnostics) */
120
+ sessionFilePath: string;
121
+ }
122
+
123
+ /**
124
+ * Filters passed by the CLI to {@link AgentAdapter.listSessions}.
125
+ *
126
+ * The CLI is the source of truth for filter defaults and semantics
127
+ * (e.g. cwd defaults to process.cwd(); --all clears it). Adapters apply
128
+ * the values they receive — they don't invent defaults.
129
+ */
130
+ export interface ListSessionsOptions {
131
+ /**
132
+ * Filter to sessions whose recorded cwd matches this path using strict
133
+ * equality (no prefix/ancestor matching in v1). Undefined = no cwd
134
+ * filter.
135
+ */
136
+ cwd?: string;
137
+
138
+ /**
139
+ * Filter to a single tool. Enforced by `AgentManager.listSessions`,
140
+ * which skips adapters whose `type` doesn't match. Adapters MAY
141
+ * ignore this field — by the time their `listSessions` runs, the
142
+ * type filter is already satisfied. Undefined = include every
143
+ * registered adapter.
144
+ */
145
+ type?: AgentType;
146
+ }
147
+
84
148
  /**
85
149
  * Agent Adapter Interface
86
150
  *
@@ -110,4 +174,16 @@ export interface AgentAdapter {
110
174
  * @returns Array of conversation messages
111
175
  */
112
176
  getConversation(sessionFilePath: string, options?: { verbose?: boolean }): ConversationMessage[];
177
+
178
+ /**
179
+ * Enumerate historical sessions for this tool from disk.
180
+ *
181
+ * Applies `opts.cwd` as a strict-equality filter when set. Returns
182
+ * {@link SessionSummary} entries unsorted; sorting and global filters
183
+ * are handled by `AgentManager` and the CLI.
184
+ *
185
+ * @param opts Filter options computed by the CLI
186
+ * @returns Array of sessions discovered on disk
187
+ */
188
+ listSessions(opts?: ListSessionsOptions): Promise<SessionSummary[]>;
113
189
  }