ds-harness-remote 0.4.26 → 0.5.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/README.md +6 -6
- package/dist/acp/adapter.d.ts +19 -0
- package/dist/acp/adapter.d.ts.map +1 -0
- package/dist/acp/adapter.js +14 -0
- package/dist/acp/adapter.js.map +1 -0
- package/dist/acp/adapters/antigravity/executor.d.ts +20 -0
- package/dist/acp/adapters/antigravity/executor.d.ts.map +1 -0
- package/dist/acp/adapters/antigravity/executor.js +99 -0
- package/dist/acp/adapters/antigravity/executor.js.map +1 -0
- package/dist/acp/adapters/antigravity/image-store.d.ts +9 -0
- package/dist/acp/adapters/antigravity/image-store.d.ts.map +1 -0
- package/dist/acp/adapters/antigravity/image-store.js +116 -0
- package/dist/acp/adapters/antigravity/image-store.js.map +1 -0
- package/dist/acp/adapters/antigravity/index.d.ts +5 -0
- package/dist/acp/adapters/antigravity/index.d.ts.map +1 -0
- package/dist/acp/adapters/antigravity/index.js +5 -0
- package/dist/acp/adapters/antigravity/index.js.map +1 -0
- package/dist/acp/adapters/antigravity/models.d.ts +35 -0
- package/dist/acp/adapters/antigravity/models.d.ts.map +1 -0
- package/dist/acp/adapters/antigravity/models.js +59 -0
- package/dist/acp/adapters/antigravity/models.js.map +1 -0
- package/dist/acp/adapters/antigravity/service.d.ts +35 -0
- package/dist/acp/adapters/antigravity/service.d.ts.map +1 -0
- package/dist/acp/adapters/antigravity/service.js +67 -0
- package/dist/acp/adapters/antigravity/service.js.map +1 -0
- package/dist/acp/adapters/antigravity/transcript-loader.d.ts +21 -0
- package/dist/acp/adapters/antigravity/transcript-loader.d.ts.map +1 -0
- package/dist/acp/adapters/antigravity/transcript-loader.js +356 -0
- package/dist/acp/adapters/antigravity/transcript-loader.js.map +1 -0
- package/dist/acp/adapters/antigravity/transcript-watcher.d.ts +28 -0
- package/dist/acp/adapters/antigravity/transcript-watcher.d.ts.map +1 -0
- package/dist/acp/adapters/antigravity/transcript-watcher.js +157 -0
- package/dist/acp/adapters/antigravity/transcript-watcher.js.map +1 -0
- package/dist/acp/adapters/antigravity/types.d.ts +56 -0
- package/dist/acp/adapters/antigravity/types.d.ts.map +1 -0
- package/dist/acp/adapters/antigravity/types.js +2 -0
- package/dist/acp/adapters/antigravity/types.js.map +1 -0
- package/dist/acp/adapters/antigravity-process.d.ts +69 -0
- package/dist/acp/adapters/antigravity-process.d.ts.map +1 -0
- package/dist/acp/adapters/antigravity-process.js +566 -0
- package/dist/acp/adapters/antigravity-process.js.map +1 -0
- package/dist/acp/adapters/antigravity.d.ts +8 -0
- package/dist/acp/adapters/antigravity.d.ts.map +1 -0
- package/dist/acp/adapters/antigravity.js +9 -0
- package/dist/acp/adapters/antigravity.js.map +1 -0
- package/dist/acp/adapters/cursor-process.d.ts +75 -0
- package/dist/acp/adapters/cursor-process.d.ts.map +1 -0
- package/dist/acp/adapters/cursor-process.js +273 -0
- package/dist/acp/adapters/cursor-process.js.map +1 -0
- package/dist/acp/adapters/cursor.d.ts +8 -0
- package/dist/acp/adapters/cursor.d.ts.map +1 -0
- package/dist/acp/adapters/cursor.js +8 -0
- package/dist/acp/adapters/cursor.js.map +1 -0
- package/dist/acp/gateway.d.ts +108 -0
- package/dist/acp/gateway.d.ts.map +1 -0
- package/dist/acp/gateway.js +1040 -0
- package/dist/acp/gateway.js.map +1 -0
- package/dist/acp/image-content.d.ts +19 -0
- package/dist/acp/image-content.d.ts.map +1 -0
- package/dist/acp/image-content.js +76 -0
- package/dist/acp/image-content.js.map +1 -0
- package/dist/acp/method-policy.d.ts +239 -0
- package/dist/acp/method-policy.d.ts.map +1 -0
- package/dist/acp/method-policy.js +102 -0
- package/dist/acp/method-policy.js.map +1 -0
- package/dist/acp/peer-bridge.d.ts +59 -0
- package/dist/acp/peer-bridge.d.ts.map +1 -0
- package/dist/acp/peer-bridge.js +367 -0
- package/dist/acp/peer-bridge.js.map +1 -0
- package/dist/acp/virtual-harness.d.ts +181 -0
- package/dist/acp/virtual-harness.d.ts.map +1 -0
- package/dist/acp/virtual-harness.js +1556 -0
- package/dist/acp/virtual-harness.js.map +1 -0
- package/dist/acp/workspace-tools.d.ts +13 -0
- package/dist/acp/workspace-tools.d.ts.map +1 -0
- package/dist/acp/workspace-tools.js +46 -0
- package/dist/acp/workspace-tools.js.map +1 -0
- package/dist/acp.d.ts.map +1 -1
- package/dist/acp.js +1 -0
- package/dist/acp.js.map +1 -1
- package/dist/api-proxy-switch.d.ts +2 -0
- package/dist/api-proxy-switch.d.ts.map +1 -1
- package/dist/api-proxy-switch.js +9 -0
- package/dist/api-proxy-switch.js.map +1 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +4 -3
- package/dist/cli.js.map +1 -1
- package/dist/client-runtime.d.ts +25 -1
- package/dist/client-runtime.d.ts.map +1 -1
- package/dist/client-runtime.js +222 -8
- package/dist/client-runtime.js.map +1 -1
- package/dist/client.github.js +327 -190
- package/dist/client.js +327 -190
- package/dist/client.js.map +1 -1
- package/dist/codex/domain.d.ts +4 -1
- package/dist/codex/domain.d.ts.map +1 -1
- package/dist/codex/domain.js +54 -4
- package/dist/codex/domain.js.map +1 -1
- package/dist/codex/virtual-harness.d.ts.map +1 -1
- package/dist/codex/virtual-harness.js +3 -0
- package/dist/codex/virtual-harness.js.map +1 -1
- package/dist/codex-workspace-bridge.d.ts +7 -105
- package/dist/codex-workspace-bridge.d.ts.map +1 -1
- package/dist/codex-workspace-bridge.js +10 -498
- package/dist/codex-workspace-bridge.js.map +1 -1
- package/dist/config.d.ts +28 -10
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +49 -5
- package/dist/config.js.map +1 -1
- package/dist/connection-controller.d.ts.map +1 -1
- package/dist/connection-controller.js +7 -4
- package/dist/connection-controller.js.map +1 -1
- package/dist/control-runtime.d.ts +1 -0
- package/dist/control-runtime.d.ts.map +1 -1
- package/dist/control-runtime.js +65 -10
- package/dist/control-runtime.js.map +1 -1
- package/dist/harness-version.d.ts.map +1 -1
- package/dist/harness-version.js +16 -3
- package/dist/harness-version.js.map +1 -1
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +13197 -8204
- package/dist/index.js.map +4 -4
- package/dist/loopback-host.d.ts +2 -2
- package/dist/loopback-host.d.ts.map +1 -1
- package/dist/loopback-host.js +41 -16
- package/dist/loopback-host.js.map +1 -1
- package/dist/remote-typert-gateway.d.ts.map +1 -1
- package/dist/remote-typert-gateway.js +7 -4
- package/dist/remote-typert-gateway.js.map +1 -1
- package/dist/rpc-router.d.ts +6 -5
- package/dist/rpc-router.d.ts.map +1 -1
- package/dist/rpc-router.js +49 -18
- package/dist/rpc-router.js.map +1 -1
- package/dist/server-api.d.ts +2 -0
- package/dist/server-api.d.ts.map +1 -1
- package/dist/server-api.js +5 -0
- package/dist/server-api.js.map +1 -1
- package/dist/server-connection.d.ts.map +1 -1
- package/dist/server-connection.js +15 -0
- package/dist/server-connection.js.map +1 -1
- package/dist/service.d.ts +30 -1
- package/dist/service.d.ts.map +1 -1
- package/dist/service.js +114 -12
- package/dist/service.js.map +1 -1
- package/dist/session-format-compat.d.ts +1 -0
- package/dist/session-format-compat.d.ts.map +1 -1
- package/dist/session-format-compat.js +14 -0
- package/dist/session-format-compat.js.map +1 -1
- package/dist/tui-command.js +2 -0
- package/dist/tui-command.js.map +1 -1
- package/dist/typert-gateway-switch.d.ts +2 -0
- package/dist/typert-gateway-switch.d.ts.map +1 -1
- package/dist/typert-gateway-switch.js +16 -0
- 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/dist/workspace-bridge.d.ts +117 -0
- package/dist/workspace-bridge.d.ts.map +1 -0
- package/dist/workspace-bridge.js +512 -0
- package/dist/workspace-bridge.js.map +1 -0
- package/dsh-plugin.json +1 -1
- package/package.json +1 -1
package/dist/client-runtime.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { LoopbackPreview } from './loopback-preview.js';
|
|
2
|
-
import {
|
|
2
|
+
import { parseRemoteWorkspaceTypes, } from '@dsh-remote/protocol';
|
|
3
|
+
import { AgentAcpClient, CodexRemoteClient, RemoteClientCore, remoteWorkspaceTypeAvailable } from '@dsh-remote/client-core';
|
|
3
4
|
import { AdaptiveTransport, stunOnlyIceServers, } from '@dsh-remote/webrtc';
|
|
4
5
|
import { ApiProxySwitch } from './api-proxy-switch.js';
|
|
5
6
|
import { ClientSecureTransport } from './client-secure-transport.js';
|
|
@@ -9,13 +10,22 @@ import { harnessSessionGeneration } from './harness-version.js';
|
|
|
9
10
|
import { RemoteHarnessApiProxy } from './remote-api-proxy.js';
|
|
10
11
|
import { RemoteTypertGateway } from './remote-typert-gateway.js';
|
|
11
12
|
import { codexProjectWorkspaceId, CodexVirtualHarness, discoverCodexVirtualWorkspaces, } from './codex/virtual-harness.js';
|
|
13
|
+
import { AcpVirtualHarness, discoverAcpVirtualWorkspaces, } from './acp/virtual-harness.js';
|
|
12
14
|
import { ServerApiError, } from './server-api.js';
|
|
13
15
|
import { TypertGatewaySwitch } from './typert-gateway-switch.js';
|
|
14
16
|
import { loadNodeRtcFactory } from './werift-rtc.js';
|
|
17
|
+
import { safeErrorCode } from './safe-error.js';
|
|
15
18
|
const REMOTE_COMMAND_LIST_MIN_VERSION = [0, 3, 16];
|
|
16
19
|
const REMOTE_FILE_VIEWER_MIN_VERSION = [0, 3, 17];
|
|
17
20
|
const DIRECT_WEBRTC_NEGOTIATE_TIMEOUT_MS = 12_000;
|
|
18
21
|
const DIRECT_LAN_PROGRESS_DISPLAY_MS = 1_400;
|
|
22
|
+
const HOST_AUTHORIZATION_ERRORS = new Set([
|
|
23
|
+
'ACCOUNT_AUTH_REQUIRED',
|
|
24
|
+
'AUTH_INVALID',
|
|
25
|
+
'DEVICE_OWNERSHIP_REQUIRED',
|
|
26
|
+
'DEVICE_REVOKED',
|
|
27
|
+
'TOKEN_EXPIRED',
|
|
28
|
+
]);
|
|
19
29
|
export class ClientModeRuntime {
|
|
20
30
|
config;
|
|
21
31
|
identities;
|
|
@@ -28,6 +38,8 @@ export class ClientModeRuntime {
|
|
|
28
38
|
connected;
|
|
29
39
|
pendingWorkspaceSelection;
|
|
30
40
|
codexVirtual;
|
|
41
|
+
cursorVirtual;
|
|
42
|
+
acpVirtualBackend = 'cursor';
|
|
31
43
|
proxySwitch;
|
|
32
44
|
gatewaySwitch;
|
|
33
45
|
codexStreams = new Map();
|
|
@@ -55,6 +67,39 @@ export class ClientModeRuntime {
|
|
|
55
67
|
deviceId: shortId(this.identity.deviceId),
|
|
56
68
|
fingerprint: this.identity.fingerprint,
|
|
57
69
|
});
|
|
70
|
+
// A previously authorized Client should make the local Host controllable
|
|
71
|
+
// on startup as well. Do not register an anonymous Client just to probe:
|
|
72
|
+
// only persisted Client credentials opt into this default.
|
|
73
|
+
if (this.config.hostControl?.enabled !== false
|
|
74
|
+
&& this.host !== undefined
|
|
75
|
+
&& this.server.hasStoredAuthorization !== undefined) {
|
|
76
|
+
try {
|
|
77
|
+
if (await this.server.hasStoredAuthorization()
|
|
78
|
+
&& (this.host.hasStoredAuthorization === undefined || !await this.host.hasStoredAuthorization())) {
|
|
79
|
+
await this.authorizeHostByDefault();
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
catch (error) {
|
|
83
|
+
this.logger.warn('automatic Host authorization failed', { code: safeErrorCode(error) });
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
async authorizeHostByDefault() {
|
|
88
|
+
try {
|
|
89
|
+
if (this.host === undefined)
|
|
90
|
+
return;
|
|
91
|
+
if (this.config.hostControl?.enabled === false)
|
|
92
|
+
return;
|
|
93
|
+
if (this.host.hostStatus().authorized)
|
|
94
|
+
return;
|
|
95
|
+
if (this.host.hasStoredAuthorization !== undefined && await this.host.hasStoredAuthorization())
|
|
96
|
+
return;
|
|
97
|
+
const credentials = await this.server.authenticate(this.requireIdentity());
|
|
98
|
+
await this.host.authorizeHostAsOwned(credentials.accessToken, credentials.account);
|
|
99
|
+
}
|
|
100
|
+
catch (error) {
|
|
101
|
+
this.logger.warn('automatic Host authorization failed', { code: safeErrorCode(error) });
|
|
102
|
+
}
|
|
58
103
|
}
|
|
59
104
|
registerControl(connection, webServer) {
|
|
60
105
|
return registerControlRoute(connection, (endpoint, payload, signal) => this.handleControl(endpoint, payload, signal), webServer);
|
|
@@ -87,7 +132,9 @@ export class ClientModeRuntime {
|
|
|
87
132
|
...(this.pendingWorkspaceSelection === undefined
|
|
88
133
|
? {}
|
|
89
134
|
: { workspaceSelection: { ...this.pendingWorkspaceSelection } }),
|
|
90
|
-
backend: this.
|
|
135
|
+
backend: this.cursorVirtual !== undefined
|
|
136
|
+
? this.acpVirtualBackend
|
|
137
|
+
: this.codexVirtual === undefined ? 'harness' : 'codex',
|
|
91
138
|
hostAuthorizationAvailable: this.host !== undefined,
|
|
92
139
|
...(this.host === undefined ? {} : { host: this.host.hostStatus() }),
|
|
93
140
|
};
|
|
@@ -122,6 +169,7 @@ export class ClientModeRuntime {
|
|
|
122
169
|
};
|
|
123
170
|
}
|
|
124
171
|
async devices() {
|
|
172
|
+
this.assertHostAuthorizationForDeviceDiscovery();
|
|
125
173
|
this.requireIdentity();
|
|
126
174
|
const serverDevices = await this.server.listDevices();
|
|
127
175
|
const remoteDevices = serverDevices.filter(device => device.deviceId !== this.host?.hostStatus().deviceId);
|
|
@@ -131,6 +179,23 @@ export class ClientModeRuntime {
|
|
|
131
179
|
return { ...device, ...presence };
|
|
132
180
|
}));
|
|
133
181
|
}
|
|
182
|
+
/**
|
|
183
|
+
* Device discovery is exposed through the local app control route. When this
|
|
184
|
+
* installation also runs a Host, keep that route closed after the Host's
|
|
185
|
+
* Server credential has become terminally invalid. The Client credential can
|
|
186
|
+
* remain usable for a short time after a revoke, so checking only
|
|
187
|
+
* `ClientServerApi.listDevices()` would otherwise leak the device directory
|
|
188
|
+
* from a Host that the user has already been told to re-authorize.
|
|
189
|
+
*/
|
|
190
|
+
assertHostAuthorizationForDeviceDiscovery() {
|
|
191
|
+
const status = this.host?.hostStatus();
|
|
192
|
+
if (status === undefined || status.error === undefined || !HOST_AUTHORIZATION_ERRORS.has(status.error))
|
|
193
|
+
return;
|
|
194
|
+
const message = status.error === 'DEVICE_REVOKED'
|
|
195
|
+
? 'The local Host was revoked on the Server. Sign out and authorize this Host again.'
|
|
196
|
+
: 'The local Host authorization is no longer valid. Sign out and authorize this Host again.';
|
|
197
|
+
throw new ClientModeError(status.error, message);
|
|
198
|
+
}
|
|
134
199
|
async authorizeClientWithAccount(email, password) {
|
|
135
200
|
let authorization;
|
|
136
201
|
try {
|
|
@@ -143,6 +208,7 @@ export class ClientModeRuntime {
|
|
|
143
208
|
this.server.bindIdentity(this.identity);
|
|
144
209
|
authorization = await this.server.authorizeWithAccount(this.identity, email, password);
|
|
145
210
|
}
|
|
211
|
+
await this.authorizeHostByDefault();
|
|
146
212
|
this.logger.info('Client account authorized');
|
|
147
213
|
return authorization;
|
|
148
214
|
}
|
|
@@ -158,6 +224,8 @@ export class ClientModeRuntime {
|
|
|
158
224
|
});
|
|
159
225
|
if (result.status === 'complete')
|
|
160
226
|
this.logger.info('Client account authorized with QR login');
|
|
227
|
+
if (result.status === 'complete')
|
|
228
|
+
await this.authorizeHostByDefault();
|
|
161
229
|
return result;
|
|
162
230
|
}
|
|
163
231
|
async clearClientAuthorization() {
|
|
@@ -166,6 +234,7 @@ export class ClientModeRuntime {
|
|
|
166
234
|
this.connectionProgress = undefined;
|
|
167
235
|
this.pendingWorkspaceSelection = undefined;
|
|
168
236
|
await this.closeCodexVirtual();
|
|
237
|
+
await this.closeCursorVirtual();
|
|
169
238
|
this.proxySwitch?.selectLocal();
|
|
170
239
|
await this.closePreview();
|
|
171
240
|
this.gatewaySwitch.selectLocal();
|
|
@@ -189,6 +258,7 @@ export class ClientModeRuntime {
|
|
|
189
258
|
async setMode(mode, targetDeviceId, signal) {
|
|
190
259
|
if (mode === 'local') {
|
|
191
260
|
await this.closeCodexVirtual();
|
|
261
|
+
await this.closeCursorVirtual();
|
|
192
262
|
this.proxySwitch?.selectLocal();
|
|
193
263
|
await this.closePreview();
|
|
194
264
|
this.gatewaySwitch.selectLocal();
|
|
@@ -219,6 +289,7 @@ export class ClientModeRuntime {
|
|
|
219
289
|
this.clearConnectionProgress(next.progressRunId);
|
|
220
290
|
this.pendingWorkspaceSelection = undefined;
|
|
221
291
|
await this.closeCodexVirtual();
|
|
292
|
+
await this.closeCursorVirtual();
|
|
222
293
|
this.selectRemoteTarget(next);
|
|
223
294
|
await this.closeCodexStreams(previous?.client);
|
|
224
295
|
await previous?.client.close().catch(() => undefined);
|
|
@@ -279,6 +350,7 @@ export class ClientModeRuntime {
|
|
|
279
350
|
workspace = unwrapNativeResult(response);
|
|
280
351
|
}
|
|
281
352
|
await this.closeCodexVirtual();
|
|
353
|
+
await this.closeCursorVirtual();
|
|
282
354
|
this.selectRemoteTarget(remote, transport);
|
|
283
355
|
const workspaceId = workspaceRecordId(workspace.workspace);
|
|
284
356
|
this.pendingWorkspaceSelection = { targetDeviceId: remote.target.deviceId, workspaceId };
|
|
@@ -313,6 +385,7 @@ export class ClientModeRuntime {
|
|
|
313
385
|
throw new ClientModeError('WORKSPACE_NOT_FOUND', 'The selected CodeX workspace is no longer available.');
|
|
314
386
|
}
|
|
315
387
|
await this.closeCodexVirtual();
|
|
388
|
+
await this.closeCursorVirtual();
|
|
316
389
|
this.codexVirtual = virtual;
|
|
317
390
|
this.selectCodexTarget(virtual, remote);
|
|
318
391
|
const preferredSessionId = await virtual.preferredSessionId(signal);
|
|
@@ -346,6 +419,81 @@ export class ClientModeRuntime {
|
|
|
346
419
|
}
|
|
347
420
|
return this.openCodexWorkspace(targetDeviceId, codexProjectWorkspaceId(project.id), signal);
|
|
348
421
|
}
|
|
422
|
+
async listCursorWorkspaces(targetDeviceId, signal, backend = 'cursor') {
|
|
423
|
+
const remote = await this.ensureConnected(targetDeviceId, signal);
|
|
424
|
+
remote.features = await probeRemoteHostFeatures(remote.client, remote.clientVersion);
|
|
425
|
+
if (backend === 'antigravity' ? !remote.features.antigravity : !remote.features.cursor) {
|
|
426
|
+
throw new ClientModeError('FEATURE_NOT_SUPPORTED', `The selected Host does not provide ${backend} workspaces.`);
|
|
427
|
+
}
|
|
428
|
+
if (this.cursorVirtual !== undefined && this.connected?.target.deviceId === targetDeviceId && this.acpVirtualBackend === backend) {
|
|
429
|
+
return this.cursorVirtual.workspaces();
|
|
430
|
+
}
|
|
431
|
+
const acpClient = new AgentAcpClient(remote.client);
|
|
432
|
+
const workspaces = await discoverAcpVirtualWorkspaces(acpClient, backend, signal);
|
|
433
|
+
return workspaces;
|
|
434
|
+
}
|
|
435
|
+
async openCursorWorkspace(targetDeviceId, workspaceId, signal, backend = 'cursor') {
|
|
436
|
+
const remote = await this.ensureConnected(targetDeviceId, signal);
|
|
437
|
+
remote.features = await probeRemoteHostFeatures(remote.client, remote.clientVersion);
|
|
438
|
+
if (backend === 'antigravity' ? !remote.features.antigravity : !remote.features.cursor) {
|
|
439
|
+
throw new ClientModeError('FEATURE_NOT_SUPPORTED', `The selected Host does not provide ${backend} workspaces.`);
|
|
440
|
+
}
|
|
441
|
+
this.assertLocalHarnessCarrierAvailable();
|
|
442
|
+
const virtual = AcpVirtualHarness.remote(remote.client, {
|
|
443
|
+
deviceId: remote.target.deviceId,
|
|
444
|
+
name: remote.target.name,
|
|
445
|
+
}, backend, remote.features.acpModelLoadsSession ? ['agent.acp.antigravity.model-load.v1'] : []);
|
|
446
|
+
let workspace;
|
|
447
|
+
try {
|
|
448
|
+
workspace = await virtual.selectWorkspace(workspaceId);
|
|
449
|
+
}
|
|
450
|
+
catch {
|
|
451
|
+
await virtual.close();
|
|
452
|
+
throw new ClientModeError('WORKSPACE_NOT_FOUND', `The selected ${backend} workspace is no longer available.`);
|
|
453
|
+
}
|
|
454
|
+
await this.closeCodexVirtual();
|
|
455
|
+
await this.closeCursorVirtual();
|
|
456
|
+
this.cursorVirtual = virtual;
|
|
457
|
+
this.acpVirtualBackend = backend;
|
|
458
|
+
this.selectCursorTarget(virtual, remote);
|
|
459
|
+
const preferredSessionId = await virtual.preferredSessionId();
|
|
460
|
+
this.pendingWorkspaceSelection = {
|
|
461
|
+
targetDeviceId: remote.target.deviceId,
|
|
462
|
+
workspaceId: workspace.workspaceId,
|
|
463
|
+
backend,
|
|
464
|
+
...(preferredSessionId === undefined ? {} : { sessionId: preferredSessionId }),
|
|
465
|
+
};
|
|
466
|
+
this.logger.info(`${backend} virtual workspace opened`, { targetDeviceId: shortId(remote.target.deviceId) });
|
|
467
|
+
return { ...this.status(), workspace };
|
|
468
|
+
}
|
|
469
|
+
async createCursorWorkspace(targetDeviceId, path, signal, backend = 'cursor') {
|
|
470
|
+
const trimmedPath = path.trim();
|
|
471
|
+
if (trimmedPath === '')
|
|
472
|
+
throw new ClientModeError('INVALID_MESSAGE', `A ${backend} project directory is required.`);
|
|
473
|
+
const remote = await this.ensureConnected(targetDeviceId, signal);
|
|
474
|
+
remote.features = await probeRemoteHostFeatures(remote.client, remote.clientVersion);
|
|
475
|
+
if (backend === 'antigravity' ? !remote.features.antigravity : !remote.features.cursor) {
|
|
476
|
+
throw new ClientModeError('FEATURE_NOT_SUPPORTED', `The selected Host does not provide ${backend} workspaces.`);
|
|
477
|
+
}
|
|
478
|
+
this.assertLocalHarnessCarrierAvailable();
|
|
479
|
+
const virtual = AcpVirtualHarness.remote(remote.client, {
|
|
480
|
+
deviceId: remote.target.deviceId,
|
|
481
|
+
name: remote.target.name,
|
|
482
|
+
}, backend, remote.features.acpModelLoadsSession ? ['agent.acp.antigravity.model-load.v1'] : []);
|
|
483
|
+
const workspace = await virtual.selectOrCreateWorkspace(trimmedPath);
|
|
484
|
+
await this.closeCodexVirtual();
|
|
485
|
+
await this.closeCursorVirtual();
|
|
486
|
+
this.cursorVirtual = virtual;
|
|
487
|
+
this.acpVirtualBackend = backend;
|
|
488
|
+
this.selectCursorTarget(virtual, remote);
|
|
489
|
+
this.pendingWorkspaceSelection = {
|
|
490
|
+
targetDeviceId: remote.target.deviceId,
|
|
491
|
+
workspaceId: workspace.workspaceId,
|
|
492
|
+
backend,
|
|
493
|
+
};
|
|
494
|
+
this.logger.info(`${backend} virtual workspace created`, { targetDeviceId: shortId(remote.target.deviceId) });
|
|
495
|
+
return { ...this.status(), workspace };
|
|
496
|
+
}
|
|
349
497
|
consumeWorkspaceSelection(selection) {
|
|
350
498
|
const pending = this.pendingWorkspaceSelection;
|
|
351
499
|
if (pending?.targetDeviceId === selection.targetDeviceId
|
|
@@ -365,6 +513,7 @@ export class ClientModeRuntime {
|
|
|
365
513
|
this.gatewaySwitch.selectLocal();
|
|
366
514
|
this.pendingWorkspaceSelection = undefined;
|
|
367
515
|
await this.closeCodexVirtual();
|
|
516
|
+
await this.closeCursorVirtual();
|
|
368
517
|
await this.closeCodexStreams(this.connected?.client);
|
|
369
518
|
await this.connected?.client.close().catch(() => undefined);
|
|
370
519
|
this.connected = undefined;
|
|
@@ -563,6 +712,20 @@ export class ClientModeRuntime {
|
|
|
563
712
|
this.codexVirtual = undefined;
|
|
564
713
|
await virtual?.close();
|
|
565
714
|
}
|
|
715
|
+
async closeCursorVirtual() {
|
|
716
|
+
const virtual = this.cursorVirtual;
|
|
717
|
+
this.cursorVirtual = undefined;
|
|
718
|
+
await virtual?.close();
|
|
719
|
+
}
|
|
720
|
+
selectCursorTarget(virtual, remote) {
|
|
721
|
+
const target = { deviceId: remote.target.deviceId, name: remote.target.name };
|
|
722
|
+
if (this.gatewaySwitch.supportsCarrier()) {
|
|
723
|
+
this.gatewaySwitch.selectRemote(virtual, undefined, target);
|
|
724
|
+
return;
|
|
725
|
+
}
|
|
726
|
+
this.proxySwitch.selectRemote(virtual.api, target);
|
|
727
|
+
this.gatewaySwitch.selectRemote(request => virtual.invoke(request), { execute: true, list: true }, target);
|
|
728
|
+
}
|
|
566
729
|
assertRemoteCompatible(remote) {
|
|
567
730
|
this.selectHarnessRemoteTransport(remote);
|
|
568
731
|
}
|
|
@@ -682,10 +845,11 @@ export class ClientModeRuntime {
|
|
|
682
845
|
this.connectionProgress = undefined;
|
|
683
846
|
this.pendingWorkspaceSelection = undefined;
|
|
684
847
|
void this.closeCodexVirtual();
|
|
685
|
-
this.
|
|
686
|
-
this.
|
|
848
|
+
void this.closeCursorVirtual();
|
|
849
|
+
this.proxySwitch?.disconnectRemote();
|
|
850
|
+
this.gatewaySwitch.disconnectRemote();
|
|
687
851
|
void connectedClient.close().catch(() => undefined);
|
|
688
|
-
this.logger.warn('remote Harness transport closed;
|
|
852
|
+
this.logger.warn('remote Harness transport closed; keeping Remote authority disconnected', {
|
|
689
853
|
targetDeviceId: shortId(target.deviceId),
|
|
690
854
|
});
|
|
691
855
|
});
|
|
@@ -837,6 +1001,46 @@ export class ClientModeRuntime {
|
|
|
837
1001
|
}
|
|
838
1002
|
return ok(await this.createCodexWorkspace(value.targetDeviceId, value.path, signal));
|
|
839
1003
|
}
|
|
1004
|
+
if (endpoint === 'cursor.workspaces.list') {
|
|
1005
|
+
const value = record(payload);
|
|
1006
|
+
if (typeof value.targetDeviceId !== 'string')
|
|
1007
|
+
throw new ClientModeError('INVALID_MESSAGE', 'A Host is required.');
|
|
1008
|
+
return ok(await this.listCursorWorkspaces(value.targetDeviceId, signal, 'cursor'));
|
|
1009
|
+
}
|
|
1010
|
+
if (endpoint === 'cursor.workspace.open') {
|
|
1011
|
+
const value = record(payload);
|
|
1012
|
+
if (typeof value.targetDeviceId !== 'string' || typeof value.workspaceId !== 'string') {
|
|
1013
|
+
throw new ClientModeError('INVALID_MESSAGE', 'A Host and Cursor Workspace are required.');
|
|
1014
|
+
}
|
|
1015
|
+
return ok(await this.openCursorWorkspace(value.targetDeviceId, value.workspaceId, signal, 'cursor'));
|
|
1016
|
+
}
|
|
1017
|
+
if (endpoint === 'cursor.workspace.create') {
|
|
1018
|
+
const value = record(payload);
|
|
1019
|
+
if (typeof value.targetDeviceId !== 'string' || typeof value.path !== 'string') {
|
|
1020
|
+
throw new ClientModeError('INVALID_MESSAGE', 'A Host and Cursor project directory are required.');
|
|
1021
|
+
}
|
|
1022
|
+
return ok(await this.createCursorWorkspace(value.targetDeviceId, value.path, signal, 'cursor'));
|
|
1023
|
+
}
|
|
1024
|
+
if (endpoint === 'antigravity.workspaces.list') {
|
|
1025
|
+
const value = record(payload);
|
|
1026
|
+
if (typeof value.targetDeviceId !== 'string')
|
|
1027
|
+
throw new ClientModeError('INVALID_MESSAGE', 'A Host is required.');
|
|
1028
|
+
return ok(await this.listCursorWorkspaces(value.targetDeviceId, signal, 'antigravity'));
|
|
1029
|
+
}
|
|
1030
|
+
if (endpoint === 'antigravity.workspace.open') {
|
|
1031
|
+
const value = record(payload);
|
|
1032
|
+
if (typeof value.targetDeviceId !== 'string' || typeof value.workspaceId !== 'string') {
|
|
1033
|
+
throw new ClientModeError('INVALID_MESSAGE', 'A Host and Antigravity Workspace are required.');
|
|
1034
|
+
}
|
|
1035
|
+
return ok(await this.openCursorWorkspace(value.targetDeviceId, value.workspaceId, signal, 'antigravity'));
|
|
1036
|
+
}
|
|
1037
|
+
if (endpoint === 'antigravity.workspace.create') {
|
|
1038
|
+
const value = record(payload);
|
|
1039
|
+
if (typeof value.targetDeviceId !== 'string' || typeof value.path !== 'string') {
|
|
1040
|
+
throw new ClientModeError('INVALID_MESSAGE', 'A Host and Antigravity project directory are required.');
|
|
1041
|
+
}
|
|
1042
|
+
return ok(await this.createCursorWorkspace(value.targetDeviceId, value.path, signal, 'antigravity'));
|
|
1043
|
+
}
|
|
840
1044
|
if (endpoint === 'workspace.selection.consume') {
|
|
841
1045
|
const value = record(payload);
|
|
842
1046
|
if (typeof value.targetDeviceId !== 'string' || typeof value.workspaceId !== 'string') {
|
|
@@ -845,7 +1049,7 @@ export class ClientModeRuntime {
|
|
|
845
1049
|
return ok(this.consumeWorkspaceSelection({
|
|
846
1050
|
targetDeviceId: value.targetDeviceId,
|
|
847
1051
|
workspaceId: value.workspaceId,
|
|
848
|
-
...(value.backend === 'codex' ? { backend:
|
|
1052
|
+
...(value.backend === 'codex' || value.backend === 'cursor' || value.backend === 'antigravity' ? { backend: value.backend } : {}),
|
|
849
1053
|
...(typeof value.sessionId === 'string' ? { sessionId: value.sessionId } : {}),
|
|
850
1054
|
}));
|
|
851
1055
|
}
|
|
@@ -1096,6 +1300,8 @@ export function remoteHostFeatures(clientVersion) {
|
|
|
1096
1300
|
apiProxy: true,
|
|
1097
1301
|
remoteGateway: false,
|
|
1098
1302
|
codex: false,
|
|
1303
|
+
cursor: false,
|
|
1304
|
+
antigravity: false,
|
|
1099
1305
|
};
|
|
1100
1306
|
}
|
|
1101
1307
|
export async function probeRemoteHostFeatures(client, clientVersion) {
|
|
@@ -1118,13 +1324,17 @@ export async function probeRemoteHostFeatures(client, clientVersion) {
|
|
|
1118
1324
|
const remoteV1 = capabilities.has('harness.remote.v1');
|
|
1119
1325
|
const remoteV3 = capabilities.has('harness.remote.v3');
|
|
1120
1326
|
const terminal = capabilities.has('harness.terminal.v1');
|
|
1121
|
-
const
|
|
1327
|
+
const workspaceTypes = 'workspaceTypes' in value ? parseRemoteWorkspaceTypes(value.workspaceTypes) : undefined;
|
|
1328
|
+
const available = (id, capability) => remoteWorkspaceTypeAvailable(value.capabilities, workspaceTypes, id, capability);
|
|
1329
|
+
const codex = available('codex', 'codex.appserver.v1');
|
|
1330
|
+
const cursor = available('cursor', 'agent.acp.cursor.v1');
|
|
1331
|
+
const antigravity = available('antigravity', 'agent.acp.antigravity.v1');
|
|
1122
1332
|
if (remoteV1 && remoteV3) {
|
|
1123
1333
|
throw new ClientModeError('INVALID_MESSAGE', 'The remote Host advertised conflicting Harness Session formats.');
|
|
1124
1334
|
}
|
|
1125
1335
|
const sessionFormat = remoteV3 ? 3 : undefined;
|
|
1126
1336
|
const remoteGateway = remoteV3 || remoteV1;
|
|
1127
|
-
if (!apiProxy && !remoteGateway && !codex) {
|
|
1337
|
+
if (!apiProxy && !remoteGateway && !codex && !cursor && !antigravity) {
|
|
1128
1338
|
throw new ClientModeError('FEATURE_NOT_SUPPORTED', 'The remote Host exposes no supported Harness transport.');
|
|
1129
1339
|
}
|
|
1130
1340
|
return {
|
|
@@ -1135,6 +1345,10 @@ export async function probeRemoteHostFeatures(client, clientVersion) {
|
|
|
1135
1345
|
remoteGateway,
|
|
1136
1346
|
...(sessionFormat === undefined ? {} : { sessionFormat }),
|
|
1137
1347
|
codex,
|
|
1348
|
+
cursor,
|
|
1349
|
+
antigravity,
|
|
1350
|
+
...(capabilities.has('agent.acp.antigravity.model-load.v1') ? { acpModelLoadsSession: true } : {}),
|
|
1351
|
+
...(workspaceTypes === undefined ? {} : { workspaceTypes }),
|
|
1138
1352
|
};
|
|
1139
1353
|
}
|
|
1140
1354
|
async function waitForCodexFrames(stream, signal) {
|