diff --git a/gitnexus-web/src/components/EmbeddingStatus.tsx b/gitnexus-web/src/components/EmbeddingStatus.tsx index ab706b99a..e5a0e5418 100644 --- a/gitnexus-web/src/components/EmbeddingStatus.tsx +++ b/gitnexus-web/src/components/EmbeddingStatus.tsx @@ -14,7 +14,7 @@ export const EmbeddingStatus = () => { startEmbeddings, graph, viewMode, - isBackendMode, + serverBaseUrl, testArrayParams, } = useAppState(); @@ -22,7 +22,7 @@ export const EmbeddingStatus = () => { const [showFallbackDialog, setShowFallbackDialog] = useState(false); // Only show when exploring a loaded graph; hide in backend mode (no WASM DB) - if (viewMode !== 'exploring' || !graph || isBackendMode) return null; + if (viewMode !== 'exploring' || !graph || serverBaseUrl) return null; const nodeCount = graph.nodes.length; diff --git a/gitnexus-web/src/services/server-connection.ts b/gitnexus-web/src/services/server-connection.ts index 7687a1fef..8c262000b 100644 --- a/gitnexus-web/src/services/server-connection.ts +++ b/gitnexus-web/src/services/server-connection.ts @@ -69,7 +69,9 @@ export async function fetchRepoInfo(baseUrl: string, repoName?: string): Promise if (!response.ok) { throw new Error(`Server returned ${response.status}: ${response.statusText}`); } - return response.json(); + const data = await response.json(); + // npm gitnexus@1.3.3 returns "path"; git HEAD returns "repoPath" + return { ...data, repoPath: data.repoPath ?? data.path }; } export async function fetchGraph( diff --git a/gitnexus/package-lock.json b/gitnexus/package-lock.json index 66a291cb8..b57c2208f 100644 --- a/gitnexus/package-lock.json +++ b/gitnexus/package-lock.json @@ -1,12 +1,12 @@ { "name": "gitnexus", - "version": "1.3.3", + "version": "1.3.4", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "gitnexus", - "version": "1.3.3", + "version": "1.3.4", "license": "PolyForm-Noncommercial-1.0.0", "dependencies": { "@huggingface/transformers": "^3.0.0", diff --git a/gitnexus/package.json b/gitnexus/package.json index 16de567bb..0845899e2 100644 --- a/gitnexus/package.json +++ b/gitnexus/package.json @@ -1,6 +1,6 @@ { "name": "gitnexus", - "version": "1.3.3", + "version": "1.3.4", "description": "Graph-powered code intelligence for AI agents. Index any codebase, query via MCP or CLI.", "author": "Abhigyan Patwari", "license": "PolyForm-Noncommercial-1.0.0", diff --git a/gitnexus/src/cli/analyze.ts b/gitnexus/src/cli/analyze.ts index 6ed6c05ef..f0c1ee320 100644 --- a/gitnexus/src/cli/analyze.ts +++ b/gitnexus/src/cli/analyze.ts @@ -10,7 +10,9 @@ import v8 from 'v8'; import cliProgress from 'cli-progress'; import { runPipelineFromRepo } from '../core/ingestion/pipeline.js'; import { initKuzu, loadGraphToKuzu, getKuzuStats, executeQuery, executeWithReusedStatement, closeKuzu, createFTSIndex, loadCachedEmbeddings } from '../core/kuzu/kuzu-adapter.js'; -import { runEmbeddingPipeline } from '../core/embeddings/embedding-pipeline.js'; +// Embedding imports are lazy (dynamic import) so onnxruntime-node is never +// loaded when embeddings are not requested. This avoids crashes on Node +// versions whose ABI is not yet supported by the native binary (#89). // disposeEmbedder intentionally not called — ONNX Runtime segfaults on cleanup (see #38) import { getStoragePaths, saveMeta, loadMeta, addToGitignore, registerRepo, getGlobalRegistryPath } from '../storage/repo-manager.js'; import { getCurrentCommit, isGitRepo, getGitRoot } from '../storage/git.js'; @@ -256,6 +258,7 @@ export const analyzeCommand = async ( if (!embeddingSkipped) { updateBar(90, 'Loading embedding model...'); const t0Emb = Date.now(); + const { runEmbeddingPipeline } = await import('../core/embeddings/embedding-pipeline.js'); await runEmbeddingPipeline( executeQuery, executeWithReusedStatement, diff --git a/gitnexus/src/cli/mcp.ts b/gitnexus/src/cli/mcp.ts index 90c55b9bd..933356ff4 100644 --- a/gitnexus/src/cli/mcp.ts +++ b/gitnexus/src/cli/mcp.ts @@ -8,7 +8,6 @@ import { startMCPServer } from '../mcp/server.js'; import { LocalBackend } from '../mcp/local/local-backend.js'; -import { listRegisteredRepos } from '../storage/repo-manager.js'; export const mcpCommand = async () => { // Prevent unhandled errors from crashing the MCP server process. @@ -21,33 +20,19 @@ export const mcpCommand = async () => { console.error(`GitNexus MCP: unhandled rejection — ${msg}`); }); - // Load all registered repos - const entries = await listRegisteredRepos({ validate: true }); - - if (entries.length === 0) { - console.error(''); - console.error(' GitNexus: No indexed repositories found.'); - console.error(''); - console.error(' To get started:'); - console.error(' 1. cd into a git repository'); - console.error(' 2. Run: gitnexus analyze'); - console.error(' 3. Restart your editor'); - console.error(''); - process.exit(1); - } - - // Initialize multi-repo backend from registry + // Initialize multi-repo backend from registry. + // The server starts even with 0 repos — tools call refreshRepos() lazily, + // so repos indexed after the server starts are discovered automatically. const backend = new LocalBackend(); - const ok = await backend.init(); + await backend.init(); - if (!ok) { - console.error('GitNexus: Failed to initialize backend from registry.'); - process.exit(1); + const repos = await backend.listRepos(); + if (repos.length === 0) { + console.error('GitNexus: No indexed repos yet. Run `gitnexus analyze` in a git repo — the server will pick it up automatically.'); + } else { + console.error(`GitNexus: MCP server starting with ${repos.length} repo(s): ${repos.map(r => r.name).join(', ')}`); } - const repoNames = (await backend.listRepos()).map(r => r.name); - console.error(`GitNexus: MCP server starting with ${repoNames.length} repo(s): ${repoNames.join(', ')}`); - - // Start MCP server (serves all repos) + // Start MCP server (serves all repos, discovers new ones lazily) await startMCPServer(backend); }; diff --git a/gitnexus/src/cli/wiki.ts b/gitnexus/src/cli/wiki.ts index ac626fe80..70ab00785 100644 --- a/gitnexus/src/cli/wiki.ts +++ b/gitnexus/src/cli/wiki.ts @@ -7,7 +7,7 @@ import path from 'path'; import readline from 'readline'; -import { execSync } from 'child_process'; +import { execSync, execFileSync } from 'child_process'; import cliProgress from 'cli-progress'; import { getGitRoot, isGitRepo } from '../storage/git.js'; import { getStoragePaths, loadMeta, loadCLIConfig, saveCLIConfig } from '../storage/repo-manager.js'; @@ -343,10 +343,11 @@ function hasGhCLI(): boolean { function publishGist(htmlPath: string): { url: string; rawUrl: string } | null { try { - const output = execSync( - `gh gist create "${htmlPath}" --desc "Repository Wiki — generated by GitNexus" --public`, - { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] }, - ).trim(); + const output = execFileSync('gh', [ + 'gist', 'create', htmlPath, + '--desc', 'Repository Wiki — generated by GitNexus', + '--public', + ], { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] }).trim(); // gh gist create prints the gist URL as the last line const lines = output.split('\n'); diff --git a/gitnexus/src/core/graph/types.ts b/gitnexus/src/core/graph/types.ts index a5b32e9c6..c675bdf1d 100644 --- a/gitnexus/src/core/graph/types.ts +++ b/gitnexus/src/core/graph/types.ts @@ -42,6 +42,9 @@ export type NodeProperties = { endLine?: number, language?: string, isExported?: boolean, + // Optional AST-derived framework hint (e.g. @Controller, @GetMapping) + astFrameworkMultiplier?: number, + astFrameworkReason?: string, // Community-specific properties heuristicLabel?: string, cohesion?: number, @@ -113,4 +116,4 @@ export interface KnowledgeGraph { addRelationship: (relationship: GraphRelationship) => void, removeNode: (nodeId: string) => boolean, removeNodesByFile: (filePath: string) => number, -} \ No newline at end of file +} diff --git a/gitnexus/src/core/ingestion/framework-detection.ts b/gitnexus/src/core/ingestion/framework-detection.ts index 4aec27f7c..299966c7e 100644 --- a/gitnexus/src/core/ingestion/framework-detection.ts +++ b/gitnexus/src/core/ingestion/framework-detection.ts @@ -1,8 +1,10 @@ /** * Framework Detection * - * Detects frameworks from file path patterns and provides entry point multipliers. - * This enables framework-aware entry point scoring. + * Detects frameworks from: + * 1) file path patterns + * 2) AST definition text (decorators/annotations/attributes) + * and provides entry point multipliers for process scoring. * * DESIGN: Returns null for unknown frameworks, which causes a 1.0 multiplier * (no bonus, no penalty) - same behavior as before this feature. @@ -272,12 +274,12 @@ export function detectFrameworkFromPath(filePath: string): FrameworkHint | null } // ============================================================================ -// FUTURE: AST-BASED PATTERNS (for Phase 3) +// AST-BASED FRAMEWORK DETECTION // ============================================================================ /** - * Patterns that indicate entry points within code (for future AST-based detection) - * These would require parsing decorators/annotations in the code itself. + * Patterns that indicate framework entry points within code definitions. + * These are matched against AST node text (class/method/function declaration text). */ export const FRAMEWORK_AST_PATTERNS = { // JavaScript/TypeScript decorators @@ -307,3 +309,63 @@ export const FRAMEWORK_AST_PATTERNS = { 'axum': ['Router::new'], 'rocket': ['#[get', '#[post'], }; + +interface AstFrameworkPatternConfig { + framework: string; + entryPointMultiplier: number; + reason: string; + patterns: string[]; +} + +const AST_FRAMEWORK_PATTERNS_BY_LANGUAGE: Record = { + javascript: [ + { framework: 'nestjs', entryPointMultiplier: 3.2, reason: 'nestjs-decorator', patterns: FRAMEWORK_AST_PATTERNS.nestjs }, + ], + typescript: [ + { framework: 'nestjs', entryPointMultiplier: 3.2, reason: 'nestjs-decorator', patterns: FRAMEWORK_AST_PATTERNS.nestjs }, + ], + python: [ + { framework: 'fastapi', entryPointMultiplier: 3.0, reason: 'fastapi-decorator', patterns: FRAMEWORK_AST_PATTERNS.fastapi }, + { framework: 'flask', entryPointMultiplier: 2.8, reason: 'flask-decorator', patterns: FRAMEWORK_AST_PATTERNS.flask }, + ], + java: [ + { framework: 'spring', entryPointMultiplier: 3.2, reason: 'spring-annotation', patterns: FRAMEWORK_AST_PATTERNS.spring }, + { framework: 'jaxrs', entryPointMultiplier: 3.0, reason: 'jaxrs-annotation', patterns: FRAMEWORK_AST_PATTERNS.jaxrs }, + ], + csharp: [ + { framework: 'aspnet', entryPointMultiplier: 3.2, reason: 'aspnet-attribute', patterns: FRAMEWORK_AST_PATTERNS.aspnet }, + ], + php: [ + { framework: 'laravel', entryPointMultiplier: 3.0, reason: 'php-route-attribute', patterns: FRAMEWORK_AST_PATTERNS.laravel }, + ], +}; + +/** + * Detect framework entry points from AST definition text (decorators/annotations/attributes). + * Returns null if no known pattern is found. + */ +export function detectFrameworkFromAST( + language: string, + definitionText: string +): FrameworkHint | null { + if (!language || !definitionText) return null; + + const configs = AST_FRAMEWORK_PATTERNS_BY_LANGUAGE[language.toLowerCase()]; + if (!configs || configs.length === 0) return null; + + const normalized = definitionText.toLowerCase(); + + for (const cfg of configs) { + for (const pattern of cfg.patterns) { + if (normalized.includes(pattern.toLowerCase())) { + return { + framework: cfg.framework, + entryPointMultiplier: cfg.entryPointMultiplier, + reason: cfg.reason, + }; + } + } + } + + return null; +} diff --git a/gitnexus/src/core/ingestion/parsing-processor.ts b/gitnexus/src/core/ingestion/parsing-processor.ts index b447afef8..a8242c58f 100644 --- a/gitnexus/src/core/ingestion/parsing-processor.ts +++ b/gitnexus/src/core/ingestion/parsing-processor.ts @@ -6,6 +6,7 @@ import { generateId } from '../../lib/utils.js'; import { SymbolTable } from './symbol-table.js'; import { ASTCache } from './ast-cache.js'; import { findSiblingChild, getLanguageFromFilename, yieldToEventLoop } from './utils.js'; +import { detectFrameworkFromAST } from './framework-detection.js'; import { WorkerPool } from './workers/worker-pool.js'; import type { ParseWorkerResult, ParseWorkerInput, ExtractedImport, ExtractedCall, ExtractedHeritage } from './workers/parse-worker.js'; @@ -17,6 +18,38 @@ export interface WorkerExtractedData { heritage: ExtractedHeritage[]; } +const getDefinitionNodeFromCaptures = (captureMap: Record): any | null => { + const definitionKeys = [ + 'definition.function', + 'definition.class', + 'definition.interface', + 'definition.method', + 'definition.struct', + 'definition.enum', + 'definition.namespace', + 'definition.module', + 'definition.trait', + 'definition.impl', + 'definition.type', + 'definition.const', + 'definition.static', + 'definition.typedef', + 'definition.macro', + 'definition.union', + 'definition.property', + 'definition.record', + 'definition.delegate', + 'definition.annotation', + 'definition.constructor', + 'definition.template', + ]; + + for (const key of definitionKeys) { + if (captureMap[key]) return captureMap[key]; + } + return null; +}; + // ============================================================================ // EXPORT DETECTION - Language-specific visibility detection // ============================================================================ @@ -304,14 +337,25 @@ const processParsingSequential = async ( const node: GraphNode = { id: nodeId, label: nodeLabel as any, - properties: { + properties: (() => { + const definitionNode = getDefinitionNodeFromCaptures(captureMap); + const frameworkHint = definitionNode + ? detectFrameworkFromAST(language, definitionNode.text || '') + : null; + + return { name: nodeName, filePath: file.path, startLine: nameNode.startPosition.row, endLine: nameNode.endPosition.row, language: language, isExported: isNodeExported(nameNode, nodeName, language), - } + ...(frameworkHint ? { + astFrameworkMultiplier: frameworkHint.entryPointMultiplier, + astFrameworkReason: frameworkHint.reason, + } : {}), + }; + })() }; graph.addNode(node); diff --git a/gitnexus/src/core/ingestion/process-processor.ts b/gitnexus/src/core/ingestion/process-processor.ts index 587aa3359..1c54001cb 100644 --- a/gitnexus/src/core/ingestion/process-processor.ts +++ b/gitnexus/src/core/ingestion/process-processor.ts @@ -285,7 +285,7 @@ const findEntryPoints = ( if (callees.length === 0) continue; // Calculate entry point score using new scoring system - const { score, reasons } = calculateEntryPointScore( + const { score: baseScore, reasons } = calculateEntryPointScore( node.properties.name, node.properties.language || 'javascript', node.properties.isExported ?? false, @@ -294,6 +294,13 @@ const findEntryPoints = ( filePath // Pass filePath for framework detection ); + let score = baseScore; + const astFrameworkMultiplier = node.properties.astFrameworkMultiplier ?? 1.0; + if (astFrameworkMultiplier > 1.0) { + score *= astFrameworkMultiplier; + reasons.push(`framework-ast:${node.properties.astFrameworkReason || 'decorator'}`); + } + if (score > 0) { entryPointCandidates.push({ id: node.id, score, reasons }); } diff --git a/gitnexus/src/core/ingestion/workers/parse-worker.ts b/gitnexus/src/core/ingestion/workers/parse-worker.ts index 24c63d0a2..3cb3fc3eb 100644 --- a/gitnexus/src/core/ingestion/workers/parse-worker.ts +++ b/gitnexus/src/core/ingestion/workers/parse-worker.ts @@ -14,6 +14,7 @@ import Kotlin from 'tree-sitter-kotlin'; import { SupportedLanguages } from '../../../config/supported-languages.js'; import { LANGUAGE_QUERIES } from '../tree-sitter-queries.js'; import { findSiblingChild, getLanguageFromFilename } from '../utils.js'; +import { detectFrameworkFromAST } from '../framework-detection.js'; import { generateId } from '../../../lib/utils.js'; // ============================================================================ @@ -30,6 +31,8 @@ interface ParsedNode { endLine: number; language: string; isExported: boolean; + astFrameworkMultiplier?: number; + astFrameworkReason?: string; description?: string; }; } @@ -399,6 +402,38 @@ const getLabelFromCaptures = (captureMap: Record): string | null => return 'CodeElement'; }; +const getDefinitionNodeFromCaptures = (captureMap: Record): any | null => { + const definitionKeys = [ + 'definition.function', + 'definition.class', + 'definition.interface', + 'definition.method', + 'definition.struct', + 'definition.enum', + 'definition.namespace', + 'definition.module', + 'definition.trait', + 'definition.impl', + 'definition.type', + 'definition.const', + 'definition.static', + 'definition.typedef', + 'definition.macro', + 'definition.union', + 'definition.property', + 'definition.record', + 'definition.delegate', + 'definition.annotation', + 'definition.constructor', + 'definition.template', + ]; + + for (const key of definitionKeys) { + if (captureMap[key]) return captureMap[key]; + } + return null; +}; + // ============================================================================ // Process a batch of files // ============================================================================ @@ -706,6 +741,11 @@ const processFileGroup = ( } } + const definitionNode = getDefinitionNodeFromCaptures(captureMap); + const frameworkHint = definitionNode + ? detectFrameworkFromAST(language, definitionNode.text || '') + : null; + result.nodes.push({ id: nodeId, label: nodeLabel, @@ -716,6 +756,10 @@ const processFileGroup = ( endLine: nameNode.endPosition.row, language: language, isExported: isNodeExported(nameNode, nodeName, language), + ...(frameworkHint ? { + astFrameworkMultiplier: frameworkHint.entryPointMultiplier, + astFrameworkReason: frameworkHint.reason, + } : {}), ...(description !== undefined ? { description } : {}), }, }); diff --git a/gitnexus/src/core/kuzu/kuzu-adapter.ts b/gitnexus/src/core/kuzu/kuzu-adapter.ts index b42978ae7..3f20f9084 100644 --- a/gitnexus/src/core/kuzu/kuzu-adapter.ts +++ b/gitnexus/src/core/kuzu/kuzu-adapter.ts @@ -674,21 +674,18 @@ export const getEmbeddingTableName = (): string => EMBEDDING_TABLE_NAME; /** * Load the FTS extension (required before using FTS functions). - * Safe to call multiple times — tracks loaded state. + * Safe to call multiple times — tracks loaded state via module-level ftsLoaded. */ export const loadFTSExtension = async (): Promise => { if (ftsLoaded) return; if (!conn) { throw new Error('KuzuDB not initialized. Call initKuzu first.'); } - if (ftsLoaded) return; try { await conn.query('INSTALL fts'); await conn.query('LOAD EXTENSION fts'); - ftsLoaded = true; } catch { // Extension may already be loaded - ftsLoaded = true; } ftsLoaded = true; }; diff --git a/gitnexus/src/core/wiki/generator.ts b/gitnexus/src/core/wiki/generator.ts index 29a16a541..666dc6e46 100644 --- a/gitnexus/src/core/wiki/generator.ts +++ b/gitnexus/src/core/wiki/generator.ts @@ -12,7 +12,7 @@ import fs from 'fs/promises'; import path from 'path'; -import { execSync } from 'child_process'; +import { execSync, execFileSync } from 'child_process'; import { initWikiDb, @@ -712,8 +712,8 @@ export class WikiGenerator { private getChangedFiles(fromCommit: string, toCommit: string): string[] { try { - const output = execSync( - `git diff ${fromCommit}..${toCommit} --name-only`, + const output = execFileSync( + 'git', ['diff', `${fromCommit}..${toCommit}`, '--name-only'], { cwd: this.repoPath }, ).toString().trim(); return output ? output.split('\n').filter(Boolean) : []; diff --git a/gitnexus/src/mcp/local/local-backend.ts b/gitnexus/src/mcp/local/local-backend.ts index c47560d2b..608a17a37 100644 --- a/gitnexus/src/mcp/local/local-backend.ts +++ b/gitnexus/src/mcp/local/local-backend.ts @@ -9,7 +9,8 @@ import fs from 'fs/promises'; import path from 'path'; import { initKuzu, executeQuery, closeKuzu, isKuzuReady } from '../core/kuzu-adapter.js'; -import { embedQuery, getEmbeddingDims, disposeEmbedder } from '../core/embedder.js'; +// Embedding imports are lazy (dynamic import) to avoid loading onnxruntime-node +// at MCP server startup — crashes on unsupported Node ABI versions (#89) // git utilities available if needed // import { isGitRepo, getCurrentCommit, getGitRoot } from '../../storage/git.js'; import { @@ -586,6 +587,7 @@ export class LocalBackend { const tableCheck = await executeQuery(repo.id, `MATCH (e:CodeEmbedding) RETURN COUNT(*) AS cnt LIMIT 1`); if (!tableCheck.length || (tableCheck[0].cnt ?? tableCheck[0][0]) === 0) return []; + const { embedQuery, getEmbeddingDims } = await import('../core/embedder.js'); const queryVec = await embedQuery(query); const dims = getEmbeddingDims(); const queryVecStr = `[${queryVec.join(',')}]`; @@ -1027,30 +1029,30 @@ export class LocalBackend { await this.ensureInitialized(repo.id); const scope = params.scope || 'unstaged'; - const { execSync } = await import('child_process'); - - // Build git diff command based on scope - let diffCmd: string; + const { execFileSync } = await import('child_process'); + + // Build git diff args based on scope (using execFileSync to avoid shell injection) + let diffArgs: string[]; switch (scope) { case 'staged': - diffCmd = 'git diff --staged --name-only'; + diffArgs = ['diff', '--staged', '--name-only']; break; case 'all': - diffCmd = 'git diff HEAD --name-only'; + diffArgs = ['diff', 'HEAD', '--name-only']; break; case 'compare': if (!params.base_ref) return { error: 'base_ref is required for "compare" scope' }; - diffCmd = `git diff ${params.base_ref} --name-only`; + diffArgs = ['diff', params.base_ref, '--name-only']; break; case 'unstaged': default: - diffCmd = 'git diff --name-only'; + diffArgs = ['diff', '--name-only']; break; } - + let changedFiles: string[]; try { - const output = execSync(diffCmd, { cwd: repo.repoPath, encoding: 'utf-8' }); + const output = execFileSync('git', diffArgs, { cwd: repo.repoPath, encoding: 'utf-8' }); changedFiles = output.trim().split('\n').filter(f => f.length > 0); } catch (err: any) { return { error: `Git diff failed: ${err.message}` }; @@ -1224,9 +1226,15 @@ export class LocalBackend { // Simple text search across the repo for the old name (in files not already covered by graph) try { - const { execSync } = await import('child_process'); - const rgCmd = `rg -l --type-add "code:*.{ts,tsx,js,jsx,py,go,rs,java}" -t code "\\b${oldName}\\b" .`; - const output = execSync(rgCmd, { cwd: repo.repoPath, encoding: 'utf-8', timeout: 5000 }); + const { execFileSync } = await import('child_process'); + const rgArgs = [ + '-l', + '--type-add', 'code:*.{ts,tsx,js,jsx,py,go,rs,java}', + '-t', 'code', + `\\b${oldName}\\b`, + '.', + ]; + const output = execFileSync('rg', rgArgs, { cwd: repo.repoPath, encoding: 'utf-8', timeout: 5000 }); const files = output.trim().split('\n').filter(f => f.length > 0); for (const file of files) { @@ -1590,7 +1598,11 @@ export class LocalBackend { async disconnect(): Promise { await closeKuzu(); // close all connections - await disposeEmbedder(); + // Note: we intentionally do NOT call disposeEmbedder() here. + // ONNX Runtime's native cleanup segfaults on macOS and some Linux configs, + // and importing the embedder module on Node v24+ crashes if onnxruntime + // was never loaded during the session. Since process.exit(0) follows + // immediately after disconnect(), the OS reclaims everything. See #38, #89. this.repos.clear(); this.contextCache.clear(); this.initializedRepos.clear(); diff --git a/gitnexus/src/mcp/staleness.ts b/gitnexus/src/mcp/staleness.ts index 0b7cf2a15..8c044e61d 100644 --- a/gitnexus/src/mcp/staleness.ts +++ b/gitnexus/src/mcp/staleness.ts @@ -5,7 +5,7 @@ * Returns a hint for the LLM to call analyze if stale. */ -import { execSync } from 'child_process'; +import { execFileSync } from 'child_process'; import path from 'path'; export interface StalenessInfo { @@ -20,8 +20,8 @@ export interface StalenessInfo { export function checkStaleness(repoPath: string, lastCommit: string): StalenessInfo { try { // Get count of commits between lastCommit and HEAD - const result = execSync( - `git rev-list --count ${lastCommit}..HEAD`, + const result = execFileSync( + 'git', ['rev-list', '--count', `${lastCommit}..HEAD`], { cwd: repoPath, encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] } ).trim(); diff --git a/gitnexus/src/server/api.ts b/gitnexus/src/server/api.ts index b0c9e9845..d587eff9a 100644 --- a/gitnexus/src/server/api.ts +++ b/gitnexus/src/server/api.ts @@ -18,8 +18,8 @@ import { NODE_TABLES } from '../core/kuzu/schema.js'; import { GraphNode, GraphRelationship } from '../core/graph/types.js'; import { searchFTSFromKuzu } from '../core/search/bm25-index.js'; import { hybridSearch } from '../core/search/hybrid-search.js'; -import { semanticSearch } from '../core/embeddings/embedding-pipeline.js'; -import { isEmbedderReady } from '../core/embeddings/embedder.js'; +// Embedding imports are lazy (dynamic import) to avoid loading onnxruntime-node +// at server startup — crashes on unsupported Node ABI versions (#89) import { LocalBackend } from '../mcp/local/local-backend.js'; import { mountMCPEndpoints } from './mcp-http.js'; @@ -230,7 +230,9 @@ export const createServer = async (port: number, host: string = '127.0.0.1') => : 10; const results = await withKuzuDb(kuzuPath, async () => { + const { isEmbedderReady } = await import('../core/embeddings/embedder.js'); if (isEmbedderReady()) { + const { semanticSearch } = await import('../core/embeddings/embedding-pipeline.js'); return hybridSearch(query, limit, executeQuery, semanticSearch); } // FTS-only fallback when embeddings aren't loaded