gitnexus 1.1.9 → 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 (55) hide show
  1. package/README.md +50 -59
  2. package/dist/cli/analyze.js +114 -32
  3. package/dist/cli/eval-server.d.ts +30 -0
  4. package/dist/cli/eval-server.js +372 -0
  5. package/dist/cli/index.js +51 -1
  6. package/dist/cli/mcp.js +9 -0
  7. package/dist/cli/setup.js +44 -7
  8. package/dist/cli/tool.d.ts +37 -0
  9. package/dist/cli/tool.js +91 -0
  10. package/dist/cli/wiki.d.ts +13 -0
  11. package/dist/cli/wiki.js +199 -0
  12. package/dist/core/embeddings/embedder.d.ts +2 -2
  13. package/dist/core/embeddings/embedder.js +10 -10
  14. package/dist/core/embeddings/embedding-pipeline.d.ts +2 -1
  15. package/dist/core/embeddings/embedding-pipeline.js +12 -4
  16. package/dist/core/embeddings/types.d.ts +2 -2
  17. package/dist/core/ingestion/call-processor.d.ts +7 -0
  18. package/dist/core/ingestion/call-processor.js +61 -23
  19. package/dist/core/ingestion/community-processor.js +34 -26
  20. package/dist/core/ingestion/filesystem-walker.js +15 -10
  21. package/dist/core/ingestion/heritage-processor.d.ts +6 -0
  22. package/dist/core/ingestion/heritage-processor.js +68 -5
  23. package/dist/core/ingestion/import-processor.d.ts +22 -0
  24. package/dist/core/ingestion/import-processor.js +214 -19
  25. package/dist/core/ingestion/parsing-processor.d.ts +8 -1
  26. package/dist/core/ingestion/parsing-processor.js +66 -25
  27. package/dist/core/ingestion/pipeline.js +103 -39
  28. package/dist/core/ingestion/workers/parse-worker.d.ts +58 -0
  29. package/dist/core/ingestion/workers/parse-worker.js +451 -0
  30. package/dist/core/ingestion/workers/worker-pool.d.ts +22 -0
  31. package/dist/core/ingestion/workers/worker-pool.js +65 -0
  32. package/dist/core/kuzu/kuzu-adapter.d.ts +15 -1
  33. package/dist/core/kuzu/kuzu-adapter.js +177 -67
  34. package/dist/core/kuzu/schema.d.ts +1 -1
  35. package/dist/core/kuzu/schema.js +3 -0
  36. package/dist/core/wiki/generator.d.ts +96 -0
  37. package/dist/core/wiki/generator.js +674 -0
  38. package/dist/core/wiki/graph-queries.d.ts +80 -0
  39. package/dist/core/wiki/graph-queries.js +238 -0
  40. package/dist/core/wiki/html-viewer.d.ts +10 -0
  41. package/dist/core/wiki/html-viewer.js +297 -0
  42. package/dist/core/wiki/llm-client.d.ts +36 -0
  43. package/dist/core/wiki/llm-client.js +111 -0
  44. package/dist/core/wiki/prompts.d.ts +53 -0
  45. package/dist/core/wiki/prompts.js +174 -0
  46. package/dist/mcp/core/embedder.js +4 -2
  47. package/dist/mcp/core/kuzu-adapter.d.ts +2 -1
  48. package/dist/mcp/core/kuzu-adapter.js +35 -15
  49. package/dist/mcp/local/local-backend.js +9 -2
  50. package/dist/mcp/server.js +1 -1
  51. package/dist/storage/git.d.ts +0 -1
  52. package/dist/storage/git.js +1 -8
  53. package/dist/storage/repo-manager.d.ts +17 -0
  54. package/dist/storage/repo-manager.js +26 -0
  55. package/package.json +1 -1
@@ -0,0 +1,372 @@
1
+ /**
2
+ * Eval Server — Lightweight HTTP server for SWE-bench evaluation
3
+ *
4
+ * Keeps KuzuDB warm in memory so tool calls from the agent are near-instant.
5
+ * Designed to run inside Docker containers during SWE-bench evaluation.
6
+ *
7
+ * KEY DESIGN: Returns LLM-friendly text, not raw JSON.
8
+ * Raw JSON wastes tokens and is hard for models to parse. The text formatter
9
+ * converts structured results into compact, readable output that models
10
+ * can immediately act on. Next-step hints guide the agent through a
11
+ * productive tool-chaining workflow (query → context → impact → fix).
12
+ *
13
+ * Architecture:
14
+ * Agent bash cmd → curl localhost:PORT/tool/query → eval-server → LocalBackend → format → text
15
+ *
16
+ * Usage:
17
+ * gitnexus eval-server # default port 4848
18
+ * gitnexus eval-server --port 4848 # explicit port
19
+ * gitnexus eval-server --idle-timeout 300 # auto-shutdown after 300s idle
20
+ *
21
+ * API:
22
+ * POST /tool/:name — Call a tool. Body is JSON arguments. Returns formatted text.
23
+ * GET /health — Health check. Returns {"status":"ok","repos":[...]}
24
+ * POST /shutdown — Graceful shutdown.
25
+ */
26
+ import http from 'http';
27
+ import { LocalBackend } from '../mcp/local/local-backend.js';
28
+ // ─── Text Formatters ──────────────────────────────────────────────────
29
+ // Convert structured JSON results into compact, LLM-friendly text.
30
+ // Design: minimize tokens, maximize actionability.
31
+ function formatQueryResult(result) {
32
+ if (result.error)
33
+ return `Error: ${result.error}`;
34
+ const lines = [];
35
+ const processes = result.processes || [];
36
+ const symbols = result.process_symbols || [];
37
+ const defs = result.definitions || [];
38
+ if (processes.length === 0 && defs.length === 0) {
39
+ return 'No matching execution flows found. Try a different search term or use grep.';
40
+ }
41
+ lines.push(`Found ${processes.length} execution flow(s):\n`);
42
+ for (let i = 0; i < processes.length; i++) {
43
+ const p = processes[i];
44
+ lines.push(`${i + 1}. ${p.summary} (${p.step_count} steps, ${p.symbol_count} symbols)`);
45
+ // Show symbols belonging to this process
46
+ const procSymbols = symbols.filter((s) => s.process_id === p.id);
47
+ for (const s of procSymbols.slice(0, 6)) {
48
+ const loc = s.startLine ? `:${s.startLine}` : '';
49
+ lines.push(` ${s.type} ${s.name} → ${s.filePath}${loc}`);
50
+ }
51
+ if (procSymbols.length > 6) {
52
+ lines.push(` ... and ${procSymbols.length - 6} more`);
53
+ }
54
+ lines.push('');
55
+ }
56
+ if (defs.length > 0) {
57
+ lines.push(`Standalone definitions:`);
58
+ for (const d of defs.slice(0, 8)) {
59
+ lines.push(` ${d.type || 'Symbol'} ${d.name} → ${d.filePath || '?'}`);
60
+ }
61
+ if (defs.length > 8)
62
+ lines.push(` ... and ${defs.length - 8} more`);
63
+ }
64
+ return lines.join('\n').trim();
65
+ }
66
+ function formatContextResult(result) {
67
+ if (result.error)
68
+ return `Error: ${result.error}`;
69
+ if (result.status === 'ambiguous') {
70
+ const lines = [`Multiple symbols named '${result.candidates?.[0]?.name || '?'}'. Disambiguate with file path:\n`];
71
+ for (const c of result.candidates || []) {
72
+ lines.push(` ${c.kind} ${c.name} → ${c.filePath}:${c.line || '?'} (uid: ${c.uid})`);
73
+ }
74
+ lines.push(`\nRe-run: gitnexus-context "${result.candidates?.[0]?.name}" "<file_path>"`);
75
+ return lines.join('\n');
76
+ }
77
+ const sym = result.symbol;
78
+ if (!sym)
79
+ return 'Symbol not found.';
80
+ const lines = [];
81
+ const loc = sym.startLine ? `:${sym.startLine}-${sym.endLine}` : '';
82
+ lines.push(`${sym.kind} ${sym.name} → ${sym.filePath}${loc}`);
83
+ lines.push('');
84
+ // Incoming refs (who calls/imports/extends this)
85
+ const incoming = result.incoming || {};
86
+ const incomingCount = Object.values(incoming).reduce((sum, arr) => sum + arr.length, 0);
87
+ if (incomingCount > 0) {
88
+ lines.push(`Called/imported by (${incomingCount}):`);
89
+ for (const [relType, refs] of Object.entries(incoming)) {
90
+ for (const ref of refs.slice(0, 10)) {
91
+ lines.push(` ← [${relType}] ${ref.kind} ${ref.name} → ${ref.filePath}`);
92
+ }
93
+ }
94
+ lines.push('');
95
+ }
96
+ // Outgoing refs (what this calls/imports)
97
+ const outgoing = result.outgoing || {};
98
+ const outgoingCount = Object.values(outgoing).reduce((sum, arr) => sum + arr.length, 0);
99
+ if (outgoingCount > 0) {
100
+ lines.push(`Calls/imports (${outgoingCount}):`);
101
+ for (const [relType, refs] of Object.entries(outgoing)) {
102
+ for (const ref of refs.slice(0, 10)) {
103
+ lines.push(` → [${relType}] ${ref.kind} ${ref.name} → ${ref.filePath}`);
104
+ }
105
+ }
106
+ lines.push('');
107
+ }
108
+ // Processes
109
+ const procs = result.processes || [];
110
+ if (procs.length > 0) {
111
+ lines.push(`Participates in ${procs.length} execution flow(s):`);
112
+ for (const p of procs) {
113
+ lines.push(` • ${p.name} (step ${p.step_index}/${p.step_count})`);
114
+ }
115
+ }
116
+ if (sym.content) {
117
+ lines.push('');
118
+ lines.push(`Source:`);
119
+ lines.push(sym.content);
120
+ }
121
+ return lines.join('\n').trim();
122
+ }
123
+ function formatImpactResult(result) {
124
+ if (result.error)
125
+ return `Error: ${result.error}`;
126
+ const target = result.target;
127
+ const direction = result.direction;
128
+ const byDepth = result.byDepth || {};
129
+ const total = result.impactedCount || 0;
130
+ if (total === 0) {
131
+ return `${target?.name || '?'}: No ${direction} dependencies found. This symbol appears isolated.`;
132
+ }
133
+ const lines = [];
134
+ const dirLabel = direction === 'upstream' ? 'depends on this (will break if changed)' : 'this depends on';
135
+ lines.push(`Blast radius for ${target?.kind || ''} ${target?.name} (${direction}): ${total} symbol(s) ${dirLabel}\n`);
136
+ const depthLabels = {
137
+ 1: 'WILL BREAK (direct)',
138
+ 2: 'LIKELY AFFECTED (indirect)',
139
+ 3: 'MAY NEED TESTING (transitive)',
140
+ };
141
+ for (const depth of [1, 2, 3]) {
142
+ const items = byDepth[depth];
143
+ if (!items || items.length === 0)
144
+ continue;
145
+ lines.push(`d=${depth}: ${depthLabels[depth] || ''} (${items.length})`);
146
+ for (const item of items.slice(0, 12)) {
147
+ const conf = item.confidence < 1 ? ` (conf: ${item.confidence})` : '';
148
+ lines.push(` ${item.type} ${item.name} → ${item.filePath} [${item.relationType}]${conf}`);
149
+ }
150
+ if (items.length > 12) {
151
+ lines.push(` ... and ${items.length - 12} more`);
152
+ }
153
+ lines.push('');
154
+ }
155
+ return lines.join('\n').trim();
156
+ }
157
+ function formatCypherResult(result) {
158
+ if (result.error)
159
+ return `Error: ${result.error}`;
160
+ if (Array.isArray(result)) {
161
+ if (result.length === 0)
162
+ return 'Query returned 0 rows.';
163
+ // Format as simple table
164
+ const keys = Object.keys(result[0]);
165
+ const lines = [`${result.length} row(s):\n`];
166
+ for (const row of result.slice(0, 30)) {
167
+ const parts = keys.map(k => `${k}: ${row[k]}`);
168
+ lines.push(` ${parts.join(' | ')}`);
169
+ }
170
+ if (result.length > 30) {
171
+ lines.push(` ... ${result.length - 30} more rows`);
172
+ }
173
+ return lines.join('\n');
174
+ }
175
+ return typeof result === 'string' ? result : JSON.stringify(result, null, 2);
176
+ }
177
+ function formatDetectChangesResult(result) {
178
+ if (result.error)
179
+ return `Error: ${result.error}`;
180
+ const summary = result.summary || {};
181
+ const lines = [];
182
+ if (summary.changed_count === 0) {
183
+ return 'No changes detected.';
184
+ }
185
+ lines.push(`Changes: ${summary.changed_files || 0} files, ${summary.changed_count || 0} symbols`);
186
+ lines.push(`Affected processes: ${summary.affected_count || 0}`);
187
+ lines.push(`Risk level: ${summary.risk_level || 'unknown'}\n`);
188
+ const changed = result.changed_symbols || [];
189
+ if (changed.length > 0) {
190
+ lines.push(`Changed symbols:`);
191
+ for (const s of changed.slice(0, 15)) {
192
+ lines.push(` ${s.type} ${s.name} → ${s.filePath}`);
193
+ }
194
+ if (changed.length > 15)
195
+ lines.push(` ... and ${changed.length - 15} more`);
196
+ lines.push('');
197
+ }
198
+ const affected = result.affected_processes || [];
199
+ if (affected.length > 0) {
200
+ lines.push(`Affected execution flows:`);
201
+ for (const p of affected.slice(0, 10)) {
202
+ const steps = (p.changed_steps || []).map((s) => s.symbol).join(', ');
203
+ lines.push(` • ${p.name} (${p.step_count} steps) — changed: ${steps}`);
204
+ }
205
+ }
206
+ return lines.join('\n').trim();
207
+ }
208
+ function formatListReposResult(result) {
209
+ if (!Array.isArray(result) || result.length === 0) {
210
+ return 'No indexed repositories.';
211
+ }
212
+ const lines = ['Indexed repositories:\n'];
213
+ for (const r of result) {
214
+ const stats = r.stats || {};
215
+ lines.push(` ${r.name} — ${stats.nodes || '?'} symbols, ${stats.edges || '?'} relationships, ${stats.processes || '?'} flows`);
216
+ lines.push(` Path: ${r.path}`);
217
+ lines.push(` Indexed: ${r.indexedAt}`);
218
+ }
219
+ return lines.join('\n');
220
+ }
221
+ /**
222
+ * Format a tool result as compact, LLM-friendly text.
223
+ */
224
+ function formatToolResult(toolName, result) {
225
+ switch (toolName) {
226
+ case 'query': return formatQueryResult(result);
227
+ case 'context': return formatContextResult(result);
228
+ case 'impact': return formatImpactResult(result);
229
+ case 'cypher': return formatCypherResult(result);
230
+ case 'detect_changes': return formatDetectChangesResult(result);
231
+ case 'list_repos': return formatListReposResult(result);
232
+ default: return typeof result === 'string' ? result : JSON.stringify(result, null, 2);
233
+ }
234
+ }
235
+ // ─── Next-Step Hints ──────────────────────────────────────────────────
236
+ // Guide the agent to the logical next tool call.
237
+ // Critical for tool chaining: query → context → impact → fix.
238
+ function getNextStepHint(toolName) {
239
+ switch (toolName) {
240
+ case 'query':
241
+ return '\n---\nNext: Pick a symbol above and run gitnexus-context "<name>" to see all its callers, callees, and execution flows.';
242
+ case 'context':
243
+ return '\n---\nNext: To check what breaks if you change this, run gitnexus-impact "<name>" upstream';
244
+ case 'impact':
245
+ return '\n---\nNext: Review d=1 items first (WILL BREAK). Read the source with cat to understand the code, then make your fix.';
246
+ case 'cypher':
247
+ return '\n---\nNext: To explore a result symbol in depth, run gitnexus-context "<name>"';
248
+ case 'detect_changes':
249
+ return '\n---\nNext: Run gitnexus-context "<symbol>" on high-risk changed symbols to check their callers.';
250
+ default:
251
+ return '';
252
+ }
253
+ }
254
+ // ─── Server ───────────────────────────────────────────────────────────
255
+ export async function evalServerCommand(options) {
256
+ const port = parseInt(options?.port || '4848');
257
+ const idleTimeoutSec = parseInt(options?.idleTimeout || '0');
258
+ const backend = new LocalBackend();
259
+ const ok = await backend.init();
260
+ if (!ok) {
261
+ console.error('GitNexus eval-server: No indexed repositories found. Run: gitnexus analyze');
262
+ process.exit(1);
263
+ }
264
+ const repos = backend.listRepos();
265
+ console.error(`GitNexus eval-server: ${repos.length} repo(s) loaded: ${repos.map(r => r.name).join(', ')}`);
266
+ let idleTimer = null;
267
+ function resetIdleTimer() {
268
+ if (idleTimeoutSec <= 0)
269
+ return;
270
+ if (idleTimer)
271
+ clearTimeout(idleTimer);
272
+ idleTimer = setTimeout(async () => {
273
+ console.error('GitNexus eval-server: Idle timeout reached, shutting down');
274
+ await backend.disconnect();
275
+ process.exit(0);
276
+ }, idleTimeoutSec * 1000);
277
+ }
278
+ const server = http.createServer(async (req, res) => {
279
+ resetIdleTimer();
280
+ try {
281
+ // Health check
282
+ if (req.method === 'GET' && req.url === '/health') {
283
+ res.setHeader('Content-Type', 'application/json');
284
+ res.writeHead(200);
285
+ res.end(JSON.stringify({ status: 'ok', repos: repos.map(r => r.name) }));
286
+ return;
287
+ }
288
+ // Shutdown
289
+ if (req.method === 'POST' && req.url === '/shutdown') {
290
+ res.setHeader('Content-Type', 'application/json');
291
+ res.writeHead(200);
292
+ res.end(JSON.stringify({ status: 'shutting_down' }));
293
+ setTimeout(async () => {
294
+ await backend.disconnect();
295
+ server.close();
296
+ process.exit(0);
297
+ }, 100);
298
+ return;
299
+ }
300
+ // Tool calls: POST /tool/:name
301
+ const toolMatch = req.url?.match(/^\/tool\/(\w+)$/);
302
+ if (req.method === 'POST' && toolMatch) {
303
+ const toolName = toolMatch[1];
304
+ const body = await readBody(req);
305
+ let args = {};
306
+ if (body.trim()) {
307
+ try {
308
+ args = JSON.parse(body);
309
+ }
310
+ catch {
311
+ res.setHeader('Content-Type', 'text/plain');
312
+ res.writeHead(400);
313
+ res.end('Error: Invalid JSON body');
314
+ return;
315
+ }
316
+ }
317
+ // Call tool, format result as text, append next-step hint
318
+ const result = await backend.callTool(toolName, args);
319
+ const formatted = formatToolResult(toolName, result);
320
+ const hint = getNextStepHint(toolName);
321
+ res.setHeader('Content-Type', 'text/plain');
322
+ res.writeHead(200);
323
+ res.end(formatted + hint);
324
+ return;
325
+ }
326
+ // 404
327
+ res.setHeader('Content-Type', 'text/plain');
328
+ res.writeHead(404);
329
+ res.end('Not found. Use POST /tool/:name or GET /health');
330
+ }
331
+ catch (err) {
332
+ res.setHeader('Content-Type', 'text/plain');
333
+ res.writeHead(500);
334
+ res.end(`Error: ${err.message || 'Internal error'}`);
335
+ }
336
+ });
337
+ server.listen(port, '127.0.0.1', () => {
338
+ console.error(`GitNexus eval-server: listening on http://127.0.0.1:${port}`);
339
+ console.error(` POST /tool/query — search execution flows`);
340
+ console.error(` POST /tool/context — 360-degree symbol view`);
341
+ console.error(` POST /tool/impact — blast radius analysis`);
342
+ console.error(` POST /tool/cypher — raw Cypher query`);
343
+ console.error(` GET /health — health check`);
344
+ console.error(` POST /shutdown — graceful shutdown`);
345
+ if (idleTimeoutSec > 0) {
346
+ console.error(` Auto-shutdown after ${idleTimeoutSec}s idle`);
347
+ }
348
+ try {
349
+ process.stdout.write(`GITNEXUS_EVAL_SERVER_READY:${port}\n`);
350
+ }
351
+ catch {
352
+ // stdout may not be available
353
+ }
354
+ });
355
+ resetIdleTimer();
356
+ const shutdown = async () => {
357
+ console.error('GitNexus eval-server: shutting down...');
358
+ await backend.disconnect();
359
+ server.close();
360
+ process.exit(0);
361
+ };
362
+ process.on('SIGINT', shutdown);
363
+ process.on('SIGTERM', shutdown);
364
+ }
365
+ function readBody(req) {
366
+ return new Promise((resolve, reject) => {
367
+ const chunks = [];
368
+ req.on('data', (chunk) => chunks.push(chunk));
369
+ req.on('end', () => resolve(Buffer.concat(chunks).toString('utf-8')));
370
+ req.on('error', reject);
371
+ });
372
+ }
package/dist/cli/index.js CHANGED
@@ -8,11 +8,14 @@ import { mcpCommand } from './mcp.js';
8
8
  import { cleanCommand } from './clean.js';
9
9
  import { setupCommand } from './setup.js';
10
10
  import { augmentCommand } from './augment.js';
11
+ import { wikiCommand } from './wiki.js';
12
+ import { queryCommand, contextCommand, impactCommand, cypherCommand } from './tool.js';
13
+ import { evalServerCommand } from './eval-server.js';
11
14
  const program = new Command();
12
15
  program
13
16
  .name('gitnexus')
14
17
  .description('GitNexus local CLI and MCP server')
15
- .version('1.1.1');
18
+ .version('1.1.9');
16
19
  program
17
20
  .command('setup')
18
21
  .description('One-time setup: configure MCP for Cursor, Claude Code, OpenCode')
@@ -46,8 +49,55 @@ program
46
49
  .option('-f, --force', 'Skip confirmation prompt')
47
50
  .option('--all', 'Clean all indexed repos')
48
51
  .action(cleanCommand);
52
+ program
53
+ .command('wiki [path]')
54
+ .description('Generate repository wiki from knowledge graph')
55
+ .option('-f, --force', 'Force full regeneration even if up to date')
56
+ .option('--model <model>', 'LLM model name (default: gpt-4o-mini)')
57
+ .option('--base-url <url>', 'LLM API base URL (default: OpenAI)')
58
+ .option('--api-key <key>', 'LLM API key (saved to ~/.gitnexus/config.json)')
59
+ .action(wikiCommand);
49
60
  program
50
61
  .command('augment <pattern>')
51
62
  .description('Augment a search pattern with knowledge graph context (used by hooks)')
52
63
  .action(augmentCommand);
64
+ // ─── Direct Tool Commands (no MCP overhead) ────────────────────────
65
+ // These invoke LocalBackend directly for use in eval, scripts, and CI.
66
+ program
67
+ .command('query <search_query>')
68
+ .description('Search the knowledge graph for execution flows related to a concept')
69
+ .option('-r, --repo <name>', 'Target repository (omit if only one indexed)')
70
+ .option('-c, --context <text>', 'Task context to improve ranking')
71
+ .option('-g, --goal <text>', 'What you want to find')
72
+ .option('-l, --limit <n>', 'Max processes to return (default: 5)')
73
+ .option('--content', 'Include full symbol source code')
74
+ .action(queryCommand);
75
+ program
76
+ .command('context [name]')
77
+ .description('360-degree view of a code symbol: callers, callees, processes')
78
+ .option('-r, --repo <name>', 'Target repository')
79
+ .option('-u, --uid <uid>', 'Direct symbol UID (zero-ambiguity lookup)')
80
+ .option('-f, --file <path>', 'File path to disambiguate common names')
81
+ .option('--content', 'Include full symbol source code')
82
+ .action(contextCommand);
83
+ program
84
+ .command('impact <target>')
85
+ .description('Blast radius analysis: what breaks if you change a symbol')
86
+ .option('-d, --direction <dir>', 'upstream (dependants) or downstream (dependencies)', 'upstream')
87
+ .option('-r, --repo <name>', 'Target repository')
88
+ .option('--depth <n>', 'Max relationship depth (default: 3)')
89
+ .option('--include-tests', 'Include test files in results')
90
+ .action(impactCommand);
91
+ program
92
+ .command('cypher <query>')
93
+ .description('Execute raw Cypher query against the knowledge graph')
94
+ .option('-r, --repo <name>', 'Target repository')
95
+ .action(cypherCommand);
96
+ // ─── Eval Server (persistent daemon for SWE-bench) ─────────────────
97
+ program
98
+ .command('eval-server')
99
+ .description('Start lightweight HTTP server for fast tool calls during evaluation')
100
+ .option('-p, --port <port>', 'Port number', '4848')
101
+ .option('--idle-timeout <seconds>', 'Auto-shutdown after N seconds idle (0 = disabled)', '0')
102
+ .action(evalServerCommand);
53
103
  program.parse(process.argv);
package/dist/cli/mcp.js CHANGED
@@ -9,6 +9,15 @@ import { startMCPServer } from '../mcp/server.js';
9
9
  import { LocalBackend } from '../mcp/local/local-backend.js';
10
10
  import { listRegisteredRepos } from '../storage/repo-manager.js';
11
11
  export const mcpCommand = async () => {
12
+ // Prevent unhandled errors from crashing the MCP server process.
13
+ // KuzuDB lock conflicts and transient errors should degrade gracefully.
14
+ process.on('uncaughtException', (err) => {
15
+ console.error(`GitNexus MCP: uncaught exception — ${err.message}`);
16
+ });
17
+ process.on('unhandledRejection', (reason) => {
18
+ const msg = reason instanceof Error ? reason.message : String(reason);
19
+ console.error(`GitNexus MCP: unhandled rejection — ${msg}`);
20
+ });
12
21
  // Load all registered repos
13
22
  const entries = await listRegisteredRepos({ validate: true });
14
23
  if (entries.length === 0) {
package/dist/cli/setup.js CHANGED
@@ -196,25 +196,62 @@ const SKILL_NAMES = ['exploring', 'debugging', 'impact-analysis', 'refactoring']
196
196
  * Install GitNexus skills to a target directory.
197
197
  * Each skill is installed as {targetDir}/gitnexus-{skillName}/SKILL.md
198
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.)
199
203
  */
200
204
  async function installSkillsTo(targetDir) {
201
205
  const installed = [];
206
+ const skillsRoot = path.join(__dirname, '..', '..', 'skills');
202
207
  for (const skillName of SKILL_NAMES) {
203
- const sourcePath = path.join(__dirname, '..', '..', 'skills', `${skillName}.md`);
204
208
  const skillDir = path.join(targetDir, `gitnexus-${skillName}`);
205
- const destPath = path.join(skillDir, 'SKILL.md');
206
209
  try {
207
- const content = await fs.readFile(sourcePath, 'utf-8');
208
- await fs.mkdir(skillDir, { recursive: true });
209
- await fs.writeFile(destPath, content, 'utf-8');
210
- installed.push(skillName);
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
+ }
211
231
  }
212
232
  catch {
213
- // Source skill file not found — skip
233
+ // Source skill not found — skip
214
234
  }
215
235
  }
216
236
  return installed;
217
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
+ }
218
255
  /**
219
256
  * Install global Cursor skills to ~/.cursor/skills/gitnexus/
220
257
  */
@@ -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>;