agent-gear 1.1.4 → 1.1.6

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.
@@ -32,21 +32,25 @@ describe('AcpDetector', () => {
32
32
  });
33
33
 
34
34
  describe('detect', () => {
35
- it('should detect all 5 servers when all available', async () => {
35
+ it('should detect all 7 servers when all available', async () => {
36
36
  mockExecSuccessOnce('opencode v1.2.3\n');
37
37
  mockExecSuccessOnce('copilot 1.0.0\n');
38
38
  mockExecSuccessOnce('claude-agent-acp 0.1.0\n');
39
39
  mockExecSuccessOnce('gemini 2.0.0\n');
40
40
  mockExecSuccessOnce('codex-acp 0.5.0\n');
41
+ mockExecSuccessOnce('mimo 0.1.0\n');
42
+ mockExecSuccessOnce('kimi 1.0.0\n');
41
43
 
42
44
  const result = await detector.detect();
43
45
 
44
- expect(result).toHaveLength(5);
46
+ expect(result).toHaveLength(7);
45
47
  expect(result[0]).toEqual({ type: 'opencode', available: true, version: '1.2.3' });
46
48
  expect(result[1]).toEqual({ type: 'github_copilot', available: true, version: '1.0.0' });
47
49
  expect(result[2]).toEqual({ type: 'claude_code', available: true, version: '0.1.0' });
48
50
  expect(result[3]).toEqual({ type: 'gemini_cli', available: true, version: '2.0.0' });
49
51
  expect(result[4]).toEqual({ type: 'codex', available: true, version: '0.5.0' });
52
+ expect(result[5]).toEqual({ type: 'mimo_code', available: true, version: '0.1.0' });
53
+ expect(result[6]).toEqual({ type: 'kimi_code', available: true, version: '1.0.0' });
50
54
  });
51
55
 
52
56
  it('should detect as unavailable when command fails', async () => {
@@ -55,15 +59,19 @@ describe('AcpDetector', () => {
55
59
  mockExecFailureOnce();
56
60
  mockExecFailureOnce();
57
61
  mockExecFailureOnce();
62
+ mockExecFailureOnce();
63
+ mockExecFailureOnce();
58
64
 
59
65
  const result = await detector.detect();
60
66
 
61
- expect(result).toHaveLength(5);
67
+ expect(result).toHaveLength(7);
62
68
  expect(result[0]).toEqual({ type: 'opencode', available: false });
63
69
  expect(result[1]).toEqual({ type: 'github_copilot', available: false });
64
70
  expect(result[2]).toEqual({ type: 'claude_code', available: false });
65
71
  expect(result[3]).toEqual({ type: 'gemini_cli', available: false });
66
72
  expect(result[4]).toEqual({ type: 'codex', available: false });
73
+ expect(result[5]).toEqual({ type: 'mimo_code', available: false });
74
+ expect(result[6]).toEqual({ type: 'kimi_code', available: false });
67
75
  });
68
76
 
69
77
  it('should detect claude_code only when other servers fail', async () => {
@@ -72,15 +80,19 @@ describe('AcpDetector', () => {
72
80
  mockExecSuccessOnce('claude-agent-acp 0.1.0\n');
73
81
  mockExecFailureOnce();
74
82
  mockExecFailureOnce();
83
+ mockExecFailureOnce();
84
+ mockExecFailureOnce();
75
85
 
76
86
  const result = await detector.detect();
77
87
 
78
- expect(result).toHaveLength(5);
88
+ expect(result).toHaveLength(7);
79
89
  expect(result[0]).toEqual({ type: 'opencode', available: false });
80
90
  expect(result[1]).toEqual({ type: 'github_copilot', available: false });
81
91
  expect(result[2]).toEqual({ type: 'claude_code', available: true, version: '0.1.0' });
82
92
  expect(result[3]).toEqual({ type: 'gemini_cli', available: false });
83
93
  expect(result[4]).toEqual({ type: 'codex', available: false });
94
+ expect(result[5]).toEqual({ type: 'mimo_code', available: false });
95
+ expect(result[6]).toEqual({ type: 'kimi_code', available: false });
84
96
  });
85
97
 
86
98
  it('should detect gemini_cli only when other servers fail', async () => {
@@ -89,10 +101,12 @@ describe('AcpDetector', () => {
89
101
  mockExecFailureOnce();
90
102
  mockExecSuccessOnce('gemini 2.0.0\n');
91
103
  mockExecFailureOnce();
104
+ mockExecFailureOnce();
105
+ mockExecFailureOnce();
92
106
 
93
107
  const result = await detector.detect();
94
108
 
95
- expect(result).toHaveLength(5);
109
+ expect(result).toHaveLength(7);
96
110
  expect(result[3]).toEqual({ type: 'gemini_cli', available: true, version: '2.0.0' });
97
111
  });
98
112
 
@@ -102,19 +116,53 @@ describe('AcpDetector', () => {
102
116
  mockExecFailureOnce();
103
117
  mockExecFailureOnce();
104
118
  mockExecSuccessOnce('codex-acp 0.5.0\n');
119
+ mockExecFailureOnce();
120
+ mockExecFailureOnce();
105
121
 
106
122
  const result = await detector.detect();
107
123
 
108
- expect(result).toHaveLength(5);
124
+ expect(result).toHaveLength(7);
109
125
  expect(result[4]).toEqual({ type: 'codex', available: true, version: '0.5.0' });
110
126
  });
111
127
 
128
+ it('should detect mimo_code only when other servers fail', async () => {
129
+ mockExecFailureOnce();
130
+ mockExecFailureOnce();
131
+ mockExecFailureOnce();
132
+ mockExecFailureOnce();
133
+ mockExecFailureOnce();
134
+ mockExecSuccessOnce('mimo 0.1.0\n');
135
+ mockExecFailureOnce();
136
+
137
+ const result = await detector.detect();
138
+
139
+ expect(result).toHaveLength(7);
140
+ expect(result[5]).toEqual({ type: 'mimo_code', available: true, version: '0.1.0' });
141
+ });
142
+
143
+ it('should detect kimi_code only when other servers fail', async () => {
144
+ mockExecFailureOnce();
145
+ mockExecFailureOnce();
146
+ mockExecFailureOnce();
147
+ mockExecFailureOnce();
148
+ mockExecFailureOnce();
149
+ mockExecFailureOnce();
150
+ mockExecSuccessOnce('kimi 2.0.0\n');
151
+
152
+ const result = await detector.detect();
153
+
154
+ expect(result).toHaveLength(7);
155
+ expect(result[6]).toEqual({ type: 'kimi_code', available: true, version: '2.0.0' });
156
+ });
157
+
112
158
  it('should parse version with different formats', async () => {
113
159
  mockExecSuccessOnce('OpenCode version 2.0.1\n');
114
160
  mockExecSuccessOnce('copilot 3.0.0\n');
115
161
  mockExecSuccessOnce('v0.1.0\n');
116
162
  mockExecSuccessOnce('Gemini 2.0.0\n');
117
163
  mockExecSuccessOnce('codex-acp version 0.5.0\n');
164
+ mockExecSuccessOnce('MiMo CLI v1.0.0\n');
165
+ mockExecSuccessOnce('kimi 2.0.0\n');
118
166
 
119
167
  const result = await detector.detect();
120
168
 
@@ -123,6 +171,8 @@ describe('AcpDetector', () => {
123
171
  expect(result[2].version).toBe('0.1.0');
124
172
  expect(result[3].version).toBe('2.0.0');
125
173
  expect(result[4].version).toBe('0.5.0');
174
+ expect(result[5].version).toBe('1.0.0');
175
+ expect(result[6].version).toBe('2.0.0');
126
176
  });
127
177
  });
128
178
 
@@ -152,6 +202,16 @@ describe('AcpDetector', () => {
152
202
  expect(config).toEqual({ command: 'codex-acp', args: [] });
153
203
  });
154
204
 
205
+ it('should return mimo_code spawner config', () => {
206
+ const config = detector.getSpawnerConfig('mimo_code');
207
+ expect(config).toEqual({ command: 'mimo', args: ['acp'] });
208
+ });
209
+
210
+ it('should return kimi_code spawner config', () => {
211
+ const config = detector.getSpawnerConfig('kimi_code');
212
+ expect(config).toEqual({ command: 'kimi', args: ['acp'] });
213
+ });
214
+
155
215
  it('should return opencode as default for unknown serverType', () => {
156
216
  const config = detector.getSpawnerConfig('unknown_server');
157
217
  expect(config).toEqual({ command: 'opencode', args: ['acp'] });
@@ -4,8 +4,9 @@ function buildMockConn() {
4
4
  return {
5
5
  initialize: vi.fn<[unknown], Promise<{ protocolVersion: number }>>().mockResolvedValue({ protocolVersion: 1 }),
6
6
  newSession: vi.fn<[unknown], Promise<{ sessionId: string }>>().mockResolvedValue({ sessionId: 'sync-sess-1' }),
7
- listSessions: vi.fn<[unknown], Promise<{ sessions: Array<{ sessionId: string; title: string | null; cwd: string; updatedAt: string | null }> }>>()
7
+ listSessions: vi.fn<[unknown], Promise<{ sessions: Array<{ sessionId: string; title: string | null; cwd: string; updatedAt: string | null }>; nextCursor?: string | null }>>()
8
8
  .mockResolvedValue({ sessions: [{ sessionId: 'hist-1', title: 'Session 1', cwd: '/home/user', updatedAt: '2026-01-01T00:00:00Z' }] }),
9
+ deleteSession: vi.fn<[unknown], Promise<object>>().mockResolvedValue({}),
9
10
  loadSession: vi.fn<[unknown], Promise<object>>().mockResolvedValue({}),
10
11
  cancel: vi.fn<[unknown], Promise<void>>().mockResolvedValue(undefined),
11
12
  closeSession: vi.fn<[unknown], Promise<object>>().mockResolvedValue({}),
@@ -128,6 +129,43 @@ describe('AcpSyncBridge', () => {
128
129
 
129
130
  expect(result.sessions).toEqual([]);
130
131
  });
132
+
133
+ it('should throw when not connected', async () => {
134
+ const bridge = createBridge(new MockAcpSpawner());
135
+ await expect(bridge.listSessions('/home/user')).rejects.toThrow('ACP connection not established');
136
+ });
137
+
138
+ it('should pass cursor to ACP and return nextCursor', async () => {
139
+ mc().listSessions.mockResolvedValue({
140
+ sessions: [{ sessionId: 'hist-2', title: 'Session 2', cwd: '/home/user', updatedAt: null }],
141
+ nextCursor: 'cursor-2',
142
+ });
143
+ const mockSpawner = new MockAcpSpawner();
144
+ const bridge = createBridge(mockSpawner);
145
+ await bridge.connect();
146
+
147
+ const result = await bridge.listSessions('/home/user', 'cursor-1');
148
+
149
+ expect(mc().listSessions).toHaveBeenCalledWith({ cwd: '/home/user', cursor: 'cursor-1' });
150
+ expect(result.nextCursor).toBe('cursor-2');
151
+ });
152
+ });
153
+
154
+ describe('deleteSession', () => {
155
+ it('should call ACP deleteSession with the given session ID', async () => {
156
+ const mockSpawner = new MockAcpSpawner();
157
+ const bridge = createBridge(mockSpawner);
158
+ await bridge.connect();
159
+
160
+ await bridge.deleteSession('hist-1');
161
+
162
+ expect(mc().deleteSession).toHaveBeenCalledWith({ sessionId: 'hist-1' });
163
+ });
164
+
165
+ it('should throw when not connected', async () => {
166
+ const bridge = createBridge(new MockAcpSpawner());
167
+ await expect(bridge.deleteSession('hist-1')).rejects.toThrow();
168
+ });
131
169
  });
132
170
 
133
171
  describe('loadSession', () => {
@@ -9,6 +9,25 @@ const { mockStart, mockStop, MockHeartbeat } = vi.hoisted(() => {
9
9
  return { mockStart: start, mockStop: stop, MockHeartbeat: HB };
10
10
  });
11
11
 
12
+ const { MockAcpSyncBridge, mockBridge } = vi.hoisted(() => {
13
+ const bridge = {
14
+ connect: vi.fn().mockResolvedValue(undefined),
15
+ disconnect: vi.fn().mockResolvedValue(undefined),
16
+ listSessions: vi.fn(),
17
+ deleteSession: vi.fn(),
18
+ supportsDelete: vi.fn(),
19
+ start: vi.fn(),
20
+ stop: vi.fn(),
21
+ resume: vi.fn(),
22
+ };
23
+ const Cls = vi.fn(function () { return bridge; });
24
+ return { MockAcpSyncBridge: Cls, mockBridge: bridge };
25
+ });
26
+
27
+ vi.mock('../src/acp/sync-bridge.js', () => ({
28
+ AcpSyncBridge: MockAcpSyncBridge,
29
+ }));
30
+
12
31
  vi.mock('socket.io-client', () => ({
13
32
  io: vi.fn(),
14
33
  }));
@@ -316,4 +335,196 @@ describe('SocketIO Client', () => {
316
335
  });
317
336
  });
318
337
  });
338
+
339
+ describe('acp:list-sessions', () => {
340
+ function setup() {
341
+ mockBridge.connect.mockClear();
342
+ mockBridge.disconnect.mockClear();
343
+ mockBridge.listSessions.mockClear();
344
+ const mockSocket = createMockSocket();
345
+ vi.mocked(io).mockReturnValue(mockSocket as never);
346
+ connectToServer('http://localhost:4001', 'zag_k_1_abc123');
347
+ mockSocket._trigger('connect');
348
+ mockSocket._trigger('registered', { controllerId: 'ctrl-1' });
349
+ return mockSocket;
350
+ }
351
+
352
+ it('should pass cursor and emit nextCursor in result', async () => {
353
+ mockBridge.listSessions.mockResolvedValue({
354
+ sessions: [{ sessionId: 'hist-2', title: 'Session 2', cwd: '/home/user', updatedAt: null }],
355
+ nextCursor: 'cursor-2',
356
+ });
357
+ const mockSocket = setup();
358
+
359
+ mockSocket._trigger('acp:list-sessions', {
360
+ requestId: 'req-1',
361
+ serverType: 'opencode',
362
+ cwd: '/home/user',
363
+ cursor: 'cursor-1',
364
+ });
365
+
366
+ await vi.waitFor(() => {
367
+ expect(mockBridge.listSessions).toHaveBeenCalledWith('/home/user', 'cursor-1');
368
+ expect(mockSocket.emit).toHaveBeenCalledWith('acp:list-sessions:result', {
369
+ requestId: 'req-1',
370
+ success: true,
371
+ sessions: [{ sessionId: 'hist-2', title: 'Session 2', cwd: '/home/user', updatedAt: null }],
372
+ nextCursor: 'cursor-2',
373
+ });
374
+ });
375
+ });
376
+
377
+ it('should omit nextCursor when absent', async () => {
378
+ mockBridge.listSessions.mockResolvedValue({
379
+ sessions: [{ sessionId: 'hist-1', title: 'Session 1', cwd: '/home/user', updatedAt: null }],
380
+ });
381
+ const mockSocket = setup();
382
+
383
+ mockSocket._trigger('acp:list-sessions', {
384
+ requestId: 'req-2',
385
+ serverType: 'opencode',
386
+ cwd: '/home/user',
387
+ });
388
+
389
+ await vi.waitFor(() => {
390
+ expect(mockBridge.listSessions).toHaveBeenCalledWith('/home/user', undefined);
391
+ expect(mockSocket.emit).toHaveBeenCalledWith('acp:list-sessions:result', {
392
+ requestId: 'req-2',
393
+ success: true,
394
+ sessions: [{ sessionId: 'hist-1', title: 'Session 1', cwd: '/home/user', updatedAt: null }],
395
+ });
396
+ });
397
+ });
398
+
399
+ it('should emit failure result on error', async () => {
400
+ mockBridge.listSessions.mockRejectedValue(new Error('boom'));
401
+ const mockSocket = setup();
402
+
403
+ mockSocket._trigger('acp:list-sessions', {
404
+ requestId: 'req-3',
405
+ serverType: 'opencode',
406
+ cwd: '/home/user',
407
+ });
408
+
409
+ await vi.waitFor(() => {
410
+ expect(mockSocket.emit).toHaveBeenCalledWith('acp:list-sessions:result', {
411
+ requestId: 'req-3',
412
+ success: false,
413
+ error: 'boom',
414
+ });
415
+ });
416
+ });
417
+ });
418
+
419
+ describe('acp:set-config-option', () => {
420
+ it('should emit failure result when bridge not found', async () => {
421
+ const mockSocket = createMockSocket();
422
+ vi.mocked(io).mockReturnValue(mockSocket as never);
423
+ connectToServer('http://localhost:4001', 'zag_k_1_abc123');
424
+ mockSocket._trigger('connect');
425
+ mockSocket._trigger('registered', { controllerId: 'ctrl-1' });
426
+
427
+ mockSocket._trigger('acp:set-config-option', {
428
+ sessionId: 'unknown-sess',
429
+ configId: 'opt-1',
430
+ value: 'val-1',
431
+ });
432
+
433
+ await vi.waitFor(() => {
434
+ expect(mockSocket.emit).toHaveBeenCalledWith('acp:set-config-option:result', {
435
+ sessionId: 'unknown-sess',
436
+ success: false,
437
+ configId: 'opt-1',
438
+ error: 'ACP bridge not found for session',
439
+ });
440
+ });
441
+ });
442
+ });
443
+
444
+ describe('acp:delete-history-session', () => {
445
+ function setup() {
446
+ mockBridge.connect.mockClear();
447
+ mockBridge.disconnect.mockClear();
448
+ mockBridge.deleteSession.mockClear();
449
+ mockBridge.supportsDelete.mockClear();
450
+ const mockSocket = createMockSocket();
451
+ vi.mocked(io).mockReturnValue(mockSocket as never);
452
+ connectToServer('http://localhost:4001', 'zag_k_1_abc123');
453
+ mockSocket._trigger('connect');
454
+ mockSocket._trigger('registered', { controllerId: 'ctrl-1' });
455
+ return mockSocket;
456
+ }
457
+
458
+ it('should delete session via sync bridge when supported', async () => {
459
+ mockBridge.supportsDelete.mockReturnValue(true);
460
+ mockBridge.deleteSession.mockResolvedValue(undefined);
461
+ const mockSocket = setup();
462
+
463
+ mockSocket._trigger('acp:delete-history-session', {
464
+ requestId: 'req-1',
465
+ sessionId: 'db-id-1',
466
+ externalSessionId: 'ext-1',
467
+ cwd: '/home/user',
468
+ serverType: 'opencode',
469
+ });
470
+
471
+ await vi.waitFor(() => {
472
+ expect(mockBridge.connect).toHaveBeenCalled();
473
+ expect(mockBridge.supportsDelete).toHaveBeenCalled();
474
+ expect(mockBridge.deleteSession).toHaveBeenCalledWith('ext-1');
475
+ expect(mockSocket.emit).toHaveBeenCalledWith('acp:delete-history-session:result', {
476
+ requestId: 'req-1',
477
+ sessionId: 'db-id-1',
478
+ success: true,
479
+ supported: true,
480
+ });
481
+ });
482
+ });
483
+
484
+ it('should report unsupported without deleting when agent lacks capability', async () => {
485
+ mockBridge.supportsDelete.mockReturnValue(false);
486
+ const mockSocket = setup();
487
+
488
+ mockSocket._trigger('acp:delete-history-session', {
489
+ requestId: 'req-2',
490
+ sessionId: 'db-id-2',
491
+ externalSessionId: 'ext-2',
492
+ cwd: '/home/user',
493
+ });
494
+
495
+ await vi.waitFor(() => {
496
+ expect(mockBridge.deleteSession).not.toHaveBeenCalled();
497
+ expect(mockSocket.emit).toHaveBeenCalledWith('acp:delete-history-session:result', {
498
+ requestId: 'req-2',
499
+ sessionId: 'db-id-2',
500
+ success: false,
501
+ supported: false,
502
+ error: 'Session delete not supported by agent',
503
+ });
504
+ });
505
+ });
506
+
507
+ it('should emit failure result on error', async () => {
508
+ mockBridge.supportsDelete.mockReturnValue(true);
509
+ mockBridge.deleteSession.mockRejectedValue(new Error('boom'));
510
+ const mockSocket = setup();
511
+
512
+ mockSocket._trigger('acp:delete-history-session', {
513
+ requestId: 'req-3',
514
+ sessionId: 'db-id-3',
515
+ externalSessionId: 'ext-3',
516
+ cwd: '/home/user',
517
+ });
518
+
519
+ await vi.waitFor(() => {
520
+ expect(mockSocket.emit).toHaveBeenCalledWith('acp:delete-history-session:result', {
521
+ requestId: 'req-3',
522
+ sessionId: 'db-id-3',
523
+ success: false,
524
+ supported: true,
525
+ error: 'boom',
526
+ });
527
+ });
528
+ });
529
+ });
319
530
  });