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.
@@ -48,11 +48,12 @@ interface AcpStartPayload {
48
48
  serverType: string;
49
49
  cwd: string;
50
50
  mcpServers?: Array<{ name: string; command: string; args?: string[]; env?: Array<{ name: string; value: string }> }>;
51
+ additionalDirectories?: string[];
51
52
  }
52
53
 
53
54
  interface AcpMessagePayload {
54
55
  sessionId: string;
55
- message: { type: string; content: string };
56
+ message: { type: string; content: string; attachments?: Array<Record<string, unknown>> };
56
57
  }
57
58
 
58
59
  interface AcpCancelPayload {
@@ -63,6 +64,29 @@ interface AcpStopPayload {
63
64
  sessionId: string;
64
65
  }
65
66
 
67
+ interface AcpSetConfigOptionPayload {
68
+ sessionId: string;
69
+ configId: string;
70
+ value: string | boolean;
71
+ }
72
+
73
+ interface AcpDeleteHistorySessionPayload {
74
+ requestId: string;
75
+ sessionId: string;
76
+ externalSessionId: string;
77
+ cwd: string;
78
+ serverType?: string;
79
+ }
80
+
81
+ interface AcpAuthenticatePayload {
82
+ sessionId: string;
83
+ methodId: string;
84
+ }
85
+
86
+ interface AcpLogoutPayload {
87
+ sessionId: string;
88
+ }
89
+
66
90
  interface AcpSetModePayload {
67
91
  sessionId: string;
68
92
  modeId: string;
@@ -79,12 +103,14 @@ interface AcpResumePayload {
79
103
  externalSessionId: string;
80
104
  serverType: string;
81
105
  cwd: string;
106
+ additionalDirectories?: string[];
82
107
  }
83
108
 
84
109
  interface AcpListSessionsPayload {
85
110
  requestId: string;
86
111
  serverType: string;
87
112
  cwd: string;
113
+ cursor?: string | null;
88
114
  }
89
115
 
90
116
  interface AcpLoadSessionPayload {
@@ -226,6 +252,7 @@ export function connectToServer(
226
252
  sessionId: data.sessionId,
227
253
  cwd: data.cwd,
228
254
  mcpServers: data.mcpServers,
255
+ additionalDirectories: data.additionalDirectories,
229
256
  }, spawner);
230
257
 
231
258
  acpBridges.set(data.sessionId, bridge);
@@ -250,6 +277,13 @@ export function connectToServer(
250
277
  if (payload.currentModel) result.currentModel = payload.currentModel;
251
278
  result.availableModels = payload.availableModels ?? [];
252
279
  if (payload.currentModelId) result.currentModelId = payload.currentModelId;
280
+ result.configOptions = payload.configOptions ?? [];
281
+ if (payload.promptCapabilities) result.promptCapabilities = payload.promptCapabilities;
282
+ if (payload.agentInfo) result.agentInfo = payload.agentInfo;
283
+ if (payload.authMethods) result.authMethods = payload.authMethods;
284
+ if (payload.supportsAdditionalDirectories != null) {
285
+ result.supportsAdditionalDirectories = payload.supportsAdditionalDirectories;
286
+ }
253
287
  socket.emit('acp:start:result', result);
254
288
  };
255
289
 
@@ -291,6 +325,7 @@ export function connectToServer(
291
325
  const bridge = new AcpSessionBridge(socket, {
292
326
  sessionId: data.sessionId,
293
327
  cwd: data.cwd,
328
+ additionalDirectories: data.additionalDirectories,
294
329
  }, spawner);
295
330
 
296
331
  acpBridges.set(data.sessionId, bridge);
@@ -313,6 +348,10 @@ export function connectToServer(
313
348
  if (payload.currentModel) result.currentModel = payload.currentModel;
314
349
  result.availableModels = payload.availableModels ?? [];
315
350
  if (payload.currentModelId) result.currentModelId = payload.currentModelId;
351
+ result.configOptions = payload.configOptions ?? [];
352
+ if (payload.promptCapabilities) result.promptCapabilities = payload.promptCapabilities;
353
+ if (payload.agentInfo) result.agentInfo = payload.agentInfo;
354
+ if (payload.authMethods) result.authMethods = payload.authMethods;
316
355
  socket.emit('acp:resume:result', result);
317
356
  };
318
357
 
@@ -351,7 +390,7 @@ export function connectToServer(
351
390
  const bridge = new AcpSyncBridge({ cwd: data.cwd }, spawner);
352
391
  try {
353
392
  await bridge.connect();
354
- const result = await bridge.listSessions(data.cwd);
393
+ const result = await bridge.listSessions(data.cwd, data.cursor);
355
394
  await bridge.disconnect();
356
395
  socket.emit('acp:list-sessions:result', {
357
396
  requestId: data.requestId,
@@ -362,6 +401,7 @@ export function connectToServer(
362
401
  cwd: s.cwd,
363
402
  updatedAt: s.updatedAt ?? null,
364
403
  })),
404
+ ...(result.nextCursor ? { nextCursor: result.nextCursor } : {}),
365
405
  });
366
406
  } catch (error) {
367
407
  await bridge.disconnect().catch(() => {});
@@ -509,6 +549,118 @@ export function connectToServer(
509
549
  }
510
550
  });
511
551
 
552
+ socket.on('acp:set-config-option', (data: AcpSetConfigOptionPayload) => {
553
+ console.log(`[ACP] acp:set-config-option received: sessionId=${data.sessionId} configId=${data.configId}`);
554
+ const bridge = acpBridges.get(data.sessionId);
555
+ if (!bridge) {
556
+ socket.emit('acp:set-config-option:result', {
557
+ sessionId: data.sessionId,
558
+ success: false,
559
+ configId: data.configId,
560
+ error: 'ACP bridge not found for session',
561
+ });
562
+ return;
563
+ }
564
+ // 成功时 bridge 内部已用响应快照更新 configOptions 并 emit acp:config-options
565
+ bridge.setConfigOption(data.configId, data.value).then(
566
+ () => {
567
+ socket.emit('acp:set-config-option:result', {
568
+ sessionId: data.sessionId,
569
+ success: true,
570
+ configId: data.configId,
571
+ });
572
+ },
573
+ (err) => {
574
+ console.error(`[ACP] setConfigOption failed: ${(err as Error).message}`);
575
+ socket.emit('acp:set-config-option:result', {
576
+ sessionId: data.sessionId,
577
+ success: false,
578
+ configId: data.configId,
579
+ error: (err as Error).message,
580
+ });
581
+ },
582
+ );
583
+ });
584
+
585
+ socket.on('acp:delete-history-session', async (data: AcpDeleteHistorySessionPayload) => {
586
+ const serverType = data.serverType ?? 'opencode';
587
+ console.log(`[ACP sync] acp:delete-history-session received: sessionId=${data.externalSessionId} cwd=${data.cwd}`);
588
+ const spawner = createSpawner(serverType, `sync_${Date.now()}`, data.cwd);
589
+ const bridge = new AcpSyncBridge({ cwd: data.cwd }, spawner);
590
+ try {
591
+ await bridge.connect();
592
+ if (!bridge.supportsDelete()) {
593
+ await bridge.disconnect().catch(() => {});
594
+ socket.emit('acp:delete-history-session:result', {
595
+ requestId: data.requestId,
596
+ sessionId: data.sessionId,
597
+ success: false,
598
+ supported: false,
599
+ error: 'Session delete not supported by agent',
600
+ });
601
+ return;
602
+ }
603
+ await bridge.deleteSession(data.externalSessionId);
604
+ await bridge.disconnect();
605
+ socket.emit('acp:delete-history-session:result', {
606
+ requestId: data.requestId,
607
+ sessionId: data.sessionId,
608
+ success: true,
609
+ supported: true,
610
+ });
611
+ } catch (error) {
612
+ await bridge.disconnect().catch(() => {});
613
+ console.error(`[ACP sync] delete-history-session failed: ${(error as Error).message}`);
614
+ socket.emit('acp:delete-history-session:result', {
615
+ requestId: data.requestId,
616
+ sessionId: data.sessionId,
617
+ success: false,
618
+ supported: true,
619
+ error: (error as Error).message,
620
+ });
621
+ }
622
+ });
623
+
624
+ socket.on('acp:authenticate', async (data: AcpAuthenticatePayload) => {
625
+ console.log(`[ACP] acp:authenticate received: sessionId=${data.sessionId} methodId=${data.methodId}`);
626
+ const bridge = acpBridges.get(data.sessionId);
627
+ try {
628
+ if (!bridge) throw new Error('ACP bridge not found for session');
629
+ await bridge.authenticate(data.methodId);
630
+ socket.emit('acp:authenticate:result', {
631
+ sessionId: data.sessionId,
632
+ success: true,
633
+ });
634
+ } catch (error) {
635
+ console.error(`[ACP] authenticate failed: ${(error as Error).message}`);
636
+ socket.emit('acp:authenticate:result', {
637
+ sessionId: data.sessionId,
638
+ success: false,
639
+ error: (error as Error).message,
640
+ });
641
+ }
642
+ });
643
+
644
+ socket.on('acp:logout', async (data: AcpLogoutPayload) => {
645
+ console.log(`[ACP] acp:logout received: sessionId=${data.sessionId}`);
646
+ const bridge = acpBridges.get(data.sessionId);
647
+ try {
648
+ if (!bridge) throw new Error('ACP bridge not found for session');
649
+ await bridge.logout();
650
+ socket.emit('acp:logout:result', {
651
+ sessionId: data.sessionId,
652
+ success: true,
653
+ });
654
+ } catch (error) {
655
+ console.error(`[ACP] logout failed: ${(error as Error).message}`);
656
+ socket.emit('acp:logout:result', {
657
+ sessionId: data.sessionId,
658
+ success: false,
659
+ error: (error as Error).message,
660
+ });
661
+ }
662
+ });
663
+
512
664
  // File system events
513
665
  socket.on('fs:browse', async (data: FsBrowsePayload) => {
514
666
  console.log(`FS browse requested: requestId=${data.requestId} path=${data.path}`);
@@ -9,7 +9,9 @@ function buildMockConn() {
9
9
  cancel: vi.fn<[unknown], Promise<void>>().mockResolvedValue(undefined),
10
10
  closeSession: vi.fn<[unknown], Promise<object>>().mockResolvedValue({}),
11
11
  setSessionMode: vi.fn<[unknown], Promise<object>>().mockResolvedValue({}),
12
- unstable_setSessionModel: vi.fn<[unknown], Promise<object>>().mockResolvedValue({}),
12
+ setSessionConfigOption: vi.fn<[unknown], Promise<object>>().mockResolvedValue({}),
13
+ authenticate: vi.fn<[unknown], Promise<object>>().mockResolvedValue({}),
14
+ logout: vi.fn<[unknown], Promise<object>>().mockResolvedValue({}),
13
15
  };
14
16
  }
15
17
 
@@ -20,7 +22,7 @@ vi.mock('@agentclientprotocol/sdk', () => ({
20
22
  ClientSideConnection: vi.fn(function () { return mockConnInstance; }),
21
23
  }));
22
24
 
23
- import { AcpSessionBridge } from '../../src/acp/bridge.js';
25
+ import { AcpSessionBridge, extractConfigOptions } from '../../src/acp/bridge.js';
24
26
 
25
27
  class MockAcpSpawner {
26
28
  start = vi.fn();
@@ -74,8 +76,12 @@ describe('AcpSessionBridge', () => {
74
76
  expect(mockSpawner.start).toHaveBeenCalledOnce();
75
77
  expect(mc().initialize).toHaveBeenCalledWith({
76
78
  protocolVersion: 1,
77
- clientCapabilities: {},
78
- clientInfo: { name: 'agent-gear', version: '1.0.0' },
79
+ clientCapabilities: {
80
+ session: {
81
+ configOptions: { boolean: {} },
82
+ },
83
+ },
84
+ clientInfo: { name: 'agent-gear', version: '1.1.5' },
79
85
  });
80
86
  expect(mc().newSession).toHaveBeenCalledWith({
81
87
  cwd: '/home/user',
@@ -121,10 +127,16 @@ describe('AcpSessionBridge', () => {
121
127
  it('should include currentModel in acp:initialized when available', async () => {
122
128
  mockConnInstance.newSession = vi.fn().mockResolvedValue({
123
129
  sessionId: 'acp-sess-abc',
124
- models: {
125
- availableModels: [{ modelId: 'gpt-4', name: 'GPT-4' }],
126
- currentModelId: 'gpt-4',
127
- },
130
+ configOptions: [
131
+ {
132
+ id: 'model',
133
+ name: 'Model',
134
+ category: 'model',
135
+ type: 'select',
136
+ currentValue: 'gpt-4',
137
+ options: [{ value: 'gpt-4', name: 'GPT-4' }],
138
+ },
139
+ ],
128
140
  });
129
141
  const bridge = createBridge();
130
142
  const emitSpy = vi.fn();
@@ -141,13 +153,19 @@ describe('AcpSessionBridge', () => {
141
153
  it('should include availableModels and currentModelId in acp:initialized', async () => {
142
154
  mockConnInstance.newSession = vi.fn().mockResolvedValue({
143
155
  sessionId: 'acp-sess-abc',
144
- models: {
145
- availableModels: [
146
- { modelId: 'gpt-4', name: 'GPT-4' },
147
- { modelId: 'claude-3', name: 'Claude 3' },
148
- ],
149
- currentModelId: 'claude-3',
150
- },
156
+ configOptions: [
157
+ {
158
+ id: 'model',
159
+ name: 'Model',
160
+ category: 'model',
161
+ type: 'select',
162
+ currentValue: 'claude-3',
163
+ options: [
164
+ { value: 'gpt-4', name: 'GPT-4' },
165
+ { value: 'claude-3', name: 'Claude 3' },
166
+ ],
167
+ },
168
+ ],
151
169
  });
152
170
  const bridge = createBridge();
153
171
  const emitSpy = vi.fn();
@@ -165,6 +183,22 @@ describe('AcpSessionBridge', () => {
165
183
  }));
166
184
  });
167
185
 
186
+ it('should map auth method without type field to kind agent', async () => {
187
+ mc().initialize.mockResolvedValue({
188
+ protocolVersion: 1,
189
+ authMethods: [{ id: 'oauth', description: 'Login with agent' }],
190
+ });
191
+ const bridge = createBridge();
192
+
193
+ bridge.start();
194
+ await vi.advanceTimersByTimeAsync(0);
195
+
196
+ expect(mockSocket.emit).toHaveBeenCalledWith('acp:auth:required', {
197
+ sessionId: 'sess-1',
198
+ authMethods: [{ id: 'oauth', kind: 'agent', description: null, url: null }],
199
+ });
200
+ });
201
+
168
202
  it('should emit acp:stopped on initialize failure', async () => {
169
203
  mc().initialize.mockRejectedValue(new Error('init failed'));
170
204
  const bridge = createBridge();
@@ -511,51 +545,236 @@ describe('AcpSessionBridge', () => {
511
545
  await expect(bridge.setMode('mode-1')).resolves.toBeUndefined();
512
546
  expect(mc().setSessionMode).not.toHaveBeenCalled();
513
547
  });
548
+
549
+ it('should use setSessionConfigOption when mode config option exists', async () => {
550
+ mockConnInstance.newSession = vi.fn().mockResolvedValue({
551
+ sessionId: 'acp-sess-abc',
552
+ configOptions: [
553
+ {
554
+ id: 'mode',
555
+ name: 'Mode',
556
+ category: 'mode',
557
+ type: 'select',
558
+ currentValue: 'code',
559
+ options: [{ value: 'code', name: 'Code' }, { value: 'plan', name: 'Plan' }],
560
+ },
561
+ ],
562
+ });
563
+ const bridge = createBridge();
564
+ bridge.start();
565
+ await vi.advanceTimersByTimeAsync(0);
566
+
567
+ await bridge.setMode('plan');
568
+
569
+ expect(mc().setSessionConfigOption).toHaveBeenCalledWith({
570
+ sessionId: 'acp-sess-abc',
571
+ configId: 'mode',
572
+ value: 'plan',
573
+ });
574
+ });
514
575
  });
515
576
 
516
577
  describe('setModel', () => {
517
- it('should call connection.unstable_setSessionModel with correct params', async () => {
578
+ it('should call setSessionConfigOption when model config option exists', async () => {
579
+ mockConnInstance.newSession = vi.fn().mockResolvedValue({
580
+ sessionId: 'acp-sess-abc',
581
+ configOptions: [
582
+ {
583
+ id: 'model',
584
+ name: 'Model',
585
+ category: 'model',
586
+ type: 'select',
587
+ currentValue: 'gpt-4',
588
+ options: [{ value: 'gpt-4', name: 'GPT-4' }, { value: 'claude-3', name: 'Claude 3' }],
589
+ },
590
+ ],
591
+ });
518
592
  const bridge = createBridge();
519
593
  bridge.start();
520
594
  await vi.advanceTimersByTimeAsync(0);
521
595
 
522
- await bridge.setModel('model-2');
596
+ await bridge.setModel('claude-3');
523
597
 
524
- expect(mc().unstable_setSessionModel).toHaveBeenCalledWith({
598
+ expect(mc().setSessionConfigOption).toHaveBeenCalledWith({
525
599
  sessionId: 'acp-sess-abc',
526
- modelId: 'model-2',
600
+ configId: 'model',
601
+ value: 'claude-3',
527
602
  });
528
603
  });
529
604
 
605
+ it('should throw when initialized but no model config option', async () => {
606
+ const bridge = createBridge();
607
+ bridge.start();
608
+ await vi.advanceTimersByTimeAsync(0);
609
+
610
+ await expect(bridge.setModel('model-1')).rejects.toThrow(
611
+ 'Model switching is not supported',
612
+ );
613
+ expect(mc().setSessionConfigOption).not.toHaveBeenCalled();
614
+ });
615
+
530
616
  it('should not throw when not initialized', async () => {
531
617
  const bridge = createBridge();
532
618
 
533
619
  await expect(bridge.setModel('model-1')).resolves.toBeUndefined();
534
- expect(mc().unstable_setSessionModel).not.toHaveBeenCalled();
535
620
  });
536
621
  });
537
622
 
538
- describe('extractAvailableModels', () => {
539
- it('should return empty array when models is null', () => {
623
+ describe('setConfigOption', () => {
624
+ it('should call connection.setSessionConfigOption with boolean value', async () => {
540
625
  const bridge = createBridge();
626
+ bridge.start();
627
+ await vi.advanceTimersByTimeAsync(0);
628
+
629
+ await bridge.setConfigOption('opt-1', true);
541
630
 
542
- // Access private method via bracket notation
543
- const result = (bridge as unknown as Record<string, unknown>).extractAvailableModels(null);
544
- expect(result).toEqual([]);
631
+ expect(mc().setSessionConfigOption).toHaveBeenCalledWith({
632
+ sessionId: 'acp-sess-abc',
633
+ configId: 'opt-1',
634
+ value: true,
635
+ type: 'boolean',
636
+ });
545
637
  });
546
638
 
547
- it('should return empty array when models is undefined', () => {
639
+ it('should call connection.setSessionConfigOption with select value', async () => {
548
640
  const bridge = createBridge();
641
+ bridge.start();
642
+ await vi.advanceTimersByTimeAsync(0);
643
+
644
+ await bridge.setConfigOption('opt-2', 'val-1');
549
645
 
550
- const result = (bridge as unknown as Record<string, unknown>).extractAvailableModels(undefined);
551
- expect(result).toEqual([]);
646
+ expect(mc().setSessionConfigOption).toHaveBeenCalledWith({
647
+ sessionId: 'acp-sess-abc',
648
+ configId: 'opt-2',
649
+ value: 'val-1',
650
+ });
552
651
  });
553
652
 
554
- it('should return empty array when availableModels is missing', () => {
653
+ it('should update snapshot and emit acp:config-options from set_config_option response', async () => {
654
+ mockConnInstance.setSessionConfigOption = vi.fn().mockResolvedValue({
655
+ configOptions: [
656
+ {
657
+ id: 'flags',
658
+ name: 'Flags',
659
+ category: null,
660
+ type: 'boolean',
661
+ currentValue: true,
662
+ },
663
+ ],
664
+ });
555
665
  const bridge = createBridge();
666
+ bridge.start();
667
+ await vi.advanceTimersByTimeAsync(0);
556
668
 
557
- const result = (bridge as unknown as Record<string, unknown>).extractAvailableModels({});
558
- expect(result).toEqual([]);
669
+ await bridge.setConfigOption('flags', 'true');
670
+
671
+ expect(bridge.getConfigOptions()).toEqual([
672
+ {
673
+ id: 'flags',
674
+ name: 'Flags',
675
+ description: null,
676
+ category: null,
677
+ type: 'boolean',
678
+ currentValue: 'true',
679
+ options: [],
680
+ },
681
+ ]);
682
+ expect(mockSocket.emit).toHaveBeenCalledWith('acp:config-options', {
683
+ sessionId: 'sess-1',
684
+ configOptions: bridge.getConfigOptions(),
685
+ });
686
+ });
687
+
688
+ it('should not emit acp:config-options when response has no configOptions', async () => {
689
+ mockConnInstance.setSessionConfigOption = vi.fn().mockResolvedValue({});
690
+ const bridge = createBridge();
691
+ bridge.start();
692
+ await vi.advanceTimersByTimeAsync(0);
693
+
694
+ await bridge.setConfigOption('opt-2', 'val-1');
695
+
696
+ expect(mockSocket.emit).not.toHaveBeenCalledWith('acp:config-options', expect.anything());
697
+ });
698
+ });
699
+
700
+ describe('extractConfigOptions', () => {
701
+ it('should return empty array when configOptions is null or undefined', () => {
702
+ expect(extractConfigOptions(null)).toEqual([]);
703
+ expect(extractConfigOptions(undefined)).toEqual([]);
704
+ });
705
+
706
+ it('should flatten grouped select options with group name prefix', () => {
707
+ const result = extractConfigOptions([
708
+ {
709
+ id: 'model',
710
+ name: 'Model',
711
+ category: 'model',
712
+ type: 'select',
713
+ currentValue: 'gpt-4',
714
+ options: [
715
+ {
716
+ group: 'openai',
717
+ name: 'OpenAI',
718
+ options: [
719
+ { value: 'gpt-4', name: 'GPT-4' },
720
+ { value: 'gpt-4o', name: 'GPT-4o' },
721
+ ],
722
+ },
723
+ {
724
+ group: 'anthropic',
725
+ name: 'Anthropic',
726
+ options: [{ value: 'claude-3', name: 'Claude 3' }],
727
+ },
728
+ ],
729
+ },
730
+ ]);
731
+
732
+ expect(result[0].options).toEqual([
733
+ { value: 'gpt-4', name: 'OpenAI/GPT-4' },
734
+ { value: 'gpt-4o', name: 'OpenAI/GPT-4o' },
735
+ { value: 'claude-3', name: 'Anthropic/Claude 3' },
736
+ ]);
737
+ });
738
+
739
+ it('should map select and boolean config options', () => {
740
+ const result = extractConfigOptions([
741
+ {
742
+ id: 'model',
743
+ name: 'Model',
744
+ category: 'model',
745
+ type: 'select',
746
+ currentValue: 'gpt-4',
747
+ options: [{ value: 'gpt-4', name: 'GPT-4' }],
748
+ },
749
+ {
750
+ id: 'flags',
751
+ name: 'Flags',
752
+ category: null,
753
+ type: 'boolean',
754
+ currentValue: true,
755
+ },
756
+ ]);
757
+
758
+ expect(result).toEqual([
759
+ {
760
+ id: 'model',
761
+ name: 'Model',
762
+ description: null,
763
+ category: 'model',
764
+ type: 'select',
765
+ currentValue: 'gpt-4',
766
+ options: [{ value: 'gpt-4', name: 'GPT-4' }],
767
+ },
768
+ {
769
+ id: 'flags',
770
+ name: 'Flags',
771
+ description: null,
772
+ category: null,
773
+ type: 'boolean',
774
+ currentValue: 'true',
775
+ options: [],
776
+ },
777
+ ]);
559
778
  });
560
779
  });
561
780
  });