From 18a52633966cb71478c2d8d7121f5c2ba0101302 Mon Sep 17 00:00:00 2001 From: abhigyanpatwari Date: Fri, 13 Feb 2026 22:39:12 +0530 Subject: [PATCH] logging global registry --- AGENTS.md | 17 +- CLAUDE.md | 62 +++++ gitnexus/package-lock.json | 252 ++++-------------- gitnexus/package.json | 3 +- gitnexus/src/cli/analyze.ts | 92 ++++--- gitnexus/src/core/embeddings/embedder.ts | 2 +- .../src/core/embeddings/embedding-pipeline.ts | 2 +- .../src/core/ingestion/import-processor.ts | 2 +- gitnexus/src/core/ingestion/pipeline.ts | 2 +- .../src/core/ingestion/process-processor.ts | 2 +- gitnexus/src/core/kuzu/kuzu-adapter.ts | 23 +- gitnexus/src/mcp/local/local-backend.ts | 56 ++-- 12 files changed, 240 insertions(+), 275 deletions(-) create mode 100644 CLAUDE.md diff --git a/AGENTS.md b/AGENTS.md index a9fbc688ae..68fd527cc6 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -3,9 +3,9 @@ # 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 | diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000000..583356388a --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,62 @@ + +# 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 +``` + + diff --git a/gitnexus/package-lock.json b/gitnexus/package-lock.json index c2d95b25e2..eab4823062 100644 --- a/gitnexus/package-lock.json +++ b/gitnexus/package-lock.json @@ -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", diff --git a/gitnexus/package.json b/gitnexus/package.json index 03ba76d754..74c0c1ec05 100644 --- a/gitnexus/package.json +++ b/gitnexus/package.json @@ -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", diff --git a/gitnexus/src/cli/analyze.ts b/gitnexus/src/cli/analyze.ts index 3abf4a31a7..bd4554b539 100644 --- a/gitnexus/src/cli/analyze.ts +++ b/gitnexus/src/cli/analyze.ts @@ -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 = { + 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: (main file) + .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(); @@ -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(''); }; diff --git a/gitnexus/src/core/embeddings/embedder.ts b/gitnexus/src/core/embeddings/embedder.ts index f0cdcde458..6d40f04b07 100644 --- a/gitnexus/src/core/embeddings/embedder.ts +++ b/gitnexus/src/core/embeddings/embedder.ts @@ -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}`); } diff --git a/gitnexus/src/core/embeddings/embedding-pipeline.ts b/gitnexus/src/core/embeddings/embedding-pipeline.ts index 128e5d9374..4673b59697 100644 --- a/gitnexus/src/core/embeddings/embedding-pipeline.ts +++ b/gitnexus/src/core/embeddings/embedding-pipeline.ts @@ -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 diff --git a/gitnexus/src/core/ingestion/import-processor.ts b/gitnexus/src/core/ingestion/import-processor.ts index 4cb4073b00..07c1b17141 100644 --- a/gitnexus/src/core/ingestion/import-processor.ts +++ b/gitnexus/src/core/ingestion/import-processor.ts @@ -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> // Stores all files that a given file imports from diff --git a/gitnexus/src/core/ingestion/pipeline.ts b/gitnexus/src/core/ingestion/pipeline.ts index b5e6fc53c7..df027b8a22 100644 --- a/gitnexus/src/core/ingestion/pipeline.ts +++ b/gitnexus/src/core/ingestion/pipeline.ts @@ -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, diff --git a/gitnexus/src/core/ingestion/process-processor.ts b/gitnexus/src/core/ingestion/process-processor.ts index 805ea9304b..10d3261fbd 100644 --- a/gitnexus/src/core/ingestion/process-processor.ts +++ b/gitnexus/src/core/ingestion/process-processor.ts @@ -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 diff --git a/gitnexus/src/core/kuzu/kuzu-adapter.ts b/gitnexus/src/core/kuzu/kuzu-adapter.ts index d99ac5cee8..6752510479 100644 --- a/gitnexus/src/core/kuzu/kuzu-adapter.ts +++ b/gitnexus/src/core/kuzu/kuzu-adapter.ts @@ -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(NODE_TABLES as readonly string[]); diff --git a/gitnexus/src/mcp/local/local-backend.ts b/gitnexus/src/mcp/local/local-backend.ts index 834a0ca4a9..c7ab45a88a 100644 --- a/gitnexus/src/mcp/local/local-backend.ts +++ b/gitnexus/src/mcp/local/local-backend.ts @@ -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 { + 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 { 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; }