llm-cli-gateway 2.6.3 → 2.8.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.
package/CHANGELOG.md CHANGED
@@ -4,6 +4,89 @@ All notable changes to the llm-cli-gateway project.
4
4
 
5
5
  ## Unreleased
6
6
 
7
+ ## [2.8.0] - 2026-06-14: HTTP workspace gating and ACP transport
8
+
9
+ ### Added
10
+
11
+ - Added ACP gateway extension foundations, including the contract, config and
12
+ capability surface, and transport core.
13
+ - Added provider CLI release-target evidence for release validation: exact
14
+ probed Claude Code, Codex, Antigravity (`agy`), Grok, and Mistral Vibe
15
+ versions plus artifact SHA-256 values.
16
+ - Added Gemini, Grok, and Mistral pricing families and Codex token/cache usage
17
+ telemetry extraction.
18
+
19
+ ### Changed
20
+
21
+ - HTTP and tunnel provider execution now uses explicit request transport
22
+ metadata and requires a registered workspace, default workspace, or session
23
+ workspace before spawning a provider across sync/async request tools and
24
+ `codex_fork_session`. Local stdio keeps the prior unrestricted
25
+ `workingDir`/`addDir` behavior.
26
+ - Replaced the OAuth-only remote workspace guard with transport-based HTTP
27
+ workspace gating, so bearer, OAuth, auth-disabled, and configured no-auth
28
+ connector HTTP paths all fail closed before provider spawn.
29
+ - Replaced CodeQL with the OSS SAST workflow using OpenGrep, gosec, and
30
+ govulncheck.
31
+
32
+ ### Fixed
33
+
34
+ - Corrected Claude default pricing/cache classification and Grok API pricing
35
+ bucket handling.
36
+ - Preserved captured async orphan readbacks as completed flight-recorder rows
37
+ when stdout was captured and no failure was recorded.
38
+
39
+ ### Documentation
40
+
41
+ - Documented Grok CLI prompt-surface telemetry limitations.
42
+ - Documented ACP gateway extension research, design, and smoke validation.
43
+
44
+ ## [2.7.0] - 2026-06-12: Provider capability inventory
45
+
46
+ ### Added
47
+
48
+ - Added `provider_tool_capabilities`, a read-only MCP tool that reports the
49
+ gateway request tools, provider kind, supported controls, feature flags,
50
+ model info, config-surface hints, local skill discovery, provider-native tool
51
+ discovery, unsupported/degraded inputs, warnings, and cache metadata for
52
+ Claude Code, Codex CLI, Gemini/Antigravity (`agy`), Grok CLI, optional
53
+ `grok_api`, and Mistral Vibe.
54
+ - Added `provider-tools://catalog` and per-provider resources
55
+ `provider-tools://claude`, `provider-tools://codex`,
56
+ `provider-tools://gemini`, `provider-tools://grok`,
57
+ `provider-tools://grok_api`, and `provider-tools://mistral` for clients that
58
+ prefer resource discovery over tool calls.
59
+ - Added `doctor --json` `provider_capabilities`, a compact setup-assistant
60
+ summary with schema version, resource URIs, per-provider request tools,
61
+ supported feature names, unsupported input names, config-surface counts,
62
+ discovered skill/tool counts, and warnings without raw local paths.
63
+ - Added bounded, redacted local skill/config discovery for provider capability
64
+ reporting. Grok local/bundled skills can now surface provider-native tools
65
+ such as Imagine `image_gen`, `image_edit`, `image_to_video`, and
66
+ `reference_to_video` when present, while keeping execution routed through
67
+ `grok_request`.
68
+
69
+ ### Changed
70
+
71
+ - Updated agent skills so LLM agents have provider-specific gateway usage
72
+ guidance for Claude, Codex, Gemini/Antigravity (`agy`), Grok, and Mistral
73
+ Vibe, and so orchestration skills check `provider_tool_capabilities` before
74
+ assuming tool allowlists, MCP-server semantics, sessions, output formats, or
75
+ provider-native tools.
76
+ - Updated setup assistant guidance and `setup/status.schema.json` so install
77
+ agents treat `doctor.provider_capabilities` as the compact source of truth
78
+ for outbound provider capability claims.
79
+ - Documented the intentionally published prod-only `npm-shrinkwrap.json` in
80
+ README and `socket.yml`, including the release audit and packed-consumer
81
+ checks that bound the shrinkwrap and shell-spawn Socket alerts.
82
+
83
+ ### Fixed
84
+
85
+ - Corrected stale internal skill guidance that described Codex continuity as
86
+ bookkeeping-only, described Gemini allowlists/MCP allowlists as pass-through,
87
+ omitted Mistral async from async orchestration docs, or encouraged copying
88
+ Claude tool names into other provider allowlists.
89
+
7
90
  ## [2.6.3] - 2026-06-12: Claude cache-control veracity and Grok 0.2.50
8
91
 
9
92
  ### Fixed
package/README.md CHANGED
@@ -45,7 +45,7 @@ Or use directly with `npx` from an MCP client:
45
45
  - Can run requests inside gateway-managed git worktrees for isolated multi-agent review and implementation loops.
46
46
  - Ships personal-appliance setup surfaces: HTTP transport with bearer-token auth, `doctor --json`, setup UI artifacts, provider setup snippets, Docker fallback, and checked release bundles.
47
47
  - Remote web connectors use MCP OAuth discovery and authorization-code setup with static client or shared-secret gates. Client secrets are generated locally, stored only as hashes, and printed only by explicit copy-once commands.
48
- - Provider CLI requests can select registered workspaces by alias via `workspace`; remote requests should use aliases, not arbitrary filesystem paths. New local folder/Git workspaces can be created only under configured allowed roots.
48
+ - Provider CLI requests can select registered workspaces by alias via `workspace`; every HTTP/tunnel request must use a registered alias, session workspace, or `[workspaces].default` before provider execution. Local unrestricted filesystem access is the stdio transport.
49
49
 
50
50
  ## Workflow Assets
51
51
 
@@ -62,6 +62,8 @@ The next documentation focus is provider-specific skill and DAG-TOML pairs for e
62
62
  - Security CI runs actionlint, zizmor, shellcheck, typos, osv-scanner, gitleaks, and lychee.
63
63
  - GitHub release installer artifacts are checksummed and signed with Sigstore keyless signing.
64
64
  - npm releases use provenance through OIDC trusted publishing.
65
+ - The npm package intentionally ships a generated, prod-only `npm-shrinkwrap.json` so registry installs resolve the audited release tree. Release gates regenerate it from `package-lock.json`, compare for parity, and run a registry-fidelity consumer install before publishing.
66
+ - Socket behavioural alerts are documented in [`socket.yml`](./socket.yml) and under "Security Considerations" below. `shellAccess` and `shrinkwrap` are reviewed package capabilities/configuration for this CLI appliance, not hidden install behaviour.
65
67
 
66
68
  ## Personal MCP Appliance
67
69
 
@@ -167,6 +169,7 @@ docker compose -f docker/personal.compose.yml run --rm doctor
167
169
  - **SQLite Flight Recorder**: Every request/response logged to `~/.llm-cli-gateway/logs.db` with correlation IDs, token usage, duration, retry counts, and circuit breaker state. Browse with [Datasette](https://datasette.io/): `datasette ~/.llm-cli-gateway/logs.db`
168
170
  - **Structured Metadata**: Tool responses include machine-readable `structuredContent` (model, cli, correlationId, sessionId, durationMs, token counts)
169
171
  - **Cache observability resources**: `cache-state://global`, `cache-state://session/{id}`, and `cache-state://prefix/{hash}` MCP resources return aggregate cache hit/miss/savings — tokens and hashes only, no prompt text. `session_get` includes a `cacheState` block when the session has prior requests.
172
+ - **Provider capability inventory**: `provider_tool_capabilities` and `provider-tools://catalog` expose the gateway request fields, supported/degraded provider controls, local skill/tool discovery, and safe config-surface hints for Claude Code, Codex CLI, Gemini/Antigravity, Grok CLI/API, and Mistral Vibe. `doctor --json` includes a compact `provider_capabilities` summary for setup assistants.
170
173
 
171
174
  ### Cache-aware operation
172
175
 
@@ -334,6 +337,8 @@ For clients that already support local stdio MCP servers, add a configuration li
334
337
  }
335
338
  ```
336
339
 
340
+ Stdio is the recommended path for unrestricted machine-local development access. HTTP MCP, including localhost HTTP and tunneled HTTPS, is treated as remote-capable for provider execution: provider tools must resolve a registered workspace alias, a session workspace, or `[workspaces].default` before spawning a CLI. Remote clients should pass relative `workingDir`, `addDir`, and include-directory values inside the selected workspace; disabling auth or using a no-auth connector path is not a filesystem bypass.
341
+
337
342
  This generic stdio example is not provider-support verification for the Personal MCP Appliance. Client-specific setup guides for ChatGPT, Claude web, Claude Desktop, Codex, Gemini CLI, Gemini web, and Grok remain gated by the provider-support matrix in [docs/personal-mcp/PRODUCT_CONTRACT.md](docs/personal-mcp/PRODUCT_CONTRACT.md).
338
343
 
339
344
  ### Available Tools
@@ -396,6 +401,8 @@ Execute a Claude Code request with optional session management.
396
401
  - `promptParts` (object, optional): Cache-aware structured prompt `{ system?, tools?, context?, task }`; mutually exclusive with `prompt`
397
402
  - `forceRefresh` (boolean, optional): Bypass dedup and force a fresh CLI run, default: false
398
403
 
404
+ Workspace boundary: stdio callers may use machine-local paths directly. HTTP/tunnel callers must pass `workspace` or rely on a configured default/session workspace; path fields are then validated relative to that workspace. `[workspaces].allow_unregistered_working_dir` is a stdio/local legacy setting and does not allow arbitrary HTTP working directories or additional directories.
405
+
399
406
  **Response extras:**
400
407
 
401
408
  - `approval`: Approval decision record when `approvalStrategy="mcp_managed"`
@@ -1019,6 +1026,42 @@ GEMINI_HISTORY_ROOT=/path/to/.gemini/tmp
1019
1026
  LLM_GATEWAY_DISABLE_MODEL_DISCOVERY=1
1020
1027
  ```
1021
1028
 
1029
+ ##### `provider_tool_capabilities`
1030
+
1031
+ Report the provider tool and feature capability catalog. Use this before
1032
+ orchestrating provider-specific requests so callers can distinguish supported
1033
+ controls, provider-owned configuration, ignored parity fields, and unsupported
1034
+ inputs.
1035
+
1036
+ **Parameters:**
1037
+
1038
+ - `cli` (string, optional): Provider filter (`"claude"`, `"codex"`, `"gemini"`, `"grok"`, `"grok_api"`, or `"mistral"`)
1039
+ - `includeSkills` (boolean, default `true`): Include bounded local skill discovery
1040
+ - `includeProviderTools` (boolean, default `true`): Include provider-native tools extracted from discovered skills
1041
+ - `includeUnsupported` (boolean, default `true`): Include explicit unsupported/degraded input records
1042
+ - `includePaths` (boolean, default `false`): Include raw local filesystem paths in discovery output
1043
+ - `refresh` (boolean, default `false`): Bypass the short-lived capability cache
1044
+
1045
+ The response schema is `provider-tool-capabilities.v2`. Capability discovery is
1046
+ read-only and bounded; raw local paths are redacted unless `includePaths` is
1047
+ explicitly true, and secret-bearing auth files are not read.
1048
+
1049
+ Equivalent MCP resources:
1050
+
1051
+ - `provider-tools://catalog`: full provider catalog
1052
+ - `provider-tools://claude`
1053
+ - `provider-tools://codex`
1054
+ - `provider-tools://gemini`
1055
+ - `provider-tools://grok`
1056
+ - `provider-tools://grok_api`
1057
+ - `provider-tools://mistral`
1058
+
1059
+ `doctor --json` also emits a compact `provider_capabilities` block with the
1060
+ same schema version, per-provider request tool names, supported feature names,
1061
+ unsupported input names, config-surface counts, discovery counts, and resource
1062
+ URIs. This block is intended for setup assistants that need a concise capability
1063
+ summary without local skill bodies or raw paths.
1064
+
1022
1065
  ##### `cli_versions`
1023
1066
 
1024
1067
  Report installed CLI versions.
@@ -1299,15 +1342,18 @@ The gateway supports concurrent requests across different CLIs. Each request spa
1299
1342
 
1300
1343
  ### Socket alerts — context for reviewers
1301
1344
 
1302
- If you're vetting `llm-cli-gateway` through [Socket](https://socket.dev/npm/package/llm-cli-gateway) or a similar supply-chain scanner, you'll see behavioural alerts and some dependency-ownership alerts. They are accurate descriptions of what the package does and what it depends on. The reviewed `shellAccess` capability is configured in `socket.yml` for repository/PR policy surfaces, but Socket's public package page may still display it for the published npm artifact; the rationale remains documented here and in the package.
1345
+ If you're vetting `llm-cli-gateway` through [Socket](https://socket.dev/npm/package/llm-cli-gateway) or a similar supply-chain scanner, you'll see behavioural alerts and some dependency-ownership alerts. They are accurate descriptions of what the package does and what it depends on. The reviewed `shellAccess` and `shrinkwrap` entries are configured in `socket.yml` for repository/PR policy surfaces, but Socket's public package page may still display them for the published npm artifact; the rationale remains documented here and in the package.
1346
+
1347
+ The currently flagged surfaces are not new in 2.6.x: the 2.3.0, 2.4.0, 2.5.0, and 2.6.3 npm tarballs all include `npm-shrinkwrap.json`, and all include the same `dist/executor.js` child-process spawn surface used to run provider CLIs. The `socket.yml` policy for 2.4.0, 2.5.0, 2.6.0, and 2.6.3 is materially the same for `shellAccess`; this README now adds the missing shrinkwrap disclosure as well.
1303
1348
 
1304
- | Alert | Where | Why it's bounded |
1305
- | -------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
1306
- | **Network access** | `src/http-transport.ts` opens an HTTP MCP transport when started via `npm run start:http`. `src/endpoint-exposure.ts` issues a HEAD probe to verify configured public/tunnel URLs. Socket also flagged `dist/upstream-contracts.js` in v1.17.2 from descriptive text, not a network call. | The transport binds to `127.0.0.1` by default and requires `LLM_GATEWAY_AUTH_TOKEN` to be set. The default stdio MCP entry point (`npm start`) opens no sockets. `src/upstream-contracts.ts` stores provider CLI metadata and imports no HTTP client APIs. |
1307
- | **Shell access** | `src/executor.ts` uses `child_process.spawn(cmd, args, …)` to invoke the underlying LLM CLIs. | `spawn` is called with an argument array and **never** `shell: true`, so there is no shell interpolation path for caller input. The command name is restricted to an allow-list of known CLI binaries (`claude`, `codex`, `agy`, `grok`, `vibe`). |
1308
- | **Uses eval** | None in our source. Transitive: `@modelcontextprotocol/sdk` `ajv@8` uses `new Function(...)` in `ajv/dist/compile/index.js` to compile JSON Schema validators. | This is ajv's standard codegen path. Only known schemas (defined in our source and the MCP SDK) flow into it; no caller-supplied data ever reaches the compiled function body. |
1309
- | **SQLite adapter isolation** | Persistence uses Node's built-in `node:sqlite` module (no native binding, no install scripts) through a single adapter, `src/sqlite-driver.ts`. | `node:sqlite` is touched by exactly one production module (the adapter); every other module talks to SQLite through its typed surface. We never call any `db.pragma()` helper (it does not exist on `node:sqlite`); SQLite setup uses fixed literal `db.exec("PRAGMA ...")` statements. `npm run security:audit` fails the release if production code references `node:sqlite` outside the adapter or reintroduces a `.pragma()` call. |
1310
- | **Dependency ownership** | A handful of small transitive packages (e.g. `media-typer` via `@modelcontextprotocol/sdk`) trip Socket's "unstable ownership" or "obfuscated code" heuristics. | These are pinned, well-known micro-deps in the Node ecosystem with no known issues. We pin direct override versions of `content-type` and `type-is` in `package.json#overrides`. As of 2.0.0 the prod graph carries no native module (`better-sqlite3` moved to devDependencies; `node:sqlite` is built into Node), eliminating the entire `prebuild-install`/`tar-fs`/`tar-stream` install-time chain. Our earlier direct dependency on `toml@3.0.0` was replaced with `smol-toml`. |
1349
+ | Alert | Where | Why it's bounded |
1350
+ | ---------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
1351
+ | **Network access** | `src/http-transport.ts` opens an HTTP MCP transport when started via `npm run start:http`. `src/endpoint-exposure.ts` issues a HEAD probe to verify configured public/tunnel URLs. Socket also flagged `dist/upstream-contracts.js` in v1.17.2 from descriptive text, not a network call. | The transport binds to `127.0.0.1` by default and requires `LLM_GATEWAY_AUTH_TOKEN` to be set. The default stdio MCP entry point (`npm start`) opens no sockets. `src/upstream-contracts.ts` stores provider CLI metadata and imports no HTTP client APIs. |
1352
+ | **Shell access** | `src/executor.ts` uses `child_process.spawn(cmd, args, …)` to invoke the underlying LLM CLIs. | `spawn` is called with an argument array and **never** `shell: true`, so there is no shell interpolation path for caller input. The command name is restricted to an allow-list of known CLI binaries (`claude`, `codex`, `agy`, `grok`, `vibe`). |
1353
+ | **Published shrinkwrap** | The npm artifact includes `npm-shrinkwrap.json`; `package.json#files` includes it and `scripts/make-prod-shrinkwrap.mjs` generates it from `package-lock.json`. | This is a CLI/application package. npm documents the shrinkwrap use case for applications, daemons, and command-line tools published through the registry. Our shrinkwrap is a prod-only projection, not a committed full dev lockfile: `scripts/release-security-audit.sh` verifies parity with the audited lockfile, and `scripts/verify-registry-install.sh` proves fresh registry consumers receive no `better-sqlite3`/`prebuild-install`/`tar-fs`/`tar-stream` production chain. |
1354
+ | **Uses eval** | None in our source. Transitive: `@modelcontextprotocol/sdk` `ajv@8` uses `new Function(...)` in `ajv/dist/compile/index.js` to compile JSON Schema validators. | This is ajv's standard codegen path. Only known schemas (defined in our source and the MCP SDK) flow into it; no caller-supplied data ever reaches the compiled function body. |
1355
+ | **SQLite adapter isolation** | Persistence uses Node's built-in `node:sqlite` module (no native binding, no install scripts) through a single adapter, `src/sqlite-driver.ts`. | `node:sqlite` is touched by exactly one production module (the adapter); every other module talks to SQLite through its typed surface. We never call any `db.pragma()` helper (it does not exist on `node:sqlite`); SQLite setup uses fixed literal `db.exec("PRAGMA ...")` statements. `npm run security:audit` fails the release if production code references `node:sqlite` outside the adapter or reintroduces a `.pragma()` call. |
1356
+ | **Dependency ownership** | A handful of small transitive packages (e.g. `media-typer` via `@modelcontextprotocol/sdk`) trip Socket's "unstable ownership" or "obfuscated code" heuristics. | These are pinned, well-known micro-deps in the Node ecosystem with no known issues. We pin direct override versions of `content-type` and `type-is` in `package.json#overrides`. As of 2.0.0 the prod graph carries no native module (`better-sqlite3` moved to devDependencies; `node:sqlite` is built into Node), eliminating the entire `prebuild-install`/`tar-fs`/`tar-stream` install-time chain. Our earlier direct dependency on `toml@3.0.0` was replaced with `smol-toml`. |
1311
1357
 
1312
1358
  See [`socket.yml`](./socket.yml) for the same context in machine-readable form.
1313
1359
 
@@ -0,0 +1,78 @@
1
+ import { AcpError } from "./errors.js";
2
+ import type { JsonRpcStdioTransport, JsonRpcId } from "./json-rpc-stdio.js";
3
+ import { type ContentBlock, type InitializeResponse, type ReadTextFileRequest, type ReadTextFileResponse, type RequestPermissionRequest, type RequestPermissionResponse, type SessionLoadResponse, type SessionNewResponse, type SessionPromptResponse, type SessionUpdateNotification, type WriteTextFileRequest, type WriteTextFileResponse } from "./types.js";
4
+ import type { Logger } from "../logger.js";
5
+ import type { CliType } from "../session-manager.js";
6
+ export declare const DEFAULT_ACP_PROTOCOL_VERSION = 1;
7
+ export interface HostServices {
8
+ readTextFile?(request: ReadTextFileRequest, context: HostCallbackContext): Promise<ReadTextFileResponse>;
9
+ writeTextFile?(request: WriteTextFileRequest, context: HostCallbackContext): Promise<WriteTextFileResponse>;
10
+ requestPermission?(request: RequestPermissionRequest, context: HostCallbackContext): Promise<RequestPermissionResponse>;
11
+ }
12
+ export interface HostCallbackContext {
13
+ readonly provider: CliType;
14
+ readonly method: string;
15
+ }
16
+ export interface AcpClientCallbacks {
17
+ readonly onSessionUpdate?: (update: SessionUpdateNotification) => void;
18
+ readonly onProcessExit?: (error: AcpError) => void;
19
+ }
20
+ export interface AcpClientOptions {
21
+ readonly transport: JsonRpcStdioTransport;
22
+ readonly provider: CliType;
23
+ readonly hostServices: HostServices;
24
+ readonly callbacks?: AcpClientCallbacks;
25
+ readonly logger?: Logger;
26
+ readonly protocolVersion?: number;
27
+ readonly timeouts?: AcpClientTimeouts;
28
+ }
29
+ export interface AcpClientTimeouts {
30
+ readonly initializeMs?: number;
31
+ readonly sessionNewMs?: number;
32
+ readonly sessionLoadMs?: number;
33
+ readonly promptMs?: number;
34
+ }
35
+ export interface InitializeOptions {
36
+ readonly readTextFile?: boolean;
37
+ readonly writeTextFile?: boolean;
38
+ readonly terminal?: boolean;
39
+ }
40
+ export interface NewSessionParams {
41
+ readonly cwd: string;
42
+ readonly mcpServers?: ReadonlyArray<Record<string, unknown>>;
43
+ }
44
+ export interface LoadSessionParams extends NewSessionParams {
45
+ readonly sessionId: string;
46
+ }
47
+ export interface PromptParams {
48
+ readonly sessionId: string;
49
+ readonly prompt: ReadonlyArray<ContentBlock>;
50
+ }
51
+ export declare class AcpClient {
52
+ private readonly transport;
53
+ private readonly provider;
54
+ private readonly hostServices;
55
+ private readonly callbacks?;
56
+ private readonly logger;
57
+ private readonly protocolVersion;
58
+ private readonly timeouts;
59
+ private initialized;
60
+ private initializeResult;
61
+ constructor(options: AcpClientOptions);
62
+ get isInitialized(): boolean;
63
+ get agentInfo(): InitializeResponse | null;
64
+ initialize(options?: InitializeOptions): Promise<InitializeResponse>;
65
+ newSession(params: NewSessionParams): Promise<SessionNewResponse>;
66
+ loadSession(params: LoadSessionParams): Promise<SessionLoadResponse>;
67
+ prompt(params: PromptParams): Promise<SessionPromptResponse>;
68
+ cancel(sessionId: string): void;
69
+ private send;
70
+ private normalizeError;
71
+ private assertInitialized;
72
+ handleNotification(method: string, params: unknown): void;
73
+ private handleSessionUpdate;
74
+ handleRequest(id: JsonRpcId, method: string, params: unknown): void;
75
+ private dispatchRequest;
76
+ private requireHandler;
77
+ notifyProcessExit(error: AcpError): void;
78
+ }
@@ -0,0 +1,201 @@
1
+ import { AcpProtocolError, isAcpError } from "./errors.js";
2
+ import { parseInitializeResponse, parseReadTextFileRequest, parseRequestPermissionRequest, parseSessionLoadResponse, parseSessionNewResponse, parseSessionPromptResponse, parseSessionUpdateNotification, parseWriteTextFileRequest, } from "./types.js";
3
+ import { noopLogger } from "../logger.js";
4
+ export const DEFAULT_ACP_PROTOCOL_VERSION = 1;
5
+ export class AcpClient {
6
+ transport;
7
+ provider;
8
+ hostServices;
9
+ callbacks;
10
+ logger;
11
+ protocolVersion;
12
+ timeouts;
13
+ initialized = false;
14
+ initializeResult = null;
15
+ constructor(options) {
16
+ this.transport = options.transport;
17
+ this.provider = options.provider;
18
+ this.hostServices = options.hostServices;
19
+ this.callbacks = options.callbacks;
20
+ this.logger = options.logger ?? noopLogger;
21
+ this.protocolVersion = options.protocolVersion ?? DEFAULT_ACP_PROTOCOL_VERSION;
22
+ this.timeouts = options.timeouts ?? {};
23
+ }
24
+ get isInitialized() {
25
+ return this.initialized;
26
+ }
27
+ get agentInfo() {
28
+ return this.initializeResult;
29
+ }
30
+ async initialize(options = {}) {
31
+ if (this.initialized && this.initializeResult) {
32
+ return this.initializeResult;
33
+ }
34
+ const params = {
35
+ protocolVersion: this.protocolVersion,
36
+ clientCapabilities: {
37
+ fs: {
38
+ readTextFile: options.readTextFile ?? false,
39
+ writeTextFile: options.writeTextFile ?? false,
40
+ },
41
+ terminal: options.terminal ?? false,
42
+ },
43
+ };
44
+ const raw = await this.send("initialize", params, this.timeouts.initializeMs);
45
+ const result = parseInitializeResponse(raw, this.provider);
46
+ this.initialized = true;
47
+ this.initializeResult = result;
48
+ return result;
49
+ }
50
+ async newSession(params) {
51
+ this.assertInitialized("session/new");
52
+ const raw = await this.send("session/new", { cwd: params.cwd, mcpServers: params.mcpServers ?? [] }, this.timeouts.sessionNewMs);
53
+ return parseSessionNewResponse(raw, this.provider);
54
+ }
55
+ async loadSession(params) {
56
+ this.assertInitialized("session/load");
57
+ const raw = await this.send("session/load", {
58
+ sessionId: params.sessionId,
59
+ cwd: params.cwd,
60
+ mcpServers: params.mcpServers ?? [],
61
+ }, this.timeouts.sessionLoadMs);
62
+ return parseSessionLoadResponse(raw, this.provider);
63
+ }
64
+ async prompt(params) {
65
+ this.assertInitialized("session/prompt");
66
+ const raw = await this.send("session/prompt", { sessionId: params.sessionId, prompt: params.prompt }, this.timeouts.promptMs);
67
+ return parseSessionPromptResponse(raw, this.provider);
68
+ }
69
+ cancel(sessionId) {
70
+ this.transport.notify("session/cancel", { sessionId });
71
+ }
72
+ async send(method, params, timeoutMs) {
73
+ try {
74
+ return await this.transport.request(method, params, timeoutMs);
75
+ }
76
+ catch (err) {
77
+ throw this.normalizeError(method, err);
78
+ }
79
+ }
80
+ normalizeError(method, err) {
81
+ if (isAcpError(err)) {
82
+ return err;
83
+ }
84
+ return new AcpProtocolError(`ACP request ${method} failed unexpectedly.`, {
85
+ provider: this.provider,
86
+ debug: { method, errorClass: err instanceof Error ? err.name : "unknown" },
87
+ });
88
+ }
89
+ assertInitialized(method) {
90
+ if (!this.initialized) {
91
+ throw new AcpProtocolError(`ACP ${method} requires initialize to complete first.`, {
92
+ provider: this.provider,
93
+ debug: { method, reason: "not_initialized" },
94
+ });
95
+ }
96
+ }
97
+ handleNotification(method, params) {
98
+ if (method === "session/update") {
99
+ this.handleSessionUpdate(params);
100
+ return;
101
+ }
102
+ this.logger.debug("acp.client.unknown_notification", {
103
+ provider: this.provider,
104
+ method,
105
+ });
106
+ }
107
+ handleSessionUpdate(params) {
108
+ let parsed;
109
+ try {
110
+ parsed = parseSessionUpdateNotification(params, this.provider);
111
+ }
112
+ catch (err) {
113
+ this.logger.error("acp.client.session_update.parse_error", {
114
+ provider: this.provider,
115
+ errorClass: err instanceof Error ? err.name : "unknown",
116
+ });
117
+ return;
118
+ }
119
+ try {
120
+ this.callbacks?.onSessionUpdate?.(parsed);
121
+ }
122
+ catch (err) {
123
+ this.logger.error("acp.client.session_update.handler_error", {
124
+ provider: this.provider,
125
+ errorClass: err instanceof Error ? err.name : "unknown",
126
+ });
127
+ }
128
+ }
129
+ handleRequest(id, method, params) {
130
+ void this.dispatchRequest(id, method, params);
131
+ }
132
+ async dispatchRequest(id, method, params) {
133
+ const context = { provider: this.provider, method };
134
+ try {
135
+ switch (method) {
136
+ case "fs/read_text_file": {
137
+ const request = parseReadTextFileRequest(params, this.provider);
138
+ const result = await this.requireHandler(this.hostServices.readTextFile, method)(request, context);
139
+ this.transport.respond(id, result);
140
+ return;
141
+ }
142
+ case "fs/write_text_file": {
143
+ const request = parseWriteTextFileRequest(params, this.provider);
144
+ const result = await this.requireHandler(this.hostServices.writeTextFile, method)(request, context);
145
+ this.transport.respond(id, result);
146
+ return;
147
+ }
148
+ case "session/request_permission": {
149
+ const request = parseRequestPermissionRequest(params, this.provider);
150
+ const result = await this.requireHandler(this.hostServices.requestPermission, method)(request, context);
151
+ this.transport.respond(id, result);
152
+ return;
153
+ }
154
+ default: {
155
+ this.logger.debug("acp.client.unknown_host_request", {
156
+ provider: this.provider,
157
+ method,
158
+ });
159
+ this.transport.respondError(id, {
160
+ code: -32601,
161
+ message: "Method not found",
162
+ });
163
+ return;
164
+ }
165
+ }
166
+ }
167
+ catch (err) {
168
+ const acpError = isAcpError(err) ? err : this.normalizeError(method, err);
169
+ this.logger.error("acp.client.host_request.failed", {
170
+ provider: this.provider,
171
+ method,
172
+ errorClass: acpError.name,
173
+ kind: acpError.kind,
174
+ });
175
+ this.transport.respondError(id, {
176
+ code: -32000,
177
+ message: acpError.userMessage,
178
+ });
179
+ }
180
+ }
181
+ requireHandler(handler, method) {
182
+ if (!handler) {
183
+ throw new AcpProtocolError(`Host does not support ACP ${method}.`, {
184
+ provider: this.provider,
185
+ debug: { method, reason: "unsupported_host_method" },
186
+ });
187
+ }
188
+ return handler.bind(this.hostServices);
189
+ }
190
+ notifyProcessExit(error) {
191
+ try {
192
+ this.callbacks?.onProcessExit?.(error);
193
+ }
194
+ catch (err) {
195
+ this.logger.error("acp.client.process_exit.handler_error", {
196
+ provider: this.provider,
197
+ errorClass: err instanceof Error ? err.name : "unknown",
198
+ });
199
+ }
200
+ }
201
+ }
@@ -0,0 +1,63 @@
1
+ import type { CliType } from "../session-manager.js";
2
+ export type AcpErrorKind = "acp_disabled" | "provider_acp_disabled" | "provider_acp_unsupported" | "provider_runtime_disabled" | "provider_unavailable" | "protocol" | "timeout" | "permission_denied" | "process_exit";
3
+ export declare function redactAcpMessage(input: string): string;
4
+ export declare function redactAcpDebug(value: unknown): unknown;
5
+ export declare function redactAcpCause(cause: unknown): unknown;
6
+ export interface AcpErrorDebug {
7
+ readonly [key: string]: unknown;
8
+ }
9
+ export declare class AcpError extends Error {
10
+ readonly kind: AcpErrorKind;
11
+ readonly provider?: CliType;
12
+ readonly debug: AcpErrorDebug;
13
+ constructor(kind: AcpErrorKind, userMessage: string, options?: {
14
+ provider?: CliType;
15
+ debug?: AcpErrorDebug;
16
+ cause?: unknown;
17
+ });
18
+ get userMessage(): string;
19
+ }
20
+ export declare class AcpDisabledError extends AcpError {
21
+ constructor(debug?: AcpErrorDebug);
22
+ }
23
+ export declare class ProviderAcpDisabledError extends AcpError {
24
+ constructor(provider: CliType, debug?: AcpErrorDebug);
25
+ }
26
+ export declare class ProviderAcpUnsupportedError extends AcpError {
27
+ constructor(provider: CliType, debug?: AcpErrorDebug);
28
+ }
29
+ export declare class ProviderRuntimeDisabledError extends AcpError {
30
+ constructor(provider: CliType, debug?: AcpErrorDebug);
31
+ }
32
+ export declare class ProviderUnavailableError extends AcpError {
33
+ constructor(provider: CliType, reason: string, debug?: AcpErrorDebug);
34
+ }
35
+ export declare class AcpProtocolError extends AcpError {
36
+ readonly code?: number;
37
+ constructor(userMessage: string, options?: {
38
+ provider?: CliType;
39
+ code?: number;
40
+ debug?: AcpErrorDebug;
41
+ });
42
+ }
43
+ export declare class AcpTimeoutError extends AcpError {
44
+ readonly method: string;
45
+ readonly timeoutMs: number;
46
+ constructor(method: string, timeoutMs: number, options?: {
47
+ provider?: CliType;
48
+ debug?: AcpErrorDebug;
49
+ });
50
+ }
51
+ export declare class AcpPermissionDeniedError extends AcpError {
52
+ constructor(provider: CliType, reason: string, debug?: AcpErrorDebug);
53
+ }
54
+ export declare class AcpProcessExitError extends AcpError {
55
+ readonly exitCode: number | null;
56
+ readonly signal: string | null;
57
+ constructor(provider: CliType, options?: {
58
+ exitCode?: number | null;
59
+ signal?: string | null;
60
+ debug?: AcpErrorDebug;
61
+ });
62
+ }
63
+ export declare function isAcpError(value: unknown): value is AcpError;