ds-harness-remote 0.5.8 → 0.5.10
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/dist/acp/adapter.d.ts +1 -1
- package/dist/acp/adapter.d.ts.map +1 -1
- package/dist/acp/adapters/claude-history.d.ts +3 -0
- package/dist/acp/adapters/claude-history.d.ts.map +1 -0
- package/dist/acp/adapters/claude-history.js +118 -0
- package/dist/acp/adapters/claude-history.js.map +1 -0
- package/dist/acp/adapters/claude-process.d.ts +21 -0
- package/dist/acp/adapters/claude-process.d.ts.map +1 -0
- package/dist/acp/adapters/claude-process.js +147 -0
- package/dist/acp/adapters/claude-process.js.map +1 -0
- package/dist/acp/adapters/cursor-process.d.ts +9 -4
- package/dist/acp/adapters/cursor-process.d.ts.map +1 -1
- package/dist/acp/adapters/cursor-process.js +31 -3
- package/dist/acp/adapters/cursor-process.js.map +1 -1
- package/dist/acp/adapters/opencode-process.d.ts +67 -0
- package/dist/acp/adapters/opencode-process.d.ts.map +1 -0
- package/dist/acp/adapters/opencode-process.js +272 -0
- package/dist/acp/adapters/opencode-process.js.map +1 -0
- package/dist/acp/adapters/opencode.d.ts +8 -0
- package/dist/acp/adapters/opencode.d.ts.map +1 -0
- package/dist/acp/adapters/opencode.js +8 -0
- package/dist/acp/adapters/opencode.js.map +1 -0
- package/dist/acp/adapters/pi-process.d.ts +42 -0
- package/dist/acp/adapters/pi-process.d.ts.map +1 -0
- package/dist/acp/adapters/pi-process.js +410 -0
- package/dist/acp/adapters/pi-process.js.map +1 -0
- package/dist/acp/gateway.d.ts +8 -0
- package/dist/acp/gateway.d.ts.map +1 -1
- package/dist/acp/gateway.js +299 -67
- package/dist/acp/gateway.js.map +1 -1
- package/dist/acp/method-policy.d.ts +19 -19
- package/dist/acp/method-policy.js +5 -5
- package/dist/acp/method-policy.js.map +1 -1
- package/dist/acp/pi-tool-result.d.ts +9 -0
- package/dist/acp/pi-tool-result.d.ts.map +1 -0
- package/dist/acp/pi-tool-result.js +22 -0
- package/dist/acp/pi-tool-result.js.map +1 -0
- package/dist/acp/virtual-catalog.d.ts +50 -0
- package/dist/acp/virtual-catalog.d.ts.map +1 -0
- package/dist/acp/virtual-catalog.js +103 -0
- package/dist/acp/virtual-catalog.js.map +1 -0
- package/dist/acp/virtual-contract.d.ts +165 -0
- package/dist/acp/virtual-contract.d.ts.map +1 -0
- package/dist/acp/virtual-contract.js +216 -0
- package/dist/acp/virtual-contract.js.map +1 -0
- package/dist/acp/virtual-event-carrier.d.ts +50 -0
- package/dist/acp/virtual-event-carrier.d.ts.map +1 -0
- package/dist/acp/virtual-event-carrier.js +222 -0
- package/dist/acp/virtual-event-carrier.js.map +1 -0
- package/dist/acp/virtual-harness.d.ts +13 -112
- package/dist/acp/virtual-harness.d.ts.map +1 -1
- package/dist/acp/virtual-harness.js +129 -911
- package/dist/acp/virtual-harness.js.map +1 -1
- package/dist/acp/virtual-live.d.ts +78 -0
- package/dist/acp/virtual-live.d.ts.map +1 -0
- package/dist/acp/virtual-live.js +513 -0
- package/dist/acp/virtual-live.js.map +1 -0
- package/dist/acp/workspace-tools.js +1 -1
- package/dist/acp/workspace-tools.js.map +1 -1
- package/dist/acp.d.ts.map +1 -1
- package/dist/acp.js +1 -0
- package/dist/acp.js.map +1 -1
- package/dist/client/header.d.ts +10 -0
- package/dist/client/header.d.ts.map +1 -0
- package/dist/client/header.js +169 -0
- package/dist/client/header.js.map +1 -0
- package/dist/client/icons.d.ts +3 -0
- package/dist/client/icons.d.ts.map +1 -0
- package/dist/client/icons.js +3 -0
- package/dist/client/icons.js.map +1 -0
- package/dist/client/navigation.d.ts +5 -0
- package/dist/client/navigation.d.ts.map +1 -0
- package/dist/client/navigation.js +30 -0
- package/dist/client/navigation.js.map +1 -0
- package/dist/client/presentation.d.ts +17 -0
- package/dist/client/presentation.d.ts.map +1 -0
- package/dist/client/presentation.js +208 -0
- package/dist/client/presentation.js.map +1 -0
- package/dist/client/progress.d.ts +10 -0
- package/dist/client/progress.d.ts.map +1 -0
- package/dist/client/progress.js +82 -0
- package/dist/client/progress.js.map +1 -0
- package/dist/client/settings.d.ts +10 -0
- package/dist/client/settings.d.ts.map +1 -0
- package/dist/client/settings.js +335 -0
- package/dist/client/settings.js.map +1 -0
- package/dist/client/shell.d.ts +6 -0
- package/dist/client/shell.d.ts.map +1 -0
- package/dist/client/shell.js +128 -0
- package/dist/client/shell.js.map +1 -0
- package/dist/client/types.d.ts +264 -0
- package/dist/client/types.d.ts.map +1 -0
- package/dist/client/types.js +2 -0
- package/dist/client/types.js.map +1 -0
- package/dist/client/workspaces.d.ts +11 -0
- package/dist/client/workspaces.d.ts.map +1 -0
- package/dist/client/workspaces.js +1179 -0
- package/dist/client/workspaces.js.map +1 -0
- package/dist/client-capabilities.d.ts +6 -0
- package/dist/client-capabilities.d.ts.map +1 -0
- package/dist/client-capabilities.js +88 -0
- package/dist/client-capabilities.js.map +1 -0
- package/dist/client-codex-streams.d.ts +23 -0
- package/dist/client-codex-streams.d.ts.map +1 -0
- package/dist/client-codex-streams.js +156 -0
- package/dist/client-codex-streams.js.map +1 -0
- package/dist/client-connection.d.ts +38 -0
- package/dist/client-connection.d.ts.map +1 -0
- package/dist/client-connection.js +210 -0
- package/dist/client-connection.js.map +1 -0
- package/dist/client-control.d.ts +48 -0
- package/dist/client-control.d.ts.map +1 -0
- package/dist/client-control.js +273 -0
- package/dist/client-control.js.map +1 -0
- package/dist/client-locale.d.ts +8 -0
- package/dist/client-locale.d.ts.map +1 -1
- package/dist/client-locale.js +16 -0
- package/dist/client-locale.js.map +1 -1
- package/dist/client-runtime-contract.d.ts +161 -0
- package/dist/client-runtime-contract.d.ts.map +1 -0
- package/dist/client-runtime-contract.js +127 -0
- package/dist/client-runtime-contract.js.map +1 -0
- package/dist/client-runtime.d.ts +24 -126
- package/dist/client-runtime.d.ts.map +1 -1
- package/dist/client-runtime.js +171 -867
- package/dist/client-runtime.js.map +1 -1
- package/dist/client-secure-transport.d.ts +1 -0
- package/dist/client-secure-transport.d.ts.map +1 -1
- package/dist/client-secure-transport.js +35 -16
- package/dist/client-secure-transport.js.map +1 -1
- package/dist/client-workspace-queries.d.ts +12 -0
- package/dist/client-workspace-queries.d.ts.map +1 -0
- package/dist/client-workspace-queries.js +66 -0
- package/dist/client-workspace-queries.js.map +1 -0
- package/dist/client.d.ts.map +1 -1
- package/dist/client.github.js +2091 -1896
- package/dist/client.js +2091 -1896
- package/dist/client.js.map +1 -1
- package/dist/codex/domain.d.ts.map +1 -1
- package/dist/codex/domain.js +67 -46
- package/dist/codex/domain.js.map +1 -1
- package/dist/codex/method-policy.d.ts +9 -9
- package/dist/codex/virtual-catalog.d.ts +90 -0
- package/dist/codex/virtual-catalog.d.ts.map +1 -0
- package/dist/codex/virtual-catalog.js +317 -0
- package/dist/codex/virtual-catalog.js.map +1 -0
- package/dist/codex/virtual-contract.d.ts +96 -0
- package/dist/codex/virtual-contract.d.ts.map +1 -0
- package/dist/codex/virtual-contract.js +192 -0
- package/dist/codex/virtual-contract.js.map +1 -0
- package/dist/codex/virtual-event-carrier.d.ts +52 -0
- package/dist/codex/virtual-event-carrier.d.ts.map +1 -0
- package/dist/codex/virtual-event-carrier.js +212 -0
- package/dist/codex/virtual-event-carrier.js.map +1 -0
- package/dist/codex/virtual-harness.d.ts +16 -117
- package/dist/codex/virtual-harness.d.ts.map +1 -1
- package/dist/codex/virtual-harness.js +123 -1909
- package/dist/codex/virtual-harness.js.map +1 -1
- package/dist/codex/virtual-history.d.ts +74 -0
- package/dist/codex/virtual-history.d.ts.map +1 -0
- package/dist/codex/virtual-history.js +546 -0
- package/dist/codex/virtual-history.js.map +1 -0
- package/dist/codex/virtual-live.d.ts +75 -0
- package/dist/codex/virtual-live.d.ts.map +1 -0
- package/dist/codex/virtual-live.js +563 -0
- package/dist/codex/virtual-live.js.map +1 -0
- package/dist/codex/virtual-values.d.ts +29 -0
- package/dist/codex/virtual-values.d.ts.map +1 -0
- package/dist/codex/virtual-values.js +75 -0
- package/dist/codex/virtual-values.js.map +1 -0
- package/dist/config.js +4 -4
- package/dist/config.js.map +1 -1
- package/dist/control-runtime.js +2 -2
- package/dist/control-runtime.js.map +1 -1
- package/dist/harness-remote-bridge.d.ts +2 -1
- package/dist/harness-remote-bridge.d.ts.map +1 -1
- package/dist/harness-remote-bridge.js +11 -3
- package/dist/harness-remote-bridge.js.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +16044 -13878
- package/dist/index.js.map +4 -4
- package/dist/plugin-route-dispatcher.d.ts +2 -2
- package/dist/plugin-route-dispatcher.d.ts.map +1 -1
- package/dist/plugin-route-dispatcher.js +2 -2
- package/dist/plugin-route-dispatcher.js.map +1 -1
- package/dist/remote-typert-gateway.js +7 -1
- package/dist/remote-typert-gateway.js.map +1 -1
- package/dist/server-connection.d.ts +1 -0
- package/dist/server-connection.d.ts.map +1 -1
- package/dist/server-connection.js +6 -1
- package/dist/server-connection.js.map +1 -1
- package/dist/service.d.ts +1 -1
- package/dist/service.d.ts.map +1 -1
- package/dist/service.js +6 -5
- package/dist/service.js.map +1 -1
- package/dist/terminal-carrier-compat.d.ts +14 -0
- package/dist/terminal-carrier-compat.d.ts.map +1 -0
- package/dist/terminal-carrier-compat.js +259 -0
- package/dist/terminal-carrier-compat.js.map +1 -0
- package/dist/typert-gateway-contract.d.ts +6 -1
- package/dist/typert-gateway-contract.d.ts.map +1 -1
- package/dist/typert-gateway-switch.d.ts +2 -1
- package/dist/typert-gateway-switch.d.ts.map +1 -1
- package/dist/typert-gateway-switch.js +31 -15
- package/dist/typert-gateway-switch.js.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.d.ts.map +1 -1
- package/dist/version.js +1 -1
- package/dist/version.js.map +1 -1
- package/dsh-plugin.json +1 -1
- package/package.json +21 -19
package/dist/acp/gateway.js
CHANGED
|
@@ -8,7 +8,10 @@ import { basename, isAbsolute, join, relative, resolve } from 'node:path';
|
|
|
8
8
|
import { RpcError } from '../safe-error.js';
|
|
9
9
|
import { PLUGIN_VERSION } from '../version.js';
|
|
10
10
|
import { CursorAcpClient, CursorAcpError, } from './adapters/cursor-process.js';
|
|
11
|
+
import { ClaudeAcpClient } from './adapters/claude-process.js';
|
|
12
|
+
import { PiAcpClient } from './adapters/pi-process.js';
|
|
11
13
|
import { AntigravityAcpClient } from './adapters/antigravity-process.js';
|
|
14
|
+
import { OpencodeAcpClient, OpencodeAcpError } from './adapters/opencode-process.js';
|
|
12
15
|
import { ACP_METHOD_ALLOWLIST, isSessionMutation, parseAcpCall, sessionIdFromParams, } from './method-policy.js';
|
|
13
16
|
import { discoverAntigravitySessions, discoverAntigravityWorkspaces, loadTranscriptEvents } from './adapters/antigravity/transcript-loader.js';
|
|
14
17
|
import { AcpPeerBridge } from './peer-bridge.js';
|
|
@@ -60,6 +63,10 @@ export class AcpRemoteGateway {
|
|
|
60
63
|
/** Catch-up buffer for turns that finish while the Client is reconnecting. */
|
|
61
64
|
recentFrames = new Map();
|
|
62
65
|
/** Per-prompt live updates; attached to prompt_completed when streaming was lossy. */
|
|
66
|
+
promptsInProgress = new Set();
|
|
67
|
+
cancelledSessions = new Set();
|
|
68
|
+
/** Identity fences asynchronous Pi preparation from a later retry of the same session. */
|
|
69
|
+
preparingPrompts = new Map();
|
|
63
70
|
turnCatchUp = new Map();
|
|
64
71
|
startPromise;
|
|
65
72
|
startingClients = new Map();
|
|
@@ -69,6 +76,13 @@ export class AcpRemoteGateway {
|
|
|
69
76
|
if (backend.id === 'antigravity') {
|
|
70
77
|
return new AntigravityAcpClient(binary, targetLogger, undefined, { args: backend.args, cwd: backend.cwd });
|
|
71
78
|
}
|
|
79
|
+
if (backend.id === 'claude')
|
|
80
|
+
return new ClaudeAcpClient(binary, targetLogger, undefined, { args: backend.args, cwd: backend.cwd });
|
|
81
|
+
if (backend.id === 'pi')
|
|
82
|
+
return new PiAcpClient(binary, targetLogger, { args: backend.args, cwd: backend.cwd });
|
|
83
|
+
if (backend.id === 'opencode') {
|
|
84
|
+
return new OpencodeAcpClient(binary, targetLogger, undefined, { args: backend.args, cwd: backend.cwd });
|
|
85
|
+
}
|
|
72
86
|
return new CursorAcpClient(binary, targetLogger, undefined, { args: backend.args, cwd: backend.cwd });
|
|
73
87
|
}, restartDelaysMs = DEFAULT_RESTART_DELAYS_MS, terminalEnabled = () => false, terminalSpawner) {
|
|
74
88
|
this.config = config;
|
|
@@ -82,7 +96,7 @@ export class AcpRemoteGateway {
|
|
|
82
96
|
enabledBackends() {
|
|
83
97
|
if (!this.config.enabled)
|
|
84
98
|
return [];
|
|
85
|
-
return this.config.backends.filter(item => item.enabled && ['antigravity', 'cursor'].includes(item.id)).map(item => item.id);
|
|
99
|
+
return this.config.backends.filter(item => item.enabled && ['antigravity', 'cursor', 'pi', 'claude', 'opencode'].includes(item.id)).map(item => item.id);
|
|
86
100
|
}
|
|
87
101
|
start() {
|
|
88
102
|
if (this.closed)
|
|
@@ -136,17 +150,17 @@ export class AcpRemoteGateway {
|
|
|
136
150
|
const bridge = new AcpPeerBridge(this, context, publish, this.logger);
|
|
137
151
|
this.peers.set(context.connectionId, bridge);
|
|
138
152
|
const workspaceBridge = new WorkspaceBridge(async (rawId) => {
|
|
139
|
-
const match = rawId.match(/^(acp|cursor):(.+)$/);
|
|
153
|
+
const match = rawId.match(/^(acp|cursor|pi|opencode):(.+)$/);
|
|
140
154
|
const session = match ? this.sessionCwds.get(match[2]) : undefined;
|
|
141
|
-
const backend = match?.[1] === 'acp' ? 'antigravity' : 'cursor';
|
|
155
|
+
const backend = match?.[1] === 'acp' ? 'antigravity' : match?.[1] === 'pi' ? 'pi' : match?.[1] === 'opencode' ? 'opencode' : 'cursor';
|
|
142
156
|
if (!session || session.backend !== backend)
|
|
143
157
|
throw new RpcError('ACP_WORKSPACE_UNAVAILABLE', 'The ACP session has no trusted workspace.');
|
|
144
158
|
this.requireAcp(backend);
|
|
145
159
|
this.requireSessionOwner(context.connectionId, match[2]);
|
|
146
160
|
return session.cwd;
|
|
147
161
|
}, this.terminalEnabled, this.workspaceState, this.terminalSpawner, {
|
|
148
|
-
domain: 'ACP', owns: value => typeof value === 'string' && /^(acp|cursor):/.test(value),
|
|
149
|
-
parse: value => typeof value === 'string' && /^(acp|cursor):[A-Za-z0-9_-]{1,240}$/.test(value)
|
|
162
|
+
domain: 'ACP', owns: value => typeof value === 'string' && /^(acp|cursor|pi|opencode):/.test(value),
|
|
163
|
+
parse: value => typeof value === 'string' && /^(acp|cursor|pi|opencode):[A-Za-z0-9_-]{1,240}$/.test(value)
|
|
150
164
|
? { sessionId: value, nativeId: value } : undefined,
|
|
151
165
|
});
|
|
152
166
|
this.workspaceTools.set(context.connectionId, new AcpWorkspaceTools(workspaceBridge, new TerminalPolicy(this.terminalEnabled, context.peerDeviceId, this.terminalOwners)));
|
|
@@ -160,8 +174,12 @@ export class AcpRemoteGateway {
|
|
|
160
174
|
sessionId: typeof call.params?.sessionId === 'string' ? shortSessionId(call.params.sessionId) : undefined,
|
|
161
175
|
});
|
|
162
176
|
if (typeof call.params.backend === 'string') {
|
|
177
|
+
const prefix = typeof call.params.sessionId === 'string' ? /^(acp|cursor|pi|claude):/.exec(call.params.sessionId)?.[1] : undefined;
|
|
178
|
+
if (prefix && (prefix === 'acp' ? 'antigravity' : prefix) !== call.params.backend) {
|
|
179
|
+
throw new RpcError('INVALID_MESSAGE', 'The ACP session backend does not match.');
|
|
180
|
+
}
|
|
163
181
|
const sessionId = call.method === 'dsh/sessionHistory'
|
|
164
|
-
? String(call.params.sessionId).replace(/^(acp|cursor):/, '')
|
|
182
|
+
? String(call.params.sessionId).replace(/^(acp|cursor|pi|claude|opencode):/, '')
|
|
165
183
|
: sessionIdFromParams(call.method, call.params);
|
|
166
184
|
const boundBackend = sessionId === undefined ? undefined : this.sessionBackends.get(sessionId);
|
|
167
185
|
if (boundBackend !== undefined && boundBackend !== call.params.backend) {
|
|
@@ -178,6 +196,11 @@ export class AcpRemoteGateway {
|
|
|
178
196
|
return this.listDirectory(String(call.params.path));
|
|
179
197
|
}
|
|
180
198
|
if (call.method === 'dsh/workspaceList') {
|
|
199
|
+
const requestedBackend = typeof call.params.backend === 'string' ? call.params.backend : this.defaultBackend();
|
|
200
|
+
if (requestedBackend === 'pi' || requestedBackend === 'claude') {
|
|
201
|
+
const sessions = await this.listCatalogSessions(requestedBackend);
|
|
202
|
+
return [...new Set(sessions.map(item => item.cwd))].map(path => ({ path, title: basename(path) || 'workspace' }));
|
|
203
|
+
}
|
|
181
204
|
const candidates = new Set();
|
|
182
205
|
const cwd = process.cwd();
|
|
183
206
|
if (cwd)
|
|
@@ -243,23 +266,41 @@ export class AcpRemoteGateway {
|
|
|
243
266
|
this.logger.info('ACP session list fetched', { count: items.length });
|
|
244
267
|
return { items };
|
|
245
268
|
}
|
|
269
|
+
if (backend === 'pi' || backend === 'claude') {
|
|
270
|
+
const cwd = path ? await this.requireExistingDirectory(path) : undefined;
|
|
271
|
+
const sessions = await this.listCatalogSessions(backend, cwd);
|
|
272
|
+
return { items: sessions.slice(0, limit).map(item => ({ conversationId: item.sessionId, title: item.title ?? 'Untitled Session', createdAt: Date.parse(item.updatedAt ?? '') || 0, updatedAt: Date.parse(item.updatedAt ?? '') || 0, cwd: item.cwd })) };
|
|
273
|
+
}
|
|
246
274
|
return { items: [] };
|
|
247
275
|
}
|
|
248
276
|
if (call.method === 'dsh/sessionHistory') {
|
|
249
277
|
const rawSessionId = String(call.params.sessionId);
|
|
250
|
-
const conversationId = rawSessionId.startsWith('acp:')
|
|
278
|
+
const conversationId = rawSessionId.startsWith('claude:') ? rawSessionId.slice(7) : rawSessionId.startsWith('pi:') ? rawSessionId.slice(3) : rawSessionId.startsWith('acp:')
|
|
251
279
|
? rawSessionId.slice('acp:'.length)
|
|
252
280
|
: rawSessionId.startsWith('cursor:')
|
|
253
281
|
? rawSessionId.slice('cursor:'.length)
|
|
254
|
-
: rawSessionId
|
|
282
|
+
: rawSessionId.startsWith('opencode:')
|
|
283
|
+
? rawSessionId.slice('opencode:'.length)
|
|
284
|
+
: rawSessionId;
|
|
255
285
|
const boundBackend = this.sessionBackends.get(conversationId);
|
|
256
286
|
const backend = typeof call.params.backend === 'string' ? call.params.backend : boundBackend ?? this.defaultBackend();
|
|
257
287
|
if ((boundBackend !== undefined && boundBackend !== backend)
|
|
288
|
+
|| (rawSessionId.startsWith('claude:') && backend !== 'claude')
|
|
289
|
+
|| (rawSessionId.startsWith('pi:') && backend !== 'pi')
|
|
258
290
|
|| (rawSessionId.startsWith('cursor:') && backend !== 'cursor')
|
|
291
|
+
|| (rawSessionId.startsWith('opencode:') && backend !== 'opencode')
|
|
259
292
|
|| (rawSessionId.startsWith('acp:') && backend !== 'antigravity')) {
|
|
260
293
|
throw new RpcError('INVALID_MESSAGE', 'The ACP session backend does not match.');
|
|
261
294
|
}
|
|
262
295
|
this.requireAcp(backend);
|
|
296
|
+
if (backend === 'pi' || backend === 'claude') {
|
|
297
|
+
const cwd = await this.catalogSessionCwd(backend, conversationId);
|
|
298
|
+
this.requireSessionOwner(connectionId, conversationId);
|
|
299
|
+
const client = this.requireAcp(backend);
|
|
300
|
+
if (!client.history)
|
|
301
|
+
throw new RpcError('METHOD_NOT_ALLOWED', 'The ACP backend does not support history.');
|
|
302
|
+
return { events: await client.history(conversationId, cwd) };
|
|
303
|
+
}
|
|
263
304
|
if (backend !== 'antigravity')
|
|
264
305
|
return { events: [] };
|
|
265
306
|
const events = await loadTranscriptEvents(conversationId, rawSessionId);
|
|
@@ -282,6 +323,10 @@ export class AcpRemoteGateway {
|
|
|
282
323
|
});
|
|
283
324
|
const sessionId = readSessionId(result);
|
|
284
325
|
if (sessionId !== undefined) {
|
|
326
|
+
const bound = this.sessionBackends.get(sessionId);
|
|
327
|
+
if (this.sessionOwners.has(sessionId) || this.sessionCwds.has(sessionId) || (bound !== undefined && bound !== requestedBackend)) {
|
|
328
|
+
throw new RpcError('ACP_SESSION_COLLISION', 'The ACP backend returned an existing session identifier.');
|
|
329
|
+
}
|
|
285
330
|
this.sessionOwners.set(sessionId, connectionId);
|
|
286
331
|
this.sessionBackends.set(sessionId, requestedBackend);
|
|
287
332
|
this.sessionCwds.set(sessionId, { backend: requestedBackend, cwd });
|
|
@@ -289,7 +334,7 @@ export class AcpRemoteGateway {
|
|
|
289
334
|
return sanitizeSessionResult(result);
|
|
290
335
|
}
|
|
291
336
|
const sessionId = sessionIdFromParams(call.method, call.params);
|
|
292
|
-
if (sessionId !== undefined)
|
|
337
|
+
if (sessionId !== undefined && call.method !== 'session/load')
|
|
293
338
|
this.requireSessionAccess(connectionId, sessionId, call.method);
|
|
294
339
|
if (call.method === 'dsh/toolCall') {
|
|
295
340
|
this.requireSessionOwner(connectionId, sessionId);
|
|
@@ -307,14 +352,24 @@ export class AcpRemoteGateway {
|
|
|
307
352
|
}
|
|
308
353
|
if (call.method === 'session/load') {
|
|
309
354
|
const boundBackend = sessionId !== undefined ? this.sessionBackends.get(sessionId) : undefined;
|
|
310
|
-
|
|
355
|
+
if (typeof call.params.backend !== 'string')
|
|
356
|
+
throw new RpcError('INVALID_MESSAGE', 'Session load requires an explicit backend.');
|
|
357
|
+
const targetBackend = call.params.backend;
|
|
358
|
+
if (targetBackend === 'claude' && this.promptsInProgress.has(sessionId))
|
|
359
|
+
throw new RpcError('PROMPT_IN_PROGRESS', 'An ACP reply is already in progress.');
|
|
311
360
|
if (boundBackend !== undefined && targetBackend !== boundBackend) {
|
|
312
361
|
throw new RpcError('INVALID_MESSAGE', 'The ACP session backend does not match.');
|
|
313
362
|
}
|
|
363
|
+
if (targetBackend === 'pi' || targetBackend === 'claude')
|
|
364
|
+
await this.catalogSessionCwd(targetBackend, sessionId);
|
|
365
|
+
this.requireSessionOwner(connectionId, sessionId);
|
|
314
366
|
const acp = this.requireAcp(targetBackend);
|
|
315
|
-
const result = await acp.call(call.method, { ...call.params, ...(sessionId && this.sessionCwds.get(sessionId) ? { cwd: this.sessionCwds.get(sessionId).cwd } : {}) });
|
|
367
|
+
const result = await acp.call(call.method, { ...call.params, ...(sessionId && this.sessionCwds.get(sessionId) ? { cwd: this.sessionCwds.get(sessionId).cwd, mcpServers: [] } : {}) });
|
|
316
368
|
const loadedId = readSessionId(result) ?? sessionId;
|
|
317
369
|
if (loadedId !== undefined) {
|
|
370
|
+
if (loadedId !== sessionId || (this.sessionBackends.has(loadedId) && targetBackend !== undefined && this.sessionBackends.get(loadedId) !== targetBackend)) {
|
|
371
|
+
throw new RpcError('ACP_SESSION_COLLISION', 'The ACP backend returned a different session identifier.');
|
|
372
|
+
}
|
|
318
373
|
this.sessionOwners.set(loadedId, connectionId);
|
|
319
374
|
if (targetBackend)
|
|
320
375
|
this.sessionBackends.set(loadedId, targetBackend);
|
|
@@ -324,6 +379,11 @@ export class AcpRemoteGateway {
|
|
|
324
379
|
if (isSessionMutation(call.method) && sessionId !== undefined) {
|
|
325
380
|
this.requireSessionOwner(connectionId, sessionId);
|
|
326
381
|
}
|
|
382
|
+
if ((call.method === 'dsh/sessionModels' || call.method === 'dsh/selectModel') && call.params.backend === 'pi') {
|
|
383
|
+
this.requireSessionOwner(connectionId, sessionId);
|
|
384
|
+
const cwd = await this.piSessionCwd(sessionId);
|
|
385
|
+
await this.requireAcp('pi').call('session/load', { sessionId, cwd, mcpServers: [] });
|
|
386
|
+
}
|
|
327
387
|
if (call.method === 'dsh/selectModel') {
|
|
328
388
|
const session = this.sessionCwds.get(sessionId);
|
|
329
389
|
if (!session || session.backend !== call.params.backend) {
|
|
@@ -340,6 +400,35 @@ export class AcpRemoteGateway {
|
|
|
340
400
|
// interleaved agent.acp.frame events (Android stays on "正在回复" with no
|
|
341
401
|
// thought/text). Accept immediately and finish via stream updates.
|
|
342
402
|
if (call.method === 'session/prompt' && sessionId !== undefined) {
|
|
403
|
+
if (this.promptsInProgress.has(sessionId))
|
|
404
|
+
throw new RpcError('PROMPT_IN_PROGRESS', 'An ACP reply is already in progress.');
|
|
405
|
+
this.promptsInProgress.add(sessionId);
|
|
406
|
+
this.cancelledSessions.delete(sessionId);
|
|
407
|
+
if (this.sessionBackends.get(sessionId) === 'pi') {
|
|
408
|
+
const preparation = { cancelled: false };
|
|
409
|
+
this.preparingPrompts.set(sessionId, preparation);
|
|
410
|
+
try {
|
|
411
|
+
const cwd = await this.piSessionCwd(sessionId);
|
|
412
|
+
if (preparation.cancelled)
|
|
413
|
+
return { accepted: false, stopReason: 'cancelled' };
|
|
414
|
+
await this.requireAcp('pi').call('session/load', { sessionId, cwd, mcpServers: [] });
|
|
415
|
+
if (preparation.cancelled)
|
|
416
|
+
return { accepted: false, stopReason: 'cancelled' };
|
|
417
|
+
}
|
|
418
|
+
catch (error) {
|
|
419
|
+
if (preparation.cancelled)
|
|
420
|
+
return { accepted: false, stopReason: 'cancelled' };
|
|
421
|
+
this.promptsInProgress.delete(sessionId);
|
|
422
|
+
throw error;
|
|
423
|
+
}
|
|
424
|
+
finally {
|
|
425
|
+
if (this.preparingPrompts.get(sessionId) === preparation) {
|
|
426
|
+
this.preparingPrompts.delete(sessionId);
|
|
427
|
+
if (preparation.cancelled)
|
|
428
|
+
this.promptsInProgress.delete(sessionId);
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
}
|
|
343
432
|
const ownerPeer = this.peers.get(connectionId);
|
|
344
433
|
if (ownerPeer !== undefined && !ownerPeer.hasStreamFor(sessionId)) {
|
|
345
434
|
this.logger?.warn('Cursor prompt started without an open ACP stream', {
|
|
@@ -350,6 +439,29 @@ export class AcpRemoteGateway {
|
|
|
350
439
|
void this.runPromptInBackground(sessionId, call.params);
|
|
351
440
|
return { accepted: true, stopReason: 'in_progress' };
|
|
352
441
|
}
|
|
442
|
+
if (call.method === 'session/cancel' && sessionId !== undefined) {
|
|
443
|
+
this.cancelledSessions.add(sessionId);
|
|
444
|
+
const preparation = this.preparingPrompts.get(sessionId);
|
|
445
|
+
if (preparation) {
|
|
446
|
+
// No turn exists upstream yet. Do not send a late cancel into a retry
|
|
447
|
+
// sharing this load: cancel only this preparation identity.
|
|
448
|
+
preparation.cancelled = true;
|
|
449
|
+
this.preparingPrompts.delete(sessionId);
|
|
450
|
+
this.promptsInProgress.delete(sessionId);
|
|
451
|
+
return { stopReason: 'cancelled' };
|
|
452
|
+
}
|
|
453
|
+
const pending = [...this.approvals].filter(([, approval]) => approval.sessionId === sessionId && approval.backend === this.sessionBackends.get(sessionId));
|
|
454
|
+
for (const [handle] of pending)
|
|
455
|
+
this.approvals.delete(handle);
|
|
456
|
+
// Send cancellation before rejecting approvals: rejecting a blocked tool first
|
|
457
|
+
// can otherwise complete the turn as end_turn before upstream sees cancel.
|
|
458
|
+
try {
|
|
459
|
+
return sanitizeSessionResult(await this.requireAcp(this.sessionBackends.get(sessionId)).call(call.method, call.params));
|
|
460
|
+
}
|
|
461
|
+
finally {
|
|
462
|
+
await Promise.all(pending.map(([, approval]) => approval.client.respondError(approval.upstreamId, -32800, 'ACP session cancelled.').catch(() => undefined)));
|
|
463
|
+
}
|
|
464
|
+
}
|
|
353
465
|
const sessionBackend = sessionId !== undefined ? this.sessionBackends.get(sessionId) : undefined;
|
|
354
466
|
return sanitizeSessionResult(await this.requireAcp(typeof call.params.backend === 'string' ? call.params.backend : sessionBackend).call(call.method, call.params));
|
|
355
467
|
}
|
|
@@ -399,6 +511,9 @@ export class AcpRemoteGateway {
|
|
|
399
511
|
},
|
|
400
512
|
}).catch(() => undefined);
|
|
401
513
|
}
|
|
514
|
+
finally {
|
|
515
|
+
this.promptsInProgress.delete(sessionId);
|
|
516
|
+
}
|
|
402
517
|
}
|
|
403
518
|
async respond(connectionId, input) {
|
|
404
519
|
const params = parseRespondEnvelope(input);
|
|
@@ -410,13 +525,18 @@ export class AcpRemoteGateway {
|
|
|
410
525
|
if (pending.connectionId !== connectionId) {
|
|
411
526
|
throw new RpcError('CURSOR_APPROVAL_NOT_FOUND', 'The Cursor approval is missing, expired, or belongs to another connection.');
|
|
412
527
|
}
|
|
528
|
+
if (this.sessionOwners.get(pending.sessionId) !== connectionId
|
|
529
|
+
|| this.sessionBackends.get(pending.sessionId) !== pending.backend
|
|
530
|
+
|| this.requireAcp(pending.backend) !== pending.client) {
|
|
531
|
+
throw new RpcError('CURSOR_APPROVAL_NOT_FOUND', 'The ACP approval scope is no longer active.');
|
|
532
|
+
}
|
|
533
|
+
const result = mapPermissionDecision(params, pending);
|
|
413
534
|
this.approvals.delete(params.requestHandle);
|
|
414
|
-
const acp =
|
|
535
|
+
const acp = pending.client;
|
|
415
536
|
if (params.decision === 'cancel') {
|
|
416
537
|
await acp.respondError(pending.upstreamId, -32800, 'Cancelled by Remote client.');
|
|
417
538
|
return { resolved: true };
|
|
418
539
|
}
|
|
419
|
-
const result = params.result ?? mapPermissionDecision(params.decision, pending.method);
|
|
420
540
|
await acp.respond(pending.upstreamId, result);
|
|
421
541
|
return { resolved: true };
|
|
422
542
|
}
|
|
@@ -431,7 +551,7 @@ export class AcpRemoteGateway {
|
|
|
431
551
|
for (const [handle, approval] of this.approvals) {
|
|
432
552
|
if (approval.connectionId === connectionId) {
|
|
433
553
|
this.approvals.delete(handle);
|
|
434
|
-
void
|
|
554
|
+
void approval.client.respondError(approval.upstreamId, -32800, 'Remote peer disconnected.').catch(() => undefined);
|
|
435
555
|
}
|
|
436
556
|
}
|
|
437
557
|
}
|
|
@@ -491,7 +611,7 @@ export class AcpRemoteGateway {
|
|
|
491
611
|
throw new RpcError('CURSOR_CLOSED', 'The ACP Remote domain is closed.');
|
|
492
612
|
const previous = this.config;
|
|
493
613
|
const implemented = (value) => ({ enabled: value.enabled,
|
|
494
|
-
backends: value.backends.filter(item => ['cursor', 'antigravity'].includes(item.id)) });
|
|
614
|
+
backends: value.backends.filter(item => ['cursor', 'antigravity', 'pi', 'claude', 'opencode'].includes(item.id)) });
|
|
495
615
|
this.config = config;
|
|
496
616
|
if (JSON.stringify(implemented(previous)) === JSON.stringify(implemented(config)) && this.pendingReconfiguration.size === 0)
|
|
497
617
|
return;
|
|
@@ -512,7 +632,7 @@ export class AcpRemoteGateway {
|
|
|
512
632
|
for (const [id, session] of this.sessionCwds)
|
|
513
633
|
if (changed.has(session.backend))
|
|
514
634
|
this.sessionCwds.delete(id);
|
|
515
|
-
const terminalError = await this.workspaceState.dispose(sessionId => changed.has(sessionId.startsWith('acp:') ? 'antigravity' : 'cursor'))
|
|
635
|
+
const terminalError = await this.workspaceState.dispose(sessionId => changed.has(sessionId.startsWith('acp:') ? 'antigravity' : sessionId.startsWith('pi:') ? 'pi' : sessionId.startsWith('opencode:') ? 'opencode' : 'cursor'))
|
|
516
636
|
.then(() => undefined, () => new RpcError('TERMINAL_CLEANUP_FAILED', 'Some ACP terminal processes could not be stopped. Save the backend settings again to retry.'));
|
|
517
637
|
for (const id of sessions) {
|
|
518
638
|
this.sessionOwners.delete(id);
|
|
@@ -635,7 +755,7 @@ export class AcpRemoteGateway {
|
|
|
635
755
|
}
|
|
636
756
|
const unsubscribeInbound = acp.onInbound(message => {
|
|
637
757
|
this.inboundChain = this.inboundChain
|
|
638
|
-
.then(() => this.handleInbound(id, message))
|
|
758
|
+
.then(() => this.backendInstances.get(id)?.client === acp ? this.handleInbound(id, message) : undefined)
|
|
639
759
|
.catch(error => {
|
|
640
760
|
this.logger.warn('ACP inbound fanout failed', { id, code: errorCode(error) });
|
|
641
761
|
});
|
|
@@ -660,6 +780,13 @@ export class AcpRemoteGateway {
|
|
|
660
780
|
});
|
|
661
781
|
}
|
|
662
782
|
async handleInbound(backend, message) {
|
|
783
|
+
const incomingId = readSessionId(message.params) ?? readNestedSessionId(message.params);
|
|
784
|
+
const boundBackend = incomingId === undefined ? undefined : this.sessionBackends.get(incomingId);
|
|
785
|
+
if (boundBackend !== undefined && boundBackend !== backend) {
|
|
786
|
+
if (message.kind === 'request')
|
|
787
|
+
await this.requireAcp(backend).respondError(message.id, -32602, 'The ACP session backend does not match.');
|
|
788
|
+
return;
|
|
789
|
+
}
|
|
663
790
|
// session/update is a stream notification even if a buggy agent attaches an id.
|
|
664
791
|
if (message.kind === 'notification' || message.method === 'session/update') {
|
|
665
792
|
const sessionId = readSessionId(message.params) ?? readNestedSessionId(message.params);
|
|
@@ -671,38 +798,44 @@ export class AcpRemoteGateway {
|
|
|
671
798
|
await this.publishToSession(sessionId, { method: message.method, params: message.params });
|
|
672
799
|
return;
|
|
673
800
|
}
|
|
674
|
-
const sessionId =
|
|
675
|
-
|
|
676
|
-
|
|
801
|
+
const sessionId = incomingId;
|
|
802
|
+
const owner = sessionId === undefined ? undefined : this.sessionOwners.get(sessionId);
|
|
803
|
+
const peer = owner === undefined ? undefined : this.peers.get(owner);
|
|
804
|
+
const client = this.requireAcp(backend);
|
|
805
|
+
if (sessionId === undefined || this.cancelledSessions.has(sessionId) || owner === undefined || peer === undefined || boundBackend !== backend
|
|
806
|
+
|| !['session/request_permission', 'cursor/create_plan', 'cursor/ask_question'].includes(message.method)
|
|
807
|
+
|| (message.method.startsWith('cursor/') && backend !== 'cursor')) {
|
|
808
|
+
await client.respondError(message.id, -32602, 'The ACP request has no active owner or supported method.');
|
|
809
|
+
return;
|
|
810
|
+
}
|
|
811
|
+
let options = [];
|
|
812
|
+
try {
|
|
813
|
+
if (message.method === 'session/request_permission')
|
|
814
|
+
options = permissionOptions(message.params);
|
|
815
|
+
}
|
|
816
|
+
catch {
|
|
817
|
+
await client.respondError(message.id, -32602, 'The ACP permission options are invalid.');
|
|
818
|
+
return;
|
|
677
819
|
}
|
|
678
820
|
const requestHandle = randomUUID();
|
|
679
821
|
this.approvals.set(requestHandle, {
|
|
680
|
-
upstreamId: message.id,
|
|
681
|
-
|
|
682
|
-
sessionId,
|
|
683
|
-
method: message.method,
|
|
684
|
-
expiresAt: Date.now() + APPROVAL_TTL_MS,
|
|
685
|
-
backend,
|
|
822
|
+
upstreamId: message.id, connectionId: owner, sessionId, method: message.method,
|
|
823
|
+
expiresAt: Date.now() + APPROVAL_TTL_MS, backend, client, options,
|
|
686
824
|
});
|
|
687
825
|
this.scheduleApprovalExpiry();
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
if (peer !== undefined) {
|
|
701
|
-
await peer.publishInbound(sessionId, frame);
|
|
702
|
-
return;
|
|
703
|
-
}
|
|
826
|
+
try {
|
|
827
|
+
await peer.publishInbound(sessionId, {
|
|
828
|
+
method: message.method,
|
|
829
|
+
params: {
|
|
830
|
+
...(isRecord(message.params) ? message.params : {}),
|
|
831
|
+
requestHandle, sessionId, upstreamMethod: message.method,
|
|
832
|
+
},
|
|
833
|
+
});
|
|
834
|
+
}
|
|
835
|
+
catch {
|
|
836
|
+
this.approvals.delete(requestHandle);
|
|
837
|
+
await client.respondError(message.id, -32800, 'Remote approval delivery failed.');
|
|
704
838
|
}
|
|
705
|
-
await this.publishToSession(sessionId, frame);
|
|
706
839
|
}
|
|
707
840
|
async publishToSession(sessionId, frame) {
|
|
708
841
|
this.recordTurnCatchUp(sessionId, frame);
|
|
@@ -808,7 +941,7 @@ export class AcpRemoteGateway {
|
|
|
808
941
|
for (const [handle, approval] of this.approvals) {
|
|
809
942
|
if (approval.expiresAt <= now) {
|
|
810
943
|
this.approvals.delete(handle);
|
|
811
|
-
void
|
|
944
|
+
void approval.client.respondError(approval.upstreamId, -32800, 'Cursor approval expired.').catch(() => undefined);
|
|
812
945
|
}
|
|
813
946
|
}
|
|
814
947
|
this.scheduleApprovalExpiry();
|
|
@@ -853,8 +986,10 @@ export class AcpRemoteGateway {
|
|
|
853
986
|
return this.requireAcp().call(method, params);
|
|
854
987
|
}
|
|
855
988
|
requireSessionAccess(connectionId, sessionId, method) {
|
|
856
|
-
if (method === 'session/load')
|
|
989
|
+
if (method === 'session/load') {
|
|
990
|
+
this.requireSessionOwner(connectionId, sessionId);
|
|
857
991
|
return;
|
|
992
|
+
}
|
|
858
993
|
const owner = this.sessionOwners.get(sessionId);
|
|
859
994
|
if (owner === undefined) {
|
|
860
995
|
// Allow reclaim after the owning peer disconnected; the ACP process still
|
|
@@ -869,6 +1004,74 @@ export class AcpRemoteGateway {
|
|
|
869
1004
|
requireSessionOwner(connectionId, sessionId) {
|
|
870
1005
|
this.claimSession(connectionId, sessionId);
|
|
871
1006
|
}
|
|
1007
|
+
captureCatalogClient(backend) {
|
|
1008
|
+
const client = this.requireAcp(backend);
|
|
1009
|
+
const instance = this.backendInstances.get(backend);
|
|
1010
|
+
return { client, assertCurrent: () => {
|
|
1011
|
+
if (this.closed || this.backendInstances.get(backend) !== instance || this.requireAcp(backend) !== client) {
|
|
1012
|
+
throw new RpcError('CURSOR_UNAVAILABLE', 'The ACP backend changed while resolving session authority.');
|
|
1013
|
+
}
|
|
1014
|
+
} };
|
|
1015
|
+
}
|
|
1016
|
+
async listCatalogSessions(backend, cwd) {
|
|
1017
|
+
const { client, assertCurrent } = this.captureCatalogClient(backend);
|
|
1018
|
+
const items = [];
|
|
1019
|
+
let cursor;
|
|
1020
|
+
const seen = new Set();
|
|
1021
|
+
for (let page = 0; page < 100; page++) {
|
|
1022
|
+
const result = await client.call('session/list', { ...(cwd ? { cwd } : {}), ...(cursor ? { cursor } : {}) });
|
|
1023
|
+
assertCurrent();
|
|
1024
|
+
if (!isRecord(result) || !Array.isArray(result.sessions)
|
|
1025
|
+
|| (result.nextCursor != null && (typeof result.nextCursor !== 'string' || result.nextCursor.length === 0))) {
|
|
1026
|
+
throw new RpcError('INVALID_MESSAGE', 'The ACP session catalog response is invalid.');
|
|
1027
|
+
}
|
|
1028
|
+
for (const item of result.sessions) {
|
|
1029
|
+
if (!isRecord(item) || typeof item.sessionId !== 'string' || typeof item.cwd !== 'string')
|
|
1030
|
+
continue;
|
|
1031
|
+
// Directory I/O can outlive a backend restart/reconfiguration. Only the
|
|
1032
|
+
// directory failure is skippable; a stale generation must reject the RPC.
|
|
1033
|
+
const canonical = await this.requireExistingDirectory(item.cwd).catch(() => undefined);
|
|
1034
|
+
assertCurrent();
|
|
1035
|
+
if (!canonical || (cwd && canonical !== cwd))
|
|
1036
|
+
continue;
|
|
1037
|
+
if (this.sessionBackends.has(item.sessionId) && this.sessionBackends.get(item.sessionId) !== backend)
|
|
1038
|
+
continue;
|
|
1039
|
+
const trusted = this.sessionCwds.get(item.sessionId);
|
|
1040
|
+
if (trusted && (trusted.backend !== backend || trusted.cwd !== canonical))
|
|
1041
|
+
continue;
|
|
1042
|
+
this.sessionCwds.set(item.sessionId, { backend, cwd: canonical });
|
|
1043
|
+
this.sessionBackends.set(item.sessionId, backend);
|
|
1044
|
+
items.push({ sessionId: item.sessionId, cwd: canonical,
|
|
1045
|
+
...(typeof item.title === 'string' ? { title: item.title } : {}),
|
|
1046
|
+
...(typeof item.updatedAt === 'string' ? { updatedAt: item.updatedAt } : {}) });
|
|
1047
|
+
}
|
|
1048
|
+
if (result.nextCursor == null)
|
|
1049
|
+
return items;
|
|
1050
|
+
if (seen.has(result.nextCursor) || page === 99) {
|
|
1051
|
+
throw new RpcError('INVALID_MESSAGE', 'The ACP session catalog pagination did not complete.');
|
|
1052
|
+
}
|
|
1053
|
+
cursor = result.nextCursor;
|
|
1054
|
+
seen.add(cursor);
|
|
1055
|
+
}
|
|
1056
|
+
return items;
|
|
1057
|
+
}
|
|
1058
|
+
piSessionCwd(sessionId) { return this.catalogSessionCwd('pi', sessionId); }
|
|
1059
|
+
async catalogSessionCwd(backend, sessionId) {
|
|
1060
|
+
const { assertCurrent } = this.captureCatalogClient(backend);
|
|
1061
|
+
if (!this.sessionCwds.has(sessionId))
|
|
1062
|
+
await this.listCatalogSessions(backend);
|
|
1063
|
+
assertCurrent();
|
|
1064
|
+
const session = this.sessionCwds.get(sessionId);
|
|
1065
|
+
if (!session || session.backend !== backend)
|
|
1066
|
+
throw new RpcError('ACP_WORKSPACE_UNAVAILABLE', 'The ACP session has no trusted workspace.');
|
|
1067
|
+
const canonical = await this.requireExistingDirectory(session.cwd);
|
|
1068
|
+
assertCurrent();
|
|
1069
|
+
const current = this.sessionCwds.get(sessionId);
|
|
1070
|
+
if (canonical !== session.cwd || current?.backend !== backend || current.cwd !== canonical) {
|
|
1071
|
+
throw new RpcError('ACP_WORKSPACE_UNAVAILABLE', 'The ACP session workspace authority changed.');
|
|
1072
|
+
}
|
|
1073
|
+
return canonical;
|
|
1074
|
+
}
|
|
872
1075
|
async requireExistingDirectory(path) {
|
|
873
1076
|
if (!isAbsolute(path)) {
|
|
874
1077
|
throw new RpcError('CURSOR_PATH_NOT_ALLOWED', 'The Cursor working directory must be an absolute path.');
|
|
@@ -941,32 +1144,55 @@ function parseCallEnvelope(input) {
|
|
|
941
1144
|
return { method: input.method, params: input.params ?? {} };
|
|
942
1145
|
}
|
|
943
1146
|
function parseRespondEnvelope(input) {
|
|
944
|
-
if (!isRecord(input) || typeof input.requestHandle !== 'string' ||
|
|
945
|
-
|
|
1147
|
+
if (!isRecord(input) || typeof input.requestHandle !== 'string' || input.requestHandle.length === 0
|
|
1148
|
+
|| Object.keys(input).some(key => !['requestHandle', 'decision', 'result'].includes(key))
|
|
1149
|
+
|| typeof input.decision !== 'string' || !['allow-once', 'reject-once', 'cancel'].includes(input.decision)) {
|
|
1150
|
+
throw new RpcError('INVALID_MESSAGE', 'The ACP respond envelope is invalid.');
|
|
946
1151
|
}
|
|
947
|
-
|
|
948
|
-
if (decision !== 'allow-once' && decision !== 'allow-always' && decision !== 'reject-once' && decision !== 'cancel') {
|
|
949
|
-
throw new RpcError('INVALID_MESSAGE', 'The Cursor respond envelope is invalid.');
|
|
950
|
-
}
|
|
951
|
-
return {
|
|
952
|
-
requestHandle: input.requestHandle,
|
|
953
|
-
decision,
|
|
954
|
-
...(input.result === undefined ? {} : { result: input.result }),
|
|
955
|
-
};
|
|
1152
|
+
return input;
|
|
956
1153
|
}
|
|
957
|
-
function
|
|
958
|
-
if (
|
|
959
|
-
|
|
1154
|
+
function permissionOptions(params) {
|
|
1155
|
+
if (!isRecord(params) || !Array.isArray(params.options) || params.options.length === 0) {
|
|
1156
|
+
throw new Error('Missing permission options');
|
|
1157
|
+
}
|
|
1158
|
+
const ids = new Set();
|
|
1159
|
+
const options = params.options.map(option => {
|
|
1160
|
+
if (!isRecord(option) || typeof option.optionId !== 'string' || !option.optionId
|
|
1161
|
+
|| typeof option.kind !== 'string' || !['allow_once', 'allow_always', 'reject_once', 'reject_always'].includes(option.kind)
|
|
1162
|
+
|| ids.has(option.optionId))
|
|
1163
|
+
throw new Error('Invalid permission option');
|
|
1164
|
+
ids.add(option.optionId);
|
|
1165
|
+
return { kind: option.kind, optionId: option.optionId };
|
|
1166
|
+
});
|
|
1167
|
+
for (const kind of ['allow_once', 'reject_once']) {
|
|
1168
|
+
if (options.filter(option => option.kind === kind).length > 1)
|
|
1169
|
+
throw new Error('Ambiguous permission option');
|
|
960
1170
|
}
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
1171
|
+
return options;
|
|
1172
|
+
}
|
|
1173
|
+
function mapPermissionDecision(params, pending) {
|
|
1174
|
+
const invalid = () => new RpcError('INVALID_MESSAGE', 'The ACP approval result is not allowed.');
|
|
1175
|
+
if (pending.method === 'session/request_permission') {
|
|
1176
|
+
if (params.result !== undefined)
|
|
1177
|
+
throw invalid();
|
|
1178
|
+
if (params.decision === 'cancel')
|
|
1179
|
+
return undefined;
|
|
1180
|
+
const kind = params.decision === 'allow-once' ? 'allow_once' : 'reject_once';
|
|
1181
|
+
const option = pending.options.find(item => item.kind === kind);
|
|
1182
|
+
if (!option)
|
|
1183
|
+
throw invalid();
|
|
1184
|
+
return { outcome: { outcome: 'selected', optionId: option.optionId } };
|
|
965
1185
|
}
|
|
966
|
-
|
|
967
|
-
|
|
1186
|
+
const outcome = pending.method === 'cursor/create_plan'
|
|
1187
|
+
? params.decision === 'allow-once' ? 'accepted' : params.decision === 'cancel' ? 'cancelled' : 'rejected'
|
|
1188
|
+
: 'cancelled';
|
|
1189
|
+
if (params.result !== undefined) {
|
|
1190
|
+
if (!isRecord(params.result) || Object.keys(params.result).length !== 1
|
|
1191
|
+
|| !isRecord(params.result.outcome) || Object.keys(params.result.outcome).length !== 1
|
|
1192
|
+
|| params.result.outcome.outcome !== outcome)
|
|
1193
|
+
throw invalid();
|
|
968
1194
|
}
|
|
969
|
-
return { outcome: { outcome
|
|
1195
|
+
return { outcome: { outcome } };
|
|
970
1196
|
}
|
|
971
1197
|
function readSessionId(value) {
|
|
972
1198
|
if (!isRecord(value))
|
|
@@ -1028,10 +1254,16 @@ export function cursorBinaryCandidates(configured) {
|
|
|
1028
1254
|
'agent',
|
|
1029
1255
|
];
|
|
1030
1256
|
}
|
|
1257
|
+
if (configured === 'opencode') {
|
|
1258
|
+
return [
|
|
1259
|
+
join(userHome, '.local', 'bin', 'opencode'),
|
|
1260
|
+
'opencode',
|
|
1261
|
+
];
|
|
1262
|
+
}
|
|
1031
1263
|
return [configured];
|
|
1032
1264
|
}
|
|
1033
1265
|
function errorCode(error) {
|
|
1034
|
-
if (error instanceof CursorAcpError || error instanceof RpcError)
|
|
1266
|
+
if (error instanceof CursorAcpError || error instanceof OpencodeAcpError || error instanceof RpcError)
|
|
1035
1267
|
return error.code;
|
|
1036
1268
|
return 'CURSOR_UNAVAILABLE';
|
|
1037
1269
|
}
|