@ai-devkit/agent-manager 0.16.0 → 0.18.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 (105) hide show
  1. package/.swcrc +27 -0
  2. package/dist/AgentManager.js +72 -74
  3. package/dist/AgentManager.js.map +1 -1
  4. package/dist/__tests__/AgentManager.test.js +564 -0
  5. package/dist/__tests__/AgentManager.test.js.map +1 -0
  6. package/dist/__tests__/adapters/ClaudeCodeAdapter.test.js +2157 -0
  7. package/dist/__tests__/adapters/ClaudeCodeAdapter.test.js.map +1 -0
  8. package/dist/__tests__/adapters/CodexAdapter.test.js +1419 -0
  9. package/dist/__tests__/adapters/CodexAdapter.test.js.map +1 -0
  10. package/dist/__tests__/adapters/CopilotAdapter.test.js +500 -0
  11. package/dist/__tests__/adapters/CopilotAdapter.test.js.map +1 -0
  12. package/dist/__tests__/adapters/GeminiCliAdapter.test.js +1229 -0
  13. package/dist/__tests__/adapters/GeminiCliAdapter.test.js.map +1 -0
  14. package/dist/__tests__/adapters/OpenCodeAdapter.test.js +626 -0
  15. package/dist/__tests__/adapters/OpenCodeAdapter.test.js.map +1 -0
  16. package/dist/__tests__/adapters/PiAdapter.test.js +590 -0
  17. package/dist/__tests__/adapters/PiAdapter.test.js.map +1 -0
  18. package/dist/__tests__/terminal/TmuxManager.test.js +159 -0
  19. package/dist/__tests__/terminal/TmuxManager.test.js.map +1 -0
  20. package/dist/__tests__/terminal/TtyWriter.test.js +162 -0
  21. package/dist/__tests__/terminal/TtyWriter.test.js.map +1 -0
  22. package/dist/__tests__/utils/AgentRegistry.test.js +282 -0
  23. package/dist/__tests__/utils/AgentRegistry.test.js.map +1 -0
  24. package/dist/__tests__/utils/ClaudeSessionParser.test.js +312 -0
  25. package/dist/__tests__/utils/ClaudeSessionParser.test.js.map +1 -0
  26. package/dist/__tests__/utils/matching.test.js +324 -0
  27. package/dist/__tests__/utils/matching.test.js.map +1 -0
  28. package/dist/__tests__/utils/process.test.js +177 -0
  29. package/dist/__tests__/utils/process.test.js.map +1 -0
  30. package/dist/__tests__/utils/session.test.js +158 -0
  31. package/dist/__tests__/utils/session.test.js.map +1 -0
  32. package/dist/adapters/AgentAdapter.d.ts +1 -1
  33. package/dist/adapters/AgentAdapter.d.ts.map +1 -1
  34. package/dist/adapters/AgentAdapter.js +8 -7
  35. package/dist/adapters/AgentAdapter.js.map +1 -1
  36. package/dist/adapters/ClaudeCodeAdapter.js +66 -77
  37. package/dist/adapters/ClaudeCodeAdapter.js.map +1 -1
  38. package/dist/adapters/CodexAdapter.d.ts +13 -0
  39. package/dist/adapters/CodexAdapter.d.ts.map +1 -1
  40. package/dist/adapters/CodexAdapter.js +225 -141
  41. package/dist/adapters/CodexAdapter.js.map +1 -1
  42. package/dist/adapters/CopilotAdapter.d.ts +44 -0
  43. package/dist/adapters/CopilotAdapter.d.ts.map +1 -0
  44. package/dist/adapters/CopilotAdapter.js +314 -0
  45. package/dist/adapters/CopilotAdapter.js.map +1 -0
  46. package/dist/adapters/GeminiCliAdapter.d.ts +2 -0
  47. package/dist/adapters/GeminiCliAdapter.d.ts.map +1 -1
  48. package/dist/adapters/GeminiCliAdapter.js +138 -156
  49. package/dist/adapters/GeminiCliAdapter.js.map +1 -1
  50. package/dist/adapters/OpenCodeAdapter.js +60 -69
  51. package/dist/adapters/OpenCodeAdapter.js.map +1 -1
  52. package/dist/adapters/PiAdapter.d.ts +62 -0
  53. package/dist/adapters/PiAdapter.d.ts.map +1 -0
  54. package/dist/adapters/PiAdapter.js +450 -0
  55. package/dist/adapters/PiAdapter.js.map +1 -0
  56. package/dist/adapters/index.d.ts +2 -0
  57. package/dist/adapters/index.d.ts.map +1 -1
  58. package/dist/adapters/index.js +3 -0
  59. package/dist/adapters/index.js.map +1 -1
  60. package/dist/index.d.ts +2 -0
  61. package/dist/index.d.ts.map +1 -1
  62. package/dist/index.js +3 -0
  63. package/dist/index.js.map +1 -1
  64. package/dist/terminal/TerminalFocusManager.js +62 -49
  65. package/dist/terminal/TerminalFocusManager.js.map +1 -1
  66. package/dist/terminal/TmuxManager.js +59 -34
  67. package/dist/terminal/TmuxManager.js.map +1 -1
  68. package/dist/terminal/TtyWriter.js +37 -16
  69. package/dist/terminal/TtyWriter.js.map +1 -1
  70. package/dist/terminal/index.js +1 -0
  71. package/dist/terminal/index.js.map +1 -1
  72. package/dist/utils/AgentRegistry.js +41 -32
  73. package/dist/utils/AgentRegistry.js.map +1 -1
  74. package/dist/utils/ClaudeSessionParser.js +46 -87
  75. package/dist/utils/ClaudeSessionParser.js.map +1 -1
  76. package/dist/utils/agents.d.ts +1 -1
  77. package/dist/utils/agents.d.ts.map +1 -1
  78. package/dist/utils/agents.js +22 -11
  79. package/dist/utils/agents.js.map +1 -1
  80. package/dist/utils/applescript.js +3 -6
  81. package/dist/utils/applescript.js.map +1 -1
  82. package/dist/utils/index.js +1 -0
  83. package/dist/utils/index.js.map +1 -1
  84. package/dist/utils/matching.js +21 -29
  85. package/dist/utils/matching.js.map +1 -1
  86. package/dist/utils/process.js +77 -56
  87. package/dist/utils/process.js.map +1 -1
  88. package/dist/utils/session.js +20 -34
  89. package/dist/utils/session.js.map +1 -1
  90. package/dist/utils/sortAgents.js +6 -8
  91. package/dist/utils/sortAgents.js.map +1 -1
  92. package/package.json +11 -7
  93. package/src/__tests__/adapters/ClaudeCodeAdapter.test.ts +2 -0
  94. package/src/__tests__/adapters/CodexAdapter.test.ts +145 -0
  95. package/src/__tests__/adapters/CopilotAdapter.test.ts +411 -0
  96. package/src/__tests__/adapters/GeminiCliAdapter.test.ts +74 -1
  97. package/src/__tests__/adapters/PiAdapter.test.ts +435 -0
  98. package/src/adapters/AgentAdapter.ts +1 -1
  99. package/src/adapters/CodexAdapter.ts +164 -6
  100. package/src/adapters/CopilotAdapter.ts +453 -0
  101. package/src/adapters/GeminiCliAdapter.ts +42 -3
  102. package/src/adapters/PiAdapter.ts +597 -0
  103. package/src/adapters/index.ts +2 -0
  104. package/src/index.ts +2 -0
  105. package/src/utils/agents.ts +1 -1
@@ -0,0 +1,626 @@
1
+ /**
2
+ * Tests for OpenCodeAdapter
3
+ */ import * as fs from 'fs';
4
+ import * as path from 'path';
5
+ import { OpenCodeAdapter } from '../../adapters/OpenCodeAdapter.js';
6
+ import { AgentStatus } from '../../adapters/AgentAdapter.js';
7
+ import { listAgentProcesses, enrichProcesses } from '../../utils/process.js';
8
+ import { generateAgentName } from '../../utils/matching.js';
9
+ import * as os from 'os';
10
+ vi.mock('../../utils/process.js', ()=>({
11
+ listAgentProcesses: vi.fn(),
12
+ enrichProcesses: vi.fn()
13
+ }));
14
+ vi.mock('../../utils/matching.js', ()=>({
15
+ generateAgentName: vi.fn(),
16
+ matchProcessesToSessions: vi.fn()
17
+ }));
18
+ const mockedListAgentProcesses = listAgentProcesses;
19
+ const mockedEnrichProcesses = enrichProcesses;
20
+ const mockedGenerateAgentName = generateAgentName;
21
+ function makeDb(queries) {
22
+ const prepareImpl = (sql)=>{
23
+ const normalized = sql.replace(/\s+/g, ' ').trim().toLowerCase();
24
+ if (normalized.includes('from session')) {
25
+ return {
26
+ all: ()=>(queries.session ?? []).map((r)=>({
27
+ id: r.id,
28
+ directory: r.directory,
29
+ timeCreated: r.time_created
30
+ })),
31
+ get: (dir)=>{
32
+ const match = (queries.session ?? []).find((r)=>r.directory === dir);
33
+ if (!match) return undefined;
34
+ return {
35
+ id: match.id,
36
+ directory: match.directory,
37
+ time_created: match.time_created
38
+ };
39
+ }
40
+ };
41
+ }
42
+ if (normalized.includes('max(time_updated)')) {
43
+ return {
44
+ get: ()=>({
45
+ maxUpdated: queries.lastMessage?.timeUpdated ?? 0
46
+ })
47
+ };
48
+ }
49
+ if (normalized.includes('from message') && !normalized.includes("'$.time.completed'") && !normalized.includes('order by p.time_created')) {
50
+ return {
51
+ get: ()=>queries.lastMessage ?? undefined
52
+ };
53
+ }
54
+ if (normalized.includes("'$.time.completed'")) {
55
+ return {
56
+ get: ()=>queries.lastAssistant === undefined ? undefined : queries.lastAssistant ?? undefined
57
+ };
58
+ }
59
+ if (normalized.includes("json_extract(m.data, '$.role') = 'user'")) {
60
+ return {
61
+ get: ()=>queries.firstUserText === undefined ? undefined : queries.firstUserText ?? undefined
62
+ };
63
+ }
64
+ if (normalized.includes('order by p.time_created asc')) {
65
+ return {
66
+ all: ()=>queries.parts ?? []
67
+ };
68
+ }
69
+ return {
70
+ all: ()=>[],
71
+ get: ()=>undefined
72
+ };
73
+ };
74
+ return {
75
+ prepare: prepareImpl,
76
+ close: vi.fn()
77
+ };
78
+ }
79
+ function makeDbConstructor(db) {
80
+ return vi.fn().mockReturnValue(db);
81
+ }
82
+ describe('OpenCodeAdapter', ()=>{
83
+ let adapter;
84
+ let tmpDir;
85
+ let dbPath;
86
+ beforeEach(()=>{
87
+ adapter = new OpenCodeAdapter();
88
+ mockedListAgentProcesses.mockReset();
89
+ mockedEnrichProcesses.mockReset();
90
+ mockedGenerateAgentName.mockReset();
91
+ mockedEnrichProcesses.mockImplementation((procs)=>procs);
92
+ mockedGenerateAgentName.mockImplementation((cwd, pid)=>{
93
+ const folder = path.basename(cwd) || 'unknown';
94
+ return `${folder}-${pid}`;
95
+ });
96
+ tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'opencode-test-'));
97
+ dbPath = path.join(tmpDir, 'opencode.db');
98
+ adapter.dbPath = dbPath;
99
+ adapter.db = null;
100
+ });
101
+ afterEach(()=>{
102
+ fs.rmSync(tmpDir, {
103
+ recursive: true,
104
+ force: true
105
+ });
106
+ vi.restoreAllMocks();
107
+ });
108
+ describe('type', ()=>{
109
+ it('exposes opencode type', ()=>{
110
+ expect(adapter.type).toBe('opencode');
111
+ });
112
+ });
113
+ describe('canHandle', ()=>{
114
+ it('returns true for opencode command', ()=>{
115
+ expect(adapter.canHandle({
116
+ pid: 1,
117
+ command: 'opencode',
118
+ cwd: '/repo',
119
+ tty: 'ttys001'
120
+ })).toBe(true);
121
+ });
122
+ it('returns true for full path opencode', ()=>{
123
+ expect(adapter.canHandle({
124
+ pid: 2,
125
+ command: '/usr/local/bin/opencode serve',
126
+ cwd: '/repo',
127
+ tty: 'ttys002'
128
+ })).toBe(true);
129
+ });
130
+ it('returns true for opencode.exe with unix-style path', ()=>{
131
+ expect(adapter.canHandle({
132
+ pid: 3,
133
+ command: '/usr/bin/opencode.exe',
134
+ cwd: '/repo',
135
+ tty: 'ttys003'
136
+ })).toBe(true);
137
+ });
138
+ it('returns false for non-opencode processes', ()=>{
139
+ expect(adapter.canHandle({
140
+ pid: 4,
141
+ command: 'node server.js',
142
+ cwd: '/repo',
143
+ tty: 'ttys004'
144
+ })).toBe(false);
145
+ });
146
+ it('returns false when opencode appears only in path args', ()=>{
147
+ expect(adapter.canHandle({
148
+ pid: 5,
149
+ command: 'node /projects/opencode-plugin/index.js',
150
+ cwd: '/repo',
151
+ tty: 'ttys005'
152
+ })).toBe(false);
153
+ });
154
+ });
155
+ describe('detectAgents', ()=>{
156
+ it('returns empty list when no opencode processes running', async ()=>{
157
+ mockedListAgentProcesses.mockReturnValue([]);
158
+ const agents = await adapter.detectAgents();
159
+ expect(agents).toEqual([]);
160
+ expect(mockedListAgentProcesses).toHaveBeenCalledWith('opencode');
161
+ });
162
+ it('returns process-only agent when DB does not exist', async ()=>{
163
+ const procs = [
164
+ {
165
+ pid: 100,
166
+ command: 'opencode',
167
+ cwd: '/repo',
168
+ tty: 'ttys001'
169
+ }
170
+ ];
171
+ mockedListAgentProcesses.mockReturnValue(procs);
172
+ mockedEnrichProcesses.mockReturnValue(procs);
173
+ const agents = await adapter.detectAgents();
174
+ expect(agents).toHaveLength(1);
175
+ expect(agents[0]).toMatchObject({
176
+ type: 'opencode',
177
+ status: AgentStatus.RUNNING,
178
+ pid: 100,
179
+ projectPath: '/repo',
180
+ sessionId: 'pid-100',
181
+ summary: 'OpenCode process running'
182
+ });
183
+ });
184
+ it('returns process-only agent when no session matches CWD', async ()=>{
185
+ const procs = [
186
+ {
187
+ pid: 100,
188
+ command: 'opencode',
189
+ cwd: '/repo',
190
+ tty: 'ttys001'
191
+ }
192
+ ];
193
+ mockedListAgentProcesses.mockReturnValue(procs);
194
+ mockedEnrichProcesses.mockReturnValue(procs);
195
+ fs.writeFileSync(dbPath, ''); // file exists but empty → sqlite throws
196
+ const db = makeDb({
197
+ session: []
198
+ }); // no matching session
199
+ vi.doMock('better-sqlite3', ()=>makeDbConstructor(db));
200
+ adapter.db = db;
201
+ const agents = await adapter.detectAgents();
202
+ expect(agents).toHaveLength(1);
203
+ expect(agents[0]).toMatchObject({
204
+ type: 'opencode',
205
+ status: AgentStatus.RUNNING,
206
+ sessionId: 'pid-100'
207
+ });
208
+ });
209
+ it('returns waiting when assistant turn has time.completed set', async ()=>{
210
+ const now = Date.now();
211
+ const procs = [
212
+ {
213
+ pid: 200,
214
+ command: 'opencode',
215
+ cwd: '/my-project',
216
+ tty: 'ttys002'
217
+ }
218
+ ];
219
+ mockedListAgentProcesses.mockReturnValue(procs);
220
+ mockedEnrichProcesses.mockReturnValue(procs);
221
+ const db = makeDb({
222
+ session: [
223
+ {
224
+ id: 'sess-001',
225
+ directory: '/my-project',
226
+ time_created: now - 60000
227
+ }
228
+ ],
229
+ lastMessage: {
230
+ role: 'assistant',
231
+ timeUpdated: now - 60_000
232
+ },
233
+ lastAssistant: {
234
+ completed: now - 30_000,
235
+ errored: null
236
+ },
237
+ firstUserText: {
238
+ text: 'Refactor the auth module'
239
+ }
240
+ });
241
+ adapter.db = db;
242
+ const agents = await adapter.detectAgents();
243
+ expect(agents).toHaveLength(1);
244
+ expect(agents[0]).toMatchObject({
245
+ type: 'opencode',
246
+ status: AgentStatus.WAITING,
247
+ sessionId: 'sess-001',
248
+ summary: 'Refactor the auth module'
249
+ });
250
+ });
251
+ it('returns running when assistant turn has no time.completed (in-progress, any age)', async ()=>{
252
+ const now = Date.now();
253
+ const procs = [
254
+ {
255
+ pid: 250,
256
+ command: 'opencode',
257
+ cwd: '/proj',
258
+ tty: 'ttys004'
259
+ }
260
+ ];
261
+ mockedListAgentProcesses.mockReturnValue(procs);
262
+ mockedEnrichProcesses.mockReturnValue(procs);
263
+ const db = makeDb({
264
+ session: [
265
+ {
266
+ id: 'sess-tool',
267
+ directory: '/proj',
268
+ time_created: now - 180_000
269
+ }
270
+ ],
271
+ lastMessage: {
272
+ role: 'assistant',
273
+ timeUpdated: now - 120_000
274
+ },
275
+ lastAssistant: {
276
+ completed: null,
277
+ errored: null
278
+ },
279
+ firstUserText: {
280
+ text: 'Run the build'
281
+ }
282
+ });
283
+ adapter.db = db;
284
+ const agents = await adapter.detectAgents();
285
+ expect(agents[0]).toMatchObject({
286
+ status: AgentStatus.RUNNING,
287
+ sessionId: 'sess-tool'
288
+ });
289
+ });
290
+ it('returns running between steps (the bug fix) — no time.completed even during quiet moment', async ()=>{
291
+ const now = Date.now();
292
+ const procs = [
293
+ {
294
+ pid: 260,
295
+ command: 'opencode',
296
+ cwd: '/proj-b',
297
+ tty: 'ttys005'
298
+ }
299
+ ];
300
+ mockedListAgentProcesses.mockReturnValue(procs);
301
+ mockedEnrichProcesses.mockReturnValue(procs);
302
+ const db = makeDb({
303
+ session: [
304
+ {
305
+ id: 'sess-mid',
306
+ directory: '/proj-b',
307
+ time_created: now - 60_000
308
+ }
309
+ ],
310
+ lastMessage: {
311
+ role: 'assistant',
312
+ timeUpdated: now - 45_000
313
+ },
314
+ lastAssistant: {
315
+ completed: null,
316
+ errored: null
317
+ },
318
+ firstUserText: null
319
+ });
320
+ adapter.db = db;
321
+ const agents = await adapter.detectAgents();
322
+ expect(agents[0].status).toBe(AgentStatus.RUNNING);
323
+ });
324
+ it('returns waiting even when latest user message was metadata-updated after assistant completion', async ()=>{
325
+ // Regression: OpenCode updates user.message.time_updated when appending
326
+ // summary diffs after a turn finishes. Ordering by time_created (not
327
+ // time_updated) keeps the assistant message correctly identified as latest.
328
+ const now = Date.now();
329
+ const procs = [
330
+ {
331
+ pid: 270,
332
+ command: 'opencode',
333
+ cwd: '/proj-c',
334
+ tty: 'ttys006'
335
+ }
336
+ ];
337
+ mockedListAgentProcesses.mockReturnValue(procs);
338
+ mockedEnrichProcesses.mockReturnValue(procs);
339
+ // makeDb returns lastMessage from the time_created-ordered query — supply the assistant.
340
+ const db = makeDb({
341
+ session: [
342
+ {
343
+ id: 'sess-meta',
344
+ directory: '/proj-c',
345
+ time_created: now - 120_000
346
+ }
347
+ ],
348
+ lastMessage: {
349
+ role: 'assistant',
350
+ timeUpdated: now - 30_000
351
+ },
352
+ lastAssistant: {
353
+ completed: now - 30_000,
354
+ errored: null
355
+ },
356
+ firstUserText: null
357
+ });
358
+ adapter.db = db;
359
+ const agents = await adapter.detectAgents();
360
+ expect(agents[0].status).toBe(AgentStatus.WAITING);
361
+ });
362
+ it('returns running agent when last role is user (no assistant message yet)', async ()=>{
363
+ const now = Date.now();
364
+ const procs = [
365
+ {
366
+ pid: 300,
367
+ command: 'opencode',
368
+ cwd: '/work',
369
+ tty: 'ttys003'
370
+ }
371
+ ];
372
+ mockedListAgentProcesses.mockReturnValue(procs);
373
+ mockedEnrichProcesses.mockReturnValue(procs);
374
+ const db = makeDb({
375
+ session: [
376
+ {
377
+ id: 'sess-002',
378
+ directory: '/work',
379
+ time_created: now - 30000
380
+ }
381
+ ],
382
+ lastMessage: {
383
+ role: 'user',
384
+ timeUpdated: now - 30_000
385
+ },
386
+ lastAssistant: null,
387
+ firstUserText: {
388
+ text: 'Add unit tests'
389
+ }
390
+ });
391
+ adapter.db = db;
392
+ const agents = await adapter.detectAgents();
393
+ expect(agents[0]).toMatchObject({
394
+ status: AgentStatus.RUNNING,
395
+ sessionId: 'sess-002'
396
+ });
397
+ });
398
+ it('returns idle agent when last activity exceeds threshold', async ()=>{
399
+ const now = Date.now();
400
+ const staleTime = now - 10 * 60 * 1000; // 10 minutes ago
401
+ const procs = [
402
+ {
403
+ pid: 400,
404
+ command: 'opencode',
405
+ cwd: '/old-work',
406
+ tty: 'ttys004'
407
+ }
408
+ ];
409
+ mockedListAgentProcesses.mockReturnValue(procs);
410
+ mockedEnrichProcesses.mockReturnValue(procs);
411
+ const db = makeDb({
412
+ session: [
413
+ {
414
+ id: 'sess-003',
415
+ directory: '/old-work',
416
+ time_created: staleTime
417
+ }
418
+ ],
419
+ lastMessage: {
420
+ role: 'assistant',
421
+ timeUpdated: staleTime
422
+ },
423
+ lastAssistant: {
424
+ completed: staleTime,
425
+ errored: null
426
+ },
427
+ firstUserText: null
428
+ });
429
+ adapter.db = db;
430
+ const agents = await adapter.detectAgents();
431
+ expect(agents[0]).toMatchObject({
432
+ status: AgentStatus.IDLE,
433
+ sessionId: 'sess-003'
434
+ });
435
+ });
436
+ });
437
+ describe('getConversation', ()=>{
438
+ it('returns empty array for invalid session ref', ()=>{
439
+ const messages = adapter.getConversation('/no-separator-here');
440
+ expect(messages).toEqual([]);
441
+ });
442
+ it('returns text messages from session parts', ()=>{
443
+ const db = makeDb({
444
+ parts: [
445
+ {
446
+ role: 'user',
447
+ partData: JSON.stringify({
448
+ type: 'text',
449
+ text: 'Hello agent'
450
+ }),
451
+ timeCreated: 1000
452
+ },
453
+ {
454
+ role: 'assistant',
455
+ partData: JSON.stringify({
456
+ type: 'text',
457
+ text: 'Hi, how can I help?'
458
+ }),
459
+ timeCreated: 2000
460
+ }
461
+ ]
462
+ });
463
+ adapter.db = db;
464
+ const ref = `${dbPath}::sess-abc`;
465
+ const messages = adapter.getConversation(ref);
466
+ expect(messages).toHaveLength(2);
467
+ expect(messages[0]).toEqual({
468
+ role: 'user',
469
+ content: 'Hello agent'
470
+ });
471
+ expect(messages[1]).toEqual({
472
+ role: 'assistant',
473
+ content: 'Hi, how can I help?'
474
+ });
475
+ });
476
+ it('skips reasoning parts when verbose is false', ()=>{
477
+ const db = makeDb({
478
+ parts: [
479
+ {
480
+ role: 'assistant',
481
+ partData: JSON.stringify({
482
+ type: 'reasoning',
483
+ reasoning: 'internal thought'
484
+ }),
485
+ timeCreated: 1000
486
+ },
487
+ {
488
+ role: 'assistant',
489
+ partData: JSON.stringify({
490
+ type: 'text',
491
+ text: 'Answer'
492
+ }),
493
+ timeCreated: 2000
494
+ }
495
+ ]
496
+ });
497
+ adapter.db = db;
498
+ const messages = adapter.getConversation(`${dbPath}::sess-x`);
499
+ expect(messages).toHaveLength(1);
500
+ expect(messages[0].content).toBe('Answer');
501
+ });
502
+ it('includes reasoning and tool parts when verbose is true', ()=>{
503
+ const db = makeDb({
504
+ parts: [
505
+ {
506
+ role: 'assistant',
507
+ partData: JSON.stringify({
508
+ type: 'reasoning',
509
+ reasoning: 'my thinking'
510
+ }),
511
+ timeCreated: 1000
512
+ },
513
+ {
514
+ role: 'assistant',
515
+ partData: JSON.stringify({
516
+ type: 'tool',
517
+ tool: 'read_file'
518
+ }),
519
+ timeCreated: 2000
520
+ }
521
+ ]
522
+ });
523
+ adapter.db = db;
524
+ const messages = adapter.getConversation(`${dbPath}::sess-y`, {
525
+ verbose: true
526
+ });
527
+ expect(messages).toHaveLength(2);
528
+ expect(messages[0].content).toContain('my thinking');
529
+ expect(messages[1].content).toContain('read_file');
530
+ });
531
+ it('returns empty array when DB cannot be opened', ()=>{
532
+ adapter.db = null;
533
+ const messages = adapter.getConversation(`${dbPath}::sess-z`);
534
+ expect(messages).toEqual([]);
535
+ });
536
+ });
537
+ describe('listSessions', ()=>{
538
+ it('returns empty array when DB does not exist', async ()=>{
539
+ const sessions = await adapter.listSessions();
540
+ expect(sessions).toEqual([]);
541
+ });
542
+ it('returns all sessions from DB', async ()=>{
543
+ const now = Date.now();
544
+ const db = makeDb({
545
+ session: [
546
+ {
547
+ id: 'sess-a',
548
+ directory: '/proj-a',
549
+ time_created: now - 3000
550
+ },
551
+ {
552
+ id: 'sess-b',
553
+ directory: '/proj-b',
554
+ time_created: now - 6000
555
+ }
556
+ ],
557
+ lastMessage: null,
558
+ lastAssistant: null,
559
+ firstUserText: {
560
+ text: 'Build the feature'
561
+ }
562
+ });
563
+ adapter.db = db;
564
+ const sessions = await adapter.listSessions();
565
+ expect(sessions).toHaveLength(2);
566
+ expect(sessions[0]).toMatchObject({
567
+ type: 'opencode',
568
+ sessionId: 'sess-a',
569
+ cwd: '/proj-a'
570
+ });
571
+ expect(sessions[1]).toMatchObject({
572
+ type: 'opencode',
573
+ sessionId: 'sess-b',
574
+ cwd: '/proj-b'
575
+ });
576
+ expect(sessions[0].sessionFilePath).toContain('sess-a');
577
+ });
578
+ it('filters sessions by cwd when opts.cwd is set', async ()=>{
579
+ const now = Date.now();
580
+ const db = makeDb({
581
+ session: [
582
+ {
583
+ id: 'sess-a',
584
+ directory: '/proj-a',
585
+ time_created: now - 3000
586
+ },
587
+ {
588
+ id: 'sess-b',
589
+ directory: '/proj-b',
590
+ time_created: now - 6000
591
+ }
592
+ ],
593
+ lastMessage: null,
594
+ lastAssistant: null,
595
+ firstUserText: null
596
+ });
597
+ adapter.db = db;
598
+ const sessions = await adapter.listSessions({
599
+ cwd: '/proj-a'
600
+ });
601
+ expect(sessions).toHaveLength(1);
602
+ expect(sessions[0].sessionId).toBe('sess-a');
603
+ });
604
+ it('uses session time_created as startedAt and lastActive when no parts exist', async ()=>{
605
+ const timeCreated = Date.now() - 120000;
606
+ const db = makeDb({
607
+ session: [
608
+ {
609
+ id: 'sess-c',
610
+ directory: '/repo',
611
+ time_created: timeCreated
612
+ }
613
+ ],
614
+ lastMessage: null,
615
+ lastAssistant: null,
616
+ firstUserText: null
617
+ });
618
+ adapter.db = db;
619
+ const [session] = await adapter.listSessions();
620
+ expect(session.startedAt.getTime()).toBeCloseTo(timeCreated, -2);
621
+ expect(session.lastActive.getTime()).toBeCloseTo(timeCreated, -2);
622
+ });
623
+ });
624
+ });
625
+
626
+ //# sourceMappingURL=OpenCodeAdapter.test.js.map