gitnexus 1.1.8 → 1.2.0

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 (76) hide show
  1. package/README.md +50 -59
  2. package/dist/cli/ai-context.js +9 -9
  3. package/dist/cli/analyze.js +139 -47
  4. package/dist/cli/augment.d.ts +13 -0
  5. package/dist/cli/augment.js +33 -0
  6. package/dist/cli/claude-hooks.d.ts +22 -0
  7. package/dist/cli/claude-hooks.js +97 -0
  8. package/dist/cli/eval-server.d.ts +30 -0
  9. package/dist/cli/eval-server.js +372 -0
  10. package/dist/cli/index.js +56 -1
  11. package/dist/cli/mcp.js +9 -0
  12. package/dist/cli/setup.js +184 -5
  13. package/dist/cli/tool.d.ts +37 -0
  14. package/dist/cli/tool.js +91 -0
  15. package/dist/cli/wiki.d.ts +13 -0
  16. package/dist/cli/wiki.js +199 -0
  17. package/dist/core/augmentation/engine.d.ts +26 -0
  18. package/dist/core/augmentation/engine.js +213 -0
  19. package/dist/core/embeddings/embedder.d.ts +2 -2
  20. package/dist/core/embeddings/embedder.js +11 -11
  21. package/dist/core/embeddings/embedding-pipeline.d.ts +2 -1
  22. package/dist/core/embeddings/embedding-pipeline.js +13 -5
  23. package/dist/core/embeddings/types.d.ts +2 -2
  24. package/dist/core/ingestion/call-processor.d.ts +7 -0
  25. package/dist/core/ingestion/call-processor.js +61 -23
  26. package/dist/core/ingestion/community-processor.js +34 -26
  27. package/dist/core/ingestion/filesystem-walker.js +15 -10
  28. package/dist/core/ingestion/heritage-processor.d.ts +6 -0
  29. package/dist/core/ingestion/heritage-processor.js +68 -5
  30. package/dist/core/ingestion/import-processor.d.ts +22 -0
  31. package/dist/core/ingestion/import-processor.js +215 -20
  32. package/dist/core/ingestion/parsing-processor.d.ts +8 -1
  33. package/dist/core/ingestion/parsing-processor.js +66 -25
  34. package/dist/core/ingestion/pipeline.js +104 -40
  35. package/dist/core/ingestion/process-processor.js +1 -1
  36. package/dist/core/ingestion/workers/parse-worker.d.ts +58 -0
  37. package/dist/core/ingestion/workers/parse-worker.js +451 -0
  38. package/dist/core/ingestion/workers/worker-pool.d.ts +22 -0
  39. package/dist/core/ingestion/workers/worker-pool.js +65 -0
  40. package/dist/core/kuzu/kuzu-adapter.d.ts +15 -1
  41. package/dist/core/kuzu/kuzu-adapter.js +177 -63
  42. package/dist/core/kuzu/schema.d.ts +1 -1
  43. package/dist/core/kuzu/schema.js +3 -0
  44. package/dist/core/search/bm25-index.js +13 -15
  45. package/dist/core/wiki/generator.d.ts +96 -0
  46. package/dist/core/wiki/generator.js +674 -0
  47. package/dist/core/wiki/graph-queries.d.ts +80 -0
  48. package/dist/core/wiki/graph-queries.js +238 -0
  49. package/dist/core/wiki/html-viewer.d.ts +10 -0
  50. package/dist/core/wiki/html-viewer.js +297 -0
  51. package/dist/core/wiki/llm-client.d.ts +36 -0
  52. package/dist/core/wiki/llm-client.js +111 -0
  53. package/dist/core/wiki/prompts.d.ts +53 -0
  54. package/dist/core/wiki/prompts.js +174 -0
  55. package/dist/mcp/core/embedder.js +4 -2
  56. package/dist/mcp/core/kuzu-adapter.d.ts +2 -1
  57. package/dist/mcp/core/kuzu-adapter.js +35 -15
  58. package/dist/mcp/local/local-backend.d.ts +54 -1
  59. package/dist/mcp/local/local-backend.js +716 -171
  60. package/dist/mcp/resources.d.ts +1 -1
  61. package/dist/mcp/resources.js +111 -73
  62. package/dist/mcp/server.d.ts +1 -1
  63. package/dist/mcp/server.js +91 -22
  64. package/dist/mcp/tools.js +80 -61
  65. package/dist/storage/git.d.ts +0 -1
  66. package/dist/storage/git.js +1 -8
  67. package/dist/storage/repo-manager.d.ts +17 -0
  68. package/dist/storage/repo-manager.js +26 -0
  69. package/hooks/claude/gitnexus-hook.cjs +135 -0
  70. package/hooks/claude/pre-tool-use.sh +78 -0
  71. package/hooks/claude/session-start.sh +42 -0
  72. package/package.json +4 -2
  73. package/skills/debugging.md +24 -22
  74. package/skills/exploring.md +26 -24
  75. package/skills/impact-analysis.md +19 -13
  76. package/skills/refactoring.md +37 -26
package/dist/cli/setup.js CHANGED
@@ -8,7 +8,10 @@
8
8
  import fs from 'fs/promises';
9
9
  import path from 'path';
10
10
  import os from 'os';
11
+ import { fileURLToPath } from 'url';
11
12
  import { getGlobalDir } from '../storage/repo-manager.js';
13
+ const __filename = fileURLToPath(import.meta.url);
14
+ const __dirname = path.dirname(__filename);
12
15
  /**
13
16
  * The MCP server entry for all editors
14
17
  */
@@ -82,8 +85,6 @@ async function setupCursor(result) {
82
85
  }
83
86
  }
84
87
  async function setupClaudeCode(result) {
85
- // Claude Code uses `claude mcp add` — we just print the command
86
- // Check for common Claude Code indicators
87
88
  const claudeDir = path.join(os.homedir(), '.claude');
88
89
  const hasClaude = await dirExists(claudeDir);
89
90
  if (!hasClaude) {
@@ -92,11 +93,82 @@ async function setupClaudeCode(result) {
92
93
  }
93
94
  // Claude Code uses a JSON settings file at ~/.claude.json or claude mcp add
94
95
  console.log('');
95
- console.log(' Claude Code detected. Run this command to add GitNexus:');
96
+ console.log(' Claude Code detected. Run this command to add GitNexus MCP:');
96
97
  console.log('');
97
98
  console.log(' claude mcp add gitnexus -- npx -y gitnexus mcp');
98
99
  console.log('');
99
- result.configured.push('Claude Code (manual step printed)');
100
+ result.configured.push('Claude Code (MCP manual step printed)');
101
+ }
102
+ /**
103
+ * Install GitNexus skills to ~/.claude/skills/ for Claude Code.
104
+ */
105
+ async function installClaudeCodeSkills(result) {
106
+ const claudeDir = path.join(os.homedir(), '.claude');
107
+ if (!(await dirExists(claudeDir)))
108
+ return;
109
+ const skillsDir = path.join(claudeDir, 'skills');
110
+ try {
111
+ const installed = await installSkillsTo(skillsDir);
112
+ if (installed.length > 0) {
113
+ result.configured.push(`Claude Code skills (${installed.length} skills → ~/.claude/skills/)`);
114
+ }
115
+ }
116
+ catch (err) {
117
+ result.errors.push(`Claude Code skills: ${err.message}`);
118
+ }
119
+ }
120
+ /**
121
+ * Install GitNexus hooks to ~/.claude/settings.json for Claude Code.
122
+ * Merges hook config without overwriting existing hooks.
123
+ */
124
+ async function installClaudeCodeHooks(result) {
125
+ const claudeDir = path.join(os.homedir(), '.claude');
126
+ if (!(await dirExists(claudeDir)))
127
+ return;
128
+ const settingsPath = path.join(claudeDir, 'settings.json');
129
+ // Source hooks bundled within the gitnexus package (hooks/claude/)
130
+ const pluginHooksPath = path.join(__dirname, '..', '..', 'hooks', 'claude');
131
+ // Copy unified hook script to ~/.claude/hooks/gitnexus/
132
+ const destHooksDir = path.join(claudeDir, 'hooks', 'gitnexus');
133
+ try {
134
+ await fs.mkdir(destHooksDir, { recursive: true });
135
+ const src = path.join(pluginHooksPath, 'gitnexus-hook.cjs');
136
+ const dest = path.join(destHooksDir, 'gitnexus-hook.cjs');
137
+ try {
138
+ const content = await fs.readFile(src, 'utf-8');
139
+ await fs.writeFile(dest, content, 'utf-8');
140
+ }
141
+ catch {
142
+ // Script not found in source — skip
143
+ }
144
+ const hookCmd = `node "${path.join(destHooksDir, 'gitnexus-hook.cjs').replace(/\\/g, '/')}"`;
145
+ // Merge hook config into ~/.claude/settings.json
146
+ const existing = await readJsonFile(settingsPath) || {};
147
+ if (!existing.hooks)
148
+ existing.hooks = {};
149
+ // NOTE: SessionStart hooks are broken on Windows (Claude Code bug #23576).
150
+ // Session context is delivered via CLAUDE.md / skills instead.
151
+ // Add PreToolUse hook if not already present
152
+ if (!existing.hooks.PreToolUse)
153
+ existing.hooks.PreToolUse = [];
154
+ const hasPreToolHook = existing.hooks.PreToolUse.some((h) => h.hooks?.some((hh) => hh.command?.includes('gitnexus')));
155
+ if (!hasPreToolHook) {
156
+ existing.hooks.PreToolUse.push({
157
+ matcher: 'Grep|Glob|Bash',
158
+ hooks: [{
159
+ type: 'command',
160
+ command: hookCmd,
161
+ timeout: 8000,
162
+ statusMessage: 'Enriching with GitNexus graph context...',
163
+ }],
164
+ });
165
+ }
166
+ await writeJsonFile(settingsPath, existing);
167
+ result.configured.push('Claude Code hooks (PreToolUse)');
168
+ }
169
+ catch (err) {
170
+ result.errors.push(`Claude Code hooks: ${err.message}`);
171
+ }
100
172
  }
101
173
  async function setupOpenCode(result) {
102
174
  const opencodeDir = path.join(os.homedir(), '.config', 'opencode');
@@ -118,6 +190,104 @@ async function setupOpenCode(result) {
118
190
  result.errors.push(`OpenCode: ${err.message}`);
119
191
  }
120
192
  }
193
+ // ─── Skill Installation ───────────────────────────────────────────
194
+ const SKILL_NAMES = ['exploring', 'debugging', 'impact-analysis', 'refactoring'];
195
+ /**
196
+ * Install GitNexus skills to a target directory.
197
+ * Each skill is installed as {targetDir}/gitnexus-{skillName}/SKILL.md
198
+ * following the Agent Skills standard (both Cursor and Claude Code).
199
+ *
200
+ * Supports two source layouts:
201
+ * - Flat file: skills/{name}.md → copied as SKILL.md
202
+ * - Directory: skills/{name}/SKILL.md → copied recursively (includes references/, etc.)
203
+ */
204
+ async function installSkillsTo(targetDir) {
205
+ const installed = [];
206
+ const skillsRoot = path.join(__dirname, '..', '..', 'skills');
207
+ for (const skillName of SKILL_NAMES) {
208
+ const skillDir = path.join(targetDir, `gitnexus-${skillName}`);
209
+ try {
210
+ // Try directory-based skill first (skills/{name}/SKILL.md)
211
+ const dirSource = path.join(skillsRoot, skillName);
212
+ const dirSkillFile = path.join(dirSource, 'SKILL.md');
213
+ let isDirectory = false;
214
+ try {
215
+ const stat = await fs.stat(dirSource);
216
+ isDirectory = stat.isDirectory();
217
+ }
218
+ catch { /* not a directory */ }
219
+ if (isDirectory) {
220
+ await copyDirRecursive(dirSource, skillDir);
221
+ installed.push(skillName);
222
+ }
223
+ else {
224
+ // Fall back to flat file (skills/{name}.md)
225
+ const flatSource = path.join(skillsRoot, `${skillName}.md`);
226
+ const content = await fs.readFile(flatSource, 'utf-8');
227
+ await fs.mkdir(skillDir, { recursive: true });
228
+ await fs.writeFile(path.join(skillDir, 'SKILL.md'), content, 'utf-8');
229
+ installed.push(skillName);
230
+ }
231
+ }
232
+ catch {
233
+ // Source skill not found — skip
234
+ }
235
+ }
236
+ return installed;
237
+ }
238
+ /**
239
+ * Recursively copy a directory tree.
240
+ */
241
+ async function copyDirRecursive(src, dest) {
242
+ await fs.mkdir(dest, { recursive: true });
243
+ const entries = await fs.readdir(src, { withFileTypes: true });
244
+ for (const entry of entries) {
245
+ const srcPath = path.join(src, entry.name);
246
+ const destPath = path.join(dest, entry.name);
247
+ if (entry.isDirectory()) {
248
+ await copyDirRecursive(srcPath, destPath);
249
+ }
250
+ else {
251
+ await fs.copyFile(srcPath, destPath);
252
+ }
253
+ }
254
+ }
255
+ /**
256
+ * Install global Cursor skills to ~/.cursor/skills/gitnexus/
257
+ */
258
+ async function installCursorSkills(result) {
259
+ const cursorDir = path.join(os.homedir(), '.cursor');
260
+ if (!(await dirExists(cursorDir)))
261
+ return;
262
+ const skillsDir = path.join(cursorDir, 'skills');
263
+ try {
264
+ const installed = await installSkillsTo(skillsDir);
265
+ if (installed.length > 0) {
266
+ result.configured.push(`Cursor skills (${installed.length} skills → ~/.cursor/skills/)`);
267
+ }
268
+ }
269
+ catch (err) {
270
+ result.errors.push(`Cursor skills: ${err.message}`);
271
+ }
272
+ }
273
+ /**
274
+ * Install global OpenCode skills to ~/.config/opencode/skill/gitnexus/
275
+ */
276
+ async function installOpenCodeSkills(result) {
277
+ const opencodeDir = path.join(os.homedir(), '.config', 'opencode');
278
+ if (!(await dirExists(opencodeDir)))
279
+ return;
280
+ const skillsDir = path.join(opencodeDir, 'skill');
281
+ try {
282
+ const installed = await installSkillsTo(skillsDir);
283
+ if (installed.length > 0) {
284
+ result.configured.push(`OpenCode skills (${installed.length} skills → ~/.config/opencode/skill/)`);
285
+ }
286
+ }
287
+ catch (err) {
288
+ result.errors.push(`OpenCode skills: ${err.message}`);
289
+ }
290
+ }
121
291
  // ─── Main command ──────────────────────────────────────────────────
122
292
  export const setupCommand = async () => {
123
293
  console.log('');
@@ -132,10 +302,15 @@ export const setupCommand = async () => {
132
302
  skipped: [],
133
303
  errors: [],
134
304
  };
135
- // Detect and configure each editor
305
+ // Detect and configure each editor's MCP
136
306
  await setupCursor(result);
137
307
  await setupClaudeCode(result);
138
308
  await setupOpenCode(result);
309
+ // Install global skills for platforms that support them
310
+ await installClaudeCodeSkills(result);
311
+ await installClaudeCodeHooks(result);
312
+ await installCursorSkills(result);
313
+ await installOpenCodeSkills(result);
139
314
  // Print results
140
315
  if (result.configured.length > 0) {
141
316
  console.log(' Configured:');
@@ -158,6 +333,10 @@ export const setupCommand = async () => {
158
333
  }
159
334
  }
160
335
  console.log('');
336
+ console.log(' Summary:');
337
+ console.log(` MCP configured for: ${result.configured.filter(c => !c.includes('skills')).join(', ') || 'none'}`);
338
+ console.log(` Skills installed to: ${result.configured.filter(c => c.includes('skills')).length > 0 ? result.configured.filter(c => c.includes('skills')).join(', ') : 'none'}`);
339
+ console.log('');
161
340
  console.log(' Next steps:');
162
341
  console.log(' 1. cd into any git repo');
163
342
  console.log(' 2. Run: gitnexus analyze');
@@ -0,0 +1,37 @@
1
+ /**
2
+ * Direct CLI Tool Commands
3
+ *
4
+ * Exposes GitNexus tools (query, context, impact, cypher) as direct CLI commands.
5
+ * Bypasses MCP entirely — invokes LocalBackend directly for minimal overhead.
6
+ *
7
+ * Usage:
8
+ * gitnexus query "authentication flow"
9
+ * gitnexus context --name "validateUser"
10
+ * gitnexus impact --target "AuthService" --direction upstream
11
+ * gitnexus cypher "MATCH (n:Function) RETURN n.name LIMIT 10"
12
+ *
13
+ * Note: Output goes to stderr because KuzuDB's native module captures stdout
14
+ * at the OS level during init. This is consistent with augment.ts.
15
+ */
16
+ export declare function queryCommand(queryText: string, options?: {
17
+ repo?: string;
18
+ context?: string;
19
+ goal?: string;
20
+ limit?: string;
21
+ content?: boolean;
22
+ }): Promise<void>;
23
+ export declare function contextCommand(name: string, options?: {
24
+ repo?: string;
25
+ file?: string;
26
+ uid?: string;
27
+ content?: boolean;
28
+ }): Promise<void>;
29
+ export declare function impactCommand(target: string, options?: {
30
+ direction?: string;
31
+ repo?: string;
32
+ depth?: string;
33
+ includeTests?: boolean;
34
+ }): Promise<void>;
35
+ export declare function cypherCommand(query: string, options?: {
36
+ repo?: string;
37
+ }): Promise<void>;
@@ -0,0 +1,91 @@
1
+ /**
2
+ * Direct CLI Tool Commands
3
+ *
4
+ * Exposes GitNexus tools (query, context, impact, cypher) as direct CLI commands.
5
+ * Bypasses MCP entirely — invokes LocalBackend directly for minimal overhead.
6
+ *
7
+ * Usage:
8
+ * gitnexus query "authentication flow"
9
+ * gitnexus context --name "validateUser"
10
+ * gitnexus impact --target "AuthService" --direction upstream
11
+ * gitnexus cypher "MATCH (n:Function) RETURN n.name LIMIT 10"
12
+ *
13
+ * Note: Output goes to stderr because KuzuDB's native module captures stdout
14
+ * at the OS level during init. This is consistent with augment.ts.
15
+ */
16
+ import { LocalBackend } from '../mcp/local/local-backend.js';
17
+ let _backend = null;
18
+ async function getBackend() {
19
+ if (_backend)
20
+ return _backend;
21
+ _backend = new LocalBackend();
22
+ const ok = await _backend.init();
23
+ if (!ok) {
24
+ console.error('GitNexus: No indexed repositories found. Run: gitnexus analyze');
25
+ process.exit(1);
26
+ }
27
+ return _backend;
28
+ }
29
+ function output(data) {
30
+ const text = typeof data === 'string' ? data : JSON.stringify(data, null, 2);
31
+ // stderr because KuzuDB captures stdout at OS level
32
+ process.stderr.write(text + '\n');
33
+ }
34
+ export async function queryCommand(queryText, options) {
35
+ if (!queryText?.trim()) {
36
+ console.error('Usage: gitnexus query <search_query>');
37
+ process.exit(1);
38
+ }
39
+ const backend = await getBackend();
40
+ const result = await backend.callTool('query', {
41
+ query: queryText,
42
+ task_context: options?.context,
43
+ goal: options?.goal,
44
+ limit: options?.limit ? parseInt(options.limit) : undefined,
45
+ include_content: options?.content ?? false,
46
+ repo: options?.repo,
47
+ });
48
+ output(result);
49
+ }
50
+ export async function contextCommand(name, options) {
51
+ if (!name?.trim() && !options?.uid) {
52
+ console.error('Usage: gitnexus context <symbol_name> [--uid <uid>] [--file <path>]');
53
+ process.exit(1);
54
+ }
55
+ const backend = await getBackend();
56
+ const result = await backend.callTool('context', {
57
+ name: name || undefined,
58
+ uid: options?.uid,
59
+ file_path: options?.file,
60
+ include_content: options?.content ?? false,
61
+ repo: options?.repo,
62
+ });
63
+ output(result);
64
+ }
65
+ export async function impactCommand(target, options) {
66
+ if (!target?.trim()) {
67
+ console.error('Usage: gitnexus impact <symbol_name> [--direction upstream|downstream]');
68
+ process.exit(1);
69
+ }
70
+ const backend = await getBackend();
71
+ const result = await backend.callTool('impact', {
72
+ target,
73
+ direction: options?.direction || 'upstream',
74
+ maxDepth: options?.depth ? parseInt(options.depth) : undefined,
75
+ includeTests: options?.includeTests ?? false,
76
+ repo: options?.repo,
77
+ });
78
+ output(result);
79
+ }
80
+ export async function cypherCommand(query, options) {
81
+ if (!query?.trim()) {
82
+ console.error('Usage: gitnexus cypher <cypher_query>');
83
+ process.exit(1);
84
+ }
85
+ const backend = await getBackend();
86
+ const result = await backend.callTool('cypher', {
87
+ query,
88
+ repo: options?.repo,
89
+ });
90
+ output(result);
91
+ }
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Wiki Command
3
+ *
4
+ * Generates repository documentation from the knowledge graph.
5
+ * Usage: gitnexus wiki [path] [options]
6
+ */
7
+ export interface WikiCommandOptions {
8
+ force?: boolean;
9
+ model?: string;
10
+ baseUrl?: string;
11
+ apiKey?: string;
12
+ }
13
+ export declare const wikiCommand: (inputPath?: string, options?: WikiCommandOptions) => Promise<void>;
@@ -0,0 +1,199 @@
1
+ /**
2
+ * Wiki Command
3
+ *
4
+ * Generates repository documentation from the knowledge graph.
5
+ * Usage: gitnexus wiki [path] [options]
6
+ */
7
+ import path from 'path';
8
+ import readline from 'readline';
9
+ import cliProgress from 'cli-progress';
10
+ import { getGitRoot, isGitRepo } from '../storage/git.js';
11
+ import { getStoragePaths, loadMeta, loadCLIConfig, saveCLIConfig } from '../storage/repo-manager.js';
12
+ import { WikiGenerator } from '../core/wiki/generator.js';
13
+ import { resolveLLMConfig } from '../core/wiki/llm-client.js';
14
+ /**
15
+ * Prompt the user for input via stdin.
16
+ */
17
+ function prompt(question, hide = false) {
18
+ return new Promise((resolve) => {
19
+ const rl = readline.createInterface({
20
+ input: process.stdin,
21
+ output: process.stdout,
22
+ });
23
+ if (hide && process.stdin.isTTY) {
24
+ // Mask input for API keys
25
+ process.stdout.write(question);
26
+ let input = '';
27
+ process.stdin.setRawMode(true);
28
+ process.stdin.resume();
29
+ process.stdin.setEncoding('utf-8');
30
+ const onData = (char) => {
31
+ if (char === '\n' || char === '\r' || char === '\u0004') {
32
+ process.stdin.setRawMode(false);
33
+ process.stdin.removeListener('data', onData);
34
+ process.stdout.write('\n');
35
+ rl.close();
36
+ resolve(input);
37
+ }
38
+ else if (char === '\u0003') {
39
+ // Ctrl+C
40
+ process.stdin.setRawMode(false);
41
+ rl.close();
42
+ process.exit(1);
43
+ }
44
+ else if (char === '\u007F' || char === '\b') {
45
+ // Backspace
46
+ if (input.length > 0) {
47
+ input = input.slice(0, -1);
48
+ process.stdout.write('\b \b');
49
+ }
50
+ }
51
+ else {
52
+ input += char;
53
+ process.stdout.write('*');
54
+ }
55
+ };
56
+ process.stdin.on('data', onData);
57
+ }
58
+ else {
59
+ rl.question(question, (answer) => {
60
+ rl.close();
61
+ resolve(answer.trim());
62
+ });
63
+ }
64
+ });
65
+ }
66
+ export const wikiCommand = async (inputPath, options) => {
67
+ console.log('\n GitNexus Wiki Generator\n');
68
+ // ── Resolve repo path ───────────────────────────────────────────────
69
+ let repoPath;
70
+ if (inputPath) {
71
+ repoPath = path.resolve(inputPath);
72
+ }
73
+ else {
74
+ const gitRoot = getGitRoot(process.cwd());
75
+ if (!gitRoot) {
76
+ console.log(' Error: Not inside a git repository\n');
77
+ process.exitCode = 1;
78
+ return;
79
+ }
80
+ repoPath = gitRoot;
81
+ }
82
+ if (!isGitRepo(repoPath)) {
83
+ console.log(' Error: Not a git repository\n');
84
+ process.exitCode = 1;
85
+ return;
86
+ }
87
+ // ── Check for existing index ────────────────────────────────────────
88
+ const { storagePath, kuzuPath } = getStoragePaths(repoPath);
89
+ const meta = await loadMeta(storagePath);
90
+ if (!meta) {
91
+ console.log(' Error: No GitNexus index found.');
92
+ console.log(' Run `gitnexus analyze` first to index this repository.\n');
93
+ process.exitCode = 1;
94
+ return;
95
+ }
96
+ // ── Resolve LLM config (with interactive fallback) ─────────────────
97
+ // If --api-key was passed via CLI, save it immediately
98
+ if (options?.apiKey) {
99
+ const existing = await loadCLIConfig();
100
+ await saveCLIConfig({ ...existing, apiKey: options.apiKey });
101
+ console.log(' API key saved to ~/.gitnexus/config.json\n');
102
+ }
103
+ let llmConfig = await resolveLLMConfig({
104
+ model: options?.model,
105
+ baseUrl: options?.baseUrl,
106
+ apiKey: options?.apiKey,
107
+ });
108
+ if (!llmConfig.apiKey) {
109
+ if (!process.stdin.isTTY) {
110
+ console.log(' Error: No LLM API key found.');
111
+ console.log(' Set OPENAI_API_KEY or GITNEXUS_API_KEY environment variable,');
112
+ console.log(' or pass --api-key <key>.\n');
113
+ process.exitCode = 1;
114
+ return;
115
+ }
116
+ console.log(' No API key configured.\n');
117
+ console.log(' The wiki command requires an LLM API key (OpenAI-compatible).');
118
+ console.log(' You can also set OPENAI_API_KEY or GITNEXUS_API_KEY env var.\n');
119
+ const key = await prompt(' Enter your API key: ', true);
120
+ if (!key) {
121
+ console.log('\n No key provided. Aborting.\n');
122
+ process.exitCode = 1;
123
+ return;
124
+ }
125
+ const save = await prompt(' Save key to ~/.gitnexus/config.json for future use? (Y/n): ');
126
+ if (!save || save.toLowerCase() === 'y' || save.toLowerCase() === 'yes') {
127
+ const existing = await loadCLIConfig();
128
+ await saveCLIConfig({ ...existing, apiKey: key });
129
+ console.log(' Key saved.\n');
130
+ }
131
+ else {
132
+ console.log(' Key will be used for this session only.\n');
133
+ }
134
+ llmConfig = { ...llmConfig, apiKey: key };
135
+ }
136
+ // ── Setup progress bar ──────────────────────────────────────────────
137
+ const bar = new cliProgress.SingleBar({
138
+ format: ' {bar} {percentage}% | {phase}',
139
+ barCompleteChar: '\u2588',
140
+ barIncompleteChar: '\u2591',
141
+ hideCursor: true,
142
+ barGlue: '',
143
+ autopadding: true,
144
+ clearOnComplete: false,
145
+ stopOnComplete: false,
146
+ }, cliProgress.Presets.shades_grey);
147
+ bar.start(100, 0, { phase: 'Initializing...' });
148
+ const t0 = Date.now();
149
+ // ── Run generator ───────────────────────────────────────────────────
150
+ const wikiOptions = {
151
+ force: options?.force,
152
+ model: options?.model,
153
+ baseUrl: options?.baseUrl,
154
+ };
155
+ const generator = new WikiGenerator(repoPath, storagePath, kuzuPath, llmConfig, wikiOptions, (phase, percent, detail) => {
156
+ bar.update(percent, { phase: detail || phase });
157
+ });
158
+ try {
159
+ const result = await generator.run();
160
+ bar.update(100, { phase: 'Done' });
161
+ bar.stop();
162
+ const elapsed = ((Date.now() - t0) / 1000).toFixed(1);
163
+ if (result.mode === 'up-to-date' && !options?.force) {
164
+ console.log('\n Wiki is already up to date.');
165
+ console.log(` ${path.join(storagePath, 'wiki')}\n`);
166
+ return;
167
+ }
168
+ const wikiDir = path.join(storagePath, 'wiki');
169
+ console.log(`\n Wiki generated successfully (${elapsed}s)\n`);
170
+ console.log(` Mode: ${result.mode}`);
171
+ console.log(` Pages: ${result.pagesGenerated}`);
172
+ console.log(` Output: ${wikiDir}`);
173
+ console.log(` Viewer: ${path.join(wikiDir, 'index.html')}`);
174
+ if (result.failedModules && result.failedModules.length > 0) {
175
+ console.log(`\n Failed modules (${result.failedModules.length}):`);
176
+ for (const mod of result.failedModules) {
177
+ console.log(` - ${mod}`);
178
+ }
179
+ console.log(' Re-run to retry failed modules (pages will be regenerated).');
180
+ }
181
+ console.log('');
182
+ }
183
+ catch (err) {
184
+ bar.stop();
185
+ if (err.message?.includes('No source files')) {
186
+ console.log(`\n ${err.message}\n`);
187
+ }
188
+ else if (err.message?.includes('API key') || err.message?.includes('API error')) {
189
+ console.log(`\n LLM Error: ${err.message}\n`);
190
+ }
191
+ else {
192
+ console.log(`\n Error: ${err.message}\n`);
193
+ if (process.env.DEBUG) {
194
+ console.error(err);
195
+ }
196
+ }
197
+ process.exitCode = 1;
198
+ }
199
+ };
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Augmentation Engine
3
+ *
4
+ * Lightweight, fast-path enrichment of search patterns with knowledge graph context.
5
+ * Designed to be called from platform hooks (Claude Code PreToolUse, Cursor beforeShellExecution)
6
+ * when an agent runs grep/glob/search.
7
+ *
8
+ * Performance target: <500ms cold start, <200ms warm.
9
+ *
10
+ * Design decisions:
11
+ * - Uses only BM25 search (no semantic/embedding) for speed
12
+ * - Clusters used internally for ranking, NEVER in output
13
+ * - Output is pure relationships: callers, callees, process participation
14
+ * - Graceful failure: any error → return empty string
15
+ */
16
+ /**
17
+ * Augment a search pattern with knowledge graph context.
18
+ *
19
+ * 1. BM25 search for the pattern
20
+ * 2. For top matches, fetch callers/callees/processes
21
+ * 3. Rank by internal cluster cohesion (not exposed)
22
+ * 4. Format as structured text block
23
+ *
24
+ * Returns empty string on any error (graceful failure).
25
+ */
26
+ export declare function augment(pattern: string, cwd?: string): Promise<string>;