llm-cli-gateway 2.7.0 → 2.9.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 (54) hide show
  1. package/CHANGELOG.md +82 -0
  2. package/README.md +28 -1
  3. package/dist/acp/client.d.ts +78 -0
  4. package/dist/acp/client.js +201 -0
  5. package/dist/acp/errors.d.ts +63 -0
  6. package/dist/acp/errors.js +139 -0
  7. package/dist/acp/json-rpc-stdio.d.ts +71 -0
  8. package/dist/acp/json-rpc-stdio.js +375 -0
  9. package/dist/acp/process-manager.d.ts +66 -0
  10. package/dist/acp/process-manager.js +364 -0
  11. package/dist/acp/provider-registry.d.ts +24 -0
  12. package/dist/acp/provider-registry.js +82 -0
  13. package/dist/acp/types.d.ts +557 -0
  14. package/dist/acp/types.js +335 -0
  15. package/dist/approval-manager.d.ts +1 -0
  16. package/dist/approval-manager.js +14 -1
  17. package/dist/async-job-manager.d.ts +3 -0
  18. package/dist/async-job-manager.js +56 -16
  19. package/dist/auth.d.ts +4 -0
  20. package/dist/auth.js +16 -0
  21. package/dist/cache-stats.d.ts +1 -0
  22. package/dist/cache-stats.js +19 -11
  23. package/dist/cli-updater.js +5 -2
  24. package/dist/codex-json-parser.d.ts +3 -0
  25. package/dist/codex-json-parser.js +17 -0
  26. package/dist/config.d.ts +30 -0
  27. package/dist/config.js +140 -0
  28. package/dist/flight-recorder.d.ts +7 -1
  29. package/dist/flight-recorder.js +33 -6
  30. package/dist/http-transport.js +21 -18
  31. package/dist/index.js +104 -34
  32. package/dist/job-store.d.ts +4 -0
  33. package/dist/job-store.js +16 -4
  34. package/dist/oauth.d.ts +2 -0
  35. package/dist/oauth.js +90 -8
  36. package/dist/pricing.d.ts +1 -1
  37. package/dist/pricing.js +67 -2
  38. package/dist/provider-tool-capabilities.d.ts +38 -0
  39. package/dist/provider-tool-capabilities.js +142 -0
  40. package/dist/request-context.d.ts +4 -0
  41. package/dist/request-context.js +16 -0
  42. package/dist/request-helpers.d.ts +4 -4
  43. package/dist/request-limits.d.ts +8 -0
  44. package/dist/request-limits.js +49 -0
  45. package/dist/secret-redaction.d.ts +3 -0
  46. package/dist/secret-redaction.js +53 -0
  47. package/dist/session-manager-pg.js +8 -5
  48. package/dist/session-manager.d.ts +1 -0
  49. package/dist/session-manager.js +2 -0
  50. package/dist/upstream-contracts.d.ts +27 -0
  51. package/dist/upstream-contracts.js +131 -0
  52. package/migrations/004_session_owner_principal.sql +10 -0
  53. package/npm-shrinkwrap.json +2 -2
  54. package/package.json +1 -1
@@ -0,0 +1,364 @@
1
+ import { spawn as nodeSpawn } from "node:child_process";
2
+ import { mkdirSync } from "node:fs";
3
+ import { tmpdir } from "node:os";
4
+ import { AcpClient } from "./client.js";
5
+ import { AcpError, AcpProcessExitError, ProviderUnavailableError } from "./errors.js";
6
+ import { JsonRpcStdioTransport, } from "./json-rpc-stdio.js";
7
+ import { getAcpProviderEntry } from "./provider-registry.js";
8
+ import { envWithExtendedPath, getExtendedPath } from "../executor.js";
9
+ import { noopLogger } from "../logger.js";
10
+ const SHELL_METACHARACTERS = /[\s|&;<>(){}$`"'\\*?[\]~#! ]/;
11
+ function assertSafeExecutable(command, provider) {
12
+ if (command.length === 0 || SHELL_METACHARACTERS.test(command)) {
13
+ throw new ProviderUnavailableError(provider, "configured ACP command is not a bare executable (shell metacharacters are not allowed)", { provider });
14
+ }
15
+ }
16
+ export function buildProviderEnv(provider, providerConfig, baseEnv) {
17
+ const env = envWithExtendedPath(baseEnv, getExtendedPath());
18
+ if (provider === "grok" && providerConfig.isolatedLeaderSocket) {
19
+ const socketDir = tmpdir();
20
+ const socketName = `grok-acp-leader-${process.pid}-${Date.now()}.sock`;
21
+ env.GROK_LEADER_SOCKET = `${socketDir}/${socketName}`;
22
+ }
23
+ return env;
24
+ }
25
+ export function resolveProviderSpawn(provider, config, baseEnv, cwd) {
26
+ const providerConfig = config.providers[provider];
27
+ const registryEntry = getAcpProviderEntry(provider);
28
+ let command;
29
+ let args;
30
+ if (providerConfig && providerConfig.command.length > 0) {
31
+ command = providerConfig.command;
32
+ args = providerConfig.args;
33
+ }
34
+ else if (registryEntry.entrypoint) {
35
+ command = registryEntry.entrypoint.command;
36
+ args = registryEntry.entrypoint.args;
37
+ }
38
+ else {
39
+ throw new ProviderUnavailableError(provider, "no ACP entrypoint configured and provider has no native ACP entrypoint", { provider });
40
+ }
41
+ assertSafeExecutable(command, provider);
42
+ for (const arg of args) {
43
+ if (typeof arg !== "string") {
44
+ throw new ProviderUnavailableError(provider, "ACP args must be strings", { provider });
45
+ }
46
+ }
47
+ const effectiveConfig = providerConfig ?? {
48
+ enabled: false,
49
+ command,
50
+ args: [...args],
51
+ runtimeEnabled: false,
52
+ isolatedLeaderSocket: false,
53
+ };
54
+ return {
55
+ command,
56
+ args: [...args],
57
+ cwd: cwd ?? `${tmpdir()}/llm-gateway-acp-${provider}`,
58
+ env: buildProviderEnv(provider, effectiveConfig, baseEnv),
59
+ };
60
+ }
61
+ export const defaultSpawn = (resolved) => {
62
+ mkdirSync(resolved.cwd, { recursive: true });
63
+ const child = nodeSpawn(resolved.command, [...resolved.args], {
64
+ cwd: resolved.cwd,
65
+ env: resolved.env,
66
+ shell: false,
67
+ stdio: ["pipe", "pipe", "pipe"],
68
+ windowsHide: true,
69
+ });
70
+ return child;
71
+ };
72
+ export class AcpProcessManager {
73
+ config;
74
+ logger;
75
+ spawnFn;
76
+ baseEnv;
77
+ live = new Set();
78
+ constructor(options) {
79
+ this.config = options.config;
80
+ this.logger = options.logger ?? noopLogger;
81
+ this.spawnFn = options.spawn ?? defaultSpawn;
82
+ this.baseEnv = options.baseEnv ?? process.env;
83
+ }
84
+ async start(options) {
85
+ const resolved = resolveProviderSpawn(options.provider, this.config, this.baseEnv, options.cwd);
86
+ let child;
87
+ try {
88
+ child = this.spawnFn(resolved);
89
+ }
90
+ catch (err) {
91
+ this.logger.error("acp.process.spawn.failed", {
92
+ provider: options.provider,
93
+ errorClass: err instanceof Error ? err.name : "unknown",
94
+ });
95
+ throw new ProviderUnavailableError(options.provider, "failed to spawn ACP process", {
96
+ provider: options.provider,
97
+ });
98
+ }
99
+ if (!child.stdin || !child.stdout) {
100
+ try {
101
+ child.kill("SIGKILL");
102
+ }
103
+ catch {
104
+ }
105
+ throw new ProviderUnavailableError(options.provider, "ACP process did not expose stdin/stdout pipes", { provider: options.provider });
106
+ }
107
+ this.logger.info("acp.process.spawn", {
108
+ provider: options.provider,
109
+ pid: child.pid,
110
+ });
111
+ const managed = new ManagedProcessImpl({
112
+ provider: options.provider,
113
+ child,
114
+ resolved,
115
+ logger: this.logger,
116
+ hostServices: options.hostServices,
117
+ callbacks: options.callbacks,
118
+ idleTimeoutMs: options.idleTimeoutMs ?? this.config.processIdleTimeoutMs,
119
+ initializeTimeoutMs: this.config.initializeTimeoutMs,
120
+ onTerminal: m => this.live.delete(m),
121
+ });
122
+ this.live.add(managed);
123
+ try {
124
+ await managed.initialize();
125
+ }
126
+ catch (err) {
127
+ managed.shutdown("SIGKILL");
128
+ this.live.delete(managed);
129
+ throw err instanceof AcpError
130
+ ? err
131
+ : new ProviderUnavailableError(options.provider, "ACP initialize failed", {
132
+ provider: options.provider,
133
+ });
134
+ }
135
+ return managed;
136
+ }
137
+ shutdownAll(signal = "SIGTERM") {
138
+ for (const managed of [...this.live]) {
139
+ managed.shutdown(signal);
140
+ }
141
+ this.live.clear();
142
+ }
143
+ get liveCount() {
144
+ return this.live.size;
145
+ }
146
+ }
147
+ class ManagedProcessImpl {
148
+ provider;
149
+ pid;
150
+ transport;
151
+ client;
152
+ resolved;
153
+ child;
154
+ logger;
155
+ callbacks;
156
+ idleTimeoutMs;
157
+ onTerminal;
158
+ _state = "starting";
159
+ _exitCode = null;
160
+ _signal = null;
161
+ _terminalError = null;
162
+ idleTimer = null;
163
+ constructor(options) {
164
+ this.provider = options.provider;
165
+ this.child = options.child;
166
+ this.pid = options.child.pid;
167
+ this.resolved = options.resolved;
168
+ this.logger = options.logger;
169
+ this.callbacks = options.callbacks;
170
+ this.idleTimeoutMs = options.idleTimeoutMs;
171
+ this.onTerminal = options.onTerminal;
172
+ this.transport = new JsonRpcStdioTransport({
173
+ streams: {
174
+ stdin: options.child.stdin,
175
+ stdout: options.child.stdout,
176
+ stderr: options.child.stderr ?? null,
177
+ },
178
+ logger: this.logger,
179
+ provider: this.provider,
180
+ onNotification: (n) => {
181
+ this.client.handleNotification(n.method, n.params);
182
+ },
183
+ onRequest: (r) => {
184
+ this.client.handleRequest(r.id, r.method, r.params);
185
+ },
186
+ onActivity: () => this.touchIdle(),
187
+ onClose: () => this.handleChannelClosed(),
188
+ });
189
+ this.client = new AcpClient({
190
+ transport: this.transport,
191
+ provider: this.provider,
192
+ hostServices: options.hostServices,
193
+ callbacks: options.callbacks,
194
+ logger: this.logger,
195
+ timeouts: { initializeMs: options.initializeTimeoutMs },
196
+ });
197
+ options.child.on("exit", (code, signal) => this.handleExit(code, signal));
198
+ options.child.on("error", err => this.handleSpawnError(err));
199
+ }
200
+ get state() {
201
+ return this._state;
202
+ }
203
+ get exitCode() {
204
+ return this._exitCode;
205
+ }
206
+ get signal() {
207
+ return this._signal;
208
+ }
209
+ get terminalError() {
210
+ return this._terminalError;
211
+ }
212
+ async initialize() {
213
+ this.logger.info("acp.initialize.start", { provider: this.provider });
214
+ const response = await this.client.initialize();
215
+ this._state = "running";
216
+ this.armIdleTimer();
217
+ this.logger.info("acp.initialize.success", {
218
+ provider: this.provider,
219
+ protocolVersion: response.protocolVersion,
220
+ });
221
+ }
222
+ touchIdle() {
223
+ if (this._state !== "running") {
224
+ return;
225
+ }
226
+ this.armIdleTimer();
227
+ }
228
+ armIdleTimer() {
229
+ if (this.idleTimer) {
230
+ clearTimeout(this.idleTimer);
231
+ this.idleTimer = null;
232
+ }
233
+ if (this.idleTimeoutMs <= 0) {
234
+ return;
235
+ }
236
+ this.idleTimer = setTimeout(() => {
237
+ if (this.transport.pendingCount > 0) {
238
+ this.armIdleTimer();
239
+ return;
240
+ }
241
+ this.logger.info("acp.process.idle_timeout", {
242
+ provider: this.provider,
243
+ pid: this.pid,
244
+ idleTimeoutMs: this.idleTimeoutMs,
245
+ });
246
+ this.shutdown("SIGTERM");
247
+ }, this.idleTimeoutMs);
248
+ this.idleTimer.unref?.();
249
+ }
250
+ clearIdleTimer() {
251
+ if (this.idleTimer) {
252
+ clearTimeout(this.idleTimer);
253
+ this.idleTimer = null;
254
+ }
255
+ }
256
+ handleExit(code, signal) {
257
+ if (this._state === "exited" || this._state === "quarantined") {
258
+ this._exitCode = code;
259
+ this._signal = signal;
260
+ return;
261
+ }
262
+ this._exitCode = code;
263
+ this._signal = signal;
264
+ this._state = "exited";
265
+ this.clearIdleTimer();
266
+ this.transport.handleProcessExit(code, signal);
267
+ const error = new AcpProcessExitError(this.provider, {
268
+ exitCode: code,
269
+ signal,
270
+ debug: { code, signal },
271
+ });
272
+ this._terminalError = error;
273
+ this.logger.error("acp.process.exit", {
274
+ provider: this.provider,
275
+ pid: this.pid,
276
+ exitCode: code,
277
+ signal,
278
+ });
279
+ this.client.notifyProcessExit(error);
280
+ this.onTerminal(this);
281
+ }
282
+ handleSpawnError(err) {
283
+ if (this._state === "exited" || this._state === "quarantined") {
284
+ return;
285
+ }
286
+ this._state = "quarantined";
287
+ this.clearIdleTimer();
288
+ const error = new AcpProcessExitError(this.provider, {
289
+ debug: { reason: "spawn_error", errorClass: err.name },
290
+ });
291
+ this._terminalError = error;
292
+ this.transport.dispose();
293
+ this.logger.error("acp.process.error", {
294
+ provider: this.provider,
295
+ pid: this.pid,
296
+ errorClass: err.name,
297
+ });
298
+ this.client.notifyProcessExit(error);
299
+ this.onTerminal(this);
300
+ }
301
+ handleChannelClosed() {
302
+ if (this._state === "exited" || this._state === "quarantined") {
303
+ return;
304
+ }
305
+ this._state = "quarantined";
306
+ this.clearIdleTimer();
307
+ const error = this._terminalError ??
308
+ new AcpProcessExitError(this.provider, {
309
+ debug: { reason: "stdout_channel_closed" },
310
+ });
311
+ this._terminalError = error;
312
+ this.logger.error("acp.process.channel_closed", {
313
+ provider: this.provider,
314
+ pid: this.pid,
315
+ });
316
+ this.client.notifyProcessExit(error);
317
+ try {
318
+ this.child.kill("SIGTERM");
319
+ }
320
+ catch (err) {
321
+ this.logger.error("acp.process.kill_failed", {
322
+ provider: this.provider,
323
+ pid: this.pid,
324
+ errorClass: err instanceof Error ? err.name : "unknown",
325
+ });
326
+ }
327
+ this.onTerminal(this);
328
+ }
329
+ shutdown(signal = "SIGTERM") {
330
+ if (this._state === "exited" || this._state === "quarantined") {
331
+ return;
332
+ }
333
+ this.clearIdleTimer();
334
+ const wasRunning = this._state === "running" || this._state === "starting";
335
+ this._state = "quarantined";
336
+ if (!this._terminalError) {
337
+ this._terminalError = new AcpProcessExitError(this.provider, {
338
+ debug: { reason: "shutdown", signal },
339
+ });
340
+ }
341
+ this.transport.dispose();
342
+ if (wasRunning) {
343
+ try {
344
+ this.child.kill(signal);
345
+ }
346
+ catch (err) {
347
+ this.logger.error("acp.process.kill_failed", {
348
+ provider: this.provider,
349
+ pid: this.pid,
350
+ errorClass: err instanceof Error ? err.name : "unknown",
351
+ });
352
+ }
353
+ }
354
+ this.logger.info("acp.process.shutdown", {
355
+ provider: this.provider,
356
+ pid: this.pid,
357
+ signal,
358
+ });
359
+ this.onTerminal(this);
360
+ }
361
+ isHealthy() {
362
+ return this._state === "running";
363
+ }
364
+ }
@@ -0,0 +1,24 @@
1
+ import type { CliType } from "../session-manager.js";
2
+ export type AcpProviderStatus = "native_smoke_passed" | "adapter_mediated_deferred" | "absent_watchlist";
3
+ export type AcpSupportKind = "native" | "adapter_mediated" | "none";
4
+ export interface AcpEntrypoint {
5
+ readonly command: string;
6
+ readonly args: readonly string[];
7
+ }
8
+ export interface AcpProviderRegistryEntry {
9
+ readonly provider: CliType;
10
+ readonly displayName: string;
11
+ readonly status: AcpProviderStatus;
12
+ readonly supportKind: AcpSupportKind;
13
+ readonly targetVersion: string;
14
+ readonly entrypoint: AcpEntrypoint | null;
15
+ readonly runtimeEnabledDefault: boolean;
16
+ readonly shipRuntimePilot: boolean;
17
+ readonly runtimePriority: number;
18
+ readonly adapterCandidates: readonly string[];
19
+ readonly caveat: string;
20
+ }
21
+ export declare function getAcpProviderRegistry(): Readonly<Record<CliType, AcpProviderRegistryEntry>>;
22
+ export declare function getAcpProviderEntry(provider: CliType): AcpProviderRegistryEntry;
23
+ export declare function providerHasNativeAcp(provider: CliType): boolean;
24
+ export declare function getRuntimePilotProviders(): readonly CliType[];
@@ -0,0 +1,82 @@
1
+ const ACP_PROVIDER_REGISTRY = Object.freeze({
2
+ mistral: Object.freeze({
3
+ provider: "mistral",
4
+ displayName: "Mistral Vibe",
5
+ status: "native_smoke_passed",
6
+ supportKind: "native",
7
+ targetVersion: "vibe 2.14.1",
8
+ entrypoint: Object.freeze({ command: "vibe-acp", args: Object.freeze([]) }),
9
+ runtimeEnabledDefault: false,
10
+ shipRuntimePilot: true,
11
+ runtimePriority: 1,
12
+ adapterCandidates: Object.freeze([]),
13
+ caveat: "Native ACP entrypoint vibe-acp. Manual initialize and session/new smoke passed. First native runtime pilot; runtime routing stays config-gated.",
14
+ }),
15
+ grok: Object.freeze({
16
+ provider: "grok",
17
+ displayName: "xAI Grok CLI",
18
+ status: "native_smoke_passed",
19
+ supportKind: "native",
20
+ targetVersion: "grok 0.2.50 (cadf94855)",
21
+ entrypoint: Object.freeze({ command: "grok", args: Object.freeze(["agent", "stdio"]) }),
22
+ runtimeEnabledDefault: false,
23
+ shipRuntimePilot: true,
24
+ runtimePriority: 2,
25
+ adapterCandidates: Object.freeze([]),
26
+ caveat: "Native ACP entrypoint grok agent stdio. Manual smoke passed with the installed CLI managing credentials; empty-env smoke is expected to fail. Second native runtime pilot; runtime routing stays config-gated.",
27
+ }),
28
+ codex: Object.freeze({
29
+ provider: "codex",
30
+ displayName: "OpenAI Codex CLI",
31
+ status: "adapter_mediated_deferred",
32
+ supportKind: "adapter_mediated",
33
+ targetVersion: "codex-cli 0.139.0",
34
+ entrypoint: null,
35
+ runtimeEnabledDefault: false,
36
+ shipRuntimePilot: false,
37
+ runtimePriority: 0,
38
+ adapterCandidates: Object.freeze(["zed-industries/codex-acp", "agentclientprotocol/codex-acp"]),
39
+ caveat: "No native ACP entrypoint at the target version. Adapter-mediated only; tracked but not shipped as native gateway ACP support.",
40
+ }),
41
+ claude: Object.freeze({
42
+ provider: "claude",
43
+ displayName: "Anthropic Claude Code",
44
+ status: "adapter_mediated_deferred",
45
+ supportKind: "adapter_mediated",
46
+ targetVersion: "claude 2.1.175",
47
+ entrypoint: null,
48
+ runtimeEnabledDefault: false,
49
+ shipRuntimePilot: false,
50
+ runtimePriority: 0,
51
+ adapterCandidates: Object.freeze(["Claude Agent SDK ACP adapter"]),
52
+ caveat: "No native Claude Code CLI ACP entrypoint at the target version. Adapter-mediated only; adapter ownership, permission bridging, and install story unresolved.",
53
+ }),
54
+ gemini: Object.freeze({
55
+ provider: "gemini",
56
+ displayName: "Google Antigravity",
57
+ status: "absent_watchlist",
58
+ supportKind: "none",
59
+ targetVersion: "agy 1.0.7",
60
+ entrypoint: null,
61
+ runtimeEnabledDefault: false,
62
+ shipRuntimePilot: false,
63
+ runtimePriority: 0,
64
+ adapterCandidates: Object.freeze([]),
65
+ caveat: "No ACP flag or subcommand at the target version. Legacy Gemini CLI ACP evidence does not transfer to Antigravity agy. Watchlist only.",
66
+ }),
67
+ });
68
+ export function getAcpProviderRegistry() {
69
+ return ACP_PROVIDER_REGISTRY;
70
+ }
71
+ export function getAcpProviderEntry(provider) {
72
+ return ACP_PROVIDER_REGISTRY[provider];
73
+ }
74
+ export function providerHasNativeAcp(provider) {
75
+ return ACP_PROVIDER_REGISTRY[provider].supportKind === "native";
76
+ }
77
+ export function getRuntimePilotProviders() {
78
+ return Object.values(ACP_PROVIDER_REGISTRY)
79
+ .filter(entry => entry.shipRuntimePilot)
80
+ .sort((a, b) => a.runtimePriority - b.runtimePriority)
81
+ .map(entry => entry.provider);
82
+ }