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.
package/src/acp/bridge.ts CHANGED
@@ -8,6 +8,7 @@ const ACP_PROTOCOL_VERSION = 1;
8
8
  export interface AcpBridgeOptions {
9
9
  sessionId: string;
10
10
  cwd: string;
11
+ additionalDirectories?: string[];
11
12
  mcpServers?: Array<{ name: string; command: string; args?: string[]; env?: Array<{ name: string; value: string }> }>;
12
13
  }
13
14
 
@@ -16,6 +17,39 @@ interface QueuedInput {
16
17
  content: string;
17
18
  }
18
19
 
20
+ export interface SessionConfigOptionData {
21
+ id: string;
22
+ name: string;
23
+ description?: string | null;
24
+ category?: string | null;
25
+ type: 'select' | 'boolean';
26
+ currentValue: string | null;
27
+ options: Array<{ value: string; name?: string }>;
28
+ }
29
+
30
+ export interface AcpAuthMethodData {
31
+ id: string;
32
+ kind: 'env_var' | 'terminal' | 'agent' | null;
33
+ description?: string | null;
34
+ url?: string | null;
35
+ }
36
+
37
+ export interface AcpInitializedPayload {
38
+ sessionId: string;
39
+ acpSessionId: string | null;
40
+ modes: Array<{ id: string; name: string; description?: string }>;
41
+ currentModeId: string | null;
42
+ currentModel: { id: string; name: string; provider?: string } | null;
43
+ availableModels: Array<{ id: string; name: string; provider?: string }>;
44
+ currentModelId: string | null;
45
+ configOptions: SessionConfigOptionData[];
46
+ promptCapabilities: { image: boolean; audio: boolean; embeddedContext: boolean } | null;
47
+ agentInfo: { name: string; version?: string | null } | null;
48
+ authMethods: AcpAuthMethodData[];
49
+ supportsAdditionalDirectories: boolean;
50
+ protocolVersion?: number | null;
51
+ }
52
+
19
53
  function toMcpServers(servers: AcpBridgeOptions['mcpServers']): acp.McpServer[] {
20
54
  return (servers ?? []).map((s) => ({
21
55
  name: s.name,
@@ -25,17 +59,104 @@ function toMcpServers(servers: AcpBridgeOptions['mcpServers']): acp.McpServer[]
25
59
  }));
26
60
  }
27
61
 
62
+ export function extractConfigOptions(
63
+ configOptions: acp.SessionConfigOption[] | null | undefined,
64
+ ): SessionConfigOptionData[] {
65
+ if (!configOptions) return [];
66
+ return configOptions.map((opt) => {
67
+ const base = {
68
+ id: opt.id,
69
+ name: opt.name,
70
+ description: opt.description ?? null,
71
+ category: opt.category ?? null,
72
+ };
73
+ if (opt.type === 'boolean') {
74
+ return {
75
+ ...base,
76
+ type: 'boolean' as const,
77
+ currentValue: String(opt.currentValue),
78
+ options: [],
79
+ };
80
+ }
81
+ const options = Array.isArray(opt.options)
82
+ ? (opt.options as Array<acp.SessionConfigSelectOption | acp.SessionConfigSelectGroup>).flatMap((o) =>
83
+ 'group' in o
84
+ ? o.options.map((inner) => ({ value: inner.value, name: `${o.name}/${inner.name}` }))
85
+ : [{ value: o.value, name: o.name }],
86
+ )
87
+ : [];
88
+ return {
89
+ ...base,
90
+ type: 'select' as const,
91
+ currentValue: opt.currentValue,
92
+ options,
93
+ };
94
+ });
95
+ }
96
+
97
+ function findConfigByCategory(
98
+ configOptions: SessionConfigOptionData[],
99
+ category: string,
100
+ ): SessionConfigOptionData | null {
101
+ return configOptions.find((o) => o.category === category) ?? null;
102
+ }
103
+
104
+ function extractConfigModes(
105
+ configOptions: SessionConfigOptionData[],
106
+ ): { modes: Array<{ id: string; name: string }>; currentModeId: string | null } | null {
107
+ const opt = findConfigByCategory(configOptions, 'mode');
108
+ if (!opt || opt.type !== 'select') return null;
109
+ return {
110
+ modes: opt.options.map((o) => ({ id: o.value, name: o.name ?? o.value })),
111
+ currentModeId: opt.currentValue,
112
+ };
113
+ }
114
+
115
+ function extractConfigModels(
116
+ configOptions: SessionConfigOptionData[],
117
+ ): {
118
+ availableModels: Array<{ id: string; name: string; provider?: string }>;
119
+ currentModelId: string | null;
120
+ currentModel: { id: string; name: string; provider?: string } | null;
121
+ } | null {
122
+ const opt = findConfigByCategory(configOptions, 'model');
123
+ if (!opt || opt.type !== 'select') return null;
124
+ const availableModels = opt.options.map((o) => ({ id: o.value, name: o.name ?? o.value }));
125
+ const currentModelId = opt.currentValue;
126
+ const current = availableModels.find((m) => m.id === currentModelId) ?? null;
127
+ return { availableModels, currentModelId, currentModel: current };
128
+ }
129
+
130
+ function toAuthMethods(authMethods: acp.AuthMethod[] | undefined): AcpAuthMethodData[] {
131
+ if (!authMethods || authMethods.length === 0) return [];
132
+ return authMethods.map((m) => {
133
+ if ('type' in m) {
134
+ const kind = m.type === 'env_var' ? 'env_var' : m.type === 'terminal' ? 'terminal' : 'agent';
135
+ return {
136
+ id: m.id,
137
+ kind,
138
+ description: (m as { description?: string | null }).description ?? null,
139
+ url: (m as { url?: string | null }).url ?? null,
140
+ };
141
+ }
142
+ // AuthMethodAgent 无 type 字段(SDK 1.3.0),是默认认证方式
143
+ return { id: m.id, kind: 'agent', description: null, url: null };
144
+ });
145
+ }
146
+
28
147
  export class AcpSessionBridge extends EventEmitter implements acp.Client {
29
148
  private spawner: AcpSpawner;
30
149
  private socket: Socket;
31
150
  private sessionId: string;
32
151
  private cwd: string;
152
+ private additionalDirectories: string[];
33
153
  private mcpServers: AcpBridgeOptions['mcpServers'];
34
154
  private connection: acp.ClientSideConnection | null = null;
35
155
  private acpSessionId: string | null = null;
36
156
  private activePrompt: Promise<acp.PromptResponse> | null = null;
37
157
  private inputQueue: QueuedInput[] = [];
38
158
  private textChunkBuffer = '';
159
+ private textChunkMessageId: string | null | undefined;
39
160
  private textChunkTimer: ReturnType<typeof setTimeout> | null = null;
40
161
  private initializing = false;
41
162
  private stopped = false;
@@ -44,11 +165,20 @@ export class AcpSessionBridge extends EventEmitter implements acp.Client {
44
165
  timeout: NodeJS.Timeout;
45
166
  }>();
46
167
 
168
+ // Capability snapshot from initialize / session/new / resume
169
+ private configOptions: SessionConfigOptionData[] = [];
170
+ private promptCapabilities: AcpInitializedPayload['promptCapabilities'] = null;
171
+ private agentInfo: AcpInitializedPayload['agentInfo'] = null;
172
+ private authMethods: AcpAuthMethodData[] = [];
173
+ private supportsAdditionalDirectories = false;
174
+ private agentProtocolVersion: number | null = null;
175
+
47
176
  constructor(socket: Socket, options: AcpBridgeOptions, spawner?: AcpSpawner) {
48
177
  super();
49
178
  this.socket = socket;
50
179
  this.sessionId = options.sessionId;
51
180
  this.cwd = options.cwd;
181
+ this.additionalDirectories = options.additionalDirectories ?? [];
52
182
  this.mcpServers = options.mcpServers;
53
183
  this.spawner = spawner ?? new AcpSpawner({ sessionId: options.sessionId, cwd: options.cwd });
54
184
  }
@@ -63,20 +193,12 @@ export class AcpSessionBridge extends EventEmitter implements acp.Client {
63
193
  const result = await this.connection!.newSession({
64
194
  cwd: this.cwd,
65
195
  mcpServers: toMcpServers(this.mcpServers),
196
+ additionalDirectories: this.additionalDirectories.length > 0 ? this.additionalDirectories : undefined,
66
197
  });
67
198
  this.acpSessionId = result.sessionId;
68
199
  this.initializing = false;
69
200
 
70
- this.emit('acp:initialized', {
71
- sessionId: this.sessionId,
72
- acpSessionId: this.acpSessionId,
73
- modes: this.extractModes(result.modes),
74
- currentModeId: result.modes?.currentModeId ?? null,
75
- currentModel: this.extractCurrentModel(result.models),
76
- availableModels: this.extractAvailableModels(result.models),
77
- currentModelId: result.models?.currentModelId ?? null,
78
- });
79
-
201
+ this.emit('acp:initialized', this.buildInitializedPayload(result));
80
202
  this.flushInputQueue();
81
203
  } catch (error) {
82
204
  this.initializing = false;
@@ -94,19 +216,12 @@ export class AcpSessionBridge extends EventEmitter implements acp.Client {
94
216
  const result = await this.connection!.resumeSession({
95
217
  sessionId: externalSessionId,
96
218
  cwd: this.cwd,
219
+ additionalDirectories: this.additionalDirectories.length > 0 ? this.additionalDirectories : undefined,
97
220
  });
98
221
  this.acpSessionId = externalSessionId;
99
222
  this.initializing = false;
100
223
 
101
- this.emit('acp:initialized', {
102
- sessionId: this.sessionId,
103
- acpSessionId: this.acpSessionId,
104
- modes: this.extractModes(result.modes),
105
- currentModeId: result.modes?.currentModeId ?? null,
106
- currentModel: this.extractCurrentModel(result.models),
107
- availableModels: this.extractAvailableModels(result.models),
108
- currentModelId: result.models?.currentModelId ?? null,
109
- });
224
+ this.emit('acp:initialized', this.buildInitializedPayload(result));
110
225
  } catch (error) {
111
226
  // resumeSession may fail if agent doesn't support session.resume
112
227
  // fallback to newSession
@@ -114,19 +229,12 @@ export class AcpSessionBridge extends EventEmitter implements acp.Client {
114
229
  const result = await this.connection!.newSession({
115
230
  cwd: this.cwd,
116
231
  mcpServers: toMcpServers(this.mcpServers),
232
+ additionalDirectories: this.additionalDirectories.length > 0 ? this.additionalDirectories : undefined,
117
233
  });
118
234
  this.acpSessionId = result.sessionId;
119
235
  this.initializing = false;
120
236
 
121
- this.emit('acp:initialized', {
122
- sessionId: this.sessionId,
123
- acpSessionId: this.acpSessionId,
124
- modes: this.extractModes(result.modes),
125
- currentModeId: result.modes?.currentModeId ?? null,
126
- currentModel: this.extractCurrentModel(result.models),
127
- availableModels: this.extractAvailableModels(result.models),
128
- currentModelId: result.models?.currentModelId ?? null,
129
- });
237
+ this.emit('acp:initialized', this.buildInitializedPayload(result));
130
238
  } catch (fallbackError) {
131
239
  this.initializing = false;
132
240
  this.handleError(fallbackError);
@@ -155,6 +263,16 @@ export class AcpSessionBridge extends EventEmitter implements acp.Client {
155
263
 
156
264
  async setMode(modeId: string): Promise<void> {
157
265
  if (!this.connection || !this.acpSessionId) return;
266
+ const modeConfig = findConfigByCategory(this.configOptions, 'mode');
267
+ if (modeConfig) {
268
+ // 配置项通道:Agent 提供 configOptions 时以配置项为准
269
+ await this.connection.setSessionConfigOption({
270
+ sessionId: this.acpSessionId,
271
+ configId: modeConfig.id,
272
+ value: modeId,
273
+ });
274
+ return;
275
+ }
158
276
  await this.connection.setSessionMode({
159
277
  sessionId: this.acpSessionId,
160
278
  modeId,
@@ -163,13 +281,70 @@ export class AcpSessionBridge extends EventEmitter implements acp.Client {
163
281
 
164
282
  async setModel(modelId: string): Promise<void> {
165
283
  if (!this.connection || !this.acpSessionId) return;
166
- await this.connection.unstable_setSessionModel({
284
+ const modelConfig = findConfigByCategory(this.configOptions, 'model');
285
+ if (modelConfig) {
286
+ await this.connection.setSessionConfigOption({
287
+ sessionId: this.acpSessionId,
288
+ configId: modelConfig.id,
289
+ value: modelId,
290
+ });
291
+ return;
292
+ }
293
+ // SDK 1.x 已移除 unstable_setSessionModel:无 configOptions 的旧 Agent 无法走配置项通道。
294
+ // 模型切换暂不支持,由上层提示(不保留 unstable 调用)。
295
+ throw new Error('Model switching is not supported: agent does not expose a model config option');
296
+ }
297
+
298
+ async setConfigOption(configId: string, value: string | boolean): Promise<acp.SetSessionConfigOptionResponse> {
299
+ if (!this.connection || !this.acpSessionId) {
300
+ throw new Error('ACP bridge is not initialized');
301
+ }
302
+ const response = await this.connection.setSessionConfigOption({
167
303
  sessionId: this.acpSessionId,
168
- modelId,
304
+ configId,
305
+ ...(typeof value === 'boolean' ? { value, type: 'boolean' as const } : { value }),
169
306
  });
307
+ if (response.configOptions) {
308
+ // SDK 1.3.0:set_config_option 响应携带全量快照,与 config_option_update 通知同源
309
+ this.configOptions = extractConfigOptions(response.configOptions);
310
+ this.socket.emit('acp:config-options', {
311
+ sessionId: this.sessionId,
312
+ configOptions: this.configOptions,
313
+ });
314
+ }
315
+ return response;
170
316
  }
171
317
 
172
- sendInput(message: { type: string; content: string }): void {
318
+ async authenticate(methodId: string): Promise<void> {
319
+ if (!this.connection) {
320
+ throw new Error('ACP bridge is not initialized');
321
+ }
322
+ await this.connection.authenticate({ methodId });
323
+ }
324
+
325
+ async logout(): Promise<void> {
326
+ if (!this.connection) {
327
+ throw new Error('ACP bridge is not initialized');
328
+ }
329
+ await this.connection.logout({});
330
+ }
331
+
332
+ async deleteSession(): Promise<void> {
333
+ if (!this.connection || !this.acpSessionId) {
334
+ throw new Error('ACP bridge is not initialized');
335
+ }
336
+ await this.connection.deleteSession({ sessionId: this.acpSessionId });
337
+ }
338
+
339
+ getConfigOptions(): SessionConfigOptionData[] {
340
+ return this.configOptions;
341
+ }
342
+
343
+ getSupportsAdditionalDirectories(): boolean {
344
+ return this.supportsAdditionalDirectories;
345
+ }
346
+
347
+ sendInput(message: { type: string; content: string; attachments?: Array<Record<string, unknown>> }): void {
173
348
  if (this.activePrompt) {
174
349
  this.inputQueue.push(message);
175
350
  return;
@@ -177,12 +352,18 @@ export class AcpSessionBridge extends EventEmitter implements acp.Client {
177
352
  this.processPrompt(message);
178
353
  }
179
354
 
180
- private async processPrompt(message: QueuedInput): Promise<void> {
355
+ private async processPrompt(message: QueuedInput & { attachments?: Array<Record<string, unknown>> }): Promise<void> {
181
356
  if (!this.connection || !this.acpSessionId || this.stopped) return;
182
357
 
358
+ const prompt: acp.ContentBlock[] = [];
359
+ for (const attachment of message.attachments ?? []) {
360
+ prompt.push(attachment as acp.ContentBlock);
361
+ }
362
+ prompt.push({ type: 'text' as const, text: message.content });
363
+
183
364
  this.activePrompt = this.connection.prompt({
184
365
  sessionId: this.acpSessionId,
185
- prompt: [{ type: 'text' as const, text: message.content }],
366
+ prompt,
186
367
  });
187
368
 
188
369
  this.socket.emit('acp:prompt:start', { sessionId: this.sessionId });
@@ -212,44 +393,106 @@ export class AcpSessionBridge extends EventEmitter implements acp.Client {
212
393
  const output = this.spawner.stdout;
213
394
  const stream = acp.ndJsonStream(input, output);
214
395
  this.connection = new acp.ClientSideConnection(() => this, stream);
215
- await this.connection.initialize({
396
+ const result = await this.connection.initialize({
216
397
  protocolVersion: ACP_PROTOCOL_VERSION,
217
- clientCapabilities: {},
398
+ clientCapabilities: {
399
+ session: {
400
+ configOptions: { boolean: {} },
401
+ },
402
+ },
218
403
  clientInfo: {
219
404
  name: 'agent-gear',
220
- version: '1.0.0',
405
+ version: '1.1.5',
221
406
  },
222
407
  });
223
- }
224
408
 
225
- private flushInputQueue(): void {
226
- while (this.inputQueue.length > 0) {
227
- this.processPrompt(this.inputQueue.shift()!);
409
+ this.agentProtocolVersion = result.protocolVersion ?? null;
410
+ if (result.protocolVersion != null && result.protocolVersion !== ACP_PROTOCOL_VERSION) {
411
+ // 版本不兼容:不断连,由上层提示用户
412
+ this.socket.emit('acp:protocol-version-mismatch', {
413
+ sessionId: this.sessionId,
414
+ expectedVersion: ACP_PROTOCOL_VERSION,
415
+ agentVersion: result.protocolVersion,
416
+ });
228
417
  }
229
- }
230
418
 
231
- private extractModes(modes: acp.SessionModeState | null | undefined): Array<{ id: string; name: string; description?: string }> {
232
- if (!modes?.availableModes) return [];
233
- return modes.availableModes.map((m) => ({
234
- id: m.id,
235
- name: m.name,
236
- description: m.description ?? undefined,
237
- }));
419
+ this.promptCapabilities = result.agentCapabilities?.promptCapabilities
420
+ ? {
421
+ image: result.agentCapabilities.promptCapabilities.image ?? false,
422
+ audio: result.agentCapabilities.promptCapabilities.audio ?? false,
423
+ embeddedContext: result.agentCapabilities.promptCapabilities.embeddedContext ?? false,
424
+ }
425
+ : null;
426
+ this.agentInfo = result.agentInfo
427
+ ? { name: result.agentInfo.name, version: result.agentInfo.version ?? null }
428
+ : null;
429
+ this.authMethods = toAuthMethods(result.authMethods);
430
+ this.supportsAdditionalDirectories =
431
+ result.agentCapabilities?.sessionCapabilities?.additionalDirectories !== undefined &&
432
+ result.agentCapabilities?.sessionCapabilities?.additionalDirectories !== null;
433
+
434
+ if (this.authMethods.length > 0) {
435
+ this.socket.emit('acp:auth:required', {
436
+ sessionId: this.sessionId,
437
+ authMethods: this.authMethods,
438
+ });
439
+ }
238
440
  }
239
441
 
240
- private extractCurrentModel(models: acp.SessionModelState | null | undefined): { id: string; name: string; provider?: string } | null {
241
- if (!models?.availableModels || !models.currentModelId) return null;
242
- const found = models.availableModels.find((m) => m.modelId === models.currentModelId);
243
- if (!found) return null;
244
- return { id: found.modelId, name: found.name };
442
+ private buildInitializedPayload(
443
+ result: acp.NewSessionResponse | acp.ResumeSessionResponse,
444
+ ): AcpInitializedPayload {
445
+ const configOptions = extractConfigOptions(result.configOptions);
446
+ this.configOptions = configOptions.length > 0 ? configOptions : this.configOptions;
447
+
448
+ // 协议要求:响应含 configOptions 时以其为准(忽略 modes)
449
+ const configModes = extractConfigModes(this.configOptions);
450
+ const configModels = extractConfigModels(this.configOptions);
451
+
452
+ let modes: Array<{ id: string; name: string; description?: string }> = [];
453
+ let currentModeId: string | null = null;
454
+ if (configModes) {
455
+ modes = configModes.modes;
456
+ currentModeId = configModes.currentModeId;
457
+ } else if (result.modes?.availableModes) {
458
+ modes = result.modes.availableModes.map((m) => ({
459
+ id: m.id,
460
+ name: m.name,
461
+ description: m.description ?? undefined,
462
+ }));
463
+ currentModeId = result.modes.currentModeId ?? null;
464
+ }
465
+
466
+ let currentModel: AcpInitializedPayload['currentModel'] = null;
467
+ let availableModels: Array<{ id: string; name: string; provider?: string }> = [];
468
+ let currentModelId: string | null = null;
469
+ if (configModels) {
470
+ availableModels = configModels.availableModels;
471
+ currentModelId = configModels.currentModelId;
472
+ currentModel = configModels.currentModel;
473
+ }
474
+
475
+ return {
476
+ sessionId: this.sessionId,
477
+ acpSessionId: this.acpSessionId,
478
+ modes,
479
+ currentModeId,
480
+ currentModel,
481
+ availableModels,
482
+ currentModelId,
483
+ configOptions: this.configOptions,
484
+ promptCapabilities: this.promptCapabilities,
485
+ agentInfo: this.agentInfo,
486
+ authMethods: this.authMethods,
487
+ supportsAdditionalDirectories: this.supportsAdditionalDirectories,
488
+ protocolVersion: this.agentProtocolVersion,
489
+ };
245
490
  }
246
491
 
247
- private extractAvailableModels(models: acp.SessionModelState | null | undefined): Array<{ id: string; name: string; provider?: string }> {
248
- if (!models?.availableModels) return [];
249
- return models.availableModels.map((m) => ({
250
- id: m.modelId,
251
- name: m.name,
252
- }));
492
+ private flushInputQueue(): void {
493
+ while (this.inputQueue.length > 0) {
494
+ this.processPrompt(this.inputQueue.shift()!);
495
+ }
253
496
  }
254
497
 
255
498
  // ── acp.Client implementation ──
@@ -259,8 +502,22 @@ export class AcpSessionBridge extends EventEmitter implements acp.Client {
259
502
  switch (update.sessionUpdate) {
260
503
  case 'agent_message_chunk':
261
504
  if (update.content.type === 'text') {
505
+ if (this.textChunkBuffer && this.textChunkMessageId !== update.messageId) {
506
+ // messageId 边界:不同消息立即 flush
507
+ this.flushTextBuffer();
508
+ }
262
509
  this.textChunkBuffer += update.content.text;
263
- this.scheduleTextFlush();
510
+ this.textChunkMessageId = update.messageId;
511
+ if (update.messageId) {
512
+ // 有 messageId 时跳过 50ms debounce 猜测,按 ID 边界 flush
513
+ this.flushTextBuffer();
514
+ } else {
515
+ this.scheduleTextFlush();
516
+ }
517
+ } else {
518
+ // 非 text chunk(image/audio/resource)原样透传
519
+ this.flushTextBuffer();
520
+ this.sendUpdate(update);
264
521
  }
265
522
  break;
266
523
  case 'tool_call':
@@ -270,6 +527,14 @@ export class AcpSessionBridge extends EventEmitter implements acp.Client {
270
527
  this.flushTextBuffer();
271
528
  this.sendUpdate(update);
272
529
  break;
530
+ case 'config_option_update':
531
+ this.flushTextBuffer();
532
+ this.configOptions = extractConfigOptions(update.configOptions);
533
+ this.socket.emit('acp:config-options', {
534
+ sessionId: this.sessionId,
535
+ configOptions: this.configOptions,
536
+ });
537
+ break;
273
538
  case 'user_message_chunk':
274
539
  break;
275
540
  default:
@@ -331,8 +596,10 @@ export class AcpSessionBridge extends EventEmitter implements acp.Client {
331
596
  this.sendUpdate({
332
597
  sessionUpdate: 'agent_message_chunk' as const,
333
598
  content: { type: 'text' as const, text: this.textChunkBuffer },
599
+ ...(this.textChunkMessageId ? { messageId: this.textChunkMessageId } : {}),
334
600
  } as acp.SessionUpdate);
335
601
  this.textChunkBuffer = '';
602
+ this.textChunkMessageId = undefined;
336
603
  }
337
604
  }
338
605
 
@@ -11,14 +11,16 @@ interface AcpServerInfo {
11
11
 
12
12
  export class AcpDetector {
13
13
  async detect(): Promise<AcpServerInfo[]> {
14
- const [opencode, copilot, claudeCode, geminiCli, codex] = await Promise.all([
14
+ const [opencode, copilot, claudeCode, geminiCli, codex, mimoCode, kimiCode] = await Promise.all([
15
15
  this.detectOpenCode(),
16
16
  this.detectGitHubCopilot(),
17
17
  this.detectClaudeCode(),
18
18
  this.detectGeminiCli(),
19
19
  this.detectCodex(),
20
+ this.detectMiMoCode(),
21
+ this.detectKimiCode(),
20
22
  ]);
21
- return [opencode, copilot, claudeCode, geminiCli, codex];
23
+ return [opencode, copilot, claudeCode, geminiCli, codex, mimoCode, kimiCode];
22
24
  }
23
25
 
24
26
  private async detectOpenCode(): Promise<AcpServerInfo> {
@@ -71,6 +73,26 @@ export class AcpDetector {
71
73
  }
72
74
  }
73
75
 
76
+ private async detectMiMoCode(): Promise<AcpServerInfo> {
77
+ try {
78
+ const { stdout } = await execAsync('mimo --version');
79
+ const version = this.parseVersion(stdout);
80
+ return { type: 'mimo_code', available: true, version };
81
+ } catch {
82
+ return { type: 'mimo_code', available: false };
83
+ }
84
+ }
85
+
86
+ private async detectKimiCode(): Promise<AcpServerInfo> {
87
+ try {
88
+ const { stdout } = await execAsync('kimi --version');
89
+ const version = this.parseVersion(stdout);
90
+ return { type: 'kimi_code', available: true, version };
91
+ } catch {
92
+ return { type: 'kimi_code', available: false };
93
+ }
94
+ }
95
+
74
96
  getSpawnerConfig(serverType: string): { command: string; args: string[] } {
75
97
  switch (serverType) {
76
98
  case 'github_copilot':
@@ -81,6 +103,10 @@ export class AcpDetector {
81
103
  return { command: 'gemini', args: ['--acp'] };
82
104
  case 'codex':
83
105
  return { command: 'codex-acp', args: [] };
106
+ case 'mimo_code':
107
+ return { command: 'mimo', args: ['acp'] };
108
+ case 'kimi_code':
109
+ return { command: 'kimi', args: ['acp'] };
84
110
  case 'opencode':
85
111
  default:
86
112
  return { command: 'opencode', args: ['acp'] };
@@ -29,6 +29,7 @@ export class AcpSyncBridge implements acp.Client {
29
29
  private spawner: AcpSpawner;
30
30
  private connection: acp.ClientSideConnection | null = null;
31
31
  private updates: acp.SessionNotification[] = [];
32
+ private capabilities: acp.AgentCapabilities | null = null;
32
33
 
33
34
  constructor(options: AcpSyncBridgeOptions, spawner?: AcpSpawner) {
34
35
  this.spawner = spawner ?? new AcpSpawner({
@@ -53,15 +54,30 @@ export class AcpSyncBridge implements acp.Client {
53
54
  clientInfo: { name: 'agent-gear', version: '1.0.0' },
54
55
  });
55
56
 
56
- await withTimeout(
57
+ const result = await withTimeout(
57
58
  Promise.race([initPromise, exitPromise]),
58
59
  timeoutMs,
59
60
  `ACP connect timed out after ${timeoutMs}ms`,
60
61
  );
62
+ this.capabilities = result.agentCapabilities ?? null;
61
63
  }
62
64
 
63
- async listSessions(cwd: string): Promise<acp.ListSessionsResponse> {
64
- return this.connection!.listSessions({ cwd });
65
+ supportsDelete(): boolean {
66
+ return Boolean(this.capabilities?.sessionCapabilities?.delete);
67
+ }
68
+
69
+ async listSessions(cwd: string, cursor?: string | null): Promise<acp.ListSessionsResponse> {
70
+ if (!this.connection) {
71
+ throw new Error('ACP connection not established');
72
+ }
73
+ return this.connection.listSessions({ cwd, ...(cursor ? { cursor } : {}) });
74
+ }
75
+
76
+ async deleteSession(sessionId: string): Promise<void> {
77
+ if (!this.connection) {
78
+ throw new Error('ACP connection not established');
79
+ }
80
+ await this.connection.deleteSession({ sessionId });
65
81
  }
66
82
 
67
83
  async loadSession(