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.
- package/README.md +50 -59
- package/dist/cli/ai-context.js +9 -9
- package/dist/cli/analyze.js +139 -47
- package/dist/cli/augment.d.ts +13 -0
- package/dist/cli/augment.js +33 -0
- package/dist/cli/claude-hooks.d.ts +22 -0
- package/dist/cli/claude-hooks.js +97 -0
- package/dist/cli/eval-server.d.ts +30 -0
- package/dist/cli/eval-server.js +372 -0
- package/dist/cli/index.js +56 -1
- package/dist/cli/mcp.js +9 -0
- package/dist/cli/setup.js +184 -5
- package/dist/cli/tool.d.ts +37 -0
- package/dist/cli/tool.js +91 -0
- package/dist/cli/wiki.d.ts +13 -0
- package/dist/cli/wiki.js +199 -0
- package/dist/core/augmentation/engine.d.ts +26 -0
- package/dist/core/augmentation/engine.js +213 -0
- package/dist/core/embeddings/embedder.d.ts +2 -2
- package/dist/core/embeddings/embedder.js +11 -11
- package/dist/core/embeddings/embedding-pipeline.d.ts +2 -1
- package/dist/core/embeddings/embedding-pipeline.js +13 -5
- package/dist/core/embeddings/types.d.ts +2 -2
- package/dist/core/ingestion/call-processor.d.ts +7 -0
- package/dist/core/ingestion/call-processor.js +61 -23
- package/dist/core/ingestion/community-processor.js +34 -26
- package/dist/core/ingestion/filesystem-walker.js +15 -10
- package/dist/core/ingestion/heritage-processor.d.ts +6 -0
- package/dist/core/ingestion/heritage-processor.js +68 -5
- package/dist/core/ingestion/import-processor.d.ts +22 -0
- package/dist/core/ingestion/import-processor.js +215 -20
- package/dist/core/ingestion/parsing-processor.d.ts +8 -1
- package/dist/core/ingestion/parsing-processor.js +66 -25
- package/dist/core/ingestion/pipeline.js +104 -40
- package/dist/core/ingestion/process-processor.js +1 -1
- package/dist/core/ingestion/workers/parse-worker.d.ts +58 -0
- package/dist/core/ingestion/workers/parse-worker.js +451 -0
- package/dist/core/ingestion/workers/worker-pool.d.ts +22 -0
- package/dist/core/ingestion/workers/worker-pool.js +65 -0
- package/dist/core/kuzu/kuzu-adapter.d.ts +15 -1
- package/dist/core/kuzu/kuzu-adapter.js +177 -63
- package/dist/core/kuzu/schema.d.ts +1 -1
- package/dist/core/kuzu/schema.js +3 -0
- package/dist/core/search/bm25-index.js +13 -15
- package/dist/core/wiki/generator.d.ts +96 -0
- package/dist/core/wiki/generator.js +674 -0
- package/dist/core/wiki/graph-queries.d.ts +80 -0
- package/dist/core/wiki/graph-queries.js +238 -0
- package/dist/core/wiki/html-viewer.d.ts +10 -0
- package/dist/core/wiki/html-viewer.js +297 -0
- package/dist/core/wiki/llm-client.d.ts +36 -0
- package/dist/core/wiki/llm-client.js +111 -0
- package/dist/core/wiki/prompts.d.ts +53 -0
- package/dist/core/wiki/prompts.js +174 -0
- package/dist/mcp/core/embedder.js +4 -2
- package/dist/mcp/core/kuzu-adapter.d.ts +2 -1
- package/dist/mcp/core/kuzu-adapter.js +35 -15
- package/dist/mcp/local/local-backend.d.ts +54 -1
- package/dist/mcp/local/local-backend.js +716 -171
- package/dist/mcp/resources.d.ts +1 -1
- package/dist/mcp/resources.js +111 -73
- package/dist/mcp/server.d.ts +1 -1
- package/dist/mcp/server.js +91 -22
- package/dist/mcp/tools.js +80 -61
- package/dist/storage/git.d.ts +0 -1
- package/dist/storage/git.js +1 -8
- package/dist/storage/repo-manager.d.ts +17 -0
- package/dist/storage/repo-manager.js +26 -0
- package/hooks/claude/gitnexus-hook.cjs +135 -0
- package/hooks/claude/pre-tool-use.sh +78 -0
- package/hooks/claude/session-start.sh +42 -0
- package/package.json +4 -2
- package/skills/debugging.md +24 -22
- package/skills/exploring.md +26 -24
- package/skills/impact-analysis.md +19 -13
- 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>;
|
package/dist/cli/tool.js
ADDED
|
@@ -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>;
|
package/dist/cli/wiki.js
ADDED
|
@@ -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>;
|