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 +83 -0
- package/README.md +55 -9
- package/dist/acp/client.d.ts +78 -0
- package/dist/acp/client.js +201 -0
- package/dist/acp/errors.d.ts +63 -0
- package/dist/acp/errors.js +139 -0
- package/dist/acp/json-rpc-stdio.d.ts +71 -0
- package/dist/acp/json-rpc-stdio.js +375 -0
- package/dist/acp/process-manager.d.ts +66 -0
- package/dist/acp/process-manager.js +364 -0
- package/dist/acp/provider-registry.d.ts +24 -0
- package/dist/acp/provider-registry.js +82 -0
- package/dist/acp/types.d.ts +557 -0
- package/dist/acp/types.js +335 -0
- package/dist/async-job-manager.d.ts +2 -0
- package/dist/async-job-manager.js +45 -16
- package/dist/cache-stats.js +17 -10
- package/dist/codex-json-parser.d.ts +3 -0
- package/dist/codex-json-parser.js +17 -0
- package/dist/config.d.ts +30 -0
- package/dist/config.js +119 -0
- package/dist/doctor.d.ts +22 -0
- package/dist/doctor.js +45 -0
- package/dist/http-transport.js +2 -1
- package/dist/index.js +78 -15
- package/dist/pricing.d.ts +1 -1
- package/dist/pricing.js +67 -2
- package/dist/provider-tool-capabilities.d.ts +135 -0
- package/dist/provider-tool-capabilities.js +1280 -0
- package/dist/request-context.d.ts +1 -0
- package/dist/request-helpers.d.ts +4 -4
- package/dist/resources.js +51 -0
- package/dist/upstream-contracts.d.ts +27 -0
- package/dist/upstream-contracts.js +131 -0
- package/npm-shrinkwrap.json +2 -2
- package/package.json +1 -1
- package/setup/status.schema.json +67 -6
- package/socket.yml +25 -2
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
export function redactAcpMessage(input) {
|
|
2
|
+
let out = input;
|
|
3
|
+
out = out.replace(/\{[\s\S]*?\}/g, "<redacted-json>");
|
|
4
|
+
out = out.replace(/\[[\s\S]*?\]/g, "<redacted-json>");
|
|
5
|
+
out = out.replace(/\b(bearer|token|api[_-]?key|secret)\b\s*[:=]?\s*\S+/gi, "$1 <redacted>");
|
|
6
|
+
out = out.replace(/\b(sk|xai|gsk|key)-[A-Za-z0-9_-]{8,}\b/gi, "<redacted-token>");
|
|
7
|
+
out = out.replace(/(^|[^A-Za-z0-9._~\\/-])[A-Za-z]:\\[^\s"')\]}>]*/g, "$1<redacted-path>");
|
|
8
|
+
out = out.replace(/(^|[^A-Za-z0-9._~\\/-])\\\\[^\s"')\]}>]+/g, "$1<redacted-path>");
|
|
9
|
+
out = out.replace(/(^|[^A-Za-z0-9._~/-])~\/[^\s"')\]}>]+/g, "$1<redacted-path>");
|
|
10
|
+
out = out.replace(/(^|[^A-Za-z0-9._~/-])\/[A-Za-z0-9._/-]{2,}/g, "$1<redacted-path>");
|
|
11
|
+
out = out.replace(/[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}/gi, "<redacted-email>");
|
|
12
|
+
return out;
|
|
13
|
+
}
|
|
14
|
+
export function redactAcpDebug(value) {
|
|
15
|
+
const sensitiveKey = /(payload|body|prompt|content|token|secret|api[_-]?key|credential|auth|cwd|path)/i;
|
|
16
|
+
if (typeof value === "string") {
|
|
17
|
+
return redactAcpMessage(value);
|
|
18
|
+
}
|
|
19
|
+
if (Array.isArray(value)) {
|
|
20
|
+
return value.map(item => redactAcpDebug(item));
|
|
21
|
+
}
|
|
22
|
+
if (value !== null && typeof value === "object") {
|
|
23
|
+
const out = {};
|
|
24
|
+
for (const [key, inner] of Object.entries(value)) {
|
|
25
|
+
if (sensitiveKey.test(key)) {
|
|
26
|
+
out[key] = "<redacted>";
|
|
27
|
+
continue;
|
|
28
|
+
}
|
|
29
|
+
out[key] = redactAcpDebug(inner);
|
|
30
|
+
}
|
|
31
|
+
return out;
|
|
32
|
+
}
|
|
33
|
+
return value;
|
|
34
|
+
}
|
|
35
|
+
export function redactAcpCause(cause) {
|
|
36
|
+
if (cause instanceof Error) {
|
|
37
|
+
const redacted = new Error(redactAcpMessage(cause.message));
|
|
38
|
+
redacted.name = redactAcpMessage(cause.name);
|
|
39
|
+
if (typeof cause.stack === "string") {
|
|
40
|
+
redacted.stack = redactAcpMessage(cause.stack);
|
|
41
|
+
}
|
|
42
|
+
return redacted;
|
|
43
|
+
}
|
|
44
|
+
return redactAcpDebug(cause);
|
|
45
|
+
}
|
|
46
|
+
export class AcpError extends Error {
|
|
47
|
+
kind;
|
|
48
|
+
provider;
|
|
49
|
+
debug;
|
|
50
|
+
constructor(kind, userMessage, options) {
|
|
51
|
+
super(redactAcpMessage(userMessage));
|
|
52
|
+
this.name = "AcpError";
|
|
53
|
+
this.kind = kind;
|
|
54
|
+
this.provider = options?.provider;
|
|
55
|
+
this.debug = redactAcpDebug(options?.debug ?? {}) ?? {};
|
|
56
|
+
if (options?.cause !== undefined) {
|
|
57
|
+
this.cause = redactAcpCause(options.cause);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
get userMessage() {
|
|
61
|
+
return this.message;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
export class AcpDisabledError extends AcpError {
|
|
65
|
+
constructor(debug) {
|
|
66
|
+
super("acp_disabled", "ACP transport is disabled. Enable [acp] in the gateway config to use transport=acp, or omit transport to use the default CLI path.", { debug });
|
|
67
|
+
this.name = "AcpDisabledError";
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
export class ProviderAcpDisabledError extends AcpError {
|
|
71
|
+
constructor(provider, debug) {
|
|
72
|
+
super("provider_acp_disabled", `ACP is disabled for provider ${provider}. Enable it under [acp.providers.${provider}] or omit transport to use the default CLI path.`, { provider, debug });
|
|
73
|
+
this.name = "ProviderAcpDisabledError";
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
export class ProviderAcpUnsupportedError extends AcpError {
|
|
77
|
+
constructor(provider, debug) {
|
|
78
|
+
super("provider_acp_unsupported", `Provider ${provider} has no native ACP support at its target version. Use the default CLI transport for this provider.`, { provider, debug });
|
|
79
|
+
this.name = "ProviderAcpUnsupportedError";
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
export class ProviderRuntimeDisabledError extends AcpError {
|
|
83
|
+
constructor(provider, debug) {
|
|
84
|
+
super("provider_runtime_disabled", `ACP runtime routing is not enabled for provider ${provider}. Set runtime_enabled=true under [acp.providers.${provider}] to allow prompt routing.`, { provider, debug });
|
|
85
|
+
this.name = "ProviderRuntimeDisabledError";
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
export class ProviderUnavailableError extends AcpError {
|
|
89
|
+
constructor(provider, reason, debug) {
|
|
90
|
+
super("provider_unavailable", `Provider ${provider} ACP entrypoint is unavailable: ${reason}`, {
|
|
91
|
+
provider,
|
|
92
|
+
debug,
|
|
93
|
+
});
|
|
94
|
+
this.name = "ProviderUnavailableError";
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
export class AcpProtocolError extends AcpError {
|
|
98
|
+
code;
|
|
99
|
+
constructor(userMessage, options) {
|
|
100
|
+
super("protocol", userMessage, { provider: options?.provider, debug: options?.debug });
|
|
101
|
+
this.name = "AcpProtocolError";
|
|
102
|
+
this.code = options?.code;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
export class AcpTimeoutError extends AcpError {
|
|
106
|
+
method;
|
|
107
|
+
timeoutMs;
|
|
108
|
+
constructor(method, timeoutMs, options) {
|
|
109
|
+
super("timeout", `ACP request ${method} timed out after ${timeoutMs}ms.`, {
|
|
110
|
+
provider: options?.provider,
|
|
111
|
+
debug: options?.debug,
|
|
112
|
+
});
|
|
113
|
+
this.name = "AcpTimeoutError";
|
|
114
|
+
this.method = method;
|
|
115
|
+
this.timeoutMs = timeoutMs;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
export class AcpPermissionDeniedError extends AcpError {
|
|
119
|
+
constructor(provider, reason, debug) {
|
|
120
|
+
super("permission_denied", `ACP permission request was denied: ${reason}`, { provider, debug });
|
|
121
|
+
this.name = "AcpPermissionDeniedError";
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
export class AcpProcessExitError extends AcpError {
|
|
125
|
+
exitCode;
|
|
126
|
+
signal;
|
|
127
|
+
constructor(provider, options) {
|
|
128
|
+
super("process_exit", `Provider ${provider} ACP process exited before the request completed.`, {
|
|
129
|
+
provider,
|
|
130
|
+
debug: options?.debug,
|
|
131
|
+
});
|
|
132
|
+
this.name = "AcpProcessExitError";
|
|
133
|
+
this.exitCode = options?.exitCode ?? null;
|
|
134
|
+
this.signal = options?.signal ?? null;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
export function isAcpError(value) {
|
|
138
|
+
return value instanceof AcpError;
|
|
139
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import type { Readable, Writable } from "node:stream";
|
|
2
|
+
import type { Logger } from "../logger.js";
|
|
3
|
+
import type { CliType } from "../session-manager.js";
|
|
4
|
+
export type JsonRpcId = number | string;
|
|
5
|
+
export interface JsonRpcErrorObject {
|
|
6
|
+
readonly code: number;
|
|
7
|
+
readonly message: string;
|
|
8
|
+
readonly data?: unknown;
|
|
9
|
+
}
|
|
10
|
+
export interface JsonRpcNotification {
|
|
11
|
+
readonly jsonrpc: "2.0";
|
|
12
|
+
readonly method: string;
|
|
13
|
+
readonly params?: unknown;
|
|
14
|
+
}
|
|
15
|
+
export interface JsonRpcInboundRequest {
|
|
16
|
+
readonly jsonrpc: "2.0";
|
|
17
|
+
readonly id: JsonRpcId;
|
|
18
|
+
readonly method: string;
|
|
19
|
+
readonly params?: unknown;
|
|
20
|
+
}
|
|
21
|
+
export interface ProviderStdioStreams {
|
|
22
|
+
readonly stdin: Writable;
|
|
23
|
+
readonly stdout: Readable;
|
|
24
|
+
readonly stderr?: Readable | null;
|
|
25
|
+
}
|
|
26
|
+
export interface JsonRpcStdioTransportOptions {
|
|
27
|
+
readonly streams: ProviderStdioStreams;
|
|
28
|
+
readonly logger?: Logger;
|
|
29
|
+
readonly provider?: CliType;
|
|
30
|
+
readonly defaultTimeoutMs?: number;
|
|
31
|
+
readonly onNotification?: (notification: JsonRpcNotification) => void;
|
|
32
|
+
readonly onRequest?: (request: JsonRpcInboundRequest) => void;
|
|
33
|
+
readonly onActivity?: () => void;
|
|
34
|
+
readonly onClose?: () => void;
|
|
35
|
+
}
|
|
36
|
+
export declare class JsonRpcStdioTransport {
|
|
37
|
+
private readonly streams;
|
|
38
|
+
private readonly logger;
|
|
39
|
+
private readonly provider?;
|
|
40
|
+
private readonly defaultTimeoutMs;
|
|
41
|
+
private readonly onNotification?;
|
|
42
|
+
private readonly onRequest?;
|
|
43
|
+
private readonly onActivity?;
|
|
44
|
+
private readonly onClose?;
|
|
45
|
+
private readonly pending;
|
|
46
|
+
private nextId;
|
|
47
|
+
private stdoutBuffer;
|
|
48
|
+
private stderrBuffer;
|
|
49
|
+
private closed;
|
|
50
|
+
private exitError;
|
|
51
|
+
constructor(options: JsonRpcStdioTransportOptions);
|
|
52
|
+
private attach;
|
|
53
|
+
private onStdoutData;
|
|
54
|
+
private onStderrData;
|
|
55
|
+
private handleLine;
|
|
56
|
+
private dispatchNotification;
|
|
57
|
+
private dispatchInboundRequest;
|
|
58
|
+
private resolveResponse;
|
|
59
|
+
request(method: string, params?: unknown, timeoutMs?: number): Promise<unknown>;
|
|
60
|
+
notify(method: string, params?: unknown): void;
|
|
61
|
+
respond(id: JsonRpcId, result: unknown): void;
|
|
62
|
+
respondError(id: JsonRpcId, error: JsonRpcErrorObject): void;
|
|
63
|
+
handleProcessExit(exitCode: number | null, signal: string | null): void;
|
|
64
|
+
private handleStreamClose;
|
|
65
|
+
private emitActivity;
|
|
66
|
+
private emitClose;
|
|
67
|
+
private failPending;
|
|
68
|
+
dispose(): void;
|
|
69
|
+
get pendingCount(): number;
|
|
70
|
+
get isClosed(): boolean;
|
|
71
|
+
}
|
|
@@ -0,0 +1,375 @@
|
|
|
1
|
+
import { AcpProcessExitError, AcpProtocolError, AcpTimeoutError, redactAcpMessage, } from "./errors.js";
|
|
2
|
+
import { noopLogger } from "../logger.js";
|
|
3
|
+
function isJsonRpcId(value) {
|
|
4
|
+
return typeof value === "number" || typeof value === "string";
|
|
5
|
+
}
|
|
6
|
+
function isJsonRpcErrorObject(value) {
|
|
7
|
+
return (value !== null &&
|
|
8
|
+
typeof value === "object" &&
|
|
9
|
+
typeof value.code === "number" &&
|
|
10
|
+
typeof value.message === "string");
|
|
11
|
+
}
|
|
12
|
+
export class JsonRpcStdioTransport {
|
|
13
|
+
streams;
|
|
14
|
+
logger;
|
|
15
|
+
provider;
|
|
16
|
+
defaultTimeoutMs;
|
|
17
|
+
onNotification;
|
|
18
|
+
onRequest;
|
|
19
|
+
onActivity;
|
|
20
|
+
onClose;
|
|
21
|
+
pending = new Map();
|
|
22
|
+
nextId = 1;
|
|
23
|
+
stdoutBuffer = "";
|
|
24
|
+
stderrBuffer = "";
|
|
25
|
+
closed = false;
|
|
26
|
+
exitError = null;
|
|
27
|
+
constructor(options) {
|
|
28
|
+
this.streams = options.streams;
|
|
29
|
+
this.logger = options.logger ?? noopLogger;
|
|
30
|
+
this.provider = options.provider;
|
|
31
|
+
this.defaultTimeoutMs = options.defaultTimeoutMs ?? 0;
|
|
32
|
+
this.onNotification = options.onNotification;
|
|
33
|
+
this.onRequest = options.onRequest;
|
|
34
|
+
this.onActivity = options.onActivity;
|
|
35
|
+
this.onClose = options.onClose;
|
|
36
|
+
this.attach();
|
|
37
|
+
}
|
|
38
|
+
attach() {
|
|
39
|
+
const { stdout, stderr } = this.streams;
|
|
40
|
+
stdout.setEncoding("utf8");
|
|
41
|
+
stdout.on("data", (chunk) => this.onStdoutData(chunk));
|
|
42
|
+
stdout.on("error", (err) => {
|
|
43
|
+
this.logger.error("acp.transport.stdout.error", {
|
|
44
|
+
provider: this.provider,
|
|
45
|
+
errorClass: err.name,
|
|
46
|
+
});
|
|
47
|
+
this.handleStreamClose();
|
|
48
|
+
});
|
|
49
|
+
stdout.on("close", () => this.handleStreamClose());
|
|
50
|
+
stdout.on("end", () => this.handleStreamClose());
|
|
51
|
+
if (stderr) {
|
|
52
|
+
stderr.setEncoding("utf8");
|
|
53
|
+
stderr.on("data", (chunk) => this.onStderrData(chunk));
|
|
54
|
+
stderr.on("error", (err) => {
|
|
55
|
+
this.logger.error("acp.transport.stderr.error", {
|
|
56
|
+
provider: this.provider,
|
|
57
|
+
errorClass: err.name,
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
onStdoutData(chunk) {
|
|
63
|
+
this.stdoutBuffer += chunk;
|
|
64
|
+
let newlineIndex = this.stdoutBuffer.indexOf("\n");
|
|
65
|
+
while (newlineIndex !== -1) {
|
|
66
|
+
const line = this.stdoutBuffer.slice(0, newlineIndex);
|
|
67
|
+
this.stdoutBuffer = this.stdoutBuffer.slice(newlineIndex + 1);
|
|
68
|
+
this.handleLine(line);
|
|
69
|
+
newlineIndex = this.stdoutBuffer.indexOf("\n");
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
onStderrData(chunk) {
|
|
73
|
+
this.stderrBuffer += chunk;
|
|
74
|
+
let newlineIndex = this.stderrBuffer.indexOf("\n");
|
|
75
|
+
while (newlineIndex !== -1) {
|
|
76
|
+
const line = this.stderrBuffer.slice(0, newlineIndex).replace(/\r$/, "");
|
|
77
|
+
this.stderrBuffer = this.stderrBuffer.slice(newlineIndex + 1);
|
|
78
|
+
if (line.length > 0) {
|
|
79
|
+
const redacted = redactAcpMessage(line) !== line;
|
|
80
|
+
this.logger.debug("acp.provider.stderr", {
|
|
81
|
+
provider: this.provider,
|
|
82
|
+
bytes: Buffer.byteLength(line, "utf8"),
|
|
83
|
+
redacted,
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
newlineIndex = this.stderrBuffer.indexOf("\n");
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
handleLine(line) {
|
|
90
|
+
const trimmed = line.trim();
|
|
91
|
+
if (trimmed.length === 0) {
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
let parsed;
|
|
95
|
+
try {
|
|
96
|
+
parsed = JSON.parse(trimmed);
|
|
97
|
+
}
|
|
98
|
+
catch {
|
|
99
|
+
this.logger.error("acp.transport.invalid_json", {
|
|
100
|
+
provider: this.provider,
|
|
101
|
+
errorClass: "SyntaxError",
|
|
102
|
+
bytes: trimmed.length,
|
|
103
|
+
});
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
if (parsed === null || typeof parsed !== "object") {
|
|
107
|
+
this.logger.error("acp.transport.invalid_message", { provider: this.provider });
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
const hasId = isJsonRpcId(parsed.id);
|
|
111
|
+
const hasMethod = typeof parsed.method === "string";
|
|
112
|
+
if (hasMethod || hasId) {
|
|
113
|
+
this.emitActivity();
|
|
114
|
+
}
|
|
115
|
+
if (hasMethod && hasId) {
|
|
116
|
+
this.dispatchInboundRequest(parsed);
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
if (hasMethod && !hasId) {
|
|
120
|
+
this.dispatchNotification(parsed.method, parsed.params);
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
if (hasId) {
|
|
124
|
+
this.resolveResponse(parsed.id, parsed);
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
this.logger.error("acp.transport.unroutable_message", { provider: this.provider });
|
|
128
|
+
}
|
|
129
|
+
dispatchNotification(method, params) {
|
|
130
|
+
const notification = { jsonrpc: "2.0", method, params };
|
|
131
|
+
try {
|
|
132
|
+
this.onNotification?.(notification);
|
|
133
|
+
}
|
|
134
|
+
catch (err) {
|
|
135
|
+
this.logger.error("acp.transport.notification_handler_error", {
|
|
136
|
+
provider: this.provider,
|
|
137
|
+
method,
|
|
138
|
+
errorClass: err instanceof Error ? err.name : "unknown",
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
dispatchInboundRequest(parsed) {
|
|
143
|
+
const request = {
|
|
144
|
+
jsonrpc: "2.0",
|
|
145
|
+
id: parsed.id,
|
|
146
|
+
method: parsed.method,
|
|
147
|
+
params: parsed.params,
|
|
148
|
+
};
|
|
149
|
+
try {
|
|
150
|
+
this.onRequest?.(request);
|
|
151
|
+
}
|
|
152
|
+
catch (err) {
|
|
153
|
+
this.logger.error("acp.transport.request_handler_error", {
|
|
154
|
+
provider: this.provider,
|
|
155
|
+
method: request.method,
|
|
156
|
+
errorClass: err instanceof Error ? err.name : "unknown",
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
resolveResponse(id, parsed) {
|
|
161
|
+
const pending = this.pending.get(id);
|
|
162
|
+
if (!pending) {
|
|
163
|
+
this.logger.debug("acp.transport.orphan_response", { provider: this.provider });
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
this.pending.delete(id);
|
|
167
|
+
if (pending.timer) {
|
|
168
|
+
clearTimeout(pending.timer);
|
|
169
|
+
}
|
|
170
|
+
if (parsed.error !== undefined && parsed.error !== null) {
|
|
171
|
+
if (isJsonRpcErrorObject(parsed.error)) {
|
|
172
|
+
pending.reject(new AcpProtocolError(`ACP request ${pending.method} failed with JSON-RPC error ${parsed.error.code}.`, {
|
|
173
|
+
provider: this.provider,
|
|
174
|
+
code: parsed.error.code,
|
|
175
|
+
debug: {
|
|
176
|
+
method: pending.method,
|
|
177
|
+
code: parsed.error.code,
|
|
178
|
+
providerMessage: parsed.error.message,
|
|
179
|
+
},
|
|
180
|
+
}));
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
pending.reject(new AcpProtocolError(`ACP request ${pending.method} returned a malformed error object.`, {
|
|
184
|
+
provider: this.provider,
|
|
185
|
+
debug: { method: pending.method },
|
|
186
|
+
}));
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
pending.resolve(parsed.result);
|
|
190
|
+
}
|
|
191
|
+
request(method, params, timeoutMs) {
|
|
192
|
+
if (this.exitError) {
|
|
193
|
+
return Promise.reject(this.exitError);
|
|
194
|
+
}
|
|
195
|
+
if (this.closed) {
|
|
196
|
+
return Promise.reject(new AcpProcessExitError(this.provider ?? "unknown", {
|
|
197
|
+
debug: { method, reason: "transport_closed" },
|
|
198
|
+
}));
|
|
199
|
+
}
|
|
200
|
+
const id = this.nextId++;
|
|
201
|
+
const effectiveTimeout = timeoutMs ?? this.defaultTimeoutMs;
|
|
202
|
+
this.emitActivity();
|
|
203
|
+
return new Promise((resolve, reject) => {
|
|
204
|
+
const pending = { method, resolve, reject, timer: null };
|
|
205
|
+
if (effectiveTimeout > 0) {
|
|
206
|
+
pending.timer = setTimeout(() => {
|
|
207
|
+
if (this.pending.get(id) === pending) {
|
|
208
|
+
this.pending.delete(id);
|
|
209
|
+
this.logger.error("acp.transport.timeout", {
|
|
210
|
+
provider: this.provider,
|
|
211
|
+
method,
|
|
212
|
+
timeoutMs: effectiveTimeout,
|
|
213
|
+
});
|
|
214
|
+
reject(new AcpTimeoutError(method, effectiveTimeout, {
|
|
215
|
+
provider: this.provider,
|
|
216
|
+
debug: { method },
|
|
217
|
+
}));
|
|
218
|
+
}
|
|
219
|
+
}, effectiveTimeout);
|
|
220
|
+
pending.timer.unref?.();
|
|
221
|
+
}
|
|
222
|
+
this.pending.set(id, pending);
|
|
223
|
+
const frame = JSON.stringify({
|
|
224
|
+
jsonrpc: "2.0",
|
|
225
|
+
id,
|
|
226
|
+
method,
|
|
227
|
+
...(params !== undefined ? { params } : {}),
|
|
228
|
+
}) + "\n";
|
|
229
|
+
try {
|
|
230
|
+
this.streams.stdin.write(frame);
|
|
231
|
+
}
|
|
232
|
+
catch (err) {
|
|
233
|
+
this.pending.delete(id);
|
|
234
|
+
if (pending.timer) {
|
|
235
|
+
clearTimeout(pending.timer);
|
|
236
|
+
}
|
|
237
|
+
reject(new AcpProcessExitError(this.provider ?? "unknown", {
|
|
238
|
+
debug: {
|
|
239
|
+
method,
|
|
240
|
+
reason: "stdin_write_failed",
|
|
241
|
+
errorClass: err instanceof Error ? err.name : "unknown",
|
|
242
|
+
},
|
|
243
|
+
}));
|
|
244
|
+
}
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
notify(method, params) {
|
|
248
|
+
if (this.exitError || this.closed) {
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
const frame = JSON.stringify({ jsonrpc: "2.0", method, ...(params !== undefined ? { params } : {}) }) +
|
|
252
|
+
"\n";
|
|
253
|
+
try {
|
|
254
|
+
this.streams.stdin.write(frame);
|
|
255
|
+
}
|
|
256
|
+
catch (err) {
|
|
257
|
+
this.logger.error("acp.transport.notify_write_failed", {
|
|
258
|
+
provider: this.provider,
|
|
259
|
+
method,
|
|
260
|
+
errorClass: err instanceof Error ? err.name : "unknown",
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
respond(id, result) {
|
|
265
|
+
if (this.exitError || this.closed) {
|
|
266
|
+
return;
|
|
267
|
+
}
|
|
268
|
+
const frame = JSON.stringify({ jsonrpc: "2.0", id, result }) + "\n";
|
|
269
|
+
try {
|
|
270
|
+
this.streams.stdin.write(frame);
|
|
271
|
+
}
|
|
272
|
+
catch (err) {
|
|
273
|
+
this.logger.error("acp.transport.respond_write_failed", {
|
|
274
|
+
provider: this.provider,
|
|
275
|
+
errorClass: err instanceof Error ? err.name : "unknown",
|
|
276
|
+
});
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
respondError(id, error) {
|
|
280
|
+
if (this.exitError || this.closed) {
|
|
281
|
+
return;
|
|
282
|
+
}
|
|
283
|
+
const frame = JSON.stringify({
|
|
284
|
+
jsonrpc: "2.0",
|
|
285
|
+
id,
|
|
286
|
+
error: {
|
|
287
|
+
code: error.code,
|
|
288
|
+
message: error.message,
|
|
289
|
+
...(error.data !== undefined ? { data: error.data } : {}),
|
|
290
|
+
},
|
|
291
|
+
}) + "\n";
|
|
292
|
+
try {
|
|
293
|
+
this.streams.stdin.write(frame);
|
|
294
|
+
}
|
|
295
|
+
catch (err) {
|
|
296
|
+
this.logger.error("acp.transport.respond_error_write_failed", {
|
|
297
|
+
provider: this.provider,
|
|
298
|
+
errorClass: err instanceof Error ? err.name : "unknown",
|
|
299
|
+
});
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
handleProcessExit(exitCode, signal) {
|
|
303
|
+
if (this.exitError) {
|
|
304
|
+
return;
|
|
305
|
+
}
|
|
306
|
+
this.exitError = new AcpProcessExitError(this.provider ?? "unknown", {
|
|
307
|
+
exitCode,
|
|
308
|
+
signal,
|
|
309
|
+
debug: { exitCode, signal },
|
|
310
|
+
});
|
|
311
|
+
this.logger.error("acp.process.exit", {
|
|
312
|
+
provider: this.provider,
|
|
313
|
+
exitCode,
|
|
314
|
+
signal,
|
|
315
|
+
pending: this.pending.size,
|
|
316
|
+
});
|
|
317
|
+
this.failPending(this.exitError);
|
|
318
|
+
this.closed = true;
|
|
319
|
+
}
|
|
320
|
+
handleStreamClose() {
|
|
321
|
+
if (this.exitError || this.closed) {
|
|
322
|
+
return;
|
|
323
|
+
}
|
|
324
|
+
this.exitError = new AcpProcessExitError(this.provider ?? "unknown", {
|
|
325
|
+
debug: { reason: "stdout_closed" },
|
|
326
|
+
});
|
|
327
|
+
this.logger.error("acp.transport.stdout_closed", {
|
|
328
|
+
provider: this.provider,
|
|
329
|
+
pending: this.pending.size,
|
|
330
|
+
});
|
|
331
|
+
this.failPending(this.exitError);
|
|
332
|
+
this.closed = true;
|
|
333
|
+
this.emitClose();
|
|
334
|
+
}
|
|
335
|
+
emitActivity() {
|
|
336
|
+
try {
|
|
337
|
+
this.onActivity?.();
|
|
338
|
+
}
|
|
339
|
+
catch {
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
emitClose() {
|
|
343
|
+
try {
|
|
344
|
+
this.onClose?.();
|
|
345
|
+
}
|
|
346
|
+
catch {
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
failPending(error) {
|
|
350
|
+
for (const [id, pending] of this.pending) {
|
|
351
|
+
if (pending.timer) {
|
|
352
|
+
clearTimeout(pending.timer);
|
|
353
|
+
}
|
|
354
|
+
this.pending.delete(id);
|
|
355
|
+
pending.reject(error);
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
dispose() {
|
|
359
|
+
if (this.closed) {
|
|
360
|
+
return;
|
|
361
|
+
}
|
|
362
|
+
this.closed = true;
|
|
363
|
+
const error = this.exitError ??
|
|
364
|
+
new AcpProcessExitError(this.provider ?? "unknown", {
|
|
365
|
+
debug: { reason: "disposed" },
|
|
366
|
+
});
|
|
367
|
+
this.failPending(error);
|
|
368
|
+
}
|
|
369
|
+
get pendingCount() {
|
|
370
|
+
return this.pending.size;
|
|
371
|
+
}
|
|
372
|
+
get isClosed() {
|
|
373
|
+
return this.closed;
|
|
374
|
+
}
|
|
375
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import type { Readable, Writable } from "node:stream";
|
|
2
|
+
import { AcpClient, type AcpClientCallbacks, type HostServices } from "./client.js";
|
|
3
|
+
import { AcpError } from "./errors.js";
|
|
4
|
+
import { JsonRpcStdioTransport } from "./json-rpc-stdio.js";
|
|
5
|
+
import type { AcpConfig, AcpProviderConfig } from "../config.js";
|
|
6
|
+
import type { Logger } from "../logger.js";
|
|
7
|
+
import type { CliType } from "../session-manager.js";
|
|
8
|
+
export type ProcessEnv = Record<string, string | undefined>;
|
|
9
|
+
export type TerminationSignal = "SIGTERM" | "SIGKILL" | "SIGINT" | "SIGHUP";
|
|
10
|
+
export interface AcpChildProcess {
|
|
11
|
+
readonly pid?: number | undefined;
|
|
12
|
+
readonly stdin: Writable | null;
|
|
13
|
+
readonly stdout: Readable | null;
|
|
14
|
+
readonly stderr: Readable | null;
|
|
15
|
+
on(event: "exit", listener: (code: number | null, signal: TerminationSignal | null) => void): unknown;
|
|
16
|
+
on(event: "error", listener: (err: Error) => void): unknown;
|
|
17
|
+
kill(signal?: TerminationSignal | number): boolean;
|
|
18
|
+
}
|
|
19
|
+
export interface ResolvedAcpSpawn {
|
|
20
|
+
readonly command: string;
|
|
21
|
+
readonly args: readonly string[];
|
|
22
|
+
readonly cwd: string;
|
|
23
|
+
readonly env: ProcessEnv;
|
|
24
|
+
}
|
|
25
|
+
export type AcpSpawnFn = (resolved: ResolvedAcpSpawn) => AcpChildProcess;
|
|
26
|
+
export type AcpProcessState = "starting" | "running" | "exited" | "quarantined";
|
|
27
|
+
export interface AcpProcessManagerOptions {
|
|
28
|
+
readonly config: AcpConfig;
|
|
29
|
+
readonly logger?: Logger;
|
|
30
|
+
readonly spawn?: AcpSpawnFn;
|
|
31
|
+
readonly baseEnv?: ProcessEnv;
|
|
32
|
+
}
|
|
33
|
+
export interface StartProviderOptions {
|
|
34
|
+
readonly provider: CliType;
|
|
35
|
+
readonly cwd?: string;
|
|
36
|
+
readonly hostServices: HostServices;
|
|
37
|
+
readonly callbacks?: AcpClientCallbacks;
|
|
38
|
+
readonly idleTimeoutMs?: number;
|
|
39
|
+
}
|
|
40
|
+
export interface ManagedAcpProcess {
|
|
41
|
+
readonly provider: CliType;
|
|
42
|
+
readonly pid: number | undefined;
|
|
43
|
+
readonly transport: JsonRpcStdioTransport;
|
|
44
|
+
readonly client: AcpClient;
|
|
45
|
+
readonly state: AcpProcessState;
|
|
46
|
+
readonly exitCode: number | null;
|
|
47
|
+
readonly signal: string | null;
|
|
48
|
+
readonly terminalError: AcpError | null;
|
|
49
|
+
readonly resolved: ResolvedAcpSpawn;
|
|
50
|
+
shutdown(signal?: TerminationSignal): void;
|
|
51
|
+
isHealthy(): boolean;
|
|
52
|
+
}
|
|
53
|
+
export declare function buildProviderEnv(provider: CliType, providerConfig: AcpProviderConfig, baseEnv: ProcessEnv): ProcessEnv;
|
|
54
|
+
export declare function resolveProviderSpawn(provider: CliType, config: AcpConfig, baseEnv: ProcessEnv, cwd?: string): ResolvedAcpSpawn;
|
|
55
|
+
export declare const defaultSpawn: AcpSpawnFn;
|
|
56
|
+
export declare class AcpProcessManager {
|
|
57
|
+
private readonly config;
|
|
58
|
+
private readonly logger;
|
|
59
|
+
private readonly spawnFn;
|
|
60
|
+
private readonly baseEnv;
|
|
61
|
+
private readonly live;
|
|
62
|
+
constructor(options: AcpProcessManagerOptions);
|
|
63
|
+
start(options: StartProviderOptions): Promise<ManagedAcpProcess>;
|
|
64
|
+
shutdownAll(signal?: TerminationSignal): void;
|
|
65
|
+
get liveCount(): number;
|
|
66
|
+
}
|