mcp-fs-shell-windows 0.2.19 → 0.2.29
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/LICENSE +2 -1
- package/README.md +352 -190
- package/dist/analyze_project/handler.js +62 -0
- package/dist/analyze_project/schema.js +5 -0
- package/dist/browser/browserActions.js +128 -0
- package/dist/browser/fuzzySearch.js +49 -0
- package/dist/browser/handler.js +227 -0
- package/dist/browser/launcher.js +103 -0
- package/dist/browser/schema.js +37 -0
- package/dist/browser/session.js +14 -0
- package/dist/compat/handler.js +254 -0
- package/dist/compat/schema.js +97 -0
- package/dist/gh/handler.js +406 -0
- package/dist/gh/schema.js +36 -0
- package/dist/git/handler.js +209 -0
- package/dist/git/schema.js +23 -0
- package/dist/launch_file/handler.js +3 -1
- package/dist/query_database/handler.js +27 -0
- package/dist/query_database/schema.js +5 -0
- package/dist/rag/handler.js +103 -0
- package/dist/rag/helpers.js +126 -0
- package/dist/rag/schema.js +16 -0
- package/dist/read_document/handler.js +61 -0
- package/dist/read_document/schema.js +4 -0
- package/dist/run_javascript/handler.js +124 -0
- package/dist/run_javascript/schema.js +28 -0
- package/dist/server.js +885 -1
- package/dist/shell/handler.js +14 -6
- package/dist/subagent/handler.js +752 -0
- package/dist/subagent/handoffMessage.js +56 -0
- package/dist/subagent/schema.js +18 -0
- package/dist/subagent/subAgentToolCallParser.js +491 -0
- package/dist/subagent/toolCallValidator.js +97 -0
- package/dist/system/handler.js +239 -0
- package/dist/system/schema.js +21 -0
- package/dist/web/ddgParse.js +51 -0
- package/dist/web/handler.js +286 -0
- package/dist/web/schema.js +18 -0
- package/package.json +22 -2
|
@@ -0,0 +1,406 @@
|
|
|
1
|
+
// gh/handler.ts — gh CLI wrappers for the 8 gh_* tools (MCP filesystem fork).
|
|
2
|
+
//
|
|
3
|
+
// Design: spawn the `gh` binary directly (no new dependency; `git push` for
|
|
4
|
+
// gh_push spawns the git CLI). Every tool operates against the repo at the
|
|
5
|
+
// server process's current working directory (process.cwd()) — the same
|
|
6
|
+
// "current working directory context" the git/shell tools use — and takes NO
|
|
7
|
+
// working-directory parameter (matching the reference Beledarian tools: the
|
|
8
|
+
// reference's gh spawns inherit the plugin process cwd, and gh itself resolves
|
|
9
|
+
// the repo from that cwd's git remote). All argument values (titles, bodies,
|
|
10
|
+
// labels, branch names, issue/PR numbers) are passed as individual argv
|
|
11
|
+
// elements (no shell), so quotes / Unicode / special characters pass through
|
|
12
|
+
// verbatim. Long issue/PR bodies are written to a temp file in the working
|
|
13
|
+
// directory and passed via --body-file (mirroring the reference); the temp
|
|
14
|
+
// file is always removed afterwards.
|
|
15
|
+
//
|
|
16
|
+
// gh binary resolution (port of the reference's checkGhInstalled, with a
|
|
17
|
+
// documented fallback): PATH lookup via `where gh` (win32) / `which gh` first;
|
|
18
|
+
// if gh is not on the server process's PATH, fall back to the known portable
|
|
19
|
+
// install <home>/tools/gh/bin/gh.exe (this machine's layout — gh 2.99.0,
|
|
20
|
+
// authenticated to github.com). If neither is found, the reference's
|
|
21
|
+
// "not installed" error string is returned.
|
|
22
|
+
//
|
|
23
|
+
// gh_auth: runs `gh auth status`; on failure it opens a detached terminal
|
|
24
|
+
// window (cmd /k) for interactive `gh auth login --git-protocol=https` exactly
|
|
25
|
+
// as the reference does — spawned with {detached: true, stdio: "ignore"} so it
|
|
26
|
+
// survives the MCP call returning, using the RESOLVED gh path so the window
|
|
27
|
+
// works even when gh is not on the inherited PATH. The window persists for the
|
|
28
|
+
// user (cmd /k) and can be closed manually.
|
|
29
|
+
//
|
|
30
|
+
// Return shape mirrors the reference implementation: each handler returns a JSON
|
|
31
|
+
// text string carrying the reference's fields ({success, ...} / {error} /
|
|
32
|
+
// {issues} / {pull_requests} / {comments} / {diff} / raw_output) instead of
|
|
33
|
+
// throwing, so a missing gh, a bad remote, or a bad branch yields a clear
|
|
34
|
+
// structured error string rather than a crash or hang.
|
|
35
|
+
import { spawn } from "child_process";
|
|
36
|
+
import fs from "fs";
|
|
37
|
+
import nodeOs from "os";
|
|
38
|
+
import nodePath from "path";
|
|
39
|
+
/** Run `<bin> <args>` with the given cwd. Never rejects; spawn failures resolve with code -1. */
|
|
40
|
+
function runCmd(bin, args, cwd) {
|
|
41
|
+
return new Promise((resolve) => {
|
|
42
|
+
let child;
|
|
43
|
+
try {
|
|
44
|
+
child = spawn(bin, args, { cwd });
|
|
45
|
+
}
|
|
46
|
+
catch (e) {
|
|
47
|
+
resolve({ code: -1, stdout: "", stderr: e instanceof Error ? e.message : String(e) });
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
let stdout = "";
|
|
51
|
+
let stderr = "";
|
|
52
|
+
let settled = false;
|
|
53
|
+
const finish = (code) => {
|
|
54
|
+
if (settled)
|
|
55
|
+
return;
|
|
56
|
+
settled = true;
|
|
57
|
+
resolve({ code, stdout, stderr });
|
|
58
|
+
};
|
|
59
|
+
if (child.stdout) {
|
|
60
|
+
child.stdout.on("data", (d) => {
|
|
61
|
+
stdout += d.toString("utf8");
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
if (child.stderr) {
|
|
65
|
+
child.stderr.on("data", (d) => {
|
|
66
|
+
stderr += d.toString("utf8");
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
child.on("error", (e) => {
|
|
70
|
+
// e.g. ENOENT when the binary cannot be found
|
|
71
|
+
stderr += (stderr ? "\n" : "") + (e.message || String(e));
|
|
72
|
+
finish(-1);
|
|
73
|
+
});
|
|
74
|
+
child.on("close", (code) => finish(code === null ? -1 : code));
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
/** Run a shell one-liner (e.g. `where gh`). Never rejects. */
|
|
78
|
+
function runShellLine(cmd) {
|
|
79
|
+
return new Promise((resolve) => {
|
|
80
|
+
let child;
|
|
81
|
+
try {
|
|
82
|
+
child = spawn(cmd, [], { shell: true });
|
|
83
|
+
}
|
|
84
|
+
catch (e) {
|
|
85
|
+
resolve({ code: -1, stdout: "", stderr: e instanceof Error ? e.message : String(e) });
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
let stdout = "";
|
|
89
|
+
let stderr = "";
|
|
90
|
+
let settled = false;
|
|
91
|
+
const finish = (code) => {
|
|
92
|
+
if (settled)
|
|
93
|
+
return;
|
|
94
|
+
settled = true;
|
|
95
|
+
resolve({ code, stdout, stderr });
|
|
96
|
+
};
|
|
97
|
+
if (child.stdout) {
|
|
98
|
+
child.stdout.on("data", (d) => {
|
|
99
|
+
stdout += d.toString("utf8");
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
if (child.stderr) {
|
|
103
|
+
child.stderr.on("data", (d) => {
|
|
104
|
+
stderr += d.toString("utf8");
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
child.on("error", (e) => {
|
|
108
|
+
stderr += (stderr ? "\n" : "") + (e.message || String(e));
|
|
109
|
+
finish(-1);
|
|
110
|
+
});
|
|
111
|
+
child.on("close", (code) => finish(code === null ? -1 : code));
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
const GH_NOT_INSTALLED = "GitHub CLI ('gh') is not installed. Please ask the user to install it from https://cli.github.com/";
|
|
115
|
+
/** Absolute path of gh on the PATH (first hit of `where gh` / `which gh`), or null. */
|
|
116
|
+
async function findGhOnPath() {
|
|
117
|
+
const r = await runShellLine(process.platform === "win32" ? "where gh" : "which gh");
|
|
118
|
+
if (!r.stdout.trim())
|
|
119
|
+
return null;
|
|
120
|
+
const first = r.stdout
|
|
121
|
+
.split(/\r?\n/)
|
|
122
|
+
.map((s) => s.trim())
|
|
123
|
+
.find((s) => s.length > 0);
|
|
124
|
+
if (!first)
|
|
125
|
+
return null;
|
|
126
|
+
try {
|
|
127
|
+
if (fs.existsSync(first))
|
|
128
|
+
return first;
|
|
129
|
+
}
|
|
130
|
+
catch {
|
|
131
|
+
// fall through
|
|
132
|
+
}
|
|
133
|
+
return null;
|
|
134
|
+
}
|
|
135
|
+
/** Known portable-install locations, checked in order (documented fallback). */
|
|
136
|
+
function findGhKnownPaths() {
|
|
137
|
+
const home = nodeOs.homedir();
|
|
138
|
+
const candidates = [nodePath.join(home, "tools", "gh", "bin", "gh.exe")];
|
|
139
|
+
for (const c of candidates) {
|
|
140
|
+
try {
|
|
141
|
+
if (fs.existsSync(c))
|
|
142
|
+
return c;
|
|
143
|
+
}
|
|
144
|
+
catch {
|
|
145
|
+
// ignore
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
return null;
|
|
149
|
+
}
|
|
150
|
+
/** Resolve the gh executable (absolute path): PATH first, then known portable install; null if absent. */
|
|
151
|
+
async function resolveGh() {
|
|
152
|
+
const onPath = await findGhOnPath();
|
|
153
|
+
if (onPath)
|
|
154
|
+
return onPath;
|
|
155
|
+
return findGhKnownPaths();
|
|
156
|
+
}
|
|
157
|
+
/** The repo context for all gh_* tools: the server process's current working directory. */
|
|
158
|
+
function ghCwd() {
|
|
159
|
+
return process.cwd();
|
|
160
|
+
}
|
|
161
|
+
/** Detail text for a non-zero exit: stderr first, then stdout, then the exit code. */
|
|
162
|
+
function detail(out) {
|
|
163
|
+
return out.stderr.trim() || out.stdout.trim() || `gh exited with code ${out.code}`;
|
|
164
|
+
}
|
|
165
|
+
// ---------------------------------------------------------------- gh_auth
|
|
166
|
+
export async function handleGhAuth() {
|
|
167
|
+
const dir = ghCwd();
|
|
168
|
+
const ghBin = await resolveGh();
|
|
169
|
+
if (!ghBin)
|
|
170
|
+
return JSON.stringify({ error: GH_NOT_INSTALLED });
|
|
171
|
+
try {
|
|
172
|
+
const st = await runCmd(ghBin, ["auth", "status"], dir);
|
|
173
|
+
if (st.code === 0) {
|
|
174
|
+
return JSON.stringify({ success: true, message: "Already authenticated with GitHub." });
|
|
175
|
+
}
|
|
176
|
+
// Not authenticated: open a detached terminal window for interactive login,
|
|
177
|
+
// exactly as the reference does. The window is spawned detached with ignored
|
|
178
|
+
// stdio so it outlives this call; cmd /k keeps it open for the user.
|
|
179
|
+
// The resolved absolute gh path is used so the window works even when gh is
|
|
180
|
+
// not on the server process's PATH.
|
|
181
|
+
const escapedDir = dir.replace(/"/g, '""');
|
|
182
|
+
const shellCommand = `start "" /D "${escapedDir}" cmd.exe /k "${ghBin} auth login --git-protocol=https & exit"`;
|
|
183
|
+
try {
|
|
184
|
+
spawn("cmd.exe", ["/c", shellCommand], { detached: true, stdio: "ignore", cwd: dir });
|
|
185
|
+
}
|
|
186
|
+
catch (e) {
|
|
187
|
+
return JSON.stringify({
|
|
188
|
+
error: `Auth check failed: ${e instanceof Error ? e.message : String(e)}`,
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
return JSON.stringify({
|
|
192
|
+
success: true,
|
|
193
|
+
message: "Opened a terminal window for GitHub authentication. Please sign in there.",
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
catch (e) {
|
|
197
|
+
return JSON.stringify({ error: `Auth check failed: ${e instanceof Error ? e.message : String(e)}` });
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
// ----------------------------------------------------------- gh_create_issue
|
|
201
|
+
export async function handleGhCreateIssue(title, body, labels) {
|
|
202
|
+
const dir = ghCwd();
|
|
203
|
+
const ghBin = await resolveGh();
|
|
204
|
+
if (!ghBin)
|
|
205
|
+
return JSON.stringify({ error: GH_NOT_INSTALLED });
|
|
206
|
+
try {
|
|
207
|
+
let tempFilePath = "";
|
|
208
|
+
const ghArgs = ["issue", "create", "--title", title];
|
|
209
|
+
if (body) {
|
|
210
|
+
tempFilePath = nodePath.join(dir, `gh_issue_body_${Date.now()}.md`);
|
|
211
|
+
fs.writeFileSync(tempFilePath, body, "utf-8");
|
|
212
|
+
ghArgs.push("--body-file", tempFilePath);
|
|
213
|
+
}
|
|
214
|
+
if (labels) {
|
|
215
|
+
for (const label of labels) {
|
|
216
|
+
ghArgs.push("-l", label);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
const out = await runCmd(ghBin, ghArgs, dir);
|
|
220
|
+
if (tempFilePath) {
|
|
221
|
+
try {
|
|
222
|
+
fs.rmSync(tempFilePath, { force: true });
|
|
223
|
+
}
|
|
224
|
+
catch {
|
|
225
|
+
// best-effort cleanup
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
if (out.code === 0)
|
|
229
|
+
return JSON.stringify({ success: true, url: out.stdout.trim() });
|
|
230
|
+
return JSON.stringify({ error: `Failed to create issue: ${detail(out)}` });
|
|
231
|
+
}
|
|
232
|
+
catch (e) {
|
|
233
|
+
return JSON.stringify({ error: `Create issue failed: ${e instanceof Error ? e.message : String(e)}` });
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
// ------------------------------------------------------------ gh_list_issues
|
|
237
|
+
export async function handleGhListIssues(state, labels, limit) {
|
|
238
|
+
const dir = ghCwd();
|
|
239
|
+
const ghBin = await resolveGh();
|
|
240
|
+
if (!ghBin)
|
|
241
|
+
return JSON.stringify({ error: GH_NOT_INSTALLED });
|
|
242
|
+
const st = state ?? "open";
|
|
243
|
+
const lim = typeof limit === "number" && Number.isFinite(limit) && limit >= 1
|
|
244
|
+
? Math.min(50, Math.floor(limit))
|
|
245
|
+
: 10;
|
|
246
|
+
try {
|
|
247
|
+
const ghArgs = [
|
|
248
|
+
"issue",
|
|
249
|
+
"list",
|
|
250
|
+
"--state",
|
|
251
|
+
st,
|
|
252
|
+
"--limit",
|
|
253
|
+
String(lim),
|
|
254
|
+
"--json",
|
|
255
|
+
"number,title,state,url,labels",
|
|
256
|
+
];
|
|
257
|
+
if (labels) {
|
|
258
|
+
for (const label of labels) {
|
|
259
|
+
ghArgs.push("-l", label);
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
const out = await runCmd(ghBin, ghArgs, dir);
|
|
263
|
+
if (out.code === 0) {
|
|
264
|
+
try {
|
|
265
|
+
return JSON.stringify({ issues: JSON.parse(out.stdout) });
|
|
266
|
+
}
|
|
267
|
+
catch {
|
|
268
|
+
return JSON.stringify({ error: "Failed to parse issue list output" });
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
return JSON.stringify({ error: `List issues failed: ${detail(out)}` });
|
|
272
|
+
}
|
|
273
|
+
catch (e) {
|
|
274
|
+
return JSON.stringify({ error: `List issues failed: ${e instanceof Error ? e.message : String(e)}` });
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
// ---------------------------------------------------------- gh_view_comments
|
|
278
|
+
export async function handleGhViewComments(number, type) {
|
|
279
|
+
const dir = ghCwd();
|
|
280
|
+
const ghBin = await resolveGh();
|
|
281
|
+
if (!ghBin)
|
|
282
|
+
return JSON.stringify({ error: GH_NOT_INSTALLED });
|
|
283
|
+
try {
|
|
284
|
+
// Fallback to standard gh command for reliable JSON parsing of comments
|
|
285
|
+
const ghArgs = type === "pr"
|
|
286
|
+
? ["pr", "view", String(number), "--json", "comments"]
|
|
287
|
+
: ["issue", "view", String(number), "--json", "comments"];
|
|
288
|
+
const out = await runCmd(ghBin, ghArgs, dir);
|
|
289
|
+
if (out.code === 0) {
|
|
290
|
+
try {
|
|
291
|
+
const data = JSON.parse(out.stdout);
|
|
292
|
+
return JSON.stringify({ comments: data.comments || [] });
|
|
293
|
+
}
|
|
294
|
+
catch {
|
|
295
|
+
return JSON.stringify({ raw_output: out.stdout });
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
return JSON.stringify({ error: `View comments failed: ${detail(out)}` });
|
|
299
|
+
}
|
|
300
|
+
catch (e) {
|
|
301
|
+
return JSON.stringify({ error: `View comments failed: ${e instanceof Error ? e.message : String(e)}` });
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
// ------------------------------------------------------------- gh_create_pr
|
|
305
|
+
export async function handleGhCreatePr(title, body, head_branch, base_branch) {
|
|
306
|
+
const dir = ghCwd();
|
|
307
|
+
const ghBin = await resolveGh();
|
|
308
|
+
if (!ghBin)
|
|
309
|
+
return JSON.stringify({ error: GH_NOT_INSTALLED });
|
|
310
|
+
const base = base_branch || "main";
|
|
311
|
+
try {
|
|
312
|
+
let tempFilePath = "";
|
|
313
|
+
const ghArgs = ["pr", "create", "--title", title, "--head", head_branch, "--base", base];
|
|
314
|
+
if (body) {
|
|
315
|
+
tempFilePath = nodePath.join(dir, `gh_pr_body_${Date.now()}.md`);
|
|
316
|
+
fs.writeFileSync(tempFilePath, body, "utf-8");
|
|
317
|
+
ghArgs.push("--body-file", tempFilePath);
|
|
318
|
+
}
|
|
319
|
+
const out = await runCmd(ghBin, ghArgs, dir);
|
|
320
|
+
if (tempFilePath) {
|
|
321
|
+
try {
|
|
322
|
+
fs.rmSync(tempFilePath, { force: true });
|
|
323
|
+
}
|
|
324
|
+
catch {
|
|
325
|
+
// best-effort cleanup
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
if (out.code === 0)
|
|
329
|
+
return JSON.stringify({ success: true, url: out.stdout.trim() });
|
|
330
|
+
return JSON.stringify({ error: `Failed to create PR: ${detail(out)}` });
|
|
331
|
+
}
|
|
332
|
+
catch (e) {
|
|
333
|
+
return JSON.stringify({ error: `Create PR failed: ${e instanceof Error ? e.message : String(e)}` });
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
// -------------------------------------------------------------- gh_list_prs
|
|
337
|
+
export async function handleGhListPrs(state, limit) {
|
|
338
|
+
const dir = ghCwd();
|
|
339
|
+
const ghBin = await resolveGh();
|
|
340
|
+
if (!ghBin)
|
|
341
|
+
return JSON.stringify({ error: GH_NOT_INSTALLED });
|
|
342
|
+
const st = state ?? "open";
|
|
343
|
+
const lim = typeof limit === "number" && Number.isFinite(limit) && limit >= 1
|
|
344
|
+
? Math.min(50, Math.floor(limit))
|
|
345
|
+
: 10;
|
|
346
|
+
try {
|
|
347
|
+
const ghArgs = [
|
|
348
|
+
"pr",
|
|
349
|
+
"list",
|
|
350
|
+
"--state",
|
|
351
|
+
st,
|
|
352
|
+
"--limit",
|
|
353
|
+
String(lim),
|
|
354
|
+
"--json",
|
|
355
|
+
"number,title,state,url,headRefName,baseRefName",
|
|
356
|
+
];
|
|
357
|
+
const out = await runCmd(ghBin, ghArgs, dir);
|
|
358
|
+
if (out.code === 0) {
|
|
359
|
+
try {
|
|
360
|
+
return JSON.stringify({ pull_requests: JSON.parse(out.stdout) });
|
|
361
|
+
}
|
|
362
|
+
catch {
|
|
363
|
+
return JSON.stringify({ error: "Failed to parse PR list output" });
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
return JSON.stringify({ error: `List PRs failed: ${detail(out)}` });
|
|
367
|
+
}
|
|
368
|
+
catch (e) {
|
|
369
|
+
return JSON.stringify({ error: `List PRs failed: ${e instanceof Error ? e.message : String(e)}` });
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
// ---------------------------------------------------------- gh_view_pr_diff
|
|
373
|
+
export async function handleGhViewPrDiff(number) {
|
|
374
|
+
const dir = ghCwd();
|
|
375
|
+
const ghBin = await resolveGh();
|
|
376
|
+
if (!ghBin)
|
|
377
|
+
return JSON.stringify({ error: GH_NOT_INSTALLED });
|
|
378
|
+
try {
|
|
379
|
+
const out = await runCmd(ghBin, ["pr", "diff", String(number)], dir);
|
|
380
|
+
if (out.code === 0) {
|
|
381
|
+
return JSON.stringify({
|
|
382
|
+
diff: out.stdout.substring(0, 50000) + (out.stdout.length > 50000 ? "\n... (truncated)" : ""),
|
|
383
|
+
});
|
|
384
|
+
}
|
|
385
|
+
return JSON.stringify({ error: `Fetch PR diff failed: ${detail(out)}` });
|
|
386
|
+
}
|
|
387
|
+
catch (e) {
|
|
388
|
+
return JSON.stringify({ error: `Fetch PR diff failed: ${e instanceof Error ? e.message : String(e)}` });
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
// ----------------------------------------------------------------- gh_push
|
|
392
|
+
export async function handleGhPush(branch) {
|
|
393
|
+
const dir = ghCwd();
|
|
394
|
+
try {
|
|
395
|
+
const gitArgs = ["push", "origin"];
|
|
396
|
+
if (branch)
|
|
397
|
+
gitArgs.push(branch);
|
|
398
|
+
const out = await runCmd("git", gitArgs, dir);
|
|
399
|
+
if (out.code === 0)
|
|
400
|
+
return JSON.stringify({ success: true, message: "Pushed successfully." });
|
|
401
|
+
return JSON.stringify({ error: `Git push failed: ${detail(out)}` });
|
|
402
|
+
}
|
|
403
|
+
catch (e) {
|
|
404
|
+
return JSON.stringify({ error: `Git push failed: ${e instanceof Error ? e.message : String(e)}` });
|
|
405
|
+
}
|
|
406
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
// gh/schema.ts — argument schemas for the 8 gh_* tools.
|
|
2
|
+
// (MCP filesystem fork; thin gh CLI wrappers that operate against the repo at the
|
|
3
|
+
// server process's current working directory — no working-directory parameter, same
|
|
4
|
+
// "current working directory context" as the git/shell tools.)
|
|
5
|
+
import { z } from "zod";
|
|
6
|
+
export const GhAuthArgsSchema = z.object({});
|
|
7
|
+
export const GhCreateIssueArgsSchema = z.object({
|
|
8
|
+
title: z.string(),
|
|
9
|
+
body: z.string().optional(),
|
|
10
|
+
labels: z.array(z.string()).optional(),
|
|
11
|
+
});
|
|
12
|
+
export const GhListIssuesArgsSchema = z.object({
|
|
13
|
+
state: z.enum(["open", "closed"]).optional().default("open"),
|
|
14
|
+
labels: z.array(z.string()).optional(),
|
|
15
|
+
limit: z.number().min(1).max(50).optional().default(10),
|
|
16
|
+
});
|
|
17
|
+
export const GhViewCommentsArgsSchema = z.object({
|
|
18
|
+
number: z.number().describe("The issue or PR number"),
|
|
19
|
+
type: z.enum(["issue", "pr"]).default("issue").describe("Whether it's an issue or a pull request"),
|
|
20
|
+
});
|
|
21
|
+
export const GhCreatePrArgsSchema = z.object({
|
|
22
|
+
title: z.string(),
|
|
23
|
+
body: z.string().optional(),
|
|
24
|
+
head_branch: z.string().describe("The branch containing your changes"),
|
|
25
|
+
base_branch: z.string().default("main").describe("The branch you want to merge into (e.g., main, master)"),
|
|
26
|
+
});
|
|
27
|
+
export const GhListPrsArgsSchema = z.object({
|
|
28
|
+
state: z.enum(["open", "closed"]).optional().default("open"),
|
|
29
|
+
limit: z.number().min(1).max(50).optional().default(10),
|
|
30
|
+
});
|
|
31
|
+
export const GhViewPrDiffArgsSchema = z.object({
|
|
32
|
+
number: z.number().describe("The PR number"),
|
|
33
|
+
});
|
|
34
|
+
export const GhPushArgsSchema = z.object({
|
|
35
|
+
branch: z.string().optional().describe("Optional: The branch to push. Defaults to current branch."),
|
|
36
|
+
});
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
// git/handler.ts — git CLI wrappers for the 6 git_* tools (MCP filesystem fork).
|
|
2
|
+
//
|
|
3
|
+
// Design: spawn the `git` binary directly (git is on PATH; no new dependency).
|
|
4
|
+
// Every tool operates against the repo at the server process's current working
|
|
5
|
+
// directory (process.cwd()) — the same "current working directory context" the
|
|
6
|
+
// shell tools use — and takes NO working-directory parameter (matching the
|
|
7
|
+
// reference Beledarian tools). Path arguments are resolved relative to that
|
|
8
|
+
// directory. All argument values (messages, branch names, file paths) are passed
|
|
9
|
+
// as individual argv elements (no shell), so quotes / Unicode / special
|
|
10
|
+
// characters pass through verbatim.
|
|
11
|
+
//
|
|
12
|
+
// Return shape mirrors the reference implementation: each handler returns a JSON
|
|
13
|
+
// text string carrying the reference's fields ({error} on failure, {diff},
|
|
14
|
+
// {history}, {success, ...}) instead of throwing, so a working directory that is
|
|
15
|
+
// not inside a git repository yields a clear error string rather than a crash.
|
|
16
|
+
import { spawn } from "child_process";
|
|
17
|
+
import nodePath from "path";
|
|
18
|
+
/** Run `git <args>` with the given cwd. Never rejects; spawn failures resolve with code -1. */
|
|
19
|
+
function runGit(args, cwd) {
|
|
20
|
+
return new Promise((resolve) => {
|
|
21
|
+
let child;
|
|
22
|
+
try {
|
|
23
|
+
child = spawn("git", args, { cwd });
|
|
24
|
+
}
|
|
25
|
+
catch (e) {
|
|
26
|
+
resolve({ code: -1, stdout: "", stderr: e instanceof Error ? e.message : String(e) });
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
let stdout = "";
|
|
30
|
+
let stderr = "";
|
|
31
|
+
let settled = false;
|
|
32
|
+
const finish = (code) => {
|
|
33
|
+
if (settled)
|
|
34
|
+
return;
|
|
35
|
+
settled = true;
|
|
36
|
+
resolve({ code, stdout, stderr });
|
|
37
|
+
};
|
|
38
|
+
if (child.stdout) {
|
|
39
|
+
child.stdout.on("data", (d) => {
|
|
40
|
+
stdout += d.toString("utf8");
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
if (child.stderr) {
|
|
44
|
+
child.stderr.on("data", (d) => {
|
|
45
|
+
stderr += d.toString("utf8");
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
child.on("error", (e) => {
|
|
49
|
+
// e.g. ENOENT when the git binary cannot be found on PATH
|
|
50
|
+
stderr += (stderr ? "\n" : "") + (e.message || String(e));
|
|
51
|
+
finish(-1);
|
|
52
|
+
});
|
|
53
|
+
child.on("close", (code) => finish(code === null ? -1 : code));
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
/** JSON error text matching the reference shape: {error: "Git <what> failed: <detail>"}. */
|
|
57
|
+
function failText(out, what) {
|
|
58
|
+
const detail = out.stderr.trim() || out.stdout.trim() || `git exited with code ${out.code}`;
|
|
59
|
+
return JSON.stringify({ error: `Git ${what} failed: ${detail}` });
|
|
60
|
+
}
|
|
61
|
+
/** The repo context for all git_* tools: the server process's current working directory. */
|
|
62
|
+
function gitCwd() {
|
|
63
|
+
return process.cwd();
|
|
64
|
+
}
|
|
65
|
+
export async function handleGitStatus() {
|
|
66
|
+
const dir = gitCwd();
|
|
67
|
+
const st = await runGit(["status", "--porcelain=v1", "--branch"], dir);
|
|
68
|
+
if (st.code !== 0) {
|
|
69
|
+
return failText(st, "status");
|
|
70
|
+
}
|
|
71
|
+
const lines = st.stdout.replace(/\r/g, "").split("\n");
|
|
72
|
+
let current = null;
|
|
73
|
+
let tracking = null;
|
|
74
|
+
let ahead = 0;
|
|
75
|
+
let behind = 0;
|
|
76
|
+
const staged = [];
|
|
77
|
+
const notStaged = [];
|
|
78
|
+
const untracked = [];
|
|
79
|
+
const files = [];
|
|
80
|
+
for (let i = 0; i < lines.length; i++) {
|
|
81
|
+
const line = lines[i];
|
|
82
|
+
if (i === 0 && line.startsWith("## ")) {
|
|
83
|
+
// Header: "## branch", "## branch...upstream", "## branch...upstream [ahead 1, behind 2]",
|
|
84
|
+
// or "## HEAD (no branch)" (detached HEAD).
|
|
85
|
+
let rest = line.slice(3);
|
|
86
|
+
const bracket = / \[([^\]]*)\]$/.exec(rest);
|
|
87
|
+
if (bracket) {
|
|
88
|
+
const am = /ahead (\d+)/.exec(bracket[1]);
|
|
89
|
+
const bm = /behind (\d+)/.exec(bracket[1]);
|
|
90
|
+
if (am)
|
|
91
|
+
ahead = parseInt(am[1], 10);
|
|
92
|
+
if (bm)
|
|
93
|
+
behind = parseInt(bm[1], 10);
|
|
94
|
+
rest = rest.slice(0, rest.length - bracket[0].length);
|
|
95
|
+
}
|
|
96
|
+
const sep = rest.indexOf("...");
|
|
97
|
+
if (sep >= 0) {
|
|
98
|
+
const left = rest.slice(0, sep);
|
|
99
|
+
current = left === "HEAD" ? null : left;
|
|
100
|
+
tracking = rest.slice(sep + 3) || null;
|
|
101
|
+
}
|
|
102
|
+
else if (rest.startsWith("HEAD (")) {
|
|
103
|
+
current = null;
|
|
104
|
+
tracking = null;
|
|
105
|
+
}
|
|
106
|
+
else {
|
|
107
|
+
current = rest || null;
|
|
108
|
+
tracking = null;
|
|
109
|
+
}
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
if (!line)
|
|
113
|
+
continue;
|
|
114
|
+
const index = line[0];
|
|
115
|
+
const working = line[1];
|
|
116
|
+
let p = line.slice(3);
|
|
117
|
+
// Rename/copy entries: "R old -> new" / "C old -> new" — report the destination.
|
|
118
|
+
if (index === "R" || index === "C") {
|
|
119
|
+
const arrow = p.indexOf(" -> ");
|
|
120
|
+
if (arrow >= 0)
|
|
121
|
+
p = p.slice(arrow + 4);
|
|
122
|
+
}
|
|
123
|
+
files.push({ path: p, index, working_dir: working });
|
|
124
|
+
if (index === "?" && working === "?") {
|
|
125
|
+
untracked.push(p);
|
|
126
|
+
}
|
|
127
|
+
else {
|
|
128
|
+
if (index !== " " && index !== "?")
|
|
129
|
+
staged.push(p);
|
|
130
|
+
if (working !== " " && working !== "?")
|
|
131
|
+
notStaged.push(p);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
const isClean = staged.length === 0 && notStaged.length === 0 && untracked.length === 0;
|
|
135
|
+
return JSON.stringify({ current, tracking, ahead, behind, isClean, staged, notStaged, untracked, files });
|
|
136
|
+
}
|
|
137
|
+
export async function handleGitDiff(file_path, cached) {
|
|
138
|
+
const dir = gitCwd();
|
|
139
|
+
const args = ["diff"];
|
|
140
|
+
if (cached)
|
|
141
|
+
args.push("--cached");
|
|
142
|
+
if (file_path) {
|
|
143
|
+
args.push("--", nodePath.resolve(dir, file_path));
|
|
144
|
+
}
|
|
145
|
+
const d = await runGit(args, dir);
|
|
146
|
+
if (d.code !== 0) {
|
|
147
|
+
return failText(d, "diff");
|
|
148
|
+
}
|
|
149
|
+
const text = d.stdout.replace(/\n$/, "");
|
|
150
|
+
return JSON.stringify({ diff: text.length > 0 ? text : "No changes." });
|
|
151
|
+
}
|
|
152
|
+
export async function handleGitCommit(message) {
|
|
153
|
+
const dir = gitCwd();
|
|
154
|
+
// Pre-image stat of what is staged (mirrors the reference's {changed, insertions, deletions} summary).
|
|
155
|
+
const summary = { changed: 0, insertions: 0, deletions: 0 };
|
|
156
|
+
const pre = await runGit(["diff", "--cached", "--shortstat"], dir);
|
|
157
|
+
if (pre.code === 0 && pre.stdout.trim()) {
|
|
158
|
+
const mf = /(\d+) files? changed/.exec(pre.stdout);
|
|
159
|
+
if (mf)
|
|
160
|
+
summary.changed = parseInt(mf[1], 10);
|
|
161
|
+
const mi = /(\d+) insertions?\(\+\)/.exec(pre.stdout);
|
|
162
|
+
if (mi)
|
|
163
|
+
summary.insertions = parseInt(mi[1], 10);
|
|
164
|
+
const md = /(\d+) deletions?\(-\)/.exec(pre.stdout);
|
|
165
|
+
if (md)
|
|
166
|
+
summary.deletions = parseInt(md[1], 10);
|
|
167
|
+
}
|
|
168
|
+
// Standard git behavior: commit only what is staged.
|
|
169
|
+
const c = await runGit(["commit", "-m", message], dir);
|
|
170
|
+
if (c.code !== 0) {
|
|
171
|
+
return failText(c, "commit");
|
|
172
|
+
}
|
|
173
|
+
return JSON.stringify({ success: true, summary });
|
|
174
|
+
}
|
|
175
|
+
export async function handleGitLog(max_count) {
|
|
176
|
+
const dir = gitCwd();
|
|
177
|
+
const n = typeof max_count === "number" && Number.isFinite(max_count) && max_count > 0
|
|
178
|
+
? Math.floor(max_count)
|
|
179
|
+
: 10;
|
|
180
|
+
const l = await runGit(["log", "-n", String(n), "--pretty=format:%h %s (%an, %ar)"], dir);
|
|
181
|
+
if (l.code !== 0) {
|
|
182
|
+
return failText(l, "log");
|
|
183
|
+
}
|
|
184
|
+
const history = l.stdout
|
|
185
|
+
.split("\n")
|
|
186
|
+
.map((s) => s.trimEnd())
|
|
187
|
+
.filter((s) => s.length > 0);
|
|
188
|
+
return JSON.stringify({ history });
|
|
189
|
+
}
|
|
190
|
+
export async function handleGitAdd(paths) {
|
|
191
|
+
const dir = gitCwd();
|
|
192
|
+
const args = paths && paths.length > 0
|
|
193
|
+
? ["add", "--", ...paths.map((p) => nodePath.resolve(dir, p))]
|
|
194
|
+
: ["add", "."];
|
|
195
|
+
const a = await runGit(args, dir);
|
|
196
|
+
if (a.code !== 0) {
|
|
197
|
+
return failText(a, "add");
|
|
198
|
+
}
|
|
199
|
+
return JSON.stringify({ success: true, message: "Files staged successfully." });
|
|
200
|
+
}
|
|
201
|
+
export async function handleGitCheckout(branch_name, create_new) {
|
|
202
|
+
const dir = gitCwd();
|
|
203
|
+
const args = create_new ? ["checkout", "-b", branch_name] : ["checkout", branch_name];
|
|
204
|
+
const c = await runGit(args, dir);
|
|
205
|
+
if (c.code !== 0) {
|
|
206
|
+
return failText(c, "checkout");
|
|
207
|
+
}
|
|
208
|
+
return JSON.stringify({ success: true, message: `Switched to branch '${branch_name}'.` });
|
|
209
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
// git/schema.ts — argument schemas for the 6 git_* tools.
|
|
2
|
+
// (MCP filesystem fork; thin git CLI wrappers that operate against the repo at the
|
|
3
|
+
// server process's current working directory — no working-directory parameter, same
|
|
4
|
+
// "current working directory context" as the shell tools.)
|
|
5
|
+
import { z } from "zod";
|
|
6
|
+
export const GitStatusArgsSchema = z.object({});
|
|
7
|
+
export const GitDiffArgsSchema = z.object({
|
|
8
|
+
file_path: z.string().optional().describe("Optional: Path to specific file to diff."),
|
|
9
|
+
cached: z.boolean().optional().describe("Optional: Show staged changes only (git diff --cached)."),
|
|
10
|
+
});
|
|
11
|
+
export const GitCommitArgsSchema = z.object({
|
|
12
|
+
message: z.string(),
|
|
13
|
+
});
|
|
14
|
+
export const GitLogArgsSchema = z.object({
|
|
15
|
+
max_count: z.number().optional().describe("Max number of commits to return (default: 10)"),
|
|
16
|
+
});
|
|
17
|
+
export const GitAddArgsSchema = z.object({
|
|
18
|
+
paths: z.array(z.string()).optional().describe("Optional: Specific file paths to stage. If omitted, stages all changes."),
|
|
19
|
+
});
|
|
20
|
+
export const GitCheckoutArgsSchema = z.object({
|
|
21
|
+
branch_name: z.string().describe("Name of the branch to checkout."),
|
|
22
|
+
create_new: z.boolean().optional().default(false).describe("If true, creates the branch if it doesn't exist (like git checkout -b)."),
|
|
23
|
+
});
|
|
@@ -51,7 +51,9 @@ const URI_SCHEME = /^[A-Za-z][A-Za-z0-9+.-]+:/;
|
|
|
51
51
|
* Only the opener executable changes; the target still passes the full
|
|
52
52
|
* URL/path policy below.
|
|
53
53
|
*/
|
|
54
|
-
|
|
54
|
+
// Exported (v0.2.23) so the system tools (preview_html) can reuse the exact
|
|
55
|
+
// same opener resolution — no behavior change.
|
|
56
|
+
export function openerPath() {
|
|
55
57
|
if (process.platform !== "win32") {
|
|
56
58
|
throw new Error("launch_file is supported only on Windows");
|
|
57
59
|
}
|