mcp-probe-kit 3.0.12 → 3.0.13

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.
@@ -61,9 +61,9 @@ function isBridgeEnabled() {
61
61
  }
62
62
  return !/^(0|false|no|off)$/i.test(raw.trim());
63
63
  }
64
- function splitArgs(raw) {
64
+ function splitArgs(raw, fallback = DEFAULT_GITNEXUS_ARGS) {
65
65
  if (!raw) {
66
- return [...DEFAULT_GITNEXUS_ARGS];
66
+ return [...fallback];
67
67
  }
68
68
  return raw
69
69
  .trim()
@@ -119,14 +119,14 @@ function parseAvailableReposFromError(text) {
119
119
  .map((item) => item.trim())
120
120
  .filter(Boolean);
121
121
  }
122
- function resolveBridgeCommand() {
123
- const command = (process.env.MCP_GITNEXUS_COMMAND || "npx").trim() || "npx";
124
- const args = splitArgs(process.env.MCP_GITNEXUS_ARGS);
125
- return resolveSpawnCommand(command, args);
122
+ function isGitNexusCliCommand(command) {
123
+ const normalized = path.basename((command || "").trim()).toLowerCase();
124
+ return normalized === "gitnexus"
125
+ || normalized === "gitnexus.cmd"
126
+ || normalized === "gitnexus.exe"
127
+ || normalized === "gitnexus.bat";
126
128
  }
127
- function resolveGitNexusCliCommand(subcommand) {
128
- const command = (process.env.MCP_GITNEXUS_COMMAND || "npx").trim() || "npx";
129
- const bridgeArgs = splitArgs(process.env.MCP_GITNEXUS_ARGS);
129
+ function resolveNpxPackageArgs(bridgeArgs) {
130
130
  const flags = [];
131
131
  let packageSpec = "gitnexus@latest";
132
132
  for (const arg of bridgeArgs) {
@@ -140,17 +140,58 @@ function resolveGitNexusCliCommand(subcommand) {
140
140
  packageSpec = arg;
141
141
  break;
142
142
  }
143
- return resolveSpawnCommand(command, [...flags, packageSpec, subcommand]);
143
+ return [...flags, packageSpec];
144
144
  }
145
- export function resolveExecutableCommand(command, platform = process.platform) {
145
+ export function resolveGitNexusBridgeCommand(env = process.env, platform = process.platform) {
146
+ const explicitCommand = env.MCP_GITNEXUS_COMMAND?.trim();
147
+ if (explicitCommand) {
148
+ const args = splitArgs(env.MCP_GITNEXUS_ARGS, isGitNexusCliCommand(explicitCommand) ? ["mcp"] : DEFAULT_GITNEXUS_ARGS);
149
+ return {
150
+ ...resolveSpawnCommand(explicitCommand, args, platform, env),
151
+ strategy: "env",
152
+ };
153
+ }
154
+ const localCli = findExecutablePath("gitnexus", platform, env);
155
+ if (localCli) {
156
+ return {
157
+ command: localCli,
158
+ args: ["mcp"],
159
+ strategy: "local",
160
+ };
161
+ }
162
+ return {
163
+ ...resolveSpawnCommand("npx", splitArgs(env.MCP_GITNEXUS_ARGS), platform, env),
164
+ strategy: "npx",
165
+ };
166
+ }
167
+ function resolveGitNexusCliCommand(subcommand, env = process.env, platform = process.platform) {
168
+ const explicitCommand = env.MCP_GITNEXUS_COMMAND?.trim();
169
+ if (explicitCommand) {
170
+ if (isGitNexusCliCommand(explicitCommand)) {
171
+ return resolveSpawnCommand(explicitCommand, [subcommand], platform, env);
172
+ }
173
+ const bridgeArgs = splitArgs(env.MCP_GITNEXUS_ARGS);
174
+ return resolveSpawnCommand(explicitCommand, [...resolveNpxPackageArgs(bridgeArgs), subcommand], platform, env);
175
+ }
176
+ const localCli = findExecutablePath("gitnexus", platform, env);
177
+ if (localCli) {
178
+ return {
179
+ command: localCli,
180
+ args: [subcommand],
181
+ };
182
+ }
183
+ const bridgeArgs = splitArgs(env.MCP_GITNEXUS_ARGS);
184
+ return resolveSpawnCommand("npx", [...resolveNpxPackageArgs(bridgeArgs), subcommand], platform, env);
185
+ }
186
+ export function resolveExecutableCommand(command, platform = process.platform, env = process.env) {
146
187
  const normalized = (command || "").trim();
147
188
  if (!normalized) {
148
- return resolveExecutableCommand("npx", platform);
189
+ return resolveExecutableCommand("npx", platform, env);
149
190
  }
150
191
  if (path.isAbsolute(normalized) && fs.existsSync(normalized)) {
151
192
  return normalized;
152
193
  }
153
- const found = findExecutablePath(normalized, platform);
194
+ const found = findExecutablePath(normalized, platform, env);
154
195
  if (found) {
155
196
  return found;
156
197
  }
@@ -166,7 +207,7 @@ export function resolveExecutableCommand(command, platform = process.platform) {
166
207
  }
167
208
  return normalized;
168
209
  }
169
- function findExecutablePath(command, platform = process.platform) {
210
+ function findExecutablePath(command, platform = process.platform, env = process.env) {
170
211
  const trimmed = (command || "").trim();
171
212
  if (!trimmed || path.isAbsolute(trimmed)) {
172
213
  return undefined;
@@ -197,6 +238,7 @@ function findExecutablePath(command, platform = process.platform) {
197
238
  try {
198
239
  const output = execFileSync("where.exe", [trimmed], {
199
240
  encoding: "utf-8",
241
+ env,
200
242
  stdio: ["ignore", "pipe", "ignore"],
201
243
  windowsHide: true,
202
244
  }).trim();
@@ -210,7 +252,7 @@ function findExecutablePath(command, platform = process.platform) {
210
252
  // fall back to PATH scan
211
253
  }
212
254
  }
213
- const pathEntries = (process.env.PATH || "")
255
+ const pathEntries = (env.PATH || "")
214
256
  .split(path.delimiter)
215
257
  .map((entry) => entry.trim())
216
258
  .filter(Boolean);
@@ -232,21 +274,215 @@ function findExecutablePath(command, platform = process.platform) {
232
274
  }
233
275
  if (platform === "win32" && trimmed.toLowerCase() === "git") {
234
276
  const commonCandidates = [
235
- path.join(process.env["ProgramFiles"] || "C:\\Program Files", "Git", "cmd", "git.exe"),
236
- path.join(process.env["ProgramFiles"] || "C:\\Program Files", "Git", "bin", "git.exe"),
237
- path.join(process.env["ProgramFiles(x86)"] || "C:\\Program Files (x86)", "Git", "cmd", "git.exe"),
277
+ path.join(env["ProgramFiles"] || "C:\\Program Files", "Git", "cmd", "git.exe"),
278
+ path.join(env["ProgramFiles"] || "C:\\Program Files", "Git", "bin", "git.exe"),
279
+ path.join(env["ProgramFiles(x86)"] || "C:\\Program Files (x86)", "Git", "cmd", "git.exe"),
238
280
  ];
239
281
  return preferredPath(commonCandidates);
240
282
  }
241
283
  return undefined;
242
284
  }
243
- export function resolveSpawnCommand(command, args, platform = process.platform) {
244
- const executable = resolveExecutableCommand(command, platform);
285
+ export function resolveSpawnCommand(command, args, platform = process.platform, env = process.env) {
286
+ const executable = resolveExecutableCommand(command, platform, env);
245
287
  return {
246
288
  command: executable,
247
289
  args,
248
290
  };
249
291
  }
292
+ function toRecord(value) {
293
+ return value && typeof value === "object" && !Array.isArray(value)
294
+ ? value
295
+ : undefined;
296
+ }
297
+ function extractStructuredStatus(value) {
298
+ const status = toRecord(value)?.status;
299
+ return typeof status === "string" ? status : undefined;
300
+ }
301
+ function extractStructuredMessage(value) {
302
+ const record = toRecord(value);
303
+ if (!record) {
304
+ return undefined;
305
+ }
306
+ for (const key of ["message", "summary", "explanation", "detail"]) {
307
+ const candidate = record[key];
308
+ if (typeof candidate === "string" && candidate.trim()) {
309
+ return candidate.trim();
310
+ }
311
+ }
312
+ return undefined;
313
+ }
314
+ function extractStructuredCandidates(value) {
315
+ const candidates = toRecord(value)?.candidates;
316
+ if (!Array.isArray(candidates)) {
317
+ return [];
318
+ }
319
+ return candidates
320
+ .filter((item) => item && typeof item === "object" && !Array.isArray(item))
321
+ .map((item) => ({ ...item }));
322
+ }
323
+ function buildAmbiguities(executions) {
324
+ return executions
325
+ .filter((item) => item.ok && item.status === "ambiguous")
326
+ .map((item) => {
327
+ const candidates = extractStructuredCandidates(item.structuredContent);
328
+ const message = extractStructuredMessage(item.structuredContent) || item.text;
329
+ return {
330
+ tool: item.tool,
331
+ message: message ? shorten(message, 220) : undefined,
332
+ candidates,
333
+ };
334
+ });
335
+ }
336
+ const QUERY_STOP_WORDS = new Set([
337
+ "the", "and", "for", "with", "from", "into", "that", "this", "user", "flow",
338
+ "sign", "code", "project", "module",
339
+ "理解", "分析", "流程", "模块", "项目", "代码", "查询", "相关", "功能",
340
+ ]);
341
+ function tokenizeQueryHints(...values) {
342
+ const tokens = new Set();
343
+ for (const value of values) {
344
+ for (const token of (value || "").toLowerCase().split(/[^a-z0-9_\u4e00-\u9fa5]+/)) {
345
+ const normalized = token.trim();
346
+ if (!normalized || QUERY_STOP_WORDS.has(normalized)) {
347
+ continue;
348
+ }
349
+ if (normalized.length < 3 && !/[\u4e00-\u9fa5]/.test(normalized)) {
350
+ continue;
351
+ }
352
+ tokens.add(normalized);
353
+ }
354
+ }
355
+ return [...tokens];
356
+ }
357
+ function collectStringFields(value, depth = 0, bag = [], currentKey = "") {
358
+ if (depth > 3 || value == null) {
359
+ return bag;
360
+ }
361
+ if (typeof value === "string") {
362
+ const normalized = value.trim().toLowerCase();
363
+ if (normalized) {
364
+ bag.push({ key: currentKey.toLowerCase(), value: normalized });
365
+ }
366
+ return bag;
367
+ }
368
+ if (Array.isArray(value)) {
369
+ for (const item of value.slice(0, 12)) {
370
+ collectStringFields(item, depth + 1, bag, currentKey);
371
+ }
372
+ return bag;
373
+ }
374
+ if (typeof value === "object") {
375
+ for (const [key, item] of Object.entries(value).slice(0, 20)) {
376
+ collectStringFields(item, depth + 1, bag, key);
377
+ }
378
+ }
379
+ return bag;
380
+ }
381
+ function scoreQueryCandidate(candidate, terms) {
382
+ const fields = collectStringFields(candidate);
383
+ if (fields.length === 0 || terms.length === 0) {
384
+ return 0;
385
+ }
386
+ let score = 0;
387
+ let matchedTerms = 0;
388
+ for (const term of terms) {
389
+ let matched = false;
390
+ for (const field of fields) {
391
+ if (!field.value.includes(term)) {
392
+ continue;
393
+ }
394
+ matched = true;
395
+ if (field.key.includes("name") || field.key.includes("title") || field.key.includes("label")) {
396
+ score += field.value === term ? 24 : 16;
397
+ }
398
+ else if (field.key.includes("path") || field.key.includes("file") || field.key.includes("module")) {
399
+ score += 10;
400
+ }
401
+ else {
402
+ score += 4;
403
+ }
404
+ }
405
+ if (matched) {
406
+ matchedTerms += 1;
407
+ }
408
+ }
409
+ if (matchedTerms > 0) {
410
+ score += matchedTerms * 5;
411
+ }
412
+ if (matchedTerms === terms.length) {
413
+ score += 12;
414
+ }
415
+ const candidateRecord = toRecord(candidate);
416
+ const priority = candidateRecord?.priority;
417
+ if (typeof priority === "number" && Number.isFinite(priority)) {
418
+ score += priority;
419
+ }
420
+ return score;
421
+ }
422
+ function describeQueryTopMatches(structuredContent) {
423
+ const processes = toRecord(structuredContent)?.processes;
424
+ if (!Array.isArray(processes) || processes.length === 0) {
425
+ return undefined;
426
+ }
427
+ const labels = processes
428
+ .slice(0, 3)
429
+ .map((item) => {
430
+ const record = toRecord(item);
431
+ return [
432
+ record?.heuristicLabel,
433
+ record?.title,
434
+ record?.name,
435
+ record?.processName,
436
+ ].find((value) => typeof value === "string" && value.trim());
437
+ })
438
+ .filter((value) => typeof value === "string" && value.trim().length > 0);
439
+ return labels.length > 0 ? `Top matches: ${labels.join(" | ")}` : undefined;
440
+ }
441
+ export function rerankQueryStructuredContent(structuredContent, hints) {
442
+ const record = toRecord(structuredContent);
443
+ if (!record) {
444
+ return { structuredContent, changed: false };
445
+ }
446
+ const terms = tokenizeQueryHints(hints.query, hints.goal, hints.taskContext);
447
+ if (terms.length === 0) {
448
+ return { structuredContent, changed: false };
449
+ }
450
+ let changed = false;
451
+ const next = { ...record };
452
+ for (const key of ["processes", "definitions"]) {
453
+ const value = next[key];
454
+ if (!Array.isArray(value) || value.length < 2) {
455
+ continue;
456
+ }
457
+ const reranked = value
458
+ .map((item, index) => ({
459
+ item,
460
+ index,
461
+ score: scoreQueryCandidate(item, terms),
462
+ }))
463
+ .sort((a, b) => (b.score - a.score) || (a.index - b.index))
464
+ .map((entry) => entry.item);
465
+ const orderChanged = reranked.some((item, index) => item !== value[index]);
466
+ if (orderChanged) {
467
+ next[key] = reranked;
468
+ changed = true;
469
+ }
470
+ }
471
+ return {
472
+ structuredContent: next,
473
+ changed,
474
+ note: changed ? describeQueryTopMatches(next) : undefined,
475
+ };
476
+ }
477
+ function describeExecutionSummary(item) {
478
+ if (item.tool === "query") {
479
+ const rerankedTopMatches = describeQueryTopMatches(item.structuredContent);
480
+ if (rerankedTopMatches) {
481
+ return shorten(rerankedTopMatches, 110);
482
+ }
483
+ }
484
+ return shorten(item.text || "已返回结构化结果", 110);
485
+ }
250
486
  function extractText(result) {
251
487
  if (!result || typeof result !== "object") {
252
488
  return "";
@@ -507,10 +743,10 @@ function resolveMode(request) {
507
743
  if (mode === "query" || mode === "context" || mode === "impact") {
508
744
  return mode;
509
745
  }
510
- if (request.target && request.direction) {
746
+ if ((request.target || request.uid) && request.direction) {
511
747
  return "impact";
512
748
  }
513
- if (request.target && !request.query) {
749
+ if ((request.target || request.uid) && !request.query) {
514
750
  return "context";
515
751
  }
516
752
  return "query";
@@ -528,6 +764,10 @@ async function callBridgeTool(client, tool, args, signal, workspace) {
528
764
  const durationMs = Date.now() - startedAt;
529
765
  const text = extractText(result);
530
766
  const isError = Boolean(result.isError);
767
+ const structuredContent = workspace
768
+ ? mapValueToSourceRoot(result.structuredContent, workspace)
769
+ : result.structuredContent;
770
+ const status = extractStructuredStatus(structuredContent);
531
771
  if (isError) {
532
772
  return {
533
773
  tool,
@@ -535,9 +775,8 @@ async function callBridgeTool(client, tool, args, signal, workspace) {
535
775
  ok: false,
536
776
  durationMs,
537
777
  text: workspace ? mapValueToSourceRoot(text, workspace) : text,
538
- structuredContent: workspace
539
- ? mapValueToSourceRoot(result.structuredContent, workspace)
540
- : result.structuredContent,
778
+ structuredContent,
779
+ status,
541
780
  error: workspace
542
781
  ? mapValueToSourceRoot(text || `GitNexus 工具 ${tool} 返回错误`, workspace)
543
782
  : text || `GitNexus 工具 ${tool} 返回错误`,
@@ -549,9 +788,8 @@ async function callBridgeTool(client, tool, args, signal, workspace) {
549
788
  ok: true,
550
789
  durationMs,
551
790
  text: workspace ? mapValueToSourceRoot(text, workspace) : text,
552
- structuredContent: workspace
553
- ? mapValueToSourceRoot(result.structuredContent, workspace)
554
- : result.structuredContent,
791
+ structuredContent,
792
+ status,
555
793
  };
556
794
  }
557
795
  catch (error) {
@@ -571,6 +809,7 @@ export async function runCodeInsightBridge(request) {
571
809
  const modeRequested = request.mode || "auto";
572
810
  const modeResolved = resolveMode(request);
573
811
  const requestedProjectRoot = resolveRequestedProjectRoot(request.projectRoot);
812
+ const launcher = resolveGitNexusBridgeCommand();
574
813
  if (!isBridgeEnabled() || isEnvDisabled("MCP_ENABLE_GITNEXUS_BRIDGE")) {
575
814
  const sourceRoot = findGitRoot(requestedProjectRoot) || requestedProjectRoot;
576
815
  return {
@@ -584,6 +823,8 @@ export async function runCodeInsightBridge(request) {
584
823
  executions: [],
585
824
  warnings: ["bridge_disabled"],
586
825
  repo: resolvePreferredRepoName(request.repo),
826
+ launcherStrategy: launcher.strategy,
827
+ ambiguities: [],
587
828
  workspaceMode: "direct",
588
829
  sourceRoot,
589
830
  analysisRoot: sourceRoot,
@@ -603,6 +844,8 @@ export async function runCodeInsightBridge(request) {
603
844
  executions: [],
604
845
  warnings: ["bridge_failure_cached"],
605
846
  repo: resolvePreferredRepoName(request.repo),
847
+ launcherStrategy: launcher.strategy,
848
+ ambiguities: [],
606
849
  workspaceMode: "direct",
607
850
  sourceRoot,
608
851
  analysisRoot: sourceRoot,
@@ -622,6 +865,8 @@ export async function runCodeInsightBridge(request) {
622
865
  executions: [],
623
866
  warnings: ["project_root_required", "unsafe_home_directory"],
624
867
  repo: resolvePreferredRepoName(request.repo),
868
+ launcherStrategy: launcher.strategy,
869
+ ambiguities: [],
625
870
  workspaceMode: "direct",
626
871
  sourceRoot: requestedProjectRoot,
627
872
  analysisRoot: requestedProjectRoot,
@@ -633,7 +878,7 @@ export async function runCodeInsightBridge(request) {
633
878
  const effectiveRepo = workspace.workspaceMode === "temp-repo"
634
879
  ? workspace.repoName
635
880
  : resolvePreferredRepoName(request.repo) || workspace.repoName;
636
- const { command, args } = resolveBridgeCommand();
881
+ const { command, args } = launcher;
637
882
  const warnings = [];
638
883
  if (workspace.workspaceMode === "temp-repo") {
639
884
  warnings.push("temp_repo_workspace");
@@ -670,30 +915,50 @@ export async function runCodeInsightBridge(request) {
670
915
  warnings.push("缺少 query 参数,已跳过 query");
671
916
  return;
672
917
  }
673
- executions.push(await callBridgeTool(client, "query", {
918
+ const execution = await callBridgeTool(client, "query", {
674
919
  query: queryText,
675
920
  ...(request.goal ? { goal: request.goal } : {}),
676
921
  ...(request.taskContext ? { task_context: request.taskContext } : {}),
922
+ ...(request.includeContent ? { include_content: true } : {}),
677
923
  ...(effectiveRepo ? { repo: effectiveRepo } : {}),
678
- }, request.signal, workspace));
924
+ }, request.signal, workspace);
925
+ if (execution.ok) {
926
+ const reranked = rerankQueryStructuredContent(execution.structuredContent, {
927
+ query: queryText,
928
+ goal: request.goal,
929
+ taskContext: request.taskContext,
930
+ });
931
+ execution.structuredContent = reranked.structuredContent;
932
+ if (reranked.changed) {
933
+ warnings.push("query_results_reranked");
934
+ if (reranked.note) {
935
+ execution.text = execution.text
936
+ ? `${reranked.note}\n\n${execution.text}`
937
+ : reranked.note;
938
+ }
939
+ }
940
+ }
941
+ executions.push(execution);
679
942
  };
680
943
  const runContext = async () => {
681
- if (!request.target) {
682
- warnings.push("缺少 target 参数,已跳过 context");
944
+ if (!request.target && !request.uid) {
945
+ warnings.push("缺少 target/uid 参数,已跳过 context");
683
946
  return;
684
947
  }
685
948
  executions.push(await callBridgeTool(client, "context", {
686
- name: request.target,
949
+ ...(request.uid ? { uid: request.uid } : { name: request.target }),
950
+ ...(request.filePath ? { file_path: request.filePath } : {}),
951
+ ...(request.includeContent ? { include_content: true } : {}),
687
952
  ...(effectiveRepo ? { repo: effectiveRepo } : {}),
688
953
  }, request.signal, workspace));
689
954
  };
690
955
  const runImpact = async () => {
691
- if (!request.target) {
692
- warnings.push("缺少 target 参数,已跳过 impact");
956
+ if (!request.target && !request.uid) {
957
+ warnings.push("缺少 target/uid 参数,已跳过 impact");
693
958
  return;
694
959
  }
695
960
  executions.push(await callBridgeTool(client, "impact", {
696
- target: request.target,
961
+ target: request.uid || request.target,
697
962
  direction: request.direction || "upstream",
698
963
  ...(request.maxDepth ? { maxDepth: request.maxDepth } : {}),
699
964
  ...(typeof request.includeTests === "boolean"
@@ -704,10 +969,10 @@ export async function runCodeInsightBridge(request) {
704
969
  };
705
970
  if (modeRequested === "auto") {
706
971
  await runQuery();
707
- if (request.target) {
972
+ if (request.target || request.uid) {
708
973
  await runContext();
709
974
  }
710
- if (request.target && request.direction) {
975
+ if ((request.target || request.uid) && request.direction) {
711
976
  await runImpact();
712
977
  }
713
978
  }
@@ -740,6 +1005,8 @@ export async function runCodeInsightBridge(request) {
740
1005
  executions: [],
741
1006
  warnings: ["bridge_unavailable", ...warnings],
742
1007
  repo: effectiveRepo,
1008
+ launcherStrategy: launcher.strategy,
1009
+ ambiguities: [],
743
1010
  workspaceMode: workspace.workspaceMode,
744
1011
  sourceRoot: workspace.sourceRoot,
745
1012
  analysisRoot: workspace.analysisRoot,
@@ -768,6 +1035,7 @@ export async function runCodeInsightBridge(request) {
768
1035
  }
769
1036
  bridgeFailureUntil = 0;
770
1037
  bridgeFailureReason = "";
1038
+ const ambiguities = buildAmbiguities(successful);
771
1039
  if (successful.length === 0) {
772
1040
  const fallbackError = failed.length > 0
773
1041
  ? shorten(failed.map((item) => `${item.tool}: ${item.error || "unknown error"}`).join(" | "), 220)
@@ -783,14 +1051,19 @@ export async function runCodeInsightBridge(request) {
783
1051
  executions,
784
1052
  warnings: ["bridge_call_failed", ...warnings],
785
1053
  repo: effectiveRepo,
1054
+ launcherStrategy: launcher.strategy,
1055
+ ambiguities,
786
1056
  workspaceMode: workspace.workspaceMode,
787
1057
  sourceRoot: workspace.sourceRoot,
788
1058
  analysisRoot: workspace.analysisRoot,
789
1059
  pathMapped: workspace.pathMapped,
790
1060
  };
791
1061
  }
792
- const summaryParts = successful.map((item) => `${item.tool}: ${shorten(item.text || "已返回结构化结果", 110)}`);
793
- const summary = `GitNexus 图谱增强已启用(${successful.length}/${executions.length} 成功): ${summaryParts.join(" | ")}`;
1062
+ const summary = ambiguities.length > 0
1063
+ ? `GitNexus 返回了 ${ambiguities.length} 个歧义结果,请指定 uid file_path 后重试。`
1064
+ : `GitNexus 图谱增强已启用(${successful.length}/${executions.length} 成功): ${successful
1065
+ .map((item) => `${item.tool}: ${describeExecutionSummary(item)}`)
1066
+ .join(" | ")}`;
794
1067
  return {
795
1068
  provider: "gitnexus",
796
1069
  enabled: true,
@@ -802,6 +1075,8 @@ export async function runCodeInsightBridge(request) {
802
1075
  executions,
803
1076
  warnings,
804
1077
  repo: effectiveRepo,
1078
+ launcherStrategy: launcher.strategy,
1079
+ ambiguities,
805
1080
  workspaceMode: workspace.workspaceMode,
806
1081
  sourceRoot: workspace.sourceRoot,
807
1082
  analysisRoot: workspace.analysisRoot,
@@ -1,5 +1,5 @@
1
1
  import { describe, expect, test } from "vitest";
2
- import { codeInsight, resolveCodeInsightQuery } from "../code_insight.js";
2
+ import { buildCodeInsightDelegatedPlan, codeInsight, deriveCodeInsightStatus, resolveCodeInsightQuery, } from "../code_insight.js";
3
3
  import * as os from "node:os";
4
4
  import * as fs from "node:fs";
5
5
  import * as path from "node:path";
@@ -76,6 +76,56 @@ describe("code_insight 单元测试", () => {
76
76
  expect(resolved.finalQuery).toMatch(/核心流程/);
77
77
  expect(resolved.finalQuery).toMatch(/依赖关系/);
78
78
  });
79
+ test("歧义结果会生成候选选择 delegated plan", () => {
80
+ const status = deriveCodeInsightStatus({
81
+ available: true,
82
+ ambiguities: [
83
+ {
84
+ tool: "context",
85
+ message: "找到多个 login 符号",
86
+ candidates: [{ uid: "Method:auth.ts:login:12", file_path: "/repo/src/auth.ts" }],
87
+ },
88
+ ],
89
+ executions: [{ tool: "context", ok: true, durationMs: 10, args: {}, status: "ambiguous" }],
90
+ });
91
+ const plan = buildCodeInsightDelegatedPlan({
92
+ status,
93
+ ambiguities: [
94
+ {
95
+ tool: "context",
96
+ message: "找到多个 login 符号",
97
+ candidates: [{ uid: "Method:auth.ts:login:12", file_path: "/repo/src/auth.ts" }],
98
+ },
99
+ ],
100
+ showPlan: true,
101
+ });
102
+ expect(status).toBe("ambiguous");
103
+ expect(plan?.kind).toBe("ambiguity");
104
+ expect(plan?.steps[1].action).toMatch(/uid 或 file_path/);
105
+ });
106
+ test("未显式要求保存时不生成 docs delegated plan", async () => {
107
+ const prev = process.env.MCP_ENABLE_GITNEXUS_BRIDGE;
108
+ process.env.MCP_ENABLE_GITNEXUS_BRIDGE = "0";
109
+ try {
110
+ const result = await codeInsight({
111
+ mode: "auto",
112
+ });
113
+ expect(result.isError).toBe(false);
114
+ const text = String(result.content?.[0]?.text || "");
115
+ const structured = result.structuredContent;
116
+ expect(text).not.toMatch(/delegated plan/);
117
+ expect(structured.plan).toBeUndefined();
118
+ expect(structured.projectDocs).toBeUndefined();
119
+ }
120
+ finally {
121
+ if (prev === undefined) {
122
+ delete process.env.MCP_ENABLE_GITNEXUS_BRIDGE;
123
+ }
124
+ else {
125
+ process.env.MCP_ENABLE_GITNEXUS_BRIDGE = prev;
126
+ }
127
+ }
128
+ });
79
129
  test("返回 docs 保存指引而不直接代写文件", async () => {
80
130
  const prev = process.env.MCP_ENABLE_GITNEXUS_BRIDGE;
81
131
  process.env.MCP_ENABLE_GITNEXUS_BRIDGE = "0";
@@ -1,5 +1,34 @@
1
- import { type CodeInsightMode } from "../lib/gitnexus-bridge.js";
1
+ import { type CodeInsightAmbiguity, type CodeInsightBridgeResult, type CodeInsightMode } from "../lib/gitnexus-bridge.js";
2
2
  import { type ToolExecutionContext } from "../lib/tool-execution-context.js";
3
+ type CodeInsightStatus = "ok" | "degraded" | "ambiguous" | "not_found";
4
+ interface ProjectDocsPlan {
5
+ docsDir: string;
6
+ projectContextFilePath: string;
7
+ latestMarkdownFilePath: string;
8
+ latestJsonFilePath: string;
9
+ archiveMarkdownFilePath: string;
10
+ archiveJsonFilePath: string;
11
+ navigationSnippet: string;
12
+ devGuideSnippet: string;
13
+ }
14
+ interface DelegatedPlanStep {
15
+ id: string;
16
+ action: string;
17
+ outputs?: string[];
18
+ note?: string;
19
+ }
20
+ interface DelegatedPlan {
21
+ mode: "delegated";
22
+ kind: "docs" | "ambiguity";
23
+ steps: DelegatedPlanStep[];
24
+ }
25
+ export declare function deriveCodeInsightStatus(result: Pick<CodeInsightBridgeResult, "available" | "ambiguities" | "executions">): CodeInsightStatus;
26
+ export declare function buildCodeInsightDelegatedPlan(input: {
27
+ status: CodeInsightStatus;
28
+ ambiguities: CodeInsightAmbiguity[];
29
+ projectDocs?: ProjectDocsPlan;
30
+ showPlan: boolean;
31
+ }): DelegatedPlan | undefined;
3
32
  export declare function resolveCodeInsightQuery(input: {
4
33
  mode: CodeInsightMode;
5
34
  query: string;
@@ -16,3 +45,4 @@ export declare function codeInsight(args: any, context?: ToolExecutionContext):
16
45
  }[];
17
46
  isError: boolean;
18
47
  }>;
48
+ export {};