mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-10-11 03:38:07 +00:00
Some checks are pending
CodeQL / Analyze (javascript-typescript) (push) Waiting to run
CodeQL / Analyze (python) (push) Waiting to run
Gitleaks / gitleaks (push) Waiting to run
Publish / Classify release event (push) Waiting to run
Publish / RC guard (marker + release-PR skip) (push) Blocked by required conditions
Publish / ci (push) Blocked by required conditions
Publish / Publish to npm (push) Blocked by required conditions
Publish / Build & Push RC Docker images (push) Blocked by required conditions
Scorecard / Scorecard analysis (push) Waiting to run
Trivy Image Scan / Trivy (gitnexus-cli) (push) Waiting to run
Trivy Image Scan / Trivy (gitnexus-web) (push) Waiting to run
* feat(mcp): add trace tool for shortest call path between symbols (#1821) Implement the \ race\ MCP tool and \gitnexus trace\ CLI command that finds the shortest directed call path between two symbols using BFS over CALLS + HAS_METHOD edges. - MCP tool definition in tools.ts with READ_ONLY annotations - Directed BFS in local-backend.ts with parent-map path reconstruction - Symbol resolution via resolveSymbolCandidates (name/UID/file-hint) - Gap reporting with furthest reachable node and depth tracking - CLI wiring: gitnexus trace <from> <to> [--from-uid] [--to-uid] [--depth] - i18n keys in en.ts and zh-CN.ts + help-i18n.ts registration - ARCHITECTURE.md tools table entry - 16 unit tests (11 BFS core + 5 CLI wiring) * test(mcp): account for trace tool in tools.test.ts count The trace tool makes GITNEXUS_TOOLS length 15; update the hardcoded count, add 'trace' to the expected-names list, and refresh the stale "13 tools" comment and it() title. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(mcp): sanitize trace maxDepth to reject 0/NaN/negative `Math.min(params.maxDepth ?? 10, 30)` had no lower bound and `??` does not recover 0 or NaN, so `--depth 0|-5|abc` made the BFS loop run zero iterations and return a false `no_path`. Clamp at the real boundary with a `Number.isInteger && > 0` guard (the MCP inputSchema minimum is advisory only), and reject a non-numeric `--depth` in the CLI up front rather than forwarding NaN. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(mcp): check trace target before applying test-file filter The `isTestFilePath` filter ran before the target-equality check, but resolveSymbolCandidates does not exclude test-file symbols. A target (or a required hop) that lives in a test file was therefore skipped under the default includeTests=false and produced a false no_path with a misleading dynamic-dispatch suggestion. Match the explicitly-requested target first; non-target test-file nodes are still filtered. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * perf(mcp): bound trace BFS with per-level LIMIT and visited cap The per-level query had no LIMIT and the visited set was uncapped, so a high-fanout hub could materialize an unbounded frontier. Cap per-level rows (interpolated LIMIT — Kuzu does not bind LIMIT) and the total visited set; either cap sets a `truncated` flag so a resulting no_path reports that the search was cut short rather than implying the graph was exhausted. Note: the sibling impact BFS shares the same unbounded pattern; applying the cap there is deferred (out of scope for this PR). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * refactor(mcp): clarify trace traverses call + class-member edges trace was advertised as a "shortest call path" but also traverses HAS_METHOD (class→member) containment edges so a class-rooted trace can descend into its methods. Keep that capability (consistent with impact/ context) and make the docs honest: rename EDGE_TYPES→TRAVERSAL_EDGE_TYPES, state the call + class-member traversal in the MCP/CLI/i18n/ARCHITECTURE descriptions, and note each hop's edge type is reported in edges[]. No behavior change. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(mcp): set status:'error' on trace failure responses Every trace return path sets a `status` discriminator except the caught-error path, so a consumer switching on `result.status` saw undefined on failure. Add status:'error' to both the backend trace() catch and the CLI traceCommand catch. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(mcp): return a friendly error for non-string trace from/to A non-string from/to reaching resolveSymbolCandidates surfaced a low-level "x.includes is not a function" via name.includes. Guard the four name/uid params at the top of _traceImpl and return a structured status:'error' with a clear message instead. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * refactor(mcp): single row-decode + drop dead field in trace BFS Decode each BFS row once into named locals instead of repeating `(row.x ?? row[N])` across the two parent.set calls and the furthest-tracking. Drop the `type` field from the parent map value (it was written but never read), and rename the internal `deepestInfo` to `lastReached` for accuracy (the output field `furthest` is unchanged). Pure refactor — no behavior change. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * refactor(cli): dedicated trace includeTests i18n key + guard coverage `trace|--include-tests` reused the impact help key, so rewording the impact option would silently change trace's help text. Add a dedicated help.option.trace.includeTests key in en + zh-CN and repoint it. Add CLI coverage for the (already symmetric) --from-uid/--to-uid flag-value guard and for --include-tests forwarding. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * test(mcp): faithful BFS mock + expand trace coverage Fix makeResolveMock: concatenate neighbors across ALL frontier ids (it returned only the first node's, so a multi-node frontier was unmodelled) and key the UID branch on params.uid (the old query-text match never fired). Add coverage: shortest path through the second frontier node (proves the mock fix), confidence floor fallback, HAS_METHOD traversal with a mixed edge-type chain, no_path furthest:null, and from_file disambiguation. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * style(trace): apply root prettier formatting to trace files The root `quality / format` gate (prettier --check, printWidth 100) runs on the full repo and flagged the trace sources/tests (the local config masks it). Reformat to root style — no behavior change; trace + tools suites and tsc stay green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * docs(skills): document the trace tool for AI agents Add `trace` to the GitNexus skill docs so agents reach for it instead of hand-chaining context/impact hops. The guide gains a Tools Reference row and a "shortest path between two symbols" subsection (params, result shape, status/furthest/truncated semantics); the debugging skill gains a "how does A reach B?" pattern row and a trace tool example. Mirrored to the .claude and claude-plugin copies (byte-identical) and the cursor copy (compact style). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * test(mcp): drop unused trace test fixtures (CodeQL js/unused-local-variable) CodeQL flagged two unused locals in the trace BFS tests: the top-level SYMBOL_C and a SYMBOL_D inside the maxDepth test (both defined, never referenced). Remove them. No behavior change — 58 trace/tools tests stay green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Gergő Magyar <gergomagyar@icloud.com> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
255 lines
12 KiB
TypeScript
255 lines
12 KiB
TypeScript
import type { Command, Option } from 'commander';
|
|
import { t, type CliMessageKey } from './i18n/index.js';
|
|
|
|
const TITLE_KEYS = {
|
|
'Usage:': 'help.title.usage',
|
|
'Arguments:': 'help.title.arguments',
|
|
'Options:': 'help.title.options',
|
|
'Global Options:': 'help.title.globalOptions',
|
|
'Commands:': 'help.title.commands',
|
|
} satisfies Record<string, CliMessageKey>;
|
|
|
|
const COMMAND_DESCRIPTION_KEYS = {
|
|
'': 'help.description.root',
|
|
setup: 'help.command.setup.description',
|
|
uninstall: 'help.command.uninstall.description',
|
|
analyze: 'help.command.analyze.description',
|
|
index: 'help.command.index.description',
|
|
serve: 'help.command.serve.description',
|
|
mcp: 'help.command.mcp.description',
|
|
list: 'help.command.list.description',
|
|
status: 'help.command.status.description',
|
|
doctor: 'help.command.doctor.description',
|
|
clean: 'help.command.clean.description',
|
|
remove: 'help.command.remove.description',
|
|
wiki: 'help.command.wiki.description',
|
|
augment: 'help.command.augment.description',
|
|
publish: 'help.command.publish.description',
|
|
query: 'help.command.query.description',
|
|
context: 'help.command.context.description',
|
|
impact: 'help.command.impact.description',
|
|
cypher: 'help.command.cypher.description',
|
|
'detect-changes': 'help.command.detectChanges.description',
|
|
check: 'help.command.check.description',
|
|
trace: 'help.command.trace.description',
|
|
'eval-server': 'help.command.evalServer.description',
|
|
group: 'help.command.group.description',
|
|
'group create': 'help.command.group.create.description',
|
|
'group add': 'help.command.group.add.description',
|
|
'group remove': 'help.command.group.remove.description',
|
|
'group list': 'help.command.group.list.description',
|
|
'group status': 'help.command.group.status.description',
|
|
'group sync': 'help.command.group.sync.description',
|
|
'group impact': 'help.command.group.impact.description',
|
|
'group query': 'help.command.group.query.description',
|
|
'group contracts': 'help.command.group.contracts.description',
|
|
} satisfies Record<string, CliMessageKey>;
|
|
|
|
const OPTION_DESCRIPTION_KEYS = {
|
|
'|-V, --version': 'help.option.version',
|
|
'setup|-c, --coding-agent <agents>': 'help.option.setup.codingAgent',
|
|
'analyze|-f, --force': 'help.option.analyze.force',
|
|
'analyze|--repair-fts': 'help.option.analyze.repairFts',
|
|
'analyze|--embeddings [limit]': 'help.option.analyze.embeddings',
|
|
'analyze|--drop-embeddings': 'help.option.analyze.dropEmbeddings',
|
|
'analyze|--skills': 'help.option.analyze.skills',
|
|
'analyze|--skip-agents-md': 'help.option.analyze.skipAgentsMd',
|
|
'analyze|--no-stats': 'help.option.analyze.noStats',
|
|
'analyze|--skip-skills': 'help.option.analyze.skipSkills',
|
|
'analyze|--index-only': 'help.option.analyze.indexOnly',
|
|
'analyze|--skip-git': 'help.option.skipGit',
|
|
'analyze|--name <alias>': 'help.option.analyze.name',
|
|
'analyze|--allow-duplicate-name': 'help.option.analyze.allowDuplicateName',
|
|
'analyze|-v, --verbose': 'help.option.verbose',
|
|
'analyze|--max-file-size <kb>': 'help.option.analyze.maxFileSize',
|
|
'analyze|--worker-timeout <seconds>': 'help.option.analyze.workerTimeout',
|
|
'analyze|--wal-checkpoint-threshold <bytes>': 'help.option.analyze.walCheckpointThreshold',
|
|
'analyze|--workers <n>': 'help.option.analyze.workers',
|
|
'analyze|--embedding-threads <n>': 'help.option.analyze.embeddingThreads',
|
|
'analyze|--embedding-batch-size <n>': 'help.option.analyze.embeddingBatchSize',
|
|
'analyze|--embedding-sub-batch-size <n>': 'help.option.analyze.embeddingSubBatchSize',
|
|
'analyze|--embedding-device <device>': 'help.option.analyze.embeddingDevice',
|
|
'index|-f, --force': 'help.option.index.force',
|
|
'index|--allow-non-git': 'help.option.index.allowNonGit',
|
|
'mcp|--http': 'help.option.mcp.http',
|
|
'mcp|-p, --port <port>': 'help.option.port',
|
|
'mcp|--host <host>': 'help.option.mcp.host',
|
|
'mcp|--auth-token <token>': 'help.option.mcp.authToken',
|
|
'serve|-p, --port <port>': 'help.option.port',
|
|
'serve|--host <host>': 'help.option.serve.host',
|
|
'uninstall|-f, --force': 'help.option.uninstall.force',
|
|
'clean|-f, --force': 'help.option.force.confirmation',
|
|
'clean|--all': 'help.option.clean.all',
|
|
'clean|--branch <name>': 'help.option.clean.branch',
|
|
'clean|--lbug-sidecars': 'help.option.clean.lbugSidecars',
|
|
'remove|-f, --force': 'help.option.force.confirmation',
|
|
'wiki|-f, --force': 'help.option.wiki.force',
|
|
'wiki|--provider <provider>': 'help.option.wiki.provider',
|
|
'wiki|--model <model>': 'help.option.wiki.model',
|
|
'wiki|--base-url <url>': 'help.option.wiki.baseUrl',
|
|
'wiki|--api-key <key>': 'help.option.wiki.apiKey',
|
|
'wiki|--api-version <version>': 'help.option.wiki.apiVersion',
|
|
'wiki|--reasoning-model': 'help.option.wiki.reasoningModel',
|
|
'wiki|--no-reasoning-model': 'help.option.wiki.noReasoningModel',
|
|
'wiki|--concurrency <n>': 'help.option.wiki.concurrency',
|
|
'wiki|--timeout <seconds>': 'help.option.wiki.timeout',
|
|
'wiki|--retries <n>': 'help.option.wiki.retries',
|
|
'wiki|--gist': 'help.option.wiki.gist',
|
|
'wiki|-v, --verbose': 'help.option.verbose',
|
|
'wiki|--review': 'help.option.wiki.review',
|
|
'wiki|--lang <lang>': 'help.option.wiki.lang',
|
|
'publish|--id <owner/repo>': 'help.option.publish.id',
|
|
'publish|--skip-git': 'help.option.skipGit',
|
|
'query|-r, --repo <name>': 'help.option.repo.targetOmitOne',
|
|
'query|--branch <name>': 'help.option.branch',
|
|
'query|-c, --context <text>': 'help.option.query.context',
|
|
'query|-g, --goal <text>': 'help.option.query.goal',
|
|
'query|-l, --limit <n>': 'help.option.query.limit',
|
|
'query|--content': 'help.option.content',
|
|
'context|-r, --repo <name>': 'help.option.repo.target',
|
|
'context|--branch <name>': 'help.option.branch',
|
|
'context|-u, --uid <uid>': 'help.option.context.uid',
|
|
'context|-f, --file <path>': 'help.option.context.file',
|
|
'context|--content': 'help.option.content',
|
|
'impact|-d, --direction <dir>': 'help.option.impact.direction',
|
|
'impact|-r, --repo <name>': 'help.option.repo.target',
|
|
'impact|--branch <name>': 'help.option.branch',
|
|
'impact|-u, --uid <uid>': 'help.option.context.uid',
|
|
'impact|-f, --file <path>': 'help.option.context.file',
|
|
'impact|--kind <kind>': 'help.option.impact.kind',
|
|
'impact|--depth <n>': 'help.option.impact.depth',
|
|
'impact|--include-tests': 'help.option.impact.includeTests',
|
|
'impact|--limit <n>': 'help.option.impact.limit',
|
|
'impact|--offset <n>': 'help.option.impact.offset',
|
|
'impact|--summary-only': 'help.option.impact.summaryOnly',
|
|
'cypher|-r, --repo <name>': 'help.option.repo.target',
|
|
'cypher|--branch <name>': 'help.option.branch',
|
|
'detect-changes|-s, --scope <scope>': 'help.option.detectChanges.scope',
|
|
'detect-changes|-b, --base-ref <ref>': 'help.option.detectChanges.baseRef',
|
|
'detect-changes|-r, --repo <name>': 'help.option.repo.target',
|
|
'detect-changes|--branch <name>': 'help.option.branch',
|
|
'check|--cycles': 'help.option.check.cycles',
|
|
'check|--json': 'help.option.json',
|
|
'check|-r, --repo <name>': 'help.option.repo.target',
|
|
'check|--branch <name>': 'help.option.branch',
|
|
'trace|--from-uid <uid>': 'help.option.trace.fromUid',
|
|
'trace|--from-file <path>': 'help.option.trace.fromFile',
|
|
'trace|--to-uid <uid>': 'help.option.trace.toUid',
|
|
'trace|--to-file <path>': 'help.option.trace.toFile',
|
|
'trace|--depth <n>': 'help.option.trace.depth',
|
|
'trace|--include-tests': 'help.option.trace.includeTests',
|
|
'trace|-r, --repo <name>': 'help.option.repo.target',
|
|
'trace|--branch <name>': 'help.option.branch',
|
|
'eval-server|-p, --port <port>': 'help.option.port',
|
|
'eval-server|--host <host>': 'help.option.evalServer.host',
|
|
'eval-server|--idle-timeout <seconds>': 'help.option.evalServer.idleTimeout',
|
|
'group create|--force': 'help.option.group.create.force',
|
|
'group sync|--skip-embeddings': 'help.option.group.sync.skipEmbeddings',
|
|
'group sync|--exact-only': 'help.option.group.sync.exactOnly',
|
|
'group sync|--allow-stale': 'help.option.group.sync.allowStale',
|
|
'group sync|--verbose': 'help.option.group.sync.verbose',
|
|
'group sync|--json': 'help.option.json',
|
|
'group impact|--target <symbol>': 'help.option.group.impact.target',
|
|
'group impact|--repo <groupPath>': 'help.option.group.impact.repo',
|
|
'group impact|--direction <dir>': 'help.option.impact.direction',
|
|
'group impact|--service <path>': 'help.option.group.impact.service',
|
|
'group impact|--subgroup <path>': 'help.option.group.impact.subgroup',
|
|
'group impact|--max-depth <n>': 'help.option.impact.depth',
|
|
'group impact|--cross-depth <n>': 'help.option.group.impact.crossDepth',
|
|
'group impact|--min-confidence <n>': 'help.option.group.impact.minConfidence',
|
|
'group impact|--include-tests': 'help.option.impact.includeTests',
|
|
'group impact|--timeout-ms <n>': 'help.option.group.impact.timeoutMs',
|
|
'group impact|--json': 'help.option.json',
|
|
'group query|--subgroup <path>': 'help.option.group.query.subgroup',
|
|
'group query|--limit <n>': 'help.option.group.query.limit',
|
|
'group query|--json': 'help.option.json',
|
|
'group contracts|--type <type>': 'help.option.group.contracts.type',
|
|
'group contracts|--repo <repo>': 'help.option.group.contracts.repo',
|
|
'group contracts|--unmatched': 'help.option.group.contracts.unmatched',
|
|
'group contracts|--json': 'help.option.json',
|
|
} satisfies Record<string, CliMessageKey>;
|
|
|
|
function localizeTitle(title: string): string {
|
|
const key = TITLE_KEYS[title as keyof typeof TITLE_KEYS];
|
|
return key ? t(key) : title;
|
|
}
|
|
|
|
function localizeOptionDescription(option: Option): string {
|
|
const extraInfo = [];
|
|
|
|
if (option.argChoices) {
|
|
const label = t('help.optionMeta.choices');
|
|
extraInfo.push(
|
|
`${label}: ${option.argChoices.map((choice) => JSON.stringify(choice)).join(', ')}`,
|
|
);
|
|
}
|
|
|
|
if (option.defaultValue !== undefined) {
|
|
const showDefault =
|
|
option.required ||
|
|
option.optional ||
|
|
(option.isBoolean() && typeof option.defaultValue === 'boolean');
|
|
if (showDefault) {
|
|
const label = t('help.optionMeta.default');
|
|
extraInfo.push(
|
|
`${label}: ${option.defaultValueDescription || JSON.stringify(option.defaultValue)}`,
|
|
);
|
|
}
|
|
}
|
|
|
|
if (option.presetArg !== undefined && option.optional) {
|
|
const label = t('help.optionMeta.preset');
|
|
extraInfo.push(`${label}: ${JSON.stringify(option.presetArg)}`);
|
|
}
|
|
|
|
if (option.envVar !== undefined) {
|
|
const label = t('help.optionMeta.env');
|
|
extraInfo.push(`${label}: ${option.envVar}`);
|
|
}
|
|
|
|
if (extraInfo.length > 0) {
|
|
const extraDescription = `(${extraInfo.join(', ')})`;
|
|
if (option.description) return `${option.description} ${extraDescription}`;
|
|
return extraDescription;
|
|
}
|
|
|
|
return option.description;
|
|
}
|
|
|
|
function pathFor(commandPath: string, command: Command): string {
|
|
if (!command.parent) return '';
|
|
return commandPath ? `${commandPath} ${command.name()}` : command.name();
|
|
}
|
|
|
|
function applyHelpI18n(command: Command, commandPath = ''): void {
|
|
const descriptionKey =
|
|
COMMAND_DESCRIPTION_KEYS[commandPath as keyof typeof COMMAND_DESCRIPTION_KEYS];
|
|
if (descriptionKey) command.description(t(descriptionKey));
|
|
|
|
command.helpOption('-h, --help', t('help.option.help'));
|
|
command.configureHelp({
|
|
styleTitle: localizeTitle,
|
|
optionDescription: localizeOptionDescription,
|
|
});
|
|
|
|
if (command.commands.length > 0) {
|
|
command.helpCommand('help [command]', t('help.command.help.description'));
|
|
}
|
|
|
|
for (const option of command.options) {
|
|
const optionKey =
|
|
OPTION_DESCRIPTION_KEYS[
|
|
`${commandPath}|${option.flags}` as keyof typeof OPTION_DESCRIPTION_KEYS
|
|
];
|
|
if (optionKey) option.description = t(optionKey);
|
|
}
|
|
|
|
for (const subcommand of command.commands) {
|
|
applyHelpI18n(subcommand, pathFor(commandPath, subcommand));
|
|
}
|
|
}
|
|
|
|
export function localizeCliHelp(program: Command): Command {
|
|
applyHelpI18n(program);
|
|
return program;
|
|
}
|