diff --git a/.gitignore b/.gitignore index 2f3659245..0d9b76fbb 100644 --- a/.gitignore +++ b/.gitignore @@ -40,3 +40,4 @@ coverage/ .env*.local +.gitnexus diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 56cabebca..6b7f3144b 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -589,3 +589,5 @@ The design cleanly separates concerns across 7 layers, from symbolic math to num + + diff --git a/ARCHITECTURE_QUICK_REF.md b/ARCHITECTURE_QUICK_REF.md index bdd27a239..52066fcb1 100644 --- a/ARCHITECTURE_QUICK_REF.md +++ b/ARCHITECTURE_QUICK_REF.md @@ -380,3 +380,5 @@ model = pybamm.lithium_ion.DFN( + + diff --git a/GITNEXUS_ANALYSIS.md b/GITNEXUS_ANALYSIS.md index c9cd0b58e..49254ec50 100644 --- a/GITNEXUS_ANALYSIS.md +++ b/GITNEXUS_ANALYSIS.md @@ -379,3 +379,5 @@ Expression tree traversal: + + diff --git a/gitnexus-cli/src/cli/analyze.ts b/gitnexus-cli/src/cli/analyze.ts index 902a26bfd..b79303646 100644 --- a/gitnexus-cli/src/cli/analyze.ts +++ b/gitnexus-cli/src/cli/analyze.ts @@ -1,15 +1,26 @@ +/** + * Analyze Command + * + * Indexes a repository and stores the knowledge graph in .gitnexus/ + */ + import path from 'path'; import ora from 'ora'; import { runPipelineFromRepo } from '../core/ingestion/pipeline.js'; -import { initKuzu, loadGraphToKuzu, getKuzuStats, executeQuery, executeWithReusedStatement } from '../core/kuzu/kuzu-adapter.js'; +import { initKuzu, loadGraphToKuzu, getKuzuStats, executeQuery, executeWithReusedStatement, closeKuzu } from '../core/kuzu/kuzu-adapter.js'; import { buildBM25Index, exportBM25Index } from '../core/search/bm25-index.js'; import { runEmbeddingPipeline } from '../core/embeddings/embedding-pipeline.js'; -import { ensureRepoBase, getRepoStoragePath, saveMeta, loadMeta } from '../storage/repo-manager.js'; +import { getStoragePaths, saveMeta, loadMeta, addToGitignore } from '../storage/repo-manager.js'; import { getCurrentCommit, isGitRepo } from '../storage/git.js'; +export interface AnalyzeOptions { + force?: boolean; + skipEmbeddings?: boolean; +} + export const analyzeCommand = async ( inputPath?: string, - options?: { force?: boolean; skipEmbeddings?: boolean } + options?: AnalyzeOptions ) => { const repoPath = path.resolve(inputPath || '.'); const spinner = ora('Checking repository...').start(); @@ -20,42 +31,45 @@ export const analyzeCommand = async ( return; } - await ensureRepoBase(); - const storagePath = getRepoStoragePath(repoPath); - const kuzuPath = path.join(storagePath, 'kuzu'); - const bm25Path = path.join(storagePath, 'bm25.json'); - + const { storagePath, kuzuPath, bm25Path } = getStoragePaths(repoPath); const currentCommit = getCurrentCommit(repoPath); const existingMeta = await loadMeta(storagePath); + + // Skip if already indexed at same commit if (existingMeta && !options?.force && existingMeta.lastCommit === currentCommit) { spinner.succeed('Repository already up to date'); return; } + // Run ingestion pipeline spinner.text = 'Running ingestion pipeline...'; const pipelineResult = await runPipelineFromRepo(repoPath, (progress) => { spinner.text = `${progress.phase}: ${progress.percent}%`; }); + // Load graph into KuzuDB spinner.text = 'Loading graph into KuzuDB...'; await initKuzu(kuzuPath); await loadGraphToKuzu(pipelineResult.graph, pipelineResult.fileContents, storagePath); + // Build BM25 search index spinner.text = 'Building BM25 index...'; buildBM25Index(pipelineResult.fileContents); await exportBM25Index(bm25Path); + // Generate embeddings if (!options?.skipEmbeddings) { spinner.text = 'Generating embeddings...'; await runEmbeddingPipeline( executeQuery, executeWithReusedStatement, (progress) => { - spinner.text = `embeddings: ${progress.percent}%`; + spinner.text = `Embeddings: ${progress.percent}%`; } ); } + // Save metadata const stats = await getKuzuStats(); await saveMeta(storagePath, { repoPath, @@ -70,7 +84,14 @@ export const analyzeCommand = async ( }, }); - spinner.succeed('Repository indexed successfully'); - console.log(`Storage: ${storagePath}`); -}; + // Add .gitnexus to .gitignore + await addToGitignore(repoPath); + + // Close database + await closeKuzu(); + spinner.succeed('Repository indexed successfully'); + console.log(` Path: ${repoPath}`); + console.log(` Storage: ${storagePath}`); + console.log(` Stats: ${stats.nodes} nodes, ${stats.edges} edges`); +}; diff --git a/gitnexus-cli/src/cli/clean.ts b/gitnexus-cli/src/cli/clean.ts index a9eb4eb2c..40214333f 100644 --- a/gitnexus-cli/src/cli/clean.ts +++ b/gitnexus-cli/src/cli/clean.ts @@ -1,90 +1,34 @@ +/** + * Clean Command + * + * Removes the .gitnexus index from the current repository. + */ + import fs from 'fs/promises'; -import { listIndexedRepos, getRepoStoragePath, hashRepoPath } from '../storage/repo-manager.js'; +import { findRepo, getStoragePath } from '../storage/repo-manager.js'; -export const cleanCommand = async (target?: string, options?: { all?: boolean; force?: boolean }) => { - const repos = await listIndexedRepos(); - - if (repos.length === 0) { - console.log('No indexed repositories found.'); +export const cleanCommand = async (options?: { force?: boolean }) => { + const cwd = process.cwd(); + const repo = await findRepo(cwd); + + if (!repo) { + console.log('No indexed repository found in this directory.'); return; } - // Clean all repos - if (options?.all) { - if (!options.force) { - console.log(`āš ļø This will delete ${repos.length} indexed repository(ies):`); - repos.forEach(repo => { - const repoName = repo.meta.repoPath.split(/[/\\]/).pop() || repo.meta.repoPath; - console.log(` - ${repoName} (${repo.id})`); - }); - console.log('\nRun with --force to confirm deletion.'); - return; - } + const repoName = repo.repoPath.split(/[/\\]/).pop() || repo.repoPath; - for (const repo of repos) { - try { - await fs.rm(repo.storagePath, { recursive: true, force: true }); - const repoName = repo.meta.repoPath.split(/[/\\]/).pop() || repo.meta.repoPath; - console.log(`šŸ—‘ļø Deleted: ${repoName} (${repo.id})`); - } catch (err) { - console.error(`Failed to delete ${repo.id}:`, err); - } - } - console.log(`\nāœ… Cleaned ${repos.length} indexed repository(ies).`); + if (!options?.force) { + console.log(`āš ļø This will delete the GitNexus index for: ${repoName}`); + console.log(` Path: ${repo.storagePath}`); + console.log('\nRun with --force to confirm deletion.'); return; } - // Clean specific repo by ID or path - if (target) { - // Try to match by ID first - let repoToDelete = repos.find(r => r.id === target || r.id.startsWith(target)); - - // If not found by ID, try to match by path - if (!repoToDelete) { - const targetLower = target.toLowerCase(); - repoToDelete = repos.find(r => { - const repoPath = r.meta.repoPath.toLowerCase(); - const repoName = repoPath.split(/[/\\]/).pop() || ''; - return repoPath.includes(targetLower) || repoName === targetLower; - }); - } - - if (!repoToDelete) { - console.log(`āŒ No indexed repository found matching: ${target}`); - console.log('\nAvailable repositories:'); - repos.forEach(repo => { - const repoName = repo.meta.repoPath.split(/[/\\]/).pop() || repo.meta.repoPath; - console.log(` šŸ“ ${repoName} (${repo.id})`); - }); - return; - } - - const repoName = repoToDelete.meta.repoPath.split(/[/\\]/).pop() || repoToDelete.meta.repoPath; - - if (!options?.force) { - console.log(`āš ļø This will delete the index for: ${repoName}`); - console.log(` Path: ${repoToDelete.meta.repoPath}`); - console.log(` ID: ${repoToDelete.id}`); - console.log('\nRun with --force to confirm deletion.'); - return; - } - - try { - await fs.rm(repoToDelete.storagePath, { recursive: true, force: true }); - console.log(`šŸ—‘ļø Deleted: ${repoName} (${repoToDelete.id})`); - } catch (err) { - console.error(`Failed to delete ${repoToDelete.id}:`, err); - } - return; + try { + await fs.rm(repo.storagePath, { recursive: true, force: true }); + console.log(`šŸ—‘ļø Deleted: ${repo.storagePath}`); + } catch (err) { + console.error('Failed to delete:', err); } - - // No target specified - show usage - console.log('Usage:'); - console.log(' gitnexus clean [--force] Delete a specific repo'); - console.log(' gitnexus clean --all [--force] Delete all indexed repos'); - console.log('\nIndexed repositories:'); - repos.forEach(repo => { - const repoName = repo.meta.repoPath.split(/[/\\]/).pop() || repo.meta.repoPath; - console.log(` šŸ“ ${repoName} (${repo.id})`); - }); }; diff --git a/gitnexus-cli/src/cli/list.ts b/gitnexus-cli/src/cli/list.ts index 136fa9ff3..c034e68cb 100644 --- a/gitnexus-cli/src/cli/list.ts +++ b/gitnexus-cli/src/cli/list.ts @@ -1,24 +1,31 @@ -import { listIndexedRepos } from '../storage/repo-manager.js'; +/** + * List Command + * + * Shows info about the indexed repo in the current directory. + */ + +import path from 'path'; +import { findRepo } from '../storage/repo-manager.js'; export const listCommand = async () => { - const repos = await listIndexedRepos(); - if (repos.length === 0) { - console.log('No indexed repositories found.'); + const cwd = process.cwd(); + const repo = await findRepo(cwd); + + if (!repo) { + console.log('No indexed repository found in this directory.'); + console.log('Run `gitnexus analyze` to index your codebase.'); return; } - repos.forEach((repo, index) => { - const stats = repo.meta.stats || {}; - const repoName = repo.meta.repoPath.split(/[/\\]/).pop() || repo.meta.repoPath; - const indexedDate = new Date(repo.meta.indexedAt).toLocaleString(); - - console.log(`\nšŸ“ ${repoName}`); - console.log(` Path: ${repo.meta.repoPath}`); - console.log(` Indexed: ${indexedDate}`); - console.log(` Stats: ${stats.files ?? 0} files, ${stats.nodes ?? 0} nodes, ${stats.edges ?? 0} edges`); - console.log(` Commit: ${repo.meta.lastCommit?.slice(0, 7) || 'unknown'} (id: ${repo.id})`); - }); + const stats = repo.meta.stats || {}; + const repoName = repo.repoPath.split(/[/\\]/).pop() || repo.repoPath; + const indexedDate = new Date(repo.meta.indexedAt).toLocaleString(); + + console.log(`\nšŸ“ ${repoName}`); + console.log(` Path: ${repo.repoPath}`); + console.log(` Indexed: ${indexedDate}`); + console.log(` Stats: ${stats.files ?? 0} files, ${stats.nodes ?? 0} nodes, ${stats.edges ?? 0} edges`); + console.log(` Commit: ${repo.meta.lastCommit?.slice(0, 7) || 'unknown'}`); + if (stats.communities) console.log(` Communities: ${stats.communities}`); + if (stats.processes) console.log(` Processes: ${stats.processes}`); }; - - - diff --git a/gitnexus-cli/src/cli/status.ts b/gitnexus-cli/src/cli/status.ts index 5c7363530..314e6f062 100644 --- a/gitnexus-cli/src/cli/status.ts +++ b/gitnexus-cli/src/cli/status.ts @@ -1,28 +1,33 @@ -import { detectRepoByCwd } from '../storage/repo-manager.js'; +/** + * Status Command + * + * Shows the indexing status of the current repository. + */ + +import { findRepo } from '../storage/repo-manager.js'; import { getCurrentCommit, isGitRepo } from '../storage/git.js'; export const statusCommand = async () => { const cwd = process.cwd(); + if (!isGitRepo(cwd)) { console.log('Not a git repository.'); return; } - const repo = await detectRepoByCwd(cwd); + const repo = await findRepo(cwd); if (!repo) { - console.log('Repository not indexed. Run: gitnexus analyze'); + console.log('Repository not indexed.'); + console.log('Run: gitnexus analyze'); return; } - const current = getCurrentCommit(repo.meta.repoPath); - const upToDate = current && current === repo.meta.lastCommit; + const currentCommit = getCurrentCommit(repo.repoPath); + const isUpToDate = currentCommit === repo.meta.lastCommit; - console.log(`Repo: ${repo.meta.repoPath}`); - console.log(`Indexed at: ${repo.meta.indexedAt}`); - console.log(`Last commit indexed: ${repo.meta.lastCommit}`); - console.log(`Current commit: ${current}`); - console.log(`Status: ${upToDate ? 'up-to-date' : 'stale'}`); + console.log(`Repository: ${repo.repoPath}`); + console.log(`Indexed: ${new Date(repo.meta.indexedAt).toLocaleString()}`); + console.log(`Indexed commit: ${repo.meta.lastCommit?.slice(0, 7)}`); + console.log(`Current commit: ${currentCommit?.slice(0, 7)}`); + console.log(`Status: ${isUpToDate ? 'āœ… up-to-date' : 'āš ļø stale (re-run gitnexus analyze)'}`); }; - - - diff --git a/gitnexus-cli/src/mcp/server.ts b/gitnexus-cli/src/mcp/server.ts index fbfbe2a9e..9c59776ba 100644 --- a/gitnexus-cli/src/mcp/server.ts +++ b/gitnexus-cli/src/mcp/server.ts @@ -1,3 +1,9 @@ +/** + * CLI MCP Server + * + * Standalone MCP server that uses local .gitnexus/ index. + */ + import path from 'path'; import fs from 'fs/promises'; import { Server } from '@modelcontextprotocol/sdk/server/index.js'; @@ -9,7 +15,7 @@ import { ReadResourceRequestSchema, } from '@modelcontextprotocol/sdk/types.js'; import { GITNEXUS_TOOLS } from './tools.js'; -import { detectRepoByCwd, loadMeta } from '../storage/repo-manager.js'; +import { findRepo } from '../storage/repo-manager.js'; import { initKuzu, executeQuery } from '../core/kuzu/kuzu-adapter.js'; import { loadBM25Index, isBM25Ready, searchBM25 } from '../core/search/bm25-index.js'; import { hybridSearch } from '../core/search/hybrid-search.js'; @@ -51,7 +57,7 @@ export const startMCPServer = async () => { ); server.setRequestHandler(ListResourcesRequestSchema, async () => { - const repo = await detectRepoByCwd(process.cwd()); + const repo = await findRepo(process.cwd()); if (!repo) return { resources: [] }; return { resources: [ @@ -69,7 +75,7 @@ export const startMCPServer = async () => { if (request.params.uri !== 'gitnexus://context') { throw new Error(`Unknown resource: ${request.params.uri}`); } - const repo = await detectRepoByCwd(process.cwd()); + const repo = await findRepo(process.cwd()); if (!repo) { return { contents: [ @@ -101,7 +107,7 @@ export const startMCPServer = async () => { })); server.setRequestHandler(CallToolRequestSchema, async (request) => { - const repo = await detectRepoByCwd(process.cwd()); + const repo = await findRepo(process.cwd()); if (!repo) { return { content: [{ type: 'text', text: notIndexedMessage(process.cwd()) }], @@ -147,14 +153,7 @@ export const startMCPServer = async () => { isError: true, }; } - const meta = await loadMeta(repo.storagePath); - if (!meta) { - return { - content: [{ type: 'text', text: notIndexedMessage(process.cwd()) }], - isError: true, - }; - } - const fullPath = path.join(meta.repoPath, String(filePath)); + const fullPath = path.join(repo.repoPath, String(filePath)); const content = await fs.readFile(fullPath, 'utf-8'); return { content: [{ type: 'text', text: content }] }; } @@ -174,5 +173,3 @@ export const startMCPServer = async () => { const transport = new StdioServerTransport(); await server.connect(transport); }; - - diff --git a/gitnexus-cli/src/server/api.ts b/gitnexus-cli/src/server/api.ts index cbebcca19..caa2c4fb8 100644 --- a/gitnexus-cli/src/server/api.ts +++ b/gitnexus-cli/src/server/api.ts @@ -1,8 +1,14 @@ +/** + * HTTP API Server + * + * REST API for browser-based clients to query the local .gitnexus/ index. + */ + import express from 'express'; import cors from 'cors'; import path from 'path'; import fs from 'fs/promises'; -import { listIndexedRepos, loadMeta } from '../storage/repo-manager.js'; +import { findRepo, loadMeta } from '../storage/repo-manager.js'; import { initKuzu, executeQuery } from '../core/kuzu/kuzu-adapter.js'; import { NODE_TABLES } from '../core/kuzu/schema.js'; import { GraphNode, GraphRelationship } from '../core/graph/types.js'; @@ -10,7 +16,6 @@ import { loadBM25Index, searchBM25, isBM25Ready } from '../core/search/bm25-inde import { hybridSearch } from '../core/search/hybrid-search.js'; import { semanticSearch } from '../core/embeddings/embedding-pipeline.js'; import { isEmbedderReady } from '../core/embeddings/embedder.js'; -import { getRepoStoragePath } from '../storage/repo-manager.js'; const buildGraph = async (): Promise<{ nodes: GraphNode[]; relationships: GraphRelationship[] }> => { const nodes: GraphNode[] = []; @@ -31,38 +36,23 @@ const buildGraph = async (): Promise<{ nodes: GraphNode[]; relationships: GraphR const rows = await executeQuery(query); for (const row of rows) { - const id = row.id ?? row[0]; - const name = row.name ?? row.label ?? row[1]; - const filePath = row.filePath ?? row[2]; - const startLine = row.startLine ?? row[3]; - const endLine = row.endLine ?? row[4]; - const content = row.content ?? row[5]; - const heuristicLabel = row.heuristicLabel ?? row[2]; - const cohesion = row.cohesion ?? row[3]; - const symbolCount = row.symbolCount ?? row[4]; - const processType = row.processType ?? row[3]; - const stepCount = row.stepCount ?? row[4]; - const communities = row.communities ?? row[5]; - const entryPointId = row.entryPointId ?? row[6]; - const terminalId = row.terminalId ?? row[7]; - nodes.push({ - id, + id: row.id ?? row[0], label: table as GraphNode['label'], properties: { - name, - filePath, - startLine, - endLine, - content, - heuristicLabel, - cohesion, - symbolCount, - processType, - stepCount, - communities, - entryPointId, - terminalId, + name: row.name ?? row.label ?? row[1], + filePath: row.filePath ?? row[2], + startLine: row.startLine, + endLine: row.endLine, + content: row.content, + heuristicLabel: row.heuristicLabel, + cohesion: row.cohesion, + symbolCount: row.symbolCount, + processType: row.processType, + stepCount: row.stepCount, + communities: row.communities, + entryPointId: row.entryPointId, + terminalId: row.terminalId, } as GraphNode['properties'], }); } @@ -76,20 +66,14 @@ const buildGraph = async (): Promise<{ nodes: GraphNode[]; relationships: GraphR `MATCH (a)-[r:CodeRelation]->(b) RETURN a.id AS sourceId, b.id AS targetId, r.type AS type, r.confidence AS confidence, r.reason AS reason, r.step AS step` ); for (const row of relRows) { - const sourceId = row.sourceId ?? row[0]; - const targetId = row.targetId ?? row[1]; - const type = row.type ?? row[2]; - const confidence = row.confidence ?? row[3]; - const reason = row.reason ?? row[4]; - const step = row.step ?? row[5]; relationships.push({ - id: `${sourceId}_${type}_${targetId}`, - type, - sourceId, - targetId, - confidence, - reason, - step, + id: `${row.sourceId}_${row.type}_${row.targetId}`, + type: row.type, + sourceId: row.sourceId, + targetId: row.targetId, + confidence: row.confidence, + reason: row.reason, + step: row.step, }); } @@ -101,77 +85,53 @@ export const createServer = async (port: number) => { app.use(cors()); app.use(express.json({ limit: '10mb' })); - app.get('/api/repos', async (_req, res) => { - const repos = await listIndexedRepos(); + // Get repo info + app.get('/api/repo', async (_req, res) => { + const repo = await findRepo(process.cwd()); + if (!repo) { + res.status(404).json({ error: 'Repository not indexed. Run: gitnexus analyze' }); + return; + } res.json({ - repos: repos.map((r) => ({ - id: r.id, - repoPath: r.meta.repoPath, - indexedAt: r.meta.indexedAt, - stats: r.meta.stats || {}, - })), + repoPath: repo.repoPath, + indexedAt: repo.meta.indexedAt, + stats: repo.meta.stats || {}, }); }); - app.get('/api/repos/:id/graph', async (req, res) => { - const storagePath = getRepoStoragePath(req.params.id); - const meta = await loadMeta(storagePath); - if (!meta) { + // Get full graph + app.get('/api/graph', async (_req, res) => { + const repo = await findRepo(process.cwd()); + if (!repo) { res.status(404).json({ error: 'Repository not indexed' }); return; } - await initKuzu(path.join(storagePath, 'kuzu')); + await initKuzu(repo.kuzuPath); const graph = await buildGraph(); res.json(graph); }); - app.get('/api/repos/:id/serialized', async (req, res) => { - const storagePath = getRepoStoragePath(req.params.id); - const meta = await loadMeta(storagePath); - if (!meta) { + // Execute Cypher query + app.post('/api/query', async (req, res) => { + const repo = await findRepo(process.cwd()); + if (!repo) { res.status(404).json({ error: 'Repository not indexed' }); return; } - await initKuzu(path.join(storagePath, 'kuzu')); - const graph = await buildGraph(); - - const fileRows = await executeQuery(`MATCH (f:File) RETURN f.filePath AS path`); - const fileContents: Record = {}; - for (const row of fileRows) { - const relPath = row.path ?? row[0]; - try { - const fullPath = path.join(meta.repoPath, relPath); - const content = await fs.readFile(fullPath, 'utf-8'); - fileContents[relPath] = content; - } catch { - // ignore missing - } - } - - res.json({ nodes: graph.nodes, relationships: graph.relationships, fileContents }); - }); - - app.post('/api/repos/:id/query', async (req, res) => { - const storagePath = getRepoStoragePath(req.params.id); - const meta = await loadMeta(storagePath); - if (!meta) { - res.status(404).json({ error: 'Repository not indexed' }); - return; - } - await initKuzu(path.join(storagePath, 'kuzu')); + await initKuzu(repo.kuzuPath); const result = await executeQuery(req.body.cypher); res.json({ result }); }); - app.post('/api/repos/:id/search', async (req, res) => { - const storagePath = getRepoStoragePath(req.params.id); - const meta = await loadMeta(storagePath); - if (!meta) { + // Search + app.post('/api/search', async (req, res) => { + const repo = await findRepo(process.cwd()); + if (!repo) { res.status(404).json({ error: 'Repository not indexed' }); return; } - await initKuzu(path.join(storagePath, 'kuzu')); - await loadBM25Index(path.join(storagePath, 'bm25.json')); + await initKuzu(repo.kuzuPath); + await loadBM25Index(repo.bm25Path); const query = req.body.query ?? ''; const limit = req.body.limit ?? 10; @@ -183,24 +143,22 @@ export const createServer = async (port: number) => { } if (isBM25Ready()) { - const results = searchBM25(query, limit); - res.json({ results }); + res.json({ results: searchBM25(query, limit) }); return; } if (isEmbedderReady()) { - const results = await semanticSearch(executeQuery, query, limit); - res.json({ results }); + res.json({ results: await semanticSearch(executeQuery, query, limit) }); return; } res.json({ results: [] }); }); - app.get('/api/repos/:id/file', async (req, res) => { - const storagePath = getRepoStoragePath(req.params.id); - const meta = await loadMeta(storagePath); - if (!meta) { + // Read file + app.get('/api/file', async (req, res) => { + const repo = await findRepo(process.cwd()); + if (!repo) { res.status(404).json({ error: 'Repository not indexed' }); return; } @@ -209,7 +167,7 @@ export const createServer = async (port: number) => { res.status(400).json({ error: 'Missing path' }); return; } - const fullPath = path.join(meta.repoPath, filePath); + const fullPath = path.join(repo.repoPath, filePath); const content = await fs.readFile(fullPath, 'utf-8'); res.json({ content }); }); @@ -218,4 +176,3 @@ export const createServer = async (port: number) => { console.log(`GitNexus server running on http://localhost:${port}`); }); }; - diff --git a/gitnexus-cli/src/storage/repo-manager.ts b/gitnexus-cli/src/storage/repo-manager.ts index a148c0fca..81b47ce3c 100644 --- a/gitnexus-cli/src/storage/repo-manager.ts +++ b/gitnexus-cli/src/storage/repo-manager.ts @@ -1,7 +1,11 @@ +/** + * Repository Manager + * + * Manages GitNexus index storage in .gitnexus/ at repo root. + */ + import fs from 'fs/promises'; import path from 'path'; -import os from 'os'; -import crypto from 'crypto'; export interface RepoMeta { repoPath: string; @@ -17,7 +21,7 @@ export interface RepoMeta { } export interface IndexedRepo { - id: string; + repoPath: string; storagePath: string; kuzuPath: string; bm25Path: string; @@ -25,23 +29,31 @@ export interface IndexedRepo { meta: RepoMeta; } -const getHomeDir = (): string => path.join(os.homedir(), '.gitnexus'); -const getReposDir = (): string => path.join(getHomeDir(), 'repos'); +const GITNEXUS_DIR = '.gitnexus'; -export const ensureRepoBase = async (): Promise => { - await fs.mkdir(getReposDir(), { recursive: true }); +/** + * Get the .gitnexus storage path for a repository + */ +export const getStoragePath = (repoPath: string): string => { + return path.join(path.resolve(repoPath), GITNEXUS_DIR); }; -export const hashRepoPath = (repoPath: string): string => { - const resolved = path.resolve(repoPath); - return crypto.createHash('sha256').update(resolved).digest('hex').slice(0, 12); -}; - -export const getRepoStoragePath = (repoPathOrHash: string): string => { - const hash = repoPathOrHash.length === 12 ? repoPathOrHash : hashRepoPath(repoPathOrHash); - return path.join(getReposDir(), hash); +/** + * Get paths to key storage files + */ +export const getStoragePaths = (repoPath: string) => { + const storagePath = getStoragePath(repoPath); + return { + storagePath, + kuzuPath: path.join(storagePath, 'kuzu'), + bm25Path: path.join(storagePath, 'bm25.json'), + metaPath: path.join(storagePath, 'meta.json'), + }; }; +/** + * Load metadata from an indexed repo + */ export const loadMeta = async (storagePath: string): Promise => { try { const metaPath = path.join(storagePath, 'meta.json'); @@ -52,50 +64,75 @@ export const loadMeta = async (storagePath: string): Promise => } }; +/** + * Save metadata to storage + */ export const saveMeta = async (storagePath: string, meta: RepoMeta): Promise => { await fs.mkdir(storagePath, { recursive: true }); const metaPath = path.join(storagePath, 'meta.json'); await fs.writeFile(metaPath, JSON.stringify(meta, null, 2), 'utf-8'); }; -export const listIndexedRepos = async (): Promise => { - await ensureRepoBase(); - const dirs = await fs.readdir(getReposDir(), { withFileTypes: true }); - const repos: IndexedRepo[] = []; - - for (const dir of dirs) { - if (!dir.isDirectory()) continue; - const id = dir.name; - const storagePath = path.join(getReposDir(), id); - const meta = await loadMeta(storagePath); - if (!meta) continue; - repos.push({ - id, - storagePath, - kuzuPath: path.join(storagePath, 'kuzu'), - bm25Path: path.join(storagePath, 'bm25.json'), - metaPath: path.join(storagePath, 'meta.json'), - meta, - }); +/** + * Check if a path has a GitNexus index + */ +export const hasIndex = async (repoPath: string): Promise => { + const { metaPath } = getStoragePaths(repoPath); + try { + await fs.access(metaPath); + return true; + } catch { + return false; } - - return repos; }; -export const detectRepoByCwd = async (cwd: string): Promise => { - const repos = await listIndexedRepos(); - const cwdResolved = path.resolve(cwd); - const cwdLower = cwdResolved.toLowerCase(); +/** + * Load an indexed repo from a path + */ +export const loadRepo = async (repoPath: string): Promise => { + const paths = getStoragePaths(repoPath); + const meta = await loadMeta(paths.storagePath); + if (!meta) return null; + + return { + repoPath: path.resolve(repoPath), + ...paths, + meta, + }; +}; - for (const repo of repos) { - const repoPath = path.resolve(repo.meta.repoPath); - const repoLower = repoPath.toLowerCase(); - if (cwdLower.startsWith(repoLower) || repoLower.startsWith(cwdLower)) { - return repo; - } +/** + * Find .gitnexus by walking up from a starting path + */ +export const findRepo = async (startPath: string): Promise => { + let current = path.resolve(startPath); + const root = path.parse(current).root; + + while (current !== root) { + const repo = await loadRepo(current); + if (repo) return repo; + current = path.dirname(current); } + return null; }; - - +/** + * Add .gitnexus to .gitignore if not already present + */ +export const addToGitignore = async (repoPath: string): Promise => { + const gitignorePath = path.join(repoPath, '.gitignore'); + + try { + const content = await fs.readFile(gitignorePath, 'utf-8'); + if (content.includes(GITNEXUS_DIR)) return; + + const newContent = content.endsWith('\n') + ? `${content}${GITNEXUS_DIR}\n` + : `${content}\n${GITNEXUS_DIR}\n`; + await fs.writeFile(gitignorePath, newContent, 'utf-8'); + } catch { + // .gitignore doesn't exist, create it + await fs.writeFile(gitignorePath, `${GITNEXUS_DIR}\n`, 'utf-8'); + } +}; diff --git a/gitnexus-mcp/package-lock.json b/gitnexus-mcp/package-lock.json index 9798a4729..07821db63 100644 --- a/gitnexus-mcp/package-lock.json +++ b/gitnexus-mcp/package-lock.json @@ -1,15 +1,17 @@ { "name": "gitnexus-mcp", - "version": "0.1.1", + "version": "0.2.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "gitnexus-mcp", - "version": "0.1.1", + "version": "0.2.0", "license": "MIT", "dependencies": { "@modelcontextprotocol/sdk": "^1.0.0", + "kuzu": "^0.11.0", + "minisearch": "^7.1.0", "uuid": "^13.0.0", "ws": "^8.16.0" }, @@ -593,6 +595,67 @@ } } }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/aproba": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/aproba/-/aproba-2.1.0.tgz", + "integrity": "sha512-tLIEcj5GuR2RSTnxNKdkK0dJ/GrC7P38sUkiDmDuHfsHmbagTFAxDVIBltoklXEVIQ/f14IL8IMJ5pn9Hez1Ew==", + "license": "ISC" + }, + "node_modules/are-we-there-yet": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-3.0.1.tgz", + "integrity": "sha512-QZW4EDmGwlYur0Yyf/b2uGucHQMa8aFUP7eu9ddR73vvhFyt4V0Vl3QHPcTNJ8l6qYOBdxgXdnBXQrHilfRQBg==", + "deprecated": "This package is no longer supported.", + "license": "ISC", + "dependencies": { + "delegates": "^1.0.0", + "readable-stream": "^3.6.0" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, + "node_modules/axios": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.4.tgz", + "integrity": "sha512-1wVkUaAO6WyaYtCkcYCOx12ZgpGf9Zif+qXa4n+oYzK558YryKqiL6UWwd5DqiH3VRW0GYhTZQ/vlgJrCoNQlg==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.4", + "proxy-from-env": "^1.1.0" + } + }, "node_modules/body-parser": { "version": "2.2.2", "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.2.tgz", @@ -655,6 +718,100 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/chownr": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", + "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/cmake-js": { + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/cmake-js/-/cmake-js-7.4.0.tgz", + "integrity": "sha512-Lw0JxEHrmk+qNj1n9W9d4IvkDdYTBn7l2BW6XmtLj7WPpIo2shvxUy+YokfjMxAAOELNonQwX3stkPhM5xSC2Q==", + "license": "MIT", + "dependencies": { + "axios": "^1.6.5", + "debug": "^4", + "fs-extra": "^11.2.0", + "memory-stream": "^1.0.0", + "node-api-headers": "^1.1.0", + "npmlog": "^6.0.2", + "rc": "^1.2.7", + "semver": "^7.5.4", + "tar": "^6.2.0", + "url-join": "^4.0.1", + "which": "^2.0.2", + "yargs": "^17.7.2" + }, + "bin": { + "cmake-js": "bin/cmake-js" + }, + "engines": { + "node": ">= 14.15.0" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "license": "MIT" + }, + "node_modules/color-support": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz", + "integrity": "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==", + "license": "ISC", + "bin": { + "color-support": "bin.js" + } + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/console-control-strings": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", + "integrity": "sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==", + "license": "ISC" + }, "node_modules/content-disposition": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.1.tgz", @@ -739,6 +896,30 @@ } } }, + "node_modules/deep-extend": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", + "license": "MIT", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/delegates": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", + "integrity": "sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==", + "license": "MIT" + }, "node_modules/depd": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", @@ -768,6 +949,12 @@ "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", "license": "MIT" }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, "node_modules/encodeurl": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", @@ -807,6 +994,21 @@ "node": ">= 0.4" } }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/esbuild": { "version": "0.27.2", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.2.tgz", @@ -849,6 +1051,15 @@ "@esbuild/win32-x64": "0.27.2" } }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/escape-html": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", @@ -986,6 +1197,63 @@ "url": "https://opencollective.com/express" } }, + "node_modules/follow-redirects": { + "version": "1.15.11", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", + "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/form-data": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", + "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/form-data/node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/form-data/node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/forwarded": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", @@ -1004,6 +1272,44 @@ "node": ">= 0.8" } }, + "node_modules/fs-extra": { + "version": "11.3.3", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.3.tgz", + "integrity": "sha512-VWSRii4t0AFm6ixFFmLLx1t7wS1gh+ckoa84aOeapGum0h+EZd1EhEumSB+ZdDLnEPuucsVB9oB7cxJHap6Afg==", + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=14.14" + } + }, + "node_modules/fs-minipass": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", + "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", + "license": "ISC", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/fs-minipass/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/fsevents": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", @@ -1028,6 +1334,35 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/gauge": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/gauge/-/gauge-4.0.4.tgz", + "integrity": "sha512-f9m+BEN5jkg6a0fZjleidjN51VE1X+mPFQ2DJ0uv1V39oCLCbsGe6yjbBnp7eK7z/+GAon99a3nHuqbuuthyPg==", + "deprecated": "This package is no longer supported.", + "license": "ISC", + "dependencies": { + "aproba": "^1.0.3 || ^2.0.0", + "color-support": "^1.1.3", + "console-control-strings": "^1.1.0", + "has-unicode": "^2.0.1", + "signal-exit": "^3.0.7", + "string-width": "^4.2.3", + "strip-ansi": "^6.0.1", + "wide-align": "^1.1.5" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, "node_modules/get-intrinsic": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", @@ -1090,6 +1425,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "license": "ISC" + }, "node_modules/has-symbols": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", @@ -1102,6 +1443,27 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-unicode": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", + "integrity": "sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==", + "license": "ISC" + }, "node_modules/hasown": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", @@ -1166,6 +1528,12 @@ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", "license": "ISC" }, + "node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "license": "ISC" + }, "node_modules/ipaddr.js": { "version": "1.9.1", "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", @@ -1175,6 +1543,15 @@ "node": ">= 0.10" } }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/is-promise": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", @@ -1208,6 +1585,30 @@ "integrity": "sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA==", "license": "BSD-2-Clause" }, + "node_modules/jsonfile": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", + "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/kuzu": { + "version": "0.11.3", + "resolved": "https://registry.npmjs.org/kuzu/-/kuzu-0.11.3.tgz", + "integrity": "sha512-4+hD3Y+YMV3e0uiqTv1/GUal47D04l8qluw1WFWg8Nx3k7rLsHG1Pmq9WHIOlf1742svxQvTYQiuY6oS1qxAZA==", + "deprecated": "Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "cmake-js": "^7.3.0", + "node-addon-api": "^6.0.0" + } + }, "node_modules/math-intrinsics": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", @@ -1226,6 +1627,15 @@ "node": ">= 0.8" } }, + "node_modules/memory-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/memory-stream/-/memory-stream-1.0.0.tgz", + "integrity": "sha512-Wm13VcsPIMdG96dzILfij09PvuS3APtcKNh7M28FsCA/w6+1mjR7hhPmfFNoilX9xU7wTdhsH5lJAm6XNzdtww==", + "license": "MIT", + "dependencies": { + "readable-stream": "^3.4.0" + } + }, "node_modules/merge-descriptors": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", @@ -1263,6 +1673,67 @@ "url": "https://opencollective.com/express" } }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/minipass": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", + "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==", + "license": "ISC", + "engines": { + "node": ">=8" + } + }, + "node_modules/minisearch": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/minisearch/-/minisearch-7.2.0.tgz", + "integrity": "sha512-dqT2XBYUOZOiC5t2HRnwADjhNS2cecp9u+TJRiJ1Qp/f5qjkeT5APcGPjHw+bz89Ms8Jp+cG4AlE+QZ/QnDglg==", + "license": "MIT" + }, + "node_modules/minizlib": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", + "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", + "license": "MIT", + "dependencies": { + "minipass": "^3.0.0", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/minizlib/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "license": "MIT", + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -1278,6 +1749,34 @@ "node": ">= 0.6" } }, + "node_modules/node-addon-api": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-6.1.0.tgz", + "integrity": "sha512-+eawOlIgy680F0kBzPUNFhMZGtJ1YmqM6l4+Crf4IkImjYrO/mqPwRMh352g23uIaQKFItcQ64I7KMaJxHgAVA==", + "license": "MIT" + }, + "node_modules/node-api-headers": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/node-api-headers/-/node-api-headers-1.8.0.tgz", + "integrity": "sha512-jfnmiKWjRAGbdD1yQS28bknFM1tbHC1oucyuMPjmkEs+kpiu76aRs40WlTmBmyEgzDM76ge1DQ7XJ3R5deiVjQ==", + "license": "MIT" + }, + "node_modules/npmlog": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-6.0.2.tgz", + "integrity": "sha512-/vBvz5Jfr9dT/aFWd0FIRf+T/Q2WBsLENygUaFUqstqsycmZAP/t5BvFJTK0viFmSUxiUKTUplWy5vt+rvKIxg==", + "deprecated": "This package is no longer supported.", + "license": "ISC", + "dependencies": { + "are-we-there-yet": "^3.0.0", + "console-control-strings": "^1.1.0", + "gauge": "^4.0.3", + "set-blocking": "^2.0.0" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, "node_modules/object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", @@ -1370,6 +1869,12 @@ "node": ">= 0.10" } }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "license": "MIT" + }, "node_modules/qs": { "version": "6.14.1", "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.1.tgz", @@ -1409,6 +1914,44 @@ "node": ">= 0.10" } }, + "node_modules/rc": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", + "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", + "license": "(BSD-2-Clause OR MIT OR Apache-2.0)", + "dependencies": { + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + }, + "bin": { + "rc": "cli.js" + } + }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/require-from-string": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", @@ -1444,12 +1987,44 @@ "node": ">= 18" } }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, "node_modules/safer-buffer": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", "license": "MIT" }, + "node_modules/semver": { + "version": "7.7.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/send": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz", @@ -1495,6 +2070,12 @@ "url": "https://opencollective.com/express" } }, + "node_modules/set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==", + "license": "ISC" + }, "node_modules/setprototypeof": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", @@ -1594,6 +2175,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "license": "ISC" + }, "node_modules/statuses": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", @@ -1603,6 +2190,68 @@ "node": ">= 0.8" } }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/tar": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz", + "integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==", + "deprecated": "Old versions of tar are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exhorbitant rates) by contacting i@izs.me", + "license": "ISC", + "dependencies": { + "chownr": "^2.0.0", + "fs-minipass": "^2.0.0", + "minipass": "^5.0.0", + "minizlib": "^2.1.1", + "mkdirp": "^1.0.3", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/toidentifier": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", @@ -1667,6 +2316,15 @@ "dev": true, "license": "MIT" }, + "node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, "node_modules/unpipe": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", @@ -1676,6 +2334,18 @@ "node": ">= 0.8" } }, + "node_modules/url-join": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/url-join/-/url-join-4.0.1.tgz", + "integrity": "sha512-jk1+QP6ZJqyOiuEI9AEWQfju/nB2Pw466kbA0LEZljHwKeMgd9WrAEgEGxjPDD2+TNbbb37rTyhEfrCXfuKXnA==", + "license": "MIT" + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT" + }, "node_modules/uuid": { "version": "13.0.0", "resolved": "https://registry.npmjs.org/uuid/-/uuid-13.0.0.tgz", @@ -1713,6 +2383,32 @@ "node": ">= 8" } }, + "node_modules/wide-align": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.5.tgz", + "integrity": "sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==", + "license": "ISC", + "dependencies": { + "string-width": "^1.0.2 || 2 || 3 || 4" + } + }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, "node_modules/wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", @@ -1740,6 +2436,48 @@ } } }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "license": "ISC" + }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, "node_modules/zod": { "version": "4.3.5", "resolved": "https://registry.npmjs.org/zod/-/zod-4.3.5.tgz", diff --git a/gitnexus-mcp/package.json b/gitnexus-mcp/package.json index 037571f93..782cfc19e 100644 --- a/gitnexus-mcp/package.json +++ b/gitnexus-mcp/package.json @@ -31,6 +31,8 @@ }, "dependencies": { "@modelcontextprotocol/sdk": "^1.0.0", + "kuzu": "^0.11.0", + "minisearch": "^7.1.0", "uuid": "^13.0.0", "ws": "^8.16.0" }, diff --git a/gitnexus-mcp/src/commands/serve.ts b/gitnexus-mcp/src/commands/serve.ts index 524e40d60..97e13fc78 100644 --- a/gitnexus-mcp/src/commands/serve.ts +++ b/gitnexus-mcp/src/commands/serve.ts @@ -1,13 +1,14 @@ /** * Serve Command * - * Starts the MCP server that bridges external AI agents to GitNexus. - * - Listens on stdio for MCP protocol (from AI tools) - * - Hosts a local WebSocket bridge for the GitNexus browser app + * Starts the MCP server with hybrid mode: + * 1. First tries local .gitnexus/ index (standalone mode) + * 2. Falls back to WebSocket bridge if browser is running */ import { startMCPServer } from '../mcp/server.js'; import { WebSocketBridge } from '../bridge/websocket-server.js'; +import { LocalBackend } from '../local/local-backend.js'; interface ServeOptions { port: string; @@ -15,17 +16,30 @@ interface ServeOptions { export async function serveCommand(options: ServeOptions) { const port = parseInt(options.port, 10); + // Use GITNEXUS_CWD env var if set, otherwise use process.cwd() + const cwd = process.env.GITNEXUS_CWD || process.cwd(); - // Start local WebSocket bridge (browser connects to ws://localhost:) - const client = new WebSocketBridge(port); - const started = await client.start(); + // Try local backend first (standalone mode) + const local = new LocalBackend(); + const hasLocalIndex = await local.init(cwd); + + if (hasLocalIndex) { + console.error(`GitNexus: Using local index at ${local.storagePath}`); + await startMCPServer(local); + return; + } + + // No local index - fall back to browser bridge + console.error('GitNexus: No local .gitnexus/ found, starting browser bridge...'); + + const bridge = new WebSocketBridge(port); + const started = await bridge.start(); if (!started) { console.error(`Failed to start GitNexus browser bridge on port ${port}.`); - console.error('Another process is already using this port.'); + console.error('Run "gitnexus analyze" to index this repository for standalone mode.'); process.exit(1); } - // Start MCP server on stdio (AI tools connect here) - await startMCPServer(client); + await startMCPServer(bridge); } diff --git a/gitnexus-mcp/src/core/bm25-index.ts b/gitnexus-mcp/src/core/bm25-index.ts new file mode 100644 index 000000000..b13d6aebd --- /dev/null +++ b/gitnexus-mcp/src/core/bm25-index.ts @@ -0,0 +1,120 @@ +/** + * BM25 Full-Text Search Index (Read-Only) + * + * Uses MiniSearch for fast keyword-based search with BM25 ranking. + * For MCP, we only load and search - not build. + */ + +import MiniSearch from 'minisearch'; +import fs from 'fs/promises'; + +export interface BM25Document { + id: string; // File path + content: string; // File content + name: string; // File name (boosted in search) +} + +export interface BM25SearchResult { + filePath: string; + score: number; + rank: number; +} + +let searchIndex: MiniSearch | null = null; +let indexedDocCount = 0; + +/** + * Common stop words to filter out + */ +const STOP_WORDS = new Set([ + 'const', 'let', 'var', 'function', 'return', 'if', 'else', 'for', 'while', + 'class', 'new', 'this', 'import', 'export', 'from', 'default', 'async', 'await', + 'try', 'catch', 'throw', 'typeof', 'instanceof', 'true', 'false', 'null', 'undefined', + 'the', 'is', 'at', 'which', 'on', 'a', 'an', 'and', 'or', 'but', 'in', 'with', + 'to', 'of', 'it', 'be', 'as', 'by', 'that', 'for', 'are', 'was', 'were', +]); + +/** + * Tokenizer for BM25 search + */ +const tokenize = (text: string): string[] => { + const tokens = text.toLowerCase().split(/[\s\-_./\\(){}[\]<>:;,!?'"]+/); + const expanded: string[] = []; + for (const token of tokens) { + if (token.length === 0) continue; + const camelParts = token.replace(/([a-z])([A-Z])/g, '$1 $2').toLowerCase().split(' '); + expanded.push(...camelParts); + if (camelParts.length > 1) { + expanded.push(token); + } + } + return expanded.filter(t => t.length > 1 && !STOP_WORDS.has(t)); +}; + +/** + * Load a BM25 index from disk + */ +export const loadBM25Index = async (filePath: string): Promise => { + try { + const json = await fs.readFile(filePath, 'utf-8'); + // MiniSearch.loadJSON expects the raw JSON string, not a parsed object + searchIndex = MiniSearch.loadJSON(json, { + fields: ['content', 'name'], + storeFields: ['id'], + tokenize, + }); + indexedDocCount = searchIndex.documentCount; + return true; + } catch { + return false; + } +}; + +/** + * Search the BM25 index + */ +export const searchBM25 = (query: string, limit: number = 20): BM25SearchResult[] => { + if (!searchIndex) { + return []; + } + + const results = searchIndex.search(query, { + fuzzy: 0.2, + prefix: true, + boost: { name: 2 }, + }); + + return results.slice(0, limit).map((r, index) => ({ + filePath: r.id, + score: r.score, + rank: index + 1, + })); +}; + +/** + * Check if the BM25 index is ready + */ +export const isBM25Ready = (): boolean => { + return searchIndex !== null && indexedDocCount > 0; +}; + +/** + * Get index statistics + */ +export const getBM25Stats = (): { documentCount: number; termCount: number } => { + if (!searchIndex) { + return { documentCount: 0, termCount: 0 }; + } + return { + documentCount: indexedDocCount, + termCount: searchIndex.termCount, + }; +}; + +/** + * Clear the index + */ +export const clearBM25Index = (): void => { + searchIndex = null; + indexedDocCount = 0; +}; diff --git a/gitnexus-mcp/src/core/kuzu-adapter.ts b/gitnexus-mcp/src/core/kuzu-adapter.ts new file mode 100644 index 000000000..80a063b05 --- /dev/null +++ b/gitnexus-mcp/src/core/kuzu-adapter.ts @@ -0,0 +1,54 @@ +/** + * KuzuDB Adapter (Read-Only) + * + * Simplified adapter for MCP that only reads from existing .gitnexus/ database. + */ + +import fs from 'fs/promises'; +import path from 'path'; +import kuzu from 'kuzu'; + +let db: kuzu.Database | null = null; +let conn: kuzu.Connection | null = null; + +export const initKuzu = async (dbPath: string): Promise => { + if (conn) return; + + // Check if database exists + try { + await fs.stat(dbPath); + } catch { + throw new Error(`KuzuDB not found at ${dbPath}. Run: gitnexus analyze`); + } + + db = new kuzu.Database(dbPath); + conn = new kuzu.Connection(db); +}; + +export const executeQuery = async (cypher: string): Promise => { + if (!conn) { + throw new Error('KuzuDB not initialized. Call initKuzu first.'); + } + + const queryResult = await conn.query(cypher); + const result = Array.isArray(queryResult) ? queryResult[0] : queryResult; + const rows = await result.getAll(); + return rows; +}; + +export const closeKuzu = async (): Promise => { + if (conn) { + try { + await conn.close(); + } catch {} + conn = null; + } + if (db) { + try { + await db.close(); + } catch {} + db = null; + } +}; + +export const isKuzuReady = (): boolean => conn !== null && db !== null; diff --git a/gitnexus-mcp/src/local/local-backend.ts b/gitnexus-mcp/src/local/local-backend.ts new file mode 100644 index 000000000..0aa191dac --- /dev/null +++ b/gitnexus-mcp/src/local/local-backend.ts @@ -0,0 +1,589 @@ +/** + * Local Backend + * + * Provides tool implementations using local .gitnexus/ index. + * This enables MCP to work without the browser. + */ + +import fs from 'fs/promises'; +import path from 'path'; +import { initKuzu, executeQuery, closeKuzu, isKuzuReady } from '../core/kuzu-adapter.js'; +import { loadBM25Index, searchBM25, isBM25Ready } from '../core/bm25-index.js'; + +export interface RepoMeta { + repoPath: string; + lastCommit: string; + indexedAt: string; + stats?: { + files?: number; + nodes?: number; + edges?: number; + communities?: number; + processes?: number; + }; +} + +export interface IndexedRepo { + repoPath: string; + storagePath: string; + kuzuPath: string; + bm25Path: string; + metaPath: string; + meta: RepoMeta; +} + +const GITNEXUS_DIR = '.gitnexus'; + +function getStoragePaths(repoPath: string) { + const storagePath = path.join(path.resolve(repoPath), GITNEXUS_DIR); + return { + storagePath, + kuzuPath: path.join(storagePath, 'kuzu'), + bm25Path: path.join(storagePath, 'bm25.json'), + metaPath: path.join(storagePath, 'meta.json'), + }; +} + +async function loadMeta(storagePath: string): Promise { + try { + const metaPath = path.join(storagePath, 'meta.json'); + const raw = await fs.readFile(metaPath, 'utf-8'); + return JSON.parse(raw) as RepoMeta; + } catch { + return null; + } +} + +async function loadRepo(repoPath: string): Promise { + const paths = getStoragePaths(repoPath); + const meta = await loadMeta(paths.storagePath); + if (!meta) return null; + + return { + repoPath: path.resolve(repoPath), + ...paths, + meta, + }; +} + +export async function findRepo(startPath: string): Promise { + let current = path.resolve(startPath); + const root = path.parse(current).root; + + while (current !== root) { + const repo = await loadRepo(current); + if (repo) return repo; + current = path.dirname(current); + } + + return null; +} + +export interface CodebaseContext { + projectName: string; + stats: { + fileCount: number; + functionCount: number; + classCount: number; + interfaceCount: number; + methodCount: number; + communityCount: number; + processCount: number; + }; + hotspots: Array<{ + name: string; + type: string; + filePath: string; + connections: number; + }>; + folderTree: string; +} + +export class LocalBackend { + private repo: IndexedRepo | null = null; + private _context: CodebaseContext | null = null; + private initialized = false; + + async init(cwd: string): Promise { + this.repo = await findRepo(cwd); + if (!this.repo) return false; + + const stats = this.repo.meta.stats || {}; + this._context = { + projectName: path.basename(this.repo.repoPath), + stats: { + fileCount: stats.files || 0, + functionCount: stats.nodes || 0, + classCount: 0, + interfaceCount: 0, + methodCount: 0, + communityCount: stats.communities || 0, + processCount: stats.processes || 0, + }, + hotspots: [], + folderTree: '', + }; + + return true; + } + + private async ensureInitialized(): Promise { + if (this.initialized || !this.repo) return; + + await initKuzu(this.repo.kuzuPath); + await loadBM25Index(this.repo.bm25Path); + this.initialized = true; + } + + get context(): CodebaseContext | null { + return this._context; + } + + get isReady(): boolean { + return this.repo !== null; + } + + get repoPath(): string | null { + return this.repo?.repoPath || null; + } + + get storagePath(): string | null { + return this.repo?.storagePath || null; + } + + async callTool(method: string, params: any): Promise { + if (!this.repo) { + throw new Error('Repository not indexed. Run: gitnexus analyze'); + } + + switch (method) { + case 'context': + return this.getContext(); + case 'search': + return this.search(params); + case 'cypher': + return this.cypher(params); + case 'overview': + return this.overview(params); + case 'explore': + return this.explore(params); + case 'impact': + return this.impact(params); + case 'analyze': + return this.analyze(params); + default: + throw new Error(`Unknown tool: ${method}`); + } + } + + private async getContext(): Promise { + if (!this._context || !this.repo) { + return 'Repository not indexed. Run: gitnexus analyze'; + } + + const stats = this.repo.meta.stats || {}; + return [ + `# GitNexus: ${this._context.projectName}`, + '', + '## Stats', + `- Files: ${stats.files || 0}`, + `- Nodes: ${stats.nodes || 0}`, + `- Edges: ${stats.edges || 0}`, + `- Communities: ${stats.communities || 0}`, + `- Processes: ${stats.processes || 0}`, + '', + `Indexed: ${this.repo.meta.indexedAt}`, + `Commit: ${this.repo.meta.lastCommit?.slice(0, 7)}`, + '', + '## Available Tools', + '- **analyze**: Index/re-index repository', + '- **search**: Hybrid semantic + keyword search', + '- **cypher**: Graph queries (Cypher)', + '- **overview**: List communities and processes', + '- **explore**: Deep dive on symbol/cluster/process', + '- **impact**: Change impact analysis', + ].join('\n'); + } + + private async search(params: { query: string; limit?: number; depth?: string }): Promise { + await this.ensureInitialized(); + + const limit = params.limit || 10; + const query = params.query; + const depth = params.depth || 'definitions'; + + // BM25 keyword search + const bm25Results = isBM25Ready() ? searchBM25(query, limit * 2) : []; + + if (bm25Results.length === 0) { + return { message: 'No results found', query, bm25Ready: isBM25Ready() }; + } + + // Get node details from kuzu for top results + const results: any[] = []; + + for (const bm25Result of bm25Results.slice(0, limit)) { + try { + // Use CONTAINS to match file paths (handles relative vs full paths) + const fileName = bm25Result.filePath.split('/').pop() || bm25Result.filePath; + const symbolQuery = ` + MATCH (n) + WHERE n.filePath CONTAINS '${fileName.replace(/'/g, "''")}' + RETURN n.id AS id, n.name AS name, labels(n)[0] AS type, n.filePath AS filePath, n.startLine AS startLine, n.endLine AS endLine + LIMIT 5 + `; + const symbols = await executeQuery(symbolQuery); + + if (symbols.length > 0) { + for (const sym of symbols) { + const result: any = { + name: sym.name || sym[1], + type: sym.type || sym[2], + filePath: sym.filePath || sym[3], + startLine: sym.startLine || sym[4], + endLine: sym.endLine || sym[5], + score: bm25Result.score, + }; + + // Add relationships if depth is 'full' + if (depth === 'full') { + const relQuery = ` + MATCH (n {id: '${(sym.id || sym[0]).replace(/'/g, "''")}' })-[r:CodeRelation]->(m) + RETURN r.type AS type, m.name AS targetName, m.filePath AS targetPath + LIMIT 5 + `; + try { + const rels = await executeQuery(relQuery); + result.connections = rels.map((rel: any) => ({ + type: rel.type || rel[0], + name: rel.targetName || rel[1], + path: rel.targetPath || rel[2], + })); + } catch { + result.connections = []; + } + } + + results.push(result); + } + } else { + // No symbols found in kuzu, return file info from BM25 + results.push({ + name: fileName, + type: 'File', + filePath: bm25Result.filePath, + score: bm25Result.score, + }); + } + } catch { + // On kuzu error, still return BM25 result + results.push({ + name: bm25Result.filePath.split('/').pop(), + type: 'File', + filePath: bm25Result.filePath, + score: bm25Result.score, + }); + } + } + + return results.slice(0, limit); + } + + private async cypher(params: { query: string }): Promise { + await this.ensureInitialized(); + + if (!isKuzuReady()) { + return { error: 'KuzuDB not ready. Index may be corrupted.' }; + } + + try { + const result = await executeQuery(params.query); + return result; + } catch (err: any) { + return { error: err.message || 'Query failed' }; + } + } + + private async overview(params: { showClusters?: boolean; showProcesses?: boolean; limit?: number }): Promise { + await this.ensureInitialized(); + + const limit = params.limit || 20; + const result: any = { + repoPath: this.repo!.repoPath, + stats: this.repo!.meta.stats, + indexedAt: this.repo!.meta.indexedAt, + lastCommit: this.repo!.meta.lastCommit, + }; + + if (params.showClusters !== false) { + try { + const clusters = await executeQuery(` + MATCH (c:Community) + RETURN c.id AS id, c.label AS label, c.heuristicLabel AS heuristicLabel, c.cohesion AS cohesion, c.symbolCount AS symbolCount + ORDER BY c.symbolCount DESC + LIMIT ${limit} + `); + result.clusters = clusters.map((c: any) => ({ + id: c.id || c[0], + label: c.label || c[1], + heuristicLabel: c.heuristicLabel || c[2], + cohesion: c.cohesion || c[3], + symbolCount: c.symbolCount || c[4], + })); + } catch { + result.clusters = []; + } + } + + if (params.showProcesses !== false) { + try { + const processes = await executeQuery(` + MATCH (p:Process) + RETURN p.id AS id, p.label AS label, p.heuristicLabel AS heuristicLabel, p.processType AS processType, p.stepCount AS stepCount + ORDER BY p.stepCount DESC + LIMIT ${limit} + `); + result.processes = processes.map((p: any) => ({ + id: p.id || p[0], + label: p.label || p[1], + heuristicLabel: p.heuristicLabel || p[2], + processType: p.processType || p[3], + stepCount: p.stepCount || p[4], + })); + } catch { + result.processes = []; + } + } + + return result; + } + + private async explore(params: { name: string; type: 'symbol' | 'cluster' | 'process' }): Promise { + await this.ensureInitialized(); + + const { name, type } = params; + + if (type === 'symbol') { + // Find symbol and its context + const symbolQuery = ` + MATCH (n) + WHERE n.name = '${name.replace(/'/g, "''")}' + RETURN n.id AS id, n.name AS name, labels(n)[0] AS type, n.filePath AS filePath, n.startLine AS startLine, n.endLine AS endLine + LIMIT 1 + `; + const symbols = await executeQuery(symbolQuery); + if (symbols.length === 0) return { error: `Symbol '${name}' not found` }; + + const sym = symbols[0]; + const symId = sym.id || sym[0]; + + // Get callers + const callersQuery = ` + MATCH (caller)-[:CodeRelation {type: 'CALLS'}]->(n {id: '${symId}'}) + RETURN caller.name AS name, caller.filePath AS filePath + LIMIT 10 + `; + const callers = await executeQuery(callersQuery); + + // Get callees + const calleesQuery = ` + MATCH (n {id: '${symId}'})-[:CodeRelation {type: 'CALLS'}]->(callee) + RETURN callee.name AS name, callee.filePath AS filePath + LIMIT 10 + `; + const callees = await executeQuery(calleesQuery); + + // Get community + const communityQuery = ` + MATCH (n {id: '${symId}'})-[:CodeRelation {type: 'MEMBER_OF'}]->(c:Community) + RETURN c.label AS label, c.heuristicLabel AS heuristicLabel + LIMIT 1 + `; + const communities = await executeQuery(communityQuery); + + return { + symbol: { + id: symId, + name: sym.name || sym[1], + type: sym.type || sym[2], + filePath: sym.filePath || sym[3], + startLine: sym.startLine || sym[4], + endLine: sym.endLine || sym[5], + }, + callers: callers.map((c: any) => ({ name: c.name || c[0], filePath: c.filePath || c[1] })), + callees: callees.map((c: any) => ({ name: c.name || c[0], filePath: c.filePath || c[1] })), + community: communities.length > 0 ? { + label: communities[0].label || communities[0][0], + heuristicLabel: communities[0].heuristicLabel || communities[0][1], + } : null, + }; + } + + if (type === 'cluster') { + const clusterQuery = ` + MATCH (c:Community) + WHERE c.label = '${name.replace(/'/g, "''")}' OR c.heuristicLabel = '${name.replace(/'/g, "''")}' + RETURN c.id AS id, c.label AS label, c.heuristicLabel AS heuristicLabel, c.cohesion AS cohesion, c.symbolCount AS symbolCount + LIMIT 1 + `; + const clusters = await executeQuery(clusterQuery); + if (clusters.length === 0) return { error: `Cluster '${name}' not found` }; + + const cluster = clusters[0]; + const clusterId = cluster.id || cluster[0]; + + const membersQuery = ` + MATCH (n)-[:CodeRelation {type: 'MEMBER_OF'}]->(c {id: '${clusterId}'}) + RETURN n.name AS name, labels(n)[0] AS type, n.filePath AS filePath + LIMIT 20 + `; + const members = await executeQuery(membersQuery); + + return { + cluster: { + id: clusterId, + label: cluster.label || cluster[1], + heuristicLabel: cluster.heuristicLabel || cluster[2], + cohesion: cluster.cohesion || cluster[3], + symbolCount: cluster.symbolCount || cluster[4], + }, + members: members.map((m: any) => ({ + name: m.name || m[0], + type: m.type || m[1], + filePath: m.filePath || m[2], + })), + }; + } + + if (type === 'process') { + const processQuery = ` + MATCH (p:Process) + WHERE p.label = '${name.replace(/'/g, "''")}' OR p.heuristicLabel = '${name.replace(/'/g, "''")}' + RETURN p.id AS id, p.label AS label, p.heuristicLabel AS heuristicLabel, p.processType AS processType, p.stepCount AS stepCount, p.entryPointId AS entryPointId, p.terminalId AS terminalId + LIMIT 1 + `; + const processes = await executeQuery(processQuery); + if (processes.length === 0) return { error: `Process '${name}' not found` }; + + const proc = processes[0]; + const procId = proc.id || proc[0]; + + const stepsQuery = ` + MATCH (n)-[r:CodeRelation {type: 'STEP_IN_PROCESS'}]->(p {id: '${procId}'}) + RETURN n.name AS name, labels(n)[0] AS type, n.filePath AS filePath, r.step AS step + ORDER BY r.step + `; + const steps = await executeQuery(stepsQuery); + + return { + process: { + id: procId, + label: proc.label || proc[1], + heuristicLabel: proc.heuristicLabel || proc[2], + processType: proc.processType || proc[3], + stepCount: proc.stepCount || proc[4], + }, + steps: steps.map((s: any) => ({ + step: s.step || s[3], + name: s.name || s[0], + type: s.type || s[1], + filePath: s.filePath || s[2], + })), + }; + } + + return { error: 'Invalid type. Use: symbol, cluster, or process' }; + } + + private async impact(params: { target: string; direction: 'upstream' | 'downstream'; maxDepth?: number }): Promise { + await this.ensureInitialized(); + + const { target, direction } = params; + const maxDepth = params.maxDepth || 3; + + // Find target symbol + const targetQuery = ` + MATCH (n) + WHERE n.name = '${target.replace(/'/g, "''")}' + RETURN n.id AS id, n.name AS name, labels(n)[0] AS type, n.filePath AS filePath + LIMIT 1 + `; + const targets = await executeQuery(targetQuery); + if (targets.length === 0) return { error: `Target '${target}' not found` }; + + const sym = targets[0]; + const symId = sym.id || sym[0]; + + // BFS to find impacted nodes + const impacted: any[] = []; + const visited = new Set([symId]); + let frontier = [symId]; + + for (let depth = 1; depth <= maxDepth && frontier.length > 0; depth++) { + const nextFrontier: string[] = []; + + for (const nodeId of frontier) { + const query = direction === 'upstream' + ? `MATCH (caller)-[r:CodeRelation]->(n {id: '${nodeId}'}) WHERE r.type IN ['CALLS', 'IMPORTS', 'EXTENDS', 'IMPLEMENTS'] RETURN caller.id AS id, caller.name AS name, labels(caller)[0] AS type, caller.filePath AS filePath, r.type AS relType, r.confidence AS confidence` + : `MATCH (n {id: '${nodeId}'})-[r:CodeRelation]->(callee) WHERE r.type IN ['CALLS', 'IMPORTS', 'EXTENDS', 'IMPLEMENTS'] RETURN callee.id AS id, callee.name AS name, labels(callee)[0] AS type, callee.filePath AS filePath, r.type AS relType, r.confidence AS confidence`; + + const related = await executeQuery(query); + + for (const rel of related) { + const relId = rel.id || rel[0]; + if (!visited.has(relId)) { + visited.add(relId); + nextFrontier.push(relId); + impacted.push({ + depth, + id: relId, + name: rel.name || rel[1], + type: rel.type || rel[2], + filePath: rel.filePath || rel[3], + relationType: rel.relType || rel[4], + confidence: rel.confidence || rel[5] || 1.0, + }); + } + } + } + + frontier = nextFrontier; + } + + // Group by depth + const grouped: Record = {}; + for (const item of impacted) { + if (!grouped[item.depth]) grouped[item.depth] = []; + grouped[item.depth].push(item); + } + + return { + target: { + id: symId, + name: sym.name || sym[1], + type: sym.type || sym[2], + filePath: sym.filePath || sym[3], + }, + direction, + impactedCount: impacted.length, + byDepth: grouped, + }; + } + + private async analyze(params: { path?: string; force?: boolean }): Promise { + const targetPath = params.path ? path.resolve(params.path) : process.cwd(); + + return { + action: 'analyze', + targetPath, + message: `To index this repository, run:\n\n cd ${targetPath}\n gitnexus analyze${params.force ? ' --force' : ''}\n\nThis will create a .gitnexus/ folder with the knowledge graph.`, + }; + } + + disconnect(): void { + closeKuzu(); + this.repo = null; + this._context = null; + this.initialized = false; + } +} diff --git a/gitnexus-mcp/src/mcp/tools.ts b/gitnexus-mcp/src/mcp/tools.ts index 84ab18d72..f4d6e997c 100644 --- a/gitnexus-mcp/src/mcp/tools.ts +++ b/gitnexus-mcp/src/mcp/tools.ts @@ -2,7 +2,7 @@ * MCP Tool Definitions * * Defines the tools that GitNexus exposes to external AI agents. - * Each tool has a rich description with examples to help agents use them correctly. + * Only includes tools that provide unique value over native IDE capabilities. */ export interface ToolDefinition { @@ -15,12 +15,37 @@ export interface ToolDefinition { description?: string; default?: any; items?: { type: string }; + enum?: string[]; }>; required: string[]; }; } export const GITNEXUS_TOOLS: ToolDefinition[] = [ + { + name: 'analyze', + description: `Index or re-index the current repository. + +Creates .gitnexus/ in repo root with: +- Knowledge graph (functions, classes, calls, imports) +- BM25 search index +- Community detection (Leiden) +- Process tracing + +Run this when: +- First time using GitNexus on a repo +- After major code changes +- When 'not indexed' error appears`, + inputSchema: { + type: 'object', + properties: { + path: { type: 'string', description: 'Repo path (default: current directory)' }, + force: { type: 'boolean', description: 'Re-index even if exists', default: false }, + skipEmbeddings: { type: 'boolean', description: 'Skip embedding generation (faster)', default: false }, + }, + required: [], + }, + }, { name: 'context', description: `Get GitNexus codebase context. CALL THIS FIRST before using other tools. @@ -28,7 +53,7 @@ export const GITNEXUS_TOOLS: ToolDefinition[] = [ Returns: - Project name and stats (files, functions, classes) - Hotspots (most connected/important nodes) -- Directory structure (TOON format for token efficiency) +- Communities and processes count - Tool usage guidance ALWAYS call this first to understand the codebase before searching or querying.`, @@ -43,10 +68,10 @@ ALWAYS call this first to understand the codebase before searching or querying.` description: `Hybrid search (keyword + semantic) across the codebase. Returns code nodes with their graph connections, grouped by process. -WHEN TO USE: -- Finding implementations ("where is auth handled?") -- Understanding code flow ("what calls UserService?") -- Locating patterns ("find all API endpoints") +BETTER THAN IDE search because: +- Process-aware grouping (shows execution flows) +- Cluster context (which functional area) +- Relationship data (callers/callees) RETURNS: Array of {name, type, filePath, code, connections[], cluster, processes[]}`, inputSchema: { @@ -54,6 +79,7 @@ RETURNS: Array of {name, type, filePath, code, connections[], cluster, processes properties: { query: { type: 'string', description: 'Natural language or keyword search query' }, limit: { type: 'number', description: 'Max results to return', default: 10 }, + depth: { type: 'string', description: 'Result detail: "definitions" (symbols only) or "full" (with relationships)', enum: ['definitions', 'full'], default: 'definitions' }, groupByProcess: { type: 'boolean', description: 'Group results by process', default: true }, }, required: ['query'], @@ -89,50 +115,6 @@ TIPS: required: ['query'], }, }, - { - name: 'grep', - description: `Regex search for exact patterns in file contents. - -WHEN TO USE: -- Finding exact strings: error codes, TODOs, specific API keys -- Pattern matching: all console.log, all fetch calls -- Finding imports of specific modules - -BETTER THAN search for: exact matches, regex patterns, case-sensitive - -RETURNS: Array of {filePath, line, lineNumber, match}`, - inputSchema: { - type: 'object', - properties: { - pattern: { type: 'string', description: 'Regex pattern to search for' }, - caseSensitive: { type: 'boolean', description: 'Case-sensitive search', default: false }, - maxResults: { type: 'number', description: 'Max results to return', default: 50 }, - }, - required: ['pattern'], - }, - }, - { - name: 'read', - description: `Read file content from the codebase. - -WHEN TO USE: -- After search/grep to see full context -- To understand implementation details -- Before making changes - -ALWAYS read before concluding - don't guess from names alone. - -RETURNS: {filePath, content, language, lines}`, - inputSchema: { - type: 'object', - properties: { - filePath: { type: 'string', description: 'Path to file to read' }, - startLine: { type: 'number', description: 'Start line (optional)' }, - endLine: { type: 'number', description: 'End line (optional)' }, - }, - required: ['filePath'], - }, - }, { name: 'explore', description: `Deep dive on a symbol, cluster, or process. @@ -206,20 +188,4 @@ Depth groups: required: ['target', 'direction'], }, }, - { - name: 'highlight', - description: `Highlight nodes in the GitNexus graph visualization. -Use after search/analysis to show the user what you found. - -The user will see the nodes glow in the graph view. -Great for visual confirmation of your findings.`, - inputSchema: { - type: 'object', - properties: { - nodeIds: { type: 'array', items: { type: 'string' }, description: 'Array of node IDs to highlight' }, - color: { type: 'string', description: 'Highlight color (optional, default: cyan)' }, - }, - required: ['nodeIds'], - }, - }, ]; diff --git a/gitnexus/src/App.tsx b/gitnexus/src/App.tsx index 646b6c97f..1288e502c 100644 --- a/gitnexus/src/App.tsx +++ b/gitnexus/src/App.tsx @@ -26,7 +26,6 @@ const AppContent = () => { isRightPanelOpen, runPipeline, runPipelineFromFiles, - loadSerializedGraph, isSettingsPanelOpen, setSettingsPanelOpen, refreshLLMSettings, @@ -43,8 +42,6 @@ const AppContent = () => { } = useAppState(); const [showClusteringModal, setShowClusteringModal] = useState(false); - const [localRepos, setLocalRepos] = useState>([]); - const [localAvailable, setLocalAvailable] = useState(false); // Trigger clustering modal after ingestion if not seen yet // DISABLED: Clustering is now in the upload flow @@ -191,64 +188,6 @@ const AppContent = () => { } }, [setViewMode, setGraph, setFileContents, setProgress, setProjectName, runPipelineFromFiles, startEmbeddings, initializeAgent, runClusterEnrichment]); - useEffect(() => { - fetch('http://localhost:4747/api/repos') - .then((res) => res.json()) - .then((data) => { - if (data?.repos?.length) { - setLocalAvailable(true); - setLocalRepos(data.repos); - } - }) - .catch(() => { - setLocalAvailable(false); - }); - }, []); - - const handleOpenLocalRepo = useCallback(async (repoId: string, repoPath: string) => { - const project = repoPath.split('/').pop() || repoPath.split('\\').pop() || 'repository'; - setProjectName(project); - setProgress({ phase: 'extracting', percent: 0, message: 'Loading local repository...' }); - setViewMode('loading'); - - try { - const res = await fetch(`http://localhost:4747/api/repos/${repoId}/serialized`); - if (!res.ok) { - throw new Error(`Failed to fetch local repo: ${res.status}`); - } - const serialized = await res.json(); - const result = await loadSerializedGraph(serialized); - - setGraph(result.graph); - setFileContents(result.fileContents); - setViewMode('exploring'); - - if (getActiveProviderConfig()) { - initializeAgent(project); - } - - startEmbeddings().catch((err) => { - if (err?.name === 'WebGPUNotAvailableError' || err?.message?.includes('WebGPU')) { - startEmbeddings('wasm').catch(console.warn); - } else { - console.warn('Embeddings auto-start failed:', err); - } - }); - } catch (error) { - console.error('Local repo load error:', error); - setProgress({ - phase: 'error', - percent: 0, - message: 'Error loading local repository', - detail: error instanceof Error ? error.message : 'Unknown error', - }); - setTimeout(() => { - setViewMode('onboarding'); - setProgress(null); - }, 3000); - } - }, [setProjectName, setProgress, setViewMode, loadSerializedGraph, setGraph, setFileContents, initializeAgent, startEmbeddings]); - const handleFocusNode = useCallback((nodeId: string) => { graphCanvasRef.current?.focusNode(nodeId); }, []); @@ -262,34 +201,7 @@ const AppContent = () => { // Render based on view mode if (viewMode === 'onboarding') { - return ( -
- {localAvailable && localRepos.length > 0 && ( -
-
Local GitNexus server detected
-
- {localRepos.map((repo) => ( -
-
-
{repo.repoPath}
-
Indexed: {repo.indexedAt}
-
- -
- ))} -
-
- )} -
- -
-
- ); + return ; } if (viewMode === 'loading' && progress) { diff --git a/gitnexus/src/hooks/useAppState.tsx b/gitnexus/src/hooks/useAppState.tsx index 278140cc8..720b92314 100644 --- a/gitnexus/src/hooks/useAppState.tsx +++ b/gitnexus/src/hooks/useAppState.tsx @@ -1,7 +1,7 @@ import { createContext, useContext, useState, useCallback, useRef, useEffect, ReactNode } from 'react'; import * as Comlink from 'comlink'; import { KnowledgeGraph, GraphNode, NodeLabel } from '../core/graph/types'; -import { PipelineProgress, PipelineResult, SerializablePipelineResult, deserializePipelineResult } from '../types/pipeline'; +import { PipelineProgress, PipelineResult, deserializePipelineResult } from '../types/pipeline'; import { createKnowledgeGraph } from '../core/graph/graph'; import { DEFAULT_VISIBLE_LABELS } from '../lib/constants'; import type { IngestionWorkerApi } from '../workers/ingestion.worker'; @@ -114,7 +114,6 @@ interface AppState { // Worker API (shared across app) runPipeline: (file: File, onProgress: (p: PipelineProgress) => void, clusteringConfig?: ProviderConfig) => Promise; runPipelineFromFiles: (files: FileEntry[], onProgress: (p: PipelineProgress) => void, clusteringConfig?: ProviderConfig) => Promise; - loadSerializedGraph: (serialized: SerializablePipelineResult) => Promise; runQuery: (cypher: string) => Promise; isDatabaseReady: () => Promise; @@ -461,15 +460,6 @@ export const AppStateProvider = ({ children }: { children: ReactNode }) => { return deserializePipelineResult(serializedResult, createKnowledgeGraph); }, []); - const loadSerializedGraph = useCallback(async ( - serialized: SerializablePipelineResult - ): Promise => { - const api = apiRef.current; - if (!api) throw new Error('Worker not initialized'); - await api.loadSerializedGraph(serialized); - return deserializePipelineResult(serialized, createKnowledgeGraph); - }, []); - const runQuery = useCallback(async (cypher: string): Promise => { const api = apiRef.current; if (!api) throw new Error('Worker not initialized'); @@ -1206,7 +1196,6 @@ export const AppStateProvider = ({ children }: { children: ReactNode }) => { setProjectName, runPipeline, runPipelineFromFiles, - loadSerializedGraph, runQuery, isDatabaseReady, // Embedding state and methods diff --git a/gitnexus/src/workers/ingestion.worker.ts b/gitnexus/src/workers/ingestion.worker.ts index 863e8a699..c5fb407f2 100644 --- a/gitnexus/src/workers/ingestion.worker.ts +++ b/gitnexus/src/workers/ingestion.worker.ts @@ -1,6 +1,6 @@ import * as Comlink from 'comlink'; import { runIngestionPipeline, runPipelineFromFiles } from '../core/ingestion/pipeline'; -import { PipelineProgress, SerializablePipelineResult, serializePipelineResult, deserializePipelineResult } from '../types/pipeline'; +import { PipelineProgress, SerializablePipelineResult, serializePipelineResult } from '../types/pipeline'; import { FileEntry } from '../services/zip'; import { runEmbeddingPipeline, @@ -25,7 +25,6 @@ import { mergeWithRRF, type HybridSearchResult, } from '../core/search'; -import { createKnowledgeGraph } from '../core/graph/graph'; // Lazy import for Kuzu to avoid breaking worker if SharedArrayBuffer unavailable let kuzuAdapter: typeof import('../core/kuzu/kuzu-adapter') | null = null; @@ -224,31 +223,6 @@ const workerApi = { return serializePipelineResult(result); }, - /** - * Load a serialized graph result into the worker (for local CLI integration) - */ - async loadSerializedGraph(serialized: SerializablePipelineResult): Promise { - const result = deserializePipelineResult(serialized, createKnowledgeGraph); - currentGraphResult = result; - storedFileContents = result.fileContents; - - const bm25DocCount = buildBM25Index(storedFileContents); - if (import.meta.env.DEV) { - console.log(`šŸ” BM25 index built: ${bm25DocCount} documents`); - } - - try { - const kuzu = await getKuzuAdapter(); - await kuzu.loadGraphToKuzu(result.graph, result.fileContents); - if (import.meta.env.DEV) { - const stats = await kuzu.getKuzuStats(); - console.log('KuzuDB loaded from serialized graph:', stats); - } - } catch { - // KuzuDB is optional - } - }, - // ============================================================ // Embedding Pipeline Methods // ============================================================