gitnexus 1.6.6-rc.5 → 1.6.6-rc.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli/index.js CHANGED
@@ -108,6 +108,7 @@ program
108
108
  .option('--gist', 'Publish wiki as a public GitHub Gist after generation')
109
109
  .option('-v, --verbose', 'Enable verbose output (show LLM commands and responses)')
110
110
  .option('--review', 'Stop after grouping to review module structure before generating pages')
111
+ .option('--lang <lang>', 'Output language for generated documentation (e.g. english, chinese, spanish, japanese)')
111
112
  .action(createLazyAction(() => import('./wiki.js'), 'wikiCommand'));
112
113
  program
113
114
  .command('augment <pattern>')
@@ -19,5 +19,6 @@ export interface WikiCommandOptions {
19
19
  review?: boolean;
20
20
  timeout?: string;
21
21
  retries?: string;
22
+ lang?: string;
22
23
  }
23
24
  export declare const wikiCommand: (inputPath?: string, options?: WikiCommandOptions) => Promise<void>;
package/dist/cli/wiki.js CHANGED
@@ -364,6 +364,7 @@ export const wikiCommand = async (inputPath, options) => {
364
364
  force: options?.force,
365
365
  concurrency: options?.concurrency ? parseInt(options.concurrency, 10) : undefined,
366
366
  reviewOnly: options?.review,
367
+ lang: options?.lang,
367
368
  };
368
369
  const generator = new WikiGenerator(repoPath, storagePath, lbugPath, llmConfig, wikiOptions, (phase, percent, detail) => {
369
370
  const label = detail || phase;
@@ -16,11 +16,14 @@ export interface WikiOptions {
16
16
  concurrency?: number;
17
17
  /** If true, stop after building module tree for user review */
18
18
  reviewOnly?: boolean;
19
+ /** Output language for generated documentation (e.g. 'english', 'chinese', 'spanish') */
20
+ lang?: string;
19
21
  }
20
22
  export interface WikiMeta {
21
23
  fromCommit: string;
22
24
  generatedAt: string;
23
25
  model: string;
26
+ lang: string;
24
27
  moduleFiles: Record<string, string[]>;
25
28
  moduleTree: ModuleTreeNode[];
26
29
  }
@@ -62,6 +65,16 @@ export declare class WikiGenerator {
62
65
  * Also touches the DB connection periodically to prevent idle timeout.
63
66
  */
64
67
  private streamOpts;
68
+ /**
69
+ * Return the effective lang string: strip control characters, trim, cap at 50 chars,
70
+ * then validate against a character allowlist. Returns '' if the value is absent or invalid.
71
+ * Used for both prompt construction and meta storage/comparison so they are always in sync.
72
+ */
73
+ private effectiveLang;
74
+ /**
75
+ * Append an output-language instruction to a system prompt when --lang is set.
76
+ */
77
+ private buildSystemPrompt;
65
78
  /**
66
79
  * Route LLM call to the appropriate provider (OpenAI-compatible or Cursor CLI).
67
80
  */
@@ -89,6 +89,27 @@ export class WikiGenerator {
89
89
  },
90
90
  };
91
91
  }
92
+ /**
93
+ * Return the effective lang string: strip control characters, trim, cap at 50 chars,
94
+ * then validate against a character allowlist. Returns '' if the value is absent or invalid.
95
+ * Used for both prompt construction and meta storage/comparison so they are always in sync.
96
+ */
97
+ effectiveLang() {
98
+ const lang = (this.options.lang ?? '')
99
+ .replace(/[\x00-\x1F\x7F]/g, '')
100
+ .trim()
101
+ .slice(0, 50);
102
+ return /^[a-zA-Z -]+$/.test(lang) ? lang : '';
103
+ }
104
+ /**
105
+ * Append an output-language instruction to a system prompt when --lang is set.
106
+ */
107
+ buildSystemPrompt(base) {
108
+ const lang = this.effectiveLang();
109
+ if (!lang)
110
+ return base;
111
+ return `${base}\n\nIMPORTANT: Write ALL documentation content in ${lang}. This includes prose, code comments in examples, and diagram labels. Note: page titles (H1 headings) are generated separately and will remain in English.`;
112
+ }
92
113
  /**
93
114
  * Route LLM call to the appropriate provider (OpenAI-compatible or Cursor CLI).
94
115
  */
@@ -112,6 +133,13 @@ export class WikiGenerator {
112
133
  const forceMode = this.options.force;
113
134
  // Up-to-date check (skip if --force)
114
135
  if (!forceMode && existingMeta && existingMeta.fromCommit === currentCommit) {
136
+ const currentLang = this.effectiveLang();
137
+ const metaLang = existingMeta.lang ?? '';
138
+ if (currentLang !== metaLang) {
139
+ const prevDisplay = metaLang || 'english (default)';
140
+ const nextDisplay = currentLang || 'english (default)';
141
+ throw new Error(`Wiki was generated in ${prevDisplay}; use --force to regenerate in ${nextDisplay}.`);
142
+ }
115
143
  // Still regenerate the HTML viewer in case it's missing
116
144
  await this.ensureHTMLViewer();
117
145
  return { pagesGenerated: 0, mode: 'up-to-date', failedModules: [] };
@@ -139,6 +167,13 @@ export class WikiGenerator {
139
167
  let result;
140
168
  try {
141
169
  if (!forceMode && existingMeta && existingMeta.fromCommit) {
170
+ const currentLang = this.effectiveLang();
171
+ const metaLang = existingMeta.lang ?? '';
172
+ if (currentLang !== metaLang) {
173
+ const prevDisplay = metaLang || 'english (default)';
174
+ const nextDisplay = currentLang || 'english (default)';
175
+ throw new Error(`Wiki was generated in ${prevDisplay}; use --force to regenerate in ${nextDisplay}.`);
176
+ }
142
177
  result = await this.incrementalUpdate(existingMeta, currentCommit);
143
178
  }
144
179
  else {
@@ -257,6 +292,7 @@ export class WikiGenerator {
257
292
  fromCommit: currentCommit,
258
293
  generatedAt: new Date().toISOString(),
259
294
  model: this.llmConfig.model,
295
+ lang: this.effectiveLang(),
260
296
  moduleFiles,
261
297
  moduleTree,
262
298
  });
@@ -298,6 +334,9 @@ export class WikiGenerator {
298
334
  FILE_LIST: fileList,
299
335
  DIRECTORY_TREE: dirTree,
300
336
  });
337
+ // Grouping is a structured-data phase (JSON output), not documentation.
338
+ // Do NOT apply buildSystemPrompt here — a language instruction would risk
339
+ // translating module-name keys, breaking slug stability and JSON parsing.
301
340
  const response = await this.invokeLLM(prompt, GROUPING_SYSTEM_PROMPT, this.streamOpts('Grouping files', 15, 13));
302
341
  const grouping = this.parseGroupingResponse(response.content, files);
303
342
  // Convert to tree nodes
@@ -444,8 +483,8 @@ export class WikiGenerator {
444
483
  INCOMING_CALLS: formatCallEdges(interCalls.incoming),
445
484
  PROCESSES: formatProcesses(processes),
446
485
  });
447
- const response = await this.invokeLLM(prompt, MODULE_SYSTEM_PROMPT, this.streamOpts(node.name));
448
- // Write page with front matter
486
+ const response = await this.invokeLLM(prompt, this.buildSystemPrompt(MODULE_SYSTEM_PROMPT), this.streamOpts(node.name));
487
+ // H1 uses the English module name (stable slug source); body is LLM-translated.
449
488
  const pageContent = sanitizeMermaidMarkdown(`# ${node.name}\n\n${response.content}`);
450
489
  await fs.writeFile(path.join(this.wikiDir, `${node.slug}.md`), pageContent, 'utf-8');
451
490
  }
@@ -480,7 +519,7 @@ export class WikiGenerator {
480
519
  CROSS_MODULE_CALLS: formatCallEdges(crossCalls),
481
520
  CROSS_PROCESSES: formatProcesses(processes),
482
521
  });
483
- const response = await this.invokeLLM(prompt, PARENT_SYSTEM_PROMPT, this.streamOpts(node.name));
522
+ const response = await this.invokeLLM(prompt, this.buildSystemPrompt(PARENT_SYSTEM_PROMPT), this.streamOpts(node.name));
484
523
  const pageContent = sanitizeMermaidMarkdown(`# ${node.name}\n\n${response.content}`);
485
524
  await fs.writeFile(path.join(this.wikiDir, `${node.slug}.md`), pageContent, 'utf-8');
486
525
  }
@@ -516,7 +555,7 @@ export class WikiGenerator {
516
555
  MODULE_EDGES: edgesText,
517
556
  TOP_PROCESSES: formatProcesses(topProcesses),
518
557
  });
519
- const response = await this.invokeLLM(prompt, OVERVIEW_SYSTEM_PROMPT, this.streamOpts('Generating overview', 88));
558
+ const response = await this.invokeLLM(prompt, this.buildSystemPrompt(OVERVIEW_SYSTEM_PROMPT), this.streamOpts('Generating overview', 88));
520
559
  const pageContent = sanitizeMermaidMarkdown(`# ${path.basename(this.repoPath)} — Wiki\n\n${response.content}`);
521
560
  await fs.writeFile(path.join(this.wikiDir, 'overview.md'), pageContent, 'utf-8');
522
561
  }
@@ -538,6 +577,7 @@ export class WikiGenerator {
538
577
  ...existingMeta,
539
578
  fromCommit: currentCommit,
540
579
  generatedAt: new Date().toISOString(),
580
+ lang: this.effectiveLang(),
541
581
  });
542
582
  return { pagesGenerated: 0, mode: 'incremental', failedModules: [] };
543
583
  }
@@ -627,6 +667,7 @@ export class WikiGenerator {
627
667
  fromCommit: currentCommit,
628
668
  generatedAt: new Date().toISOString(),
629
669
  model: this.llmConfig.model,
670
+ lang: this.effectiveLang(),
630
671
  });
631
672
  this.onProgress('done', 100, 'Incremental update complete');
632
673
  return { pagesGenerated, mode: 'incremental', failedModules: [...this.failedModules] };
@@ -59,6 +59,22 @@ interface RepoHandle {
59
59
  remoteUrl?: string;
60
60
  stats?: RegistryEntry['stats'];
61
61
  }
62
+ /**
63
+ * Resolve the git diff cwd for detect_changes, auto-detecting linked worktrees.
64
+ *
65
+ * When `launchCwd` is a linked worktree of the same canonical repository as
66
+ * `repoPath` (i.e. `getGitRoot(launchCwd)` differs from `repoPath` but both
67
+ * share the same `getCanonicalRepoRoot`), returns the worktree's git root so
68
+ * that `git diff` sees the correct working directory and index.
69
+ *
70
+ * Returns `repoPath` unchanged in all other cases (non-worktree, git
71
+ * unavailable, unrelated repo).
72
+ *
73
+ * Extracted as a module-level export so tests can pass any `launchCwd` instead
74
+ * of relying on `process.cwd()`, which is fixed to the server launch directory
75
+ * and cannot be changed mid-process.
76
+ */
77
+ export declare function resolveWorktreeCwd(repoPath: string, launchCwd: string): string;
62
78
  export declare class LocalBackend {
63
79
  private repos;
64
80
  private contextCache;
@@ -14,7 +14,8 @@ export { isWriteQuery };
14
14
  // at MCP server startup — crashes on unsupported Node ABI versions (#89)
15
15
  // git utilities available if needed
16
16
  // import { isGitRepo, getCurrentCommit, getGitRoot } from '../../storage/git.js';
17
- import { parseDiffHunks } from '../../storage/git.js';
17
+ import { parseDiffHunks, getCanonicalRepoRoot, getGitRoot, } from '../../storage/git.js';
18
+ import { realpathSync } from 'fs';
18
19
  import { listRegisteredRepos, cleanupOldKuzuFiles, } from '../../storage/repo-manager.js';
19
20
  import { GroupService } from '../../core/group/service.js';
20
21
  import { resolveAtGroupMemberRepoPath } from '../../core/group/resolve-at-member.js';
@@ -160,6 +161,55 @@ function logQueryTiming(query, phases) {
160
161
  const truncated = query.length > 80 ? `${query.slice(0, 80)}…` : query;
161
162
  logger.debug({ query: truncated, totalMs, phases }, 'GitNexus query timing');
162
163
  }
164
+ /** Resolve symlinks for path comparison; falls back to path.resolve on error.
165
+ * Uses `realpathSync.native` (not the pure-JS `realpathSync`) so that Windows
166
+ * 8.3 short names (e.g. RUNNER~1 → runneradmin) are expanded to long form,
167
+ * matching the output of `git rev-parse --show-toplevel`. */
168
+ function tryRealpath(p) {
169
+ try {
170
+ return realpathSync.native(p);
171
+ }
172
+ catch {
173
+ return path.resolve(p);
174
+ }
175
+ }
176
+ /**
177
+ * Resolve the git diff cwd for detect_changes, auto-detecting linked worktrees.
178
+ *
179
+ * When `launchCwd` is a linked worktree of the same canonical repository as
180
+ * `repoPath` (i.e. `getGitRoot(launchCwd)` differs from `repoPath` but both
181
+ * share the same `getCanonicalRepoRoot`), returns the worktree's git root so
182
+ * that `git diff` sees the correct working directory and index.
183
+ *
184
+ * Returns `repoPath` unchanged in all other cases (non-worktree, git
185
+ * unavailable, unrelated repo).
186
+ *
187
+ * Extracted as a module-level export so tests can pass any `launchCwd` instead
188
+ * of relying on `process.cwd()`, which is fixed to the server launch directory
189
+ * and cannot be changed mid-process.
190
+ */
191
+ export function resolveWorktreeCwd(repoPath, launchCwd) {
192
+ try {
193
+ const launchGitRoot = getGitRoot(launchCwd);
194
+ if (launchGitRoot) {
195
+ // Normalise via realpathSync before comparing so macOS /var → /private/var
196
+ // symlinks (and Windows 8.3 short names) don't create false mismatches.
197
+ const realLaunch = tryRealpath(launchGitRoot);
198
+ const realRepo = tryRealpath(repoPath);
199
+ if (realLaunch !== realRepo) {
200
+ const launchCanonical = getCanonicalRepoRoot(launchCwd);
201
+ const repoCanonical = getCanonicalRepoRoot(repoPath);
202
+ if (launchCanonical && repoCanonical && launchCanonical === repoCanonical) {
203
+ return launchGitRoot;
204
+ }
205
+ }
206
+ }
207
+ }
208
+ catch {
209
+ // Best-effort; fall through to repoPath.
210
+ }
211
+ return repoPath;
212
+ }
163
213
  export class LocalBackend {
164
214
  repos = new Map();
165
215
  contextCache = new Map();
@@ -1770,11 +1820,50 @@ export class LocalBackend {
1770
1820
  }
1771
1821
  let diffOutput;
1772
1822
  try {
1823
+ // Resolve the cwd for git diff.
1824
+ //
1825
+ // In a linked worktree (e.g. /repo/wt-feature/), the user's staged and
1826
+ // unstaged changes live in that worktree's separate working directory and
1827
+ // index. Running `git diff` from the canonical repo root sees a different
1828
+ // working tree and returns empty output.
1829
+ //
1830
+ // Resolution order (see resolveWorktreeCwd for details):
1831
+ // 1. params.worktree — explicit override, validated against the
1832
+ // registered repo's canonical root.
1833
+ // 2. Auto-detect — if the server's launch cwd (process.cwd()) is a
1834
+ // linked worktree of the same canonical repo, use its git root.
1835
+ // 3. repo.repoPath — fallback (original behaviour, handled inside
1836
+ // resolveWorktreeCwd when no worktree is detected).
1837
+ //
1838
+ // Start with the auto-detected value; override with the validated
1839
+ // explicit param when provided. This avoids a dead initial assignment.
1840
+ let diffCwd = resolveWorktreeCwd(repo.repoPath, process.cwd());
1841
+ if (params.worktree) {
1842
+ if (!path.isAbsolute(params.worktree)) {
1843
+ return {
1844
+ error: `worktree must be an absolute path, got: "${params.worktree}"`,
1845
+ };
1846
+ }
1847
+ const providedResolved = path.resolve(params.worktree);
1848
+ const repoCanonical = getCanonicalRepoRoot(repo.repoPath);
1849
+ if (!repoCanonical) {
1850
+ return {
1851
+ error: `Could not determine canonical root for repo "${repo.repoPath}". Is git available?`,
1852
+ };
1853
+ }
1854
+ const worktreeCanonical = getCanonicalRepoRoot(providedResolved);
1855
+ if (!worktreeCanonical || tryRealpath(worktreeCanonical) !== tryRealpath(repoCanonical)) {
1856
+ return {
1857
+ error: `worktree "${params.worktree}" is not a worktree of repo "${repo.repoPath}". Ensure the path is inside the same git repository.`,
1858
+ };
1859
+ }
1860
+ diffCwd = providedResolved;
1861
+ }
1773
1862
  // maxBuffer raised from Node's 1MB default to 256MB to avoid ENOBUFS on
1774
1863
  // repos with large unstaged/untracked diffs (e.g. unignored build folders).
1775
1864
  // See issue: spawnSync git ENOBUFS in detect_changes(scope="unstaged").
1776
1865
  diffOutput = execFileSync('git', diffArgs, {
1777
- cwd: repo.repoPath,
1866
+ cwd: diffCwd,
1778
1867
  encoding: 'utf-8',
1779
1868
  maxBuffer: 256 * 1024 * 1024,
1780
1869
  });
package/dist/mcp/tools.js CHANGED
@@ -218,6 +218,8 @@ Maps git diff hunks to indexed symbols, then traces which processes are impacted
218
218
  WHEN TO USE: Before committing — to understand what your changes affect. Pre-commit review, PR preparation.
219
219
  AFTER THIS: Review affected processes. Use context() on high-risk symbols. READ gitnexus://repo/{name}/process/{name} for full traces.
220
220
 
221
+ GIT WORKTREE SUPPORT: GitNexus automatically detects when the MCP server was launched from inside a linked git worktree and runs git diff against that worktree — no extra parameters needed in the common case. Pass "worktree" explicitly only when the server was started from a different directory than the worktree you are editing (e.g., the server runs from the canonical root but your changes are in a linked worktree at a different path).
222
+
221
223
  Returns: changed symbols, affected processes, and a risk summary.`,
222
224
  annotations: READ_ONLY_TOOL_ANNOTATIONS,
223
225
  inputSchema: {
@@ -233,6 +235,10 @@ Returns: changed symbols, affected processes, and a risk summary.`,
233
235
  type: 'string',
234
236
  description: 'Branch/commit for "compare" scope (e.g., "main")',
235
237
  },
238
+ worktree: {
239
+ type: 'string',
240
+ description: 'Absolute path to a linked git worktree. Pass this when your changes are in a worktree (the .git entry at that path is a file, not a directory). GitNexus will run git diff from that worktree so staged/unstaged changes are correctly detected.',
241
+ },
236
242
  repo: {
237
243
  type: 'string',
238
244
  description: 'Repository name or path. Omit if only one repo is indexed.',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gitnexus",
3
- "version": "1.6.6-rc.5",
3
+ "version": "1.6.6-rc.7",
4
4
  "description": "Graph-powered code intelligence for AI agents. Index any codebase, query via MCP or CLI.",
5
5
  "author": "Abhigyan Patwari",
6
6
  "license": "PolyForm-Noncommercial-1.0.0",