@jait/gateway 0.1.84 → 0.1.85

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.
@@ -0,0 +1,266 @@
1
+ /**
2
+ * Workspace Sync — pulls a remote workspace to a local temp directory
3
+ * and pushes changed files back after a CLI provider (Codex/Claude) turn.
4
+ *
5
+ * This solves the fundamental problem: CLI providers operate directly on
6
+ * the filesystem (not through Jait tools), so the workspace must exist
7
+ * locally on the gateway. This service transparently mirrors a remote
8
+ * node's workspace so CLI providers see a real local directory.
9
+ *
10
+ * Flow:
11
+ * 1. pull() — enumerate & download files from the remote node
12
+ * 2. Codex/Claude runs against the local mirror
13
+ * 3. push() — detect changed files and upload them back
14
+ * 4. cleanup() — remove the local mirror when done
15
+ */
16
+ import { join } from "node:path";
17
+ import { homedir } from "node:os";
18
+ import { mkdirSync, writeFileSync, readFileSync, readdirSync, statSync, existsSync, rmSync, } from "node:fs";
19
+ import { createHash } from "node:crypto";
20
+ /** Directories/files to skip during sync to keep it fast */
21
+ const EXCLUDE_PATTERNS = [
22
+ "node_modules",
23
+ ".git",
24
+ "dist",
25
+ "build",
26
+ ".next",
27
+ ".nuxt",
28
+ ".output",
29
+ "__pycache__",
30
+ ".venv",
31
+ "venv",
32
+ ".tox",
33
+ "target", // Rust/Java
34
+ "vendor", // Go/PHP
35
+ ".gradle",
36
+ ".idea",
37
+ ".vs",
38
+ "*.pyc",
39
+ "*.pyo",
40
+ ".DS_Store",
41
+ "Thumbs.db",
42
+ ];
43
+ /** Max file size to sync (skip large binaries) */
44
+ const MAX_FILE_SIZE = 2 * 1024 * 1024; // 2 MB
45
+ /** Max total files to sync (safety limit) */
46
+ const MAX_FILES = 5000;
47
+ export class WorkspaceSync {
48
+ ws;
49
+ nodeId;
50
+ remotePath;
51
+ localPath;
52
+ snapshot = new Map();
53
+ pulled = false;
54
+ constructor(options) {
55
+ this.ws = options.ws;
56
+ this.nodeId = options.nodeId;
57
+ this.remotePath = options.remotePath;
58
+ // Create a deterministic local path under ~/.jait/workspaces/
59
+ const hash = createHash("sha256")
60
+ .update(`${options.nodeId}:${options.remotePath}`)
61
+ .digest("hex")
62
+ .slice(0, 12);
63
+ const dirName = options.remotePath
64
+ .replace(/^[A-Za-z]:/, "")
65
+ .replace(/[\\\/]/g, "_")
66
+ .replace(/[^a-zA-Z0-9_.-]/g, "")
67
+ .slice(0, 40);
68
+ this.localPath = join(homedir(), ".jait", "workspaces", `${dirName}-${hash}`);
69
+ }
70
+ /** The local directory path CLI providers should use as cwd */
71
+ get localDir() {
72
+ return this.localPath;
73
+ }
74
+ /** Whether the workspace has been pulled yet */
75
+ get isPulled() {
76
+ return this.pulled;
77
+ }
78
+ /**
79
+ * Pull the workspace files from the remote node to the local mirror.
80
+ * Recursively enumerates and downloads files, skipping excluded patterns.
81
+ */
82
+ async pull() {
83
+ mkdirSync(this.localPath, { recursive: true });
84
+ let fileCount = 0;
85
+ let totalSize = 0;
86
+ this.snapshot.clear();
87
+ const enumerate = async (remoteDirPath, localDirPath) => {
88
+ if (fileCount >= MAX_FILES)
89
+ return;
90
+ let entries;
91
+ try {
92
+ entries = await this.ws.proxyFsOp(this.nodeId, "list", { path: remoteDirPath }, 15_000);
93
+ }
94
+ catch (err) {
95
+ console.warn(`[workspace-sync] Failed to list ${remoteDirPath}: ${err}`);
96
+ return;
97
+ }
98
+ for (const entry of entries) {
99
+ if (fileCount >= MAX_FILES)
100
+ break;
101
+ const name = entry.endsWith("/") ? entry.slice(0, -1) : entry;
102
+ const isDir = entry.endsWith("/");
103
+ // Check excludes
104
+ if (shouldExclude(name))
105
+ continue;
106
+ const fullRemotePath = joinPath(remoteDirPath, name);
107
+ const fullLocalPath = join(localDirPath, name);
108
+ if (isDir) {
109
+ mkdirSync(fullLocalPath, { recursive: true });
110
+ await enumerate(fullRemotePath, fullLocalPath);
111
+ }
112
+ else {
113
+ // Check file size before downloading
114
+ try {
115
+ const stat = await this.ws.proxyFsOp(this.nodeId, "stat", { path: fullRemotePath }, 10_000);
116
+ if (stat.size > MAX_FILE_SIZE)
117
+ continue;
118
+ }
119
+ catch {
120
+ continue;
121
+ }
122
+ // Download file content
123
+ try {
124
+ const result = await this.ws.proxyFsOp(this.nodeId, "read", { path: fullRemotePath }, 15_000);
125
+ mkdirSync(localDirPath, { recursive: true });
126
+ writeFileSync(fullLocalPath, result.content, "utf-8");
127
+ const hash = hashContent(result.content);
128
+ const relPath = fullRemotePath.slice(this.remotePath.length).replace(/^[\\\/]/, "");
129
+ this.snapshot.set(relPath, {
130
+ path: relPath,
131
+ hash,
132
+ size: result.content.length,
133
+ });
134
+ fileCount++;
135
+ totalSize += result.content.length;
136
+ }
137
+ catch (err) {
138
+ // Skip files that can't be read (binary, permission errors, etc.)
139
+ console.warn(`[workspace-sync] Skip ${fullRemotePath}: ${err}`);
140
+ }
141
+ }
142
+ }
143
+ };
144
+ console.log(`[workspace-sync] Pulling ${this.remotePath} from node ${this.nodeId}...`);
145
+ await enumerate(this.remotePath, this.localPath);
146
+ this.pulled = true;
147
+ console.log(`[workspace-sync] Pulled ${fileCount} files (${(totalSize / 1024).toFixed(1)} KB)`);
148
+ return { fileCount, totalSize };
149
+ }
150
+ /**
151
+ * Detect which files changed locally and push them back to the remote node.
152
+ * Also handles new files and deleted files.
153
+ */
154
+ async push() {
155
+ if (!this.pulled)
156
+ return { pushed: 0, deleted: 0 };
157
+ let pushed = 0;
158
+ let deleted = 0;
159
+ const seenRelPaths = new Set();
160
+ // Walk local directory for new/changed files
161
+ const walkLocal = async (localDir, relDir) => {
162
+ let entries;
163
+ try {
164
+ entries = readdirSync(localDir);
165
+ }
166
+ catch {
167
+ return;
168
+ }
169
+ for (const name of entries) {
170
+ if (shouldExclude(name))
171
+ continue;
172
+ const fullLocal = join(localDir, name);
173
+ const relPath = relDir ? `${relDir}/${name}` : name;
174
+ let stat;
175
+ try {
176
+ stat = statSync(fullLocal);
177
+ }
178
+ catch {
179
+ continue;
180
+ }
181
+ if (stat.isDirectory()) {
182
+ await walkLocal(fullLocal, relPath);
183
+ }
184
+ else if (stat.isFile()) {
185
+ seenRelPaths.add(relPath);
186
+ if (stat.size > MAX_FILE_SIZE)
187
+ continue;
188
+ try {
189
+ const content = readFileSync(fullLocal, "utf-8");
190
+ const hash = hashContent(content);
191
+ const prev = this.snapshot.get(relPath);
192
+ if (!prev || prev.hash !== hash) {
193
+ // File is new or changed — push to remote
194
+ const remotePath = joinPath(this.remotePath, relPath.replace(/\//g, getSep()));
195
+ await this.ws.proxyFsOp(this.nodeId, "write", { path: remotePath, content }, 15_000);
196
+ pushed++;
197
+ // Update snapshot
198
+ this.snapshot.set(relPath, { path: relPath, hash, size: content.length });
199
+ }
200
+ }
201
+ catch {
202
+ // Skip files that can't be read (binary)
203
+ }
204
+ }
205
+ }
206
+ };
207
+ await walkLocal(this.localPath, "");
208
+ // Check for deleted files (in original snapshot but no longer on disk)
209
+ for (const [relPath] of this.snapshot) {
210
+ if (!seenRelPaths.has(relPath)) {
211
+ // File was deleted locally — could delete on remote too, but for
212
+ // safety we leave remote files intact. The CLI provider might have
213
+ // intentionally deleted them and the user should verify.
214
+ deleted++;
215
+ }
216
+ }
217
+ if (pushed > 0 || deleted > 0) {
218
+ console.log(`[workspace-sync] Pushed ${pushed} changed files, ${deleted} deleted`);
219
+ }
220
+ return { pushed, deleted };
221
+ }
222
+ /**
223
+ * Clean up the local mirror directory.
224
+ */
225
+ async cleanup() {
226
+ try {
227
+ if (existsSync(this.localPath)) {
228
+ rmSync(this.localPath, { recursive: true, force: true });
229
+ console.log(`[workspace-sync] Cleaned up ${this.localPath}`);
230
+ }
231
+ }
232
+ catch (err) {
233
+ console.warn(`[workspace-sync] Cleanup failed: ${err}`);
234
+ }
235
+ }
236
+ }
237
+ // ── Helpers ──────────────────────────────────────────────────────────
238
+ function shouldExclude(name) {
239
+ for (const pattern of EXCLUDE_PATTERNS) {
240
+ if (pattern.startsWith("*.")) {
241
+ if (name.endsWith(pattern.slice(1)))
242
+ return true;
243
+ }
244
+ else {
245
+ if (name === pattern)
246
+ return true;
247
+ }
248
+ }
249
+ return false;
250
+ }
251
+ function hashContent(content) {
252
+ return createHash("sha256").update(content).digest("hex").slice(0, 16);
253
+ }
254
+ /** Join two path segments using the remote's separator */
255
+ function joinPath(dir, name) {
256
+ const sep = dir.includes("\\") ? "\\" : "/";
257
+ const trimmed = dir.endsWith(sep) ? dir.slice(0, -1) : dir;
258
+ return `${trimmed}${sep}${name}`;
259
+ }
260
+ /** Get the path separator from the remote path style */
261
+ function getSep() {
262
+ // Default to forward slash — the joinPath function in push()
263
+ // converts back to remote style when constructing the remote path
264
+ return "/";
265
+ }
266
+ //# sourceMappingURL=workspace-sync.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"workspace-sync.js","sourceRoot":"","sources":["../../src/services/workspace-sync.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,44 @@
1
+ /**
2
+ * Remote Tool Executor — delegates tool execution to remote nodes.
3
+ *
4
+ * When a session's workspace is bound to a remote node (i.e. the
5
+ * workspace path doesn't exist on the gateway), tool calls are proxied
6
+ * to the remote node via the `tool.op-request` / `tool.op-response`
7
+ * WS protocol instead of executing locally on the gateway.
8
+ *
9
+ * This solves the fundamental problem where CLI providers (Codex, Claude)
10
+ * and built-in tools (terminal.run, file.write, etc.) need to operate
11
+ * on the same machine where the workspace lives.
12
+ *
13
+ * Tools that are gateway-local by nature (e.g. surfaces.list, cron.*,
14
+ * gateway.status) are always executed locally regardless of the workspace
15
+ * binding.
16
+ */
17
+ import type { ToolResult, ToolContext } from "./contracts.js";
18
+ import type { WsControlPlane } from "../ws.js";
19
+ export interface RemoteToolExecutorOptions {
20
+ ws: WsControlPlane;
21
+ /** Local executor — called when the tool should run on the gateway */
22
+ localExecutor: (toolName: string, input: unknown, context: ToolContext, options?: {
23
+ dryRun?: boolean;
24
+ consentTimeoutMs?: number;
25
+ }) => Promise<ToolResult>;
26
+ }
27
+ /**
28
+ * Resolve which node ID (if any) should execute tools for a given session.
29
+ *
30
+ * Returns `null` if the workspace is local to the gateway or no matching
31
+ * remote node is connected.
32
+ */
33
+ export declare function resolveRemoteNodeForSession(ws: WsControlPlane, workspacePath: string | undefined): string | null;
34
+ /**
35
+ * Create a tool executor that transparently delegates to a remote node
36
+ * when the session's workspace is on that node.
37
+ *
38
+ * If `remoteNodeId` is null, all calls go to the local executor.
39
+ */
40
+ export declare function createRemoteToolExecutor(options: RemoteToolExecutorOptions, remoteNodeId: string | null): (toolName: string, input: unknown, context: ToolContext, execOptions?: {
41
+ dryRun?: boolean;
42
+ consentTimeoutMs?: number;
43
+ }) => Promise<ToolResult>;
44
+ //# sourceMappingURL=remote-executor.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"remote-executor.d.ts","sourceRoot":"","sources":["../../src/tools/remote-executor.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAEH,OAAO,KAAK,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAC;AAC9D,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,UAAU,CAAC;AAyB/C,MAAM,WAAW,yBAAyB;IACxC,EAAE,EAAE,cAAc,CAAC;IACnB,sEAAsE;IACtE,aAAa,EAAE,CACb,QAAQ,EAAE,MAAM,EAChB,KAAK,EAAE,OAAO,EACd,OAAO,EAAE,WAAW,EACpB,OAAO,CAAC,EAAE;QAAE,MAAM,CAAC,EAAE,OAAO,CAAC;QAAC,gBAAgB,CAAC,EAAE,MAAM,CAAA;KAAE,KACtD,OAAO,CAAC,UAAU,CAAC,CAAC;CAC1B;AAED;;;;;GAKG;AACH,wBAAgB,2BAA2B,CACzC,EAAE,EAAE,cAAc,EAClB,aAAa,EAAE,MAAM,GAAG,SAAS,GAChC,MAAM,GAAG,IAAI,CAwBf;AAED;;;;;GAKG;AACH,wBAAgB,wBAAwB,CACtC,OAAO,EAAE,yBAAyB,EAClC,YAAY,EAAE,MAAM,GAAG,IAAI,GAC1B,CACD,QAAQ,EAAE,MAAM,EAChB,KAAK,EAAE,OAAO,EACd,OAAO,EAAE,WAAW,EACpB,WAAW,CAAC,EAAE;IAAE,MAAM,CAAC,EAAE,OAAO,CAAC;IAAC,gBAAgB,CAAC,EAAE,MAAM,CAAA;CAAE,KAC1D,OAAO,CAAC,UAAU,CAAC,CAoCvB"}
@@ -0,0 +1,107 @@
1
+ /**
2
+ * Remote Tool Executor — delegates tool execution to remote nodes.
3
+ *
4
+ * When a session's workspace is bound to a remote node (i.e. the
5
+ * workspace path doesn't exist on the gateway), tool calls are proxied
6
+ * to the remote node via the `tool.op-request` / `tool.op-response`
7
+ * WS protocol instead of executing locally on the gateway.
8
+ *
9
+ * This solves the fundamental problem where CLI providers (Codex, Claude)
10
+ * and built-in tools (terminal.run, file.write, etc.) need to operate
11
+ * on the same machine where the workspace lives.
12
+ *
13
+ * Tools that are gateway-local by nature (e.g. surfaces.list, cron.*,
14
+ * gateway.status) are always executed locally regardless of the workspace
15
+ * binding.
16
+ */
17
+ /** Tools that always run on the gateway regardless of workspace location */
18
+ const GATEWAY_LOCAL_TOOLS = new Set([
19
+ "surfaces.list",
20
+ "surfaces.start",
21
+ "surfaces.stop",
22
+ "cron.add",
23
+ "cron.list",
24
+ "cron.update",
25
+ "cron.remove",
26
+ "gateway.status",
27
+ "tools.search",
28
+ "tools.list",
29
+ "memory.read",
30
+ "memory.write",
31
+ "memory.list",
32
+ "memory.search",
33
+ "voice.say",
34
+ "voice.listen",
35
+ "screen.share",
36
+ "screen.stop",
37
+ "notification.send",
38
+ ]);
39
+ /**
40
+ * Resolve which node ID (if any) should execute tools for a given session.
41
+ *
42
+ * Returns `null` if the workspace is local to the gateway or no matching
43
+ * remote node is connected.
44
+ */
45
+ export function resolveRemoteNodeForSession(ws, workspacePath) {
46
+ if (!workspacePath)
47
+ return null;
48
+ // Check if the workspace path exists on the gateway
49
+ // We use a lightweight sync check — existsSync is fine here since this
50
+ // is called once per chat request, not in a hot loop.
51
+ try {
52
+ const { existsSync } = require("node:fs");
53
+ if (existsSync(workspacePath))
54
+ return null;
55
+ }
56
+ catch {
57
+ return null;
58
+ }
59
+ // Path doesn't exist locally — find a matching remote node
60
+ const isWindowsPath = /^[A-Za-z]:[\\\/]/.test(workspacePath);
61
+ const expectedPlatform = isWindowsPath ? "windows" : null;
62
+ for (const node of ws.getFsNodes()) {
63
+ if (node.isGateway)
64
+ continue;
65
+ if (expectedPlatform && node.platform !== expectedPlatform)
66
+ continue;
67
+ return node.id;
68
+ }
69
+ return null;
70
+ }
71
+ /**
72
+ * Create a tool executor that transparently delegates to a remote node
73
+ * when the session's workspace is on that node.
74
+ *
75
+ * If `remoteNodeId` is null, all calls go to the local executor.
76
+ */
77
+ export function createRemoteToolExecutor(options, remoteNodeId) {
78
+ const { ws, localExecutor } = options;
79
+ return async (toolName, input, context, execOptions) => {
80
+ // Always execute gateway-local tools locally
81
+ if (!remoteNodeId || GATEWAY_LOCAL_TOOLS.has(toolName)) {
82
+ return localExecutor(toolName, input, context, execOptions);
83
+ }
84
+ // Check that the remote node is still connected
85
+ const node = ws.findNodeByDeviceId(remoteNodeId);
86
+ if (!node) {
87
+ console.warn(`[remote-executor] Node ${remoteNodeId} disconnected, falling back to local execution`);
88
+ return localExecutor(toolName, input, context, execOptions);
89
+ }
90
+ // Delegate to the remote node
91
+ try {
92
+ const result = await ws.proxyToolOp(remoteNodeId, toolName, input, {
93
+ timeoutMs: 120_000,
94
+ sessionId: context.sessionId,
95
+ workspaceRoot: context.workspaceRoot,
96
+ onOutputChunk: context.onOutputChunk,
97
+ });
98
+ return result;
99
+ }
100
+ catch (err) {
101
+ const message = err instanceof Error ? err.message : String(err);
102
+ console.error(`[remote-executor] Remote tool '${toolName}' failed on node ${remoteNodeId}: ${message}`);
103
+ return { ok: false, message: `Remote execution failed: ${message}` };
104
+ }
105
+ };
106
+ }
107
+ //# sourceMappingURL=remote-executor.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"remote-executor.js","sourceRoot":"","sources":["../../src/tools/remote-executor.ts"],"names":[],"mappings":"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"}
package/dist/ws.d.ts CHANGED
@@ -16,6 +16,8 @@ export declare class WsControlPlane {
16
16
  private pendingFsOps;
17
17
  /** Pending remote provider operation requests */
18
18
  private pendingProviderOps;
19
+ /** Pending remote tool execution requests */
20
+ private pendingToolOps;
19
21
  constructor(config: AppConfig);
20
22
  /**
21
23
  * Attach the WebSocket server to an existing HTTP server (shares port).
@@ -106,6 +108,16 @@ export declare class WsControlPlane {
106
108
  * Returns undefined if the path belongs to the gateway or no matching node is found.
107
109
  */
108
110
  findNodeByDeviceId(deviceId: string): FsNode | undefined;
111
+ /**
112
+ * Send a tool execution request to a remote node and wait for the result.
113
+ * Used to delegate Jait tool calls (terminal.run, file.write, etc.) to nodes.
114
+ */
115
+ proxyToolOp<T = unknown>(nodeId: string, tool: string, args: Record<string, unknown>, options?: {
116
+ timeoutMs?: number;
117
+ sessionId?: string;
118
+ workspaceRoot?: string;
119
+ onOutputChunk?: (chunk: string) => void;
120
+ }): Promise<T>;
109
121
  /**
110
122
  * Send a provider operation request to a remote node and wait for the response.
111
123
  * Used by RemoteCliProvider to proxy session lifecycle and turn operations.
package/dist/ws.d.ts.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"ws.d.ts","sourceRoot":"","sources":["../src/ws.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"ws.d.ts","sourceRoot":"","sources":["../src/ws.ts"],"names":[],"mappings":"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"}
package/dist/ws.js CHANGED
@@ -16,6 +16,9 @@ export class WsControlPlane {
16
16
  /** Pending remote provider operation requests */
17
17
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
18
18
  pendingProviderOps = new Map();
19
+ /** Pending remote tool execution requests */
20
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
21
+ pendingToolOps = new Map();
19
22
  constructor(config) {
20
23
  this.config = config;
21
24
  // Use the configured JWT secret, or a dev-mode fallback
@@ -404,6 +407,34 @@ export class WsControlPlane {
404
407
  }
405
408
  break;
406
409
  }
410
+ // ── Remote tool execution responses ─────────────────────────
411
+ case "tool.op-response": {
412
+ const resp = msg.payload;
413
+ if (resp?.requestId) {
414
+ const pending = this.pendingToolOps.get(resp.requestId);
415
+ if (pending) {
416
+ this.pendingToolOps.delete(resp.requestId);
417
+ clearTimeout(pending.timer);
418
+ if (resp.error) {
419
+ pending.reject(new Error(resp.error));
420
+ }
421
+ else {
422
+ pending.resolve(resp.result);
423
+ }
424
+ }
425
+ }
426
+ break;
427
+ }
428
+ case "tool.op-output": {
429
+ const outPayload = msg.payload;
430
+ if (outPayload?.requestId && outPayload.chunk) {
431
+ const pending = this.pendingToolOps.get(outPayload.requestId);
432
+ if (pending?.onOutputChunk) {
433
+ pending.onOutputChunk(outPayload.chunk);
434
+ }
435
+ }
436
+ break;
437
+ }
407
438
  // ── Generic fs operation responses (stat, read, write, list) ──
408
439
  case "fs.op-response": {
409
440
  const resp = msg.payload;
@@ -732,6 +763,36 @@ export class WsControlPlane {
732
763
  }
733
764
  return undefined;
734
765
  }
766
+ /**
767
+ * Send a tool execution request to a remote node and wait for the result.
768
+ * Used to delegate Jait tool calls (terminal.run, file.write, etc.) to nodes.
769
+ */
770
+ proxyToolOp(nodeId, tool, args, options = {}) {
771
+ const { timeoutMs = 120_000, sessionId, workspaceRoot, onOutputChunk } = options;
772
+ const node = this.fsNodes.get(nodeId);
773
+ if (!node)
774
+ return Promise.reject(new Error(`Unknown node: ${nodeId}`));
775
+ if (node.isGateway)
776
+ return Promise.reject(new Error("Use local execution for gateway node"));
777
+ const client = this.clients.get(node.clientId);
778
+ if (!client || client.ws.readyState !== 1) {
779
+ return Promise.reject(new Error(`Node ${nodeId} is not connected`));
780
+ }
781
+ const requestId = nanoid();
782
+ return new Promise((resolve, reject) => {
783
+ const timer = setTimeout(() => {
784
+ this.pendingToolOps.delete(requestId);
785
+ reject(new Error(`Tool '${tool}' timed out on node ${nodeId}`));
786
+ }, timeoutMs);
787
+ this.pendingToolOps.set(requestId, { resolve: resolve, reject, timer, onOutputChunk });
788
+ this.send(client.ws, {
789
+ type: "tool.op-request",
790
+ sessionId: sessionId ?? "",
791
+ timestamp: new Date().toISOString(),
792
+ payload: { requestId, tool, args, sessionId, workspaceRoot },
793
+ });
794
+ });
795
+ }
735
796
  /**
736
797
  * Send a provider operation request to a remote node and wait for the response.
737
798
  * Used by RemoteCliProvider to proxy session lifecycle and turn operations.