logging global registry

This commit is contained in:
abhigyanpatwari 2026-02-13 22:39:12 +05:30
parent 49925a6cfe
commit 18a5263396
12 changed files with 240 additions and 275 deletions

View file

@ -3,9 +3,9 @@
<!-- gitnexus:start -->
# GitNexus MCP
This project is indexed by GitNexus as **GitnexusV2** (940 symbols, 2298 relationships, 13 clusters, 67 processes).
This project is indexed by GitNexus as **GitnexusV2** (1023 symbols, 2503 relationships, 78 execution flows).
GitNexus provides a knowledge graph over this codebase — clusters, call chains, blast radius, execution flows, and semantic search.
GitNexus provides a knowledge graph over this codebase — call chains, blast radius, execution flows, and semantic search.
## Always Start Here
@ -30,10 +30,11 @@ For any task involving code understanding, debugging, impact analysis, or refact
| Tool | What it gives you |
|------|-------------------|
| `search` | Semantic + keyword code search with cluster context |
| `explore` | Symbol deep dive — callers, callees, cluster membership, processes |
| `impact` | Blast radius — what breaks at depth 1/2/3 with confidence scores |
| `overview` | All clusters and processes at a glance |
| `query` | Process-grouped code intelligence — execution flows related to a concept |
| `context` | 360-degree symbol view — categorized refs, processes it participates in |
| `impact` | Symbol blast radius — what breaks at depth 1/2/3 with confidence |
| `detect_changes` | Git-diff impact — what do your current changes affect |
| `rename` | Multi-file coordinated rename with confidence-tagged edits |
| `cypher` | Raw graph queries (read `gitnexus://repo/{name}/schema` first) |
| `list_repos` | Discover indexed repos |
@ -44,8 +45,8 @@ Lightweight reads (~100-500 tokens) for navigation:
| Resource | Content |
|----------|---------|
| `gitnexus://repo/{name}/context` | Stats, staleness check |
| `gitnexus://repo/{name}/clusters` | All clusters with cohesion scores |
| `gitnexus://repo/{name}/cluster/{clusterName}` | Cluster members |
| `gitnexus://repo/{name}/clusters` | All functional areas with cohesion scores |
| `gitnexus://repo/{name}/cluster/{clusterName}` | Area members |
| `gitnexus://repo/{name}/processes` | All execution flows |
| `gitnexus://repo/{name}/process/{processName}` | Step-by-step trace |
| `gitnexus://repo/{name}/schema` | Graph schema for Cypher |

62
CLAUDE.md Normal file
View file

@ -0,0 +1,62 @@
<!-- gitnexus:start -->
# GitNexus MCP
This project is indexed by GitNexus as **GitnexusV2** (1023 symbols, 2503 relationships, 78 execution flows).
GitNexus provides a knowledge graph over this codebase — call chains, blast radius, execution flows, and semantic search.
## Always Start Here
For any task involving code understanding, debugging, impact analysis, or refactoring, you must:
1. **Read `gitnexus://repo/{name}/context`** — codebase overview + check index freshness
2. **Match your task to a skill below** and **read that skill file**
3. **Follow the skill's workflow and checklist**
> If step 1 warns the index is stale, run `npx gitnexus analyze` in the terminal first.
## Skills
| Task | Read this skill file |
|------|---------------------|
| Understand architecture / "How does X work?" | `.claude/skills/gitnexus/exploring/SKILL.md` |
| Blast radius / "What breaks if I change X?" | `.claude/skills/gitnexus/impact-analysis/SKILL.md` |
| Trace bugs / "Why is X failing?" | `.claude/skills/gitnexus/debugging/SKILL.md` |
| Rename / extract / split / refactor | `.claude/skills/gitnexus/refactoring/SKILL.md` |
## Tools Reference
| Tool | What it gives you |
|------|-------------------|
| `query` | Process-grouped code intelligence — execution flows related to a concept |
| `context` | 360-degree symbol view — categorized refs, processes it participates in |
| `impact` | Symbol blast radius — what breaks at depth 1/2/3 with confidence |
| `detect_changes` | Git-diff impact — what do your current changes affect |
| `rename` | Multi-file coordinated rename with confidence-tagged edits |
| `cypher` | Raw graph queries (read `gitnexus://repo/{name}/schema` first) |
| `list_repos` | Discover indexed repos |
## Resources Reference
Lightweight reads (~100-500 tokens) for navigation:
| Resource | Content |
|----------|---------|
| `gitnexus://repo/{name}/context` | Stats, staleness check |
| `gitnexus://repo/{name}/clusters` | All functional areas with cohesion scores |
| `gitnexus://repo/{name}/cluster/{clusterName}` | Area members |
| `gitnexus://repo/{name}/processes` | All execution flows |
| `gitnexus://repo/{name}/process/{processName}` | Step-by-step trace |
| `gitnexus://repo/{name}/schema` | Graph schema for Cypher |
## Graph Schema
**Nodes:** File, Function, Class, Interface, Method, Community, Process
**Edges (via CodeRelation.type):** CALLS, IMPORTS, EXTENDS, IMPLEMENTS, DEFINES, MEMBER_OF, STEP_IN_PROCESS
```cypher
MATCH (caller)-[:CodeRelation {type: 'CALLS'}]->(f:Function {name: "myFunc"})
RETURN caller.name, caller.filePath
```
<!-- gitnexus:end -->

View file

@ -1,16 +1,17 @@
{
"name": "gitnexus",
"version": "0.1.0",
"version": "1.1.8",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "gitnexus",
"version": "0.1.0",
"version": "1.1.8",
"license": "PolyForm-Noncommercial-1.0.0",
"dependencies": {
"@huggingface/transformers": "^3.0.0",
"@modelcontextprotocol/sdk": "^1.0.0",
"cli-progress": "^3.12.0",
"commander": "^12.0.0",
"cors": "^2.8.5",
"express": "^4.19.2",
@ -21,7 +22,6 @@
"kuzu": "^0.11.3",
"lru-cache": "^11.0.0",
"mnemonist": "^0.39.0",
"ora": "^8.0.0",
"pandemonium": "^2.4.0",
"tree-sitter": "^0.21.0",
"tree-sitter-c": "^0.21.0",
@ -40,6 +40,7 @@
"gitnexus": "dist/cli/index.js"
},
"devDependencies": {
"@types/cli-progress": "^3.11.6",
"@types/cors": "^2.8.17",
"@types/express": "^4.17.21",
"@types/node": "^20.0.0",
@ -1449,6 +1450,16 @@
"@types/node": "*"
}
},
"node_modules/@types/cli-progress": {
"version": "3.11.6",
"resolved": "https://registry.npmjs.org/@types/cli-progress/-/cli-progress-3.11.6.tgz",
"integrity": "sha512-cE3+jb9WRlu+uOSAugewNpITJDt1VF8dHOopPO4IABFc3SXYL5WE/+PTz/FCdZRRfIujiWW3n3aMbv1eIGVRWA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/node": "*"
}
},
"node_modules/@types/connect": {
"version": "3.4.38",
"resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz",
@ -1769,18 +1780,6 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/chalk": {
"version": "5.6.2",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz",
"integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==",
"license": "MIT",
"engines": {
"node": "^12.17.0 || ^14.13 || >=16.0.0"
},
"funding": {
"url": "https://github.com/chalk/chalk?sponsor=1"
}
},
"node_modules/chownr": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz",
@ -1790,31 +1789,57 @@
"node": ">=10"
}
},
"node_modules/cli-cursor": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-5.0.0.tgz",
"integrity": "sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==",
"node_modules/cli-progress": {
"version": "3.12.0",
"resolved": "https://registry.npmjs.org/cli-progress/-/cli-progress-3.12.0.tgz",
"integrity": "sha512-tRkV3HJ1ASwm19THiiLIXLO7Im7wlTuKnvkYaTkyoAPefqjNg7W7DHKUlGRxy9vxDvbyCYQkQozvptuMkGCg8A==",
"license": "MIT",
"dependencies": {
"restore-cursor": "^5.0.0"
"string-width": "^4.2.3"
},
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
"node": ">=4"
}
},
"node_modules/cli-spinners": {
"version": "2.9.2",
"resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.9.2.tgz",
"integrity": "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==",
"node_modules/cli-progress/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": ">=6"
"node": ">=8"
}
},
"node_modules/cli-progress/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/cli-progress/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"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
"engines": {
"node": ">=8"
}
},
"node_modules/cli-progress/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/cliui": {
@ -2690,18 +2715,6 @@
"node": "6.* || 8.* || >= 10.*"
}
},
"node_modules/get-east-asian-width": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.4.0.tgz",
"integrity": "sha512-QZjmEOC+IT1uk6Rx0sX22V6uHWVwbdbxf1faPqJ1QhLdGgsRGCZoyaQBm/piRdJy/D2um6hM1UP7ZEeQ4EkP+Q==",
"license": "MIT",
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/get-intrinsic": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
@ -3003,36 +3016,12 @@
"node": ">=8"
}
},
"node_modules/is-interactive": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-2.0.0.tgz",
"integrity": "sha512-qP1vozQRI+BMOPcjFzrjXuQvdak2pHNUMZoeG2eRbiSqyvbEf/wQtEOTOX1guk6E3t36RkaqiSt8A/6YElNxLQ==",
"license": "MIT",
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/is-promise": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz",
"integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==",
"license": "MIT"
},
"node_modules/is-unicode-supported": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-2.1.0.tgz",
"integrity": "sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==",
"license": "MIT",
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/isexe": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
@ -3105,34 +3094,6 @@
"node-addon-api": "^6.0.0"
}
},
"node_modules/log-symbols": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-6.0.0.tgz",
"integrity": "sha512-i24m8rpwhmPIS4zscNzK6MSEhk0DUWa/8iYQWxhffV8jkI4Phvs3F+quL5xvS0gdQR0FyTCMMH33Y78dDTzzIw==",
"license": "MIT",
"dependencies": {
"chalk": "^5.3.0",
"is-unicode-supported": "^1.3.0"
},
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/log-symbols/node_modules/is-unicode-supported": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-1.3.0.tgz",
"integrity": "sha512-43r2mRvz+8JRIKnWJ+3j8JtjRKZ6GmjzfaE/qiBJnikNnYv/6bagRJ1kUhNk8R5EX/GkobD+r+sfxCPJsiKBLQ==",
"license": "MIT",
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/long": {
"version": "5.3.2",
"resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz",
@ -3238,18 +3199,6 @@
"node": ">= 0.6"
}
},
"node_modules/mimic-function": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/mimic-function/-/mimic-function-5.0.1.tgz",
"integrity": "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==",
"license": "MIT",
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/minimatch": {
"version": "10.1.1",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.1.1.tgz",
@ -3440,21 +3389,6 @@
"wrappy": "1"
}
},
"node_modules/onetime": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/onetime/-/onetime-7.0.0.tgz",
"integrity": "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==",
"license": "MIT",
"dependencies": {
"mimic-function": "^5.0.0"
},
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/onnxruntime-common": {
"version": "1.21.0",
"resolved": "https://registry.npmjs.org/onnxruntime-common/-/onnxruntime-common-1.21.0.tgz",
@ -3544,52 +3478,6 @@
"integrity": "sha512-vDJMkfCfb0b1A836rgHj+ORuZf4B4+cc2bASQtpeoJLueuFc5DuYwjIZUBrSvx/fO5IrLjLz+oTrB3pcGlhovQ==",
"license": "MIT"
},
"node_modules/ora": {
"version": "8.2.0",
"resolved": "https://registry.npmjs.org/ora/-/ora-8.2.0.tgz",
"integrity": "sha512-weP+BZ8MVNnlCm8c0Qdc1WSWq4Qn7I+9CJGm7Qali6g44e/PUzbjNqJX5NJ9ljlNMosfJvg1fKEGILklK9cwnw==",
"license": "MIT",
"dependencies": {
"chalk": "^5.3.0",
"cli-cursor": "^5.0.0",
"cli-spinners": "^2.9.2",
"is-interactive": "^2.0.0",
"is-unicode-supported": "^2.0.0",
"log-symbols": "^6.0.0",
"stdin-discarder": "^0.2.2",
"string-width": "^7.2.0",
"strip-ansi": "^7.1.0"
},
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/ora/node_modules/emoji-regex": {
"version": "10.6.0",
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz",
"integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==",
"license": "MIT"
},
"node_modules/ora/node_modules/string-width": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz",
"integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==",
"license": "MIT",
"dependencies": {
"emoji-regex": "^10.3.0",
"get-east-asian-width": "^1.0.0",
"strip-ansi": "^7.1.0"
},
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/package-json-from-dist": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz",
@ -3815,22 +3703,6 @@
"url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1"
}
},
"node_modules/restore-cursor": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-5.1.0.tgz",
"integrity": "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==",
"license": "MIT",
"dependencies": {
"onetime": "^7.0.0",
"signal-exit": "^4.1.0"
},
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/roarr": {
"version": "2.15.4",
"resolved": "https://registry.npmjs.org/roarr/-/roarr-2.15.4.tgz",
@ -4177,18 +4049,6 @@
"node": ">= 0.8"
}
},
"node_modules/stdin-discarder": {
"version": "0.2.2",
"resolved": "https://registry.npmjs.org/stdin-discarder/-/stdin-discarder-0.2.2.tgz",
"integrity": "sha512-UhDfHmA92YAlNnCfhmq0VeNL5bDbiZGg7sZ2IvPsXubGkiNa9EC+tUTsjBRsYUAz87btI6/1wf4XoVvQ3uRnmQ==",
"license": "MIT",
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/string_decoder": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",

View file

@ -43,6 +43,7 @@
"dependencies": {
"@huggingface/transformers": "^3.0.0",
"@modelcontextprotocol/sdk": "^1.0.0",
"cli-progress": "^3.12.0",
"commander": "^12.0.0",
"cors": "^2.8.5",
"express": "^4.19.2",
@ -53,7 +54,6 @@
"kuzu": "^0.11.3",
"lru-cache": "^11.0.0",
"mnemonist": "^0.39.0",
"ora": "^8.0.0",
"pandemonium": "^2.4.0",
"tree-sitter": "^0.21.0",
"tree-sitter-c": "^0.21.0",
@ -69,6 +69,7 @@
"uuid": "^13.0.0"
},
"devDependencies": {
"@types/cli-progress": "^3.11.6",
"@types/cors": "^2.8.17",
"@types/express": "^4.17.21",
"@types/node": "^20.0.0",

View file

@ -5,11 +5,12 @@
*/
import path from 'path';
import ora from 'ora';
import cliProgress from 'cli-progress';
import { runPipelineFromRepo } from '../core/ingestion/pipeline.js';
import { initKuzu, loadGraphToKuzu, getKuzuStats, executeQuery, executeWithReusedStatement, closeKuzu, createFTSIndex } from '../core/kuzu/kuzu-adapter.js';
import { runEmbeddingPipeline } from '../core/embeddings/embedding-pipeline.js';
import { getStoragePaths, saveMeta, loadMeta, addToGitignore, registerRepo, getGlobalRegistryPath } from '../storage/repo-manager.js';
import { disposeEmbedder } from '../core/embeddings/embedder.js';
import { getStoragePaths, saveMeta, loadMeta, addToGitignore, registerRepo, getGlobalRegistryPath, getGlobalDir } from '../storage/repo-manager.js';
import { getCurrentCommit, isGitRepo, getGitRoot } from '../storage/git.js';
import { generateAIContextFiles } from './ai-context.js';
import fs from 'fs/promises';
@ -19,20 +20,31 @@ export interface AnalyzeOptions {
skipEmbeddings?: boolean;
}
const PHASE_LABELS: Record<string, string> = {
extracting: 'Scanning files',
structure: 'Building structure',
parsing: 'Parsing code',
imports: 'Resolving imports',
calls: 'Tracing calls',
heritage: 'Extracting inheritance',
communities: 'Detecting communities',
processes: 'Detecting processes',
complete: 'Complete',
};
export const analyzeCommand = async (
inputPath?: string,
options?: AnalyzeOptions
) => {
const spinner = ora('Checking repository...').start();
console.log('\n GitNexus Analyzer\n');
// If path provided, use it directly. Otherwise, find git root from cwd.
let repoPath: string;
if (inputPath) {
repoPath = path.resolve(inputPath);
} else {
const gitRoot = getGitRoot(process.cwd());
if (!gitRoot) {
spinner.fail('Not inside a git repository');
console.log(' ✗ Not inside a git repository\n');
process.exitCode = 1;
return;
}
@ -40,7 +52,7 @@ export const analyzeCommand = async (
}
if (!isGitRepo(repoPath)) {
spinner.fail('Not a git repository');
console.log(' ✗ Not a git repository\n');
process.exitCode = 1;
return;
}
@ -49,25 +61,31 @@ export const analyzeCommand = async (
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');
console.log(' ✓ Repository already up to date\n');
return;
}
// Run ingestion pipeline
spinner.text = 'Running ingestion pipeline...';
const multibar = new cliProgress.MultiBar({
format: ' {bar} {percentage}% | {phase}',
barCompleteChar: '█',
barIncompleteChar: '░',
hideCursor: true,
barGlue: '',
autopadding: true,
}, cliProgress.Presets.shades_grey);
const progressBar = multibar.create(100, 0, { phase: 'Initializing...' });
const pipelineResult = await runPipelineFromRepo(repoPath, (progress) => {
spinner.text = `${progress.phase}: ${progress.percent}%`;
const phaseLabel = PHASE_LABELS[progress.phase] || progress.phase;
progressBar.update(progress.percent, { phase: phaseLabel });
});
// Load graph into KuzuDB
// Always start fresh - remove existing kuzu DB to avoid stale/corrupt data
spinner.text = 'Loading graph into KuzuDB...';
progressBar.update(100, { phase: 'Loading graph into KuzuDB...' });
await closeKuzu();
// Kuzu 0.11 stores databases as: <name> (main file) + <name>.wal (WAL file)
// BOTH must be deleted or kuzu will find the orphaned WAL and corrupt the database
const fsClean = await import('fs/promises');
const kuzuFiles = [kuzuPath, `${kuzuPath}.wal`, `${kuzuPath}.lock`];
for (const f of kuzuFiles) {
@ -77,9 +95,8 @@ export const analyzeCommand = async (
await initKuzu(kuzuPath);
await loadGraphToKuzu(pipelineResult.graph, pipelineResult.fileContents, storagePath);
// Create FTS indexes for keyword search
// Indexes searchable content on: File, Function, Class, Method
spinner.text = 'Creating FTS indexes...';
progressBar.update(100, { phase: 'Creating search indexes...' });
try {
await createFTSIndex('File', 'file_fts', ['name', 'content']);
await createFTSIndex('Function', 'function_fts', ['name', 'content']);
@ -87,23 +104,20 @@ export const analyzeCommand = async (
await createFTSIndex('Method', 'method_fts', ['name', 'content']);
await createFTSIndex('Interface', 'interface_fts', ['name', 'content']);
} catch (e: any) {
// FTS index creation may fail if tables are empty (no data for that type)
console.error('Note: Some FTS indexes may not have been created:', e.message);
console.error(' Note: Some FTS indexes may not have been created:', e.message);
}
// Generate embeddings
if (!options?.skipEmbeddings) {
spinner.text = 'Generating embeddings...';
progressBar.update(100, { phase: 'Generating embeddings...' });
await runEmbeddingPipeline(
executeQuery,
executeWithReusedStatement,
(progress) => {
spinner.text = `Embeddings: ${progress.percent}%`;
progressBar.update(progress.percent, { phase: `Embeddings ${progress.percent}%` });
}
);
}
// Save metadata
const stats = await getKuzuStats();
const meta = {
repoPath,
@ -119,16 +133,11 @@ export const analyzeCommand = async (
};
await saveMeta(storagePath, meta);
// Register in global registry
await registerRepo(repoPath, meta);
// Add .gitnexus to .gitignore
await addToGitignore(repoPath);
// Generate AI context files
const projectName = path.basename(repoPath);
// Compute aggregated cluster count (grouped by heuristicLabel, >=5 symbols)
// This matches the aggregation logic in local-backend.ts for tool output consistency.
let aggregatedClusterCount = 0;
if (pipelineResult.communityResult?.communities) {
const groups = new Map<string, number>();
@ -148,24 +157,27 @@ export const analyzeCommand = async (
processes: pipelineResult.processResult?.stats.totalProcesses,
});
// 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`);
await disposeEmbedder();
multibar.stop();
console.log('\n ✓ Repository indexed successfully\n');
console.log(` Path: ${repoPath}`);
console.log(` Storage: ${storagePath}`);
console.log(` Registry: ${getGlobalDir()}`);
console.log(` Stats: ${stats.nodes} nodes, ${stats.edges} edges, ${pipelineResult.communityResult?.stats.totalCommunities || 0} clusters, ${pipelineResult.processResult?.stats.totalProcesses || 0} processes`);
if (aiContext.files.length > 0) {
console.log(` AI Context: ${aiContext.files.join(', ')}`);
console.log(` Context: ${aiContext.files.join(', ')}`);
}
// Hint about setup if it hasn't been run
try {
await fs.access(getGlobalRegistryPath());
} catch {
// Registry didn't exist before this run — suggest setup
console.log('');
console.log(' Tip: Run `gitnexus setup` to configure MCP for your editor.');
console.log('\n Tip: Run `gitnexus setup` to configure MCP for your editor.');
}
console.log('');
};

View file

@ -64,7 +64,7 @@ export const initEmbedder = async (
// Configure transformers.js environment
env.allowLocalModels = false;
const isDev = process.env.NODE_ENV !== 'production';
const isDev = process.env.NODE_ENV === 'development';
if (isDev) {
console.log(`🧠 Loading embedding model: ${finalConfig.modelId}`);
}

View file

@ -21,7 +21,7 @@ import {
EMBEDDABLE_LABELS,
} from './types.js';
const isDev = process.env.NODE_ENV !== 'production';
const isDev = process.env.NODE_ENV === 'development';
/**
* Progress callback type

View file

@ -9,7 +9,7 @@ import { generateId } from '../../lib/utils.js';
import { getLanguageFromFilename, yieldToEventLoop } from './utils.js';
import { SupportedLanguages } from '../../config/supported-languages.js';
const isDev = process.env.NODE_ENV !== 'production';
const isDev = process.env.NODE_ENV === 'development';
// Type: Map<FilePath, Set<ResolvedFilePath>>
// Stores all files that a given file imports from

View file

@ -11,7 +11,7 @@ import { createASTCache } from './ast-cache.js';
import { PipelineProgress, PipelineResult } from '../../types/pipeline.js';
import { walkRepository } from './filesystem-walker.js';
const isDev = process.env.NODE_ENV !== 'production';
const isDev = process.env.NODE_ENV === 'development';
export const runPipelineFromRepo = async (
repoPath: string,

View file

@ -14,7 +14,7 @@ import { KnowledgeGraph, GraphNode, GraphRelationship, NodeLabel } from '../grap
import { CommunityMembership } from './community-processor.js';
import { calculateEntryPointScore, isTestFile } from './entry-point-scoring.js';
const isDev = process.env.NODE_ENV !== 'production';
const isDev = process.env.NODE_ENV === 'development';
// ============================================================================
// CONFIGURATION

View file

@ -11,6 +11,8 @@ import {
} from './schema.js';
import { generateAllCSVs } from './csv-generator.js';
const isDev = process.env.NODE_ENV === 'development';
let db: kuzu.Database | null = null;
let conn: kuzu.Connection | null = null;
@ -88,23 +90,24 @@ export const loadGraphToKuzu = async (
const normalizedPath = normalizeCopyPath(filePath);
const copyQuery = getCopyQuery(table, normalizedPath);
// Log CSV stats for diagnostics
const csvContent = await fs.readFile(filePath, 'utf-8');
const csvLines = csvContent.split('\n').length;
console.log(` COPY ${table}: ${csvLines - 1} rows`);
if (isDev) {
const csvContent = await fs.readFile(filePath, 'utf-8');
const csvLines = csvContent.split('\n').length;
console.log(` COPY ${table}: ${csvLines - 1} rows`);
}
try {
await conn.query(copyQuery);
} catch (err) {
const errMsg = err instanceof Error ? err.message : String(err);
console.warn(`⚠️ COPY failed for ${table} (${csvLines - 1} rows): ${errMsg}`);
console.warn(`⚠️ COPY failed for ${table}: ${errMsg.slice(0, 100)}`);
// Retry with IGNORE_ERRORS=true to skip malformed rows
console.log(` Retrying ${table} with IGNORE_ERRORS=true...`);
try {
const retryQuery = copyQuery.replace('auto_detect=false)', 'auto_detect=false, IGNORE_ERRORS=true)');
await conn.query(retryQuery);
console.log(` ✅ ${table} loaded with IGNORE_ERRORS (some rows may have been skipped)`);
if (isDev) {
console.log(` ✅ ${table} loaded with IGNORE_ERRORS (some rows may have been skipped)`);
}
} catch (retryErr) {
const retryMsg = retryErr instanceof Error ? retryErr.message : String(retryErr);
console.error(`❌ COPY failed for ${table} even with IGNORE_ERRORS: ${retryMsg}`);
@ -113,7 +116,9 @@ export const loadGraphToKuzu = async (
}
}
console.log('✅ All COPY commands succeeded. Starting relationship insertion...');
if (isDev) {
console.log('✅ All COPY commands succeeded. Starting relationship insertion...');
}
// Build a set of valid table names for fast lookup
const validTables = new Set<string>(NODE_TABLES as readonly string[]);

View file

@ -35,6 +35,14 @@ function isTestFilePath(filePath: string): boolean {
);
}
/** Valid KuzuDB node labels for safe Cypher query construction */
const VALID_NODE_LABELS = new Set([
'File', 'Folder', 'Function', 'Class', 'Interface', 'Method', 'CodeElement',
'Community', 'Process', 'Struct', 'Enum', 'Macro', 'Typedef', 'Union',
'Namespace', 'Trait', 'Impl', 'TypeAlias', 'Const', 'Static', 'Property',
'Record', 'Delegate', 'Annotation', 'Constructor', 'Template', 'Module',
]);
export interface CodebaseContext {
projectName: string;
stats: {
@ -260,12 +268,16 @@ export class LocalBackend {
max_symbols?: number;
include_content?: boolean;
}): Promise<any> {
if (!params.query?.trim()) {
return { error: 'query parameter is required and cannot be empty.' };
}
await this.ensureInitialized(repo.id);
const processLimit = params.limit || 5;
const maxSymbolsPerProcess = params.max_symbols || 10;
const includeContent = params.include_content ?? false;
const searchQuery = params.query;
const searchQuery = params.query.trim();
// Step 1: Run hybrid search to get matching symbols
const searchLimit = processLimit * maxSymbolsPerProcess; // fetch enough raw results
@ -453,7 +465,13 @@ export class LocalBackend {
*/
private async bm25Search(repo: RepoHandle, query: string, limit: number): Promise<any[]> {
const { searchFTSFromKuzu } = await import('../../core/search/bm25-index.js');
const bm25Results = await searchFTSFromKuzu(query, limit, repo.id);
let bm25Results;
try {
bm25Results = await searchFTSFromKuzu(query, limit, repo.id);
} catch (err: any) {
console.error('GitNexus: BM25/FTS search failed (FTS indexes may not exist) -', err.message);
return [];
}
const results: any[] = [];
@ -535,10 +553,14 @@ export class LocalBackend {
const labelEndIdx = nodeId.indexOf(':');
const label = labelEndIdx > 0 ? nodeId.substring(0, labelEndIdx) : 'Unknown';
// Validate label against known node types to prevent Cypher injection
if (!VALID_NODE_LABELS.has(label)) continue;
try {
const escapedId = nodeId.replace(/'/g, "''");
const nodeQuery = label === 'File'
? `MATCH (n:File {id: '${nodeId.replace(/'/g, "''")}'}) RETURN n.name AS name, n.filePath AS filePath`
: `MATCH (n:${label} {id: '${nodeId.replace(/'/g, "''")}'}) RETURN n.name AS name, n.filePath AS filePath, n.startLine AS startLine, n.endLine AS endLine`;
? `MATCH (n:File {id: '${escapedId}'}) RETURN n.name AS name, n.filePath AS filePath`
: `MATCH (n:\`${label}\` {id: '${escapedId}'}) RETURN n.name AS name, n.filePath AS filePath, n.startLine AS startLine, n.endLine AS endLine`;
const nodeRows = await executeQuery(repo.id, nodeQuery);
if (nodeRows.length > 0) {
@ -1201,16 +1223,18 @@ export class LocalBackend {
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 [${relTypeFilter}]${confidenceFilter} 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 [${relTypeFilter}]${confidenceFilter} 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`;
// Batch frontier nodes into a single Cypher query per depth level
const idList = frontier.map(id => `'${id.replace(/'/g, "''")}'`).join(', ');
const query = direction === 'upstream'
? `MATCH (caller)-[r:CodeRelation]->(n) WHERE n.id IN [${idList}] AND r.type IN [${relTypeFilter}]${confidenceFilter} RETURN n.id AS sourceId, 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)-[r:CodeRelation]->(callee) WHERE n.id IN [${idList}] AND r.type IN [${relTypeFilter}]${confidenceFilter} RETURN n.id AS sourceId, 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`;
try {
const related = await executeQuery(repo.id, query);
for (const rel of related) {
const relId = rel.id || rel[0];
const filePath = rel.filePath || rel[3] || '';
const relId = rel.id || rel[1];
const filePath = rel.filePath || rel[4] || '';
if (!includeTests && isTestFilePath(filePath)) continue;
@ -1220,15 +1244,15 @@ export class LocalBackend {
impacted.push({
depth,
id: relId,
name: rel.name || rel[1],
type: rel.type || rel[2],
name: rel.name || rel[2],
type: rel.type || rel[3],
filePath,
relationType: rel.relType || rel[4],
confidence: rel.confidence || rel[5] || 1.0,
relationType: rel.relType || rel[5],
confidence: rel.confidence || rel[6] || 1.0,
});
}
}
}
} catch { /* query failed for this depth level */ }
frontier = nextFrontier;
}