@adecore/agents 0.31.12 → 0.33.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 (50) hide show
  1. package/dist/chat/acp-backend.d.ts +4 -2
  2. package/dist/chat/acp-backend.js +50 -11
  3. package/dist/chat/acp-protocol.d.ts +29 -4
  4. package/dist/chat/acp-protocol.js +258 -21
  5. package/dist/chat/backend.d.ts +3 -0
  6. package/dist/chat/chat-core.js +6 -3
  7. package/dist/chat/chat-session.js +5 -2
  8. package/dist/chat/claude-backend.js +4 -4
  9. package/dist/chat/claude-protocol.d.ts +4 -1
  10. package/dist/chat/claude-protocol.js +55 -3
  11. package/dist/chat/codex-backend.js +4 -4
  12. package/dist/chat/fixtures/antigravity-1.4.1-question.json +69 -0
  13. package/dist/chat/fixtures/antigravity-1.4.1-quota.json +35 -0
  14. package/dist/chat/fixtures/grok-1.0.62-limits.json +66 -0
  15. package/dist/chat/fixtures/grok-1.0.62-question.json +99 -0
  16. package/dist/chat/json-rpc-transport.js +2 -2
  17. package/dist/chat/projector.js +22 -10
  18. package/dist/providers/accounts/service.js +4 -0
  19. package/dist/providers/claude-provider.js +2 -1
  20. package/dist/providers/detect.d.ts +1 -1
  21. package/dist/providers/detect.js +6 -3
  22. package/dist/providers/registry.d.ts +7 -0
  23. package/dist/providers/registry.js +64 -5
  24. package/dist/skills.d.ts +9 -1
  25. package/dist/skills.js +178 -20
  26. package/dist/testing/fake-acp-capture.d.ts +1 -0
  27. package/dist/testing/fake-acp-capture.js +4 -3
  28. package/dist/usage/accounts.js +3 -2
  29. package/package.json +3 -3
  30. package/src/chat/acp-backend.ts +56 -11
  31. package/src/chat/acp-protocol.ts +303 -24
  32. package/src/chat/backend.ts +5 -1
  33. package/src/chat/chat-core.ts +6 -3
  34. package/src/chat/chat-session.ts +5 -2
  35. package/src/chat/claude-backend.ts +4 -4
  36. package/src/chat/claude-protocol.ts +57 -3
  37. package/src/chat/codex-backend.ts +4 -4
  38. package/src/chat/fixtures/antigravity-1.4.1-question.json +69 -0
  39. package/src/chat/fixtures/antigravity-1.4.1-quota.json +35 -0
  40. package/src/chat/fixtures/grok-1.0.62-limits.json +66 -0
  41. package/src/chat/fixtures/grok-1.0.62-question.json +99 -0
  42. package/src/chat/json-rpc-transport.ts +2 -2
  43. package/src/chat/projector.ts +24 -12
  44. package/src/providers/accounts/service.ts +4 -0
  45. package/src/providers/claude-provider.ts +2 -1
  46. package/src/providers/detect.ts +7 -3
  47. package/src/providers/registry.ts +71 -5
  48. package/src/skills.ts +198 -19
  49. package/src/testing/fake-acp-capture.ts +8 -4
  50. package/src/usage/accounts.ts +3 -2
@@ -262,7 +262,7 @@ export class CodexBackend implements ChatBackend {
262
262
  private async runGoalCommand(command: GoalCommand, input: TurnInput): Promise<void> {
263
263
  const transport = this.transport;
264
264
  if (!transport) {
265
- this.emit({ type: 'failed', message: 'thread/goal failed: Codex is not running' });
265
+ this.emit({ type: 'failed', message: 'Codex is not running' });
266
266
  return;
267
267
  }
268
268
  const threadId = this.threadId;
@@ -328,7 +328,7 @@ export class CodexBackend implements ChatBackend {
328
328
  async goal(action: ChatGoalAction): Promise<void> {
329
329
  const transport = this.transport;
330
330
  if (!transport) {
331
- throw new Error('thread/goal failed: Codex is not running');
331
+ throw new Error('Codex is not running');
332
332
  }
333
333
  if (action === 'clear') {
334
334
  await transport.request('thread/goal/clear', { threadId: this.threadId, origin: 'user' });
@@ -443,7 +443,7 @@ export class CodexBackend implements ChatBackend {
443
443
  listThreadItems(params: { threadId: string; cursor?: string; limit: number; sortDirection: 'asc' | 'desc' }): Promise<unknown> {
444
444
  const transport = this.transport;
445
445
  if (!transport) {
446
- return Promise.reject(new Error('thread/items/list failed: Codex is not running'));
446
+ return Promise.reject(new Error('Codex is not running'));
447
447
  }
448
448
  return transport.request('thread/items/list', params);
449
449
  }
@@ -537,7 +537,7 @@ export class CodexBackend implements ChatBackend {
537
537
  private request(method: string, params: unknown, accepted?: () => void): void {
538
538
  const transport = this.transport;
539
539
  if (!transport) {
540
- this.emit({ type: 'failed', message: `${method} failed: Codex is not running` });
540
+ this.emit({ type: 'failed', message: 'Codex is not running' });
541
541
  return;
542
542
  }
543
543
  void transport
@@ -0,0 +1,69 @@
1
+ {
2
+ "version": "Antigravity ACP server 1.4.1, `agy_acp_server.par`",
3
+ "source": "Not recorded: a question needs the model to choose to ask one. Built from `ExternalOnInteractionHook` in the server's acp_server/hooks.py inside agy_acp_server.par 1.4.1: a `tool_call` with an `interaction_` id and an empty rawInput, then one `session/request_permission` per question with an `allow_once` option per choice, answered with the chosen optionId.",
4
+ "sessionId": "agy-session-1",
5
+ "initialize": {
6
+ "protocolVersion": 1,
7
+ "agentCapabilities": {
8
+ "loadSession": true,
9
+ "promptCapabilities": {
10
+ "image": false,
11
+ "embeddedContext": true
12
+ }
13
+ },
14
+ "authMethods": []
15
+ },
16
+ "sessionNew": {
17
+ "sessionId": "agy-session-1"
18
+ },
19
+ "commands": [],
20
+ "setConfigOption": {},
21
+ "promptUpdates": [
22
+ {
23
+ "sessionUpdate": "tool_call",
24
+ "toolCallId": "interaction_3f9a1c2e",
25
+ "title": "Which test runner should I set up?",
26
+ "rawInput": {},
27
+ "status": "pending"
28
+ },
29
+ {
30
+ "permission": {
31
+ "sessionId": "agy-session-1",
32
+ "toolCall": {
33
+ "toolCallId": "interaction_3f9a1c2e",
34
+ "title": "Which test runner should I set up?",
35
+ "rawInput": {},
36
+ "status": "pending"
37
+ },
38
+ "options": [
39
+ {
40
+ "optionId": "opt_builtin",
41
+ "name": "The built-in runner",
42
+ "kind": "allow_once"
43
+ },
44
+ {
45
+ "optionId": "opt_separate",
46
+ "name": "A separate runner",
47
+ "kind": "allow_once"
48
+ }
49
+ ]
50
+ }
51
+ },
52
+ {
53
+ "sessionUpdate": "tool_call_update",
54
+ "toolCallId": "interaction_3f9a1c2e",
55
+ "status": "completed",
56
+ "rawOutput": "Response received"
57
+ },
58
+ {
59
+ "sessionUpdate": "agent_message_chunk",
60
+ "content": {
61
+ "type": "text",
62
+ "text": "Setting it up."
63
+ }
64
+ }
65
+ ],
66
+ "promptResponse": {
67
+ "stopReason": "end_turn"
68
+ }
69
+ }
@@ -0,0 +1,35 @@
1
+ {
2
+ "version": "Antigravity ACP server 1.4.1, `agy_acp_server.par`",
3
+ "source": "Not recorded: no account here runs out of quota on demand. Built from the server's own Python modules inside agy_acp_server.par 1.4.1 (acp_server/server.py and ccpa_connection/quota_errors.py): on QUOTA_EXHAUSTED it writes this message as an agent_message_chunk and answers the prompt with `end_turn`; the reset is the delay in its two largest units, or the time when only that is known.",
4
+ "sessionId": "agy-session-1",
5
+ "initialize": {
6
+ "protocolVersion": 1,
7
+ "agentCapabilities": {
8
+ "loadSession": true,
9
+ "promptCapabilities": {
10
+ "image": false,
11
+ "embeddedContext": true
12
+ }
13
+ },
14
+ "authMethods": []
15
+ },
16
+ "sessionNew": {
17
+ "sessionId": "agy-session-1"
18
+ },
19
+ "commands": [],
20
+ "setConfigOption": {},
21
+ "promptUpdates": [
22
+ {
23
+ "sessionUpdate": "agent_message_chunk",
24
+ "content": {
25
+ "type": "text",
26
+ "text": "Usage Limit Reached\n\nYou have reached your current quota for this period. Your limit will reset in 4 days, 23 hours."
27
+ }
28
+ }
29
+ ],
30
+ "promptResponse": {
31
+ "stopReason": "end_turn"
32
+ },
33
+ "resetOn": "Usage Limit Reached\n\nYou have reached your current quota for this period. Your limit will reset on Oct 23, 2026 14:05 UTC.",
34
+ "noReset": "Usage Limit Reached\n\nYou have reached your current quota for this period."
35
+ }
@@ -0,0 +1,66 @@
1
+ {
2
+ "version": "Grok 1.0.62, `grok --no-auto-update agent stdio`",
3
+ "source": "Not recorded: no account here runs into these limits on demand. Built from the source of xai-org/grok-build at 2bdd1d6a, the closest to 1.0.62 it publishes: `handle_sampling_failure` in acp_session_impl/sampler_turn.rs fails `session/prompt` with -32003 after a 429 and with -32603 for an overloaded model once its retries ran out, and sampling/error.rs builds the `data` (a string, or `message` with `http_status`). The bodies after the status line are examples.",
4
+ "sessionId": "grok-session-1",
5
+ "initialize": {
6
+ "protocolVersion": 1,
7
+ "agentCapabilities": {
8
+ "loadSession": true,
9
+ "promptCapabilities": {
10
+ "image": false,
11
+ "embeddedContext": true
12
+ }
13
+ },
14
+ "authMethods": []
15
+ },
16
+ "sessionNew": {
17
+ "sessionId": "grok-session-1"
18
+ },
19
+ "commands": [],
20
+ "setConfigOption": {},
21
+ "promptUpdates": [
22
+ {
23
+ "sessionUpdate": "agent_thought_chunk",
24
+ "content": {
25
+ "type": "text",
26
+ "text": "Reading the request."
27
+ }
28
+ }
29
+ ],
30
+ "promptResponse": {
31
+ "stopReason": "end_turn"
32
+ },
33
+ "errors": {
34
+ "freeUsage": {
35
+ "code": -32003,
36
+ "message": "Rate limited",
37
+ "data": "API error (status 429 Too Many Requests): {\"code\":\"subscription:free-usage-exhausted\",\"error\":\"You have used the free usage of this period.\"}"
38
+ },
39
+ "rateLimited": {
40
+ "code": -32003,
41
+ "message": "Rate limited",
42
+ "data": "API error (status 429 Too Many Requests): {\"code\":\"rate_limit_exceeded\",\"error\":\"Too many requests.\"}"
43
+ },
44
+ "overloaded": {
45
+ "code": -32603,
46
+ "message": "Internal error",
47
+ "data": {
48
+ "message": "API error (status 529): Overloaded",
49
+ "http_status": 529
50
+ }
51
+ },
52
+ "overloadedStream": {
53
+ "code": -32603,
54
+ "message": "Internal error",
55
+ "data": "stream error (overloaded_error): Overloaded"
56
+ },
57
+ "badRequest": {
58
+ "code": -32603,
59
+ "message": "Internal error",
60
+ "data": {
61
+ "message": "API error (status 400 Bad Request): The prompt is too long.",
62
+ "http_status": 400
63
+ }
64
+ }
65
+ }
66
+ }
@@ -0,0 +1,99 @@
1
+ {
2
+ "version": "Grok 1.0.62, `grok --no-auto-update agent stdio`",
3
+ "source": "Not recorded: a question needs the model to choose to ask one. Built from the source of xai-org/grok-build at 2bdd1d6a, the closest to 1.0.62 it publishes: the ext request `_x.ai/ask_user_question` (acp_session_impl/spawn.rs) with the params of ask_user_question/types.rs; its answer is `{ outcome: \"accepted\", answers, annotations? }`, keyed by the question's text.",
4
+ "sessionId": "grok-session-1",
5
+ "initialize": {
6
+ "protocolVersion": 1,
7
+ "agentCapabilities": {
8
+ "loadSession": true,
9
+ "promptCapabilities": {
10
+ "image": false,
11
+ "embeddedContext": true
12
+ }
13
+ },
14
+ "authMethods": []
15
+ },
16
+ "sessionNew": {
17
+ "sessionId": "grok-session-1"
18
+ },
19
+ "commands": [],
20
+ "setConfigOption": {},
21
+ "promptUpdates": [
22
+ {
23
+ "sessionUpdate": "tool_call",
24
+ "toolCallId": "call-ask-1",
25
+ "title": "Ask the user",
26
+ "kind": "other",
27
+ "status": "pending"
28
+ },
29
+ {
30
+ "request": {
31
+ "method": "_x.ai/ask_user_question",
32
+ "params": {
33
+ "sessionId": "grok-session-1",
34
+ "toolCallId": "call-ask-1",
35
+ "mode": "default",
36
+ "questions": [
37
+ {
38
+ "question": "Which database should the service use?",
39
+ "options": [
40
+ {
41
+ "label": "Postgres",
42
+ "description": "The one the other services run on"
43
+ },
44
+ {
45
+ "label": "SQLite",
46
+ "description": "A file beside the service"
47
+ }
48
+ ],
49
+ "multiSelect": false
50
+ },
51
+ {
52
+ "question": "Which checks should run before a release?",
53
+ "options": [
54
+ {
55
+ "label": "Tests",
56
+ "description": "The unit tests"
57
+ },
58
+ {
59
+ "label": "Lint, strict",
60
+ "description": "Every warning fails"
61
+ },
62
+ {
63
+ "label": "Types",
64
+ "description": "The type check"
65
+ }
66
+ ],
67
+ "multiSelect": true
68
+ },
69
+ {
70
+ "question": "Anything else to keep in mind?",
71
+ "options": [
72
+ {
73
+ "label": "No",
74
+ "description": "Nothing"
75
+ }
76
+ ],
77
+ "multiSelect": null
78
+ }
79
+ ]
80
+ }
81
+ }
82
+ },
83
+ {
84
+ "sessionUpdate": "tool_call_update",
85
+ "toolCallId": "call-ask-1",
86
+ "status": "completed"
87
+ },
88
+ {
89
+ "sessionUpdate": "agent_message_chunk",
90
+ "content": {
91
+ "type": "text",
92
+ "text": "Noted."
93
+ }
94
+ }
95
+ ],
96
+ "promptResponse": {
97
+ "stopReason": "end_turn"
98
+ }
99
+ }
@@ -84,7 +84,7 @@ export class JsonRpcTransport {
84
84
  const timeoutMs = options.timeoutMs === undefined ? this.options.requestTimeoutMs : options.timeoutMs;
85
85
  return new Promise((resolve, reject) => {
86
86
  if (this.closed) {
87
- reject(new Error(`${method} failed: ${label} is not running`));
87
+ reject(new Error(`${label} is not running`));
88
88
  return;
89
89
  }
90
90
  const id = this.nextId++;
@@ -165,7 +165,7 @@ export class JsonRpcTransport {
165
165
  }
166
166
 
167
167
  private failed(error: unknown): void {
168
- const failure = new Error(`${this.options.label} transport failed: ${errorText(error)}`);
168
+ const failure = new Error(`Lost the connection to ${this.options.label}: ${errorText(error)}`);
169
169
  this.rejectPending(() => failure);
170
170
  try {
171
171
  this.options.onError?.(failure);
@@ -351,18 +351,30 @@ export class ThreadProjector {
351
351
  break;
352
352
  }
353
353
  case 'usage': {
354
- const contextTokens = event.contextTokens ?? info.usage.contextTokens;
355
- events.push(
356
- this.thread.patchInfo({
357
- usage: {
358
- ...info.usage,
359
- contextTokens,
360
- contextWindow: event.contextWindow ?? info.usage.contextWindow,
361
- costUsd: Math.max(info.usage.costUsd, event.costUsd ?? 0),
362
- breakdown: contextTokens > 0 ? estimateContextBreakdown(this.thread.list(), contextTokens) : undefined
363
- }
364
- })
365
- );
354
+ const usage = info.usage;
355
+ const contextTokens = event.contextTokens ?? usage.contextTokens;
356
+ const next = {
357
+ ...usage,
358
+ contextTokens,
359
+ contextWindow: event.contextWindow ?? usage.contextWindow,
360
+ costUsd: Math.max(usage.costUsd, event.costUsd ?? 0),
361
+ // The estimate reads the whole thread, so only a new count of tokens is worth it.
362
+ ...(contextTokens === usage.contextTokens
363
+ ? {}
364
+ : { breakdown: contextTokens > 0 ? estimateContextBreakdown(this.thread.list(), contextTokens) : undefined }),
365
+ ...(event.cacheTtlMs === undefined || event.cacheExpiresAt === undefined
366
+ ? {}
367
+ : { cacheTtlMs: event.cacheTtlMs, cacheExpiresAt: event.cacheExpiresAt })
368
+ };
369
+ if (
370
+ next.contextTokens !== usage.contextTokens ||
371
+ next.contextWindow !== usage.contextWindow ||
372
+ next.costUsd !== usage.costUsd ||
373
+ next.cacheTtlMs !== usage.cacheTtlMs ||
374
+ next.cacheExpiresAt !== usage.cacheExpiresAt
375
+ ) {
376
+ events.push(this.thread.patchInfo({ usage: next }));
377
+ }
366
378
  break;
367
379
  }
368
380
  case 'compaction':
@@ -225,6 +225,10 @@ export class ProviderAccountsService implements AccountLaunches {
225
225
  };
226
226
  if (isDefaultAccountOf(kind, id)) {
227
227
  const account = this.accounts[kind] ?? { kind };
228
+ // Only a start that names it: every turn of a chat on the default account asks with none.
229
+ if (id !== undefined && account.enabled === false) {
230
+ throw refuse('it is turned off');
231
+ }
228
232
  return accountEnv(kind, account, provider, baseEnv, variables(account));
229
233
  }
230
234
  const account = this.accounts[key];
@@ -33,7 +33,8 @@ export function createClaudeProvider(options: ClaudeBackendOptions = {}): ChatPr
33
33
  { kind: 'env', variable: 'ANTHROPIC_API_KEY' }
34
34
  ]
35
35
  },
36
- detect: detectCli,
36
+ // The version probe must never be the start that updates the CLI.
37
+ detect: (command, env) => detectCli(command, { ...env, DISABLE_AUTOUPDATER: '1' }),
37
38
  oneShotArgs: (prompt) => ['-p', prompt, '--output-format', 'text'],
38
39
  // `-p` without a prompt argument reads the prompt from stdin.
39
40
  oneShotStdinArgs: ['-p', '--output-format', 'text'],
@@ -6,18 +6,22 @@ export interface CliDetection {
6
6
  version: string | null;
7
7
  }
8
8
 
9
- /* Asks a CLI for its version; a missing binary is a plain "not installed", never an error. */
9
+ /*
10
+ * Asks a CLI for its version; a missing binary is a plain "not installed", never an error. `flags` go before
11
+ * `--version`, for a CLI that would otherwise look for an update of itself on any start.
12
+ */
10
13
  export async function detectCli(
11
14
  command: string,
12
15
  env: Record<string, string | undefined> = process.env,
13
- spawn: SpawnChatProcess = spawnChatProcess
16
+ spawn: SpawnChatProcess = spawnChatProcess,
17
+ flags: readonly string[] = []
14
18
  ): Promise<CliDetection> {
15
19
  try {
16
20
  let exited: (code: number | null) => void = () => undefined;
17
21
  const code = new Promise<number | null>((resolve) => {
18
22
  exited = resolve;
19
23
  });
20
- const proc = spawn({ command: [command, '--version'], cwd: process.cwd(), env: definedEnv(env), onExit: (exitCode) => exited(exitCode) });
24
+ const proc = spawn({ command: [command, ...flags, '--version'], cwd: process.cwd(), env: definedEnv(env), onExit: (exitCode) => exited(exitCode) });
21
25
  proc.stdin.end();
22
26
  const output = await new Response(proc.stdout).text();
23
27
  if ((await code) !== 0) {
@@ -72,14 +72,48 @@ export class ProviderRegistry {
72
72
  private readonly detect: ((command: string) => Promise<CliDetection>) | null;
73
73
  private readonly env: Record<string, string | undefined>;
74
74
  private readonly cache = new Map<AgentKind, { at: number; detection: CliDetection }>();
75
+ private readonly listeners = new Set<(kind: AgentKind) => void>();
76
+ // What the registry listens to on each provider, so one taken away is let go.
77
+ private readonly stops = new Map<ChatProvider, () => void>();
75
78
 
76
79
  constructor(options: ProviderRegistryOptions = {}) {
77
- this.providers = options.providers ?? [...BUILT_IN_PROVIDERS];
80
+ this.providers = [...(options.providers ?? BUILT_IN_PROVIDERS)];
78
81
  this.commands = options.commands ?? {};
79
82
  this.detect = options.detect ?? null;
80
83
  this.env = options.env ?? process.env;
81
84
  }
82
85
 
86
+ /*
87
+ * Offers one more provider after the host started, such as a CLI a person installed since, or puts it in the
88
+ * place of the one of its kind. The `onChange` listeners hear its kind, so a host hands its clients the new list.
89
+ */
90
+ add(provider: ChatProvider): void {
91
+ const index = this.providers.findIndex((entry) => entry.kind === provider.kind);
92
+ if (index === -1) {
93
+ this.providers.push(provider);
94
+ } else {
95
+ this.unfollow(this.providers[index]!);
96
+ this.providers[index] = provider;
97
+ }
98
+ this.cache.delete(provider.kind);
99
+ if (this.listeners.size > 0) {
100
+ this.follow(provider);
101
+ }
102
+ this.changed(provider.kind);
103
+ }
104
+
105
+ /* Stops offering a kind; a record that names it gets the stand-in `get` answers for a kind the host does not offer. */
106
+ remove(kind: AgentKind): void {
107
+ const index = this.providers.findIndex((entry) => entry.kind === kind);
108
+ if (index === -1) {
109
+ return;
110
+ }
111
+ this.unfollow(this.providers[index]!);
112
+ this.providers.splice(index, 1);
113
+ this.cache.delete(kind);
114
+ this.changed(kind);
115
+ }
116
+
83
117
  async list(): Promise<ProviderInfo[]> {
84
118
  return await Promise.all(
85
119
  this.providers.map(async (provider) => ({
@@ -128,12 +162,24 @@ export class ProviderRegistry {
128
162
  return this.get(kind).enabled?.() !== false;
129
163
  }
130
164
 
131
- /* Calls the listener with the kind of every provider whose models or capabilities changed, so a host can list them again; answers how to stop. */
165
+ /*
166
+ * Calls the listener with the kind of every provider whose models or capabilities changed, and of every one
167
+ * added or removed, so a host can list them again; answers how to stop.
168
+ */
132
169
  onChange(listener: (kind: AgentKind) => void): () => void {
133
- const stops = this.providers.flatMap((provider) => (provider.onChange ? [provider.onChange(() => listener(provider.kind))] : []));
170
+ if (this.listeners.size === 0) {
171
+ for (const provider of this.providers) {
172
+ this.follow(provider);
173
+ }
174
+ }
175
+ this.listeners.add(listener);
134
176
  return () => {
135
- for (const stop of stops) {
136
- stop();
177
+ this.listeners.delete(listener);
178
+ if (this.listeners.size === 0) {
179
+ for (const stop of this.stops.values()) {
180
+ stop();
181
+ }
182
+ this.stops.clear();
137
183
  }
138
184
  };
139
185
  }
@@ -146,6 +192,26 @@ export class ProviderRegistry {
146
192
  return unofferedProvider(kind);
147
193
  }
148
194
 
195
+ private follow(provider: ChatProvider): void {
196
+ if (provider.onChange && !this.stops.has(provider)) {
197
+ this.stops.set(
198
+ provider,
199
+ provider.onChange(() => this.changed(provider.kind))
200
+ );
201
+ }
202
+ }
203
+
204
+ private unfollow(provider: ChatProvider): void {
205
+ this.stops.get(provider)?.();
206
+ this.stops.delete(provider);
207
+ }
208
+
209
+ private changed(kind: AgentKind): void {
210
+ for (const listener of [...this.listeners]) {
211
+ listener(kind);
212
+ }
213
+ }
214
+
149
215
  private async detection(provider: ChatProvider): Promise<CliDetection> {
150
216
  if (provider.enabled?.() === false) {
151
217
  return DISABLED;