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.
Files changed (52) hide show
  1. package/README.md +14 -4
  2. package/build/lib/__tests__/gitnexus-bridge.unit.test.d.ts +1 -0
  3. package/build/lib/__tests__/gitnexus-bridge.unit.test.js +96 -0
  4. package/build/lib/gitnexus-bridge.d.ts +23 -0
  5. package/build/lib/gitnexus-bridge.js +526 -21
  6. package/build/schemas/code-analysis-tools.d.ts +9 -1
  7. package/build/schemas/code-analysis-tools.js +9 -1
  8. package/build/schemas/git-tools.d.ts +1 -1
  9. package/build/schemas/git-tools.js +1 -1
  10. package/build/schemas/index.d.ts +23 -3
  11. package/build/schemas/orchestration-tools.d.ts +13 -1
  12. package/build/schemas/orchestration-tools.js +13 -1
  13. package/build/schemas/output/core-tools.d.ts +130 -1
  14. package/build/schemas/output/core-tools.js +71 -1
  15. package/build/schemas/output/index.d.ts +2 -2
  16. package/build/schemas/output/index.js +2 -2
  17. package/build/schemas/output/project-tools.d.ts +13 -0
  18. package/build/schemas/output/project-tools.js +9 -0
  19. package/build/schemas/structured-output.d.ts +358 -5
  20. package/build/schemas/structured-output.js +169 -5
  21. package/build/tools/__tests__/code_insight.unit.test.js +81 -1
  22. package/build/tools/__tests__/fix_bug.unit.test.d.ts +1 -0
  23. package/build/tools/__tests__/fix_bug.unit.test.js +31 -0
  24. package/build/tools/__tests__/gencommit.unit.test.d.ts +1 -0
  25. package/build/tools/__tests__/gencommit.unit.test.js +41 -0
  26. package/build/tools/__tests__/init_project_context.unit.test.d.ts +1 -0
  27. package/build/tools/__tests__/init_project_context.unit.test.js +63 -0
  28. package/build/tools/__tests__/start_bugfix.unit.test.js +10 -0
  29. package/build/tools/__tests__/start_feature.unit.test.js +10 -0
  30. package/build/tools/code_insight.d.ts +10 -0
  31. package/build/tools/code_insight.js +156 -3
  32. package/build/tools/fix_bug.d.ts +3 -3
  33. package/build/tools/fix_bug.js +297 -312
  34. package/build/tools/gencommit.js +144 -123
  35. package/build/tools/init_project_context.js +211 -53
  36. package/build/tools/start_bugfix.js +170 -70
  37. package/build/tools/start_feature.js +79 -25
  38. package/docs/data/tools.js +33 -31
  39. package/docs/i18n/all-tools/en.json +9 -9
  40. package/docs/i18n/all-tools/ja.json +9 -9
  41. package/docs/i18n/all-tools/ko.json +9 -9
  42. package/docs/i18n/all-tools/zh-CN.json +9 -9
  43. package/docs/i18n/en.json +480 -481
  44. package/docs/i18n/ja.json +478 -479
  45. package/docs/i18n/ko.json +480 -481
  46. package/docs/i18n/zh-CN.json +480 -481
  47. package/docs/index.html +2 -2
  48. package/docs/pages/all-tools.html +2 -2
  49. package/docs/pages/examples.html +2 -2
  50. package/docs/pages/getting-started.html +2 -2
  51. package/docs/pages/migration.html +2 -2
  52. 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 inferDefaultRepoName() {
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
- const pkgPath = path.join(process.cwd(), "package.json");
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
- return pkgName;
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(process.cwd()).trim();
62
- return base || undefined;
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 { command, args };
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: result.structuredContent,
139
- error: text || `GitNexus 工具 ${tool} 返回错误`,
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: result.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 effectiveRepo = request.repo || inferDefaultRepoName();
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: effectiveRepo,
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: effectiveRepo,
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: process.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: "query",
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);