@wix/pathgrade 1.0.25 → 1.0.27

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 (89) hide show
  1. package/README.md +44 -2
  2. package/dist/agents/claude/sdk-message-projector.js +5 -0
  3. package/dist/agents/claude/sdk-options.d.ts +1 -2
  4. package/dist/agents/claude.js +2 -1
  5. package/dist/agents/codex-app-server/agent.js +89 -52
  6. package/dist/agents/codex-app-server/scenario-mount.d.ts +40 -0
  7. package/dist/agents/codex-app-server/scenario-mount.js +238 -0
  8. package/dist/agents/codex.js +1 -0
  9. package/dist/agents/cursor-scenario.d.ts +20 -0
  10. package/dist/agents/cursor-scenario.js +96 -0
  11. package/dist/agents/cursor.js +21 -0
  12. package/dist/agents/opencode/scenario.d.ts +25 -0
  13. package/dist/agents/opencode/scenario.js +195 -0
  14. package/dist/agents/opencode.js +54 -2
  15. package/dist/commands/report.d.ts +10 -2
  16. package/dist/commands/report.js +25 -4
  17. package/dist/internal/direct-mcp-v2/acp-author-projector.d.ts +20 -0
  18. package/dist/internal/direct-mcp-v2/acp-author-projector.js +147 -0
  19. package/dist/internal/direct-mcp-v2/acp-author-session.d.ts +39 -0
  20. package/dist/internal/direct-mcp-v2/acp-author-session.js +201 -0
  21. package/dist/internal/direct-mcp-v2/acp-client.d.ts +70 -0
  22. package/dist/internal/direct-mcp-v2/acp-client.js +310 -0
  23. package/dist/internal/direct-mcp-v2/acp-direct-mcp.d.ts +38 -0
  24. package/dist/internal/direct-mcp-v2/acp-direct-mcp.js +326 -0
  25. package/dist/internal/direct-mcp-v2/claude-direct-mcp.d.ts +68 -0
  26. package/dist/internal/direct-mcp-v2/claude-direct-mcp.js +456 -0
  27. package/dist/internal/direct-mcp-v2/claude-profile.d.ts +9 -0
  28. package/dist/internal/direct-mcp-v2/claude-profile.js +97 -0
  29. package/dist/internal/direct-mcp-v2/cleanup.d.ts +62 -0
  30. package/dist/internal/direct-mcp-v2/cleanup.js +182 -0
  31. package/dist/internal/direct-mcp-v2/codex-direct-mcp.d.ts +41 -0
  32. package/dist/internal/direct-mcp-v2/codex-direct-mcp.js +436 -0
  33. package/dist/internal/direct-mcp-v2/codex-profile.d.ts +12 -0
  34. package/dist/internal/direct-mcp-v2/codex-profile.js +81 -0
  35. package/dist/internal/direct-mcp-v2/compiler.d.ts +12 -0
  36. package/dist/internal/direct-mcp-v2/compiler.js +449 -0
  37. package/dist/internal/direct-mcp-v2/cursor-direct-mcp.d.ts +14 -0
  38. package/dist/internal/direct-mcp-v2/cursor-direct-mcp.js +60 -0
  39. package/dist/internal/direct-mcp-v2/cursor-profile.d.ts +38 -0
  40. package/dist/internal/direct-mcp-v2/cursor-profile.js +79 -0
  41. package/dist/internal/direct-mcp-v2/evidence.d.ts +50 -0
  42. package/dist/internal/direct-mcp-v2/evidence.js +147 -0
  43. package/dist/internal/direct-mcp-v2/host-reducer.d.ts +61 -0
  44. package/dist/internal/direct-mcp-v2/host-reducer.js +214 -0
  45. package/dist/internal/direct-mcp-v2/json.d.ts +9 -0
  46. package/dist/internal/direct-mcp-v2/json.js +220 -0
  47. package/dist/internal/direct-mcp-v2/materialize.d.ts +39 -0
  48. package/dist/internal/direct-mcp-v2/materialize.js +22 -0
  49. package/dist/internal/direct-mcp-v2/opencode-direct-mcp.d.ts +14 -0
  50. package/dist/internal/direct-mcp-v2/opencode-direct-mcp.js +71 -0
  51. package/dist/internal/direct-mcp-v2/opencode-profile.d.ts +8 -0
  52. package/dist/internal/direct-mcp-v2/opencode-profile.js +52 -0
  53. package/dist/internal/direct-mcp-v2/projection.d.ts +40 -0
  54. package/dist/internal/direct-mcp-v2/projection.js +46 -0
  55. package/dist/internal/direct-mcp-v2/public-scenario-runtime.d.ts +51 -0
  56. package/dist/internal/direct-mcp-v2/public-scenario-runtime.js +353 -0
  57. package/dist/internal/direct-mcp-v2/runtime-lock.d.ts +51 -0
  58. package/dist/internal/direct-mcp-v2/runtime-lock.js +203 -0
  59. package/dist/internal/direct-mcp-v2/runtime-profile.d.ts +28 -0
  60. package/dist/internal/direct-mcp-v2/runtime-profile.js +163 -0
  61. package/dist/internal/direct-mcp-v2/scenario-http-host.d.ts +24 -0
  62. package/dist/internal/direct-mcp-v2/scenario-http-host.js +254 -0
  63. package/dist/internal/direct-mcp-v2/types.d.ts +233 -0
  64. package/dist/internal/direct-mcp-v2/types.js +1 -0
  65. package/dist/pathgrade.js +22 -1
  66. package/dist/reporters/github-comment.d.ts +12 -3
  67. package/dist/reporters/github-comment.js +80 -9
  68. package/dist/reporting/artifacts.js +5 -2
  69. package/dist/sdk/agent.js +56 -4
  70. package/dist/sdk/evaluate.d.ts +2 -0
  71. package/dist/sdk/evaluate.js +20 -12
  72. package/dist/sdk/index.d.ts +6 -0
  73. package/dist/sdk/index.js +4 -0
  74. package/dist/sdk/lifecycle.js +8 -3
  75. package/dist/sdk/managed-session.d.ts +6 -0
  76. package/dist/sdk/managed-session.js +157 -50
  77. package/dist/sdk/scenario-evidence.d.ts +27 -0
  78. package/dist/sdk/scenario-evidence.js +94 -0
  79. package/dist/sdk/scenario-machine-v2.d.ts +9 -0
  80. package/dist/sdk/scenario-machine-v2.js +37 -0
  81. package/dist/sdk/snapshots.d.ts +3 -0
  82. package/dist/sdk/snapshots.js +2 -0
  83. package/dist/sdk/types.d.ts +11 -1
  84. package/dist/tool-event-results.d.ts +1 -1
  85. package/dist/tool-event-results.js +2 -1
  86. package/dist/tool-events.d.ts +5 -0
  87. package/dist/tool-events.js +5 -0
  88. package/dist/types.d.ts +8 -0
  89. package/package.json +2 -2
@@ -0,0 +1,147 @@
1
+ import { buildSummary } from '../../tool-events.js';
2
+ import { sanitizePersistenceValue, sanitizeToolEventResult } from '../../tool-event-results.js';
3
+ export const ACP_AUTHOR_TRACE_MAX_CHARS = 64 * 1024;
4
+ function object(value) {
5
+ return value !== null && typeof value === 'object' && !Array.isArray(value) ? value : undefined;
6
+ }
7
+ function updateFromNotification(message) {
8
+ if (message.method !== 'session/update')
9
+ throw new Error('ACP author projector received a non-update notification');
10
+ const params = object(message.params);
11
+ const update = object(params?.update);
12
+ if (!update || typeof update.sessionUpdate !== 'string')
13
+ throw new Error('ACP session/update contained no typed update');
14
+ return update;
15
+ }
16
+ function toolUpdate(update, priorTitle) {
17
+ if (update.sessionUpdate !== 'tool_call' && update.sessionUpdate !== 'tool_call_update')
18
+ throw new Error('ACP update was not a tool update');
19
+ const title = typeof update.title === 'string' && update.title.length > 0 ? update.title : priorTitle;
20
+ if (!title)
21
+ throw new Error('ACP tool update contained no title');
22
+ const rawInput = update.rawInput === undefined ? undefined : object(update.rawInput);
23
+ if (update.rawInput !== undefined && !rawInput)
24
+ throw new Error('ACP tool update rawInput was not an object');
25
+ const rawOutput = update.rawOutput === undefined ? undefined : object(update.rawOutput);
26
+ if (update.rawOutput !== undefined && !rawOutput)
27
+ throw new Error('ACP tool update rawOutput was not an object');
28
+ const output = update.output ?? rawOutput?.output;
29
+ const error = update.error ?? rawOutput?.error;
30
+ return {
31
+ sessionUpdate: update.sessionUpdate,
32
+ title,
33
+ ...(typeof update.toolCallId === 'string' ? { toolCallId: update.toolCallId } : {}),
34
+ ...(typeof update.status === 'string' ? { status: update.status } : {}),
35
+ ...(rawInput ? { rawInput } : {}),
36
+ ...(output !== undefined ? { output } : {}),
37
+ ...(error !== undefined ? { error } : {}),
38
+ };
39
+ }
40
+ function textValue(value) {
41
+ if (value === undefined)
42
+ return undefined;
43
+ return typeof value === 'string' ? value : JSON.stringify(value);
44
+ }
45
+ export function projectAcpAuthorTurn(input) {
46
+ const protectedValues = input.protectedValues ?? [];
47
+ const redactionValues = [...new Set([...protectedValues, ...(input.sensitiveValues ?? [])])];
48
+ const serialized = JSON.stringify({ terminal: input.terminal, notifications: input.notifications });
49
+ if (protectedValues.some((value) => value.length > 0 && serialized.includes(value))) {
50
+ throw new Error('ACP provider reflected a protected control-plane value');
51
+ }
52
+ const assistantChunks = [];
53
+ const toolEvents = [];
54
+ const trace = [];
55
+ const toolCalls = new Map();
56
+ let providerError = false;
57
+ for (const notification of input.notifications) {
58
+ const update = updateFromNotification(notification);
59
+ if (update.sessionUpdate === 'agent_message_chunk') {
60
+ const content = object(update.content);
61
+ const text = typeof update.text === 'string'
62
+ ? update.text
63
+ : typeof content?.text === 'string' ? content.text : undefined;
64
+ if (text === undefined)
65
+ throw new Error('ACP agent message chunk contained no text');
66
+ assistantChunks.push(text);
67
+ trace.push({ sessionUpdate: 'agent_message_chunk', text });
68
+ continue;
69
+ }
70
+ if (update.sessionUpdate === 'error') {
71
+ if (typeof update.message !== 'string' || update.message.length === 0)
72
+ throw new Error('ACP error update contained no message');
73
+ providerError = true;
74
+ trace.push({ sessionUpdate: 'error', message: update.message });
75
+ continue;
76
+ }
77
+ if (update.sessionUpdate !== 'tool_call' && update.sessionUpdate !== 'tool_call_update') {
78
+ trace.push({ sessionUpdate: update.sessionUpdate });
79
+ continue;
80
+ }
81
+ const toolCallId = typeof update.toolCallId === 'string' ? update.toolCallId : undefined;
82
+ const prior = toolCallId ? toolCalls.get(toolCallId) : undefined;
83
+ const current = toolUpdate(update, prior?.title);
84
+ if (prior && prior.title !== current.title)
85
+ throw new Error('ACP tool update changed title for one call');
86
+ const correlated = {
87
+ ...prior,
88
+ ...current,
89
+ ...(current.rawInput ?? prior?.rawInput ? { rawInput: current.rawInput ?? prior?.rawInput } : {}),
90
+ };
91
+ trace.push({
92
+ sessionUpdate: current.sessionUpdate,
93
+ title: current.title,
94
+ ...(current.status ? { status: current.status } : {}),
95
+ ...(current.rawInput ? { rawInput: current.rawInput } : {}),
96
+ });
97
+ if (current.status !== 'completed' && current.status !== 'failed') {
98
+ if (current.toolCallId)
99
+ toolCalls.set(current.toolCallId, correlated);
100
+ continue;
101
+ }
102
+ const terminalFingerprint = JSON.stringify({
103
+ status: correlated.status,
104
+ rawInput: correlated.rawInput,
105
+ output: correlated.output,
106
+ error: correlated.error,
107
+ });
108
+ if (prior?.terminalFingerprint !== undefined) {
109
+ if (prior.terminalFingerprint !== terminalFingerprint)
110
+ throw new Error('ACP tool call emitted conflicting terminal updates');
111
+ continue;
112
+ }
113
+ if (current.toolCallId)
114
+ toolCalls.set(current.toolCallId, { ...correlated, terminalFingerprint });
115
+ const normalized = input.normalizeToolName(correlated.title);
116
+ const resultText = textValue(correlated.status === 'completed' ? correlated.output : correlated.error);
117
+ toolEvents.push({
118
+ action: normalized.action,
119
+ provider: input.provider,
120
+ providerToolName: normalized.providerToolName,
121
+ ...(input.turnNumber !== undefined ? { turnNumber: input.turnNumber } : {}),
122
+ ...(correlated.rawInput ? { arguments: correlated.rawInput } : {}),
123
+ status: correlated.status === 'completed' ? 'completed' : 'error',
124
+ ...(resultText !== undefined ? { result: sanitizeToolEventResult({ content: resultText }, redactionValues) } : {}),
125
+ summary: buildSummary(normalized.action, normalized.providerToolName, correlated.rawInput),
126
+ confidence: normalized.action === 'unknown' ? 'low' : 'high',
127
+ rawSnippet: JSON.stringify(sanitizePersistenceValue({ title: normalized.providerToolName, status: correlated.status, rawInput: correlated.rawInput }, redactionValues)).slice(0, 2_000),
128
+ });
129
+ }
130
+ const assistantMessage = sanitizePersistenceValue(assistantChunks.join(''), redactionValues);
131
+ const rawTrace = JSON.stringify(sanitizePersistenceValue({
132
+ terminal: { stopReason: input.terminal.stopReason },
133
+ updates: trace,
134
+ }, redactionValues));
135
+ const traceOutput = rawTrace.length > ACP_AUTHOR_TRACE_MAX_CHARS
136
+ ? `${rawTrace.slice(0, ACP_AUTHOR_TRACE_MAX_CHARS - 1)}…`
137
+ : rawTrace;
138
+ return sanitizePersistenceValue({
139
+ rawOutput: traceOutput,
140
+ traceOutput,
141
+ assistantMessage,
142
+ visibleAssistantMessage: assistantMessage,
143
+ visibleAssistantMessageSource: 'assistant_message',
144
+ exitCode: input.terminal.stopReason === 'end_turn' && !providerError ? 0 : 1,
145
+ toolEvents,
146
+ }, redactionValues);
147
+ }
@@ -0,0 +1,39 @@
1
+ import type { AgentTurnResult } from '../../types.js';
2
+ import { type RuntimeLock } from './runtime-lock.js';
3
+ import { type AcpClient, type SpawnAcpClientOptions } from './acp-client.js';
4
+ import { type AcpToolNameNormalizer } from './acp-author-projector.js';
5
+ import type { JsonObject, ProviderId, ScenarioProviderAttachContext, ScenarioProviderAttachment } from './types.js';
6
+ export type AcpAuthorSession = {
7
+ runTurn(message: string, turnNumber?: number): Promise<AgentTurnResult>;
8
+ interrupt(reason?: string): Promise<void>;
9
+ waitForTerminal(): Promise<void>;
10
+ close(): Promise<void>;
11
+ };
12
+ export type CreateAcpAuthorSessionOptions = {
13
+ provider: Extract<ProviderId, 'cursor' | 'opencode'>;
14
+ client: AcpClient;
15
+ mcpServers: readonly JsonObject[];
16
+ normalizeToolName: AcpToolNameNormalizer;
17
+ protectedValues?: readonly string[];
18
+ sensitiveValues?: readonly string[];
19
+ assertProtectedSurfacesClean?(): Promise<void>;
20
+ closeJoinTimeoutMs?: number;
21
+ };
22
+ export type AttachAcpAuthorSessionOptions = {
23
+ provider: Extract<ProviderId, 'cursor' | 'opencode'>;
24
+ context: ScenarioProviderAttachContext;
25
+ runtimeLock: RuntimeLock;
26
+ resolvedExecutable: string;
27
+ cwd: string;
28
+ environment: NodeJS.ProcessEnv;
29
+ scanRoots: readonly string[];
30
+ normalizeToolName: AcpToolNameNormalizer;
31
+ createClient?: (options: SpawnAcpClientOptions) => AcpClient;
32
+ };
33
+ export declare function createAcpAuthorSession(options: CreateAcpAuthorSessionOptions): Promise<AcpAuthorSession>;
34
+ export declare function createAcpScenarioAttachment(session: AcpAuthorSession): ScenarioProviderAttachment<AcpAuthorSession>;
35
+ /**
36
+ * P4/P5 provider seam: attach one exact, relocatable ACP runtime to the
37
+ * coordinator and return the reusable author session as its opaque value.
38
+ */
39
+ export declare function attachAcpAuthorSession(options: AttachAcpAuthorSessionOptions): Promise<ScenarioProviderAttachment<AcpAuthorSession>>;
@@ -0,0 +1,201 @@
1
+ import { collectSensitiveEnvValues } from '../../tool-event-results.js';
2
+ import { scanBearerSurfaces } from './cleanup.js';
3
+ import { materializeProjection } from './materialize.js';
4
+ import { projectEndpoints } from './projection.js';
5
+ import { admitRuntimeLock } from './runtime-lock.js';
6
+ import { spawnAcpClient } from './acp-client.js';
7
+ import { projectAcpAuthorTurn, } from './acp-author-projector.js';
8
+ export async function createAcpAuthorSession(options) {
9
+ const sessionId = await options.client.newSession(options.mcpServers);
10
+ let activeTerminal;
11
+ let interruptSent = false;
12
+ let promptStarted = false;
13
+ let closed = false;
14
+ let closePromise;
15
+ const waitForTerminal = async () => { await activeTerminal; };
16
+ const interrupt = async () => {
17
+ if (!activeTerminal || interruptSent)
18
+ return;
19
+ interruptSent = true;
20
+ options.client.setCancelling(true);
21
+ if (promptStarted)
22
+ options.client.cancel(sessionId);
23
+ };
24
+ const session = {
25
+ async runTurn(message, turnNumber) {
26
+ if (closed)
27
+ throw new Error('ACP author session is closed');
28
+ if (activeTerminal)
29
+ throw new Error('ACP author session already has an active turn');
30
+ const checkpoint = options.client.notificationCheckpoint();
31
+ interruptSent = false;
32
+ let finishTerminal;
33
+ const terminal = new Promise((resolve) => { finishTerminal = resolve; });
34
+ activeTerminal = terminal;
35
+ let checkpointDrained = false;
36
+ try {
37
+ await options.assertProtectedSurfacesClean?.();
38
+ if (interruptSent)
39
+ throw new Error('ACP author session turn cancelled before prompt');
40
+ promptStarted = true;
41
+ const promptResult = await options.client.prompt(sessionId, message);
42
+ const notifications = options.client.drainNotifications(checkpoint);
43
+ checkpointDrained = true;
44
+ return projectAcpAuthorTurn({
45
+ provider: options.provider,
46
+ terminal: promptResult,
47
+ notifications,
48
+ normalizeToolName: options.normalizeToolName,
49
+ protectedValues: options.protectedValues,
50
+ sensitiveValues: options.sensitiveValues,
51
+ turnNumber,
52
+ });
53
+ }
54
+ finally {
55
+ if (!checkpointDrained)
56
+ options.client.drainNotifications(checkpoint);
57
+ promptStarted = false;
58
+ options.client.setCancelling(false);
59
+ finishTerminal();
60
+ if (activeTerminal === terminal)
61
+ activeTerminal = undefined;
62
+ }
63
+ },
64
+ interrupt,
65
+ waitForTerminal,
66
+ close() {
67
+ closePromise ??= (async () => {
68
+ if (closed)
69
+ return;
70
+ closed = true;
71
+ const terminal = activeTerminal;
72
+ if (terminal) {
73
+ await interrupt();
74
+ if (!await settlesWithin(terminal, options.closeJoinTimeoutMs ?? 2_000)) {
75
+ await options.client.close();
76
+ }
77
+ await terminal;
78
+ }
79
+ await options.client.close();
80
+ })();
81
+ return closePromise;
82
+ },
83
+ };
84
+ return session;
85
+ }
86
+ export function createAcpScenarioAttachment(session) {
87
+ return {
88
+ value: session,
89
+ interrupt: async (_turn, reason) => session.interrupt(reason),
90
+ waitForTerminal: async () => session.waitForTerminal(),
91
+ close: async () => session.close(),
92
+ };
93
+ }
94
+ /**
95
+ * P4/P5 provider seam: attach one exact, relocatable ACP runtime to the
96
+ * coordinator and return the reusable author session as its opaque value.
97
+ */
98
+ export async function attachAcpAuthorSession(options) {
99
+ const protectedValues = [...new Set(options.context.endpoints.flatMap((endpoint) => [
100
+ endpoint.url,
101
+ ...endpoint.headers.flatMap((header) => {
102
+ const value = options.context.resolveSecret(header.valueRef);
103
+ return [value, value.startsWith('Bearer ') ? value.slice('Bearer '.length) : value];
104
+ }),
105
+ ]).filter((value) => value.length > 0))];
106
+ const createClient = options.createClient ?? spawnAcpClient;
107
+ const sensitiveValues = collectSensitiveEnvValues(Object.fromEntries(Object.entries(options.environment).filter((entry) => typeof entry[1] === 'string')));
108
+ let client;
109
+ const admission = await admitRuntimeLock(options.runtimeLock, {
110
+ provider: options.provider,
111
+ platform: process.platform,
112
+ architecture: process.arch,
113
+ resolvedExecutable: options.resolvedExecutable,
114
+ environment: options.environment,
115
+ forbiddenSecrets: protectedValues,
116
+ async initializeControlPlane(commandPath, args) {
117
+ const initialized = createClient({
118
+ provider: options.provider,
119
+ command: commandPath,
120
+ args,
121
+ cwd: options.cwd,
122
+ environment: options.environment,
123
+ protectedValues,
124
+ });
125
+ client = initialized;
126
+ try {
127
+ const normalizedHandshake = await initialized.initialize();
128
+ return {
129
+ normalizedHandshake,
130
+ capabilities: options.runtimeLock.capabilities,
131
+ close: () => initialized.close(),
132
+ };
133
+ }
134
+ catch (error) {
135
+ await initialized.close().catch(() => undefined);
136
+ throw error;
137
+ }
138
+ },
139
+ });
140
+ const admittedClient = client;
141
+ if (!admittedClient)
142
+ throw new Error('RUNTIME_NOT_ADMITTED: ACP client unavailable');
143
+ try {
144
+ const projected = projectEndpoints(options.context.endpoints, admission.profile);
145
+ if (!Array.isArray(projected))
146
+ throw new Error(`RUNTIME_NOT_ADMITTED: ${projected.reason}`);
147
+ const mcpServers = projected.flatMap((plan) => {
148
+ const materialized = materializeProjection(plan, { resolve: options.context.resolveSecret });
149
+ if (materialized.kind !== 'acp-session' || materialized.provider !== options.provider) {
150
+ throw new Error('RUNTIME_NOT_ADMITTED: ACP projection materialized for another provider');
151
+ }
152
+ return materialized.mcpServers.map((server) => ({
153
+ type: server.type,
154
+ name: server.name,
155
+ url: server.url,
156
+ headers: server.headers.map((header) => ({ ...header })),
157
+ }));
158
+ });
159
+ const assertProtectedSurfacesClean = async () => {
160
+ for (const value of protectedValues) {
161
+ const scans = await scanBearerSurfaces(options.scanRoots, value);
162
+ if (scans.some((entry) => entry.status !== 'clean')) {
163
+ throw new Error('ACP protected projection reached a writable or unreadable surface');
164
+ }
165
+ }
166
+ if (admittedClient.inboundSecretSeen())
167
+ throw new Error('ACP provider reflected a protected control-plane value');
168
+ };
169
+ const session = await createAcpAuthorSession({
170
+ provider: options.provider,
171
+ client: admittedClient,
172
+ mcpServers,
173
+ normalizeToolName: options.normalizeToolName,
174
+ protectedValues,
175
+ sensitiveValues,
176
+ assertProtectedSurfacesClean,
177
+ });
178
+ if (admittedClient.controlPlaneSecretFrames() !== 1 || admittedClient.inboundSecretSeen()) {
179
+ throw new Error('RUNTIME_NOT_ADMITTED: ACP secret isolation failed during session admission');
180
+ }
181
+ await assertProtectedSurfacesClean();
182
+ return createAcpScenarioAttachment(session);
183
+ }
184
+ catch (error) {
185
+ try {
186
+ await admission.controlPlane.close();
187
+ }
188
+ catch (cleanupError) {
189
+ throw new AggregateError([error, cleanupError], 'ACP author-session setup and cleanup failed');
190
+ }
191
+ throw error;
192
+ }
193
+ }
194
+ async function settlesWithin(promise, timeoutMs) {
195
+ let timer;
196
+ return Promise.race([
197
+ promise.then(() => true),
198
+ new Promise((resolve) => { timer = setTimeout(() => resolve(false), timeoutMs); }),
199
+ ]).finally(() => { if (timer)
200
+ clearTimeout(timer); });
201
+ }
@@ -0,0 +1,70 @@
1
+ import { type ChildProcessWithoutNullStreams } from 'node:child_process';
2
+ import type { JsonObject, JsonValue } from './types.js';
3
+ export type JsonRpcMessage = JsonObject;
4
+ export type AcpNotificationCheckpoint = number;
5
+ export type AcpPermissionRequest = {
6
+ sessionId?: string;
7
+ toolCallId?: string;
8
+ options: readonly {
9
+ optionId: string;
10
+ kind: string;
11
+ }[];
12
+ };
13
+ export type AcpClientRequestHandler = (method: string, params: JsonValue) => Promise<JsonValue>;
14
+ export declare class AcpClientCore {
15
+ private readonly sendFrame;
16
+ private readonly onRequest;
17
+ private nextId;
18
+ private readonly pending;
19
+ private readonly notifications;
20
+ private nextNotificationSequence;
21
+ private closed;
22
+ private closeReason;
23
+ constructor(sendFrame: (message: JsonRpcMessage) => void, onRequest: AcpClientRequestHandler);
24
+ request(method: string, params: JsonObject, timeoutMs?: number): Promise<JsonValue>;
25
+ notify(method: string, params: JsonObject): void;
26
+ receiveLine(line: string): Promise<void>;
27
+ observedNotifications(): readonly JsonRpcMessage[];
28
+ notificationCheckpoint(): AcpNotificationCheckpoint;
29
+ drainNotifications(checkpoint: AcpNotificationCheckpoint): readonly JsonRpcMessage[];
30
+ close(reason?: string): void;
31
+ }
32
+ export type AcpNormalizedHandshake = JsonObject & {
33
+ normalizerVersion: 'pathgrade-acp-v1-capabilities-v1';
34
+ provider: 'cursor' | 'opencode';
35
+ protocolVersion: 1;
36
+ httpMcp: true;
37
+ sessionClose: boolean;
38
+ };
39
+ export type AcpPromptResult = {
40
+ stopReason: string;
41
+ };
42
+ export type AcpClient = {
43
+ initialize(): Promise<AcpNormalizedHandshake>;
44
+ newSession(mcpServers: readonly JsonObject[]): Promise<string>;
45
+ prompt(sessionId: string, text: string): Promise<AcpPromptResult>;
46
+ cancel(sessionId: string): void;
47
+ close(): Promise<void>;
48
+ setCancelling(value: boolean): void;
49
+ isOpen(): boolean;
50
+ controlPlaneSecretFrames(): number;
51
+ inboundSecretSeen(): boolean;
52
+ notificationCheckpoint(): AcpNotificationCheckpoint;
53
+ drainNotifications(checkpoint: AcpNotificationCheckpoint): readonly JsonRpcMessage[];
54
+ notifications(): readonly JsonRpcMessage[];
55
+ };
56
+ export type SpawnAcpClientOptions = {
57
+ provider: 'cursor' | 'opencode';
58
+ command: string;
59
+ args: readonly string[];
60
+ cwd: string;
61
+ environment: NodeJS.ProcessEnv;
62
+ protectedValues: readonly string[];
63
+ spawnProcess?: SpawnAcpProcess;
64
+ };
65
+ export type SpawnAcpProcess = (command: string, args: readonly string[], options: {
66
+ cwd: string;
67
+ env: NodeJS.ProcessEnv;
68
+ stdio: ['pipe', 'pipe', 'pipe'];
69
+ }) => ChildProcessWithoutNullStreams;
70
+ export declare function spawnAcpClient(options: SpawnAcpClientOptions): AcpClient;