@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.
- package/dist/routes/chat.d.ts.map +1 -1
- package/dist/routes/chat.js +104 -71
- package/dist/routes/chat.js.map +1 -1
- package/dist/routes/threads.d.ts.map +1 -1
- package/dist/routes/threads.js +97 -98
- package/dist/routes/threads.js.map +1 -1
- package/dist/services/workspace-sync.d.ts +56 -0
- package/dist/services/workspace-sync.d.ts.map +1 -0
- package/dist/services/workspace-sync.js +266 -0
- package/dist/services/workspace-sync.js.map +1 -0
- package/dist/tools/remote-executor.d.ts +44 -0
- package/dist/tools/remote-executor.d.ts.map +1 -0
- package/dist/tools/remote-executor.js +107 -0
- package/dist/tools/remote-executor.js.map +1 -0
- package/dist/ws.d.ts +12 -0
- package/dist/ws.d.ts.map +1 -1
- package/dist/ws.js +61 -0
- package/dist/ws.js.map +1 -1
- package/package.json +1 -1
- package/web-dist/assets/{index-DvxRPDAc.js → index-Cc_KBEXt.js} +46 -46
- package/web-dist/dist/assets/{index-DvxRPDAc.js → index-Cc_KBEXt.js} +46 -46
- package/web-dist/dist/index.html +1 -1
- package/web-dist/index.html +1 -1
|
@@ -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":"AACA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AAC5C,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AACrD,OAAO,KAAK,EAIV,uBAAuB,EACvB,MAAM,EACN,aAAa,EACd,MAAM,cAAc,CAAC;AACtB,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AAG7C,OAAO,KAAK,EAAE,MAAM,IAAI,UAAU,EAAE,MAAM,WAAW,CAAC;AActD,qBAAa,cAAc;
|
|
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.
|