mcp-probe-kit 3.0.9 → 3.0.11
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 +14 -4
- package/build/lib/__tests__/gitnexus-bridge.unit.test.d.ts +1 -0
- package/build/lib/__tests__/gitnexus-bridge.unit.test.js +96 -0
- package/build/lib/gitnexus-bridge.d.ts +23 -0
- package/build/lib/gitnexus-bridge.js +526 -21
- package/build/schemas/code-analysis-tools.d.ts +9 -1
- package/build/schemas/code-analysis-tools.js +9 -1
- package/build/schemas/git-tools.d.ts +1 -1
- package/build/schemas/git-tools.js +1 -1
- package/build/schemas/index.d.ts +23 -3
- package/build/schemas/orchestration-tools.d.ts +13 -1
- package/build/schemas/orchestration-tools.js +13 -1
- package/build/schemas/output/core-tools.d.ts +130 -1
- package/build/schemas/output/core-tools.js +71 -1
- package/build/schemas/output/index.d.ts +2 -2
- package/build/schemas/output/index.js +2 -2
- package/build/schemas/output/project-tools.d.ts +13 -0
- package/build/schemas/output/project-tools.js +9 -0
- package/build/schemas/structured-output.d.ts +358 -5
- package/build/schemas/structured-output.js +169 -5
- package/build/tools/__tests__/code_insight.unit.test.js +81 -1
- package/build/tools/__tests__/fix_bug.unit.test.d.ts +1 -0
- package/build/tools/__tests__/fix_bug.unit.test.js +31 -0
- package/build/tools/__tests__/gencommit.unit.test.d.ts +1 -0
- package/build/tools/__tests__/gencommit.unit.test.js +41 -0
- package/build/tools/__tests__/init_project_context.unit.test.d.ts +1 -0
- package/build/tools/__tests__/init_project_context.unit.test.js +63 -0
- package/build/tools/__tests__/start_bugfix.unit.test.js +10 -0
- package/build/tools/__tests__/start_feature.unit.test.js +10 -0
- package/build/tools/code_insight.d.ts +10 -0
- package/build/tools/code_insight.js +156 -3
- package/build/tools/fix_bug.d.ts +3 -3
- package/build/tools/fix_bug.js +297 -312
- package/build/tools/gencommit.js +144 -123
- package/build/tools/init_project_context.js +211 -53
- package/build/tools/start_bugfix.js +170 -70
- package/build/tools/start_feature.js +79 -25
- package/docs/data/tools.js +33 -31
- package/docs/i18n/all-tools/en.json +9 -9
- package/docs/i18n/all-tools/ja.json +9 -9
- package/docs/i18n/all-tools/ko.json +9 -9
- package/docs/i18n/all-tools/zh-CN.json +9 -9
- package/docs/i18n/en.json +480 -481
- package/docs/i18n/ja.json +478 -479
- package/docs/i18n/ko.json +480 -481
- package/docs/i18n/zh-CN.json +480 -481
- package/docs/index.html +2 -2
- package/docs/pages/all-tools.html +2 -2
- package/docs/pages/examples.html +2 -2
- package/docs/pages/getting-started.html +2 -2
- package/docs/pages/migration.html +2 -2
- package/package.json +1 -2
|
@@ -1,12 +1,41 @@
|
|
|
1
1
|
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
2
2
|
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
|
|
3
|
+
import { execFileSync, spawn } from "node:child_process";
|
|
3
4
|
import * as fs from "node:fs";
|
|
5
|
+
import * as os from "node:os";
|
|
4
6
|
import * as path from "node:path";
|
|
5
7
|
import { isAbortError, throwIfAborted, } from "./tool-execution-context.js";
|
|
6
8
|
const DEFAULT_CONNECT_TIMEOUT_MS = readIntEnv("MCP_GITNEXUS_CONNECT_TIMEOUT_MS", 12000);
|
|
7
9
|
const DEFAULT_CALL_TIMEOUT_MS = readIntEnv("MCP_GITNEXUS_TIMEOUT_MS", 20000);
|
|
8
10
|
const DEFAULT_GITNEXUS_ARGS = ["-y", "gitnexus@latest", "mcp"];
|
|
9
11
|
const FAILURE_CACHE_TTL_MS = readIntEnv("MCP_GITNEXUS_FAILURE_CACHE_TTL_MS", 30000);
|
|
12
|
+
const DEFAULT_IGNORED_DIRS = new Set([
|
|
13
|
+
".git",
|
|
14
|
+
".gitnexus",
|
|
15
|
+
".mcp-probe-kit",
|
|
16
|
+
".playwright-cli",
|
|
17
|
+
".turbo",
|
|
18
|
+
".next",
|
|
19
|
+
".nuxt",
|
|
20
|
+
".svelte-kit",
|
|
21
|
+
".idea",
|
|
22
|
+
".vscode",
|
|
23
|
+
".gradle",
|
|
24
|
+
".npm",
|
|
25
|
+
".pnpm-store",
|
|
26
|
+
".yarn",
|
|
27
|
+
".cache",
|
|
28
|
+
".cargo",
|
|
29
|
+
".rustup",
|
|
30
|
+
"node_modules",
|
|
31
|
+
"dist",
|
|
32
|
+
"build",
|
|
33
|
+
"coverage",
|
|
34
|
+
"output",
|
|
35
|
+
"temp",
|
|
36
|
+
"tmp",
|
|
37
|
+
]);
|
|
38
|
+
const DEFAULT_IGNORED_FILES = [/^\.env(\..+)?$/i];
|
|
10
39
|
let bridgeFailureUntil = 0;
|
|
11
40
|
let bridgeFailureReason = "";
|
|
12
41
|
function readIntEnv(name, fallback) {
|
|
@@ -40,31 +69,215 @@ function splitArgs(raw) {
|
|
|
40
69
|
.split(/\s+/)
|
|
41
70
|
.filter(Boolean);
|
|
42
71
|
}
|
|
43
|
-
function
|
|
72
|
+
function resolvePreferredRepoName(requestedRepo) {
|
|
73
|
+
const requested = requestedRepo?.trim();
|
|
74
|
+
if (requested) {
|
|
75
|
+
return requested;
|
|
76
|
+
}
|
|
44
77
|
const explicit = process.env.MCP_GITNEXUS_REPO?.trim();
|
|
45
78
|
if (explicit) {
|
|
46
79
|
return explicit;
|
|
47
80
|
}
|
|
48
|
-
|
|
81
|
+
return undefined;
|
|
82
|
+
}
|
|
83
|
+
function resolveRequestedProjectRoot(projectRoot) {
|
|
84
|
+
const requested = projectRoot?.trim();
|
|
85
|
+
if (requested) {
|
|
86
|
+
return path.resolve(requested);
|
|
87
|
+
}
|
|
88
|
+
return path.resolve(process.cwd());
|
|
89
|
+
}
|
|
90
|
+
function inferCandidateRepoNames(baseDir = process.cwd()) {
|
|
91
|
+
const candidates = [];
|
|
92
|
+
const pkgPath = path.join(baseDir, "package.json");
|
|
49
93
|
try {
|
|
50
94
|
if (fs.existsSync(pkgPath)) {
|
|
51
95
|
const parsed = JSON.parse(fs.readFileSync(pkgPath, "utf-8"));
|
|
52
96
|
const pkgName = typeof parsed.name === "string" ? parsed.name.trim() : "";
|
|
53
97
|
if (pkgName) {
|
|
54
|
-
|
|
98
|
+
candidates.push(pkgName);
|
|
55
99
|
}
|
|
56
100
|
}
|
|
57
101
|
}
|
|
58
102
|
catch {
|
|
59
103
|
// ignore parse failure
|
|
60
104
|
}
|
|
61
|
-
const base = path.basename(
|
|
62
|
-
|
|
105
|
+
const base = path.basename(baseDir).trim();
|
|
106
|
+
if (base) {
|
|
107
|
+
candidates.push(base);
|
|
108
|
+
}
|
|
109
|
+
return Array.from(new Set(candidates.filter(Boolean)));
|
|
110
|
+
}
|
|
111
|
+
function parseAvailableReposFromError(text) {
|
|
112
|
+
const match = text.match(/Available:\s*(.+)$/i);
|
|
113
|
+
if (!match) {
|
|
114
|
+
return [];
|
|
115
|
+
}
|
|
116
|
+
return match[1]
|
|
117
|
+
.split(",")
|
|
118
|
+
.map((item) => item.trim())
|
|
119
|
+
.filter(Boolean);
|
|
63
120
|
}
|
|
64
121
|
function resolveBridgeCommand() {
|
|
65
122
|
const command = (process.env.MCP_GITNEXUS_COMMAND || "npx").trim() || "npx";
|
|
66
123
|
const args = splitArgs(process.env.MCP_GITNEXUS_ARGS);
|
|
67
|
-
return
|
|
124
|
+
return resolveSpawnCommand(command, args);
|
|
125
|
+
}
|
|
126
|
+
function resolveGitNexusCliCommand(subcommand) {
|
|
127
|
+
const command = (process.env.MCP_GITNEXUS_COMMAND || "npx").trim() || "npx";
|
|
128
|
+
const bridgeArgs = splitArgs(process.env.MCP_GITNEXUS_ARGS);
|
|
129
|
+
const flags = [];
|
|
130
|
+
let packageSpec = "gitnexus@latest";
|
|
131
|
+
for (const arg of bridgeArgs) {
|
|
132
|
+
if (arg === "mcp") {
|
|
133
|
+
break;
|
|
134
|
+
}
|
|
135
|
+
if (arg.startsWith("-")) {
|
|
136
|
+
flags.push(arg);
|
|
137
|
+
continue;
|
|
138
|
+
}
|
|
139
|
+
packageSpec = arg;
|
|
140
|
+
break;
|
|
141
|
+
}
|
|
142
|
+
return resolveSpawnCommand(command, [...flags, packageSpec, subcommand]);
|
|
143
|
+
}
|
|
144
|
+
export function resolveExecutableCommand(command, platform = process.platform) {
|
|
145
|
+
const normalized = (command || "").trim();
|
|
146
|
+
if (!normalized) {
|
|
147
|
+
return resolveExecutableCommand("npx", platform);
|
|
148
|
+
}
|
|
149
|
+
if (path.isAbsolute(normalized) && fs.existsSync(normalized)) {
|
|
150
|
+
return normalized;
|
|
151
|
+
}
|
|
152
|
+
const found = findExecutablePath(normalized, platform);
|
|
153
|
+
if (found) {
|
|
154
|
+
return found;
|
|
155
|
+
}
|
|
156
|
+
if (platform !== "win32") {
|
|
157
|
+
return normalized;
|
|
158
|
+
}
|
|
159
|
+
const lower = normalized.toLowerCase();
|
|
160
|
+
if (lower.endsWith(".cmd") || lower.endsWith(".exe") || lower.endsWith(".bat")) {
|
|
161
|
+
return normalized;
|
|
162
|
+
}
|
|
163
|
+
if (lower === "npx" || lower === "npm" || lower === "git") {
|
|
164
|
+
return `${normalized}.cmd`;
|
|
165
|
+
}
|
|
166
|
+
return normalized;
|
|
167
|
+
}
|
|
168
|
+
function findExecutablePath(command, platform = process.platform) {
|
|
169
|
+
const trimmed = (command || "").trim();
|
|
170
|
+
if (!trimmed || path.isAbsolute(trimmed)) {
|
|
171
|
+
return undefined;
|
|
172
|
+
}
|
|
173
|
+
const preferredPath = (candidates) => {
|
|
174
|
+
const existing = candidates.filter((candidate) => candidate && fs.existsSync(candidate));
|
|
175
|
+
if (existing.length === 0) {
|
|
176
|
+
return undefined;
|
|
177
|
+
}
|
|
178
|
+
if (platform !== "win32") {
|
|
179
|
+
return existing[0];
|
|
180
|
+
}
|
|
181
|
+
const lower = trimmed.toLowerCase();
|
|
182
|
+
const preferredExts = lower === "npx" || lower === "npm"
|
|
183
|
+
? [".cmd", ".exe", ".bat", ""]
|
|
184
|
+
: lower === "git"
|
|
185
|
+
? [".exe", ".cmd", ".bat", ""]
|
|
186
|
+
: [".exe", ".cmd", ".bat", ""];
|
|
187
|
+
for (const ext of preferredExts) {
|
|
188
|
+
const match = existing.find((candidate) => path.extname(candidate).toLowerCase() === ext);
|
|
189
|
+
if (match) {
|
|
190
|
+
return match;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
return existing[0];
|
|
194
|
+
};
|
|
195
|
+
if (platform === "win32") {
|
|
196
|
+
try {
|
|
197
|
+
const output = execFileSync("where.exe", [trimmed], {
|
|
198
|
+
encoding: "utf-8",
|
|
199
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
200
|
+
windowsHide: true,
|
|
201
|
+
}).trim();
|
|
202
|
+
const candidates = output.split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
|
|
203
|
+
const preferred = preferredPath(candidates);
|
|
204
|
+
if (preferred) {
|
|
205
|
+
return preferred;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
catch {
|
|
209
|
+
// fall back to PATH scan
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
const pathEntries = (process.env.PATH || "")
|
|
213
|
+
.split(path.delimiter)
|
|
214
|
+
.map((entry) => entry.trim())
|
|
215
|
+
.filter(Boolean);
|
|
216
|
+
const lower = trimmed.toLowerCase();
|
|
217
|
+
const extensions = platform === "win32"
|
|
218
|
+
? lower === "npx" || lower === "npm"
|
|
219
|
+
? [".cmd", ".exe", ".bat", ""]
|
|
220
|
+
: lower === "git"
|
|
221
|
+
? [".exe", ".cmd", ".bat", ""]
|
|
222
|
+
: [".exe", ".cmd", ".bat", ""]
|
|
223
|
+
: [""];
|
|
224
|
+
for (const entry of pathEntries) {
|
|
225
|
+
for (const ext of extensions) {
|
|
226
|
+
const candidate = path.join(entry, `${trimmed}${ext}`);
|
|
227
|
+
if (fs.existsSync(candidate)) {
|
|
228
|
+
return candidate;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
if (platform === "win32" && trimmed.toLowerCase() === "git") {
|
|
233
|
+
const commonCandidates = [
|
|
234
|
+
path.join(process.env["ProgramFiles"] || "C:\\Program Files", "Git", "cmd", "git.exe"),
|
|
235
|
+
path.join(process.env["ProgramFiles"] || "C:\\Program Files", "Git", "bin", "git.exe"),
|
|
236
|
+
path.join(process.env["ProgramFiles(x86)"] || "C:\\Program Files (x86)", "Git", "cmd", "git.exe"),
|
|
237
|
+
];
|
|
238
|
+
return preferredPath(commonCandidates);
|
|
239
|
+
}
|
|
240
|
+
return undefined;
|
|
241
|
+
}
|
|
242
|
+
function shouldWrapWithCmd(rawCommand, executable, platform = process.platform) {
|
|
243
|
+
if (platform !== "win32") {
|
|
244
|
+
return false;
|
|
245
|
+
}
|
|
246
|
+
const rawLower = (rawCommand || "").trim().toLowerCase();
|
|
247
|
+
const executableLower = (executable || "").trim().toLowerCase();
|
|
248
|
+
const ext = path.extname(executableLower);
|
|
249
|
+
if (!rawLower && !executableLower) {
|
|
250
|
+
return true;
|
|
251
|
+
}
|
|
252
|
+
if (ext === ".cmd" || ext === ".bat") {
|
|
253
|
+
return true;
|
|
254
|
+
}
|
|
255
|
+
if (ext === ".exe") {
|
|
256
|
+
return false;
|
|
257
|
+
}
|
|
258
|
+
return rawLower === "npx" || rawLower === "npm";
|
|
259
|
+
}
|
|
260
|
+
function quoteForCmd(executable) {
|
|
261
|
+
if (!executable.includes(" ")) {
|
|
262
|
+
return executable;
|
|
263
|
+
}
|
|
264
|
+
if (executable.startsWith("\"") && executable.endsWith("\"")) {
|
|
265
|
+
return executable;
|
|
266
|
+
}
|
|
267
|
+
return `"${executable}"`;
|
|
268
|
+
}
|
|
269
|
+
export function resolveSpawnCommand(command, args, platform = process.platform) {
|
|
270
|
+
const executable = resolveExecutableCommand(command, platform);
|
|
271
|
+
if (!shouldWrapWithCmd(command, executable, platform)) {
|
|
272
|
+
return {
|
|
273
|
+
command: executable,
|
|
274
|
+
args,
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
return {
|
|
278
|
+
command: process.env.ComSpec || "cmd.exe",
|
|
279
|
+
args: ["/d", "/s", "/c", quoteForCmd(executable), ...args],
|
|
280
|
+
};
|
|
68
281
|
}
|
|
69
282
|
function extractText(result) {
|
|
70
283
|
if (!result || typeof result !== "object") {
|
|
@@ -102,6 +315,225 @@ function normalizeError(error) {
|
|
|
102
315
|
}
|
|
103
316
|
return String(error);
|
|
104
317
|
}
|
|
318
|
+
function findGitRoot(startDir) {
|
|
319
|
+
let current = path.resolve(startDir);
|
|
320
|
+
while (true) {
|
|
321
|
+
const gitPath = path.join(current, ".git");
|
|
322
|
+
if (fs.existsSync(gitPath)) {
|
|
323
|
+
return current;
|
|
324
|
+
}
|
|
325
|
+
const parent = path.dirname(current);
|
|
326
|
+
if (parent === current) {
|
|
327
|
+
return undefined;
|
|
328
|
+
}
|
|
329
|
+
current = parent;
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
function createWorkspaceId(sourceRoot) {
|
|
333
|
+
const base = path.basename(sourceRoot).replace(/[^a-zA-Z0-9._-]+/g, "-") || "workspace";
|
|
334
|
+
return `${base}-${Date.now()}-${Math.random().toString(16).slice(2, 8)}`;
|
|
335
|
+
}
|
|
336
|
+
function shouldIgnoreForTempWorkspace(sourceRoot, entryPath) {
|
|
337
|
+
const relative = path.relative(sourceRoot, entryPath);
|
|
338
|
+
if (!relative || relative.startsWith("..")) {
|
|
339
|
+
return false;
|
|
340
|
+
}
|
|
341
|
+
const parts = relative.split(path.sep).filter(Boolean);
|
|
342
|
+
if (parts.some((part) => DEFAULT_IGNORED_DIRS.has(part))) {
|
|
343
|
+
return true;
|
|
344
|
+
}
|
|
345
|
+
const name = parts[parts.length - 1] || "";
|
|
346
|
+
return DEFAULT_IGNORED_FILES.some((pattern) => pattern.test(name));
|
|
347
|
+
}
|
|
348
|
+
async function runProcess(command, args, cwd, signal) {
|
|
349
|
+
await new Promise((resolve, reject) => {
|
|
350
|
+
const child = spawn(command, args, {
|
|
351
|
+
cwd,
|
|
352
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
353
|
+
signal,
|
|
354
|
+
windowsHide: true,
|
|
355
|
+
});
|
|
356
|
+
let stderr = "";
|
|
357
|
+
child.stderr?.on("data", (chunk) => {
|
|
358
|
+
stderr += String(chunk);
|
|
359
|
+
});
|
|
360
|
+
child.on("error", reject);
|
|
361
|
+
child.on("close", (code) => {
|
|
362
|
+
if (code === 0) {
|
|
363
|
+
resolve();
|
|
364
|
+
return;
|
|
365
|
+
}
|
|
366
|
+
reject(new Error(stderr.trim() || `${command} ${args.join(" ")} 退出码 ${code ?? "unknown"}`));
|
|
367
|
+
});
|
|
368
|
+
});
|
|
369
|
+
}
|
|
370
|
+
async function removePathWithRetry(targetPath, attempts = 6) {
|
|
371
|
+
let lastError;
|
|
372
|
+
for (let index = 0; index < attempts; index += 1) {
|
|
373
|
+
try {
|
|
374
|
+
fs.rmSync(targetPath, { recursive: true, force: true });
|
|
375
|
+
if (!fs.existsSync(targetPath)) {
|
|
376
|
+
return;
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
catch (error) {
|
|
380
|
+
lastError = error;
|
|
381
|
+
}
|
|
382
|
+
await new Promise((resolve) => setTimeout(resolve, 150 * (index + 1)));
|
|
383
|
+
}
|
|
384
|
+
if (fs.existsSync(targetPath)) {
|
|
385
|
+
throw lastError instanceof Error
|
|
386
|
+
? lastError
|
|
387
|
+
: new Error(`无法删除临时目录: ${targetPath}`);
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
async function cleanupStaleTempWorkspaces(tempWorkspaceRoot, keepPath) {
|
|
391
|
+
if (!fs.existsSync(tempWorkspaceRoot)) {
|
|
392
|
+
return;
|
|
393
|
+
}
|
|
394
|
+
const entries = fs.readdirSync(tempWorkspaceRoot, { withFileTypes: true });
|
|
395
|
+
for (const entry of entries) {
|
|
396
|
+
if (!entry.isDirectory()) {
|
|
397
|
+
continue;
|
|
398
|
+
}
|
|
399
|
+
const fullPath = path.join(tempWorkspaceRoot, entry.name);
|
|
400
|
+
if (keepPath && path.resolve(fullPath) === path.resolve(keepPath)) {
|
|
401
|
+
continue;
|
|
402
|
+
}
|
|
403
|
+
try {
|
|
404
|
+
await removePathWithRetry(fullPath, 3);
|
|
405
|
+
}
|
|
406
|
+
catch {
|
|
407
|
+
// keep best-effort cleanup silent; current run should not fail because of stale dirs
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
function copyIntoAnalysisWorkspace(sourceRoot, analysisRoot) {
|
|
412
|
+
fs.mkdirSync(analysisRoot, { recursive: true });
|
|
413
|
+
const walk = (fromDir, toDir) => {
|
|
414
|
+
fs.mkdirSync(toDir, { recursive: true });
|
|
415
|
+
const entries = fs.readdirSync(fromDir, { withFileTypes: true });
|
|
416
|
+
for (const entry of entries) {
|
|
417
|
+
const fromPath = path.join(fromDir, entry.name);
|
|
418
|
+
if (shouldIgnoreForTempWorkspace(sourceRoot, fromPath)) {
|
|
419
|
+
continue;
|
|
420
|
+
}
|
|
421
|
+
const toPath = path.join(toDir, entry.name);
|
|
422
|
+
if (entry.isDirectory()) {
|
|
423
|
+
walk(fromPath, toPath);
|
|
424
|
+
continue;
|
|
425
|
+
}
|
|
426
|
+
if (entry.isFile()) {
|
|
427
|
+
fs.copyFileSync(fromPath, toPath);
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
};
|
|
431
|
+
walk(sourceRoot, analysisRoot);
|
|
432
|
+
}
|
|
433
|
+
async function createTempAnalysisWorkspace(sourceRoot, signal, options) {
|
|
434
|
+
const tempWorkspaceRoot = path.join(sourceRoot, ".mcp-probe-kit", "gitnexus-temp");
|
|
435
|
+
const workspaceId = createWorkspaceId(sourceRoot);
|
|
436
|
+
const analysisRoot = path.join(tempWorkspaceRoot, workspaceId);
|
|
437
|
+
fs.mkdirSync(tempWorkspaceRoot, { recursive: true });
|
|
438
|
+
await cleanupStaleTempWorkspaces(tempWorkspaceRoot);
|
|
439
|
+
copyIntoAnalysisWorkspace(sourceRoot, analysisRoot);
|
|
440
|
+
throwIfAborted(signal, "GitNexus 临时工作区创建已取消");
|
|
441
|
+
if (options?.bootstrap !== false) {
|
|
442
|
+
const gitInit = resolveSpawnCommand("git", ["init", "-q"]);
|
|
443
|
+
await runProcess(gitInit.command, gitInit.args, analysisRoot, signal);
|
|
444
|
+
const analyzeCli = resolveGitNexusCliCommand("analyze");
|
|
445
|
+
await runProcess(analyzeCli.command, analyzeCli.args, analysisRoot, signal);
|
|
446
|
+
}
|
|
447
|
+
return {
|
|
448
|
+
workspaceMode: "temp-repo",
|
|
449
|
+
sourceRoot,
|
|
450
|
+
analysisRoot,
|
|
451
|
+
repoName: path.basename(analysisRoot),
|
|
452
|
+
pathMapped: true,
|
|
453
|
+
cleanup: async () => {
|
|
454
|
+
if (options?.bootstrap !== false) {
|
|
455
|
+
try {
|
|
456
|
+
const cleanCli = resolveGitNexusCliCommand("clean");
|
|
457
|
+
await runProcess(cleanCli.command, cleanCli.args, analysisRoot, undefined);
|
|
458
|
+
}
|
|
459
|
+
catch {
|
|
460
|
+
// best effort cleanup only
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
await removePathWithRetry(analysisRoot);
|
|
464
|
+
try {
|
|
465
|
+
const remaining = fs.existsSync(tempWorkspaceRoot)
|
|
466
|
+
? fs.readdirSync(tempWorkspaceRoot).filter(Boolean)
|
|
467
|
+
: [];
|
|
468
|
+
if (remaining.length === 0) {
|
|
469
|
+
fs.rmSync(tempWorkspaceRoot, { recursive: true, force: true });
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
catch {
|
|
473
|
+
// best effort only
|
|
474
|
+
}
|
|
475
|
+
},
|
|
476
|
+
};
|
|
477
|
+
}
|
|
478
|
+
export async function prepareBridgeWorkspace(cwd = process.cwd(), signal, options) {
|
|
479
|
+
const resolvedCwd = path.resolve(cwd);
|
|
480
|
+
const gitRoot = findGitRoot(resolvedCwd);
|
|
481
|
+
if (gitRoot) {
|
|
482
|
+
return {
|
|
483
|
+
workspaceMode: "direct",
|
|
484
|
+
sourceRoot: gitRoot,
|
|
485
|
+
analysisRoot: gitRoot,
|
|
486
|
+
repoName: inferCandidateRepoNames(gitRoot)[0],
|
|
487
|
+
pathMapped: false,
|
|
488
|
+
};
|
|
489
|
+
}
|
|
490
|
+
return createTempAnalysisWorkspace(resolvedCwd, signal, options);
|
|
491
|
+
}
|
|
492
|
+
async function ensureWorkspaceIndexed(workspace, signal) {
|
|
493
|
+
if (workspace.workspaceMode !== "direct") {
|
|
494
|
+
return;
|
|
495
|
+
}
|
|
496
|
+
const analyzeCli = resolveGitNexusCliCommand("analyze");
|
|
497
|
+
await runProcess(analyzeCli.command, analyzeCli.args, workspace.analysisRoot, signal);
|
|
498
|
+
}
|
|
499
|
+
function isUnsafeHomeRoot(sourceRoot) {
|
|
500
|
+
return path.resolve(sourceRoot) === path.resolve(os.homedir());
|
|
501
|
+
}
|
|
502
|
+
function mapStringToSourceRoot(value, workspace) {
|
|
503
|
+
if (!workspace.pathMapped || !value) {
|
|
504
|
+
return value;
|
|
505
|
+
}
|
|
506
|
+
const candidates = [
|
|
507
|
+
[workspace.analysisRoot, workspace.sourceRoot],
|
|
508
|
+
[workspace.analysisRoot.replace(/\\/g, "/"), workspace.sourceRoot.replace(/\\/g, "/")],
|
|
509
|
+
[workspace.analysisRoot.replace(/\//g, "\\"), workspace.sourceRoot.replace(/\//g, "\\")],
|
|
510
|
+
];
|
|
511
|
+
let mapped = value;
|
|
512
|
+
for (const [from, to] of candidates) {
|
|
513
|
+
if (from && mapped.includes(from)) {
|
|
514
|
+
mapped = mapped.replaceAll(from, to);
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
return mapped;
|
|
518
|
+
}
|
|
519
|
+
function mapValueToSourceRoot(value, workspace) {
|
|
520
|
+
if (!workspace.pathMapped) {
|
|
521
|
+
return value;
|
|
522
|
+
}
|
|
523
|
+
if (typeof value === "string") {
|
|
524
|
+
return mapStringToSourceRoot(value, workspace);
|
|
525
|
+
}
|
|
526
|
+
if (Array.isArray(value)) {
|
|
527
|
+
return value.map((item) => mapValueToSourceRoot(item, workspace));
|
|
528
|
+
}
|
|
529
|
+
if (value && typeof value === "object") {
|
|
530
|
+
return Object.fromEntries(Object.entries(value).map(([key, item]) => [
|
|
531
|
+
key,
|
|
532
|
+
mapValueToSourceRoot(item, workspace),
|
|
533
|
+
]));
|
|
534
|
+
}
|
|
535
|
+
return value;
|
|
536
|
+
}
|
|
105
537
|
function resolveMode(request) {
|
|
106
538
|
const mode = request.mode || "auto";
|
|
107
539
|
if (mode === "query" || mode === "context" || mode === "impact") {
|
|
@@ -115,7 +547,7 @@ function resolveMode(request) {
|
|
|
115
547
|
}
|
|
116
548
|
return "query";
|
|
117
549
|
}
|
|
118
|
-
async function callBridgeTool(client, tool, args, signal) {
|
|
550
|
+
async function callBridgeTool(client, tool, args, signal, workspace) {
|
|
119
551
|
const startedAt = Date.now();
|
|
120
552
|
try {
|
|
121
553
|
const result = await client.callTool({
|
|
@@ -134,9 +566,13 @@ async function callBridgeTool(client, tool, args, signal) {
|
|
|
134
566
|
args,
|
|
135
567
|
ok: false,
|
|
136
568
|
durationMs,
|
|
137
|
-
text,
|
|
138
|
-
structuredContent:
|
|
139
|
-
|
|
569
|
+
text: workspace ? mapValueToSourceRoot(text, workspace) : text,
|
|
570
|
+
structuredContent: workspace
|
|
571
|
+
? mapValueToSourceRoot(result.structuredContent, workspace)
|
|
572
|
+
: result.structuredContent,
|
|
573
|
+
error: workspace
|
|
574
|
+
? mapValueToSourceRoot(text || `GitNexus 工具 ${tool} 返回错误`, workspace)
|
|
575
|
+
: text || `GitNexus 工具 ${tool} 返回错误`,
|
|
140
576
|
};
|
|
141
577
|
}
|
|
142
578
|
return {
|
|
@@ -144,8 +580,10 @@ async function callBridgeTool(client, tool, args, signal) {
|
|
|
144
580
|
args,
|
|
145
581
|
ok: true,
|
|
146
582
|
durationMs,
|
|
147
|
-
text,
|
|
148
|
-
structuredContent:
|
|
583
|
+
text: workspace ? mapValueToSourceRoot(text, workspace) : text,
|
|
584
|
+
structuredContent: workspace
|
|
585
|
+
? mapValueToSourceRoot(result.structuredContent, workspace)
|
|
586
|
+
: result.structuredContent,
|
|
149
587
|
};
|
|
150
588
|
}
|
|
151
589
|
catch (error) {
|
|
@@ -164,8 +602,9 @@ async function callBridgeTool(client, tool, args, signal) {
|
|
|
164
602
|
export async function runCodeInsightBridge(request) {
|
|
165
603
|
const modeRequested = request.mode || "auto";
|
|
166
604
|
const modeResolved = resolveMode(request);
|
|
167
|
-
const
|
|
605
|
+
const requestedProjectRoot = resolveRequestedProjectRoot(request.projectRoot);
|
|
168
606
|
if (!isBridgeEnabled() || isEnvDisabled("MCP_ENABLE_GITNEXUS_BRIDGE")) {
|
|
607
|
+
const sourceRoot = findGitRoot(requestedProjectRoot) || requestedProjectRoot;
|
|
169
608
|
return {
|
|
170
609
|
provider: "gitnexus",
|
|
171
610
|
enabled: false,
|
|
@@ -176,10 +615,15 @@ export async function runCodeInsightBridge(request) {
|
|
|
176
615
|
summary: "GitNexus bridge 已禁用(MCP_ENABLE_GITNEXUS_BRIDGE=0)。",
|
|
177
616
|
executions: [],
|
|
178
617
|
warnings: ["bridge_disabled"],
|
|
179
|
-
repo:
|
|
618
|
+
repo: resolvePreferredRepoName(request.repo),
|
|
619
|
+
workspaceMode: "direct",
|
|
620
|
+
sourceRoot,
|
|
621
|
+
analysisRoot: sourceRoot,
|
|
622
|
+
pathMapped: false,
|
|
180
623
|
};
|
|
181
624
|
}
|
|
182
625
|
if (Date.now() < bridgeFailureUntil) {
|
|
626
|
+
const sourceRoot = findGitRoot(requestedProjectRoot) || requestedProjectRoot;
|
|
183
627
|
return {
|
|
184
628
|
provider: "gitnexus",
|
|
185
629
|
enabled: true,
|
|
@@ -190,18 +634,48 @@ export async function runCodeInsightBridge(request) {
|
|
|
190
634
|
summary: `GitNexus bridge 暂不可用(缓存中): ${bridgeFailureReason || "请稍后重试"}`,
|
|
191
635
|
executions: [],
|
|
192
636
|
warnings: ["bridge_failure_cached"],
|
|
193
|
-
repo:
|
|
637
|
+
repo: resolvePreferredRepoName(request.repo),
|
|
638
|
+
workspaceMode: "direct",
|
|
639
|
+
sourceRoot,
|
|
640
|
+
analysisRoot: sourceRoot,
|
|
641
|
+
pathMapped: false,
|
|
194
642
|
};
|
|
195
643
|
}
|
|
196
644
|
throwIfAborted(request.signal, "GitNexus bridge 已取消");
|
|
645
|
+
if (isUnsafeHomeRoot(requestedProjectRoot) && !findGitRoot(requestedProjectRoot)) {
|
|
646
|
+
return {
|
|
647
|
+
provider: "gitnexus",
|
|
648
|
+
enabled: true,
|
|
649
|
+
available: false,
|
|
650
|
+
degraded: true,
|
|
651
|
+
modeRequested,
|
|
652
|
+
modeResolved,
|
|
653
|
+
summary: "GitNexus bridge 已降级:当前工作目录看起来是用户家目录。请显式传入 project_root 指向实际项目目录,避免复制 .gradle/.npm 等本地缓存。",
|
|
654
|
+
executions: [],
|
|
655
|
+
warnings: ["project_root_required", "unsafe_home_directory"],
|
|
656
|
+
repo: resolvePreferredRepoName(request.repo),
|
|
657
|
+
workspaceMode: "direct",
|
|
658
|
+
sourceRoot: requestedProjectRoot,
|
|
659
|
+
analysisRoot: requestedProjectRoot,
|
|
660
|
+
pathMapped: false,
|
|
661
|
+
};
|
|
662
|
+
}
|
|
663
|
+
const workspace = await prepareBridgeWorkspace(requestedProjectRoot, request.signal);
|
|
664
|
+
await ensureWorkspaceIndexed(workspace, request.signal);
|
|
665
|
+
const effectiveRepo = workspace.workspaceMode === "temp-repo"
|
|
666
|
+
? workspace.repoName
|
|
667
|
+
: resolvePreferredRepoName(request.repo) || workspace.repoName;
|
|
197
668
|
const { command, args } = resolveBridgeCommand();
|
|
198
669
|
const warnings = [];
|
|
670
|
+
if (workspace.workspaceMode === "temp-repo") {
|
|
671
|
+
warnings.push("temp_repo_workspace");
|
|
672
|
+
}
|
|
199
673
|
const executions = [];
|
|
200
674
|
const stderrLogs = [];
|
|
201
675
|
const transport = new StdioClientTransport({
|
|
202
676
|
command,
|
|
203
677
|
args,
|
|
204
|
-
cwd:
|
|
678
|
+
cwd: workspace.analysisRoot,
|
|
205
679
|
stderr: "pipe",
|
|
206
680
|
});
|
|
207
681
|
if (transport.stderr) {
|
|
@@ -233,7 +707,7 @@ export async function runCodeInsightBridge(request) {
|
|
|
233
707
|
...(request.goal ? { goal: request.goal } : {}),
|
|
234
708
|
...(request.taskContext ? { task_context: request.taskContext } : {}),
|
|
235
709
|
...(effectiveRepo ? { repo: effectiveRepo } : {}),
|
|
236
|
-
}, request.signal));
|
|
710
|
+
}, request.signal, workspace));
|
|
237
711
|
};
|
|
238
712
|
const runContext = async () => {
|
|
239
713
|
if (!request.target) {
|
|
@@ -243,7 +717,7 @@ export async function runCodeInsightBridge(request) {
|
|
|
243
717
|
executions.push(await callBridgeTool(client, "context", {
|
|
244
718
|
name: request.target,
|
|
245
719
|
...(effectiveRepo ? { repo: effectiveRepo } : {}),
|
|
246
|
-
}, request.signal));
|
|
720
|
+
}, request.signal, workspace));
|
|
247
721
|
};
|
|
248
722
|
const runImpact = async () => {
|
|
249
723
|
if (!request.target) {
|
|
@@ -258,7 +732,7 @@ export async function runCodeInsightBridge(request) {
|
|
|
258
732
|
? { includeTests: request.includeTests }
|
|
259
733
|
: {}),
|
|
260
734
|
...(effectiveRepo ? { repo: effectiveRepo } : {}),
|
|
261
|
-
}, request.signal));
|
|
735
|
+
}, request.signal, workspace));
|
|
262
736
|
};
|
|
263
737
|
if (modeRequested === "auto") {
|
|
264
738
|
await runQuery();
|
|
@@ -298,13 +772,32 @@ export async function runCodeInsightBridge(request) {
|
|
|
298
772
|
executions: [],
|
|
299
773
|
warnings: ["bridge_unavailable", ...warnings],
|
|
300
774
|
repo: effectiveRepo,
|
|
775
|
+
workspaceMode: workspace.workspaceMode,
|
|
776
|
+
sourceRoot: workspace.sourceRoot,
|
|
777
|
+
analysisRoot: workspace.analysisRoot,
|
|
778
|
+
pathMapped: workspace.pathMapped,
|
|
301
779
|
};
|
|
302
780
|
}
|
|
303
781
|
finally {
|
|
304
782
|
await client.close().catch(() => undefined);
|
|
783
|
+
await transport.close().catch(() => undefined);
|
|
784
|
+
await workspace.cleanup?.().catch(() => undefined);
|
|
305
785
|
}
|
|
306
786
|
const successful = executions.filter((item) => item.ok);
|
|
307
787
|
const failed = executions.filter((item) => !item.ok);
|
|
788
|
+
if (successful.length === 0
|
|
789
|
+
&& !effectiveRepo
|
|
790
|
+
&& failed.length > 0
|
|
791
|
+
&& failed.every((item) => (item.error || item.text || "").includes("Multiple repositories indexed"))) {
|
|
792
|
+
const availableRepos = parseAvailableReposFromError(failed.map((item) => item.error || item.text || "").join(" | "));
|
|
793
|
+
const retryRepo = inferCandidateRepoNames(workspace.sourceRoot).find((candidate) => availableRepos.includes(candidate));
|
|
794
|
+
if (retryRepo) {
|
|
795
|
+
return runCodeInsightBridge({
|
|
796
|
+
...request,
|
|
797
|
+
repo: retryRepo,
|
|
798
|
+
});
|
|
799
|
+
}
|
|
800
|
+
}
|
|
308
801
|
bridgeFailureUntil = 0;
|
|
309
802
|
bridgeFailureReason = "";
|
|
310
803
|
if (successful.length === 0) {
|
|
@@ -322,6 +815,10 @@ export async function runCodeInsightBridge(request) {
|
|
|
322
815
|
executions,
|
|
323
816
|
warnings: ["bridge_call_failed", ...warnings],
|
|
324
817
|
repo: effectiveRepo,
|
|
818
|
+
workspaceMode: workspace.workspaceMode,
|
|
819
|
+
sourceRoot: workspace.sourceRoot,
|
|
820
|
+
analysisRoot: workspace.analysisRoot,
|
|
821
|
+
pathMapped: workspace.pathMapped,
|
|
325
822
|
};
|
|
326
823
|
}
|
|
327
824
|
const summaryParts = successful.map((item) => `${item.tool}: ${shorten(item.text || "已返回结构化结果", 110)}`);
|
|
@@ -337,6 +834,10 @@ export async function runCodeInsightBridge(request) {
|
|
|
337
834
|
executions,
|
|
338
835
|
warnings,
|
|
339
836
|
repo: effectiveRepo,
|
|
837
|
+
workspaceMode: workspace.workspaceMode,
|
|
838
|
+
sourceRoot: workspace.sourceRoot,
|
|
839
|
+
analysisRoot: workspace.analysisRoot,
|
|
840
|
+
pathMapped: workspace.pathMapped,
|
|
340
841
|
};
|
|
341
842
|
}
|
|
342
843
|
function toEmbeddedGraphContext(result) {
|
|
@@ -356,11 +857,14 @@ function toEmbeddedGraphContext(result) {
|
|
|
356
857
|
}
|
|
357
858
|
export async function buildFeatureGraphContext(input) {
|
|
358
859
|
const bridge = await runCodeInsightBridge({
|
|
359
|
-
mode: "
|
|
860
|
+
mode: "auto",
|
|
360
861
|
query: `${input.featureName} ${input.description}`,
|
|
862
|
+
target: input.featureName,
|
|
863
|
+
direction: "upstream",
|
|
361
864
|
goal: "Find related modules and execution flows for feature planning",
|
|
362
|
-
taskContext: "start_feature planning",
|
|
865
|
+
taskContext: "start_feature planning with query/context/impact narrowing",
|
|
363
866
|
repo: input.repo,
|
|
867
|
+
projectRoot: input.projectRoot,
|
|
364
868
|
signal: input.signal,
|
|
365
869
|
});
|
|
366
870
|
return toEmbeddedGraphContext(bridge);
|
|
@@ -373,6 +877,7 @@ export async function buildBugfixGraphContext(input) {
|
|
|
373
877
|
goal: "Find likely root-cause symbols and impacted flows for bug fixing",
|
|
374
878
|
taskContext: "start_bugfix diagnosis",
|
|
375
879
|
repo: input.repo,
|
|
880
|
+
projectRoot: input.projectRoot,
|
|
376
881
|
signal: input.signal,
|
|
377
882
|
});
|
|
378
883
|
return toEmbeddedGraphContext(bridge);
|