@sipher.dev/agents 0.1.0 → 0.1.2
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/connector-platforms.json +14 -0
- package/connector-runtime.json +4 -0
- package/dist/index.js +319 -3400
- package/dist/local-control-DKnrc-X8.d.ts +27 -0
- package/dist/local-control-Dm6FcXAi.js +429 -0
- package/dist/local-control-server.d.ts +18 -0
- package/dist/local-control-server.js +149 -0
- package/dist/local-control.d.ts +355 -0
- package/dist/local-control.js +2 -0
- package/dist/local-machine-controller-CjOyllJj.js +453 -0
- package/dist/release-resolver-PnPDodU9.js +5828 -0
- package/dist/src-ghbvDJyd.js +731 -0
- package/package.json +32 -23
- package/release-trust-anchor.json +11 -0
- package/dist/local-machine-controller-Dtiash-C.js +0 -2129
- package/dist/local-machine.d.ts +0 -177
- package/dist/local-machine.js +0 -2
package/dist/index.js
CHANGED
|
@@ -1,3412 +1,337 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { A as
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
11
|
-
import WebSocket from "ws";
|
|
12
|
-
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
13
|
-
import { ListRootsRequestSchema, ToolListChangedNotificationSchema } from "@modelcontextprotocol/sdk/types.js";
|
|
14
|
-
import { z } from "zod";
|
|
15
|
-
import process$1 from "node:process";
|
|
2
|
+
import { A as insecureTlsForServer, M as parseArgs, O as deploymentServiceKey, b as resolveConnectorProfileContext, d as TokenStore, g as runServiceCommand, i as resolveVerifiedConnectorSnapshot, m as installConnectorLauncherService, r as resolveVerifiedConnectorRelease, s as withDeploymentReconciliationLock, y as defaultConnectorAuthStorePath } from "./release-resolver-PnPDodU9.js";
|
|
3
|
+
import { a as repairConnectorProfile, c as startConnectorDeviceAuthorization, i as pollConnectorDeviceToken, l as CliAuthClient, n as discoverDeploymentForStore, o as resumeConnectorBootstrap, r as ensureAccessToken, s as setupConnectorProfile } from "./local-machine-controller-CjOyllJj.js";
|
|
4
|
+
import { r as discoverLocalConnector } from "./local-control-Dm6FcXAi.js";
|
|
5
|
+
import { spawn } from "node:child_process";
|
|
6
|
+
import { homedir, hostname, platform } from "node:os";
|
|
7
|
+
import { mkdir, readFile, rename, rm, writeFile } from "node:fs/promises";
|
|
8
|
+
import { dirname, join } from "node:path";
|
|
9
|
+
import { setTimeout as setTimeout$1 } from "node:timers/promises";
|
|
10
|
+
import { randomUUID } from "node:crypto";
|
|
16
11
|
//#region src/cli-setup.ts
|
|
17
12
|
async function login(options = {}) {
|
|
18
13
|
const store = options.store ?? new TokenStore();
|
|
19
14
|
const serverUrl = await store.resolveServerUrl(options.serverUrl);
|
|
20
|
-
const
|
|
21
|
-
serverUrl,
|
|
22
|
-
deviceLabel: hostname(),
|
|
23
|
-
allowInsecureTls: options.allowInsecureTls
|
|
24
|
-
});
|
|
25
|
-
console.log(`Open ${device.verificationUriComplete}`);
|
|
26
|
-
console.log(`Code: ${device.userCode}`);
|
|
27
|
-
if (options.openBrowser !== false) openBrowser(device.verificationUriComplete);
|
|
28
|
-
await pollConnectorDeviceToken({
|
|
29
|
-
serverUrl,
|
|
30
|
-
store,
|
|
31
|
-
deviceCode: device.deviceCode,
|
|
32
|
-
intervalSeconds: device.interval,
|
|
33
|
-
expiresInSeconds: device.expiresIn,
|
|
34
|
-
allowInsecureTls: options.allowInsecureTls
|
|
35
|
-
});
|
|
36
|
-
const setup = await setupConnectorProfile({
|
|
37
|
-
serverUrl,
|
|
38
|
-
store,
|
|
39
|
-
name: options.name,
|
|
40
|
-
installService: options.installService ?? true,
|
|
41
|
-
serviceInstaller: options.serviceInstaller ?? defaultServiceInstaller(options.profileName)
|
|
42
|
-
});
|
|
43
|
-
console.log(`Logged in to ${serverUrl}.`);
|
|
44
|
-
console.log(`Machine: ${setup.machineId}`);
|
|
45
|
-
}
|
|
46
|
-
function serviceInstallConfigInput(input, profileName) {
|
|
47
|
-
return {
|
|
48
|
-
serverUrl: input.serverUrl,
|
|
49
|
-
...profileName ? { profileName } : {},
|
|
50
|
-
...input.allowInsecureTls ? { allowInsecureTls: true } : {}
|
|
51
|
-
};
|
|
52
|
-
}
|
|
53
|
-
function defaultServiceInstaller(profileName) {
|
|
54
|
-
return (input) => runServiceCommand("install", serviceInstallConfigInput(input, profileName));
|
|
55
|
-
}
|
|
56
|
-
async function logout(options = {}) {
|
|
57
|
-
const store = options.store ?? new TokenStore();
|
|
58
|
-
const serverUrl = await store.resolveServerUrl(options.serverUrl);
|
|
59
|
-
const profile = await store.removeProfile(serverUrl);
|
|
60
|
-
if (profile?.refreshToken) await new CliAuthClient(serverUrl, { allowInsecureTls: options.allowInsecureTls ?? profile.allowInsecureTls }).revoke(profile.refreshToken).catch(() => void 0);
|
|
61
|
-
console.log(`Logged out of ${serverUrl}.`);
|
|
62
|
-
}
|
|
63
|
-
async function whoami(options = {}) {
|
|
64
|
-
const store = options.store ?? new TokenStore();
|
|
65
|
-
const serverUrl = await store.resolveServerUrl(options.serverUrl);
|
|
66
|
-
const profile = await store.getProfile(serverUrl);
|
|
67
|
-
if (!profile?.refreshToken) throw new Error(`Not logged in to ${serverUrl}. Run agents login.`);
|
|
68
|
-
await ensureAccessToken({
|
|
15
|
+
const deployment = await discoverDeploymentForStore({
|
|
69
16
|
serverUrl,
|
|
70
17
|
store,
|
|
71
18
|
allowInsecureTls: options.allowInsecureTls
|
|
72
19
|
});
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
const
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
"APPDATA",
|
|
108
|
-
"HOMEDRIVE",
|
|
109
|
-
"HOMEPATH",
|
|
110
|
-
"LOCALAPPDATA",
|
|
111
|
-
"PATH",
|
|
112
|
-
"PROCESSOR_ARCHITECTURE",
|
|
113
|
-
"SYSTEMDRIVE",
|
|
114
|
-
"SYSTEMROOT",
|
|
115
|
-
"TEMP",
|
|
116
|
-
"USERNAME",
|
|
117
|
-
"USERPROFILE",
|
|
118
|
-
"PROGRAMFILES"
|
|
119
|
-
] : [
|
|
120
|
-
"HOME",
|
|
121
|
-
"LOGNAME",
|
|
122
|
-
"PATH",
|
|
123
|
-
"SHELL",
|
|
124
|
-
"TERM",
|
|
125
|
-
"USER"
|
|
126
|
-
];
|
|
127
|
-
}
|
|
128
|
-
var BoundedStdioTransport = class {
|
|
129
|
-
options;
|
|
130
|
-
onclose;
|
|
131
|
-
onerror;
|
|
132
|
-
onmessage;
|
|
133
|
-
child;
|
|
134
|
-
groupPid;
|
|
135
|
-
chunks = [];
|
|
136
|
-
bufferedBytes = 0;
|
|
137
|
-
stderrTail = Buffer.alloc(0);
|
|
138
|
-
closing;
|
|
139
|
-
forceClosing;
|
|
140
|
-
exitedCleanup;
|
|
141
|
-
constructor(options) {
|
|
142
|
-
this.options = options;
|
|
143
|
-
}
|
|
144
|
-
get pid() {
|
|
145
|
-
return this.child?.pid ?? null;
|
|
146
|
-
}
|
|
147
|
-
get stderr() {
|
|
148
|
-
return this.stderrTail.toString("utf8");
|
|
149
|
-
}
|
|
150
|
-
async start() {
|
|
151
|
-
if (this.child) throw new Error("MCP stdio transport already started.");
|
|
152
|
-
const child = spawn(this.options.command, this.options.args, {
|
|
153
|
-
cwd: this.options.cwd,
|
|
154
|
-
env: safeEnvironment(),
|
|
155
|
-
detached: process$1.platform !== "win32",
|
|
156
|
-
shell: false,
|
|
157
|
-
stdio: [
|
|
158
|
-
"pipe",
|
|
159
|
-
"pipe",
|
|
160
|
-
"pipe"
|
|
161
|
-
],
|
|
162
|
-
windowsHide: true
|
|
163
|
-
});
|
|
164
|
-
this.child = child;
|
|
165
|
-
this.groupPid = child.pid;
|
|
166
|
-
child.stdout.on("data", (chunk) => this.acceptChunk(chunk));
|
|
167
|
-
child.stderr.on("data", (chunk) => this.acceptStderr(chunk));
|
|
168
|
-
child.stdin.on("error", (error) => this.onerror?.(error));
|
|
169
|
-
child.stdout.on("error", (error) => this.fail(error));
|
|
170
|
-
child.on("error", (error) => this.fail(error));
|
|
171
|
-
child.on("close", () => {
|
|
172
|
-
const unexpected = !this.closing && !this.forceClosing;
|
|
173
|
-
if (this.child === child) this.child = void 0;
|
|
174
|
-
if (unexpected && child.pid) this.exitedCleanup ??= this.cleanupExitedGroup(child);
|
|
175
|
-
this.options.onProcessExit?.(unexpected);
|
|
176
|
-
this.onclose?.();
|
|
177
|
-
});
|
|
178
|
-
await new Promise((resolve, reject) => {
|
|
179
|
-
child.once("spawn", resolve);
|
|
180
|
-
child.once("error", reject);
|
|
181
|
-
});
|
|
182
|
-
}
|
|
183
|
-
async send(message) {
|
|
184
|
-
const child = this.child;
|
|
185
|
-
if (!child?.stdin.writable) throw new Error("MCP stdio transport is not connected.");
|
|
186
|
-
const payload = `${JSON.stringify(message)}\n`;
|
|
187
|
-
if (utf8Bytes(payload) > 6291456) throw new Error("MCP stdio request frame is too large.");
|
|
188
|
-
if (!child.stdin.write(payload)) await new Promise((resolve, reject) => {
|
|
189
|
-
child.stdin.once("drain", resolve);
|
|
190
|
-
child.stdin.once("error", reject);
|
|
191
|
-
});
|
|
192
|
-
}
|
|
193
|
-
close() {
|
|
194
|
-
this.closing ??= this.closeChild(false);
|
|
195
|
-
return this.closing;
|
|
196
|
-
}
|
|
197
|
-
forceClose() {
|
|
198
|
-
this.forceClosing ??= this.forceCloseChild();
|
|
199
|
-
return this.forceClosing;
|
|
200
|
-
}
|
|
201
|
-
acceptChunk(chunk) {
|
|
202
|
-
let offset = 0;
|
|
203
|
-
while (offset < chunk.length) {
|
|
204
|
-
const newline = chunk.indexOf(10, offset);
|
|
205
|
-
const end = newline < 0 ? chunk.length : newline;
|
|
206
|
-
const bytes = end - offset;
|
|
207
|
-
if (this.bufferedBytes + bytes + (newline >= 0 ? 1 : 0) > 6291456) {
|
|
208
|
-
this.fail(/* @__PURE__ */ new Error("MCP stdio response frame is too large."));
|
|
209
|
-
return;
|
|
210
|
-
}
|
|
211
|
-
if (bytes > 0) {
|
|
212
|
-
this.chunks.push(chunk.subarray(offset, end));
|
|
213
|
-
this.bufferedBytes += bytes;
|
|
214
|
-
}
|
|
215
|
-
if (newline < 0) return;
|
|
216
|
-
this.emitLine();
|
|
217
|
-
offset = newline + 1;
|
|
218
|
-
}
|
|
219
|
-
}
|
|
220
|
-
emitLine() {
|
|
221
|
-
const chunks = this.chunks;
|
|
222
|
-
this.chunks = [];
|
|
223
|
-
this.bufferedBytes = 0;
|
|
224
|
-
if (chunks.length === 0) return;
|
|
225
|
-
try {
|
|
226
|
-
const line = chunks.length === 1 ? chunks[0].toString("utf8") : Buffer.concat(chunks).toString("utf8");
|
|
227
|
-
this.onmessage?.(JSON.parse(line));
|
|
228
|
-
} catch (error) {
|
|
229
|
-
this.fail(error instanceof Error ? error : /* @__PURE__ */ new Error("Invalid MCP stdio JSON."));
|
|
230
|
-
}
|
|
231
|
-
}
|
|
232
|
-
acceptStderr(chunk) {
|
|
233
|
-
const combined = Buffer.concat([this.stderrTail, chunk]);
|
|
234
|
-
this.stderrTail = utf8Tail(combined, MACHINE_MCP_MAX_STDERR_BYTES);
|
|
235
|
-
this.options.onStderr?.(this.stderr);
|
|
236
|
-
}
|
|
237
|
-
fail(error) {
|
|
238
|
-
this.onerror?.(error);
|
|
239
|
-
this.forceClose();
|
|
240
|
-
}
|
|
241
|
-
async closeChild(force) {
|
|
242
|
-
const child = this.child;
|
|
243
|
-
this.chunks = [];
|
|
244
|
-
this.bufferedBytes = 0;
|
|
245
|
-
if (!child || child.exitCode !== null) {
|
|
246
|
-
if (this.child === child) this.child = void 0;
|
|
247
|
-
await this.exitedCleanup;
|
|
248
|
-
return;
|
|
249
|
-
}
|
|
250
|
-
const closed = new Promise((resolve) => child.once("close", () => resolve()));
|
|
251
|
-
try {
|
|
252
|
-
child.stdin.end();
|
|
253
|
-
} catch {}
|
|
254
|
-
const terminate = this.options.terminate ?? terminateTree;
|
|
255
|
-
if (force) {
|
|
256
|
-
await terminate(child, "force");
|
|
257
|
-
await waitFor(closed, 1e3);
|
|
258
|
-
if (this.child === child) this.child = void 0;
|
|
259
|
-
if (this.groupPid === child.pid) this.groupPid = void 0;
|
|
260
|
-
return;
|
|
261
|
-
}
|
|
262
|
-
await waitFor(closed, 1e3);
|
|
263
|
-
if (this.forceClosing) {
|
|
264
|
-
if (this.child === child) this.child = void 0;
|
|
265
|
-
return;
|
|
266
|
-
}
|
|
267
|
-
await terminate(child, "graceful");
|
|
268
|
-
await waitFor(closed, 1e3);
|
|
269
|
-
await terminate(child, "force");
|
|
270
|
-
await waitFor(closed, 1e3);
|
|
271
|
-
if (this.child === child) this.child = void 0;
|
|
272
|
-
if (this.groupPid === child.pid) this.groupPid = void 0;
|
|
273
|
-
}
|
|
274
|
-
async forceCloseChild() {
|
|
275
|
-
if (!this.closing) {
|
|
276
|
-
this.closing = this.closeChild(true);
|
|
277
|
-
await this.closing;
|
|
278
|
-
return;
|
|
279
|
-
}
|
|
280
|
-
const child = this.child;
|
|
281
|
-
if (child && child.exitCode === null) await (this.options.terminate ?? terminateTree)(child, "force");
|
|
282
|
-
await this.closing;
|
|
283
|
-
}
|
|
284
|
-
async cleanupExitedGroup(child) {
|
|
285
|
-
try {
|
|
286
|
-
await (this.options.terminate ?? terminateTree)(child, "force");
|
|
287
|
-
} finally {
|
|
288
|
-
if (this.groupPid === child.pid) this.groupPid = void 0;
|
|
289
|
-
}
|
|
290
|
-
}
|
|
291
|
-
};
|
|
292
|
-
async function terminateTree(child, mode, runtime = {}) {
|
|
293
|
-
if (!child.pid) return;
|
|
294
|
-
if ((runtime.platform ?? process$1.platform) === "win32") {
|
|
295
|
-
await waitFor(new Promise((resolve) => {
|
|
296
|
-
const args = [
|
|
297
|
-
"/PID",
|
|
298
|
-
String(child.pid),
|
|
299
|
-
"/T",
|
|
300
|
-
...mode === "force" ? ["/F"] : []
|
|
301
|
-
];
|
|
302
|
-
(runtime.spawnProcess ?? spawn)("taskkill", args, {
|
|
303
|
-
shell: false,
|
|
304
|
-
windowsHide: true,
|
|
305
|
-
stdio: "ignore"
|
|
306
|
-
}).once("close", () => resolve());
|
|
307
|
-
}), runtime.taskkillTimeoutMs ?? 1e3);
|
|
308
|
-
return;
|
|
309
|
-
}
|
|
310
|
-
try {
|
|
311
|
-
const signal = mode === "force" ? "SIGKILL" : "SIGTERM";
|
|
312
|
-
if (runtime.killProcess) runtime.killProcess(-child.pid, signal);
|
|
313
|
-
else process$1.kill(-child.pid, signal);
|
|
314
|
-
} catch {
|
|
315
|
-
try {
|
|
316
|
-
child.kill(mode === "force" ? "SIGKILL" : "SIGTERM");
|
|
317
|
-
} catch {}
|
|
318
|
-
}
|
|
319
|
-
}
|
|
320
|
-
function safeEnvironment(environment = process$1.env, platform = process$1.platform) {
|
|
321
|
-
return Object.fromEntries(inheritedEnvironmentKeys(platform).flatMap((key) => {
|
|
322
|
-
const value = environment[key];
|
|
323
|
-
return value !== void 0 && !value.startsWith("()") ? [[key, value]] : [];
|
|
324
|
-
}));
|
|
325
|
-
}
|
|
326
|
-
function utf8Tail(value, maxBytes) {
|
|
327
|
-
if (value.length <= maxBytes) return value;
|
|
328
|
-
let start = value.length - maxBytes;
|
|
329
|
-
while (start < value.length && (value[start] & 192) === 128) start += 1;
|
|
330
|
-
return value.subarray(start);
|
|
331
|
-
}
|
|
332
|
-
async function waitFor(promise, timeoutMs) {
|
|
333
|
-
return Promise.race([promise.then(() => true), new Promise((resolve) => {
|
|
334
|
-
setTimeout(() => resolve(false), timeoutMs).unref();
|
|
335
|
-
})]);
|
|
336
|
-
}
|
|
337
|
-
const MCP_PROBE_TIMEOUT_MS = 3e4;
|
|
338
|
-
const LIST_CHANGE_DEBOUNCE_MS = 100;
|
|
339
|
-
const RawListToolsResultSchema = z.object({
|
|
340
|
-
tools: z.array(z.unknown()),
|
|
341
|
-
nextCursor: z.string().optional()
|
|
342
|
-
}).passthrough();
|
|
343
|
-
var McpProcessManager = class {
|
|
344
|
-
options;
|
|
345
|
-
entries = /* @__PURE__ */ new Map();
|
|
346
|
-
starting = /* @__PURE__ */ new Map();
|
|
347
|
-
startingLaunches = /* @__PURE__ */ new Map();
|
|
348
|
-
stoppedKeys = /* @__PURE__ */ new Set();
|
|
349
|
-
stoppedSelectors = /* @__PURE__ */ new Map();
|
|
350
|
-
cancelledStartupKeys = /* @__PURE__ */ new Set();
|
|
351
|
-
startupCancelOwner = /* @__PURE__ */ new Map();
|
|
352
|
-
probeRequests = /* @__PURE__ */ new Map();
|
|
353
|
-
callRequests = /* @__PURE__ */ new Map();
|
|
354
|
-
cancelledProbeRequests = /* @__PURE__ */ new Set();
|
|
355
|
-
cancelledCallRequests = /* @__PURE__ */ new Set();
|
|
356
|
-
probeUsers = /* @__PURE__ */ new Map();
|
|
357
|
-
requestEntries = /* @__PURE__ */ new Map();
|
|
358
|
-
abortReasons = /* @__PURE__ */ new Map();
|
|
359
|
-
startupExitedKeys = /* @__PURE__ */ new Set();
|
|
360
|
-
startingTransports = /* @__PURE__ */ new Map();
|
|
361
|
-
closingTransports = /* @__PURE__ */ new Set();
|
|
362
|
-
closed = false;
|
|
363
|
-
constructor(options = {}) {
|
|
364
|
-
this.options = options;
|
|
365
|
-
}
|
|
366
|
-
get processCount() {
|
|
367
|
-
return this.entries.size + this.starting.size + this.closingTransports.size;
|
|
368
|
-
}
|
|
369
|
-
async probe(request) {
|
|
370
|
-
this.assertOpen();
|
|
371
|
-
const launch = {
|
|
372
|
-
...request.params.launch,
|
|
373
|
-
projectRoot: this.home()
|
|
374
|
-
};
|
|
375
|
-
const key = `probe\0${processKey(launch)}`;
|
|
376
|
-
this.probeRequests.set(request.id, key);
|
|
377
|
-
this.probeUsers.set(key, (this.probeUsers.get(key) ?? 0) + 1);
|
|
378
|
-
let entry;
|
|
379
|
-
try {
|
|
380
|
-
entry = await this.startEntry(launch, Math.min(request.params.timeoutMs, MCP_PROBE_TIMEOUT_MS), key);
|
|
381
|
-
if (this.cancelledProbeRequests.has(request.id)) throw new MachineMcpValidationError("cancelled", "MCP probe was cancelled.");
|
|
382
|
-
if (!entry.reusable || entry.observedGeneration !== entry.validatedGeneration) throw new MachineMcpValidationError("stale_catalog", "MCP catalog changed during probe discovery.");
|
|
383
|
-
return {
|
|
384
|
-
kind: "probe",
|
|
385
|
-
catalog: entry.catalog,
|
|
386
|
-
fingerprint: entry.fingerprint,
|
|
387
|
-
...entry.transport.stderr ? { stderrTail: entry.transport.stderr } : {}
|
|
388
|
-
};
|
|
389
|
-
} catch (error) {
|
|
390
|
-
if (this.cancelledProbeRequests.has(request.id)) throw new MachineMcpValidationError("cancelled", "MCP probe was cancelled.");
|
|
391
|
-
throw error;
|
|
392
|
-
} finally {
|
|
393
|
-
this.probeRequests.delete(request.id);
|
|
394
|
-
this.cancelledProbeRequests.delete(request.id);
|
|
395
|
-
const users = (this.probeUsers.get(key) ?? 1) - 1;
|
|
396
|
-
if (users === 0) {
|
|
397
|
-
this.probeUsers.delete(key);
|
|
398
|
-
if (entry) await this.closeEntry(entry);
|
|
399
|
-
} else this.probeUsers.set(key, users);
|
|
400
|
-
this.cleanupStartupCancellation(key);
|
|
401
|
-
}
|
|
402
|
-
}
|
|
403
|
-
async call(request) {
|
|
404
|
-
this.assertOpen();
|
|
405
|
-
if (!request.params.launch.catalogFingerprint) throw new MachineMcpValidationError("invalid_request", "MCP calls require an accepted catalog fingerprint.");
|
|
406
|
-
const key = processKey(request.params.launch);
|
|
407
|
-
const deadline = Date.now() + request.params.timeoutMs;
|
|
408
|
-
this.callRequests.set(request.id, key);
|
|
409
|
-
let entry;
|
|
410
|
-
try {
|
|
411
|
-
entry = await this.getOrStart(request.params.launch, true, deadline);
|
|
412
|
-
if (this.closed || this.cancelledCallRequests.has(request.id)) {
|
|
413
|
-
await this.closeEntry(entry, true);
|
|
414
|
-
throw new MachineMcpValidationError("cancelled", "MCP tool call was cancelled during startup.");
|
|
415
|
-
}
|
|
416
|
-
} catch (error) {
|
|
417
|
-
const stopReason = this.abortReasons.get(request.id);
|
|
418
|
-
const owner = this.startupCancelOwner.get(key);
|
|
419
|
-
const wasCancelled = this.closed || this.cancelledCallRequests.has(request.id) || owner !== void 0 || this.cancelledStartupKeys.has(key);
|
|
420
|
-
this.finishCallRequest(request.id, key);
|
|
421
|
-
if (stopReason === "stale_catalog") throw new MachineMcpValidationError("stale_catalog", "MCP tool call was stopped because its catalog authority changed.");
|
|
422
|
-
if (wasCancelled) {
|
|
423
|
-
const collateral = owner !== void 0 && owner !== request.id;
|
|
424
|
-
throw new MachineMcpValidationError(collateral ? "process_terminated_due_to_cancel" : "cancelled", collateral ? "MCP tool call ended because its shared startup was terminated." : "MCP tool call was cancelled during startup.");
|
|
425
|
-
}
|
|
426
|
-
throw error;
|
|
427
|
-
}
|
|
428
|
-
if (!entry.reusable || entry.observedGeneration !== entry.validatedGeneration) {
|
|
429
|
-
this.finishCallRequest(request.id, key);
|
|
430
|
-
throw new MachineMcpValidationError("stale_catalog", "MCP catalog changed before tool call.");
|
|
431
|
-
}
|
|
432
|
-
if (entry.activeCalls.size >= 2) {
|
|
433
|
-
this.finishCallRequest(request.id, key);
|
|
434
|
-
throw new MachineMcpValidationError("mcp_busy", "MCP process call capacity is full.");
|
|
435
|
-
}
|
|
436
|
-
const controller = new AbortController();
|
|
437
|
-
entry.activeCalls.set(request.id, controller);
|
|
438
|
-
this.requestEntries.set(request.id, entry);
|
|
439
|
-
try {
|
|
440
|
-
const timeoutMs = remainingMs(deadline);
|
|
441
|
-
const result = await entry.client.callTool({
|
|
442
|
-
name: request.params.toolName,
|
|
443
|
-
arguments: request.params.arguments
|
|
444
|
-
}, void 0, {
|
|
445
|
-
signal: controller.signal,
|
|
446
|
-
timeout: timeoutMs,
|
|
447
|
-
maxTotalTimeout: timeoutMs
|
|
448
|
-
});
|
|
449
|
-
if (!entry.reusable || entry.observedGeneration !== entry.validatedGeneration) throw new MachineMcpValidationError("stale_catalog", "MCP catalog changed during tool call.");
|
|
450
|
-
validateMachineMcpResult(result);
|
|
451
|
-
return {
|
|
452
|
-
kind: "call",
|
|
453
|
-
result
|
|
454
|
-
};
|
|
455
|
-
} catch (error) {
|
|
456
|
-
if (controller.signal.aborted) {
|
|
457
|
-
const reason = this.abortReasons.get(request.id) ?? "cancelled";
|
|
458
|
-
throw new MachineMcpValidationError(reason, reason === "process_terminated_due_to_cancel" ? "MCP tool call ended because its shared process was terminated." : reason === "process_exited" ? "MCP process exited during the tool call." : reason === "stale_catalog" ? "MCP tool call was stopped because its catalog authority changed." : "MCP tool call was cancelled.");
|
|
459
|
-
}
|
|
460
|
-
if (/timeout|timed out/i.test(error instanceof Error ? error.message : "")) {
|
|
461
|
-
entry.reusable = false;
|
|
462
|
-
for (const id of entry.activeCalls.keys()) if (id !== request.id) this.abortReasons.set(id, "process_terminated_due_to_cancel");
|
|
463
|
-
setTimeout(() => void this.closeEntry(entry), 1e3).unref();
|
|
464
|
-
throw new MachineMcpValidationError("timeout", "MCP tool call timed out.");
|
|
465
|
-
}
|
|
466
|
-
throw error;
|
|
467
|
-
} finally {
|
|
468
|
-
entry.activeCalls.delete(request.id);
|
|
469
|
-
this.requestEntries.delete(request.id);
|
|
470
|
-
this.abortReasons.delete(request.id);
|
|
471
|
-
this.finishCallRequest(request.id, key);
|
|
472
|
-
this.touch(entry);
|
|
473
|
-
}
|
|
474
|
-
}
|
|
475
|
-
cancel(requestId) {
|
|
476
|
-
const entry = this.requestEntries.get(requestId);
|
|
477
|
-
const controller = entry?.activeCalls.get(requestId);
|
|
478
|
-
if (!entry || !controller) {
|
|
479
|
-
const callKey = this.callRequests.get(requestId);
|
|
480
|
-
if (callKey) {
|
|
481
|
-
this.cancelledCallRequests.add(requestId);
|
|
482
|
-
this.cancelledStartupKeys.add(callKey);
|
|
483
|
-
this.startupCancelOwner.set(callKey, requestId);
|
|
484
|
-
this.startingTransports.get(callKey)?.forceClose();
|
|
485
|
-
const active = this.entries.get(callKey);
|
|
486
|
-
if (active) this.closeEntry(active, true);
|
|
487
|
-
return true;
|
|
488
|
-
}
|
|
489
|
-
const key = this.probeRequests.get(requestId);
|
|
490
|
-
if (!key) return false;
|
|
491
|
-
this.cancelledProbeRequests.add(requestId);
|
|
492
|
-
this.cancelledStartupKeys.add(key);
|
|
493
|
-
this.startupCancelOwner.set(key, requestId);
|
|
494
|
-
this.startingTransports.get(key)?.forceClose();
|
|
495
|
-
const active = this.entries.get(key);
|
|
496
|
-
if (active) this.closeEntry(active, true);
|
|
497
|
-
return true;
|
|
498
|
-
}
|
|
499
|
-
entry.reusable = false;
|
|
500
|
-
this.abortReasons.set(requestId, "cancelled");
|
|
501
|
-
for (const id of entry.activeCalls.keys()) if (id !== requestId) this.abortReasons.set(id, "process_terminated_due_to_cancel");
|
|
502
|
-
controller.abort();
|
|
503
|
-
setTimeout(() => void this.closeEntry(entry), 1e3).unref();
|
|
504
|
-
return true;
|
|
505
|
-
}
|
|
506
|
-
async stop(input) {
|
|
507
|
-
if (!this.addStopSelector(input)) {
|
|
508
|
-
const stopped = this.processCount;
|
|
509
|
-
await this.close(true);
|
|
510
|
-
return stopped;
|
|
511
|
-
}
|
|
512
|
-
for (const [requestId, key] of this.callRequests) {
|
|
513
|
-
const launch = this.startingLaunches.get(key) ?? this.entries.get(key)?.launch;
|
|
514
|
-
if (launch && matchesStop(launch, input)) this.abortReasons.set(requestId, "stale_catalog");
|
|
515
|
-
}
|
|
516
|
-
const matches = [...this.entries.values()].filter((entry) => entry.launch.definitionId === input.definitionId && (input.configVersion === void 0 || entry.launch.configVersion === input.configVersion) && (input.projectRoot === void 0 || entry.launch.projectRoot === input.projectRoot));
|
|
517
|
-
const startingKeys = [...this.startingLaunches.entries()].filter(([key, launch]) => {
|
|
518
|
-
return matchesStop(launch, key.startsWith("probe\0") ? {
|
|
519
|
-
definitionId: input.definitionId,
|
|
520
|
-
...input.configVersion === void 0 ? {} : { configVersion: input.configVersion }
|
|
521
|
-
} : input);
|
|
522
|
-
}).map(([key]) => key);
|
|
523
|
-
for (const key of startingKeys) {
|
|
524
|
-
this.stoppedKeys.add(key);
|
|
525
|
-
await this.startingTransports.get(key)?.forceClose();
|
|
526
|
-
}
|
|
527
|
-
for (const entry of matches) for (const requestId of entry.activeCalls.keys()) this.abortReasons.set(requestId, "stale_catalog");
|
|
528
|
-
await Promise.all(matches.map((entry) => this.closeEntry(entry)));
|
|
529
|
-
return matches.length + startingKeys.length;
|
|
530
|
-
}
|
|
531
|
-
async close(force = false) {
|
|
532
|
-
if (this.closed && this.entries.size === 0 && this.starting.size === 0 && this.closingTransports.size === 0) return;
|
|
533
|
-
this.closed = true;
|
|
534
|
-
for (const key of this.starting.keys()) this.cancelledStartupKeys.add(key);
|
|
535
|
-
const entryClosures = [...this.entries.values()].map((entry) => this.closeEntry(entry, force));
|
|
536
|
-
await Promise.all([
|
|
537
|
-
...[...this.startingTransports.values()].map((transport) => force ? transport.forceClose() : transport.close()),
|
|
538
|
-
...force ? [...[...this.closingTransports].map((transport) => transport.forceClose())] : [],
|
|
539
|
-
...entryClosures
|
|
540
|
-
]);
|
|
541
|
-
await Promise.allSettled(this.starting.values());
|
|
542
|
-
await Promise.all([...this.entries.values()].map((entry) => this.closeEntry(entry, force)));
|
|
543
|
-
}
|
|
544
|
-
async getOrStart(launch, requireFingerprint, deadline) {
|
|
545
|
-
const key = processKey(launch);
|
|
546
|
-
const existing = this.entries.get(key);
|
|
547
|
-
if (existing) if (!sameLaunchConfiguration(existing.launch, launch)) await this.closeEntry(existing);
|
|
548
|
-
else {
|
|
549
|
-
if (launch.catalogRevision < existing.catalogRevision) throw new MachineMcpValidationError("stale_catalog", "MCP catalog revision is stale.");
|
|
550
|
-
if (existing.observedGeneration !== existing.validatedGeneration && launch.catalogRevision === existing.catalogRevision) throw new MachineMcpValidationError("stale_catalog", "MCP catalog changed before a newer accepted revision was provided.");
|
|
551
|
-
if (launch.catalogRevision > existing.catalogRevision) {
|
|
552
|
-
if (existing.revalidation) {
|
|
553
|
-
await withTimeout(existing.revalidation, remainingMs(deadline));
|
|
554
|
-
return this.getOrStart(launch, requireFingerprint, deadline);
|
|
555
|
-
}
|
|
556
|
-
existing.reservations += 1;
|
|
557
|
-
let outcome;
|
|
558
|
-
const revalidation = this.revalidate(existing, launch, remainingMs(deadline));
|
|
559
|
-
existing.revalidation = revalidation;
|
|
560
|
-
try {
|
|
561
|
-
outcome = await revalidation;
|
|
562
|
-
} finally {
|
|
563
|
-
existing.reservations -= 1;
|
|
564
|
-
if (existing.revalidation === revalidation) existing.revalidation = void 0;
|
|
565
|
-
}
|
|
566
|
-
if (outcome === "matched") return existing;
|
|
567
|
-
if (outcome === "stale" || launch.catalogRevision === existing.catalogRevision) throw new MachineMcpValidationError("stale_catalog", "MCP catalog changed during validation.");
|
|
568
|
-
await this.closeEntry(existing);
|
|
569
|
-
} else if (existing.reusable && matchesAcceptedCatalog(existing, launch, requireFingerprint)) return existing;
|
|
570
|
-
else await this.closeEntry(existing);
|
|
571
|
-
}
|
|
572
|
-
const entry = await this.startEntry(launch, remainingMs(deadline));
|
|
573
|
-
if (!matchesAcceptedCatalog(entry, launch, requireFingerprint)) {
|
|
574
|
-
await this.closeEntry(entry);
|
|
575
|
-
throw new MachineMcpValidationError("root_catalog_mismatch", "MCP catalog does not match the accepted Project-root catalog.");
|
|
576
|
-
}
|
|
577
|
-
return entry;
|
|
578
|
-
}
|
|
579
|
-
async revalidate(entry, launch, timeoutMs) {
|
|
580
|
-
const observed = entry.observedGeneration;
|
|
581
|
-
const { catalog, fingerprint } = sanitizeAndFingerprintMachineMcpCatalog(await listFullCatalog(entry.client, Date.now() + timeoutMs));
|
|
582
|
-
if (entry.observedGeneration !== observed) return "stale";
|
|
583
|
-
if (launch.catalogFingerprint && fingerprint !== launch.catalogFingerprint) return "mismatch";
|
|
584
|
-
entry.catalog = catalog;
|
|
585
|
-
entry.fingerprint = fingerprint;
|
|
586
|
-
entry.catalogRevision = launch.catalogRevision;
|
|
587
|
-
entry.validatedGeneration = observed;
|
|
588
|
-
entry.launch = launch;
|
|
589
|
-
entry.reusable = true;
|
|
590
|
-
if (entry.changeTimer) clearTimeout(entry.changeTimer);
|
|
591
|
-
entry.changeTimer = void 0;
|
|
592
|
-
return "matched";
|
|
593
|
-
}
|
|
594
|
-
startEntry(launch, timeoutMs, key = processKey(launch)) {
|
|
595
|
-
if (this.isStopped(key, launch)) throw new MachineMcpValidationError("cancelled", "MCP process was stopped before startup.");
|
|
596
|
-
const current = this.starting.get(key);
|
|
597
|
-
if (current) {
|
|
598
|
-
const startingLaunch = this.startingLaunches.get(key);
|
|
599
|
-
if (!startingLaunch || !sameStartupAuthority(startingLaunch, launch)) throw new MachineMcpValidationError("invalid_request", "MCP startup request does not match the in-progress launch.");
|
|
600
|
-
return withTimeout(current, timeoutMs);
|
|
601
|
-
}
|
|
602
|
-
const promise = this.createEntry(key, launch, timeoutMs).finally(() => {
|
|
603
|
-
this.starting.delete(key);
|
|
604
|
-
this.startingLaunches.delete(key);
|
|
605
|
-
this.startingTransports.delete(key);
|
|
606
|
-
this.startupExitedKeys.delete(key);
|
|
607
|
-
this.cleanupStartupCancellation(key);
|
|
608
|
-
});
|
|
609
|
-
this.starting.set(key, promise);
|
|
610
|
-
this.startingLaunches.set(key, launch);
|
|
611
|
-
return promise;
|
|
612
|
-
}
|
|
613
|
-
async createEntry(key, launch, timeoutMs) {
|
|
614
|
-
const deadline = Date.now() + timeoutMs;
|
|
615
|
-
validateMachineMcpLaunchSpec(launch);
|
|
616
|
-
const resolved = substituteMachineMcpLaunch(launch, void 0, this.home());
|
|
617
|
-
await this.reserveProcessSlot(deadline);
|
|
618
|
-
this.assertOpen();
|
|
619
|
-
if (this.isStopped(key, launch) || this.cancelledStartupKeys.has(key)) throw new MachineMcpValidationError("cancelled", "MCP process was stopped before startup.");
|
|
620
|
-
let entry;
|
|
621
|
-
const transport = new BoundedStdioTransport({
|
|
622
|
-
...resolved,
|
|
623
|
-
onProcessExit: (unexpected) => {
|
|
624
|
-
if (entry) this.removeExitedEntry(entry);
|
|
625
|
-
else if (unexpected) this.startupExitedKeys.add(key);
|
|
626
|
-
}
|
|
627
|
-
});
|
|
628
|
-
this.startingTransports.set(key, transport);
|
|
629
|
-
const client = new Client({
|
|
630
|
-
name: "agents-machine-connector",
|
|
631
|
-
version: "0.1.0"
|
|
632
|
-
}, { capabilities: { roots: { listChanged: false } } });
|
|
633
|
-
client.setRequestHandler(ListRootsRequestSchema, () => ({ roots: [{
|
|
634
|
-
uri: pathToFileURL(launch.projectRoot).href,
|
|
635
|
-
name: "Project root"
|
|
636
|
-
}] }));
|
|
637
|
-
let observedGeneration = 0;
|
|
638
|
-
client.setNotificationHandler(ToolListChangedNotificationSchema, () => {
|
|
639
|
-
observedGeneration += 1;
|
|
640
|
-
if (!entry) return;
|
|
641
|
-
entry.observedGeneration = observedGeneration;
|
|
642
|
-
entry.reusable = false;
|
|
643
|
-
if (entry.changeTimer) return;
|
|
644
|
-
entry.changeTimer = setTimeout(() => {
|
|
645
|
-
entry.changeTimer = void 0;
|
|
646
|
-
this.options.onCatalogChanged?.({
|
|
647
|
-
type: "mcp/event",
|
|
648
|
-
event: "tools/list_changed",
|
|
649
|
-
definitionId: launch.definitionId,
|
|
650
|
-
configVersion: launch.configVersion,
|
|
651
|
-
projectRoot: launch.projectRoot,
|
|
652
|
-
observedGeneration: entry.observedGeneration
|
|
653
|
-
});
|
|
654
|
-
}, LIST_CHANGE_DEBOUNCE_MS);
|
|
655
|
-
entry.changeTimer.unref();
|
|
656
|
-
});
|
|
657
|
-
try {
|
|
658
|
-
await client.connect(transport, {
|
|
659
|
-
timeout: remainingMs(deadline),
|
|
660
|
-
maxTotalTimeout: remainingMs(deadline)
|
|
661
|
-
});
|
|
662
|
-
const observedBeforeList = observedGeneration;
|
|
663
|
-
const { catalog, fingerprint } = sanitizeAndFingerprintMachineMcpCatalog(await listFullCatalog(client, deadline));
|
|
664
|
-
if (this.closed || this.isStopped(key, launch) || this.cancelledStartupKeys.has(key)) throw new MachineMcpValidationError("cancelled", "MCP process was stopped during startup.");
|
|
665
|
-
const timer = setTimeout(() => {}, this.options.idleMs ?? 6e5);
|
|
666
|
-
timer.unref();
|
|
667
|
-
entry = {
|
|
668
|
-
key,
|
|
669
|
-
launch,
|
|
670
|
-
client,
|
|
671
|
-
transport,
|
|
672
|
-
catalog,
|
|
673
|
-
fingerprint,
|
|
674
|
-
catalogRevision: launch.catalogRevision,
|
|
675
|
-
observedGeneration,
|
|
676
|
-
validatedGeneration: observedBeforeList === observedGeneration ? observedGeneration : observedBeforeList,
|
|
677
|
-
activeCalls: /* @__PURE__ */ new Map(),
|
|
678
|
-
reservations: 0,
|
|
679
|
-
reusable: observedBeforeList === observedGeneration,
|
|
680
|
-
lastUsedAt: this.now(),
|
|
681
|
-
idleTimer: timer,
|
|
682
|
-
changeTimer: void 0,
|
|
683
|
-
revalidation: void 0
|
|
684
|
-
};
|
|
685
|
-
this.entries.set(key, entry);
|
|
686
|
-
this.startingTransports.delete(key);
|
|
687
|
-
this.touch(entry);
|
|
688
|
-
return entry;
|
|
689
|
-
} catch (error) {
|
|
690
|
-
this.startingTransports.delete(key);
|
|
691
|
-
const processExited = this.startupExitedKeys.has(key);
|
|
692
|
-
await transport.close();
|
|
693
|
-
if (this.isStopped(key, launch) || this.cancelledStartupKeys.has(key)) throw new MachineMcpValidationError("cancelled", "MCP process was stopped during startup.");
|
|
694
|
-
if (processExited) throw new MachineMcpValidationError("process_exited", "MCP process exited during startup.");
|
|
695
|
-
throw mapProcessError(error, transport.stderr);
|
|
696
|
-
}
|
|
697
|
-
}
|
|
698
|
-
async reserveProcessSlot(deadline) {
|
|
699
|
-
if (this.processCount < 16) return;
|
|
700
|
-
const idle = [...this.entries.values()].filter((entry) => entry.activeCalls.size === 0 && entry.reservations === 0).sort((a, b) => a.lastUsedAt - b.lastUsedAt)[0];
|
|
701
|
-
if (!idle) throw new MachineMcpValidationError("process_limit", "MCP process limit reached.");
|
|
702
|
-
await withTimeout(this.closeEntry(idle), remainingMs(deadline));
|
|
703
|
-
}
|
|
704
|
-
isStopped(key, launch) {
|
|
705
|
-
if (this.stoppedKeys.has(key)) return true;
|
|
706
|
-
for (const input of this.stoppedSelectors.values()) if (matchesStop(launch, key.startsWith("probe\0") ? {
|
|
707
|
-
definitionId: input.definitionId,
|
|
708
|
-
...input.configVersion === void 0 ? {} : { configVersion: input.configVersion }
|
|
709
|
-
} : input)) return true;
|
|
710
|
-
return false;
|
|
711
|
-
}
|
|
712
|
-
addStopSelector(input) {
|
|
713
|
-
for (const selector of this.stoppedSelectors.values()) if (subsumesStopSelector(selector, input)) return true;
|
|
714
|
-
for (const [key, selector] of this.stoppedSelectors) if (subsumesStopSelector(input, selector)) this.stoppedSelectors.delete(key);
|
|
715
|
-
if (this.stoppedSelectors.size >= 256) return false;
|
|
716
|
-
this.stoppedSelectors.set(stopSelectorKey(input), input);
|
|
717
|
-
return true;
|
|
718
|
-
}
|
|
719
|
-
touch(entry) {
|
|
720
|
-
if (this.entries.get(entry.key) !== entry) return;
|
|
721
|
-
entry.lastUsedAt = this.now();
|
|
722
|
-
this.armIdleTimer(entry);
|
|
723
|
-
}
|
|
724
|
-
armIdleTimer(entry) {
|
|
725
|
-
clearTimeout(entry.idleTimer);
|
|
726
|
-
entry.idleTimer = setTimeout(() => {
|
|
727
|
-
if (entry.activeCalls.size > 0 || entry.reservations > 0) {
|
|
728
|
-
this.armIdleTimer(entry);
|
|
729
|
-
return;
|
|
730
|
-
}
|
|
731
|
-
this.closeEntry(entry);
|
|
732
|
-
}, this.options.idleMs ?? 6e5);
|
|
733
|
-
entry.idleTimer.unref();
|
|
734
|
-
}
|
|
735
|
-
now() {
|
|
736
|
-
return this.options.now?.() ?? Date.now();
|
|
737
|
-
}
|
|
738
|
-
home() {
|
|
739
|
-
return this.options.home ?? homedir();
|
|
740
|
-
}
|
|
741
|
-
finishCallRequest(requestId, key) {
|
|
742
|
-
this.callRequests.delete(requestId);
|
|
743
|
-
this.cancelledCallRequests.delete(requestId);
|
|
744
|
-
this.abortReasons.delete(requestId);
|
|
745
|
-
this.cleanupStartupCancellation(key);
|
|
746
|
-
}
|
|
747
|
-
cleanupStartupCancellation(key) {
|
|
748
|
-
if (this.starting.has(key)) return;
|
|
749
|
-
if ([...this.callRequests.values()].includes(key)) return;
|
|
750
|
-
if ([...this.probeRequests.values()].includes(key)) return;
|
|
751
|
-
this.cancelledStartupKeys.delete(key);
|
|
752
|
-
this.startupCancelOwner.delete(key);
|
|
753
|
-
}
|
|
754
|
-
async closeEntry(entry, force = false) {
|
|
755
|
-
if (this.entries.get(entry.key) !== entry) return;
|
|
756
|
-
this.entries.delete(entry.key);
|
|
757
|
-
entry.reusable = false;
|
|
758
|
-
clearTimeout(entry.idleTimer);
|
|
759
|
-
if (entry.changeTimer) clearTimeout(entry.changeTimer);
|
|
760
|
-
for (const controller of entry.activeCalls.values()) controller.abort();
|
|
761
|
-
this.closingTransports.add(entry.transport);
|
|
762
|
-
const forcedClose = force ? entry.transport.forceClose() : void 0;
|
|
763
|
-
try {
|
|
764
|
-
await entry.client.close().catch(() => void 0);
|
|
765
|
-
await (forcedClose ?? entry.transport.close()).catch(() => void 0);
|
|
766
|
-
} finally {
|
|
767
|
-
this.closingTransports.delete(entry.transport);
|
|
768
|
-
}
|
|
769
|
-
}
|
|
770
|
-
removeExitedEntry(entry) {
|
|
771
|
-
if (this.entries.get(entry.key) !== entry) return;
|
|
772
|
-
this.entries.delete(entry.key);
|
|
773
|
-
entry.reusable = false;
|
|
774
|
-
clearTimeout(entry.idleTimer);
|
|
775
|
-
if (entry.changeTimer) clearTimeout(entry.changeTimer);
|
|
776
|
-
for (const [requestId, controller] of entry.activeCalls) {
|
|
777
|
-
this.abortReasons.set(requestId, "process_exited");
|
|
778
|
-
controller.abort();
|
|
779
|
-
}
|
|
780
|
-
}
|
|
781
|
-
assertOpen() {
|
|
782
|
-
if (this.closed) throw new MachineMcpValidationError("process_exited", "MCP manager is closed.");
|
|
783
|
-
}
|
|
784
|
-
};
|
|
785
|
-
async function listFullCatalog(client, deadline) {
|
|
786
|
-
const tools = [];
|
|
787
|
-
const seenCursors = /* @__PURE__ */ new Set();
|
|
788
|
-
let cursor;
|
|
789
|
-
for (let pageIndex = 0; pageIndex < 512; pageIndex += 1) {
|
|
790
|
-
const timeoutMs = remainingMs(deadline);
|
|
791
|
-
const page = await client.request({
|
|
792
|
-
method: "tools/list",
|
|
793
|
-
...cursor ? { params: { cursor } } : {}
|
|
794
|
-
}, RawListToolsResultSchema, {
|
|
795
|
-
timeout: timeoutMs,
|
|
796
|
-
maxTotalTimeout: timeoutMs
|
|
797
|
-
});
|
|
798
|
-
if (page.tools.length === 0 && page.nextCursor) throw new MachineMcpValidationError("protocol_error", "MCP tools/list returned an empty page with a continuation cursor.");
|
|
799
|
-
tools.push(...page.tools);
|
|
800
|
-
if (tools.length > 500) throw new MachineMcpValidationError("catalog_overflow", "MCP catalog has too many tools.");
|
|
801
|
-
const nextCursor = page.nextCursor;
|
|
802
|
-
if (!nextCursor) {
|
|
803
|
-
cursor = void 0;
|
|
804
|
-
break;
|
|
805
|
-
}
|
|
806
|
-
if (seenCursors.has(nextCursor)) throw new MachineMcpValidationError("protocol_error", "MCP tools/list repeated a cursor.");
|
|
807
|
-
seenCursors.add(nextCursor);
|
|
808
|
-
cursor = nextCursor;
|
|
809
|
-
}
|
|
810
|
-
if (cursor) throw new MachineMcpValidationError("catalog_overflow", "MCP tools/list exceeded the page limit.");
|
|
811
|
-
const server = client.getServerVersion();
|
|
812
|
-
return {
|
|
813
|
-
serverInfo: {
|
|
814
|
-
name: server?.name ?? "unknown",
|
|
815
|
-
...server?.version ? { version: server.version } : {}
|
|
816
|
-
},
|
|
817
|
-
tools
|
|
818
|
-
};
|
|
819
|
-
}
|
|
820
|
-
function remainingMs(deadline) {
|
|
821
|
-
const remaining = deadline - Date.now();
|
|
822
|
-
if (remaining <= 0) throw new MachineMcpValidationError("timeout", "MCP operation timed out.");
|
|
823
|
-
return remaining;
|
|
824
|
-
}
|
|
825
|
-
function processKey(launch) {
|
|
826
|
-
return `${launch.definitionId}\0${launch.configVersion}\0${launch.projectRoot}`;
|
|
827
|
-
}
|
|
828
|
-
function sameStartupAuthority(left, right) {
|
|
829
|
-
return left.catalogRevision === right.catalogRevision && left.catalogFingerprint === right.catalogFingerprint && sameLaunchConfiguration(left, right);
|
|
830
|
-
}
|
|
831
|
-
function sameLaunchConfiguration(left, right) {
|
|
832
|
-
return left.command === right.command && left.cwdMode === right.cwdMode && left.args.length === right.args.length && left.args.every((arg, index) => arg === right.args[index]);
|
|
833
|
-
}
|
|
834
|
-
function matchesStop(launch, input) {
|
|
835
|
-
return launch.definitionId === input.definitionId && (input.configVersion === void 0 || launch.configVersion === input.configVersion) && (input.projectRoot === void 0 || launch.projectRoot === input.projectRoot);
|
|
836
|
-
}
|
|
837
|
-
function stopSelectorKey(input) {
|
|
838
|
-
return `${input.definitionId}\0${input.configVersion ?? "*"}\0${input.projectRoot ?? "*"}`;
|
|
839
|
-
}
|
|
840
|
-
function subsumesStopSelector(selector, candidate) {
|
|
841
|
-
return selector.definitionId === candidate.definitionId && (selector.configVersion === void 0 || selector.configVersion === candidate.configVersion) && (selector.projectRoot === void 0 || selector.projectRoot === candidate.projectRoot);
|
|
842
|
-
}
|
|
843
|
-
function matchesAcceptedCatalog(entry, launch, requireFingerprint) {
|
|
844
|
-
if (entry.catalogRevision !== launch.catalogRevision) return false;
|
|
845
|
-
if (!launch.catalogFingerprint) return !requireFingerprint;
|
|
846
|
-
return entry.fingerprint === launch.catalogFingerprint;
|
|
847
|
-
}
|
|
848
|
-
function mapProcessError(error, stderr) {
|
|
849
|
-
if (error instanceof MachineMcpValidationError) return error;
|
|
850
|
-
const message = error instanceof Error ? error.message : "MCP process failed.";
|
|
851
|
-
const mapped = new MachineMcpValidationError(/ENOENT|not found/i.test(message) ? "command_not_found" : /timeout|timed out/i.test(message) ? "timeout" : "protocol_error", message);
|
|
852
|
-
return stderr ? Object.assign(mapped, { stderrTail: stderr }) : mapped;
|
|
853
|
-
}
|
|
854
|
-
function withTimeout(promise, timeoutMs) {
|
|
855
|
-
let timer;
|
|
856
|
-
return Promise.race([promise, new Promise((_, reject) => {
|
|
857
|
-
timer = setTimeout(() => reject(new MachineMcpValidationError("timeout", "MCP startup timed out.")), timeoutMs);
|
|
858
|
-
timer.unref();
|
|
859
|
-
})]).finally(() => {
|
|
860
|
-
if (timer) clearTimeout(timer);
|
|
861
|
-
});
|
|
862
|
-
}
|
|
863
|
-
var ConnectorMcpGeneration = class {
|
|
864
|
-
socket;
|
|
865
|
-
home;
|
|
866
|
-
manager;
|
|
867
|
-
queue = [];
|
|
868
|
-
active = /* @__PURE__ */ new Map();
|
|
869
|
-
activeByProcess = /* @__PURE__ */ new Map();
|
|
870
|
-
controlIds = /* @__PURE__ */ new Set();
|
|
871
|
-
closed = false;
|
|
872
|
-
closePromise;
|
|
873
|
-
constructor(socket, manager, home = homedir()) {
|
|
874
|
-
this.socket = socket;
|
|
875
|
-
this.home = home;
|
|
876
|
-
this.manager = manager ?? new McpProcessManager({ onCatalogChanged: (event) => this.send(event) });
|
|
877
|
-
}
|
|
878
|
-
handles(data) {
|
|
879
|
-
const buffer = rawBuffer(data);
|
|
880
|
-
return buffer.length >= 4 && buffer.subarray(0, 4).equals(Buffer.from("mcp:"));
|
|
881
|
-
}
|
|
882
|
-
accept(data) {
|
|
883
|
-
const buffer = rawBuffer(data);
|
|
884
|
-
if (buffer.length > 6291456) {
|
|
885
|
-
this.sendError("unknown", "frame_too_large", "MCP connector frame is too large.");
|
|
886
|
-
return;
|
|
887
|
-
}
|
|
888
|
-
let request;
|
|
889
|
-
try {
|
|
890
|
-
request = validateMachineMcpRequest(JSON.parse(buffer.subarray(MACHINE_MCP_TAG.length).toString("utf8")));
|
|
891
|
-
if (request.method === "mcp/probe" || request.method === "mcp/call") {
|
|
892
|
-
const launch = request.method === "mcp/probe" ? {
|
|
893
|
-
...request.params.launch,
|
|
894
|
-
projectRoot: this.home
|
|
895
|
-
} : request.params.launch;
|
|
896
|
-
validateMachineMcpLaunchSpec(launch);
|
|
897
|
-
substituteMachineMcpLaunch(launch, void 0, this.home);
|
|
898
|
-
}
|
|
899
|
-
} catch (error) {
|
|
900
|
-
const validation = toValidationError(error);
|
|
901
|
-
this.sendError(requestIdFromFrame(buffer), validation.code, validation.message);
|
|
902
|
-
return;
|
|
903
|
-
}
|
|
904
|
-
if (this.active.has(request.id) || this.controlIds.has(request.id) || this.queue.some((record) => record.request.id === request.id)) {
|
|
905
|
-
this.sendError(request.id, "invalid_request", "MCP request id is already in use.");
|
|
906
|
-
return;
|
|
907
|
-
}
|
|
908
|
-
if (request.method === "mcp/cancel") {
|
|
909
|
-
this.controlIds.add(request.id);
|
|
910
|
-
try {
|
|
911
|
-
this.cancel(request.id, request.params.requestId);
|
|
912
|
-
} finally {
|
|
913
|
-
this.controlIds.delete(request.id);
|
|
914
|
-
}
|
|
915
|
-
return;
|
|
916
|
-
}
|
|
917
|
-
if (request.method === "mcp/stop") {
|
|
918
|
-
this.controlIds.add(request.id);
|
|
919
|
-
this.stop(request).finally(() => this.controlIds.delete(request.id));
|
|
920
|
-
return;
|
|
921
|
-
}
|
|
922
|
-
if (this.queue.length >= 64) {
|
|
923
|
-
this.sendError(request.id, "mcp_busy", "MCP work queue is full.");
|
|
924
|
-
return;
|
|
925
|
-
}
|
|
926
|
-
const timeoutMs = request.params.timeoutMs;
|
|
927
|
-
const record = {
|
|
928
|
-
request,
|
|
929
|
-
receivedAt: Date.now(),
|
|
930
|
-
timer: setTimeout(() => this.expireQueued(request.id), timeoutMs)
|
|
931
|
-
};
|
|
932
|
-
record.timer.unref();
|
|
933
|
-
this.queue.push(record);
|
|
934
|
-
this.drain();
|
|
935
|
-
}
|
|
936
|
-
async close(force = false) {
|
|
937
|
-
if (!this.closed) {
|
|
938
|
-
this.closed = true;
|
|
939
|
-
for (const record of this.queue.splice(0)) {
|
|
940
|
-
clearTimeout(record.timer);
|
|
941
|
-
this.sendError(record.request.id, "cancelled", "MCP work cancelled by connector shutdown.");
|
|
942
|
-
}
|
|
943
|
-
for (const requestId of this.active.keys()) this.manager.cancel(requestId);
|
|
944
|
-
this.closePromise = this.manager.close(false);
|
|
945
|
-
}
|
|
946
|
-
if (force) await this.manager.close(true);
|
|
947
|
-
await this.closePromise;
|
|
948
|
-
}
|
|
949
|
-
drain() {
|
|
950
|
-
while (!this.closed && this.active.size < 8 && this.queue.length > 0) {
|
|
951
|
-
const record = this.queue[0];
|
|
952
|
-
const key = workProcessKey(record);
|
|
953
|
-
if (record.request.method === "mcp/call" && (this.activeByProcess.get(key) ?? 0) >= 2) return;
|
|
954
|
-
this.queue.shift();
|
|
955
|
-
clearTimeout(record.timer);
|
|
956
|
-
const elapsed = Date.now() - record.receivedAt;
|
|
957
|
-
const remaining = record.request.params.timeoutMs - elapsed;
|
|
958
|
-
if (remaining <= 0) {
|
|
959
|
-
this.sendError(record.request.id, "timeout", "MCP work timed out in queue.");
|
|
960
|
-
continue;
|
|
961
|
-
}
|
|
962
|
-
record.request.params.timeoutMs = remaining;
|
|
963
|
-
this.active.set(record.request.id, record);
|
|
964
|
-
if (record.request.method === "mcp/call") this.activeByProcess.set(key, (this.activeByProcess.get(key) ?? 0) + 1);
|
|
965
|
-
this.run(record);
|
|
966
|
-
}
|
|
967
|
-
}
|
|
968
|
-
async run(record) {
|
|
969
|
-
try {
|
|
970
|
-
const result = record.request.method === "mcp/probe" ? await this.manager.probe(record.request) : await this.manager.call(record.request);
|
|
971
|
-
this.send({
|
|
972
|
-
type: "mcp/response",
|
|
973
|
-
id: record.request.id,
|
|
974
|
-
result
|
|
975
|
-
});
|
|
976
|
-
} catch (error) {
|
|
977
|
-
const validation = toValidationError(error);
|
|
978
|
-
const stderrTail = record.request.method === "mcp/probe" && "stderrTail" in validation && typeof validation.stderrTail === "string" ? validation.stderrTail : void 0;
|
|
979
|
-
this.sendError(record.request.id, validation.code, validation.message, stderrTail);
|
|
980
|
-
} finally {
|
|
981
|
-
this.active.delete(record.request.id);
|
|
982
|
-
if (record.request.method === "mcp/call") {
|
|
983
|
-
const key = workProcessKey(record);
|
|
984
|
-
const remaining = (this.activeByProcess.get(key) ?? 1) - 1;
|
|
985
|
-
if (remaining === 0) this.activeByProcess.delete(key);
|
|
986
|
-
else this.activeByProcess.set(key, remaining);
|
|
987
|
-
}
|
|
988
|
-
this.drain();
|
|
989
|
-
}
|
|
990
|
-
}
|
|
991
|
-
cancel(controlId, requestId) {
|
|
992
|
-
const index = this.queue.findIndex((record) => record.request.id === requestId);
|
|
993
|
-
if (index >= 0) {
|
|
994
|
-
const [record] = this.queue.splice(index, 1);
|
|
995
|
-
clearTimeout(record.timer);
|
|
996
|
-
this.sendError(requestId, "cancelled", "MCP work was cancelled before startup.");
|
|
997
|
-
this.send({
|
|
998
|
-
type: "mcp/response",
|
|
999
|
-
id: controlId,
|
|
1000
|
-
result: {
|
|
1001
|
-
kind: "control",
|
|
1002
|
-
cancelled: true
|
|
1003
|
-
}
|
|
1004
|
-
});
|
|
1005
|
-
return;
|
|
1006
|
-
}
|
|
1007
|
-
const cancelled = this.manager.cancel(requestId);
|
|
1008
|
-
this.send({
|
|
1009
|
-
type: "mcp/response",
|
|
1010
|
-
id: controlId,
|
|
1011
|
-
result: {
|
|
1012
|
-
kind: "control",
|
|
1013
|
-
cancelled
|
|
1014
|
-
}
|
|
1015
|
-
});
|
|
1016
|
-
}
|
|
1017
|
-
async stop(request) {
|
|
1018
|
-
try {
|
|
1019
|
-
const removed = this.removeQueued((work) => {
|
|
1020
|
-
const launch = work.request.params.launch;
|
|
1021
|
-
return launch.definitionId === request.params.definitionId && (request.params.configVersion === void 0 || launch.configVersion === request.params.configVersion) && (request.params.projectRoot === void 0 || launch.projectRoot === request.params.projectRoot);
|
|
1022
|
-
});
|
|
1023
|
-
const stopped = await this.manager.stop(request.params);
|
|
1024
|
-
this.send({
|
|
1025
|
-
type: "mcp/response",
|
|
1026
|
-
id: request.id,
|
|
1027
|
-
result: {
|
|
1028
|
-
kind: "control",
|
|
1029
|
-
stopped: stopped + removed
|
|
1030
|
-
}
|
|
1031
|
-
});
|
|
1032
|
-
} catch (error) {
|
|
1033
|
-
const validation = toValidationError(error);
|
|
1034
|
-
this.sendError(request.id, validation.code, validation.message);
|
|
1035
|
-
}
|
|
1036
|
-
}
|
|
1037
|
-
removeQueued(predicate) {
|
|
1038
|
-
let removed = 0;
|
|
1039
|
-
for (let index = this.queue.length - 1; index >= 0; index -= 1) {
|
|
1040
|
-
const record = this.queue[index];
|
|
1041
|
-
if (!predicate(record)) continue;
|
|
1042
|
-
this.queue.splice(index, 1);
|
|
1043
|
-
clearTimeout(record.timer);
|
|
1044
|
-
this.sendError(record.request.id, "cancelled", "MCP work was stopped before startup.");
|
|
1045
|
-
removed += 1;
|
|
1046
|
-
}
|
|
1047
|
-
return removed;
|
|
1048
|
-
}
|
|
1049
|
-
expireQueued(requestId) {
|
|
1050
|
-
const index = this.queue.findIndex((record) => record.request.id === requestId);
|
|
1051
|
-
if (index < 0) return;
|
|
1052
|
-
const [record] = this.queue.splice(index, 1);
|
|
1053
|
-
clearTimeout(record.timer);
|
|
1054
|
-
this.sendError(requestId, "timeout", "MCP work timed out in queue.");
|
|
1055
|
-
}
|
|
1056
|
-
sendError(id, code, message, stderrTail) {
|
|
1057
|
-
const bounded = truncateUtf8(message, MACHINE_MCP_MAX_ERROR_MESSAGE_BYTES);
|
|
1058
|
-
this.send({
|
|
1059
|
-
type: "mcp/error",
|
|
1060
|
-
id,
|
|
1061
|
-
error: {
|
|
1062
|
-
code,
|
|
1063
|
-
message: bounded || code,
|
|
1064
|
-
...stderrTail ? { stderrTail } : {}
|
|
1065
|
-
}
|
|
1066
|
-
});
|
|
1067
|
-
}
|
|
1068
|
-
send(message) {
|
|
1069
|
-
if (this.closed || this.socket.readyState !== WebSocket.OPEN) return;
|
|
1070
|
-
let payload;
|
|
1071
|
-
try {
|
|
1072
|
-
payload = encodeMachineMcpFrame(message);
|
|
1073
|
-
} catch {
|
|
1074
|
-
this.socket.close(1009, "MCP response too large");
|
|
1075
|
-
this.close(true);
|
|
1076
|
-
return;
|
|
1077
|
-
}
|
|
1078
|
-
if (this.socket.bufferedAmount + Buffer.byteLength(payload) > 12582912) {
|
|
1079
|
-
this.socket.close(1009, "MCP outbound buffer exceeded");
|
|
1080
|
-
this.close(true);
|
|
1081
|
-
return;
|
|
1082
|
-
}
|
|
1083
|
-
this.socket.send(payload);
|
|
1084
|
-
}
|
|
1085
|
-
};
|
|
1086
|
-
function rawBuffer(data) {
|
|
1087
|
-
if (Array.isArray(data)) return Buffer.concat(data);
|
|
1088
|
-
if (Buffer.isBuffer(data)) return data;
|
|
1089
|
-
if (data instanceof ArrayBuffer) return Buffer.from(data);
|
|
1090
|
-
return Buffer.from(data);
|
|
1091
|
-
}
|
|
1092
|
-
function requestIdFromFrame(buffer) {
|
|
1093
|
-
try {
|
|
1094
|
-
const value = JSON.parse(buffer.subarray(MACHINE_MCP_TAG.length).toString("utf8"));
|
|
1095
|
-
if (typeof value === "object" && value && "id" in value && typeof value.id === "string") return value.id;
|
|
1096
|
-
} catch {}
|
|
1097
|
-
return "unknown";
|
|
1098
|
-
}
|
|
1099
|
-
function toValidationError(error) {
|
|
1100
|
-
if (error instanceof MachineMcpValidationError) return error;
|
|
1101
|
-
const message = error instanceof Error ? error.message : "MCP request failed.";
|
|
1102
|
-
if (/timeout/i.test(message)) return new MachineMcpValidationError("timeout", message);
|
|
1103
|
-
if (/abort|cancel/i.test(message)) return new MachineMcpValidationError("cancelled", message);
|
|
1104
|
-
return new MachineMcpValidationError("protocol_error", message);
|
|
1105
|
-
}
|
|
1106
|
-
function workProcessKey(record) {
|
|
1107
|
-
const launch = record.request.params.launch;
|
|
1108
|
-
return `${launch.definitionId}\0${launch.configVersion}\0${launch.projectRoot}`;
|
|
1109
|
-
}
|
|
1110
|
-
function truncateUtf8(value, maxBytes) {
|
|
1111
|
-
const bytes = Buffer.from(value);
|
|
1112
|
-
if (bytes.length <= maxBytes) return value;
|
|
1113
|
-
let end = maxBytes;
|
|
1114
|
-
while (end > 0 && (bytes[end] & 192) === 128) end -= 1;
|
|
1115
|
-
return bytes.subarray(0, end).toString("utf8");
|
|
1116
|
-
}
|
|
1117
|
-
//#endregion
|
|
1118
|
-
//#region src/file-reader.ts
|
|
1119
|
-
async function readFileForRpc(input) {
|
|
1120
|
-
const maxBytes = resolveMaxBytes(input.maxBytes);
|
|
1121
|
-
if (maxBytes === void 0) return {
|
|
1122
|
-
ok: false,
|
|
1123
|
-
errorCode: "invalid_request",
|
|
1124
|
-
message: "maxBytes must be a positive safe integer within the file read limit."
|
|
1125
|
-
};
|
|
1126
|
-
let handle;
|
|
1127
|
-
try {
|
|
1128
|
-
handle = await open(input.path, "r");
|
|
1129
|
-
const stat = await handle.stat();
|
|
1130
|
-
if (!stat.isFile()) return {
|
|
1131
|
-
ok: false,
|
|
1132
|
-
errorCode: "not_file",
|
|
1133
|
-
message: "Path is not a regular file."
|
|
1134
|
-
};
|
|
1135
|
-
if (stat.size > maxBytes) return {
|
|
1136
|
-
ok: false,
|
|
1137
|
-
errorCode: "too_large",
|
|
1138
|
-
message: `File exceeds ${maxBytes} bytes.`
|
|
1139
|
-
};
|
|
1140
|
-
const bytes = await readBounded$1(handle, maxBytes);
|
|
1141
|
-
if (bytes.byteLength > maxBytes) return {
|
|
1142
|
-
ok: false,
|
|
1143
|
-
errorCode: "too_large",
|
|
1144
|
-
message: `File exceeds ${maxBytes} bytes.`
|
|
1145
|
-
};
|
|
1146
|
-
return {
|
|
1147
|
-
ok: true,
|
|
1148
|
-
dataBase64: bytes.toString("base64"),
|
|
1149
|
-
sizeBytes: bytes.byteLength
|
|
1150
|
-
};
|
|
1151
|
-
} catch (error) {
|
|
1152
|
-
return fileReadError(error);
|
|
1153
|
-
} finally {
|
|
1154
|
-
await handle?.close();
|
|
1155
|
-
}
|
|
1156
|
-
}
|
|
1157
|
-
async function readBounded$1(handle, maxBytes) {
|
|
1158
|
-
const limitBuffer = Buffer.allocUnsafe(maxBytes + 1);
|
|
1159
|
-
let offset = 0;
|
|
1160
|
-
while (offset < limitBuffer.byteLength) {
|
|
1161
|
-
const { bytesRead } = await handle.read(limitBuffer, offset, limitBuffer.byteLength - offset, offset);
|
|
1162
|
-
if (bytesRead === 0) break;
|
|
1163
|
-
offset += bytesRead;
|
|
1164
|
-
}
|
|
1165
|
-
return limitBuffer.subarray(0, offset);
|
|
1166
|
-
}
|
|
1167
|
-
function resolveMaxBytes(value) {
|
|
1168
|
-
if (value === void 0) return DEFAULT_MACHINE_FILE_READ_BYTES;
|
|
1169
|
-
if (!Number.isSafeInteger(value) || value <= 0 || value > 20971520) return;
|
|
1170
|
-
return value;
|
|
1171
|
-
}
|
|
1172
|
-
function fileReadError(error) {
|
|
1173
|
-
const code = typeof error === "object" && error && "code" in error ? error.code : void 0;
|
|
1174
|
-
if (code === "ENOENT" || code === "ENOTDIR") return {
|
|
1175
|
-
ok: false,
|
|
1176
|
-
errorCode: "not_found",
|
|
1177
|
-
message: "File was not found."
|
|
1178
|
-
};
|
|
1179
|
-
if (code === "EACCES" || code === "EPERM") return {
|
|
1180
|
-
ok: false,
|
|
1181
|
-
errorCode: "permission_denied",
|
|
1182
|
-
message: "Permission denied."
|
|
1183
|
-
};
|
|
1184
|
-
return {
|
|
1185
|
-
ok: false,
|
|
1186
|
-
errorCode: "read_failed",
|
|
1187
|
-
message: error instanceof Error ? error.message : "File read failed."
|
|
1188
|
-
};
|
|
1189
|
-
}
|
|
1190
|
-
//#endregion
|
|
1191
|
-
//#region src/file-writer.ts
|
|
1192
|
-
async function writeFileForRpc(input) {
|
|
1193
|
-
if (Buffer.byteLength(input.content) > 5242880) return {
|
|
1194
|
-
ok: false,
|
|
1195
|
-
errorCode: "too_large",
|
|
1196
|
-
message: "File content exceeds write limit."
|
|
1197
|
-
};
|
|
1198
|
-
try {
|
|
1199
|
-
if ((await existingFileStat(input.path))?.isDirectory()) return {
|
|
1200
|
-
ok: false,
|
|
1201
|
-
errorCode: "not_file",
|
|
1202
|
-
message: "Path is a directory."
|
|
1203
|
-
};
|
|
1204
|
-
await mkdir(dirname(input.path), { recursive: true });
|
|
1205
|
-
await writeFile(input.path, input.content, "utf8");
|
|
1206
|
-
return { ok: true };
|
|
1207
|
-
} catch (error) {
|
|
1208
|
-
return writeError(error);
|
|
1209
|
-
}
|
|
1210
|
-
}
|
|
1211
|
-
async function deleteFileForRpc(input) {
|
|
1212
|
-
try {
|
|
1213
|
-
if ((await stat(input.path)).isDirectory()) return {
|
|
1214
|
-
ok: false,
|
|
1215
|
-
errorCode: "not_file",
|
|
1216
|
-
message: "Path is a directory."
|
|
1217
|
-
};
|
|
1218
|
-
await rm(input.path);
|
|
1219
|
-
return { ok: true };
|
|
1220
|
-
} catch (error) {
|
|
1221
|
-
return deleteError(error);
|
|
1222
|
-
}
|
|
1223
|
-
}
|
|
1224
|
-
async function moveFileForRpc(input) {
|
|
1225
|
-
try {
|
|
1226
|
-
if ((await stat(input.sourcePath)).isDirectory()) return {
|
|
1227
|
-
ok: false,
|
|
1228
|
-
errorCode: "not_file",
|
|
1229
|
-
message: "Path is a directory."
|
|
1230
|
-
};
|
|
1231
|
-
if ((await existingFileStat(input.targetPath))?.isDirectory()) return {
|
|
1232
|
-
ok: false,
|
|
1233
|
-
errorCode: "not_file",
|
|
1234
|
-
message: "Path is a directory."
|
|
1235
|
-
};
|
|
1236
|
-
await mkdir(dirname(input.targetPath), { recursive: true });
|
|
1237
|
-
await rename(input.sourcePath, input.targetPath);
|
|
1238
|
-
return { ok: true };
|
|
1239
|
-
} catch (error) {
|
|
1240
|
-
return moveError(error);
|
|
1241
|
-
}
|
|
1242
|
-
}
|
|
1243
|
-
async function existingFileStat(path) {
|
|
1244
|
-
try {
|
|
1245
|
-
return await stat(path);
|
|
1246
|
-
} catch (error) {
|
|
1247
|
-
const code = typeof error === "object" && error && "code" in error ? error.code : void 0;
|
|
1248
|
-
if (code === "ENOENT" || code === "ENOTDIR") return void 0;
|
|
1249
|
-
throw error;
|
|
1250
|
-
}
|
|
1251
|
-
}
|
|
1252
|
-
function writeError(error) {
|
|
1253
|
-
return mapFsError(error, "write_failed", "File write failed.");
|
|
1254
|
-
}
|
|
1255
|
-
function deleteError(error) {
|
|
1256
|
-
return mapFsError(error, "delete_failed", "File delete failed.");
|
|
1257
|
-
}
|
|
1258
|
-
function moveError(error) {
|
|
1259
|
-
return mapFsError(error, "move_failed", "File move failed.");
|
|
1260
|
-
}
|
|
1261
|
-
function mapFsError(error, fallbackCode, fallbackMessage) {
|
|
1262
|
-
const code = typeof error === "object" && error && "code" in error ? error.code : void 0;
|
|
1263
|
-
if (code === "ENOENT" || code === "ENOTDIR") return {
|
|
1264
|
-
ok: false,
|
|
1265
|
-
errorCode: "not_found",
|
|
1266
|
-
message: "File was not found."
|
|
1267
|
-
};
|
|
1268
|
-
if (code === "EISDIR") return {
|
|
1269
|
-
ok: false,
|
|
1270
|
-
errorCode: "not_file",
|
|
1271
|
-
message: "Path is a directory."
|
|
1272
|
-
};
|
|
1273
|
-
if (code === "EACCES" || code === "EPERM") return {
|
|
1274
|
-
ok: false,
|
|
1275
|
-
errorCode: "permission_denied",
|
|
1276
|
-
message: "Permission denied."
|
|
1277
|
-
};
|
|
1278
|
-
return {
|
|
1279
|
-
ok: false,
|
|
1280
|
-
errorCode: fallbackCode,
|
|
1281
|
-
message: error instanceof Error ? error.message : fallbackMessage
|
|
1282
|
-
};
|
|
1283
|
-
}
|
|
1284
|
-
//#endregion
|
|
1285
|
-
//#region src/machine-file-search-index.ts
|
|
1286
|
-
const INDEX_STALE_MS = 3e3;
|
|
1287
|
-
const SCAN_TIMEOUT_MS = 5e3;
|
|
1288
|
-
const indexes = /* @__PURE__ */ new Map();
|
|
1289
|
-
async function searchFileEntriesForRpc(params, options = {}) {
|
|
1290
|
-
const limit = params.limit ?? 50;
|
|
1291
|
-
const rootPath = path.resolve(params.rootPath);
|
|
1292
|
-
if (!path.isAbsolute(params.rootPath) || limit < 1 || limit > 100) return {
|
|
1293
|
-
ok: false,
|
|
1294
|
-
errorCode: "invalid_request",
|
|
1295
|
-
message: "Invalid file search request."
|
|
1296
|
-
};
|
|
1297
|
-
const rootState = await validateRoot(rootPath);
|
|
1298
|
-
if (!rootState.ok) return rootState;
|
|
1299
|
-
const indexResult = await getIndex(rootPath);
|
|
1300
|
-
if (!indexResult.ok) return indexResult;
|
|
1301
|
-
const refreshResult = await refreshIfStale(rootPath, indexResult.index, options.staleMs);
|
|
1302
|
-
if (!refreshResult.ok) return refreshResult;
|
|
1303
|
-
const searchResult = indexResult.index.finder.mixedSearch(params.query, { pageSize: limit });
|
|
1304
|
-
if (!searchResult.ok) return {
|
|
1305
|
-
ok: false,
|
|
1306
|
-
errorCode: "search_failed",
|
|
1307
|
-
message: searchResult.error
|
|
1308
|
-
};
|
|
1309
|
-
const candidates = searchResult.value.items.map((item) => toEntry(item)).filter((entry) => entry !== void 0);
|
|
1310
|
-
const seen = /* @__PURE__ */ new Set();
|
|
1311
|
-
const entries = candidates.filter((entry) => {
|
|
1312
|
-
const key = `${entry.kind}:${entry.relativePath}`;
|
|
1313
|
-
if (seen.has(key)) return false;
|
|
1314
|
-
seen.add(key);
|
|
1315
|
-
return true;
|
|
1316
|
-
}).slice(0, limit);
|
|
1317
|
-
return {
|
|
1318
|
-
ok: true,
|
|
1319
|
-
entries,
|
|
1320
|
-
truncated: searchResult.value.totalMatched > entries.length
|
|
1321
|
-
};
|
|
1322
|
-
}
|
|
1323
|
-
function disposeMachineFileSearchIndexes() {
|
|
1324
|
-
for (const index of indexes.values()) index.then((result) => {
|
|
1325
|
-
if (result.ok) result.index.finder.destroy();
|
|
1326
|
-
}).catch(() => {});
|
|
1327
|
-
indexes.clear();
|
|
1328
|
-
}
|
|
1329
|
-
async function validateRoot(rootPath) {
|
|
1330
|
-
try {
|
|
1331
|
-
if (!(await stat(rootPath)).isDirectory()) return {
|
|
1332
|
-
ok: false,
|
|
1333
|
-
errorCode: "not_directory",
|
|
1334
|
-
message: "Search root is not a directory."
|
|
1335
|
-
};
|
|
1336
|
-
return { ok: true };
|
|
1337
|
-
} catch (error) {
|
|
1338
|
-
const code = nodeErrorCode(error);
|
|
1339
|
-
if (code === "ENOENT") return {
|
|
1340
|
-
ok: false,
|
|
1341
|
-
errorCode: "not_found",
|
|
1342
|
-
message: "Search root does not exist."
|
|
1343
|
-
};
|
|
1344
|
-
if (code === "EACCES" || code === "EPERM") return {
|
|
1345
|
-
ok: false,
|
|
1346
|
-
errorCode: "permission_denied",
|
|
1347
|
-
message: "Search root is not readable."
|
|
1348
|
-
};
|
|
1349
|
-
return {
|
|
1350
|
-
ok: false,
|
|
1351
|
-
errorCode: "search_failed",
|
|
1352
|
-
message: "Failed to inspect search root."
|
|
1353
|
-
};
|
|
1354
|
-
}
|
|
1355
|
-
}
|
|
1356
|
-
async function getIndex(rootPath) {
|
|
1357
|
-
const existing = indexes.get(rootPath);
|
|
1358
|
-
if (existing) return existing;
|
|
1359
|
-
const pending = createIndex(rootPath);
|
|
1360
|
-
indexes.set(rootPath, pending);
|
|
1361
|
-
const result = await pending;
|
|
1362
|
-
if (!result.ok && indexes.get(rootPath) === pending) indexes.delete(rootPath);
|
|
1363
|
-
return result;
|
|
1364
|
-
}
|
|
1365
|
-
async function createIndex(rootPath) {
|
|
1366
|
-
try {
|
|
1367
|
-
const { FileFinder } = await import("@ff-labs/fff-node");
|
|
1368
|
-
const created = FileFinder.create({
|
|
1369
|
-
basePath: rootPath,
|
|
1370
|
-
disableContentIndexing: true,
|
|
1371
|
-
disableWatch: true
|
|
1372
|
-
});
|
|
1373
|
-
if (!created.ok) return unsupported(created.error);
|
|
1374
|
-
const scan = await created.value.waitForScan(SCAN_TIMEOUT_MS);
|
|
1375
|
-
if (!scan.ok || !scan.value) {
|
|
1376
|
-
created.value.destroy();
|
|
1377
|
-
return {
|
|
1378
|
-
ok: false,
|
|
1379
|
-
errorCode: "search_failed",
|
|
1380
|
-
message: "Timed out scanning search root."
|
|
1381
|
-
};
|
|
1382
|
-
}
|
|
1383
|
-
return {
|
|
1384
|
-
ok: true,
|
|
1385
|
-
index: {
|
|
1386
|
-
finder: created.value,
|
|
1387
|
-
lastScannedAt: Date.now()
|
|
1388
|
-
}
|
|
1389
|
-
};
|
|
1390
|
-
} catch (error) {
|
|
1391
|
-
return unsupported(error instanceof Error ? error.message : "Native file search is unavailable.");
|
|
1392
|
-
}
|
|
1393
|
-
}
|
|
1394
|
-
async function refreshIfStale(rootPath, index, staleMs = INDEX_STALE_MS) {
|
|
1395
|
-
if (Date.now() - index.lastScannedAt < staleMs) return { ok: true };
|
|
1396
|
-
const refresh = index.finder.scanFiles();
|
|
1397
|
-
if (!refresh.ok) return {
|
|
1398
|
-
ok: false,
|
|
1399
|
-
errorCode: "search_failed",
|
|
1400
|
-
message: refresh.error
|
|
1401
|
-
};
|
|
1402
|
-
const scan = await index.finder.waitForScan(SCAN_TIMEOUT_MS);
|
|
1403
|
-
if (!scan.ok || !scan.value) return {
|
|
1404
|
-
ok: false,
|
|
1405
|
-
errorCode: "search_failed",
|
|
1406
|
-
message: `Timed out refreshing ${rootPath}.`
|
|
1407
|
-
};
|
|
1408
|
-
index.lastScannedAt = Date.now();
|
|
1409
|
-
return { ok: true };
|
|
1410
|
-
}
|
|
1411
|
-
function toEntry(item) {
|
|
1412
|
-
if (item.type === "file") return toEntryFromPath(item.item.relativePath, "file", item.item.size);
|
|
1413
|
-
return toEntryFromPath(item.item.relativePath, "directory", null);
|
|
1414
|
-
}
|
|
1415
|
-
function toEntryFromPath(value, kind, sizeBytes) {
|
|
1416
|
-
const relativePath = normalizeRelativePath(value);
|
|
1417
|
-
if (!relativePath) return void 0;
|
|
1418
|
-
if (kind === "file") {
|
|
1419
|
-
if (sizeBytes === null) return void 0;
|
|
1420
|
-
return {
|
|
1421
|
-
relativePath,
|
|
1422
|
-
kind,
|
|
1423
|
-
sizeBytes
|
|
1424
|
-
};
|
|
1425
|
-
}
|
|
1426
|
-
return {
|
|
1427
|
-
relativePath,
|
|
1428
|
-
kind,
|
|
1429
|
-
sizeBytes: null
|
|
1430
|
-
};
|
|
1431
|
-
}
|
|
1432
|
-
function normalizeRelativePath(value) {
|
|
1433
|
-
const normalized = value.replace(/\\/g, "/").replace(/\/+$/, "");
|
|
1434
|
-
if (!normalized || normalized.startsWith("/") || normalized.includes("\0")) return void 0;
|
|
1435
|
-
if (normalized.length > 4096) return void 0;
|
|
1436
|
-
if (normalized.split("/").some((segment) => !segment || segment === "." || segment === ".." || segment === ".git")) return;
|
|
1437
|
-
return normalized;
|
|
1438
|
-
}
|
|
1439
|
-
function unsupported(message) {
|
|
1440
|
-
return {
|
|
1441
|
-
ok: false,
|
|
1442
|
-
errorCode: "unsupported",
|
|
1443
|
-
message: `Native file search is not supported on this host: ${message}`
|
|
1444
|
-
};
|
|
1445
|
-
}
|
|
1446
|
-
function nodeErrorCode(error) {
|
|
1447
|
-
return typeof error === "object" && error !== null && "code" in error ? String(error.code) : "";
|
|
1448
|
-
}
|
|
1449
|
-
//#endregion
|
|
1450
|
-
//#region src/shell-environment.ts
|
|
1451
|
-
const LOGIN_ENV_NAMES = [
|
|
1452
|
-
"PATH",
|
|
1453
|
-
"SSH_AUTH_SOCK",
|
|
1454
|
-
"HOMEBREW_PREFIX",
|
|
1455
|
-
"HOMEBREW_CELLAR",
|
|
1456
|
-
"HOMEBREW_REPOSITORY",
|
|
1457
|
-
"XDG_CONFIG_HOME",
|
|
1458
|
-
"XDG_DATA_HOME"
|
|
1459
|
-
];
|
|
1460
|
-
const WINDOWS_PROFILE_ENV_NAMES = [
|
|
1461
|
-
"PATH",
|
|
1462
|
-
"FNM_DIR",
|
|
1463
|
-
"FNM_MULTISHELL_PATH"
|
|
1464
|
-
];
|
|
1465
|
-
const WINDOWS_SHELLS = ["pwsh.exe", "powershell.exe"];
|
|
1466
|
-
const LOGIN_SHELL_TIMEOUT_MS = 5e3;
|
|
1467
|
-
const LAUNCHCTL_TIMEOUT_MS = 2e3;
|
|
1468
|
-
const FORCE_KILL_GRACE_MS = 1e3;
|
|
1469
|
-
const TERMINAL_ENV_BLOCKLIST = /* @__PURE__ */ new Set([
|
|
1470
|
-
"PORT",
|
|
1471
|
-
"ELECTRON_RENDERER_PORT",
|
|
1472
|
-
"ELECTRON_RUN_AS_NODE"
|
|
1473
|
-
]);
|
|
1474
|
-
async function installShellEnvironment(options = {}) {
|
|
1475
|
-
const env = options.env ?? process.env;
|
|
1476
|
-
const platform = options.platform ?? process.platform;
|
|
1477
|
-
const spawn$1 = options.spawn ?? spawn;
|
|
1478
|
-
const warn = options.warn ?? console.warn;
|
|
1479
|
-
if (platform === "win32") {
|
|
1480
|
-
await installWindowsEnvironment(env, spawn$1, warn);
|
|
1481
|
-
return;
|
|
1482
|
-
}
|
|
1483
|
-
if (platform !== "darwin" && platform !== "linux") return;
|
|
1484
|
-
await installPosixEnvironment(env, platform, readUserShell(options.userInfo ?? userInfo), spawn$1, warn);
|
|
1485
|
-
}
|
|
1486
|
-
function createTerminalSpawnEnv(env = process.env) {
|
|
1487
|
-
const result = {};
|
|
1488
|
-
for (const [key, value] of Object.entries(env)) {
|
|
1489
|
-
if (value === void 0 || shouldExcludeTerminalEnv(key)) continue;
|
|
1490
|
-
result[key] = value;
|
|
1491
|
-
}
|
|
1492
|
-
return result;
|
|
1493
|
-
}
|
|
1494
|
-
async function installPosixEnvironment(env, platform, userShell, spawn, warn) {
|
|
1495
|
-
const shellEnvironment = {};
|
|
1496
|
-
for (const shell of loginShellCandidates(platform, env.SHELL, userShell)) {
|
|
1497
|
-
Object.assign(shellEnvironment, await readPosixEnvironment(shell, LOGIN_ENV_NAMES, spawn, warn));
|
|
1498
|
-
if (shellEnvironment.PATH) break;
|
|
1499
|
-
}
|
|
1500
|
-
const launchctlPath = platform === "darwin" && !shellEnvironment.PATH ? trim(await runProbe("launchctl-path", "/bin/launchctl", ["getenv", "PATH"], LAUNCHCTL_TIMEOUT_MS, spawn, warn)) : void 0;
|
|
1501
|
-
const path = mergePaths(platform, shellEnvironment.PATH ?? launchctlPath, readPath(env));
|
|
1502
|
-
if (path) env.PATH = path;
|
|
1503
|
-
for (const name of LOGIN_ENV_NAMES.slice(1)) if (!env[name] && shellEnvironment[name]) env[name] = shellEnvironment[name];
|
|
1504
|
-
}
|
|
1505
|
-
async function installWindowsEnvironment(env, spawn, warn) {
|
|
1506
|
-
const noProfile = await readWindowsEnvironment(["PATH"], false, spawn, warn);
|
|
1507
|
-
const profile = await readWindowsEnvironment(WINDOWS_PROFILE_ENV_NAMES, true, spawn, warn);
|
|
1508
|
-
const path = mergePaths("win32", profile.PATH, knownWindowsCliDirs(env).join(";"), noProfile.PATH, readPath(env));
|
|
1509
|
-
if (path) env.PATH = path;
|
|
1510
|
-
if (!env.FNM_DIR && profile.FNM_DIR) env.FNM_DIR = profile.FNM_DIR;
|
|
1511
|
-
if (!env.FNM_MULTISHELL_PATH && profile.FNM_MULTISHELL_PATH) env.FNM_MULTISHELL_PATH = profile.FNM_MULTISHELL_PATH;
|
|
1512
|
-
}
|
|
1513
|
-
async function readPosixEnvironment(shell, names, spawn, warn) {
|
|
1514
|
-
return extractEnvironment(await runProbe("login-shell", shell, ["-ilc", capturePosixEnvironment(names)], LOGIN_SHELL_TIMEOUT_MS, spawn, warn), names);
|
|
1515
|
-
}
|
|
1516
|
-
async function readWindowsEnvironment(names, loadProfile, spawn, warn) {
|
|
1517
|
-
const args = [
|
|
1518
|
-
"-NoLogo",
|
|
1519
|
-
...loadProfile ? [] : ["-NoProfile"],
|
|
1520
|
-
"-NonInteractive",
|
|
1521
|
-
"-Command",
|
|
1522
|
-
captureWindowsEnvironment(names)
|
|
1523
|
-
];
|
|
1524
|
-
const probe = loadProfile ? "powershell-profile" : "powershell-no-profile";
|
|
1525
|
-
for (const shell of WINDOWS_SHELLS) {
|
|
1526
|
-
const environment = extractEnvironment(await runProbe(probe, shell, args, LOGIN_SHELL_TIMEOUT_MS, spawn, warn), names);
|
|
1527
|
-
if (Object.keys(environment).length > 0) return environment;
|
|
1528
|
-
}
|
|
1529
|
-
return {};
|
|
1530
|
-
}
|
|
1531
|
-
function runProbe(probe, file, args, timeoutMs, spawn, warn) {
|
|
1532
|
-
return new Promise((resolve) => {
|
|
1533
|
-
let output = "";
|
|
1534
|
-
let timedOut = false;
|
|
1535
|
-
let settled = false;
|
|
1536
|
-
let timeout;
|
|
1537
|
-
let forceKill;
|
|
1538
|
-
const finish = (output, error) => {
|
|
1539
|
-
if (settled) return;
|
|
1540
|
-
settled = true;
|
|
1541
|
-
if (timeout) clearTimeout(timeout);
|
|
1542
|
-
if (forceKill) clearTimeout(forceKill);
|
|
1543
|
-
if (timedOut) {
|
|
1544
|
-
warn(`Shell environment ${probe} probe (${executableName(file)}, ${args.length} args) timed out after ${timeoutMs}ms.`);
|
|
1545
|
-
resolve("");
|
|
1546
|
-
return;
|
|
1547
|
-
}
|
|
1548
|
-
if (error) {
|
|
1549
|
-
const code = errorCode(error);
|
|
1550
|
-
warn(`Shell environment ${probe} probe (${executableName(file)}, ${args.length} args) failed${code ? `: ${code}` : "."}`);
|
|
1551
|
-
resolve("");
|
|
1552
|
-
return;
|
|
1553
|
-
}
|
|
1554
|
-
resolve(output);
|
|
1555
|
-
};
|
|
1556
|
-
try {
|
|
1557
|
-
const child = spawn(file, [...args], { stdio: [
|
|
1558
|
-
"ignore",
|
|
1559
|
-
"pipe",
|
|
1560
|
-
"pipe"
|
|
1561
|
-
] });
|
|
1562
|
-
child.stdout.setEncoding("utf8");
|
|
1563
|
-
child.stdout.on("data", (chunk) => {
|
|
1564
|
-
output += chunk;
|
|
1565
|
-
});
|
|
1566
|
-
child.stderr.resume();
|
|
1567
|
-
child.once("error", (error) => finish("", error));
|
|
1568
|
-
child.once("close", (code) => {
|
|
1569
|
-
finish(code === 0 ? output : "", code === 0 ? void 0 : { code: `exit ${code}` });
|
|
1570
|
-
});
|
|
1571
|
-
timeout = setTimeout(() => {
|
|
1572
|
-
timedOut = true;
|
|
1573
|
-
child.kill("SIGTERM");
|
|
1574
|
-
forceKill = setTimeout(() => child.kill("SIGKILL"), FORCE_KILL_GRACE_MS);
|
|
1575
|
-
}, timeoutMs);
|
|
1576
|
-
} catch (error) {
|
|
1577
|
-
finish("", error);
|
|
1578
|
-
}
|
|
1579
|
-
});
|
|
1580
|
-
}
|
|
1581
|
-
function loginShellCandidates(platform, shell, userShell) {
|
|
1582
|
-
const fallback = platform === "darwin" ? "/bin/zsh" : "/bin/bash";
|
|
1583
|
-
return [...new Set([
|
|
1584
|
-
trim(shell),
|
|
1585
|
-
trim(userShell),
|
|
1586
|
-
fallback
|
|
1587
|
-
].filter(isString))];
|
|
1588
|
-
}
|
|
1589
|
-
function capturePosixEnvironment(names) {
|
|
1590
|
-
return names.map((name) => [
|
|
1591
|
-
`printf '%s\\n' '${startMarker(name)}'`,
|
|
1592
|
-
`printenv ${name} || true`,
|
|
1593
|
-
`printf '%s\\n' '${endMarker(name)}'`
|
|
1594
|
-
].join("; ")).join("; ");
|
|
1595
|
-
}
|
|
1596
|
-
function captureWindowsEnvironment(names) {
|
|
1597
|
-
return ["$ErrorActionPreference = 'Stop'", ...names.flatMap((name) => [
|
|
1598
|
-
`Write-Output '${startMarker(name)}'`,
|
|
1599
|
-
`$value = [Environment]::GetEnvironmentVariable('${name}')`,
|
|
1600
|
-
"if ($null -ne $value -and $value.Length -gt 0) { Write-Output $value }",
|
|
1601
|
-
`Write-Output '${endMarker(name)}'`
|
|
1602
|
-
])].join("; ");
|
|
1603
|
-
}
|
|
1604
|
-
function extractEnvironment(output, names) {
|
|
1605
|
-
const environment = {};
|
|
1606
|
-
for (const name of names) {
|
|
1607
|
-
const start = output.indexOf(startMarker(name));
|
|
1608
|
-
if (start === -1) continue;
|
|
1609
|
-
const valueStart = start + startMarker(name).length;
|
|
1610
|
-
const end = output.indexOf(endMarker(name), valueStart);
|
|
1611
|
-
if (end === -1) continue;
|
|
1612
|
-
const value = output.slice(valueStart, end).replace(/^\r?\n/, "").replace(/\r?\n$/, "");
|
|
1613
|
-
if (value) environment[name] = value;
|
|
1614
|
-
}
|
|
1615
|
-
return environment;
|
|
1616
|
-
}
|
|
1617
|
-
function mergePaths(platform, ...values) {
|
|
1618
|
-
const delimiter = platform === "win32" ? ";" : ":";
|
|
1619
|
-
const entries = [];
|
|
1620
|
-
const seen = /* @__PURE__ */ new Set();
|
|
1621
|
-
for (const value of values) for (const entry of value?.split(delimiter) ?? []) {
|
|
1622
|
-
const trimmed = entry.trim();
|
|
1623
|
-
const key = trimmed.replace(/^"+|"+$/g, "");
|
|
1624
|
-
const comparisonKey = platform === "win32" ? key.toLowerCase() : key;
|
|
1625
|
-
if (!comparisonKey || seen.has(comparisonKey)) continue;
|
|
1626
|
-
seen.add(comparisonKey);
|
|
1627
|
-
entries.push(trimmed);
|
|
1628
|
-
}
|
|
1629
|
-
return entries.length > 0 ? entries.join(delimiter) : void 0;
|
|
1630
|
-
}
|
|
1631
|
-
function knownWindowsCliDirs(env) {
|
|
1632
|
-
const appData = trim(env.APPDATA);
|
|
1633
|
-
const localAppData = trim(env.LOCALAPPDATA);
|
|
1634
|
-
const userProfile = trim(env.USERPROFILE);
|
|
1635
|
-
return [
|
|
1636
|
-
...appData ? [`${appData}\\npm`] : [],
|
|
1637
|
-
...localAppData ? [
|
|
1638
|
-
`${localAppData}\\Programs\\nodejs`,
|
|
1639
|
-
`${localAppData}\\Volta\\bin`,
|
|
1640
|
-
`${localAppData}\\pnpm`
|
|
1641
|
-
] : [],
|
|
1642
|
-
...userProfile ? [`${userProfile}\\.bun\\bin`, `${userProfile}\\scoop\\shims`] : []
|
|
1643
|
-
];
|
|
1644
|
-
}
|
|
1645
|
-
function shouldExcludeTerminalEnv(key) {
|
|
1646
|
-
const normalized = key.toUpperCase();
|
|
1647
|
-
return normalized.startsWith("AGENTS_") || normalized.startsWith("VITE_") || TERMINAL_ENV_BLOCKLIST.has(normalized);
|
|
1648
|
-
}
|
|
1649
|
-
function readUserShell(userInfo) {
|
|
1650
|
-
try {
|
|
1651
|
-
return trim(userInfo().shell);
|
|
1652
|
-
} catch {
|
|
1653
|
-
return;
|
|
1654
|
-
}
|
|
1655
|
-
}
|
|
1656
|
-
function readPath(env) {
|
|
1657
|
-
return env.PATH ?? env.Path ?? env.path;
|
|
1658
|
-
}
|
|
1659
|
-
function startMarker(name) {
|
|
1660
|
-
return `__AGENTS_ENV_${name}_START__`;
|
|
1661
|
-
}
|
|
1662
|
-
function endMarker(name) {
|
|
1663
|
-
return `__AGENTS_ENV_${name}_END__`;
|
|
1664
|
-
}
|
|
1665
|
-
function executableName(file) {
|
|
1666
|
-
return file.split(/[\\/]/).at(-1) ?? file;
|
|
1667
|
-
}
|
|
1668
|
-
function errorCode(error) {
|
|
1669
|
-
if (!error || typeof error !== "object" || !("code" in error)) return void 0;
|
|
1670
|
-
const code = error.code;
|
|
1671
|
-
return typeof code === "string" || typeof code === "number" ? String(code) : void 0;
|
|
1672
|
-
}
|
|
1673
|
-
function trim(value) {
|
|
1674
|
-
return value?.trim() || void 0;
|
|
1675
|
-
}
|
|
1676
|
-
function isString(value) {
|
|
1677
|
-
return value !== void 0;
|
|
1678
|
-
}
|
|
1679
|
-
//#endregion
|
|
1680
|
-
//#region src/terminal-history.ts
|
|
1681
|
-
const MAX_PENDING_TERMINAL_CONTROL_SEQUENCE_BYTES = 4 * 1024;
|
|
1682
|
-
function createTerminalHistorySanitizerState() {
|
|
1683
|
-
return {
|
|
1684
|
-
discardSawEscape: false,
|
|
1685
|
-
pending: ""
|
|
1686
|
-
};
|
|
1687
|
-
}
|
|
1688
|
-
function sanitizeTerminalHistoryChunk(state, data) {
|
|
1689
|
-
const visible = [];
|
|
1690
|
-
let input = data;
|
|
1691
|
-
let index = 0;
|
|
1692
|
-
let nextState = state;
|
|
1693
|
-
if (state.discardMode) {
|
|
1694
|
-
const discarded = discardUntilTerminator(state, data);
|
|
1695
|
-
nextState = discarded.state;
|
|
1696
|
-
if (!discarded.done) return {
|
|
1697
|
-
output: "",
|
|
1698
|
-
state: nextState
|
|
1699
|
-
};
|
|
1700
|
-
input = data.slice(discarded.consumed);
|
|
1701
|
-
} else if (state.pending) {
|
|
1702
|
-
input = `${state.pending}${data}`;
|
|
1703
|
-
nextState = createTerminalHistorySanitizerState();
|
|
1704
|
-
}
|
|
1705
|
-
while (index < input.length) {
|
|
1706
|
-
const code = input.charCodeAt(index);
|
|
1707
|
-
if (code === 27) {
|
|
1708
|
-
const next = input.charCodeAt(index + 1);
|
|
1709
|
-
if (Number.isNaN(next)) return pendingResult(visible, input.slice(index), "escape");
|
|
1710
|
-
if (next === 91) {
|
|
1711
|
-
const end = findCsiEnd(input, index + 2);
|
|
1712
|
-
if (end === null) return pendingResult(visible, input.slice(index), "csi");
|
|
1713
|
-
const sequence = input.slice(index, end);
|
|
1714
|
-
const final = sequence.at(-1) ?? "";
|
|
1715
|
-
if (!shouldStripCsiSequence(sequence.slice(2, -1), final)) visible.push(sequence);
|
|
1716
|
-
index = end;
|
|
1717
|
-
continue;
|
|
1718
|
-
}
|
|
1719
|
-
if (isStringIntroducer(next)) {
|
|
1720
|
-
const mode = next === 93 ? "osc" : "st-string";
|
|
1721
|
-
const end = findStringEnd(input, index + 2, mode === "osc");
|
|
1722
|
-
if (end === null) return pendingResult(visible, input.slice(index), mode);
|
|
1723
|
-
const sequence = input.slice(index, end);
|
|
1724
|
-
const content = stripStringTerminator(sequence.slice(2));
|
|
1725
|
-
if (next !== 93 || !shouldStripOscSequence(content)) visible.push(sequence);
|
|
1726
|
-
index = end;
|
|
1727
|
-
continue;
|
|
1728
|
-
}
|
|
1729
|
-
const end = findEscapeEnd(input, index + 1);
|
|
1730
|
-
if (end === null) return pendingResult(visible, input.slice(index), "escape");
|
|
1731
|
-
const sequence = input.slice(index, end);
|
|
1732
|
-
visible.push(sequence);
|
|
1733
|
-
index = end;
|
|
1734
|
-
continue;
|
|
1735
|
-
}
|
|
1736
|
-
if (code === 155) {
|
|
1737
|
-
const end = findCsiEnd(input, index + 1);
|
|
1738
|
-
if (end === null) return pendingResult(visible, input.slice(index), "csi");
|
|
1739
|
-
const sequence = input.slice(index, end);
|
|
1740
|
-
const final = sequence.at(-1) ?? "";
|
|
1741
|
-
if (!shouldStripCsiSequence(sequence.slice(1, -1), final)) visible.push(sequence);
|
|
1742
|
-
index = end;
|
|
1743
|
-
continue;
|
|
1744
|
-
}
|
|
1745
|
-
if (isC1StringIntroducer(code)) {
|
|
1746
|
-
const mode = code === 157 ? "osc" : "st-string";
|
|
1747
|
-
const end = findStringEnd(input, index + 1, mode === "osc");
|
|
1748
|
-
if (end === null) return pendingResult(visible, input.slice(index), mode);
|
|
1749
|
-
const sequence = input.slice(index, end);
|
|
1750
|
-
const content = stripStringTerminator(sequence.slice(1));
|
|
1751
|
-
if (code !== 157 || !shouldStripOscSequence(content)) visible.push(sequence);
|
|
1752
|
-
index = end;
|
|
1753
|
-
continue;
|
|
1754
|
-
}
|
|
1755
|
-
visible.push(input[index] ?? "");
|
|
1756
|
-
index += 1;
|
|
1757
|
-
}
|
|
1758
|
-
return {
|
|
1759
|
-
output: visible.join(""),
|
|
1760
|
-
state: nextState
|
|
1761
|
-
};
|
|
1762
|
-
}
|
|
1763
|
-
function appendBoundedTerminalHistory(current, data, maxBytes) {
|
|
1764
|
-
const combined = Buffer.from(`${current}${data}`, "utf8");
|
|
1765
|
-
if (combined.length <= maxBytes) return combined.toString("utf8");
|
|
1766
|
-
let start = combined.length - maxBytes;
|
|
1767
|
-
while (start < combined.length && (combined[start] & 192) === 128) start += 1;
|
|
1768
|
-
return combined.subarray(start).toString("utf8");
|
|
1769
|
-
}
|
|
1770
|
-
function pendingResult(visible, pending, mode) {
|
|
1771
|
-
if (bounded(pending)) return {
|
|
1772
|
-
output: visible.join(""),
|
|
1773
|
-
state: {
|
|
1774
|
-
discardSawEscape: false,
|
|
1775
|
-
pending
|
|
1776
|
-
}
|
|
1777
|
-
};
|
|
1778
|
-
return {
|
|
1779
|
-
output: visible.join(""),
|
|
1780
|
-
state: {
|
|
1781
|
-
discardMode: mode,
|
|
1782
|
-
discardSawEscape: (mode === "osc" || mode === "st-string") && pending.endsWith("\x1B"),
|
|
1783
|
-
pending: ""
|
|
1784
|
-
}
|
|
1785
|
-
};
|
|
1786
|
-
}
|
|
1787
|
-
function discardUntilTerminator(state, data) {
|
|
1788
|
-
let sawEscape = state.discardSawEscape;
|
|
1789
|
-
for (let index = 0; index < data.length; index += 1) {
|
|
1790
|
-
const code = data.charCodeAt(index);
|
|
1791
|
-
if (state.discardMode === "osc" || state.discardMode === "st-string" ? state.discardMode === "osc" && code === 7 || code === 156 || sawEscape && code === 92 : state.discardMode === "csi" ? isCsiFinalByte(code) : isEscapeFinalByte(code)) return {
|
|
1792
|
-
consumed: index + 1,
|
|
1793
|
-
done: true,
|
|
1794
|
-
state: createTerminalHistorySanitizerState()
|
|
1795
|
-
};
|
|
1796
|
-
sawEscape = (state.discardMode === "osc" || state.discardMode === "st-string") && code === 27;
|
|
1797
|
-
}
|
|
1798
|
-
return {
|
|
1799
|
-
consumed: data.length,
|
|
1800
|
-
done: false,
|
|
1801
|
-
state: {
|
|
1802
|
-
discardMode: state.discardMode,
|
|
1803
|
-
discardSawEscape: sawEscape,
|
|
1804
|
-
pending: ""
|
|
1805
|
-
}
|
|
1806
|
-
};
|
|
1807
|
-
}
|
|
1808
|
-
function bounded(value) {
|
|
1809
|
-
return Buffer.byteLength(value) <= MAX_PENDING_TERMINAL_CONTROL_SEQUENCE_BYTES;
|
|
1810
|
-
}
|
|
1811
|
-
function findCsiEnd(input, start) {
|
|
1812
|
-
for (let index = start; index < input.length; index += 1) if (isCsiFinalByte(input.charCodeAt(index))) return index + 1;
|
|
1813
|
-
return null;
|
|
1814
|
-
}
|
|
1815
|
-
function findStringEnd(input, start, allowBel) {
|
|
1816
|
-
for (let index = start; index < input.length; index += 1) {
|
|
1817
|
-
const code = input.charCodeAt(index);
|
|
1818
|
-
if (allowBel && code === 7 || code === 156) return index + 1;
|
|
1819
|
-
if (code === 27 && input.charCodeAt(index + 1) === 92) return index + 2;
|
|
1820
|
-
}
|
|
1821
|
-
return null;
|
|
1822
|
-
}
|
|
1823
|
-
function findEscapeEnd(input, start) {
|
|
1824
|
-
let index = start;
|
|
1825
|
-
while (index < input.length && isEscapeIntermediateByte(input.charCodeAt(index))) index += 1;
|
|
1826
|
-
if (index >= input.length) return null;
|
|
1827
|
-
return isEscapeFinalByte(input.charCodeAt(index)) ? index + 1 : start;
|
|
1828
|
-
}
|
|
1829
|
-
function shouldStripCsiSequence(body, final) {
|
|
1830
|
-
if (final === "n") return true;
|
|
1831
|
-
if (final === "R" && /^[0-9;?]*$/.test(body)) return true;
|
|
1832
|
-
return final === "c" && /^[>0-9;?]*$/.test(body);
|
|
1833
|
-
}
|
|
1834
|
-
function shouldStripOscSequence(content) {
|
|
1835
|
-
return /^(10|11|12);(?:\?|rgb:)/.test(content);
|
|
1836
|
-
}
|
|
1837
|
-
function stripStringTerminator(value) {
|
|
1838
|
-
if (value.endsWith("\x1B\\")) return value.slice(0, -2);
|
|
1839
|
-
return value.endsWith("\x07") || value.endsWith("") ? value.slice(0, -1) : value;
|
|
1840
|
-
}
|
|
1841
|
-
function isCsiFinalByte(code) {
|
|
1842
|
-
return code >= 64 && code <= 126;
|
|
1843
|
-
}
|
|
1844
|
-
function isEscapeIntermediateByte(code) {
|
|
1845
|
-
return code >= 32 && code <= 47;
|
|
1846
|
-
}
|
|
1847
|
-
function isEscapeFinalByte(code) {
|
|
1848
|
-
return code >= 48 && code <= 126;
|
|
1849
|
-
}
|
|
1850
|
-
function isStringIntroducer(code) {
|
|
1851
|
-
return code === 93 || code === 80 || code === 94 || code === 95;
|
|
1852
|
-
}
|
|
1853
|
-
function isC1StringIntroducer(code) {
|
|
1854
|
-
return code === 157 || code === 144 || code === 158 || code === 159;
|
|
1855
|
-
}
|
|
1856
|
-
var TerminalManager = class TerminalManager {
|
|
1857
|
-
terminals = /* @__PURE__ */ new Map();
|
|
1858
|
-
pendingOpens = /* @__PURE__ */ new Map();
|
|
1859
|
-
shell;
|
|
1860
|
-
spawnPty;
|
|
1861
|
-
onEvent;
|
|
1862
|
-
generation = 0;
|
|
1863
|
-
constructor(options = {}) {
|
|
1864
|
-
this.shell = options.shell ?? detectShell();
|
|
1865
|
-
this.spawnPty = options.spawnPty;
|
|
1866
|
-
this.onEvent = options.onEvent ?? (() => {});
|
|
1867
|
-
}
|
|
1868
|
-
static async create() {
|
|
1869
|
-
try {
|
|
1870
|
-
await ensureInstalledNodePtySpawnHelpersExecutable();
|
|
1871
|
-
const { spawn } = await import("node-pty");
|
|
1872
|
-
return new TerminalManager({ spawnPty: spawn });
|
|
1873
|
-
} catch {
|
|
1874
|
-
return new TerminalManager();
|
|
1875
|
-
}
|
|
1876
|
-
}
|
|
1877
|
-
get supported() {
|
|
1878
|
-
return this.spawnPty !== void 0;
|
|
1879
|
-
}
|
|
1880
|
-
setEventHandler(onEvent) {
|
|
1881
|
-
this.onEvent = onEvent;
|
|
1882
|
-
}
|
|
1883
|
-
open(params) {
|
|
1884
|
-
const existing = this.terminals.get(params.terminalId);
|
|
1885
|
-
if (existing) return Promise.resolve({
|
|
1886
|
-
ok: true,
|
|
1887
|
-
snapshot: snapshot$1(existing)
|
|
1888
|
-
});
|
|
1889
|
-
const pending = this.pendingOpens.get(params.terminalId);
|
|
1890
|
-
if (pending) return pending;
|
|
1891
|
-
if (!this.spawnPty) return Promise.resolve({
|
|
1892
|
-
ok: false,
|
|
1893
|
-
errorCode: "pty_start_failed",
|
|
1894
|
-
message: "Interactive terminals are unavailable on this Machine."
|
|
1895
|
-
});
|
|
1896
|
-
if (this.terminals.size + this.pendingOpens.size >= 8) return Promise.resolve({
|
|
1897
|
-
ok: false,
|
|
1898
|
-
errorCode: "terminal_limit_exceeded",
|
|
1899
|
-
message: `A Machine can run at most 8 terminals.`
|
|
1900
|
-
});
|
|
1901
|
-
const generation = this.generation;
|
|
1902
|
-
const tracked = this.openNew(params, generation, this.spawnPty).finally(() => {
|
|
1903
|
-
if (this.pendingOpens.get(params.terminalId) === tracked) this.pendingOpens.delete(params.terminalId);
|
|
1904
|
-
});
|
|
1905
|
-
this.pendingOpens.set(params.terminalId, tracked);
|
|
1906
|
-
return tracked;
|
|
1907
|
-
}
|
|
1908
|
-
async openNew(params, generation, spawnPty) {
|
|
1909
|
-
const cwd = await resolveTerminalCwd(params.cwd);
|
|
1910
|
-
if (!cwd.ok) return cwd;
|
|
1911
|
-
if (generation !== this.generation) return {
|
|
1912
|
-
ok: false,
|
|
1913
|
-
errorCode: "pty_start_failed",
|
|
1914
|
-
message: "Terminal open was cancelled because the connector disconnected."
|
|
1915
|
-
};
|
|
1916
|
-
try {
|
|
1917
|
-
const pty = spawnPty(this.shell.path, this.shell.args, {
|
|
1918
|
-
name: "xterm-256color",
|
|
1919
|
-
cols: params.cols ?? 80,
|
|
1920
|
-
rows: params.rows ?? 24,
|
|
1921
|
-
cwd: cwd.path,
|
|
1922
|
-
env: createTerminalSpawnEnv(process.env)
|
|
1923
|
-
});
|
|
1924
|
-
const session = {
|
|
1925
|
-
terminalId: params.terminalId,
|
|
1926
|
-
pty,
|
|
1927
|
-
cwd: cwd.path,
|
|
1928
|
-
status: "running",
|
|
1929
|
-
history: "",
|
|
1930
|
-
historySanitizer: createTerminalHistorySanitizerState(),
|
|
1931
|
-
exitCode: null,
|
|
1932
|
-
exitSignal: null,
|
|
1933
|
-
sequence: 0,
|
|
1934
|
-
updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1935
|
-
dispose: () => {}
|
|
1936
|
-
};
|
|
1937
|
-
const dataDisposable = pty.onData((data) => this.handleOutput(session, data));
|
|
1938
|
-
const exitDisposable = pty.onExit(({ exitCode, signal }) => this.handleExit(session, exitCode, signal ?? null));
|
|
1939
|
-
session.dispose = () => {
|
|
1940
|
-
dataDisposable.dispose();
|
|
1941
|
-
exitDisposable.dispose();
|
|
1942
|
-
};
|
|
1943
|
-
this.terminals.set(params.terminalId, session);
|
|
1944
|
-
return {
|
|
1945
|
-
ok: true,
|
|
1946
|
-
snapshot: snapshot$1(session)
|
|
1947
|
-
};
|
|
1948
|
-
} catch (error) {
|
|
1949
|
-
return {
|
|
1950
|
-
ok: false,
|
|
1951
|
-
errorCode: "pty_start_failed",
|
|
1952
|
-
message: errorMessage(error, "Failed to start terminal.")
|
|
1953
|
-
};
|
|
1954
|
-
}
|
|
1955
|
-
}
|
|
1956
|
-
write(terminalId, data) {
|
|
1957
|
-
const session = this.terminals.get(terminalId);
|
|
1958
|
-
if (!session) return missingTerminal();
|
|
1959
|
-
if (session.status !== "running") return closedTerminal();
|
|
1960
|
-
try {
|
|
1961
|
-
session.pty.write(data);
|
|
1962
|
-
return mutationSucceeded(session);
|
|
1963
|
-
} catch (error) {
|
|
1964
|
-
return mutationFailed("write_failed", error, "Failed to write terminal input.");
|
|
1965
|
-
}
|
|
1966
|
-
}
|
|
1967
|
-
resize(terminalId, cols, rows) {
|
|
1968
|
-
const session = this.terminals.get(terminalId);
|
|
1969
|
-
if (!session) return missingTerminal();
|
|
1970
|
-
if (session.status !== "running") return closedTerminal();
|
|
1971
|
-
try {
|
|
1972
|
-
session.pty.resize(cols, rows);
|
|
1973
|
-
return mutationSucceeded(session);
|
|
1974
|
-
} catch (error) {
|
|
1975
|
-
return mutationFailed("resize_failed", error, "Failed to resize terminal.");
|
|
1976
|
-
}
|
|
1977
|
-
}
|
|
1978
|
-
clear(terminalId) {
|
|
1979
|
-
const session = this.terminals.get(terminalId);
|
|
1980
|
-
if (!session) return missingTerminal();
|
|
1981
|
-
if (session.status !== "running") return closedTerminal();
|
|
1982
|
-
session.history = "";
|
|
1983
|
-
session.historySanitizer = createTerminalHistorySanitizerState();
|
|
1984
|
-
const response = mutationSucceeded(session);
|
|
1985
|
-
this.emit({
|
|
1986
|
-
type: "terminal/event",
|
|
1987
|
-
machineTerminalEvent: "cleared",
|
|
1988
|
-
terminalId,
|
|
1989
|
-
sequence: session.sequence
|
|
1990
|
-
});
|
|
1991
|
-
return response;
|
|
1992
|
-
}
|
|
1993
|
-
close(terminalId) {
|
|
1994
|
-
const session = this.terminals.get(terminalId);
|
|
1995
|
-
if (!session) return missingTerminal();
|
|
1996
|
-
const previousStatus = session.status;
|
|
1997
|
-
session.status = "closed";
|
|
1998
|
-
try {
|
|
1999
|
-
session.pty.kill();
|
|
2000
|
-
session.dispose();
|
|
2001
|
-
session.historySanitizer = createTerminalHistorySanitizerState();
|
|
2002
|
-
const response = mutationSucceeded(session);
|
|
2003
|
-
this.emit({
|
|
2004
|
-
type: "terminal/event",
|
|
2005
|
-
machineTerminalEvent: "closed",
|
|
2006
|
-
terminalId,
|
|
2007
|
-
sequence: session.sequence
|
|
2008
|
-
});
|
|
2009
|
-
this.terminals.delete(terminalId);
|
|
2010
|
-
return response;
|
|
2011
|
-
} catch (error) {
|
|
2012
|
-
session.status = previousStatus;
|
|
2013
|
-
return mutationFailed("close_failed", error, "Failed to close terminal.");
|
|
2014
|
-
}
|
|
2015
|
-
}
|
|
2016
|
-
closeAll() {
|
|
2017
|
-
this.generation += 1;
|
|
2018
|
-
this.pendingOpens.clear();
|
|
2019
|
-
for (const terminalId of [...this.terminals.keys()]) this.close(terminalId);
|
|
2020
|
-
}
|
|
2021
|
-
handleOutput(session, data) {
|
|
2022
|
-
if (session.status !== "running") return;
|
|
2023
|
-
const sanitized = sanitizeTerminalHistoryChunk(session.historySanitizer, data);
|
|
2024
|
-
session.historySanitizer = sanitized.state;
|
|
2025
|
-
session.history = appendBoundedTerminalHistory(session.history, sanitized.output, MAX_MACHINE_TERMINAL_HISTORY_BYTES);
|
|
2026
|
-
touch(session);
|
|
2027
|
-
this.emit({
|
|
2028
|
-
type: "terminal/event",
|
|
2029
|
-
machineTerminalEvent: "output",
|
|
2030
|
-
terminalId: session.terminalId,
|
|
2031
|
-
sequence: session.sequence,
|
|
2032
|
-
data
|
|
2033
|
-
});
|
|
2034
|
-
}
|
|
2035
|
-
handleExit(session, exitCode, exitSignal) {
|
|
2036
|
-
if (session.status === "closed") return;
|
|
2037
|
-
session.status = "exited";
|
|
2038
|
-
session.exitCode = exitCode;
|
|
2039
|
-
session.exitSignal = exitSignal;
|
|
2040
|
-
session.historySanitizer = createTerminalHistorySanitizerState();
|
|
2041
|
-
touch(session);
|
|
2042
|
-
this.emit({
|
|
2043
|
-
type: "terminal/event",
|
|
2044
|
-
machineTerminalEvent: "exited",
|
|
2045
|
-
terminalId: session.terminalId,
|
|
2046
|
-
sequence: session.sequence,
|
|
2047
|
-
exitCode,
|
|
2048
|
-
exitSignal
|
|
2049
|
-
});
|
|
2050
|
-
}
|
|
2051
|
-
emit(event) {
|
|
2052
|
-
try {
|
|
2053
|
-
this.onEvent(event);
|
|
2054
|
-
} catch {
|
|
2055
|
-
return;
|
|
2056
|
-
}
|
|
2057
|
-
}
|
|
2058
|
-
};
|
|
2059
|
-
async function ensureInstalledNodePtySpawnHelpersExecutable() {
|
|
2060
|
-
if (process.platform === "win32") return;
|
|
2061
|
-
const packageRoot = dirname(createRequire(import.meta.url).resolve("node-pty/package.json"));
|
|
2062
|
-
await Promise.all([join(packageRoot, "build", "Release", "spawn-helper"), join(packageRoot, "prebuilds", `${process.platform}-${process.arch}`, "spawn-helper")].map(ensureNodePtySpawnHelperExecutable));
|
|
2063
|
-
}
|
|
2064
|
-
async function ensureNodePtySpawnHelperExecutable(path) {
|
|
2065
|
-
let details;
|
|
2066
|
-
try {
|
|
2067
|
-
details = await stat(path);
|
|
2068
|
-
} catch (error) {
|
|
2069
|
-
if (isNodeError(error) && error.code === "ENOENT") return;
|
|
2070
|
-
throw error;
|
|
2071
|
-
}
|
|
2072
|
-
if (!details.isFile() || (details.mode & 73) === 73) return;
|
|
2073
|
-
await chmod(path, details.mode | 73);
|
|
2074
|
-
}
|
|
2075
|
-
function isNodeError(error) {
|
|
2076
|
-
return error instanceof Error && "code" in error;
|
|
2077
|
-
}
|
|
2078
|
-
function snapshot$1(session) {
|
|
2079
|
-
return {
|
|
2080
|
-
terminalId: session.terminalId,
|
|
2081
|
-
cwd: session.cwd,
|
|
2082
|
-
status: session.status,
|
|
2083
|
-
pid: session.pty.pid,
|
|
2084
|
-
history: session.history,
|
|
2085
|
-
exitCode: session.exitCode,
|
|
2086
|
-
exitSignal: session.exitSignal,
|
|
2087
|
-
sequence: session.sequence,
|
|
2088
|
-
updatedAt: session.updatedAt
|
|
2089
|
-
};
|
|
2090
|
-
}
|
|
2091
|
-
function mutationSucceeded(session) {
|
|
2092
|
-
touch(session);
|
|
2093
|
-
return {
|
|
2094
|
-
ok: true,
|
|
2095
|
-
terminalId: session.terminalId,
|
|
2096
|
-
sequence: session.sequence
|
|
2097
|
-
};
|
|
2098
|
-
}
|
|
2099
|
-
function touch(session) {
|
|
2100
|
-
session.sequence += 1;
|
|
2101
|
-
session.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
2102
|
-
}
|
|
2103
|
-
function missingTerminal() {
|
|
2104
|
-
return {
|
|
2105
|
-
ok: false,
|
|
2106
|
-
errorCode: "not_found",
|
|
2107
|
-
message: "Terminal was not found."
|
|
2108
|
-
};
|
|
2109
|
-
}
|
|
2110
|
-
function closedTerminal() {
|
|
2111
|
-
return {
|
|
2112
|
-
ok: false,
|
|
2113
|
-
errorCode: "closed",
|
|
2114
|
-
message: "Terminal is not running."
|
|
2115
|
-
};
|
|
2116
|
-
}
|
|
2117
|
-
function mutationFailed(errorCode, error, fallback) {
|
|
2118
|
-
return {
|
|
2119
|
-
ok: false,
|
|
2120
|
-
errorCode,
|
|
2121
|
-
message: errorMessage(error, fallback)
|
|
2122
|
-
};
|
|
2123
|
-
}
|
|
2124
|
-
async function resolveTerminalCwd(cwd) {
|
|
2125
|
-
const path = cwd ? isAbsolute(cwd) ? cwd : resolve(homedir(), cwd) : homedir();
|
|
2126
|
-
try {
|
|
2127
|
-
if ((await stat(path)).isDirectory()) return {
|
|
2128
|
-
ok: true,
|
|
2129
|
-
path
|
|
2130
|
-
};
|
|
2131
|
-
} catch {
|
|
2132
|
-
return invalidCwd$1();
|
|
2133
|
-
}
|
|
2134
|
-
return invalidCwd$1();
|
|
2135
|
-
}
|
|
2136
|
-
function invalidCwd$1() {
|
|
2137
|
-
return {
|
|
2138
|
-
ok: false,
|
|
2139
|
-
errorCode: "invalid_cwd",
|
|
2140
|
-
message: "Terminal cwd is not a directory."
|
|
2141
|
-
};
|
|
2142
|
-
}
|
|
2143
|
-
function errorMessage(error, fallback) {
|
|
2144
|
-
return error instanceof Error && error.message ? error.message : fallback;
|
|
2145
|
-
}
|
|
2146
|
-
//#endregion
|
|
2147
|
-
//#region src/text-file-reader.ts
|
|
2148
|
-
const TRUNCATION_MARKER = "[truncated]";
|
|
2149
|
-
const TAIL_READ_BYTES = 64 * 1024;
|
|
2150
|
-
async function readTextFileForRpc(input) {
|
|
2151
|
-
if (input.mode === "tail" && input.offset !== void 0) return invalidRequest("offset is not allowed when mode is tail.");
|
|
2152
|
-
const offset = positiveIntegerOrDefault(input.offset, 1, Number.MAX_SAFE_INTEGER);
|
|
2153
|
-
const limit = positiveIntegerOrDefault(input.limit, DEFAULT_TEXT_FILE_READ_LINES, MAX_TEXT_FILE_READ_LINES);
|
|
2154
|
-
const requestedMaxBytes = positiveIntegerOrDefault(input.maxBytes, DEFAULT_TEXT_FILE_READ_BYTES, MAX_TEXT_FILE_READ_BYTES);
|
|
2155
|
-
if (offset === void 0 || limit === void 0 || requestedMaxBytes === void 0) return invalidRequest("offset, limit, and maxBytes must be positive bounded integers.");
|
|
2156
|
-
const maxBytes = Math.max(requestedMaxBytes, Buffer.byteLength(TRUNCATION_MARKER));
|
|
2157
|
-
try {
|
|
2158
|
-
await assertReadableFile(input.path);
|
|
2159
|
-
if (input.mode === "tail") return await readTextTail(input.path, {
|
|
2160
|
-
limit,
|
|
2161
|
-
maxBytes
|
|
2162
|
-
});
|
|
2163
|
-
return await readTextWindow(input.path, {
|
|
2164
|
-
offset,
|
|
2165
|
-
limit,
|
|
2166
|
-
maxBytes
|
|
2167
|
-
});
|
|
2168
|
-
} catch (error) {
|
|
2169
|
-
return textReadError(error);
|
|
2170
|
-
}
|
|
2171
|
-
}
|
|
2172
|
-
async function readTextTail(path, input) {
|
|
2173
|
-
const tail = await readTailBytes(path, Math.max(TAIL_READ_BYTES, input.maxBytes + input.limit + 4));
|
|
2174
|
-
const lines = splitTailLines(tail.buffer, !tail.reachedStart);
|
|
2175
|
-
const retained = lines.slice(-input.limit);
|
|
2176
|
-
const chunks = [];
|
|
2177
|
-
let bytes = 0;
|
|
2178
|
-
let omittedByBytes = false;
|
|
2179
|
-
for (let index = retained.length - 1; index >= 0; index -= 1) {
|
|
2180
|
-
const line = retained[index];
|
|
2181
|
-
const overLimit = line.overLimit || line.content.byteLength > input.maxBytes;
|
|
2182
|
-
const content = overLimit ? TRUNCATION_MARKER : line.content.toString("utf8");
|
|
2183
|
-
const lineBytes = Buffer.byteLength(content);
|
|
2184
|
-
if (overLimit || bytes + lineBytes > input.maxBytes) {
|
|
2185
|
-
if (overLimit && chunks.length === 0) {
|
|
2186
|
-
chunks.unshift(TRUNCATION_MARKER);
|
|
2187
|
-
bytes += Buffer.byteLength(TRUNCATION_MARKER);
|
|
2188
|
-
omittedByBytes = index > 0;
|
|
2189
|
-
break;
|
|
2190
|
-
}
|
|
2191
|
-
omittedByBytes = true;
|
|
2192
|
-
break;
|
|
2193
|
-
}
|
|
2194
|
-
chunks.unshift(content);
|
|
2195
|
-
bytes += lineBytes;
|
|
2196
|
-
}
|
|
2197
|
-
const lineCount = chunks.length;
|
|
2198
|
-
const totalLines = tail.reachedStart ? lines.length : void 0;
|
|
2199
|
-
const endLine = totalLines === void 0 || lineCount === 0 ? 0 : totalLines;
|
|
2200
|
-
const startLine = totalLines === void 0 || lineCount === 0 ? 0 : totalLines - lineCount + 1;
|
|
2201
|
-
const offset = startLine || 1;
|
|
2202
|
-
return {
|
|
2203
|
-
ok: true,
|
|
2204
|
-
content: chunks.join(""),
|
|
2205
|
-
offset,
|
|
2206
|
-
limit: input.limit,
|
|
2207
|
-
maxBytes: input.maxBytes,
|
|
2208
|
-
startLine,
|
|
2209
|
-
endLine,
|
|
2210
|
-
...totalLines !== void 0 ? { totalLines } : {},
|
|
2211
|
-
truncated: !tail.reachedStart || lines.length > lineCount || omittedByBytes
|
|
2212
|
-
};
|
|
2213
|
-
}
|
|
2214
|
-
async function readTailBytes(path, maxBytes) {
|
|
2215
|
-
const handle = await open(path, "r");
|
|
2216
|
-
try {
|
|
2217
|
-
const details = await handle.stat();
|
|
2218
|
-
if (!details.isFile()) throw fileError("not_file", "Path is not a regular file.");
|
|
2219
|
-
const length = Math.min(details.size, maxBytes);
|
|
2220
|
-
const position = details.size - length;
|
|
2221
|
-
const buffer = Buffer.allocUnsafe(length);
|
|
2222
|
-
let offset = 0;
|
|
2223
|
-
while (offset < length) {
|
|
2224
|
-
const { bytesRead } = await handle.read(buffer, offset, length - offset, position + offset);
|
|
2225
|
-
if (bytesRead === 0) break;
|
|
2226
|
-
offset += bytesRead;
|
|
2227
|
-
}
|
|
2228
|
-
return {
|
|
2229
|
-
buffer: buffer.subarray(0, offset),
|
|
2230
|
-
reachedStart: position === 0
|
|
2231
|
-
};
|
|
2232
|
-
} finally {
|
|
2233
|
-
await handle.close();
|
|
2234
|
-
}
|
|
2235
|
-
}
|
|
2236
|
-
function splitTailLines(buffer, prefixTruncated) {
|
|
2237
|
-
const lines = [];
|
|
2238
|
-
let start = 0;
|
|
2239
|
-
for (let index = 0; index < buffer.byteLength; index += 1) {
|
|
2240
|
-
if (buffer[index] !== 10) continue;
|
|
2241
|
-
lines.push({
|
|
2242
|
-
content: buffer.subarray(start, index + 1),
|
|
2243
|
-
overLimit: prefixTruncated && start === 0
|
|
2244
|
-
});
|
|
2245
|
-
start = index + 1;
|
|
2246
|
-
}
|
|
2247
|
-
if (start < buffer.byteLength) lines.push({
|
|
2248
|
-
content: buffer.subarray(start),
|
|
2249
|
-
overLimit: prefixTruncated && start === 0
|
|
2250
|
-
});
|
|
2251
|
-
return lines;
|
|
2252
|
-
}
|
|
2253
|
-
async function readPatchSourceForRpc(input) {
|
|
2254
|
-
const maxBytes = positiveIntegerOrDefault(input.maxBytes, MAX_PATCH_SOURCE_FILE_BYTES, MAX_PATCH_SOURCE_FILE_BYTES);
|
|
2255
|
-
if (maxBytes === void 0) return invalidRequest("maxBytes must be a positive bounded integer.");
|
|
2256
|
-
try {
|
|
2257
|
-
const content = await readUtf8FileBounded(input.path, maxBytes);
|
|
2258
|
-
return {
|
|
2259
|
-
ok: true,
|
|
2260
|
-
content,
|
|
2261
|
-
sizeBytes: Buffer.byteLength(content)
|
|
2262
|
-
};
|
|
2263
|
-
} catch (error) {
|
|
2264
|
-
return textReadError(error);
|
|
2265
|
-
}
|
|
2266
|
-
}
|
|
2267
|
-
async function assertReadableFile(path) {
|
|
2268
|
-
const handle = await open(path, "r");
|
|
2269
|
-
try {
|
|
2270
|
-
if (!(await handle.stat()).isFile()) throw fileError("not_file", "Path is not a regular file.");
|
|
2271
|
-
} finally {
|
|
2272
|
-
await handle.close();
|
|
2273
|
-
}
|
|
2274
|
-
}
|
|
2275
|
-
async function readUtf8FileBounded(path, maxBytes) {
|
|
2276
|
-
const handle = await open(path, "r");
|
|
2277
|
-
try {
|
|
2278
|
-
const stat = await handle.stat();
|
|
2279
|
-
if (!stat.isFile()) throw fileError("not_file", "Path is not a regular file.");
|
|
2280
|
-
if (stat.size > maxBytes) throw fileError("too_large", `File exceeds ${maxBytes} bytes.`);
|
|
2281
|
-
const buffer = await readBounded(handle, maxBytes);
|
|
2282
|
-
if (buffer.byteLength > maxBytes) throw fileError("too_large", `File exceeds ${maxBytes} bytes.`);
|
|
2283
|
-
return buffer.toString("utf8");
|
|
2284
|
-
} finally {
|
|
2285
|
-
await handle.close();
|
|
2286
|
-
}
|
|
2287
|
-
}
|
|
2288
|
-
async function readBounded(handle, maxBytes) {
|
|
2289
|
-
const buffer = Buffer.allocUnsafe(maxBytes + 1);
|
|
2290
|
-
let offset = 0;
|
|
2291
|
-
while (offset < buffer.byteLength) {
|
|
2292
|
-
const { bytesRead } = await handle.read(buffer, offset, buffer.byteLength - offset, offset);
|
|
2293
|
-
if (bytesRead === 0) break;
|
|
2294
|
-
offset += bytesRead;
|
|
2295
|
-
}
|
|
2296
|
-
return buffer.subarray(0, offset);
|
|
2297
|
-
}
|
|
2298
|
-
async function readTextWindow(path, input) {
|
|
2299
|
-
const chunks = [];
|
|
2300
|
-
let bytes = 0;
|
|
2301
|
-
let currentLine = 0;
|
|
2302
|
-
let lineCount = 0;
|
|
2303
|
-
let truncatedByBytes = false;
|
|
2304
|
-
let hasMoreAfterWindow = false;
|
|
2305
|
-
let nextOffset;
|
|
2306
|
-
for await (const line of readCappedLines(path, input.maxBytes)) {
|
|
2307
|
-
currentLine += 1;
|
|
2308
|
-
if (currentLine < input.offset) continue;
|
|
2309
|
-
if (truncatedByBytes) {
|
|
2310
|
-
nextOffset = currentLine;
|
|
2311
|
-
break;
|
|
2312
|
-
}
|
|
2313
|
-
if (lineCount >= input.limit) {
|
|
2314
|
-
hasMoreAfterWindow = true;
|
|
2315
|
-
break;
|
|
2316
|
-
}
|
|
2317
|
-
const lineBytes = Buffer.byteLength(line.content);
|
|
2318
|
-
if (line.overLimit || bytes + lineBytes > input.maxBytes) {
|
|
2319
|
-
truncatedByBytes = true;
|
|
2320
|
-
if (lineCount === 0) {
|
|
2321
|
-
chunks.push(TRUNCATION_MARKER);
|
|
2322
|
-
bytes += Buffer.byteLength(TRUNCATION_MARKER);
|
|
2323
|
-
lineCount += 1;
|
|
2324
|
-
continue;
|
|
2325
|
-
}
|
|
2326
|
-
nextOffset = currentLine;
|
|
2327
|
-
break;
|
|
2328
|
-
}
|
|
2329
|
-
chunks.push(line.content);
|
|
2330
|
-
bytes += lineBytes;
|
|
2331
|
-
lineCount += 1;
|
|
2332
|
-
}
|
|
2333
|
-
const startLine = lineCount === 0 ? 0 : input.offset;
|
|
2334
|
-
const endLine = lineCount === 0 ? 0 : input.offset + lineCount - 1;
|
|
2335
|
-
nextOffset ??= lineCount > 0 && hasMoreAfterWindow ? endLine + 1 : void 0;
|
|
2336
|
-
return {
|
|
2337
|
-
ok: true,
|
|
2338
|
-
content: chunks.join(""),
|
|
2339
|
-
offset: input.offset,
|
|
2340
|
-
limit: input.limit,
|
|
2341
|
-
maxBytes: input.maxBytes,
|
|
2342
|
-
startLine,
|
|
2343
|
-
endLine,
|
|
2344
|
-
truncated: truncatedByBytes || nextOffset !== void 0,
|
|
2345
|
-
...nextOffset !== void 0 ? { nextOffset } : {}
|
|
2346
|
-
};
|
|
2347
|
-
}
|
|
2348
|
-
async function* readCappedLines(path, maxLineBytes) {
|
|
2349
|
-
let line = "";
|
|
2350
|
-
let lineBytes = 0;
|
|
2351
|
-
let overLimit = false;
|
|
2352
|
-
const pushChar = (char) => {
|
|
2353
|
-
if (!overLimit) {
|
|
2354
|
-
const charBytes = Buffer.byteLength(char);
|
|
2355
|
-
if (lineBytes + charBytes <= maxLineBytes) {
|
|
2356
|
-
line += char;
|
|
2357
|
-
lineBytes += charBytes;
|
|
2358
|
-
} else overLimit = true;
|
|
2359
|
-
}
|
|
2360
|
-
};
|
|
2361
|
-
for await (const chunk of createReadStream(path, { encoding: "utf8" })) for (const char of chunk) {
|
|
2362
|
-
pushChar(char);
|
|
2363
|
-
if (char === "\n") {
|
|
2364
|
-
yield {
|
|
2365
|
-
content: line,
|
|
2366
|
-
overLimit
|
|
2367
|
-
};
|
|
2368
|
-
line = "";
|
|
2369
|
-
lineBytes = 0;
|
|
2370
|
-
overLimit = false;
|
|
2371
|
-
}
|
|
2372
|
-
}
|
|
2373
|
-
if (line.length > 0 || overLimit) yield {
|
|
2374
|
-
content: line,
|
|
2375
|
-
overLimit
|
|
2376
|
-
};
|
|
2377
|
-
}
|
|
2378
|
-
function positiveIntegerOrDefault(value, fallback, max) {
|
|
2379
|
-
const resolved = value ?? fallback;
|
|
2380
|
-
return Number.isSafeInteger(resolved) && resolved > 0 && resolved <= max ? resolved : void 0;
|
|
2381
|
-
}
|
|
2382
|
-
function invalidRequest(message) {
|
|
2383
|
-
return {
|
|
2384
|
-
ok: false,
|
|
2385
|
-
errorCode: "invalid_request",
|
|
2386
|
-
message
|
|
2387
|
-
};
|
|
2388
|
-
}
|
|
2389
|
-
function textReadError(error) {
|
|
2390
|
-
if (isFileError(error)) return {
|
|
2391
|
-
ok: false,
|
|
2392
|
-
errorCode: error.errorCode,
|
|
2393
|
-
message: error.message
|
|
2394
|
-
};
|
|
2395
|
-
const code = typeof error === "object" && error && "code" in error ? error.code : void 0;
|
|
2396
|
-
if (code === "ENOENT" || code === "ENOTDIR") return {
|
|
2397
|
-
ok: false,
|
|
2398
|
-
errorCode: "not_found",
|
|
2399
|
-
message: "File was not found."
|
|
2400
|
-
};
|
|
2401
|
-
if (code === "EACCES" || code === "EPERM") return {
|
|
2402
|
-
ok: false,
|
|
2403
|
-
errorCode: "permission_denied",
|
|
2404
|
-
message: "Permission denied."
|
|
2405
|
-
};
|
|
2406
|
-
return {
|
|
2407
|
-
ok: false,
|
|
2408
|
-
errorCode: "read_failed",
|
|
2409
|
-
message: error instanceof Error ? error.message : "File read failed."
|
|
2410
|
-
};
|
|
2411
|
-
}
|
|
2412
|
-
function fileError(errorCode, message) {
|
|
2413
|
-
const error = new Error(message);
|
|
2414
|
-
error.errorCode = errorCode;
|
|
2415
|
-
return error;
|
|
2416
|
-
}
|
|
2417
|
-
function isFileError(error) {
|
|
2418
|
-
return error instanceof Error && "errorCode" in error && (error.errorCode === "not_file" || error.errorCode === "too_large");
|
|
2419
|
-
}
|
|
2420
|
-
var JobManager = class {
|
|
2421
|
-
jobs = /* @__PURE__ */ new Map();
|
|
2422
|
-
shell;
|
|
2423
|
-
now;
|
|
2424
|
-
randomId;
|
|
2425
|
-
homeDirectory;
|
|
2426
|
-
statPath;
|
|
2427
|
-
createDirectory;
|
|
2428
|
-
setPermissions;
|
|
2429
|
-
openLog;
|
|
2430
|
-
removeFile;
|
|
2431
|
-
spawnProcess;
|
|
2432
|
-
signalProcessGroup;
|
|
2433
|
-
runTaskkill;
|
|
2434
|
-
startWatchdog;
|
|
2435
|
-
wait;
|
|
2436
|
-
startQueue = Promise.resolve();
|
|
2437
|
-
stopping = false;
|
|
2438
|
-
constructor(options = {}) {
|
|
2439
|
-
this.shell = options.shell ?? detectShell();
|
|
2440
|
-
this.now = options.now ?? (() => /* @__PURE__ */ new Date());
|
|
2441
|
-
this.randomId = options.randomId ?? randomUUID;
|
|
2442
|
-
this.homeDirectory = options.homeDirectory ?? homedir();
|
|
2443
|
-
this.statPath = options.statPath ?? stat;
|
|
2444
|
-
this.createDirectory = options.createDirectory ?? mkdir;
|
|
2445
|
-
this.setPermissions = options.setPermissions ?? chmod;
|
|
2446
|
-
this.openLog = options.openLog ?? openJobLog;
|
|
2447
|
-
this.removeFile = options.removeFile ?? unlink;
|
|
2448
|
-
this.spawnProcess = options.spawnProcess ?? spawnJobProcess;
|
|
2449
|
-
this.signalProcessGroup = options.signalProcessGroup ?? signalPosixProcessGroup;
|
|
2450
|
-
this.runTaskkill = options.runTaskkill ?? runWindowsTaskkill;
|
|
2451
|
-
this.startWatchdog = options.startWatchdog ?? startParentWatchdog;
|
|
2452
|
-
this.wait = options.wait ?? wait;
|
|
2453
|
-
}
|
|
2454
|
-
async start(input) {
|
|
2455
|
-
if (this.stopping) return managerStopping();
|
|
2456
|
-
let releaseStart = () => {};
|
|
2457
|
-
const previousStart = this.startQueue;
|
|
2458
|
-
this.startQueue = new Promise((resolve) => {
|
|
2459
|
-
releaseStart = resolve;
|
|
2460
|
-
});
|
|
2461
|
-
await previousStart;
|
|
2462
|
-
try {
|
|
2463
|
-
if (this.stopping) return managerStopping();
|
|
2464
|
-
return await this.startExclusive(input);
|
|
20
|
+
let local = await discoverLocalConnector({
|
|
21
|
+
deploymentId: deployment.deploymentId,
|
|
22
|
+
serviceKey: deployment.serviceKey
|
|
23
|
+
});
|
|
24
|
+
if (local.state === "ownership_conflict") throw Object.assign(/* @__PURE__ */ new Error("The local connector service belongs to another deployment."), { code: "ownership_conflict" });
|
|
25
|
+
const storedProfile = await store.getProfile(deployment.deploymentId);
|
|
26
|
+
if (local.entry) {
|
|
27
|
+
await adoptInstalledConnectorBinding({
|
|
28
|
+
store,
|
|
29
|
+
deployment,
|
|
30
|
+
entry: local.entry,
|
|
31
|
+
allowInsecureTls: options.allowInsecureTls
|
|
32
|
+
});
|
|
33
|
+
if (local.state === "needs_bootstrap" && !storedProfile?.bootstrapReceipt) {
|
|
34
|
+
local = await restartInstalledConnector(deployment.deploymentId, deployment.serviceKey);
|
|
35
|
+
if (local.state !== "ready") throw Object.assign(/* @__PURE__ */ new Error("The installed connector service could not be started."), { code: "installed_connector_unavailable" });
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
if (local.state === "needs_bootstrap" && storedProfile?.bootstrapReceipt) {
|
|
39
|
+
const recoveredRelease = await (options.snapshotResolver ?? resolveVerifiedConnectorSnapshot)(storedProfile.bootstrapReceipt.snapshot);
|
|
40
|
+
try {
|
|
41
|
+
const recovered = await withDeploymentReconciliationLock({
|
|
42
|
+
serviceKey: deployment.serviceKey,
|
|
43
|
+
run: () => resumeConnectorBootstrap({
|
|
44
|
+
deployment,
|
|
45
|
+
store,
|
|
46
|
+
release: recoveredRelease,
|
|
47
|
+
allowInsecureTls: insecureTlsForServer(serverUrl, deployment.serverUrl, options.allowInsecureTls ?? storedProfile.allowInsecureTls),
|
|
48
|
+
reconciliationLockHeld: true
|
|
49
|
+
})
|
|
50
|
+
});
|
|
51
|
+
console.log(`Logged in to ${deployment.serverUrl}.`);
|
|
52
|
+
console.log(`Machine: ${recovered.machineId}`);
|
|
53
|
+
return;
|
|
2465
54
|
} finally {
|
|
2466
|
-
|
|
55
|
+
await recoveredRelease.dispose();
|
|
2467
56
|
}
|
|
2468
57
|
}
|
|
2469
|
-
|
|
2470
|
-
|
|
2471
|
-
|
|
2472
|
-
|
|
2473
|
-
|
|
2474
|
-
|
|
2475
|
-
|
|
2476
|
-
|
|
2477
|
-
|
|
2478
|
-
|
|
2479
|
-
|
|
2480
|
-
|
|
2481
|
-
|
|
2482
|
-
|
|
2483
|
-
} catch {
|
|
2484
|
-
return invalidCwd();
|
|
2485
|
-
}
|
|
2486
|
-
const jobId = this.randomId();
|
|
2487
|
-
const logPath = machineJobLogPath(this.shell.os, jobId);
|
|
2488
|
-
let log;
|
|
2489
|
-
try {
|
|
2490
|
-
await this.prepareLogDirectory();
|
|
2491
|
-
log = await this.openLog(logPath);
|
|
2492
|
-
} catch (error) {
|
|
2493
|
-
return {
|
|
2494
|
-
ok: false,
|
|
2495
|
-
errorCode: "unavailable",
|
|
2496
|
-
message: boundedError(error, "Could not create the background job log.")
|
|
2497
|
-
};
|
|
2498
|
-
}
|
|
2499
|
-
let process;
|
|
2500
|
-
try {
|
|
2501
|
-
process = this.spawnProcess(this.shell.path, oneShotShellArgs(this.shell, input.command), {
|
|
2502
|
-
cwd,
|
|
2503
|
-
env: { ...globalThis.process.env },
|
|
2504
|
-
detached: this.shell.os !== "windows",
|
|
2505
|
-
windowsHide: true
|
|
58
|
+
let release;
|
|
59
|
+
if (local.state === "needs_bootstrap" && !options.serviceInstaller) release = await (options.releaseResolver ?? resolveVerifiedConnectorRelease)(deployment);
|
|
60
|
+
try {
|
|
61
|
+
let bindingReauthorizations = 0;
|
|
62
|
+
while (true) {
|
|
63
|
+
const device = await startConnectorDeviceAuthorization({
|
|
64
|
+
serverUrl,
|
|
65
|
+
store,
|
|
66
|
+
deviceLabel: hostname(),
|
|
67
|
+
allowInsecureTls: options.allowInsecureTls,
|
|
68
|
+
...release ? { bootstrap: {
|
|
69
|
+
policyDigest: deployment.policyDigest,
|
|
70
|
+
releaseTuple: release.snapshot.tuple
|
|
71
|
+
} } : {}
|
|
2506
72
|
});
|
|
2507
|
-
|
|
2508
|
-
|
|
2509
|
-
|
|
2510
|
-
|
|
2511
|
-
|
|
2512
|
-
|
|
2513
|
-
|
|
2514
|
-
|
|
2515
|
-
|
|
2516
|
-
|
|
2517
|
-
|
|
2518
|
-
|
|
2519
|
-
|
|
2520
|
-
|
|
2521
|
-
|
|
2522
|
-
|
|
2523
|
-
|
|
2524
|
-
|
|
2525
|
-
|
|
2526
|
-
|
|
2527
|
-
|
|
2528
|
-
|
|
2529
|
-
|
|
2530
|
-
|
|
2531
|
-
|
|
2532
|
-
|
|
2533
|
-
|
|
2534
|
-
|
|
2535
|
-
|
|
2536
|
-
|
|
2537
|
-
|
|
2538
|
-
|
|
2539
|
-
|
|
2540
|
-
|
|
2541
|
-
|
|
2542
|
-
|
|
2543
|
-
|
|
2544
|
-
|
|
2545
|
-
|
|
2546
|
-
|
|
2547
|
-
|
|
2548
|
-
|
|
2549
|
-
|
|
2550
|
-
|
|
2551
|
-
|
|
2552
|
-
|
|
2553
|
-
|
|
2554
|
-
|
|
2555
|
-
|
|
2556
|
-
|
|
2557
|
-
message: cleanupError ? `${boundedError(error, "Could not start the background job watchdog.")} Process-tree cleanup also failed: ${boundedError(cleanupError, "unknown error")}`.slice(0, 1e3) : boundedError(error, "Could not start the background job watchdog.")
|
|
73
|
+
console.log(`Open ${device.verificationUriComplete}`);
|
|
74
|
+
console.log(`Code: ${device.userCode}`);
|
|
75
|
+
if (options.openBrowser !== false) openBrowser(device.verificationUriComplete);
|
|
76
|
+
await pollConnectorDeviceToken({
|
|
77
|
+
serverUrl: device.serverUrl,
|
|
78
|
+
deploymentId: device.deploymentId,
|
|
79
|
+
store,
|
|
80
|
+
deviceCode: device.deviceCode,
|
|
81
|
+
intervalSeconds: device.interval,
|
|
82
|
+
expiresInSeconds: device.expiresIn,
|
|
83
|
+
allowInsecureTls: insecureTlsForServer(serverUrl, device.serverUrl, options.allowInsecureTls)
|
|
84
|
+
});
|
|
85
|
+
const finishSetup = async () => {
|
|
86
|
+
const currentDeployment = await discoverDeploymentForStore({
|
|
87
|
+
serverUrl: device.serverUrl,
|
|
88
|
+
store,
|
|
89
|
+
allowInsecureTls: options.allowInsecureTls
|
|
90
|
+
});
|
|
91
|
+
if (currentDeployment.deploymentId !== deployment.deploymentId || currentDeployment.policyDigest !== deployment.policyDigest) throw Object.assign(/* @__PURE__ */ new Error("Connector deployment policy changed during login."), { code: "bootstrap_policy_drift" });
|
|
92
|
+
const currentLocal = await discoverLocalConnector({
|
|
93
|
+
deploymentId: deployment.deploymentId,
|
|
94
|
+
serviceKey: deployment.serviceKey
|
|
95
|
+
});
|
|
96
|
+
if (currentLocal.state === "ownership_conflict") throw Object.assign(/* @__PURE__ */ new Error("The local connector service belongs to another deployment."), { code: "ownership_conflict" });
|
|
97
|
+
if (currentLocal.entry) {
|
|
98
|
+
const adopted = await adoptInstalledConnectorBinding({
|
|
99
|
+
store,
|
|
100
|
+
deployment: currentDeployment,
|
|
101
|
+
entry: currentLocal.entry,
|
|
102
|
+
allowInsecureTls: insecureTlsForServer(serverUrl, device.serverUrl, options.allowInsecureTls)
|
|
103
|
+
});
|
|
104
|
+
if (adopted.staleCredentials) throw new BindingReauthorizationRequired(adopted.staleCredentials.refreshToken);
|
|
105
|
+
}
|
|
106
|
+
if (release && currentLocal.entry && currentLocal.state !== "ready") throw Object.assign(/* @__PURE__ */ new Error("The installed connector service could not be reconciled."), { code: "installed_connector_unavailable" });
|
|
107
|
+
return setupConnectorProfile({
|
|
108
|
+
serverUrl: device.serverUrl,
|
|
109
|
+
store,
|
|
110
|
+
name: options.name,
|
|
111
|
+
installService: release ? false : options.installService ?? Boolean(options.serviceInstaller),
|
|
112
|
+
serviceInstaller: options.serviceInstaller,
|
|
113
|
+
allowInsecureTls: insecureTlsForServer(serverUrl, device.serverUrl, options.allowInsecureTls),
|
|
114
|
+
...release && currentLocal.state !== "ready" ? {
|
|
115
|
+
bootstrap: {
|
|
116
|
+
release,
|
|
117
|
+
policyDigest: deployment.policyDigest,
|
|
118
|
+
releaseTuple: release.snapshot.tuple
|
|
119
|
+
},
|
|
120
|
+
reconciliationLockHeld: true
|
|
121
|
+
} : {}
|
|
122
|
+
});
|
|
2558
123
|
};
|
|
2559
|
-
}
|
|
2560
|
-
let resolveClosed = () => {};
|
|
2561
|
-
const closePromise = new Promise((resolve) => {
|
|
2562
|
-
resolveClosed = resolve;
|
|
2563
|
-
});
|
|
2564
|
-
job = {
|
|
2565
|
-
jobId,
|
|
2566
|
-
process,
|
|
2567
|
-
pid: process.pid,
|
|
2568
|
-
cwd,
|
|
2569
|
-
startedAt: timestamp,
|
|
2570
|
-
updatedAt: timestamp,
|
|
2571
|
-
status: "running",
|
|
2572
|
-
exitCode: null,
|
|
2573
|
-
exitSignal: null,
|
|
2574
|
-
fullLogPath: void 0,
|
|
2575
|
-
errorCode: void 0,
|
|
2576
|
-
message: void 0,
|
|
2577
|
-
pendingError: void 0,
|
|
2578
|
-
tail: Buffer.alloc(0),
|
|
2579
|
-
tailStartCursor: 0,
|
|
2580
|
-
nextCursor: 0,
|
|
2581
|
-
log,
|
|
2582
|
-
writeQueue: Promise.resolve(),
|
|
2583
|
-
logFailed: false,
|
|
2584
|
-
closePromise,
|
|
2585
|
-
resolveClosed,
|
|
2586
|
-
killRequested: false,
|
|
2587
|
-
processClosed: false,
|
|
2588
|
-
watchdog
|
|
2589
|
-
};
|
|
2590
|
-
this.jobs.set(jobId, job);
|
|
2591
|
-
for (const chunk of pendingOutput) this.enqueueOutput(job, chunk);
|
|
2592
|
-
if (pendingClose) this.handleProcessClose(job, pendingClose.exitCode, pendingClose.signal);
|
|
2593
|
-
return {
|
|
2594
|
-
ok: true,
|
|
2595
|
-
job: snapshot(job)
|
|
2596
|
-
};
|
|
2597
|
-
}
|
|
2598
|
-
get(input) {
|
|
2599
|
-
this.pruneExpired();
|
|
2600
|
-
const job = this.jobs.get(input.jobId);
|
|
2601
|
-
if (!job) return jobNotFound();
|
|
2602
|
-
const requestedCursor = input.cursor ?? 0;
|
|
2603
|
-
const cursor = Math.max(requestedCursor, job.tailStartCursor);
|
|
2604
|
-
const safeEnd = completeUtf8PrefixLength(job.tail);
|
|
2605
|
-
const relativeCursor = Math.min(Math.max(cursor - job.tailStartCursor, 0), safeEnd);
|
|
2606
|
-
const safeRelativeCursor = nextUtf8Boundary(job.tail, relativeCursor);
|
|
2607
|
-
const actualCursor = job.tailStartCursor + safeRelativeCursor;
|
|
2608
|
-
const nextCursor = job.tailStartCursor + safeEnd;
|
|
2609
|
-
const response = {
|
|
2610
|
-
ok: true,
|
|
2611
|
-
job: snapshot(job),
|
|
2612
|
-
output: job.tail.subarray(safeRelativeCursor, safeEnd).toString("utf8"),
|
|
2613
|
-
cursor: actualCursor,
|
|
2614
|
-
nextCursor,
|
|
2615
|
-
gapBytes: Math.max(0, actualCursor - requestedCursor)
|
|
2616
|
-
};
|
|
2617
|
-
if (job.status !== "running") this.jobs.delete(job.jobId);
|
|
2618
|
-
return response;
|
|
2619
|
-
}
|
|
2620
|
-
async kill(input) {
|
|
2621
|
-
this.pruneExpired();
|
|
2622
|
-
const job = this.jobs.get(input.jobId);
|
|
2623
|
-
if (!job) return jobNotFound();
|
|
2624
|
-
if (job.status !== "running") return {
|
|
2625
|
-
ok: true,
|
|
2626
|
-
job: snapshot(job)
|
|
2627
|
-
};
|
|
2628
|
-
if (job.killPromise) return job.killPromise;
|
|
2629
|
-
const killPromise = this.killRunning(job);
|
|
2630
|
-
job.killPromise = killPromise;
|
|
2631
|
-
const result = await killPromise;
|
|
2632
|
-
if (!result.ok && job.killPromise === killPromise) job.killPromise = void 0;
|
|
2633
|
-
return result;
|
|
2634
|
-
}
|
|
2635
|
-
async stop() {
|
|
2636
|
-
this.stopping = true;
|
|
2637
|
-
await this.startQueue;
|
|
2638
|
-
const active = [...this.jobs.values()].filter((job) => job.status === "running");
|
|
2639
|
-
await Promise.all(active.map(async (job) => {
|
|
2640
|
-
if (!(await this.kill({ jobId: job.jobId })).ok) {
|
|
2641
|
-
const retry = await this.kill({ jobId: job.jobId });
|
|
2642
|
-
if (!retry.ok) throw new Error(retry.message);
|
|
2643
|
-
}
|
|
2644
|
-
}));
|
|
2645
|
-
await Promise.all([...this.jobs.values()].map((job) => job.closePromise));
|
|
2646
|
-
this.jobs.clear();
|
|
2647
|
-
}
|
|
2648
|
-
enqueueOutput(job, value) {
|
|
2649
|
-
const bytes = Buffer.isBuffer(value) ? value : Buffer.from(String(value));
|
|
2650
|
-
job.writeQueue = job.writeQueue.then(async () => {
|
|
2651
|
-
if (job.logFailed) return;
|
|
2652
|
-
job.nextCursor += bytes.byteLength;
|
|
2653
|
-
job.tail = appendBoundedUtf8Bytes(job.tail, bytes, MAX_MACHINE_JOB_OUTPUT_BYTES);
|
|
2654
|
-
job.tailStartCursor = job.nextCursor - job.tail.byteLength;
|
|
2655
|
-
job.updatedAt = this.now().toISOString();
|
|
2656
124
|
try {
|
|
2657
|
-
|
|
2658
|
-
|
|
2659
|
-
|
|
2660
|
-
|
|
2661
|
-
|
|
2662
|
-
}
|
|
125
|
+
const setup = release ? await withDeploymentReconciliationLock({
|
|
126
|
+
serviceKey: deployment.serviceKey,
|
|
127
|
+
run: finishSetup
|
|
128
|
+
}) : await finishSetup();
|
|
129
|
+
console.log(`Logged in to ${device.serverUrl}.`);
|
|
130
|
+
console.log(`Machine: ${setup.machineId}`);
|
|
131
|
+
return;
|
|
2663
132
|
} catch (error) {
|
|
2664
|
-
|
|
2665
|
-
|
|
2666
|
-
|
|
2667
|
-
message: boundedError(error, "The background job log could not be written.")
|
|
2668
|
-
};
|
|
2669
|
-
this.terminate(job).catch((killError) => {
|
|
2670
|
-
job.pendingError = {
|
|
2671
|
-
errorCode: "unavailable",
|
|
2672
|
-
message: `${job.pendingError?.message ?? "The background job log failed."} Process-tree termination also failed: ${boundedError(killError, "unknown error")}`.slice(0, 1e3)
|
|
2673
|
-
};
|
|
2674
|
-
});
|
|
133
|
+
if (!(error instanceof BindingReauthorizationRequired) || bindingReauthorizations >= 1) throw error;
|
|
134
|
+
bindingReauthorizations += 1;
|
|
135
|
+
await new CliAuthClient(device.serverUrl, { allowInsecureTls: insecureTlsForServer(serverUrl, device.serverUrl, options.allowInsecureTls) }).revoke(error.refreshToken).catch(() => void 0);
|
|
2675
136
|
}
|
|
2676
|
-
});
|
|
2677
|
-
}
|
|
2678
|
-
async finish(job, exitCode, signal) {
|
|
2679
|
-
await job.writeQueue;
|
|
2680
|
-
try {
|
|
2681
|
-
await job.log.close();
|
|
2682
|
-
} catch (error) {
|
|
2683
|
-
job.logFailed = true;
|
|
2684
|
-
job.pendingError = {
|
|
2685
|
-
errorCode: "unavailable",
|
|
2686
|
-
message: boundedError(error, "The background job log could not be closed.")
|
|
2687
|
-
};
|
|
2688
|
-
}
|
|
2689
|
-
job.exitCode = exitCode;
|
|
2690
|
-
job.exitSignal = signal;
|
|
2691
|
-
if (job.logFailed) {
|
|
2692
|
-
job.status = "failed";
|
|
2693
|
-
job.fullLogPath = void 0;
|
|
2694
|
-
job.errorCode = job.pendingError?.errorCode ?? "unavailable";
|
|
2695
|
-
job.message = job.pendingError?.message ?? "The background job log is incomplete.";
|
|
2696
|
-
} else if (job.killRequested) {
|
|
2697
|
-
job.status = "killed";
|
|
2698
|
-
job.fullLogPath = machineJobLogPath(this.shell.os, job.jobId);
|
|
2699
|
-
} else if (exitCode === 0) {
|
|
2700
|
-
job.status = "completed";
|
|
2701
|
-
job.fullLogPath = machineJobLogPath(this.shell.os, job.jobId);
|
|
2702
|
-
} else {
|
|
2703
|
-
job.status = "failed";
|
|
2704
|
-
job.errorCode = "execution_failed";
|
|
2705
|
-
job.message = `Background job exited with code ${exitCode ?? "unknown"}${signal ? ` (${signal})` : ""}.`;
|
|
2706
|
-
job.fullLogPath = machineJobLogPath(this.shell.os, job.jobId);
|
|
2707
|
-
}
|
|
2708
|
-
job.updatedAt = this.now().toISOString();
|
|
2709
|
-
job.completedAtMs = this.now().getTime();
|
|
2710
|
-
job.resolveClosed();
|
|
2711
|
-
}
|
|
2712
|
-
handleProcessClose(job, exitCode, signal) {
|
|
2713
|
-
job.processClosed = true;
|
|
2714
|
-
job.exitCode = exitCode;
|
|
2715
|
-
job.exitSignal = signal;
|
|
2716
|
-
job.watchdog.stop();
|
|
2717
|
-
this.finish(job, exitCode, signal);
|
|
2718
|
-
}
|
|
2719
|
-
async killRunning(job) {
|
|
2720
|
-
if (job.processClosed) {
|
|
2721
|
-
await job.closePromise;
|
|
2722
|
-
return {
|
|
2723
|
-
ok: true,
|
|
2724
|
-
job: snapshot(job)
|
|
2725
|
-
};
|
|
2726
|
-
}
|
|
2727
|
-
job.killRequested = true;
|
|
2728
|
-
job.updatedAt = this.now().toISOString();
|
|
2729
|
-
try {
|
|
2730
|
-
await this.terminate(job);
|
|
2731
|
-
await job.closePromise;
|
|
2732
|
-
return {
|
|
2733
|
-
ok: true,
|
|
2734
|
-
job: snapshot(job)
|
|
2735
|
-
};
|
|
2736
|
-
} catch (error) {
|
|
2737
|
-
if (job.completedAtMs !== void 0) return {
|
|
2738
|
-
ok: true,
|
|
2739
|
-
job: snapshot(job)
|
|
2740
|
-
};
|
|
2741
|
-
job.killRequested = false;
|
|
2742
|
-
return {
|
|
2743
|
-
ok: false,
|
|
2744
|
-
errorCode: "unavailable",
|
|
2745
|
-
message: boundedError(error, "Could not stop the background job process tree.")
|
|
2746
|
-
};
|
|
2747
137
|
}
|
|
138
|
+
} finally {
|
|
139
|
+
await release?.dispose();
|
|
2748
140
|
}
|
|
2749
|
-
async terminate(job) {
|
|
2750
|
-
if (job.processClosed || job.completedAtMs !== void 0) return;
|
|
2751
|
-
if (this.shell.os === "windows") {
|
|
2752
|
-
await this.runTaskkill(job.pid, false);
|
|
2753
|
-
if (!await waitForClose(job.closePromise, this.wait, 5e3)) await this.runTaskkill(job.pid, true);
|
|
2754
|
-
return;
|
|
2755
|
-
}
|
|
2756
|
-
await this.signalProcessGroup(job.pid, "SIGTERM");
|
|
2757
|
-
if (!await waitForClose(job.closePromise, this.wait, 5e3)) await this.signalProcessGroup(job.pid, "SIGKILL");
|
|
2758
|
-
}
|
|
2759
|
-
async prepareLogDirectory() {
|
|
2760
|
-
const directory = machineJobLogDirectory(this.shell.os);
|
|
2761
|
-
await this.createDirectory(directory, {
|
|
2762
|
-
recursive: true,
|
|
2763
|
-
...this.shell.os === "windows" ? {} : { mode: 448 }
|
|
2764
|
-
});
|
|
2765
|
-
if (this.shell.os === "windows") return;
|
|
2766
|
-
await this.setPermissions(directory, 448);
|
|
2767
|
-
const details = await this.statPath(directory);
|
|
2768
|
-
const owner = typeof process.getuid === "function" ? process.getuid() : void 0;
|
|
2769
|
-
if (!details.isDirectory() || owner !== void 0 && details.uid !== owner || (details.mode & 63) !== 0) throw new Error("The background job log directory is not private to this connector user.");
|
|
2770
|
-
}
|
|
2771
|
-
async forceTerminateStartedProcess(pid) {
|
|
2772
|
-
if (this.shell.os === "windows") await this.runTaskkill(pid, true);
|
|
2773
|
-
else await this.signalProcessGroup(pid, "SIGKILL");
|
|
2774
|
-
}
|
|
2775
|
-
pruneExpired() {
|
|
2776
|
-
const now = this.now().getTime();
|
|
2777
|
-
for (const [jobId, job] of this.jobs) if (job.completedAtMs !== void 0 && now - job.completedAtMs >= 36e5) this.jobs.delete(jobId);
|
|
2778
|
-
}
|
|
2779
|
-
};
|
|
2780
|
-
function machineJobLogDirectory(os) {
|
|
2781
|
-
return os === "windows" ? "C:\\Windows\\Temp\\agents\\tool-output" : "/tmp/agents/tool-output";
|
|
2782
|
-
}
|
|
2783
|
-
function machineJobLogPath(os, jobId) {
|
|
2784
|
-
const safeId = jobId.replace(/[^A-Za-z0-9._-]/g, "-");
|
|
2785
|
-
const directory = machineJobLogDirectory(os);
|
|
2786
|
-
return os === "windows" ? win32.join(directory, `job-${safeId}.txt`) : posix.join(directory, `job-${safeId}.txt`);
|
|
2787
141
|
}
|
|
2788
|
-
|
|
2789
|
-
|
|
2790
|
-
|
|
2791
|
-
|
|
2792
|
-
|
|
2793
|
-
"/c",
|
|
2794
|
-
command
|
|
2795
|
-
];
|
|
2796
|
-
return [...shell.args.filter((arg) => arg !== "-"), command];
|
|
142
|
+
var BindingReauthorizationRequired = class extends Error {
|
|
143
|
+
refreshToken;
|
|
144
|
+
constructor(refreshToken) {
|
|
145
|
+
super("Connector binding changed during authorization.");
|
|
146
|
+
this.refreshToken = refreshToken;
|
|
2797
147
|
}
|
|
2798
|
-
|
|
2799
|
-
|
|
2800
|
-
|
|
2801
|
-
|
|
2802
|
-
];
|
|
2803
|
-
}
|
|
2804
|
-
function spawnJobProcess(file, args, options) {
|
|
2805
|
-
return spawn(file, args, {
|
|
2806
|
-
...options,
|
|
2807
|
-
stdio: [
|
|
2808
|
-
"ignore",
|
|
2809
|
-
"pipe",
|
|
2810
|
-
"pipe"
|
|
2811
|
-
]
|
|
2812
|
-
});
|
|
2813
|
-
}
|
|
2814
|
-
async function openJobLog(path) {
|
|
2815
|
-
const handle = await open(path, "wx", 384);
|
|
148
|
+
};
|
|
149
|
+
async function serviceLocalConfigInput(options) {
|
|
150
|
+
const profile = options.serverUrl ? await options.store.findProfileForServerUrl(options.serverUrl) : await options.store.getActiveProfile();
|
|
151
|
+
if (!profile) throw new Error("Connector deployment profile was not found. Run agents login.");
|
|
2816
152
|
return {
|
|
2817
|
-
|
|
2818
|
-
|
|
153
|
+
deploymentId: profile.deploymentId,
|
|
154
|
+
serviceKey: deploymentServiceKey(profile.deploymentId)
|
|
2819
155
|
};
|
|
2820
156
|
}
|
|
2821
|
-
async function
|
|
2822
|
-
|
|
2823
|
-
|
|
2824
|
-
|
|
2825
|
-
|
|
2826
|
-
|
|
2827
|
-
|
|
2828
|
-
|
|
2829
|
-
|
|
2830
|
-
|
|
2831
|
-
|
|
2832
|
-
|
|
2833
|
-
|
|
2834
|
-
];
|
|
2835
|
-
|
|
2836
|
-
execFile("taskkill.exe", args, { windowsHide: true }, (error) => {
|
|
2837
|
-
if (error && error.code !== "ESRCH") reject(error);
|
|
2838
|
-
else resolve();
|
|
2839
|
-
});
|
|
2840
|
-
});
|
|
157
|
+
async function logout(options = {}) {
|
|
158
|
+
const store = options.store ?? new TokenStore();
|
|
159
|
+
const serverUrl = await store.resolveServerUrl(options.serverUrl);
|
|
160
|
+
const storedProfile = options.serverUrl ? await store.findStoredProfileForServerUrl(serverUrl) : await store.getActiveProfile();
|
|
161
|
+
if (!storedProfile) throw new Error(`Not logged in to ${serverUrl}.`);
|
|
162
|
+
const removed = await store.removeClientCredentials(storedProfile.deploymentId);
|
|
163
|
+
const refreshTokens = new Set([removed.active?.refreshToken, removed.pending?.refreshToken].filter((value) => Boolean(value)));
|
|
164
|
+
const revokeServerUrl = (await discoverDeploymentForStore({
|
|
165
|
+
serverUrl: storedProfile.canonicalOrigin,
|
|
166
|
+
store,
|
|
167
|
+
allowInsecureTls: options.allowInsecureTls ?? storedProfile.allowInsecureTls
|
|
168
|
+
}).catch(() => void 0))?.serverUrl ?? storedProfile.canonicalOrigin;
|
|
169
|
+
const auth = new CliAuthClient(revokeServerUrl, { allowInsecureTls: insecureTlsForServer(storedProfile.canonicalOrigin, revokeServerUrl, options.allowInsecureTls ?? storedProfile.allowInsecureTls) });
|
|
170
|
+
await Promise.all([...refreshTokens].map((refreshToken) => auth.revoke(refreshToken).catch(() => void 0)));
|
|
171
|
+
console.log(`Logged out of ${revokeServerUrl}.`);
|
|
2841
172
|
}
|
|
2842
|
-
async function
|
|
2843
|
-
const
|
|
2844
|
-
|
|
2845
|
-
|
|
2846
|
-
|
|
2847
|
-
|
|
2848
|
-
|
|
2849
|
-
], {
|
|
2850
|
-
detached: true,
|
|
2851
|
-
stdio: "ignore",
|
|
2852
|
-
windowsHide: true
|
|
2853
|
-
});
|
|
2854
|
-
await new Promise((resolve, reject) => {
|
|
2855
|
-
child.once("spawn", resolve);
|
|
2856
|
-
child.once("error", reject);
|
|
173
|
+
async function whoami(options = {}) {
|
|
174
|
+
const store = options.store ?? new TokenStore();
|
|
175
|
+
const serverUrl = await store.resolveServerUrl(options.serverUrl);
|
|
176
|
+
const deployment = await discoverDeploymentForStore({
|
|
177
|
+
serverUrl,
|
|
178
|
+
store,
|
|
179
|
+
allowInsecureTls: options.allowInsecureTls
|
|
2857
180
|
});
|
|
2858
|
-
|
|
2859
|
-
|
|
2860
|
-
|
|
2861
|
-
|
|
2862
|
-
|
|
2863
|
-
|
|
2864
|
-
|
|
2865
|
-
}
|
|
2866
|
-
const PARENT_WATCHDOG_SCRIPT = String.raw`
|
|
2867
|
-
const { execFileSync } = require("node:child_process");
|
|
2868
|
-
const [connectorText, jobText, os] = process.argv.slice(1);
|
|
2869
|
-
const connectorPid = Number(connectorText);
|
|
2870
|
-
const jobPid = Number(jobText);
|
|
2871
|
-
function alive(pid) {
|
|
2872
|
-
try {
|
|
2873
|
-
process.kill(pid, 0);
|
|
2874
|
-
return true;
|
|
2875
|
-
} catch {
|
|
2876
|
-
return false;
|
|
2877
|
-
}
|
|
2878
|
-
}
|
|
2879
|
-
setInterval(() => {
|
|
2880
|
-
if (!alive(jobPid)) process.exit(0);
|
|
2881
|
-
if (alive(connectorPid)) return;
|
|
2882
|
-
try {
|
|
2883
|
-
if (os === "windows") {
|
|
2884
|
-
execFileSync("taskkill.exe", ["/PID", String(jobPid), "/T", "/F"], {
|
|
2885
|
-
windowsHide: true,
|
|
2886
|
-
stdio: "ignore",
|
|
2887
|
-
});
|
|
2888
|
-
} else {
|
|
2889
|
-
process.kill(-jobPid, "SIGKILL");
|
|
2890
|
-
}
|
|
2891
|
-
} catch {}
|
|
2892
|
-
process.exit(0);
|
|
2893
|
-
}, 250);
|
|
2894
|
-
`;
|
|
2895
|
-
function waitForSpawn(process) {
|
|
2896
|
-
return new Promise((resolve) => {
|
|
2897
|
-
process.once("spawn", () => resolve({ ok: true }));
|
|
2898
|
-
process.once("error", (error) => resolve({
|
|
2899
|
-
ok: false,
|
|
2900
|
-
error
|
|
2901
|
-
}));
|
|
181
|
+
const profile = await store.getProfile(deployment.deploymentId);
|
|
182
|
+
if (!profile?.refreshToken) throw new Error(`Not logged in to ${serverUrl}. Run agents login.`);
|
|
183
|
+
await ensureAccessToken({
|
|
184
|
+
serverUrl: deployment.serverUrl,
|
|
185
|
+
deploymentId: deployment.deploymentId,
|
|
186
|
+
store,
|
|
187
|
+
allowInsecureTls: insecureTlsForServer(serverUrl, deployment.serverUrl, options.allowInsecureTls ?? profile.allowInsecureTls)
|
|
2902
188
|
});
|
|
189
|
+
console.log(`Server: ${deployment.serverUrl}`);
|
|
190
|
+
console.log(`Machine: ${profile.machineId ?? "not claimed yet"}`);
|
|
2903
191
|
}
|
|
2904
|
-
|
|
2905
|
-
|
|
2906
|
-
|
|
2907
|
-
|
|
2908
|
-
|
|
2909
|
-
|
|
2910
|
-
|
|
2911
|
-
|
|
2912
|
-
|
|
2913
|
-
|
|
2914
|
-
|
|
2915
|
-
|
|
2916
|
-
|
|
2917
|
-
|
|
2918
|
-
|
|
2919
|
-
|
|
2920
|
-
...job.errorCode ? {
|
|
2921
|
-
errorCode: job.errorCode,
|
|
2922
|
-
message: job.message
|
|
2923
|
-
} : {}
|
|
2924
|
-
};
|
|
2925
|
-
}
|
|
2926
|
-
function invalidCwd() {
|
|
2927
|
-
return {
|
|
2928
|
-
ok: false,
|
|
2929
|
-
errorCode: "invalid_cwd",
|
|
2930
|
-
message: "Background job cwd is not a directory."
|
|
2931
|
-
};
|
|
2932
|
-
}
|
|
2933
|
-
function jobNotFound() {
|
|
2934
|
-
return {
|
|
2935
|
-
ok: false,
|
|
2936
|
-
errorCode: "not_found",
|
|
2937
|
-
message: "Background job was not found."
|
|
2938
|
-
};
|
|
2939
|
-
}
|
|
2940
|
-
function nextUtf8Boundary(buffer, offset) {
|
|
2941
|
-
let next = offset;
|
|
2942
|
-
while (next < buffer.length && (buffer[next] & 192) === 128) next += 1;
|
|
2943
|
-
return next;
|
|
2944
|
-
}
|
|
2945
|
-
function completeUtf8PrefixLength(buffer) {
|
|
2946
|
-
if (buffer.length === 0) return 0;
|
|
2947
|
-
let start = buffer.length - 1;
|
|
2948
|
-
while (start > 0 && (buffer[start] & 192) === 128) start -= 1;
|
|
2949
|
-
const first = buffer[start];
|
|
2950
|
-
const expected = (first & 128) === 0 ? 1 : (first & 224) === 192 ? 2 : (first & 240) === 224 ? 3 : 4;
|
|
2951
|
-
return buffer.length - start < expected ? start : buffer.length;
|
|
2952
|
-
}
|
|
2953
|
-
function appendBoundedUtf8Bytes(current, chunk, maxBytes) {
|
|
2954
|
-
const combined = Buffer.concat([current, chunk]);
|
|
2955
|
-
if (combined.byteLength <= maxBytes) return combined;
|
|
2956
|
-
let start = combined.byteLength - maxBytes;
|
|
2957
|
-
while (start < combined.byteLength && (combined[start] & 192) === 128) start += 1;
|
|
2958
|
-
return combined.subarray(start);
|
|
2959
|
-
}
|
|
2960
|
-
async function closeAndRemoveEmptyLog(log, path, removeFile) {
|
|
2961
|
-
await log.close().catch(() => void 0);
|
|
2962
|
-
await removeFile(path).catch(() => void 0);
|
|
2963
|
-
}
|
|
2964
|
-
function boundedError(error, fallback) {
|
|
2965
|
-
return (error instanceof Error && error.message ? error.message : fallback).slice(0, 1e3);
|
|
192
|
+
function openBrowser(url) {
|
|
193
|
+
const command = platform() === "darwin" ? "open" : platform() === "win32" ? "cmd" : "xdg-open";
|
|
194
|
+
const args = platform() === "win32" ? [
|
|
195
|
+
"/c",
|
|
196
|
+
"start",
|
|
197
|
+
"",
|
|
198
|
+
url
|
|
199
|
+
] : [url];
|
|
200
|
+
try {
|
|
201
|
+
const child = spawn(command, args, {
|
|
202
|
+
stdio: "ignore",
|
|
203
|
+
detached: true
|
|
204
|
+
});
|
|
205
|
+
child.once("error", () => void 0);
|
|
206
|
+
child.unref();
|
|
207
|
+
} catch {}
|
|
2966
208
|
}
|
|
2967
|
-
function
|
|
209
|
+
async function readInstalledBinding(path, deploymentId) {
|
|
210
|
+
const value = JSON.parse(await readFile(path, "utf8"));
|
|
211
|
+
if (value.deploymentId !== deploymentId || typeof value.connectorInstallId !== "string" || typeof value.challenge !== "string" || typeof value.machineId !== "string") throw Object.assign(/* @__PURE__ */ new Error("Installed connector binding is invalid."), { code: "ownership_conflict" });
|
|
2968
212
|
return {
|
|
2969
|
-
|
|
2970
|
-
|
|
2971
|
-
|
|
2972
|
-
};
|
|
213
|
+
connectorInstallId: value.connectorInstallId,
|
|
214
|
+
challenge: value.challenge,
|
|
215
|
+
machineId: value.machineId
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
async function adoptInstalledConnectorBinding(input) {
|
|
219
|
+
const binding = await readInstalledBinding(input.entry.configPath, input.deployment.deploymentId);
|
|
220
|
+
return input.store.adoptPendingDeploymentBinding(input.deployment, {
|
|
221
|
+
connectorInstallId: binding.connectorInstallId,
|
|
222
|
+
challenge: binding.challenge,
|
|
223
|
+
machineId: binding.machineId,
|
|
224
|
+
allowInsecureTls: input.allowInsecureTls
|
|
225
|
+
});
|
|
2973
226
|
}
|
|
2974
|
-
|
|
2975
|
-
|
|
2976
|
-
|
|
2977
|
-
|
|
2978
|
-
|
|
2979
|
-
|
|
2980
|
-
|
|
2981
|
-
|
|
2982
|
-
|
|
2983
|
-
|
|
2984
|
-
|
|
2985
|
-
authFailureRetried = false;
|
|
2986
|
-
mcpGenerations = /* @__PURE__ */ new Set();
|
|
2987
|
-
constructor(options) {
|
|
2988
|
-
this.options = options;
|
|
2989
|
-
this.jobManager = options.jobManager ?? new JobManager();
|
|
2990
|
-
this.terminalManagerPromise = options.terminalManager ? Promise.resolve(options.terminalManager) : TerminalManager.create();
|
|
2991
|
-
}
|
|
2992
|
-
start() {
|
|
2993
|
-
this.stopped = false;
|
|
2994
|
-
this.authFailureRetried = false;
|
|
2995
|
-
this.connect();
|
|
2996
|
-
}
|
|
2997
|
-
async stop(force = false) {
|
|
2998
|
-
this.stopped = true;
|
|
2999
|
-
if (this.reconnectTimer) clearTimeout(this.reconnectTimer);
|
|
3000
|
-
if (this.stableTimer) clearTimeout(this.stableTimer);
|
|
3001
|
-
this.reconnectTimer = void 0;
|
|
3002
|
-
this.stableTimer = void 0;
|
|
3003
|
-
this.shellManager.closeAll();
|
|
3004
|
-
this.stopPromise ??= this.stopOwnedResources();
|
|
3005
|
-
if (!force) return this.stopPromise;
|
|
3006
|
-
const forceResults = await Promise.allSettled([...this.mcpGenerations].map((generation) => generation.close(true)));
|
|
3007
|
-
const cleanupResults = await Promise.allSettled([this.stopPromise]);
|
|
3008
|
-
throwCleanupFailures([...forceResults, ...cleanupResults]);
|
|
3009
|
-
}
|
|
3010
|
-
async stopOwnedResources() {
|
|
3011
|
-
const cleanup = [
|
|
3012
|
-
this.jobManager.stop(),
|
|
3013
|
-
this.terminalManagerPromise.then((manager) => manager.closeAll()),
|
|
3014
|
-
...[...this.mcpGenerations].map((generation) => generation.close())
|
|
3015
|
-
];
|
|
3016
|
-
const failures = [];
|
|
3017
|
-
try {
|
|
3018
|
-
disposeMachineFileSearchIndexes();
|
|
3019
|
-
} catch (error) {
|
|
3020
|
-
failures.push(error);
|
|
3021
|
-
}
|
|
3022
|
-
try {
|
|
3023
|
-
this.socket?.close();
|
|
3024
|
-
} catch (error) {
|
|
3025
|
-
failures.push(error);
|
|
3026
|
-
}
|
|
3027
|
-
throwCleanupFailures(await Promise.allSettled(cleanup), failures);
|
|
3028
|
-
}
|
|
3029
|
-
async connect(options = {}) {
|
|
3030
|
-
const terminalManager = await this.terminalManagerPromise;
|
|
3031
|
-
let accessToken;
|
|
3032
|
-
try {
|
|
3033
|
-
accessToken = await this.options.accessTokenProvider(options);
|
|
3034
|
-
} catch (error) {
|
|
3035
|
-
const message = error instanceof Error ? error.message : "Failed to refresh connector access token.";
|
|
3036
|
-
console.error(message.includes("Run agents login") ? message : `${message} Run agents login.`);
|
|
3037
|
-
await this.stop();
|
|
3038
|
-
return;
|
|
3039
|
-
}
|
|
3040
|
-
if (this.stopped) return;
|
|
3041
|
-
allowInsecureTlsForLocalTesting(this.options);
|
|
3042
|
-
const socket = new WebSocket(toWebSocketUrl(this.options.serverUrl), {
|
|
3043
|
-
maxPayload: CONNECTOR_MAX_WEBSOCKET_PAYLOAD_BYTES,
|
|
3044
|
-
headers: { authorization: `Bearer ${accessToken}` },
|
|
3045
|
-
...this.options.allowInsecureTls ? { rejectUnauthorized: false } : {}
|
|
3046
|
-
});
|
|
3047
|
-
const mcpGeneration = new ConnectorMcpGeneration(socket);
|
|
3048
|
-
this.mcpGenerations.add(mcpGeneration);
|
|
3049
|
-
terminalManager.setEventHandler((event) => this.sendTerminalEvent(socket, terminalManager, event));
|
|
3050
|
-
this.socket = socket;
|
|
3051
|
-
socket.on("open", () => {
|
|
3052
|
-
this.stableTimer = setTimeout(() => {
|
|
3053
|
-
this.reconnectAttempt = 0;
|
|
3054
|
-
this.authFailureRetried = false;
|
|
3055
|
-
this.stableTimer = void 0;
|
|
3056
|
-
}, stableConnectionMs());
|
|
3057
|
-
socket.send(JSON.stringify(this.createHello(terminalManager)));
|
|
3058
|
-
});
|
|
3059
|
-
socket.on("message", (data) => {
|
|
3060
|
-
if (mcpGeneration.handles(data)) {
|
|
3061
|
-
mcpGeneration.accept(data);
|
|
3062
|
-
return;
|
|
3063
|
-
}
|
|
3064
|
-
this.handleMessage(socket, data.toString());
|
|
3065
|
-
});
|
|
3066
|
-
socket.on("close", (code, reason) => {
|
|
3067
|
-
if (this.stableTimer) clearTimeout(this.stableTimer);
|
|
3068
|
-
this.stableTimer = void 0;
|
|
3069
|
-
this.shellManager.closeAll();
|
|
3070
|
-
terminalManager.closeAll();
|
|
3071
|
-
mcpGeneration.close().finally(() => this.mcpGenerations.delete(mcpGeneration));
|
|
3072
|
-
if (code === 1008) {
|
|
3073
|
-
if (!this.authFailureRetried && this.options.reconnect !== false && !this.stopped) {
|
|
3074
|
-
this.authFailureRetried = true;
|
|
3075
|
-
this.connect({ forceRefresh: true });
|
|
3076
|
-
return;
|
|
3077
|
-
}
|
|
3078
|
-
const message = reason.toString() || "connector authentication failed";
|
|
3079
|
-
console.error(`Connector stopped: ${message}`);
|
|
3080
|
-
this.stop().catch((error) => {
|
|
3081
|
-
console.error(error instanceof Error ? error.message : "Connector cleanup failed after auth failure.");
|
|
3082
|
-
});
|
|
3083
|
-
return;
|
|
3084
|
-
}
|
|
3085
|
-
this.scheduleReconnect();
|
|
3086
|
-
});
|
|
3087
|
-
socket.on("error", (error) => {
|
|
3088
|
-
console.error("Connector websocket error:", error.message);
|
|
3089
|
-
});
|
|
3090
|
-
}
|
|
3091
|
-
async handleMessage(socket, raw) {
|
|
3092
|
-
const value = parseJson(raw);
|
|
3093
|
-
if (!isServerRpcRequest(value)) {
|
|
3094
|
-
this.send(socket, {
|
|
3095
|
-
type: "rpc/error",
|
|
3096
|
-
id: requestId(value),
|
|
3097
|
-
error: {
|
|
3098
|
-
code: "invalid_request",
|
|
3099
|
-
message: "Invalid connector RPC request."
|
|
3100
|
-
}
|
|
3101
|
-
});
|
|
3102
|
-
return;
|
|
3103
|
-
}
|
|
3104
|
-
if (value.method === "shell/exec") {
|
|
3105
|
-
const result = await this.shellManager.exec(value.params);
|
|
3106
|
-
this.send(socket, {
|
|
3107
|
-
type: "rpc/response",
|
|
3108
|
-
id: value.id,
|
|
3109
|
-
result
|
|
3110
|
-
});
|
|
3111
|
-
return;
|
|
3112
|
-
}
|
|
3113
|
-
if (value.method === "shell/job_start") {
|
|
3114
|
-
const result = await this.jobManager.start(value.params);
|
|
3115
|
-
this.send(socket, {
|
|
3116
|
-
type: "rpc/response",
|
|
3117
|
-
id: value.id,
|
|
3118
|
-
result
|
|
3119
|
-
});
|
|
3120
|
-
return;
|
|
3121
|
-
}
|
|
3122
|
-
if (value.method === "shell/job_get") {
|
|
3123
|
-
const result = this.jobManager.get(value.params);
|
|
3124
|
-
this.send(socket, {
|
|
3125
|
-
type: "rpc/response",
|
|
3126
|
-
id: value.id,
|
|
3127
|
-
result
|
|
3128
|
-
});
|
|
3129
|
-
return;
|
|
3130
|
-
}
|
|
3131
|
-
if (value.method === "shell/job_kill") {
|
|
3132
|
-
const result = await this.jobManager.kill(value.params);
|
|
3133
|
-
this.send(socket, {
|
|
3134
|
-
type: "rpc/response",
|
|
3135
|
-
id: value.id,
|
|
3136
|
-
result
|
|
3137
|
-
});
|
|
3138
|
-
return;
|
|
3139
|
-
}
|
|
3140
|
-
const terminalManager = await this.terminalManagerPromise;
|
|
3141
|
-
if (value.method === "terminal/open") {
|
|
3142
|
-
const result = await terminalManager.open(value.params);
|
|
3143
|
-
this.send(socket, {
|
|
3144
|
-
type: "rpc/response",
|
|
3145
|
-
id: value.id,
|
|
3146
|
-
result
|
|
3147
|
-
});
|
|
3148
|
-
return;
|
|
3149
|
-
}
|
|
3150
|
-
if (value.method === "terminal/write") {
|
|
3151
|
-
const result = terminalManager.write(value.params.terminalId, value.params.data);
|
|
3152
|
-
this.send(socket, {
|
|
3153
|
-
type: "rpc/response",
|
|
3154
|
-
id: value.id,
|
|
3155
|
-
result
|
|
3156
|
-
});
|
|
3157
|
-
return;
|
|
3158
|
-
}
|
|
3159
|
-
if (value.method === "terminal/resize") {
|
|
3160
|
-
const result = terminalManager.resize(value.params.terminalId, value.params.cols, value.params.rows);
|
|
3161
|
-
this.send(socket, {
|
|
3162
|
-
type: "rpc/response",
|
|
3163
|
-
id: value.id,
|
|
3164
|
-
result
|
|
3165
|
-
});
|
|
3166
|
-
return;
|
|
3167
|
-
}
|
|
3168
|
-
if (value.method === "terminal/clear") {
|
|
3169
|
-
const result = terminalManager.clear(value.params.terminalId);
|
|
3170
|
-
this.send(socket, {
|
|
3171
|
-
type: "rpc/response",
|
|
3172
|
-
id: value.id,
|
|
3173
|
-
result
|
|
3174
|
-
});
|
|
3175
|
-
return;
|
|
3176
|
-
}
|
|
3177
|
-
if (value.method === "terminal/close") {
|
|
3178
|
-
const result = terminalManager.close(value.params.terminalId);
|
|
3179
|
-
this.send(socket, {
|
|
3180
|
-
type: "rpc/response",
|
|
3181
|
-
id: value.id,
|
|
3182
|
-
result
|
|
3183
|
-
});
|
|
3184
|
-
return;
|
|
3185
|
-
}
|
|
3186
|
-
if (value.method === "file/read") {
|
|
3187
|
-
const result = await readFileForRpc(value.params);
|
|
3188
|
-
this.send(socket, {
|
|
3189
|
-
type: "rpc/response",
|
|
3190
|
-
id: value.id,
|
|
3191
|
-
result
|
|
3192
|
-
});
|
|
3193
|
-
return;
|
|
3194
|
-
}
|
|
3195
|
-
if (value.method === "file/read_text") {
|
|
3196
|
-
const result = await readTextFileForRpc(value.params);
|
|
3197
|
-
this.send(socket, {
|
|
3198
|
-
type: "rpc/response",
|
|
3199
|
-
id: value.id,
|
|
3200
|
-
result
|
|
3201
|
-
});
|
|
3202
|
-
return;
|
|
3203
|
-
}
|
|
3204
|
-
if (value.method === "file/read_patch_source") {
|
|
3205
|
-
const result = await readPatchSourceForRpc(value.params);
|
|
3206
|
-
this.send(socket, {
|
|
3207
|
-
type: "rpc/response",
|
|
3208
|
-
id: value.id,
|
|
3209
|
-
result
|
|
3210
|
-
});
|
|
3211
|
-
return;
|
|
3212
|
-
}
|
|
3213
|
-
if (value.method === "file/write") {
|
|
3214
|
-
const result = await writeFileForRpc(value.params);
|
|
3215
|
-
this.send(socket, {
|
|
3216
|
-
type: "rpc/response",
|
|
3217
|
-
id: value.id,
|
|
3218
|
-
result
|
|
3219
|
-
});
|
|
3220
|
-
return;
|
|
3221
|
-
}
|
|
3222
|
-
if (value.method === "file/delete") {
|
|
3223
|
-
const result = await deleteFileForRpc(value.params);
|
|
3224
|
-
this.send(socket, {
|
|
3225
|
-
type: "rpc/response",
|
|
3226
|
-
id: value.id,
|
|
3227
|
-
result
|
|
3228
|
-
});
|
|
3229
|
-
return;
|
|
3230
|
-
}
|
|
3231
|
-
if (value.method === "file/move") {
|
|
3232
|
-
const result = await moveFileForRpc(value.params);
|
|
3233
|
-
this.send(socket, {
|
|
3234
|
-
type: "rpc/response",
|
|
3235
|
-
id: value.id,
|
|
3236
|
-
result
|
|
3237
|
-
});
|
|
3238
|
-
return;
|
|
3239
|
-
}
|
|
3240
|
-
if (value.method === "file/search_entries") {
|
|
3241
|
-
const result = await searchFileEntriesForRpc(value.params);
|
|
3242
|
-
this.send(socket, {
|
|
3243
|
-
type: "rpc/response",
|
|
3244
|
-
id: value.id,
|
|
3245
|
-
result
|
|
3246
|
-
});
|
|
3247
|
-
return;
|
|
3248
|
-
}
|
|
3249
|
-
if (value.method === "shell/close") {
|
|
3250
|
-
this.shellManager.close(value.params.shellId);
|
|
3251
|
-
this.send(socket, {
|
|
3252
|
-
type: "rpc/response",
|
|
3253
|
-
id: value.id,
|
|
3254
|
-
result: { ok: true }
|
|
3255
|
-
});
|
|
3256
|
-
return;
|
|
3257
|
-
}
|
|
3258
|
-
this.send(socket, {
|
|
3259
|
-
type: "rpc/error",
|
|
3260
|
-
id: value.id,
|
|
3261
|
-
error: {
|
|
3262
|
-
code: "unknown_method",
|
|
3263
|
-
message: `Unsupported RPC method: ${value.method}`
|
|
3264
|
-
}
|
|
227
|
+
async function restartInstalledConnector(deploymentId, serviceKey) {
|
|
228
|
+
const servicePlatform = platform();
|
|
229
|
+
if (servicePlatform !== "darwin" && servicePlatform !== "linux" && servicePlatform !== "win32") throw new Error(`Unsupported service platform: ${servicePlatform}.`);
|
|
230
|
+
await installConnectorLauncherService({
|
|
231
|
+
platform: servicePlatform,
|
|
232
|
+
context: resolveConnectorProfileContext(serviceKey, servicePlatform)
|
|
233
|
+
});
|
|
234
|
+
for (let attempt = 0; attempt < 50; attempt += 1) {
|
|
235
|
+
const local = await discoverLocalConnector({
|
|
236
|
+
deploymentId,
|
|
237
|
+
serviceKey
|
|
3265
238
|
});
|
|
239
|
+
if (local.state === "ready" || local.state === "ownership_conflict") return local;
|
|
240
|
+
await setTimeout$1(100);
|
|
3266
241
|
}
|
|
3267
|
-
|
|
3268
|
-
|
|
3269
|
-
|
|
3270
|
-
|
|
3271
|
-
|
|
3272
|
-
|
|
3273
|
-
|
|
3274
|
-
|
|
3275
|
-
|
|
3276
|
-
|
|
3277
|
-
|
|
3278
|
-
|
|
3279
|
-
|
|
3280
|
-
|
|
3281
|
-
|
|
3282
|
-
|
|
3283
|
-
|
|
3284
|
-
|
|
3285
|
-
|
|
3286
|
-
|
|
3287
|
-
|
|
3288
|
-
|
|
3289
|
-
|
|
3290
|
-
|
|
3291
|
-
|
|
3292
|
-
|
|
3293
|
-
|
|
3294
|
-
|
|
3295
|
-
|
|
3296
|
-
|
|
3297
|
-
|
|
3298
|
-
|
|
3299
|
-
|
|
3300
|
-
|
|
3301
|
-
|
|
3302
|
-
|
|
3303
|
-
|
|
242
|
+
return discoverLocalConnector({
|
|
243
|
+
deploymentId,
|
|
244
|
+
serviceKey
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
//#endregion
|
|
248
|
+
//#region package.json
|
|
249
|
+
var version = "0.1.2";
|
|
250
|
+
//#endregion
|
|
251
|
+
//#region src/cli-update-notice.ts
|
|
252
|
+
const CLI_PACKAGE_NAME = "@sipher.dev/agents";
|
|
253
|
+
const CLI_PACKAGE_URL = "https://registry.npmjs.org/%40sipherxyz%2Fagents/latest";
|
|
254
|
+
const DEFAULT_CACHE_MS = 1440 * 60 * 1e3;
|
|
255
|
+
const DEFAULT_TIMEOUT_MS = 750;
|
|
256
|
+
async function withCliUpdateNotice(command, options) {
|
|
257
|
+
if (options.enabled !== false) maybePrintCliUpdateNotice(options).catch(() => void 0);
|
|
258
|
+
return command();
|
|
259
|
+
}
|
|
260
|
+
async function maybePrintCliUpdateNotice(options) {
|
|
261
|
+
const now = options.now ?? Date.now;
|
|
262
|
+
const cachePath = options.cachePath ?? defaultCliUpdateCachePath();
|
|
263
|
+
const cached = await readCache(cachePath);
|
|
264
|
+
let latestVersion = cached?.latestVersion;
|
|
265
|
+
if (!cached || now() - cached.checkedAt >= (options.cacheMs ?? DEFAULT_CACHE_MS)) {
|
|
266
|
+
const fresh = await lookupLatestVersion({
|
|
267
|
+
fetch: options.fetch ?? fetch,
|
|
268
|
+
timeoutMs: options.timeoutMs ?? DEFAULT_TIMEOUT_MS
|
|
269
|
+
}).catch(() => void 0);
|
|
270
|
+
if (fresh) {
|
|
271
|
+
latestVersion = fresh;
|
|
272
|
+
await writeCache(cachePath, {
|
|
273
|
+
checkedAt: now(),
|
|
274
|
+
latestVersion: fresh
|
|
275
|
+
}).catch(() => void 0);
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
if (latestVersion && compareSemver(latestVersion, options.currentVersion) > 0) (options.print ?? console.error)(`Update available for ${CLI_PACKAGE_NAME}: ${options.currentVersion} → ${latestVersion}. Run your package manager to update the CLI.`);
|
|
279
|
+
}
|
|
280
|
+
async function lookupLatestVersion(options) {
|
|
281
|
+
const controller = new AbortController();
|
|
282
|
+
const timer = setTimeout(() => controller.abort(), options.timeoutMs);
|
|
283
|
+
timer.unref();
|
|
284
|
+
try {
|
|
285
|
+
const response = await options.fetch(CLI_PACKAGE_URL, {
|
|
286
|
+
redirect: "error",
|
|
287
|
+
signal: controller.signal,
|
|
288
|
+
headers: { accept: "application/json" }
|
|
289
|
+
});
|
|
290
|
+
if (!response.ok) throw new Error(`npm returned HTTP ${response.status}.`);
|
|
291
|
+
const value = await response.json();
|
|
292
|
+
if (typeof value.version !== "string") throw new Error("npm metadata has no version.");
|
|
293
|
+
parseSemver(value.version);
|
|
294
|
+
return value.version;
|
|
295
|
+
} finally {
|
|
296
|
+
clearTimeout(timer);
|
|
3304
297
|
}
|
|
3305
|
-
};
|
|
3306
|
-
function toWebSocketUrl(value) {
|
|
3307
|
-
const url = new URL(value);
|
|
3308
|
-
if (url.protocol === "http:") url.protocol = "ws:";
|
|
3309
|
-
if (url.protocol === "https:") url.protocol = "wss:";
|
|
3310
|
-
if (url.pathname === "/" || url.pathname === "") url.pathname = "/api/connectors/ws";
|
|
3311
|
-
return url;
|
|
3312
298
|
}
|
|
3313
|
-
function
|
|
299
|
+
async function readCache(path) {
|
|
3314
300
|
try {
|
|
3315
|
-
|
|
301
|
+
const value = JSON.parse(await readFile(path, "utf8"));
|
|
302
|
+
if (!Number.isSafeInteger(value.checkedAt) || (value.checkedAt ?? -1) < 0 || typeof value.latestVersion !== "string") return;
|
|
303
|
+
parseSemver(value.latestVersion);
|
|
304
|
+
return value;
|
|
3316
305
|
} catch {
|
|
3317
306
|
return;
|
|
3318
307
|
}
|
|
3319
308
|
}
|
|
3320
|
-
function
|
|
3321
|
-
|
|
3322
|
-
|
|
3323
|
-
|
|
3324
|
-
function throwCleanupFailures(results, failures = []) {
|
|
3325
|
-
for (const result of results) if (result.status === "rejected") failures.push(result.reason);
|
|
3326
|
-
if (failures.length === 1) throw failures[0];
|
|
3327
|
-
if (failures.length > 1) throw new AggregateError(failures, "Connector cleanup failed.");
|
|
3328
|
-
}
|
|
3329
|
-
function reconnectDelayMs(attempt) {
|
|
3330
|
-
const baseMs = 1e3 * 2 ** (Math.max(1, Math.min(attempt, 6)) - 1);
|
|
3331
|
-
return Math.min(baseMs, 3e4);
|
|
3332
|
-
}
|
|
3333
|
-
function nextReconnectAttempt(currentAttempt, wasStable) {
|
|
3334
|
-
if (wasStable) return 1;
|
|
3335
|
-
return currentAttempt + 1;
|
|
3336
|
-
}
|
|
3337
|
-
function stableConnectionMs() {
|
|
3338
|
-
return 1e4;
|
|
3339
|
-
}
|
|
3340
|
-
//#endregion
|
|
3341
|
-
//#region src/connector-daemon.ts
|
|
3342
|
-
async function runConnector(options) {
|
|
3343
|
-
await installShellEnvironment();
|
|
3344
|
-
await setupConnectorProfile({
|
|
3345
|
-
serverUrl: options.serverUrl,
|
|
3346
|
-
store: options.store,
|
|
3347
|
-
name: options.name,
|
|
3348
|
-
installService: false,
|
|
3349
|
-
allowInsecureTls: options.allowInsecureTls
|
|
3350
|
-
});
|
|
3351
|
-
const profile = await options.store.getProfile(options.serverUrl);
|
|
3352
|
-
const allowInsecureTls = options.allowInsecureTls ?? profile?.allowInsecureTls;
|
|
3353
|
-
const client = (options.createClient ?? ((input) => new ConnectorClient(input)))({
|
|
3354
|
-
serverUrl: options.serverUrl,
|
|
3355
|
-
allowInsecureTls,
|
|
3356
|
-
accessTokenProvider: createAccessTokenProvider({
|
|
3357
|
-
serverUrl: options.serverUrl,
|
|
3358
|
-
store: options.store,
|
|
3359
|
-
allowInsecureTls
|
|
3360
|
-
})
|
|
309
|
+
async function writeCache(path, value) {
|
|
310
|
+
await mkdir(dirname(path), {
|
|
311
|
+
recursive: true,
|
|
312
|
+
mode: 448
|
|
3361
313
|
});
|
|
3362
|
-
|
|
3363
|
-
|
|
3364
|
-
|
|
314
|
+
const temporary = `${path}.${process.pid}.${randomUUID()}.tmp`;
|
|
315
|
+
try {
|
|
316
|
+
await writeFile(temporary, `${JSON.stringify(value)}\n`, { mode: 384 });
|
|
317
|
+
await rename(temporary, path);
|
|
318
|
+
} finally {
|
|
319
|
+
await rm(temporary, { force: true }).catch(() => void 0);
|
|
320
|
+
}
|
|
3365
321
|
}
|
|
3366
|
-
function
|
|
3367
|
-
|
|
3368
|
-
process.
|
|
3369
|
-
process.on("SIGTERM", stop);
|
|
322
|
+
function defaultCliUpdateCachePath() {
|
|
323
|
+
if (process.platform === "win32") return join(process.env.LOCALAPPDATA ?? join(homedir(), "AppData", "Local"), "agents", "cli-update.json");
|
|
324
|
+
return join(process.env.XDG_CACHE_HOME ?? join(homedir(), ".cache"), "agents", "cli-update.json");
|
|
3370
325
|
}
|
|
3371
|
-
function
|
|
3372
|
-
|
|
3373
|
-
|
|
3374
|
-
let
|
|
3375
|
-
|
|
3376
|
-
|
|
3377
|
-
|
|
3378
|
-
|
|
3379
|
-
|
|
3380
|
-
exit(code);
|
|
3381
|
-
};
|
|
3382
|
-
const stop = () => {
|
|
3383
|
-
if (stopping) {
|
|
3384
|
-
if (forceTimer) clearTimeout(forceTimer);
|
|
3385
|
-
forceStop(1);
|
|
3386
|
-
return;
|
|
3387
|
-
}
|
|
3388
|
-
stopping = true;
|
|
3389
|
-
const graceful = Promise.resolve(client.stop());
|
|
3390
|
-
forceTimer = setTimeout(() => {
|
|
3391
|
-
forceStop(0);
|
|
3392
|
-
}, forceAfterMs);
|
|
3393
|
-
forceTimer.unref();
|
|
3394
|
-
graceful.then(() => {
|
|
3395
|
-
if (!forcing) finish(0);
|
|
3396
|
-
}, () => {
|
|
3397
|
-
if (!forcing) finish(1);
|
|
3398
|
-
});
|
|
3399
|
-
};
|
|
3400
|
-
const forceStop = async (code) => {
|
|
3401
|
-
forcing = true;
|
|
3402
|
-
try {
|
|
3403
|
-
await client.stop(true);
|
|
3404
|
-
finish(code);
|
|
3405
|
-
} catch {
|
|
3406
|
-
finish(1);
|
|
3407
|
-
}
|
|
3408
|
-
};
|
|
3409
|
-
return stop;
|
|
326
|
+
function compareSemver(left, right) {
|
|
327
|
+
const a = parseSemver(left);
|
|
328
|
+
const b = parseSemver(right);
|
|
329
|
+
for (let index = 0; index < 3; index += 1) if (a[index] !== b[index]) return a[index] > b[index] ? 1 : -1;
|
|
330
|
+
return 0;
|
|
331
|
+
}
|
|
332
|
+
function parseSemver(value) {
|
|
333
|
+
if (!/^\d+\.\d+\.\d+$/u.test(value)) throw new Error(`Invalid semantic version: ${value}.`);
|
|
334
|
+
return value.split(".").map(Number);
|
|
3410
335
|
}
|
|
3411
336
|
//#endregion
|
|
3412
337
|
//#region src/index.ts
|
|
@@ -3416,11 +341,16 @@ main().catch((error) => {
|
|
|
3416
341
|
});
|
|
3417
342
|
async function main() {
|
|
3418
343
|
const args = parseArgs(process.argv.slice(2));
|
|
3419
|
-
const store = new TokenStore(
|
|
344
|
+
const store = new TokenStore(defaultConnectorAuthStorePath());
|
|
345
|
+
await withCliUpdateNotice(() => runCommand(args, store), {
|
|
346
|
+
currentVersion: version,
|
|
347
|
+
enabled: true
|
|
348
|
+
});
|
|
349
|
+
}
|
|
350
|
+
async function runCommand(args, store) {
|
|
3420
351
|
if (args.command === "login") {
|
|
3421
352
|
await login({
|
|
3422
353
|
serverUrl: args.serverUrl,
|
|
3423
|
-
profileName: args.profileName,
|
|
3424
354
|
name: args.name,
|
|
3425
355
|
store,
|
|
3426
356
|
allowInsecureTls: args.allowInsecureTls
|
|
@@ -3445,51 +375,40 @@ async function main() {
|
|
|
3445
375
|
}
|
|
3446
376
|
if (args.serviceCommand) {
|
|
3447
377
|
if (args.serviceCommand === "install") {
|
|
3448
|
-
await
|
|
378
|
+
const repaired = await repairConnectorProfile({
|
|
3449
379
|
serverUrl: await store.resolveServerUrl(args.serverUrl),
|
|
3450
380
|
store,
|
|
3451
381
|
name: args.name,
|
|
3452
|
-
installService: true,
|
|
3453
|
-
serviceInstaller: (input) => runServiceCommand("install", {
|
|
3454
|
-
serverUrl: input.serverUrl,
|
|
3455
|
-
...args.profileName ? { profileName: args.profileName } : {},
|
|
3456
|
-
...input.allowInsecureTls ? { allowInsecureTls: true } : {}
|
|
3457
|
-
}),
|
|
3458
382
|
allowInsecureTls: args.allowInsecureTls
|
|
3459
383
|
});
|
|
384
|
+
await runServiceCommand("install", {
|
|
385
|
+
serverUrl: repaired.serverUrl,
|
|
386
|
+
deploymentId: repaired.profile.deploymentId,
|
|
387
|
+
serviceKey: repaired.profile.serviceKey,
|
|
388
|
+
...repaired.profile.allowInsecureTls ? { allowInsecureTls: true } : {}
|
|
389
|
+
});
|
|
3460
390
|
return;
|
|
3461
391
|
}
|
|
3462
392
|
if (args.serviceCommand === "repair") {
|
|
3463
|
-
const
|
|
3464
|
-
|
|
3465
|
-
|
|
3466
|
-
|
|
393
|
+
const repaired = await repairConnectorProfile({
|
|
394
|
+
serverUrl: await store.resolveServerUrl(args.serverUrl),
|
|
395
|
+
store,
|
|
396
|
+
name: args.name,
|
|
397
|
+
allowInsecureTls: args.allowInsecureTls
|
|
398
|
+
});
|
|
3467
399
|
await runServiceCommand("repair", {
|
|
3468
|
-
serverUrl,
|
|
3469
|
-
|
|
3470
|
-
|
|
400
|
+
serverUrl: repaired.serverUrl,
|
|
401
|
+
deploymentId: repaired.profile.deploymentId,
|
|
402
|
+
serviceKey: repaired.profile.serviceKey,
|
|
403
|
+
...repaired.profile.allowInsecureTls ? { allowInsecureTls: true } : {}
|
|
3471
404
|
});
|
|
3472
405
|
return;
|
|
3473
406
|
}
|
|
3474
|
-
|
|
3475
|
-
|
|
3476
|
-
|
|
3477
|
-
if (args.configPath) {
|
|
3478
|
-
const config = await loadServiceConfig(args.configPath);
|
|
3479
|
-
await runConnector({
|
|
3480
|
-
serverUrl: config.serverUrl,
|
|
3481
|
-
store: new TokenStore(config.authStorePath),
|
|
3482
|
-
allowInsecureTls: config.allowInsecureTls
|
|
3483
|
-
});
|
|
3484
|
-
return;
|
|
3485
|
-
}
|
|
3486
|
-
if (args.command === "connect") {
|
|
3487
|
-
await runConnector({
|
|
3488
|
-
serverUrl: await store.resolveServerUrl(args.serverUrl),
|
|
3489
|
-
store,
|
|
3490
|
-
name: args.name,
|
|
3491
|
-
allowInsecureTls: args.allowInsecureTls
|
|
407
|
+
const config = await serviceLocalConfigInput({
|
|
408
|
+
serverUrl: args.serverUrl,
|
|
409
|
+
store
|
|
3492
410
|
});
|
|
411
|
+
await runServiceCommand(args.serviceCommand, config);
|
|
3493
412
|
return;
|
|
3494
413
|
}
|
|
3495
414
|
throw new Error("Run agents login to set up this Machine.");
|