@plaited/acp-harness 0.2.5

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 (45) hide show
  1. package/.claude/rules/accuracy.md +43 -0
  2. package/.claude/rules/bun-apis.md +80 -0
  3. package/.claude/rules/code-review.md +254 -0
  4. package/.claude/rules/git-workflow.md +37 -0
  5. package/.claude/rules/github.md +154 -0
  6. package/.claude/rules/testing.md +172 -0
  7. package/.claude/skills/acp-harness/SKILL.md +310 -0
  8. package/.claude/skills/acp-harness/assets/Dockerfile.acp +25 -0
  9. package/.claude/skills/acp-harness/assets/docker-compose.acp.yml +19 -0
  10. package/.claude/skills/acp-harness/references/downstream.md +288 -0
  11. package/.claude/skills/acp-harness/references/output-formats.md +221 -0
  12. package/.claude-plugin/marketplace.json +15 -0
  13. package/.claude-plugin/plugin.json +16 -0
  14. package/.github/CODEOWNERS +6 -0
  15. package/.github/workflows/ci.yml +63 -0
  16. package/.github/workflows/publish.yml +146 -0
  17. package/.mcp.json +20 -0
  18. package/CLAUDE.md +92 -0
  19. package/Dockerfile.test +23 -0
  20. package/LICENSE +15 -0
  21. package/README.md +94 -0
  22. package/bin/cli.ts +670 -0
  23. package/bin/tests/cli.spec.ts +362 -0
  24. package/biome.json +96 -0
  25. package/bun.lock +513 -0
  26. package/docker-compose.test.yml +21 -0
  27. package/package.json +57 -0
  28. package/scripts/bun-test-wrapper.sh +46 -0
  29. package/src/acp-client.ts +503 -0
  30. package/src/acp-helpers.ts +121 -0
  31. package/src/acp-transport.ts +455 -0
  32. package/src/acp-utils.ts +341 -0
  33. package/src/acp.constants.ts +56 -0
  34. package/src/acp.schemas.ts +161 -0
  35. package/src/acp.ts +27 -0
  36. package/src/acp.types.ts +28 -0
  37. package/src/tests/acp-client.spec.ts +205 -0
  38. package/src/tests/acp-helpers.spec.ts +105 -0
  39. package/src/tests/acp-integration.docker.ts +214 -0
  40. package/src/tests/acp-transport.spec.ts +153 -0
  41. package/src/tests/acp-utils.spec.ts +394 -0
  42. package/src/tests/fixtures/.claude/settings.local.json +8 -0
  43. package/src/tests/fixtures/.claude/skills/greeting/SKILL.md +17 -0
  44. package/src/tests/fixtures/calculator-mcp.ts +215 -0
  45. package/tsconfig.json +32 -0
package/package.json ADDED
@@ -0,0 +1,57 @@
1
+ {
2
+ "name": "@plaited/acp-harness",
3
+ "version": "0.2.5",
4
+ "description": "CLI tool for capturing agent trajectories from ACP-compatible agents",
5
+ "license": "ISC",
6
+ "engines": {
7
+ "bun": ">= v1.2.9"
8
+ },
9
+ "repository": {
10
+ "type": "git",
11
+ "url": "git+https://github.com/plaited/acp-harness.git"
12
+ },
13
+ "bugs": {
14
+ "url": "https://github.com/plaited/acp-harness/issues"
15
+ },
16
+ "homepage": "https://github.com/plaited/acp-harness/tree/main#readme",
17
+ "bin": {
18
+ "acp-harness": "./bin/cli.ts"
19
+ },
20
+ "type": "module",
21
+ "publishConfig": {
22
+ "access": "public"
23
+ },
24
+ "scripts": {
25
+ "check": "bun run check:biome && bun run check:types && bun run check:package",
26
+ "check:biome": "biome check",
27
+ "check:package": "format-package --check",
28
+ "check:types": "tsc --noEmit",
29
+ "check:write": "biome check --write && format-package --write",
30
+ "prepare": "git rev-parse --git-dir > /dev/null 2>&1 && git config core.hooksPath .hooks || true",
31
+ "test": "bun test ./src/ ./bin/ ./.claude",
32
+ "test:docker": "docker compose -f docker-compose.test.yml run --rm acp-test"
33
+ },
34
+ "lint-staged": {
35
+ "*.{js,cjs,jsx,tsx,ts}": [
36
+ "bunx biome check --write --files-ignore-unknown"
37
+ ],
38
+ "package.json": [
39
+ "format-package -w"
40
+ ]
41
+ },
42
+ "dependencies": {
43
+ "@agentclientprotocol/sdk": "^0.13.0",
44
+ "zod": "^4.3.5"
45
+ },
46
+ "peerDependencies": {
47
+ "bun": ">=1.2.9"
48
+ },
49
+ "devDependencies": {
50
+ "@biomejs/biome": "2.3.11",
51
+ "@types/bun": "1.3.6",
52
+ "@zed-industries/claude-code-acp": "0.13.0",
53
+ "format-package": "7.0.0",
54
+ "lint-staged": "16.2.7",
55
+ "typescript": "5.9.3"
56
+ }
57
+ }
@@ -0,0 +1,46 @@
1
+ #!/bin/bash
2
+ # Wrapper for bun test that handles Bun's post-test cleanup crash
3
+ # See: https://github.com/oven-sh/bun/issues/23643
4
+ #
5
+ # Bun 1.3.x has a known bug where the test runner crashes during cleanup
6
+ # after all tests complete successfully. This wrapper catches that crash
7
+ # (exit code 133 = SIGTRAP) and exits cleanly if tests actually passed.
8
+ #
9
+ # Usage:
10
+ # ./bun-test-wrapper.sh [args...] # Run bun test with provided args
11
+ # ./bun-test-wrapper.sh # (No args) Find and run all *.docker.ts files
12
+
13
+ # Determine test files to run
14
+ if [ $# -eq 0 ]; then
15
+ # No arguments: find all *.docker.ts files for Docker integration tests
16
+ docker_tests=$(find ./src ./bin -name "*.docker.ts" -type f 2>/dev/null)
17
+ if [ -z "$docker_tests" ]; then
18
+ echo "No *.docker.ts files found in ./src or ./bin"
19
+ exit 0
20
+ fi
21
+ echo "Found Docker integration tests:"
22
+ echo "$docker_tests" | sed 's/^/ /'
23
+ echo ""
24
+ # Convert newlines to arguments
25
+ set -- $docker_tests
26
+ fi
27
+
28
+ # Create temp file for output
29
+ tmpfile=$(mktemp)
30
+ trap "rm -f $tmpfile" EXIT
31
+
32
+ # Run tests with output to both terminal and file
33
+ bun test "$@" 2>&1 | tee "$tmpfile"
34
+ exit_code=${PIPESTATUS[0]}
35
+
36
+ # Check if tests passed (look for "X pass" and "0 fail" in output)
37
+ if grep -q " pass" "$tmpfile" && grep -q "0 fail" "$tmpfile"; then
38
+ # Tests passed - exit 0 even if Bun crashed during cleanup
39
+ if [ $exit_code -eq 133 ]; then
40
+ echo ""
41
+ echo "Note: Bun crashed during cleanup (known bug), but all tests passed."
42
+ exit 0
43
+ fi
44
+ fi
45
+
46
+ exit $exit_code
@@ -0,0 +1,503 @@
1
+ /**
2
+ * Headless ACP client for programmatic agent interaction.
3
+ *
4
+ * @remarks
5
+ * This client enables automated evaluation of ACP-compatible agents like
6
+ * Claude Code, Droid, Gemini CLI, and others. It provides:
7
+ *
8
+ * - **Subprocess management**: Spawn and control agent processes
9
+ * - **Session handling**: Create and manage conversation sessions
10
+ * - **Streaming prompts**: AsyncGenerator for real-time updates
11
+ * - **Sync prompts**: Simple request/response for basic evals
12
+ * - **Auto-permissions**: Automatically approves all permissions for headless use
13
+ *
14
+ * Designed for testing and evaluation, not for user-facing applications.
15
+ */
16
+
17
+ import type {
18
+ AgentCapabilities,
19
+ CancelNotification,
20
+ ClientCapabilities,
21
+ ContentBlock,
22
+ Implementation,
23
+ InitializeRequest,
24
+ InitializeResponse,
25
+ McpServer,
26
+ PromptRequest,
27
+ PromptResponse,
28
+ RequestPermissionRequest,
29
+ RequestPermissionResponse,
30
+ SessionNotification,
31
+ } from '@agentclientprotocol/sdk'
32
+ import { version } from '../package.json' with { type: 'json' }
33
+ import { ACP_METHODS, ACP_PROTOCOL_VERSION, DEFAULT_ACP_CLIENT_NAME } from './acp.constants.ts'
34
+ import { RequestPermissionRequestSchema, SessionNotificationSchema } from './acp.schemas.ts'
35
+ import type { Session } from './acp.types.ts'
36
+ import { createACPTransport } from './acp-transport.ts'
37
+ // ============================================================================
38
+ // Types
39
+ // ============================================================================
40
+
41
+ /** Configuration for the ACP client */
42
+ export type ACPClientConfig = {
43
+ /** Command to spawn agent (e.g., ['claude', 'code'] or ['droid']) */
44
+ command: string[]
45
+ /** Working directory for agent process */
46
+ cwd?: string
47
+ /** Environment variables for agent process */
48
+ env?: Record<string, string>
49
+ /** Client info for initialization */
50
+ clientInfo?: Implementation
51
+ /** Client capabilities to advertise */
52
+ capabilities?: ClientCapabilities
53
+ /** Timeout for operations in milliseconds (default: 30000) */
54
+ timeout?: number
55
+ /**
56
+ * Polling interval for streaming updates in milliseconds (default: 50).
57
+ * Lower values provide more responsive updates but increase CPU usage.
58
+ * Consider increasing for testing to reduce timing-related flakiness.
59
+ */
60
+ pollingInterval?: number
61
+ /**
62
+ * Permission handler for agent requests.
63
+ * Default: auto-approve all permissions (headless mode)
64
+ */
65
+ onPermissionRequest?: (params: RequestPermissionRequest) => Promise<RequestPermissionResponse>
66
+ }
67
+
68
+ /** Session update emitted during prompt streaming */
69
+ export type SessionUpdate = {
70
+ type: 'update'
71
+ params: SessionNotification
72
+ }
73
+
74
+ /** Prompt completion emitted when prompt finishes */
75
+ export type PromptComplete = {
76
+ type: 'complete'
77
+ result: PromptResponse
78
+ }
79
+
80
+ /** Events emitted during prompt streaming */
81
+ export type PromptEvent = SessionUpdate | PromptComplete
82
+
83
+ /** Error thrown by ACP client operations */
84
+ export class ACPClientError extends Error {
85
+ constructor(
86
+ message: string,
87
+ public readonly code?: string,
88
+ ) {
89
+ super(message)
90
+ this.name = 'ACPClientError'
91
+ }
92
+ }
93
+
94
+ // ============================================================================
95
+ // Client Implementation
96
+ // ============================================================================
97
+
98
+ /**
99
+ * Creates a headless ACP client for agent evaluation.
100
+ *
101
+ * @param config - Client configuration including command, cwd, and permission handling
102
+ * @returns Client object with lifecycle, session, and prompt methods
103
+ *
104
+ * @remarks
105
+ * The client manages:
106
+ * - Agent subprocess lifecycle (connect/disconnect)
107
+ * - Protocol initialization and capability negotiation
108
+ * - Session creation and management
109
+ * - Prompt streaming with real-time updates
110
+ * - Automatic permission approval for headless evaluation
111
+ *
112
+ * See module-level documentation in `src/acp.ts` for usage guidance.
113
+ * See client tests for usage patterns.
114
+ */
115
+ export const createACPClient = (config: ACPClientConfig) => {
116
+ const {
117
+ command,
118
+ cwd,
119
+ env,
120
+ clientInfo = { name: DEFAULT_ACP_CLIENT_NAME, version },
121
+ capabilities = {},
122
+ timeout = 30000,
123
+ pollingInterval = 50,
124
+ onPermissionRequest,
125
+ } = config
126
+
127
+ let transport: ReturnType<typeof createACPTransport> | undefined
128
+ let agentCapabilities: AgentCapabilities | undefined
129
+ let initializeResult: InitializeResponse | undefined
130
+
131
+ // Track active prompt sessions for update routing
132
+ const activePrompts = new Map<
133
+ string,
134
+ {
135
+ updates: SessionNotification[]
136
+ resolve: (result: PromptResponse) => void
137
+ reject: (error: Error) => void
138
+ }
139
+ >()
140
+
141
+ // --------------------------------------------------------------------------
142
+ // Permission Handling
143
+ // --------------------------------------------------------------------------
144
+
145
+ /**
146
+ * Default permission handler: auto-approve all requests.
147
+ * For headless evaluation in trusted environments.
148
+ *
149
+ * @remarks
150
+ * Validates params with Zod before processing.
151
+ * Prioritizes `allow_always` for faster headless evaluation with fewer
152
+ * permission round-trips. Cancels if validation fails or no allow option
153
+ * is available.
154
+ */
155
+ const autoApprovePermission = async (params: RequestPermissionRequest): Promise<RequestPermissionResponse> => {
156
+ const result = RequestPermissionRequestSchema.safeParse(params)
157
+ if (!result.success) {
158
+ return { outcome: { outcome: 'cancelled' } }
159
+ }
160
+
161
+ const { options } = result.data
162
+
163
+ // Priority: allow_always (fewer round-trips) > allow_once
164
+ const allowAlways = options.find((opt) => opt.kind === 'allow_always')
165
+ if (allowAlways) {
166
+ return { outcome: { outcome: 'selected', optionId: allowAlways.optionId } }
167
+ }
168
+
169
+ const allowOnce = options.find((opt) => opt.kind === 'allow_once')
170
+ if (allowOnce) {
171
+ return { outcome: { outcome: 'selected', optionId: allowOnce.optionId } }
172
+ }
173
+
174
+ // No allow option available - cancel
175
+ return { outcome: { outcome: 'cancelled' } }
176
+ }
177
+
178
+ const handlePermissionRequest = onPermissionRequest ?? autoApprovePermission
179
+
180
+ // --------------------------------------------------------------------------
181
+ // Transport Callbacks
182
+ // --------------------------------------------------------------------------
183
+
184
+ const onNotification = (method: string, params: unknown) => {
185
+ if (method === ACP_METHODS.UPDATE) {
186
+ const updateParams = SessionNotificationSchema.parse(params)
187
+ const activePrompt = activePrompts.get(updateParams.sessionId)
188
+ if (activePrompt) {
189
+ activePrompt.updates.push(updateParams)
190
+ }
191
+ }
192
+ }
193
+
194
+ const onRequest = async (method: string, params: unknown): Promise<unknown> => {
195
+ if (method === ACP_METHODS.REQUEST_PERMISSION) {
196
+ return handlePermissionRequest(RequestPermissionRequestSchema.parse(params))
197
+ }
198
+
199
+ throw new ACPClientError(`Unknown request method: ${method}`)
200
+ }
201
+
202
+ // --------------------------------------------------------------------------
203
+ // Lifecycle Methods
204
+ // --------------------------------------------------------------------------
205
+
206
+ /**
207
+ * Connects to the agent by spawning the subprocess and initializing the protocol.
208
+ *
209
+ * @returns Initialize result with agent capabilities
210
+ * @throws {ACPClientError} If already connected or connection fails
211
+ */
212
+ const connect = async (): Promise<InitializeResponse> => {
213
+ if (transport?.isConnected()) {
214
+ throw new ACPClientError('Already connected')
215
+ }
216
+
217
+ transport = createACPTransport({
218
+ command,
219
+ cwd,
220
+ env,
221
+ timeout,
222
+ onNotification,
223
+ onRequest,
224
+ onError: (error) => {
225
+ console.error('[ACP Client Error]:', error.message)
226
+ },
227
+ onClose: (code) => {
228
+ // Reject all active prompts on unexpected close
229
+ for (const [sessionId, prompt] of activePrompts) {
230
+ prompt.reject(new ACPClientError(`Agent process exited with code ${code}`))
231
+ activePrompts.delete(sessionId)
232
+ }
233
+ },
234
+ })
235
+
236
+ await transport.start()
237
+
238
+ // Initialize protocol
239
+ const initParams: InitializeRequest = {
240
+ protocolVersion: ACP_PROTOCOL_VERSION,
241
+ clientInfo,
242
+ clientCapabilities: capabilities,
243
+ }
244
+
245
+ initializeResult = await transport.request<InitializeResponse>(ACP_METHODS.INITIALIZE, initParams)
246
+
247
+ agentCapabilities = initializeResult?.agentCapabilities
248
+
249
+ return initializeResult
250
+ }
251
+
252
+ /**
253
+ * Disconnects from the agent, closing the subprocess.
254
+ *
255
+ * @param graceful - If true, sends shutdown request first (default: true)
256
+ */
257
+ const disconnect = async (graceful = true): Promise<void> => {
258
+ if (!transport) return
259
+
260
+ // Cancel all active prompts
261
+ for (const [sessionId, prompt] of activePrompts) {
262
+ prompt.reject(new ACPClientError('Client disconnected'))
263
+ activePrompts.delete(sessionId)
264
+ }
265
+
266
+ await transport.close(graceful)
267
+ transport = undefined
268
+ agentCapabilities = undefined
269
+ initializeResult = undefined
270
+ }
271
+
272
+ // --------------------------------------------------------------------------
273
+ // Session Methods
274
+ // --------------------------------------------------------------------------
275
+
276
+ /**
277
+ * Creates a new conversation session.
278
+ *
279
+ * @param params - Session parameters with working directory and optional MCP servers
280
+ * @returns The created session
281
+ * @throws {ACPClientError} If not connected
282
+ */
283
+ const createSession = async (params: { cwd: string; mcpServers?: McpServer[] }): Promise<Session> => {
284
+ if (!transport?.isConnected()) {
285
+ throw new ACPClientError('Not connected')
286
+ }
287
+
288
+ const response = await transport.request<{ sessionId: string }>(ACP_METHODS.CREATE_SESSION, {
289
+ cwd: params.cwd,
290
+ mcpServers: params.mcpServers ?? [],
291
+ })
292
+ return { id: response.sessionId }
293
+ }
294
+
295
+ /**
296
+ * Sets the model for a session.
297
+ *
298
+ * @experimental This is an unstable ACP feature and may change.
299
+ * @param sessionId - The session ID to set the model for
300
+ * @param modelId - The model ID (e.g., 'claude-3-5-haiku-20241022', 'claude-sonnet-4-20250514')
301
+ * @throws {ACPClientError} If not connected
302
+ */
303
+ const setModel = async (sessionId: string, modelId: string): Promise<void> => {
304
+ if (!transport?.isConnected()) {
305
+ throw new ACPClientError('Not connected')
306
+ }
307
+
308
+ await transport.request(ACP_METHODS.SET_MODEL, { sessionId, modelId })
309
+ }
310
+
311
+ // --------------------------------------------------------------------------
312
+ // Prompt Methods
313
+ // --------------------------------------------------------------------------
314
+
315
+ /**
316
+ * Sends a prompt and streams updates as they arrive.
317
+ *
318
+ * @param sessionId - The session ID to send the prompt to
319
+ * @param content - Content blocks for the prompt
320
+ * @yields Session updates and final completion
321
+ * @throws {ACPClientError} If not connected
322
+ *
323
+ * @remarks
324
+ * Use this for evaluation scenarios where you need access to
325
+ * intermediate updates (tool calls, plan changes, etc).
326
+ */
327
+ async function* prompt(sessionId: string, content: ContentBlock[]): AsyncGenerator<PromptEvent> {
328
+ if (!transport?.isConnected()) {
329
+ throw new ACPClientError('Not connected')
330
+ }
331
+
332
+ const { promise, resolve, reject } = Promise.withResolvers<PromptResponse>()
333
+ const updates: SessionNotification[] = []
334
+ const promptState = {
335
+ updates,
336
+ resolve,
337
+ reject,
338
+ }
339
+
340
+ activePrompts.set(sessionId, promptState)
341
+
342
+ // Send prompt request
343
+ const promptParams: PromptRequest = {
344
+ sessionId,
345
+ prompt: content,
346
+ }
347
+
348
+ // Start the prompt request (don't await - we'll poll for updates)
349
+ const promptPromise = transport
350
+ .request<PromptResponse>(ACP_METHODS.PROMPT, promptParams)
351
+ .then(resolve)
352
+ .catch(reject)
353
+
354
+ try {
355
+ // Poll for updates until prompt completes
356
+ let lastYieldedIndex = 0
357
+
358
+ while (true) {
359
+ // Yield any new updates
360
+ while (lastYieldedIndex < promptState.updates.length) {
361
+ const update = promptState.updates[lastYieldedIndex]
362
+ if (update) {
363
+ yield { type: 'update', params: update }
364
+ }
365
+ lastYieldedIndex++
366
+ }
367
+
368
+ // Check if prompt completed
369
+ const raceResult = await Promise.race([
370
+ promise.then((result) => ({ done: true as const, result })),
371
+ new Promise<{ done: false }>((res) => setTimeout(() => res({ done: false }), pollingInterval)),
372
+ ])
373
+
374
+ if (raceResult.done) {
375
+ // Yield any remaining updates
376
+ while (lastYieldedIndex < promptState.updates.length) {
377
+ const update = promptState.updates[lastYieldedIndex]
378
+ if (update) {
379
+ yield { type: 'update', params: update }
380
+ }
381
+ lastYieldedIndex++
382
+ }
383
+
384
+ // Yield completion
385
+ yield {
386
+ type: 'complete',
387
+ result: raceResult.result,
388
+ }
389
+ break
390
+ }
391
+ }
392
+
393
+ await promptPromise
394
+ } finally {
395
+ activePrompts.delete(sessionId)
396
+ }
397
+ }
398
+
399
+ /**
400
+ * Sends a prompt and waits for the final result.
401
+ *
402
+ * @param sessionId - The session ID to send the prompt to
403
+ * @param content - Content blocks for the prompt
404
+ * @returns The prompt result with all accumulated updates
405
+ * @throws {ACPClientError} If not connected
406
+ *
407
+ * @remarks
408
+ * Use this for simple evaluation scenarios where you only need
409
+ * the final result. All intermediate updates are collected but
410
+ * returned together at the end.
411
+ */
412
+ const promptSync = async (
413
+ sessionId: string,
414
+ content: ContentBlock[],
415
+ ): Promise<{
416
+ result: PromptResponse
417
+ updates: SessionNotification[]
418
+ }> => {
419
+ const updates: SessionNotification[] = []
420
+ let result: PromptResponse | undefined
421
+
422
+ for await (const event of prompt(sessionId, content)) {
423
+ if (event.type === 'update') {
424
+ updates.push(event.params)
425
+ } else if (event.type === 'complete') {
426
+ result = event.result
427
+ }
428
+ }
429
+
430
+ if (!result) {
431
+ throw new ACPClientError('Prompt completed without result')
432
+ }
433
+
434
+ return { result, updates }
435
+ }
436
+
437
+ /**
438
+ * Cancels an ongoing prompt.
439
+ *
440
+ * @param sessionId - The session ID to cancel
441
+ * @throws {ACPClientError} If not connected
442
+ */
443
+ const cancelPrompt = async (sessionId: string): Promise<void> => {
444
+ if (!transport?.isConnected()) {
445
+ throw new ACPClientError('Not connected')
446
+ }
447
+
448
+ const cancelParams: CancelNotification = { sessionId }
449
+ await transport.notify(ACP_METHODS.CANCEL, cancelParams)
450
+ }
451
+
452
+ // --------------------------------------------------------------------------
453
+ // State Methods
454
+ // --------------------------------------------------------------------------
455
+
456
+ /**
457
+ * Gets the agent capabilities negotiated during initialization.
458
+ *
459
+ * @returns Agent capabilities or undefined if not connected
460
+ */
461
+ const getCapabilities = (): AgentCapabilities | undefined => {
462
+ return agentCapabilities
463
+ }
464
+
465
+ /**
466
+ * Gets the full initialization result.
467
+ *
468
+ * @returns Initialize result or undefined if not connected
469
+ */
470
+ const getInitializeResult = (): InitializeResponse | undefined => {
471
+ return initializeResult
472
+ }
473
+
474
+ /**
475
+ * Checks if the client is connected to an agent.
476
+ */
477
+ const isConnected = (): boolean => {
478
+ return transport?.isConnected() ?? false
479
+ }
480
+
481
+ return {
482
+ // Lifecycle
483
+ connect,
484
+ disconnect,
485
+
486
+ // Sessions
487
+ createSession,
488
+ setModel,
489
+
490
+ // Prompts
491
+ prompt,
492
+ promptSync,
493
+ cancelPrompt,
494
+
495
+ // State
496
+ getCapabilities,
497
+ getInitializeResult,
498
+ isConnected,
499
+ }
500
+ }
501
+
502
+ /** Client instance type */
503
+ export type ACPClient = ReturnType<typeof createACPClient>