visual-remote 0.3.0 → 0.3.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/README.md +63 -8
- package/apps/cli/dist/direct-exec-mcp.js +976 -0
- package/apps/cli/dist/index.js +1353 -254
- package/apps/cli/dist/next.js +3616 -2683
- package/apps/cli/dist/vite.js +5115 -4187
- package/package.json +7 -3
- package/packages/overlay/dist/client.js +115 -7
- package/packages/overlay/dist/viewer.js +4 -4
|
@@ -0,0 +1,976 @@
|
|
|
1
|
+
// src/direct-exec-mcp.ts
|
|
2
|
+
import { resolve as resolve2 } from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
|
|
5
|
+
// ../../packages/bridge-core/src/agents/direct-exec.ts
|
|
6
|
+
import { spawn } from "node:child_process";
|
|
7
|
+
import { realpath } from "node:fs/promises";
|
|
8
|
+
import { isAbsolute, relative, resolve, sep } from "node:path";
|
|
9
|
+
|
|
10
|
+
// ../../packages/bridge-core/src/runtime/managed-process.ts
|
|
11
|
+
function safeChildProcessId(child) {
|
|
12
|
+
const pid = child.pid;
|
|
13
|
+
if (pid === void 0 || !Number.isSafeInteger(pid) || pid <= 1 || pid === process.pid) {
|
|
14
|
+
return void 0;
|
|
15
|
+
}
|
|
16
|
+
return pid;
|
|
17
|
+
}
|
|
18
|
+
function safeDetachedProcessGroupId(child) {
|
|
19
|
+
return process.platform === "win32" ? void 0 : safeChildProcessId(child);
|
|
20
|
+
}
|
|
21
|
+
function isMissingProcess(error2) {
|
|
22
|
+
return typeof error2 === "object" && error2 !== null && "code" in error2 && error2.code === "ESRCH";
|
|
23
|
+
}
|
|
24
|
+
function processGroupIsAlive(processGroupId) {
|
|
25
|
+
try {
|
|
26
|
+
process.kill(-processGroupId, 0);
|
|
27
|
+
return true;
|
|
28
|
+
} catch (error2) {
|
|
29
|
+
if (isMissingProcess(error2)) return false;
|
|
30
|
+
if (typeof error2 === "object" && error2 !== null && "code" in error2 && error2.code === "EPERM") {
|
|
31
|
+
return true;
|
|
32
|
+
}
|
|
33
|
+
throw error2;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
function childIsAlive(child) {
|
|
37
|
+
return child.exitCode === null && child.signalCode === null;
|
|
38
|
+
}
|
|
39
|
+
function signalChildProcessTree(child, signal) {
|
|
40
|
+
const pid = safeChildProcessId(child);
|
|
41
|
+
if (pid === void 0) return false;
|
|
42
|
+
try {
|
|
43
|
+
if (process.platform === "win32") {
|
|
44
|
+
if (!childIsAlive(child)) return false;
|
|
45
|
+
return child.kill(signal);
|
|
46
|
+
}
|
|
47
|
+
process.kill(-pid, signal);
|
|
48
|
+
return true;
|
|
49
|
+
} catch (error2) {
|
|
50
|
+
if (isMissingProcess(error2)) return false;
|
|
51
|
+
throw error2;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
function installEmergencyChildExitHook(child, processLike = process) {
|
|
55
|
+
let removed = false;
|
|
56
|
+
const emergencyExit = () => {
|
|
57
|
+
removed = true;
|
|
58
|
+
try {
|
|
59
|
+
signalChildProcessTree(child, "SIGKILL");
|
|
60
|
+
} catch {
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
processLike.once("exit", emergencyExit);
|
|
64
|
+
return () => {
|
|
65
|
+
if (removed) return;
|
|
66
|
+
removed = true;
|
|
67
|
+
processLike.off("exit", emergencyExit);
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
async function waitForProcessTreeExit(child, processGroupId, timeoutMs) {
|
|
71
|
+
const deadline = Date.now() + timeoutMs;
|
|
72
|
+
while (processGroupId === void 0 ? childIsAlive(child) : processGroupIsAlive(processGroupId)) {
|
|
73
|
+
const remaining = deadline - Date.now();
|
|
74
|
+
if (remaining <= 0) return false;
|
|
75
|
+
await new Promise((resolve3) => {
|
|
76
|
+
setTimeout(resolve3, Math.min(25, remaining));
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
return true;
|
|
80
|
+
}
|
|
81
|
+
async function terminateChildProcessTree(child, killGraceMs = 3e3) {
|
|
82
|
+
const processGroupId = safeDetachedProcessGroupId(child);
|
|
83
|
+
if (processGroupId === void 0 && !childIsAlive(child)) return;
|
|
84
|
+
const sendSignal = (signal) => {
|
|
85
|
+
signalChildProcessTree(child, signal);
|
|
86
|
+
};
|
|
87
|
+
sendSignal("SIGTERM");
|
|
88
|
+
if (await waitForProcessTreeExit(
|
|
89
|
+
child,
|
|
90
|
+
processGroupId,
|
|
91
|
+
Math.max(0, killGraceMs)
|
|
92
|
+
)) {
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
sendSignal("SIGKILL");
|
|
96
|
+
await waitForProcessTreeExit(
|
|
97
|
+
child,
|
|
98
|
+
processGroupId,
|
|
99
|
+
Math.min(Math.max(0, killGraceMs), 1e3)
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// ../../packages/bridge-core/src/agents/direct-exec.ts
|
|
104
|
+
var MAX_COMMANDS = 8;
|
|
105
|
+
var MAX_ARGUMENTS = 64;
|
|
106
|
+
var MAX_ARGUMENT_LENGTH = 4096;
|
|
107
|
+
var DEFAULT_TIMEOUT_MS = 3e4;
|
|
108
|
+
var MAX_TIMEOUT_MS = 12e4;
|
|
109
|
+
var DEFAULT_OUTPUT_BYTES = 64 * 1024;
|
|
110
|
+
var KILL_GRACE_MS = 250;
|
|
111
|
+
var READ_ONLY_GIT_COMMANDS = /* @__PURE__ */ new Set([
|
|
112
|
+
"describe",
|
|
113
|
+
"diff",
|
|
114
|
+
"grep",
|
|
115
|
+
"log",
|
|
116
|
+
"ls-files",
|
|
117
|
+
"rev-parse",
|
|
118
|
+
"show",
|
|
119
|
+
"status"
|
|
120
|
+
]);
|
|
121
|
+
var SAFE_NATIVE_PROGRAMS = /* @__PURE__ */ new Set([
|
|
122
|
+
"cat",
|
|
123
|
+
"find",
|
|
124
|
+
"git",
|
|
125
|
+
"ls",
|
|
126
|
+
"pwd",
|
|
127
|
+
"rg",
|
|
128
|
+
"wc"
|
|
129
|
+
]);
|
|
130
|
+
var VERSION_PROGRAMS = /* @__PURE__ */ new Set([
|
|
131
|
+
"codex",
|
|
132
|
+
"corepack",
|
|
133
|
+
"node",
|
|
134
|
+
"npm",
|
|
135
|
+
"pnpm",
|
|
136
|
+
"rtk",
|
|
137
|
+
"visual",
|
|
138
|
+
"visual-remote"
|
|
139
|
+
]);
|
|
140
|
+
var SAFE_RTK_COMMANDS = /* @__PURE__ */ new Set([
|
|
141
|
+
"deps",
|
|
142
|
+
"diff",
|
|
143
|
+
"find",
|
|
144
|
+
"git",
|
|
145
|
+
"ls",
|
|
146
|
+
"read",
|
|
147
|
+
"rg",
|
|
148
|
+
"wc"
|
|
149
|
+
]);
|
|
150
|
+
var FORBIDDEN_GIT_OPTIONS = [
|
|
151
|
+
"-C",
|
|
152
|
+
"-O",
|
|
153
|
+
"-c",
|
|
154
|
+
"--config-env",
|
|
155
|
+
"--ext-diff",
|
|
156
|
+
"--exec-path",
|
|
157
|
+
"--git-dir",
|
|
158
|
+
"--namespace",
|
|
159
|
+
"--no-index",
|
|
160
|
+
"--open-files-in-pager",
|
|
161
|
+
"--output",
|
|
162
|
+
"--pathspec-from-file",
|
|
163
|
+
"--show-signature",
|
|
164
|
+
"--textconv",
|
|
165
|
+
"--work-tree"
|
|
166
|
+
];
|
|
167
|
+
var SAFE_FIND_FLAGS = /* @__PURE__ */ new Set([
|
|
168
|
+
"-empty",
|
|
169
|
+
"-false",
|
|
170
|
+
"-mount",
|
|
171
|
+
"-print",
|
|
172
|
+
"-print0",
|
|
173
|
+
"-prune",
|
|
174
|
+
"-quit",
|
|
175
|
+
"-readable",
|
|
176
|
+
"-true",
|
|
177
|
+
"-xdev"
|
|
178
|
+
]);
|
|
179
|
+
var SAFE_FIND_VALUE_FLAGS = /* @__PURE__ */ new Set([
|
|
180
|
+
"-iname",
|
|
181
|
+
"-ipath",
|
|
182
|
+
"-maxdepth",
|
|
183
|
+
"-mindepth",
|
|
184
|
+
"-mmin",
|
|
185
|
+
"-mtime",
|
|
186
|
+
"-name",
|
|
187
|
+
"-path",
|
|
188
|
+
"-size",
|
|
189
|
+
"-type"
|
|
190
|
+
]);
|
|
191
|
+
var SAFE_FIND_OPERATORS = /* @__PURE__ */ new Set([
|
|
192
|
+
"!",
|
|
193
|
+
"(",
|
|
194
|
+
")",
|
|
195
|
+
",",
|
|
196
|
+
"-a",
|
|
197
|
+
"-and",
|
|
198
|
+
"-not",
|
|
199
|
+
"-o",
|
|
200
|
+
"-or"
|
|
201
|
+
]);
|
|
202
|
+
var DirectExecPolicyError = class extends Error {
|
|
203
|
+
constructor(message) {
|
|
204
|
+
super(message);
|
|
205
|
+
this.name = "DirectExecPolicyError";
|
|
206
|
+
}
|
|
207
|
+
};
|
|
208
|
+
function isWithin(root, candidate) {
|
|
209
|
+
const path = relative(root, candidate);
|
|
210
|
+
return path === "" || path !== ".." && !path.startsWith(`..${sep}`) && !isAbsolute(path);
|
|
211
|
+
}
|
|
212
|
+
function directFileTargets(argv) {
|
|
213
|
+
const [program, ...arguments_] = argv;
|
|
214
|
+
if (program === "cat" || program === "wc") {
|
|
215
|
+
const targets2 = [];
|
|
216
|
+
let positionalOnly2 = false;
|
|
217
|
+
for (const argument of arguments_) {
|
|
218
|
+
if (argument === "--" && !positionalOnly2) {
|
|
219
|
+
positionalOnly2 = true;
|
|
220
|
+
continue;
|
|
221
|
+
}
|
|
222
|
+
if (argument === "-") continue;
|
|
223
|
+
if (positionalOnly2 || !argument.startsWith("-")) targets2.push(argument);
|
|
224
|
+
}
|
|
225
|
+
return targets2;
|
|
226
|
+
}
|
|
227
|
+
if (program === "find") return findPathTargets(arguments_);
|
|
228
|
+
if (program === "rtk" && arguments_[0] === "find") {
|
|
229
|
+
return findPathTargets(arguments_.slice(1));
|
|
230
|
+
}
|
|
231
|
+
if (program !== "rtk" || !["read", "wc"].includes(arguments_[0] ?? "")) return [];
|
|
232
|
+
const subcommand = arguments_[0];
|
|
233
|
+
const values = subcommand === "read" ? /* @__PURE__ */ new Set(["-l", "--level", "-m", "--max-lines", "--tail-lines"]) : /* @__PURE__ */ new Set();
|
|
234
|
+
const targets = [];
|
|
235
|
+
let positionalOnly = false;
|
|
236
|
+
for (let index = 1; index < arguments_.length; index += 1) {
|
|
237
|
+
const argument = arguments_[index] ?? "";
|
|
238
|
+
if (argument === "--" && !positionalOnly) {
|
|
239
|
+
positionalOnly = true;
|
|
240
|
+
continue;
|
|
241
|
+
}
|
|
242
|
+
if (values.has(argument)) {
|
|
243
|
+
index += 1;
|
|
244
|
+
continue;
|
|
245
|
+
}
|
|
246
|
+
if (argument !== "-" && (positionalOnly || !argument.startsWith("-"))) {
|
|
247
|
+
targets.push(argument);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
return targets;
|
|
251
|
+
}
|
|
252
|
+
function findPathTargets(arguments_) {
|
|
253
|
+
const targets = [];
|
|
254
|
+
for (const argument of arguments_) {
|
|
255
|
+
if (argument.startsWith("-") || SAFE_FIND_OPERATORS.has(argument)) break;
|
|
256
|
+
targets.push(argument);
|
|
257
|
+
}
|
|
258
|
+
return targets;
|
|
259
|
+
}
|
|
260
|
+
async function validateDirectFileTargets(argv, cwd, repoRoot) {
|
|
261
|
+
for (const target of directFileTargets(argv)) {
|
|
262
|
+
let resolvedTarget;
|
|
263
|
+
try {
|
|
264
|
+
resolvedTarget = await realpath(resolve(cwd, target));
|
|
265
|
+
} catch {
|
|
266
|
+
throw new DirectExecPolicyError(
|
|
267
|
+
`Direct file reads require an existing worktree path: ${target}`
|
|
268
|
+
);
|
|
269
|
+
}
|
|
270
|
+
if (!isWithin(repoRoot, resolvedTarget)) {
|
|
271
|
+
throw new DirectExecPolicyError(
|
|
272
|
+
`Direct file reads cannot follow a symlink outside the worktree: ${target}`
|
|
273
|
+
);
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
async function validateExistingArgumentPaths(argv, cwd, repoRoot) {
|
|
278
|
+
for (const argument of argv.slice(1)) {
|
|
279
|
+
const candidates = [argument];
|
|
280
|
+
const equalsIndex = argument.indexOf("=");
|
|
281
|
+
if (equalsIndex > 0 && equalsIndex < argument.length - 1) {
|
|
282
|
+
candidates.push(argument.slice(equalsIndex + 1));
|
|
283
|
+
}
|
|
284
|
+
for (const candidate of candidates) {
|
|
285
|
+
if (!candidate || candidate === "-" || candidate === "--") continue;
|
|
286
|
+
try {
|
|
287
|
+
const resolved = await realpath(resolve(cwd, candidate));
|
|
288
|
+
if (!isWithin(repoRoot, resolved)) {
|
|
289
|
+
throw new DirectExecPolicyError(
|
|
290
|
+
`Command arguments cannot follow a path outside the registered worktree: ${candidate}`
|
|
291
|
+
);
|
|
292
|
+
}
|
|
293
|
+
} catch (error2) {
|
|
294
|
+
if (error2 instanceof DirectExecPolicyError) throw error2;
|
|
295
|
+
const code = error2.code;
|
|
296
|
+
if (code !== "ENOENT" && code !== "ENOTDIR" && code !== "EINVAL") throw error2;
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
function optionMatches(argument, option) {
|
|
302
|
+
return argument === option || argument.startsWith(`${option}=`) || /^-[^-]$/u.test(option) && argument.length > option.length && argument.startsWith(option);
|
|
303
|
+
}
|
|
304
|
+
function validateArguments(arguments_) {
|
|
305
|
+
if (arguments_.length > MAX_ARGUMENTS) {
|
|
306
|
+
throw new DirectExecPolicyError(`A command may contain at most ${MAX_ARGUMENTS} arguments`);
|
|
307
|
+
}
|
|
308
|
+
for (const argument of arguments_) {
|
|
309
|
+
if (argument.length > MAX_ARGUMENT_LENGTH || argument.includes("\0")) {
|
|
310
|
+
throw new DirectExecPolicyError("Command arguments must be bounded text without NUL bytes");
|
|
311
|
+
}
|
|
312
|
+
if (isAbsolute(argument) || argument.split(/[\\/]/u).includes("..")) {
|
|
313
|
+
throw new DirectExecPolicyError(
|
|
314
|
+
`Command arguments cannot address paths outside the registered worktree: ${argument}`
|
|
315
|
+
);
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
function validateGit(arguments_) {
|
|
320
|
+
if (arguments_.some((argument) => FORBIDDEN_GIT_OPTIONS.some((option) => optionMatches(argument, option)))) {
|
|
321
|
+
throw new DirectExecPolicyError("Git path/config/output overrides are not allowed");
|
|
322
|
+
}
|
|
323
|
+
const subcommand = arguments_.find((argument) => !argument.startsWith("-"));
|
|
324
|
+
if (subcommand === void 0 || !READ_ONLY_GIT_COMMANDS.has(subcommand)) {
|
|
325
|
+
throw new DirectExecPolicyError(
|
|
326
|
+
`Only read-only Git commands are allowed (${[...READ_ONLY_GIT_COMMANDS].join(", ")})`
|
|
327
|
+
);
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
function validateFind(arguments_) {
|
|
331
|
+
let index = findPathTargets(arguments_).length;
|
|
332
|
+
while (index < arguments_.length) {
|
|
333
|
+
const argument = arguments_[index] ?? "";
|
|
334
|
+
if (SAFE_FIND_OPERATORS.has(argument) || SAFE_FIND_FLAGS.has(argument)) {
|
|
335
|
+
index += 1;
|
|
336
|
+
continue;
|
|
337
|
+
}
|
|
338
|
+
if (!SAFE_FIND_VALUE_FLAGS.has(argument)) {
|
|
339
|
+
throw new DirectExecPolicyError(
|
|
340
|
+
`find option or action is not allowed in direct inspection mode: ${argument || "<empty>"}`
|
|
341
|
+
);
|
|
342
|
+
}
|
|
343
|
+
const value = arguments_[index + 1];
|
|
344
|
+
if (value === void 0) {
|
|
345
|
+
throw new DirectExecPolicyError(`${argument} requires a value`);
|
|
346
|
+
}
|
|
347
|
+
if (["-maxdepth", "-mindepth"].includes(argument) && !/^\d+$/u.test(value)) {
|
|
348
|
+
throw new DirectExecPolicyError(`${argument} requires a non-negative integer`);
|
|
349
|
+
}
|
|
350
|
+
if (argument === "-type" && !/^[bcdpflsD]$/u.test(value)) {
|
|
351
|
+
throw new DirectExecPolicyError("find -type requires one supported file type");
|
|
352
|
+
}
|
|
353
|
+
index += 2;
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
function validateLs(arguments_) {
|
|
357
|
+
if (arguments_.some((argument) => [
|
|
358
|
+
"-H",
|
|
359
|
+
"-L",
|
|
360
|
+
"--dereference",
|
|
361
|
+
"--dereference-command-line",
|
|
362
|
+
"--dereference-command-line-symlink-to-dir"
|
|
363
|
+
].some((option) => optionMatches(argument, option)))) {
|
|
364
|
+
throw new DirectExecPolicyError("Following ls symlinks is not allowed");
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
function validateRg(arguments_) {
|
|
368
|
+
if (arguments_.some((argument) => ["-f", "-L", "--file", "--follow", "--ignore-file", "--pre", "--pre-glob"].some((option) => optionMatches(argument, option)))) {
|
|
369
|
+
throw new DirectExecPolicyError(
|
|
370
|
+
"rg file inputs, preprocessors, and symlink traversal are not allowed"
|
|
371
|
+
);
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
function validateWc(arguments_) {
|
|
375
|
+
if (arguments_.some((argument) => optionMatches(argument, "--files0-from"))) {
|
|
376
|
+
throw new DirectExecPolicyError("wc --files0-from is not allowed");
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
function validateRtk(arguments_) {
|
|
380
|
+
if (arguments_.length === 1 && ["--version", "-V"].includes(arguments_[0] ?? "")) return;
|
|
381
|
+
const subcommand = arguments_[0];
|
|
382
|
+
if (subcommand === void 0 || !SAFE_RTK_COMMANDS.has(subcommand)) {
|
|
383
|
+
throw new DirectExecPolicyError(
|
|
384
|
+
`Only read-only RTK commands are allowed (${[...SAFE_RTK_COMMANDS].join(", ")})`
|
|
385
|
+
);
|
|
386
|
+
}
|
|
387
|
+
if (subcommand === "git") validateGit(arguments_.slice(1));
|
|
388
|
+
if (subcommand === "find") validateFind(arguments_.slice(1));
|
|
389
|
+
if (subcommand === "ls") validateLs(arguments_.slice(1));
|
|
390
|
+
if (subcommand === "rg") validateRg(arguments_.slice(1));
|
|
391
|
+
if (subcommand === "wc") validateWc(arguments_.slice(1));
|
|
392
|
+
if (subcommand === "deps" && arguments_.length > 1) {
|
|
393
|
+
throw new DirectExecPolicyError("rtk deps does not accept paths in direct inspection mode");
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
function validateDirectExecArgv(argv) {
|
|
397
|
+
const [program, ...arguments_] = argv;
|
|
398
|
+
if (program === void 0 || program.length === 0 || program.includes("/") || program.includes("\\")) {
|
|
399
|
+
throw new DirectExecPolicyError("Executable must be a PATH command name");
|
|
400
|
+
}
|
|
401
|
+
validateArguments(arguments_);
|
|
402
|
+
if (program === "rtk") {
|
|
403
|
+
validateRtk(arguments_);
|
|
404
|
+
return;
|
|
405
|
+
}
|
|
406
|
+
if (VERSION_PROGRAMS.has(program) && (arguments_.length === 1 && ["--version", "-v", "-V"].includes(arguments_[0] ?? ""))) {
|
|
407
|
+
return;
|
|
408
|
+
}
|
|
409
|
+
if (!SAFE_NATIVE_PROGRAMS.has(program)) {
|
|
410
|
+
throw new DirectExecPolicyError(`${program} is not allowed by the read-only direct executor`);
|
|
411
|
+
}
|
|
412
|
+
if (program === "git") validateGit(arguments_);
|
|
413
|
+
if (program === "find") validateFind(arguments_);
|
|
414
|
+
if (program === "ls") validateLs(arguments_);
|
|
415
|
+
if (program === "rg") validateRg(arguments_);
|
|
416
|
+
if (program === "wc") validateWc(arguments_);
|
|
417
|
+
}
|
|
418
|
+
async function directExecExecutableAvailable(executable, environment) {
|
|
419
|
+
return await new Promise((resolveAvailable) => {
|
|
420
|
+
const child = spawn(executable, ["--version"], {
|
|
421
|
+
env: environment,
|
|
422
|
+
shell: false,
|
|
423
|
+
stdio: "ignore",
|
|
424
|
+
windowsHide: true
|
|
425
|
+
});
|
|
426
|
+
const timer = setTimeout(() => child.kill("SIGKILL"), 1e3);
|
|
427
|
+
timer.unref();
|
|
428
|
+
child.once("error", () => {
|
|
429
|
+
clearTimeout(timer);
|
|
430
|
+
resolveAvailable(false);
|
|
431
|
+
});
|
|
432
|
+
child.once("close", (code) => {
|
|
433
|
+
clearTimeout(timer);
|
|
434
|
+
resolveAvailable(code === 0);
|
|
435
|
+
});
|
|
436
|
+
});
|
|
437
|
+
}
|
|
438
|
+
function rtkRewrite(argv, rtkExecutable) {
|
|
439
|
+
const [program, ...arguments_] = argv;
|
|
440
|
+
if (program === "git" || program === "rg" || program === "ls" || program === "find" || program === "wc") {
|
|
441
|
+
return [rtkExecutable, program, ...arguments_];
|
|
442
|
+
}
|
|
443
|
+
if (program === "cat" && arguments_.length === 1 && !arguments_[0]?.startsWith("-")) {
|
|
444
|
+
return [rtkExecutable, "read", ...arguments_];
|
|
445
|
+
}
|
|
446
|
+
return void 0;
|
|
447
|
+
}
|
|
448
|
+
function hardenGitArgv(argv, usedRtk) {
|
|
449
|
+
const subcommandIndex = usedRtk && argv[1] === "git" ? 2 : argv[0] === "git" ? 1 : -1;
|
|
450
|
+
const subcommand = subcommandIndex < 0 ? void 0 : argv[subcommandIndex];
|
|
451
|
+
if (subcommand === void 0 || !["diff", "log", "show"].includes(subcommand)) {
|
|
452
|
+
return [...argv];
|
|
453
|
+
}
|
|
454
|
+
return [
|
|
455
|
+
...argv.slice(0, subcommandIndex + 1),
|
|
456
|
+
"--no-ext-diff",
|
|
457
|
+
"--no-textconv",
|
|
458
|
+
...argv.slice(subcommandIndex + 1)
|
|
459
|
+
];
|
|
460
|
+
}
|
|
461
|
+
function appendBounded(current, chunk, maxBytes) {
|
|
462
|
+
const remaining = maxBytes - Buffer.byteLength(current);
|
|
463
|
+
if (remaining <= 0) return { text: current, truncated: true };
|
|
464
|
+
const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
465
|
+
if (buffer.byteLength <= remaining) {
|
|
466
|
+
return { text: current + buffer.toString(), truncated: false };
|
|
467
|
+
}
|
|
468
|
+
return {
|
|
469
|
+
text: current + buffer.subarray(0, remaining).toString(),
|
|
470
|
+
truncated: true
|
|
471
|
+
};
|
|
472
|
+
}
|
|
473
|
+
async function runCommand(argv, cwd, environment, timeoutMs, maxOutputBytes, usedRtk) {
|
|
474
|
+
const startedAt = Date.now();
|
|
475
|
+
return await new Promise((resolveResult) => {
|
|
476
|
+
const [program, ...arguments_] = argv;
|
|
477
|
+
if (program === void 0) throw new DirectExecPolicyError("Command argv cannot be empty");
|
|
478
|
+
const usesGit = program === "git" || usedRtk && arguments_[0] === "git";
|
|
479
|
+
const commandEnvironment = usesGit ? {
|
|
480
|
+
...environment,
|
|
481
|
+
GIT_CONFIG_COUNT: "4",
|
|
482
|
+
GIT_CONFIG_KEY_0: "core.fsmonitor",
|
|
483
|
+
GIT_CONFIG_VALUE_0: "false",
|
|
484
|
+
GIT_CONFIG_KEY_1: "core.hooksPath",
|
|
485
|
+
GIT_CONFIG_VALUE_1: process.platform === "win32" ? "NUL" : "/dev/null",
|
|
486
|
+
GIT_CONFIG_KEY_2: "diff.external",
|
|
487
|
+
GIT_CONFIG_VALUE_2: "",
|
|
488
|
+
GIT_CONFIG_KEY_3: "interactive.diffFilter",
|
|
489
|
+
GIT_CONFIG_VALUE_3: "",
|
|
490
|
+
GIT_OPTIONAL_LOCKS: "0",
|
|
491
|
+
GIT_PAGER: "cat",
|
|
492
|
+
PAGER: "cat"
|
|
493
|
+
} : environment;
|
|
494
|
+
const child = spawn(program, arguments_, {
|
|
495
|
+
cwd,
|
|
496
|
+
env: commandEnvironment,
|
|
497
|
+
detached: process.platform !== "win32",
|
|
498
|
+
shell: false,
|
|
499
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
500
|
+
windowsHide: true
|
|
501
|
+
});
|
|
502
|
+
const removeEmergencyExitHook = installEmergencyChildExitHook(child);
|
|
503
|
+
let stdout = "";
|
|
504
|
+
let stderr = "";
|
|
505
|
+
let truncated = false;
|
|
506
|
+
let timedOut = false;
|
|
507
|
+
let settled = false;
|
|
508
|
+
let termination;
|
|
509
|
+
let timer;
|
|
510
|
+
const finish = (exitCode) => {
|
|
511
|
+
if (settled) return;
|
|
512
|
+
settled = true;
|
|
513
|
+
if (timer !== void 0) clearTimeout(timer);
|
|
514
|
+
removeEmergencyExitHook();
|
|
515
|
+
resolveResult({
|
|
516
|
+
argv,
|
|
517
|
+
cwd,
|
|
518
|
+
durationMs: Date.now() - startedAt,
|
|
519
|
+
exitCode,
|
|
520
|
+
stdout: stdout.trimEnd(),
|
|
521
|
+
stderr: stderr.trimEnd(),
|
|
522
|
+
timedOut,
|
|
523
|
+
truncated,
|
|
524
|
+
usedRtk
|
|
525
|
+
});
|
|
526
|
+
};
|
|
527
|
+
child.stdout.on("data", (chunk) => {
|
|
528
|
+
const appended = appendBounded(stdout, chunk, maxOutputBytes);
|
|
529
|
+
stdout = appended.text;
|
|
530
|
+
truncated ||= appended.truncated;
|
|
531
|
+
});
|
|
532
|
+
child.stderr.on("data", (chunk) => {
|
|
533
|
+
const appended = appendBounded(stderr, chunk, maxOutputBytes);
|
|
534
|
+
stderr = appended.text;
|
|
535
|
+
truncated ||= appended.truncated;
|
|
536
|
+
});
|
|
537
|
+
child.once("error", (error2) => {
|
|
538
|
+
stderr = error2.message;
|
|
539
|
+
finish(127);
|
|
540
|
+
});
|
|
541
|
+
child.once("close", (code) => {
|
|
542
|
+
if (termination === void 0) {
|
|
543
|
+
finish(code ?? (timedOut ? 124 : 1));
|
|
544
|
+
return;
|
|
545
|
+
}
|
|
546
|
+
void termination.finally(() => finish(timedOut ? 124 : code ?? 1));
|
|
547
|
+
});
|
|
548
|
+
timer = setTimeout(() => {
|
|
549
|
+
timedOut = true;
|
|
550
|
+
termination ??= terminateChildProcessTree(child, KILL_GRACE_MS);
|
|
551
|
+
void termination.catch((error2) => {
|
|
552
|
+
const message = error2 instanceof Error ? error2.message : String(error2);
|
|
553
|
+
stderr = stderr ? `${stderr}
|
|
554
|
+
${message}` : message;
|
|
555
|
+
}).finally(() => finish(124));
|
|
556
|
+
}, timeoutMs);
|
|
557
|
+
timer.unref();
|
|
558
|
+
});
|
|
559
|
+
}
|
|
560
|
+
async function executeReadOnlyBatch(request, options) {
|
|
561
|
+
if (!Array.isArray(request.commands) || request.commands.length === 0) {
|
|
562
|
+
throw new DirectExecPolicyError("At least one command is required");
|
|
563
|
+
}
|
|
564
|
+
if (request.commands.length > MAX_COMMANDS) {
|
|
565
|
+
throw new DirectExecPolicyError(`At most ${MAX_COMMANDS} commands may run in one batch`);
|
|
566
|
+
}
|
|
567
|
+
const repoRoot = await realpath(options.repoRoot);
|
|
568
|
+
const workspaceRoot = await realpath(options.workspaceRoot);
|
|
569
|
+
if (!isWithin(repoRoot, workspaceRoot)) {
|
|
570
|
+
throw new DirectExecPolicyError("Registered workspace must stay inside its Git worktree");
|
|
571
|
+
}
|
|
572
|
+
const environment = options.environment ?? process.env;
|
|
573
|
+
const timeoutMs = Math.min(
|
|
574
|
+
MAX_TIMEOUT_MS,
|
|
575
|
+
Math.max(1, request.timeoutMs ?? DEFAULT_TIMEOUT_MS)
|
|
576
|
+
);
|
|
577
|
+
const maxOutputBytes = Math.max(1024, options.maxOutputBytes ?? DEFAULT_OUTPUT_BYTES);
|
|
578
|
+
const rtkExecutable = options.rtkExecutable === false ? void 0 : options.rtkExecutable ?? "rtk";
|
|
579
|
+
const rtkAvailable = options.rtkAvailable ?? (rtkExecutable === void 0 ? false : await directExecExecutableAvailable(rtkExecutable, environment));
|
|
580
|
+
const results = [];
|
|
581
|
+
let stoppedEarly = false;
|
|
582
|
+
for (const command of request.commands) {
|
|
583
|
+
if (!Array.isArray(command.argv)) {
|
|
584
|
+
throw new DirectExecPolicyError("Each command must provide an argv array");
|
|
585
|
+
}
|
|
586
|
+
validateDirectExecArgv(command.argv);
|
|
587
|
+
const requestedCwd = command.cwd === void 0 ? workspaceRoot : isAbsolute(command.cwd) ? command.cwd : resolve(workspaceRoot, command.cwd);
|
|
588
|
+
const cwd = await realpath(requestedCwd);
|
|
589
|
+
if (!isWithin(repoRoot, cwd)) {
|
|
590
|
+
throw new DirectExecPolicyError("Command cwd must stay inside the registered worktree");
|
|
591
|
+
}
|
|
592
|
+
await validateDirectFileTargets(command.argv, cwd, repoRoot);
|
|
593
|
+
await validateExistingArgumentPaths(command.argv, cwd, repoRoot);
|
|
594
|
+
let effectiveArgv = [...command.argv];
|
|
595
|
+
let usedRtk = effectiveArgv[0] === "rtk";
|
|
596
|
+
if (usedRtk) {
|
|
597
|
+
if (!rtkAvailable || rtkExecutable === void 0) {
|
|
598
|
+
throw new DirectExecPolicyError("RTK was requested but is not available");
|
|
599
|
+
}
|
|
600
|
+
effectiveArgv[0] = rtkExecutable;
|
|
601
|
+
} else if (request.preferRtk !== false && rtkAvailable && rtkExecutable !== void 0) {
|
|
602
|
+
const rewritten = rtkRewrite(effectiveArgv, rtkExecutable);
|
|
603
|
+
if (rewritten !== void 0) {
|
|
604
|
+
effectiveArgv = rewritten;
|
|
605
|
+
usedRtk = true;
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
effectiveArgv = hardenGitArgv(effectiveArgv, usedRtk);
|
|
609
|
+
const result2 = await runCommand(
|
|
610
|
+
effectiveArgv,
|
|
611
|
+
cwd,
|
|
612
|
+
environment,
|
|
613
|
+
timeoutMs,
|
|
614
|
+
maxOutputBytes,
|
|
615
|
+
usedRtk
|
|
616
|
+
);
|
|
617
|
+
results.push(result2);
|
|
618
|
+
if (result2.exitCode !== 0 && request.stopOnError !== false) {
|
|
619
|
+
stoppedEarly = results.length < request.commands.length;
|
|
620
|
+
break;
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
return { results, stoppedEarly };
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
// ../../package.json
|
|
627
|
+
var package_default = {
|
|
628
|
+
name: "visual-remote",
|
|
629
|
+
version: "0.3.2",
|
|
630
|
+
description: "Visual bridge from a running web UI to a coding agent in its Git worktree",
|
|
631
|
+
type: "module",
|
|
632
|
+
packageManager: "pnpm@10.34.5",
|
|
633
|
+
repository: {
|
|
634
|
+
type: "git",
|
|
635
|
+
url: "git+https://github.com/elicie/visual-remote.git"
|
|
636
|
+
},
|
|
637
|
+
homepage: "https://github.com/elicie/visual-remote#readme",
|
|
638
|
+
bugs: {
|
|
639
|
+
url: "https://github.com/elicie/visual-remote/issues"
|
|
640
|
+
},
|
|
641
|
+
files: [
|
|
642
|
+
"apps/cli/dist/index.js",
|
|
643
|
+
"apps/cli/dist/direct-exec-mcp.js",
|
|
644
|
+
"apps/cli/dist/vite.js",
|
|
645
|
+
"apps/cli/dist/next.js",
|
|
646
|
+
"apps/cli/dist/next-client.js",
|
|
647
|
+
"apps/cli/vite.d.ts",
|
|
648
|
+
"apps/cli/next.d.ts",
|
|
649
|
+
"apps/cli/next-client.d.ts",
|
|
650
|
+
"packages/overlay/dist/client.js",
|
|
651
|
+
"packages/overlay/dist/viewer.js"
|
|
652
|
+
],
|
|
653
|
+
bin: {
|
|
654
|
+
visual: "./apps/cli/dist/index.js",
|
|
655
|
+
"visual-remote": "./apps/cli/dist/index.js"
|
|
656
|
+
},
|
|
657
|
+
exports: {
|
|
658
|
+
"./vite": {
|
|
659
|
+
types: "./apps/cli/vite.d.ts",
|
|
660
|
+
import: "./apps/cli/dist/vite.js"
|
|
661
|
+
},
|
|
662
|
+
"./next": {
|
|
663
|
+
types: "./apps/cli/next.d.ts",
|
|
664
|
+
import: "./apps/cli/dist/next.js",
|
|
665
|
+
default: "./apps/cli/dist/next.js"
|
|
666
|
+
},
|
|
667
|
+
"./next/client": {
|
|
668
|
+
types: "./apps/cli/next-client.d.ts",
|
|
669
|
+
import: "./apps/cli/dist/next-client.js",
|
|
670
|
+
default: "./apps/cli/dist/next-client.js"
|
|
671
|
+
}
|
|
672
|
+
},
|
|
673
|
+
publishConfig: {
|
|
674
|
+
access: "public",
|
|
675
|
+
registry: "https://registry.npmjs.org"
|
|
676
|
+
},
|
|
677
|
+
engines: {
|
|
678
|
+
node: ">=24"
|
|
679
|
+
},
|
|
680
|
+
scripts: {
|
|
681
|
+
build: "corepack pnpm run build:overlay && corepack pnpm run build:server",
|
|
682
|
+
"build:overlay": "corepack pnpm --filter @visual-remote/overlay build",
|
|
683
|
+
"build:server": "corepack pnpm --filter @visual-remote/cli build",
|
|
684
|
+
dev: "corepack pnpm run build:overlay && tsx apps/cli/src/index.ts",
|
|
685
|
+
test: "vitest run",
|
|
686
|
+
"test:e2e": "corepack pnpm build && corepack pnpm exec playwright test --config tests/e2e/playwright.config.ts",
|
|
687
|
+
"test:watch": "vitest",
|
|
688
|
+
typecheck: "corepack pnpm -r --if-present typecheck && tsc --noEmit -p tsconfig.tests.json",
|
|
689
|
+
prepack: "corepack pnpm build"
|
|
690
|
+
},
|
|
691
|
+
dependencies: {
|
|
692
|
+
commander: "^15.0.0",
|
|
693
|
+
"http-proxy": "^1.18.1",
|
|
694
|
+
ws: "^8.21.1",
|
|
695
|
+
yaml: "^2.9.0",
|
|
696
|
+
zod: "^4.4.3"
|
|
697
|
+
},
|
|
698
|
+
peerDependencies: {
|
|
699
|
+
vite: ">=5"
|
|
700
|
+
},
|
|
701
|
+
peerDependenciesMeta: {
|
|
702
|
+
vite: {
|
|
703
|
+
optional: true
|
|
704
|
+
}
|
|
705
|
+
},
|
|
706
|
+
devDependencies: {
|
|
707
|
+
"@playwright/test": "^1.62.1",
|
|
708
|
+
"@types/http-proxy": "^1.17.17",
|
|
709
|
+
"@types/node": "^26.1.2",
|
|
710
|
+
"@types/ws": "^8.18.1",
|
|
711
|
+
"@visual-remote/bridge-core": "workspace:*",
|
|
712
|
+
"@visual-remote/cli": "workspace:*",
|
|
713
|
+
"@visual-remote/gateway": "workspace:*",
|
|
714
|
+
"@visual-remote/overlay": "workspace:*",
|
|
715
|
+
"@visual-remote/protocol": "workspace:*",
|
|
716
|
+
esbuild: "^0.28.1",
|
|
717
|
+
next: "15.5.16",
|
|
718
|
+
react: "19.1.0",
|
|
719
|
+
"react-dom": "19.1.0",
|
|
720
|
+
tsx: "^4.23.1",
|
|
721
|
+
typescript: "^7.0.2",
|
|
722
|
+
vite: "^8.1.5",
|
|
723
|
+
vitest: "^4.1.10"
|
|
724
|
+
}
|
|
725
|
+
};
|
|
726
|
+
|
|
727
|
+
// src/version.ts
|
|
728
|
+
var VISUAL_REMOTE_VERSION = package_default.version;
|
|
729
|
+
|
|
730
|
+
// src/direct-exec-mcp.ts
|
|
731
|
+
var TOOL_NAME = "run_readonly";
|
|
732
|
+
function recordOf(value) {
|
|
733
|
+
return value !== null && typeof value === "object" && !Array.isArray(value) ? value : void 0;
|
|
734
|
+
}
|
|
735
|
+
function parseArguments(argv) {
|
|
736
|
+
const values = /* @__PURE__ */ new Map();
|
|
737
|
+
let disableRtk = false;
|
|
738
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
739
|
+
const argument = argv[index];
|
|
740
|
+
if (argument === "--no-rtk") {
|
|
741
|
+
disableRtk = true;
|
|
742
|
+
continue;
|
|
743
|
+
}
|
|
744
|
+
if (!["--repo-root", "--workspace-root", "--rtk"].includes(argument ?? "")) {
|
|
745
|
+
throw new Error(`Unknown direct-exec MCP argument: ${argument ?? "<missing>"}`);
|
|
746
|
+
}
|
|
747
|
+
const value = argv[index + 1];
|
|
748
|
+
if (value === void 0 || value.length === 0) {
|
|
749
|
+
throw new Error(`${argument} requires a value`);
|
|
750
|
+
}
|
|
751
|
+
values.set(argument ?? "", value);
|
|
752
|
+
index += 1;
|
|
753
|
+
}
|
|
754
|
+
const repoRoot = values.get("--repo-root");
|
|
755
|
+
const workspaceRoot = values.get("--workspace-root");
|
|
756
|
+
if (repoRoot === void 0 || workspaceRoot === void 0) {
|
|
757
|
+
throw new Error("Direct-exec MCP requires --repo-root and --workspace-root");
|
|
758
|
+
}
|
|
759
|
+
return {
|
|
760
|
+
repoRoot,
|
|
761
|
+
workspaceRoot,
|
|
762
|
+
rtkExecutable: disableRtk ? false : values.get("--rtk") ?? "rtk"
|
|
763
|
+
};
|
|
764
|
+
}
|
|
765
|
+
function parseBatchRequest(value) {
|
|
766
|
+
const record = recordOf(value);
|
|
767
|
+
const commands = Array.isArray(record?.commands) ? record.commands : void 0;
|
|
768
|
+
if (commands === void 0) {
|
|
769
|
+
throw new DirectExecPolicyError("commands must be an array");
|
|
770
|
+
}
|
|
771
|
+
return {
|
|
772
|
+
commands: commands.map((candidate) => {
|
|
773
|
+
const command = recordOf(candidate);
|
|
774
|
+
if (!Array.isArray(command?.argv) || !command.argv.every((item) => typeof item === "string")) {
|
|
775
|
+
throw new DirectExecPolicyError("Each command argv must be a string array");
|
|
776
|
+
}
|
|
777
|
+
if (command.cwd !== void 0 && typeof command.cwd !== "string") {
|
|
778
|
+
throw new DirectExecPolicyError("Command cwd must be a string when provided");
|
|
779
|
+
}
|
|
780
|
+
return {
|
|
781
|
+
argv: command.argv,
|
|
782
|
+
...typeof command.cwd === "string" ? { cwd: command.cwd } : {}
|
|
783
|
+
};
|
|
784
|
+
}),
|
|
785
|
+
preferRtk: true,
|
|
786
|
+
...typeof record?.stopOnError === "boolean" ? { stopOnError: record.stopOnError } : {},
|
|
787
|
+
...typeof record?.timeoutMs === "number" ? { timeoutMs: record.timeoutMs } : {}
|
|
788
|
+
};
|
|
789
|
+
}
|
|
790
|
+
function displayArgument(argument) {
|
|
791
|
+
return /^[A-Za-z0-9_./:=@%+,-]+$/u.test(argument) ? argument : JSON.stringify(argument);
|
|
792
|
+
}
|
|
793
|
+
function formatBatchResult(batch) {
|
|
794
|
+
const sections = batch.results.map((result2) => {
|
|
795
|
+
const metadata = [
|
|
796
|
+
`cwd=${result2.cwd}`,
|
|
797
|
+
`exit=${result2.exitCode}`,
|
|
798
|
+
`duration=${result2.durationMs}ms`,
|
|
799
|
+
...result2.usedRtk ? ["rtk=yes"] : [],
|
|
800
|
+
...result2.timedOut ? ["timed_out=yes"] : [],
|
|
801
|
+
...result2.truncated ? ["truncated=yes"] : []
|
|
802
|
+
].join(" ");
|
|
803
|
+
const output = [
|
|
804
|
+
result2.stdout,
|
|
805
|
+
result2.stderr.length > 0 ? `stderr:
|
|
806
|
+
${result2.stderr}` : ""
|
|
807
|
+
].filter(Boolean).join("\n");
|
|
808
|
+
return [
|
|
809
|
+
`$ ${result2.argv.map(displayArgument).join(" ")}`,
|
|
810
|
+
metadata,
|
|
811
|
+
output || "(no output)"
|
|
812
|
+
].join("\n");
|
|
813
|
+
});
|
|
814
|
+
if (batch.stoppedEarly) sections.push("Batch stopped after the first failed command.");
|
|
815
|
+
return sections.join("\n\n");
|
|
816
|
+
}
|
|
817
|
+
function structuredBatchResult(batch) {
|
|
818
|
+
return {
|
|
819
|
+
stoppedEarly: batch.stoppedEarly,
|
|
820
|
+
results: batch.results.map(({ stdout: _stdout, stderr: _stderr, ...metadata }) => metadata)
|
|
821
|
+
};
|
|
822
|
+
}
|
|
823
|
+
function directExecToolResult(batch) {
|
|
824
|
+
return {
|
|
825
|
+
content: [{ type: "text", text: formatBatchResult(batch) }],
|
|
826
|
+
structuredContent: structuredBatchResult(batch),
|
|
827
|
+
isError: batch.results.some((command) => command.exitCode !== 0)
|
|
828
|
+
};
|
|
829
|
+
}
|
|
830
|
+
function send(message) {
|
|
831
|
+
process.stdout.write(`${JSON.stringify(message)}
|
|
832
|
+
`);
|
|
833
|
+
}
|
|
834
|
+
function result(id, value) {
|
|
835
|
+
send({ jsonrpc: "2.0", id: id ?? null, result: value });
|
|
836
|
+
}
|
|
837
|
+
function error(id, code, message) {
|
|
838
|
+
send({ jsonrpc: "2.0", id: id ?? null, error: { code, message } });
|
|
839
|
+
}
|
|
840
|
+
async function handleRequest(request, options) {
|
|
841
|
+
if (request.method === "initialize") {
|
|
842
|
+
const params = recordOf(request.params);
|
|
843
|
+
result(request.id, {
|
|
844
|
+
protocolVersion: typeof params?.protocolVersion === "string" ? params.protocolVersion : "2024-11-05",
|
|
845
|
+
capabilities: { tools: { listChanged: false } },
|
|
846
|
+
serverInfo: {
|
|
847
|
+
name: "visual-remote-direct-exec",
|
|
848
|
+
version: VISUAL_REMOTE_VERSION
|
|
849
|
+
}
|
|
850
|
+
});
|
|
851
|
+
return;
|
|
852
|
+
}
|
|
853
|
+
if (request.method === "ping") {
|
|
854
|
+
result(request.id, {});
|
|
855
|
+
return;
|
|
856
|
+
}
|
|
857
|
+
if (request.method === "tools/list") {
|
|
858
|
+
result(request.id, {
|
|
859
|
+
tools: [
|
|
860
|
+
{
|
|
861
|
+
name: TOOL_NAME,
|
|
862
|
+
title: "Run read-only repository commands directly",
|
|
863
|
+
description: "Runs up to 8 read-only argv commands without a shell in the registered worktree. Uses the registered workspace as cwd and applies RTK automatically when supported.",
|
|
864
|
+
inputSchema: {
|
|
865
|
+
type: "object",
|
|
866
|
+
additionalProperties: false,
|
|
867
|
+
required: ["commands"],
|
|
868
|
+
properties: {
|
|
869
|
+
commands: {
|
|
870
|
+
type: "array",
|
|
871
|
+
minItems: 1,
|
|
872
|
+
maxItems: 8,
|
|
873
|
+
items: {
|
|
874
|
+
type: "object",
|
|
875
|
+
additionalProperties: false,
|
|
876
|
+
required: ["argv"],
|
|
877
|
+
properties: {
|
|
878
|
+
argv: {
|
|
879
|
+
type: "array",
|
|
880
|
+
minItems: 1,
|
|
881
|
+
maxItems: 65,
|
|
882
|
+
items: { type: "string", maxLength: 4096 }
|
|
883
|
+
},
|
|
884
|
+
cwd: {
|
|
885
|
+
type: "string",
|
|
886
|
+
description: "Optional absolute worktree path or path relative to the registered workspace."
|
|
887
|
+
}
|
|
888
|
+
}
|
|
889
|
+
}
|
|
890
|
+
},
|
|
891
|
+
stopOnError: { type: "boolean", default: true },
|
|
892
|
+
timeoutMs: { type: "integer", minimum: 1, maximum: 12e4 }
|
|
893
|
+
}
|
|
894
|
+
},
|
|
895
|
+
annotations: {
|
|
896
|
+
readOnlyHint: true,
|
|
897
|
+
destructiveHint: false,
|
|
898
|
+
idempotentHint: true,
|
|
899
|
+
openWorldHint: false
|
|
900
|
+
}
|
|
901
|
+
}
|
|
902
|
+
]
|
|
903
|
+
});
|
|
904
|
+
return;
|
|
905
|
+
}
|
|
906
|
+
if (request.method === "tools/call") {
|
|
907
|
+
const params = recordOf(request.params);
|
|
908
|
+
if (params?.name !== TOOL_NAME) {
|
|
909
|
+
error(request.id, -32602, `Unknown tool: ${String(params?.name)}`);
|
|
910
|
+
return;
|
|
911
|
+
}
|
|
912
|
+
try {
|
|
913
|
+
const batch = await executeReadOnlyBatch(
|
|
914
|
+
parseBatchRequest(params.arguments),
|
|
915
|
+
options
|
|
916
|
+
);
|
|
917
|
+
result(request.id, directExecToolResult(batch));
|
|
918
|
+
} catch (caught) {
|
|
919
|
+
const message = caught instanceof Error ? caught.message : String(caught);
|
|
920
|
+
result(request.id, {
|
|
921
|
+
content: [{ type: "text", text: message }],
|
|
922
|
+
isError: true
|
|
923
|
+
});
|
|
924
|
+
}
|
|
925
|
+
return;
|
|
926
|
+
}
|
|
927
|
+
if (request.id !== void 0) error(request.id, -32601, `Method not found: ${request.method}`);
|
|
928
|
+
}
|
|
929
|
+
async function main() {
|
|
930
|
+
const parsed = parseArguments(process.argv.slice(2));
|
|
931
|
+
const options = {
|
|
932
|
+
...parsed,
|
|
933
|
+
rtkAvailable: parsed.rtkExecutable === false ? false : await directExecExecutableAvailable(parsed.rtkExecutable, process.env)
|
|
934
|
+
};
|
|
935
|
+
process.stdin.setEncoding("utf8");
|
|
936
|
+
let remainder = "";
|
|
937
|
+
process.stdin.on("data", (chunk) => {
|
|
938
|
+
const lines = (remainder + chunk).split(/\r?\n/u);
|
|
939
|
+
remainder = lines.pop() ?? "";
|
|
940
|
+
for (const line of lines) {
|
|
941
|
+
if (line.trim().length === 0) continue;
|
|
942
|
+
try {
|
|
943
|
+
const request = JSON.parse(line);
|
|
944
|
+
if (request.jsonrpc !== "2.0" || typeof request.method !== "string") {
|
|
945
|
+
error(request.id, -32600, "Invalid JSON-RPC request");
|
|
946
|
+
continue;
|
|
947
|
+
}
|
|
948
|
+
void handleRequest(request, options).catch((caught) => {
|
|
949
|
+
error(
|
|
950
|
+
request.id,
|
|
951
|
+
-32603,
|
|
952
|
+
caught instanceof Error ? caught.message : String(caught)
|
|
953
|
+
);
|
|
954
|
+
});
|
|
955
|
+
} catch {
|
|
956
|
+
error(null, -32700, "Invalid JSON");
|
|
957
|
+
}
|
|
958
|
+
}
|
|
959
|
+
});
|
|
960
|
+
}
|
|
961
|
+
if (process.argv[1] !== void 0 && fileURLToPath(import.meta.url) === resolve2(process.argv[1])) {
|
|
962
|
+
void main().catch((caught) => {
|
|
963
|
+
process.stderr.write(
|
|
964
|
+
`${caught instanceof Error ? caught.message : String(caught)}
|
|
965
|
+
`
|
|
966
|
+
);
|
|
967
|
+
process.exitCode = 1;
|
|
968
|
+
});
|
|
969
|
+
}
|
|
970
|
+
export {
|
|
971
|
+
directExecToolResult,
|
|
972
|
+
formatBatchResult,
|
|
973
|
+
parseArguments,
|
|
974
|
+
parseBatchRequest,
|
|
975
|
+
structuredBatchResult
|
|
976
|
+
};
|