mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-08-28 05:25:25 +00:00
Merge remote-tracking branch 'origin/main' into feat/kotlin-language-support
# Conflicts: # gitnexus/src/core/ingestion/parsing-processor.ts # gitnexus/src/core/ingestion/workers/parse-worker.ts
This commit is contained in:
commit
da63281a5a
17 changed files with 235 additions and 73 deletions
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
4
gitnexus/package-lock.json
generated
4
gitnexus/package-lock.json
generated
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<string, AstFrameworkPatternConfig[]> = {
|
||||
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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<string, any>): 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);
|
||||
|
|
|
|||
|
|
@ -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 });
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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, any>): string | null =>
|
|||
return 'CodeElement';
|
||||
};
|
||||
|
||||
const getDefinitionNodeFromCaptures = (captureMap: Record<string, any>): 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 } : {}),
|
||||
},
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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<void> => {
|
||||
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;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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) : [];
|
||||
|
|
|
|||
|
|
@ -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<void> {
|
||||
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();
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue