mcp-probe-kit 3.0.9 → 3.0.10
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 +86 -0
- package/build/lib/gitnexus-bridge.d.ts +23 -0
- package/build/lib/gitnexus-bridge.js +517 -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,206 @@ 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
|
+
export function resolveSpawnCommand(command, args, platform = process.platform) {
|
|
261
|
+
const executable = resolveExecutableCommand(command, platform);
|
|
262
|
+
if (!shouldWrapWithCmd(command, executable, platform)) {
|
|
263
|
+
return {
|
|
264
|
+
command: executable,
|
|
265
|
+
args,
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
return {
|
|
269
|
+
command: process.env.ComSpec || "cmd.exe",
|
|
270
|
+
args: ["/d", "/s", "/c", executable, ...args],
|
|
271
|
+
};
|
|
68
272
|
}
|
|
69
273
|
function extractText(result) {
|
|
70
274
|
if (!result || typeof result !== "object") {
|
|
@@ -102,6 +306,225 @@ function normalizeError(error) {
|
|
|
102
306
|
}
|
|
103
307
|
return String(error);
|
|
104
308
|
}
|
|
309
|
+
function findGitRoot(startDir) {
|
|
310
|
+
let current = path.resolve(startDir);
|
|
311
|
+
while (true) {
|
|
312
|
+
const gitPath = path.join(current, ".git");
|
|
313
|
+
if (fs.existsSync(gitPath)) {
|
|
314
|
+
return current;
|
|
315
|
+
}
|
|
316
|
+
const parent = path.dirname(current);
|
|
317
|
+
if (parent === current) {
|
|
318
|
+
return undefined;
|
|
319
|
+
}
|
|
320
|
+
current = parent;
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
function createWorkspaceId(sourceRoot) {
|
|
324
|
+
const base = path.basename(sourceRoot).replace(/[^a-zA-Z0-9._-]+/g, "-") || "workspace";
|
|
325
|
+
return `${base}-${Date.now()}-${Math.random().toString(16).slice(2, 8)}`;
|
|
326
|
+
}
|
|
327
|
+
function shouldIgnoreForTempWorkspace(sourceRoot, entryPath) {
|
|
328
|
+
const relative = path.relative(sourceRoot, entryPath);
|
|
329
|
+
if (!relative || relative.startsWith("..")) {
|
|
330
|
+
return false;
|
|
331
|
+
}
|
|
332
|
+
const parts = relative.split(path.sep).filter(Boolean);
|
|
333
|
+
if (parts.some((part) => DEFAULT_IGNORED_DIRS.has(part))) {
|
|
334
|
+
return true;
|
|
335
|
+
}
|
|
336
|
+
const name = parts[parts.length - 1] || "";
|
|
337
|
+
return DEFAULT_IGNORED_FILES.some((pattern) => pattern.test(name));
|
|
338
|
+
}
|
|
339
|
+
async function runProcess(command, args, cwd, signal) {
|
|
340
|
+
await new Promise((resolve, reject) => {
|
|
341
|
+
const child = spawn(command, args, {
|
|
342
|
+
cwd,
|
|
343
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
344
|
+
signal,
|
|
345
|
+
windowsHide: true,
|
|
346
|
+
});
|
|
347
|
+
let stderr = "";
|
|
348
|
+
child.stderr?.on("data", (chunk) => {
|
|
349
|
+
stderr += String(chunk);
|
|
350
|
+
});
|
|
351
|
+
child.on("error", reject);
|
|
352
|
+
child.on("close", (code) => {
|
|
353
|
+
if (code === 0) {
|
|
354
|
+
resolve();
|
|
355
|
+
return;
|
|
356
|
+
}
|
|
357
|
+
reject(new Error(stderr.trim() || `${command} ${args.join(" ")} 退出码 ${code ?? "unknown"}`));
|
|
358
|
+
});
|
|
359
|
+
});
|
|
360
|
+
}
|
|
361
|
+
async function removePathWithRetry(targetPath, attempts = 6) {
|
|
362
|
+
let lastError;
|
|
363
|
+
for (let index = 0; index < attempts; index += 1) {
|
|
364
|
+
try {
|
|
365
|
+
fs.rmSync(targetPath, { recursive: true, force: true });
|
|
366
|
+
if (!fs.existsSync(targetPath)) {
|
|
367
|
+
return;
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
catch (error) {
|
|
371
|
+
lastError = error;
|
|
372
|
+
}
|
|
373
|
+
await new Promise((resolve) => setTimeout(resolve, 150 * (index + 1)));
|
|
374
|
+
}
|
|
375
|
+
if (fs.existsSync(targetPath)) {
|
|
376
|
+
throw lastError instanceof Error
|
|
377
|
+
? lastError
|
|
378
|
+
: new Error(`无法删除临时目录: ${targetPath}`);
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
async function cleanupStaleTempWorkspaces(tempWorkspaceRoot, keepPath) {
|
|
382
|
+
if (!fs.existsSync(tempWorkspaceRoot)) {
|
|
383
|
+
return;
|
|
384
|
+
}
|
|
385
|
+
const entries = fs.readdirSync(tempWorkspaceRoot, { withFileTypes: true });
|
|
386
|
+
for (const entry of entries) {
|
|
387
|
+
if (!entry.isDirectory()) {
|
|
388
|
+
continue;
|
|
389
|
+
}
|
|
390
|
+
const fullPath = path.join(tempWorkspaceRoot, entry.name);
|
|
391
|
+
if (keepPath && path.resolve(fullPath) === path.resolve(keepPath)) {
|
|
392
|
+
continue;
|
|
393
|
+
}
|
|
394
|
+
try {
|
|
395
|
+
await removePathWithRetry(fullPath, 3);
|
|
396
|
+
}
|
|
397
|
+
catch {
|
|
398
|
+
// keep best-effort cleanup silent; current run should not fail because of stale dirs
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
function copyIntoAnalysisWorkspace(sourceRoot, analysisRoot) {
|
|
403
|
+
fs.mkdirSync(analysisRoot, { recursive: true });
|
|
404
|
+
const walk = (fromDir, toDir) => {
|
|
405
|
+
fs.mkdirSync(toDir, { recursive: true });
|
|
406
|
+
const entries = fs.readdirSync(fromDir, { withFileTypes: true });
|
|
407
|
+
for (const entry of entries) {
|
|
408
|
+
const fromPath = path.join(fromDir, entry.name);
|
|
409
|
+
if (shouldIgnoreForTempWorkspace(sourceRoot, fromPath)) {
|
|
410
|
+
continue;
|
|
411
|
+
}
|
|
412
|
+
const toPath = path.join(toDir, entry.name);
|
|
413
|
+
if (entry.isDirectory()) {
|
|
414
|
+
walk(fromPath, toPath);
|
|
415
|
+
continue;
|
|
416
|
+
}
|
|
417
|
+
if (entry.isFile()) {
|
|
418
|
+
fs.copyFileSync(fromPath, toPath);
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
};
|
|
422
|
+
walk(sourceRoot, analysisRoot);
|
|
423
|
+
}
|
|
424
|
+
async function createTempAnalysisWorkspace(sourceRoot, signal, options) {
|
|
425
|
+
const tempWorkspaceRoot = path.join(sourceRoot, ".mcp-probe-kit", "gitnexus-temp");
|
|
426
|
+
const workspaceId = createWorkspaceId(sourceRoot);
|
|
427
|
+
const analysisRoot = path.join(tempWorkspaceRoot, workspaceId);
|
|
428
|
+
fs.mkdirSync(tempWorkspaceRoot, { recursive: true });
|
|
429
|
+
await cleanupStaleTempWorkspaces(tempWorkspaceRoot);
|
|
430
|
+
copyIntoAnalysisWorkspace(sourceRoot, analysisRoot);
|
|
431
|
+
throwIfAborted(signal, "GitNexus 临时工作区创建已取消");
|
|
432
|
+
if (options?.bootstrap !== false) {
|
|
433
|
+
const gitInit = resolveSpawnCommand("git", ["init", "-q"]);
|
|
434
|
+
await runProcess(gitInit.command, gitInit.args, analysisRoot, signal);
|
|
435
|
+
const analyzeCli = resolveGitNexusCliCommand("analyze");
|
|
436
|
+
await runProcess(analyzeCli.command, analyzeCli.args, analysisRoot, signal);
|
|
437
|
+
}
|
|
438
|
+
return {
|
|
439
|
+
workspaceMode: "temp-repo",
|
|
440
|
+
sourceRoot,
|
|
441
|
+
analysisRoot,
|
|
442
|
+
repoName: path.basename(analysisRoot),
|
|
443
|
+
pathMapped: true,
|
|
444
|
+
cleanup: async () => {
|
|
445
|
+
if (options?.bootstrap !== false) {
|
|
446
|
+
try {
|
|
447
|
+
const cleanCli = resolveGitNexusCliCommand("clean");
|
|
448
|
+
await runProcess(cleanCli.command, cleanCli.args, analysisRoot, undefined);
|
|
449
|
+
}
|
|
450
|
+
catch {
|
|
451
|
+
// best effort cleanup only
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
await removePathWithRetry(analysisRoot);
|
|
455
|
+
try {
|
|
456
|
+
const remaining = fs.existsSync(tempWorkspaceRoot)
|
|
457
|
+
? fs.readdirSync(tempWorkspaceRoot).filter(Boolean)
|
|
458
|
+
: [];
|
|
459
|
+
if (remaining.length === 0) {
|
|
460
|
+
fs.rmSync(tempWorkspaceRoot, { recursive: true, force: true });
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
catch {
|
|
464
|
+
// best effort only
|
|
465
|
+
}
|
|
466
|
+
},
|
|
467
|
+
};
|
|
468
|
+
}
|
|
469
|
+
export async function prepareBridgeWorkspace(cwd = process.cwd(), signal, options) {
|
|
470
|
+
const resolvedCwd = path.resolve(cwd);
|
|
471
|
+
const gitRoot = findGitRoot(resolvedCwd);
|
|
472
|
+
if (gitRoot) {
|
|
473
|
+
return {
|
|
474
|
+
workspaceMode: "direct",
|
|
475
|
+
sourceRoot: gitRoot,
|
|
476
|
+
analysisRoot: gitRoot,
|
|
477
|
+
repoName: inferCandidateRepoNames(gitRoot)[0],
|
|
478
|
+
pathMapped: false,
|
|
479
|
+
};
|
|
480
|
+
}
|
|
481
|
+
return createTempAnalysisWorkspace(resolvedCwd, signal, options);
|
|
482
|
+
}
|
|
483
|
+
async function ensureWorkspaceIndexed(workspace, signal) {
|
|
484
|
+
if (workspace.workspaceMode !== "direct") {
|
|
485
|
+
return;
|
|
486
|
+
}
|
|
487
|
+
const analyzeCli = resolveGitNexusCliCommand("analyze");
|
|
488
|
+
await runProcess(analyzeCli.command, analyzeCli.args, workspace.analysisRoot, signal);
|
|
489
|
+
}
|
|
490
|
+
function isUnsafeHomeRoot(sourceRoot) {
|
|
491
|
+
return path.resolve(sourceRoot) === path.resolve(os.homedir());
|
|
492
|
+
}
|
|
493
|
+
function mapStringToSourceRoot(value, workspace) {
|
|
494
|
+
if (!workspace.pathMapped || !value) {
|
|
495
|
+
return value;
|
|
496
|
+
}
|
|
497
|
+
const candidates = [
|
|
498
|
+
[workspace.analysisRoot, workspace.sourceRoot],
|
|
499
|
+
[workspace.analysisRoot.replace(/\\/g, "/"), workspace.sourceRoot.replace(/\\/g, "/")],
|
|
500
|
+
[workspace.analysisRoot.replace(/\//g, "\\"), workspace.sourceRoot.replace(/\//g, "\\")],
|
|
501
|
+
];
|
|
502
|
+
let mapped = value;
|
|
503
|
+
for (const [from, to] of candidates) {
|
|
504
|
+
if (from && mapped.includes(from)) {
|
|
505
|
+
mapped = mapped.replaceAll(from, to);
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
return mapped;
|
|
509
|
+
}
|
|
510
|
+
function mapValueToSourceRoot(value, workspace) {
|
|
511
|
+
if (!workspace.pathMapped) {
|
|
512
|
+
return value;
|
|
513
|
+
}
|
|
514
|
+
if (typeof value === "string") {
|
|
515
|
+
return mapStringToSourceRoot(value, workspace);
|
|
516
|
+
}
|
|
517
|
+
if (Array.isArray(value)) {
|
|
518
|
+
return value.map((item) => mapValueToSourceRoot(item, workspace));
|
|
519
|
+
}
|
|
520
|
+
if (value && typeof value === "object") {
|
|
521
|
+
return Object.fromEntries(Object.entries(value).map(([key, item]) => [
|
|
522
|
+
key,
|
|
523
|
+
mapValueToSourceRoot(item, workspace),
|
|
524
|
+
]));
|
|
525
|
+
}
|
|
526
|
+
return value;
|
|
527
|
+
}
|
|
105
528
|
function resolveMode(request) {
|
|
106
529
|
const mode = request.mode || "auto";
|
|
107
530
|
if (mode === "query" || mode === "context" || mode === "impact") {
|
|
@@ -115,7 +538,7 @@ function resolveMode(request) {
|
|
|
115
538
|
}
|
|
116
539
|
return "query";
|
|
117
540
|
}
|
|
118
|
-
async function callBridgeTool(client, tool, args, signal) {
|
|
541
|
+
async function callBridgeTool(client, tool, args, signal, workspace) {
|
|
119
542
|
const startedAt = Date.now();
|
|
120
543
|
try {
|
|
121
544
|
const result = await client.callTool({
|
|
@@ -134,9 +557,13 @@ async function callBridgeTool(client, tool, args, signal) {
|
|
|
134
557
|
args,
|
|
135
558
|
ok: false,
|
|
136
559
|
durationMs,
|
|
137
|
-
text,
|
|
138
|
-
structuredContent:
|
|
139
|
-
|
|
560
|
+
text: workspace ? mapValueToSourceRoot(text, workspace) : text,
|
|
561
|
+
structuredContent: workspace
|
|
562
|
+
? mapValueToSourceRoot(result.structuredContent, workspace)
|
|
563
|
+
: result.structuredContent,
|
|
564
|
+
error: workspace
|
|
565
|
+
? mapValueToSourceRoot(text || `GitNexus 工具 ${tool} 返回错误`, workspace)
|
|
566
|
+
: text || `GitNexus 工具 ${tool} 返回错误`,
|
|
140
567
|
};
|
|
141
568
|
}
|
|
142
569
|
return {
|
|
@@ -144,8 +571,10 @@ async function callBridgeTool(client, tool, args, signal) {
|
|
|
144
571
|
args,
|
|
145
572
|
ok: true,
|
|
146
573
|
durationMs,
|
|
147
|
-
text,
|
|
148
|
-
structuredContent:
|
|
574
|
+
text: workspace ? mapValueToSourceRoot(text, workspace) : text,
|
|
575
|
+
structuredContent: workspace
|
|
576
|
+
? mapValueToSourceRoot(result.structuredContent, workspace)
|
|
577
|
+
: result.structuredContent,
|
|
149
578
|
};
|
|
150
579
|
}
|
|
151
580
|
catch (error) {
|
|
@@ -164,8 +593,9 @@ async function callBridgeTool(client, tool, args, signal) {
|
|
|
164
593
|
export async function runCodeInsightBridge(request) {
|
|
165
594
|
const modeRequested = request.mode || "auto";
|
|
166
595
|
const modeResolved = resolveMode(request);
|
|
167
|
-
const
|
|
596
|
+
const requestedProjectRoot = resolveRequestedProjectRoot(request.projectRoot);
|
|
168
597
|
if (!isBridgeEnabled() || isEnvDisabled("MCP_ENABLE_GITNEXUS_BRIDGE")) {
|
|
598
|
+
const sourceRoot = findGitRoot(requestedProjectRoot) || requestedProjectRoot;
|
|
169
599
|
return {
|
|
170
600
|
provider: "gitnexus",
|
|
171
601
|
enabled: false,
|
|
@@ -176,10 +606,15 @@ export async function runCodeInsightBridge(request) {
|
|
|
176
606
|
summary: "GitNexus bridge 已禁用(MCP_ENABLE_GITNEXUS_BRIDGE=0)。",
|
|
177
607
|
executions: [],
|
|
178
608
|
warnings: ["bridge_disabled"],
|
|
179
|
-
repo:
|
|
609
|
+
repo: resolvePreferredRepoName(request.repo),
|
|
610
|
+
workspaceMode: "direct",
|
|
611
|
+
sourceRoot,
|
|
612
|
+
analysisRoot: sourceRoot,
|
|
613
|
+
pathMapped: false,
|
|
180
614
|
};
|
|
181
615
|
}
|
|
182
616
|
if (Date.now() < bridgeFailureUntil) {
|
|
617
|
+
const sourceRoot = findGitRoot(requestedProjectRoot) || requestedProjectRoot;
|
|
183
618
|
return {
|
|
184
619
|
provider: "gitnexus",
|
|
185
620
|
enabled: true,
|
|
@@ -190,18 +625,48 @@ export async function runCodeInsightBridge(request) {
|
|
|
190
625
|
summary: `GitNexus bridge 暂不可用(缓存中): ${bridgeFailureReason || "请稍后重试"}`,
|
|
191
626
|
executions: [],
|
|
192
627
|
warnings: ["bridge_failure_cached"],
|
|
193
|
-
repo:
|
|
628
|
+
repo: resolvePreferredRepoName(request.repo),
|
|
629
|
+
workspaceMode: "direct",
|
|
630
|
+
sourceRoot,
|
|
631
|
+
analysisRoot: sourceRoot,
|
|
632
|
+
pathMapped: false,
|
|
194
633
|
};
|
|
195
634
|
}
|
|
196
635
|
throwIfAborted(request.signal, "GitNexus bridge 已取消");
|
|
636
|
+
if (isUnsafeHomeRoot(requestedProjectRoot) && !findGitRoot(requestedProjectRoot)) {
|
|
637
|
+
return {
|
|
638
|
+
provider: "gitnexus",
|
|
639
|
+
enabled: true,
|
|
640
|
+
available: false,
|
|
641
|
+
degraded: true,
|
|
642
|
+
modeRequested,
|
|
643
|
+
modeResolved,
|
|
644
|
+
summary: "GitNexus bridge 已降级:当前工作目录看起来是用户家目录。请显式传入 project_root 指向实际项目目录,避免复制 .gradle/.npm 等本地缓存。",
|
|
645
|
+
executions: [],
|
|
646
|
+
warnings: ["project_root_required", "unsafe_home_directory"],
|
|
647
|
+
repo: resolvePreferredRepoName(request.repo),
|
|
648
|
+
workspaceMode: "direct",
|
|
649
|
+
sourceRoot: requestedProjectRoot,
|
|
650
|
+
analysisRoot: requestedProjectRoot,
|
|
651
|
+
pathMapped: false,
|
|
652
|
+
};
|
|
653
|
+
}
|
|
654
|
+
const workspace = await prepareBridgeWorkspace(requestedProjectRoot, request.signal);
|
|
655
|
+
await ensureWorkspaceIndexed(workspace, request.signal);
|
|
656
|
+
const effectiveRepo = workspace.workspaceMode === "temp-repo"
|
|
657
|
+
? workspace.repoName
|
|
658
|
+
: resolvePreferredRepoName(request.repo) || workspace.repoName;
|
|
197
659
|
const { command, args } = resolveBridgeCommand();
|
|
198
660
|
const warnings = [];
|
|
661
|
+
if (workspace.workspaceMode === "temp-repo") {
|
|
662
|
+
warnings.push("temp_repo_workspace");
|
|
663
|
+
}
|
|
199
664
|
const executions = [];
|
|
200
665
|
const stderrLogs = [];
|
|
201
666
|
const transport = new StdioClientTransport({
|
|
202
667
|
command,
|
|
203
668
|
args,
|
|
204
|
-
cwd:
|
|
669
|
+
cwd: workspace.analysisRoot,
|
|
205
670
|
stderr: "pipe",
|
|
206
671
|
});
|
|
207
672
|
if (transport.stderr) {
|
|
@@ -233,7 +698,7 @@ export async function runCodeInsightBridge(request) {
|
|
|
233
698
|
...(request.goal ? { goal: request.goal } : {}),
|
|
234
699
|
...(request.taskContext ? { task_context: request.taskContext } : {}),
|
|
235
700
|
...(effectiveRepo ? { repo: effectiveRepo } : {}),
|
|
236
|
-
}, request.signal));
|
|
701
|
+
}, request.signal, workspace));
|
|
237
702
|
};
|
|
238
703
|
const runContext = async () => {
|
|
239
704
|
if (!request.target) {
|
|
@@ -243,7 +708,7 @@ export async function runCodeInsightBridge(request) {
|
|
|
243
708
|
executions.push(await callBridgeTool(client, "context", {
|
|
244
709
|
name: request.target,
|
|
245
710
|
...(effectiveRepo ? { repo: effectiveRepo } : {}),
|
|
246
|
-
}, request.signal));
|
|
711
|
+
}, request.signal, workspace));
|
|
247
712
|
};
|
|
248
713
|
const runImpact = async () => {
|
|
249
714
|
if (!request.target) {
|
|
@@ -258,7 +723,7 @@ export async function runCodeInsightBridge(request) {
|
|
|
258
723
|
? { includeTests: request.includeTests }
|
|
259
724
|
: {}),
|
|
260
725
|
...(effectiveRepo ? { repo: effectiveRepo } : {}),
|
|
261
|
-
}, request.signal));
|
|
726
|
+
}, request.signal, workspace));
|
|
262
727
|
};
|
|
263
728
|
if (modeRequested === "auto") {
|
|
264
729
|
await runQuery();
|
|
@@ -298,13 +763,32 @@ export async function runCodeInsightBridge(request) {
|
|
|
298
763
|
executions: [],
|
|
299
764
|
warnings: ["bridge_unavailable", ...warnings],
|
|
300
765
|
repo: effectiveRepo,
|
|
766
|
+
workspaceMode: workspace.workspaceMode,
|
|
767
|
+
sourceRoot: workspace.sourceRoot,
|
|
768
|
+
analysisRoot: workspace.analysisRoot,
|
|
769
|
+
pathMapped: workspace.pathMapped,
|
|
301
770
|
};
|
|
302
771
|
}
|
|
303
772
|
finally {
|
|
304
773
|
await client.close().catch(() => undefined);
|
|
774
|
+
await transport.close().catch(() => undefined);
|
|
775
|
+
await workspace.cleanup?.().catch(() => undefined);
|
|
305
776
|
}
|
|
306
777
|
const successful = executions.filter((item) => item.ok);
|
|
307
778
|
const failed = executions.filter((item) => !item.ok);
|
|
779
|
+
if (successful.length === 0
|
|
780
|
+
&& !effectiveRepo
|
|
781
|
+
&& failed.length > 0
|
|
782
|
+
&& failed.every((item) => (item.error || item.text || "").includes("Multiple repositories indexed"))) {
|
|
783
|
+
const availableRepos = parseAvailableReposFromError(failed.map((item) => item.error || item.text || "").join(" | "));
|
|
784
|
+
const retryRepo = inferCandidateRepoNames(workspace.sourceRoot).find((candidate) => availableRepos.includes(candidate));
|
|
785
|
+
if (retryRepo) {
|
|
786
|
+
return runCodeInsightBridge({
|
|
787
|
+
...request,
|
|
788
|
+
repo: retryRepo,
|
|
789
|
+
});
|
|
790
|
+
}
|
|
791
|
+
}
|
|
308
792
|
bridgeFailureUntil = 0;
|
|
309
793
|
bridgeFailureReason = "";
|
|
310
794
|
if (successful.length === 0) {
|
|
@@ -322,6 +806,10 @@ export async function runCodeInsightBridge(request) {
|
|
|
322
806
|
executions,
|
|
323
807
|
warnings: ["bridge_call_failed", ...warnings],
|
|
324
808
|
repo: effectiveRepo,
|
|
809
|
+
workspaceMode: workspace.workspaceMode,
|
|
810
|
+
sourceRoot: workspace.sourceRoot,
|
|
811
|
+
analysisRoot: workspace.analysisRoot,
|
|
812
|
+
pathMapped: workspace.pathMapped,
|
|
325
813
|
};
|
|
326
814
|
}
|
|
327
815
|
const summaryParts = successful.map((item) => `${item.tool}: ${shorten(item.text || "已返回结构化结果", 110)}`);
|
|
@@ -337,6 +825,10 @@ export async function runCodeInsightBridge(request) {
|
|
|
337
825
|
executions,
|
|
338
826
|
warnings,
|
|
339
827
|
repo: effectiveRepo,
|
|
828
|
+
workspaceMode: workspace.workspaceMode,
|
|
829
|
+
sourceRoot: workspace.sourceRoot,
|
|
830
|
+
analysisRoot: workspace.analysisRoot,
|
|
831
|
+
pathMapped: workspace.pathMapped,
|
|
340
832
|
};
|
|
341
833
|
}
|
|
342
834
|
function toEmbeddedGraphContext(result) {
|
|
@@ -356,11 +848,14 @@ function toEmbeddedGraphContext(result) {
|
|
|
356
848
|
}
|
|
357
849
|
export async function buildFeatureGraphContext(input) {
|
|
358
850
|
const bridge = await runCodeInsightBridge({
|
|
359
|
-
mode: "
|
|
851
|
+
mode: "auto",
|
|
360
852
|
query: `${input.featureName} ${input.description}`,
|
|
853
|
+
target: input.featureName,
|
|
854
|
+
direction: "upstream",
|
|
361
855
|
goal: "Find related modules and execution flows for feature planning",
|
|
362
|
-
taskContext: "start_feature planning",
|
|
856
|
+
taskContext: "start_feature planning with query/context/impact narrowing",
|
|
363
857
|
repo: input.repo,
|
|
858
|
+
projectRoot: input.projectRoot,
|
|
364
859
|
signal: input.signal,
|
|
365
860
|
});
|
|
366
861
|
return toEmbeddedGraphContext(bridge);
|
|
@@ -373,6 +868,7 @@ export async function buildBugfixGraphContext(input) {
|
|
|
373
868
|
goal: "Find likely root-cause symbols and impacted flows for bug fixing",
|
|
374
869
|
taskContext: "start_bugfix diagnosis",
|
|
375
870
|
repo: input.repo,
|
|
871
|
+
projectRoot: input.projectRoot,
|
|
376
872
|
signal: input.signal,
|
|
377
873
|
});
|
|
378
874
|
return toEmbeddedGraphContext(bridge);
|