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.
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 +86 -0
  4. package/build/lib/gitnexus-bridge.d.ts +23 -0
  5. package/build/lib/gitnexus-bridge.js +517 -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,206 @@ 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
+ 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: result.structuredContent,
139
- error: text || `GitNexus 工具 ${tool} 返回错误`,
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: result.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 effectiveRepo = request.repo || inferDefaultRepoName();
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: effectiveRepo,
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: effectiveRepo,
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: process.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: "query",
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);