diff --git a/.claude/settings.local.json b/.claude/settings.local.json new file mode 100644 index 000000000..e608e8599 --- /dev/null +++ b/.claude/settings.local.json @@ -0,0 +1,23 @@ +{ + "permissions": { + "allow": [ + "WebSearch", + "WebFetch(domain:cursor.com)", + "WebFetch(domain:composio.dev)", + "Bash(npx tsc:*)", + "Bash(claude rename:*)", + "Bash(npm run build:*)", + "Bash(npm link:*)", + "Bash(gitnexus --version:*)", + "Bash(gitnexus --help:*)", + "Bash(npm ls:*)", + "Bash(gitnexus augment:*)", + "Bash(node -e \"\nconst { augment } = await import\\(''./gitnexus/dist/core/augmentation/engine.js''\\);\ntry {\n const r = await augment\\(''setup'', process.cwd\\(\\)\\);\n console.log\\(''Result:'', r ? r.substring\\(0, 200\\) : ''null''\\);\n} catch\\(e\\) { console.error\\(''Error:'', e.message\\); }\nprocess.exit\\(0\\);\n\")", + "Bash(cmd.exe /c \"cd /d D:\\\\Projects\\\\GitnexusV2 && gitnexus augment setup\")", + "Bash(cmd.exe /c \"cd /d D:\\\\Projects\\\\GitnexusV2 && gitnexus status\")", + "Bash(gh repo clone:*)", + "Bash(claude mcp:*)", + "Bash(gh issue view:*)" + ] + } +} diff --git a/.claude/skills/gitnexus/debugging/SKILL.md b/.claude/skills/gitnexus/debugging/SKILL.md index 316b0aa3e..3b945835b 100644 --- a/.claude/skills/gitnexus/debugging/SKILL.md +++ b/.claude/skills/gitnexus/debugging/SKILL.md @@ -15,8 +15,8 @@ description: Trace bugs through call chains using knowledge graph ## Workflow ``` -1. gitnexus_search({query: ""}) → Find related code -2. gitnexus_explore({name: "", type: "symbol"}) → See callers/callees +1. gitnexus_query({query: ""}) → Find related execution flows +2. gitnexus_context({name: ""}) → See callers/callees/processes 3. READ gitnexus://repo/{name}/process/{name} → Trace execution flow 4. gitnexus_cypher({query: "MATCH path..."}) → Custom traces if needed ``` @@ -27,9 +27,9 @@ description: Trace bugs through call chains using knowledge graph ``` - [ ] Understand the symptom (error message, unexpected behavior) -- [ ] gitnexus_search for error text or related code -- [ ] Identify the suspect function -- [ ] gitnexus_explore to see callers and callees +- [ ] gitnexus_query for error text or related code +- [ ] Identify the suspect function from returned processes +- [ ] gitnexus_context to see callers and callees - [ ] Trace execution flow via process resource if applicable - [ ] gitnexus_cypher for custom call chain traces if needed - [ ] Read source files to confirm root cause @@ -39,26 +39,27 @@ description: Trace bugs through call chains using knowledge graph | Symptom | GitNexus Approach | |---------|-------------------| -| Error message | `gitnexus_search` for error text → `explore` throw sites | -| Wrong return value | `explore` the function → trace callees for data flow | -| Intermittent failure | `explore` → look for external calls, async deps | -| Performance issue | `explore` → find symbols with many callers (hot paths) | -| Recent regression | `gitnexus_impact` on recently changed symbols | +| Error message | `gitnexus_query` for error text → `context` on throw sites | +| Wrong return value | `context` on the function → trace callees for data flow | +| Intermittent failure | `context` → look for external calls, async deps | +| Performance issue | `context` → find symbols with many callers (hot paths) | +| Recent regression | `detect_changes` to see what your changes affect | ## Tools -**gitnexus_search** — find code related to error: +**gitnexus_query** — find code related to error: ``` -gitnexus_search({query: "payment validation error", depth: "full"}) -→ validatePayment, handlePaymentError, PaymentException +gitnexus_query({query: "payment validation error"}) +→ Processes: CheckoutFlow, ErrorHandling +→ Symbols: validatePayment, handlePaymentError, PaymentException ``` -**gitnexus_explore** — full context for a suspect: +**gitnexus_context** — full context for a suspect: ``` -gitnexus_explore({name: "validatePayment", type: "symbol"}) -→ Callers: processCheckout, webhookHandler -→ Callees: verifyCard, fetchRates (external API!) -→ Cluster: Payment +gitnexus_context({name: "validatePayment"}) +→ Incoming calls: processCheckout, webhookHandler +→ Outgoing calls: verifyCard, fetchRates (external API!) +→ Processes: CheckoutFlow (step 3/7) ``` **gitnexus_cypher** — custom call chain traces: @@ -70,11 +71,12 @@ RETURN [n IN nodes(path) | n.name] AS chain ## Example: "Payment endpoint returns 500 intermittently" ``` -1. gitnexus_search({query: "payment error handling"}) - → validatePayment, handlePaymentError, PaymentException +1. gitnexus_query({query: "payment error handling"}) + → Processes: CheckoutFlow, ErrorHandling + → Symbols: validatePayment, handlePaymentError -2. gitnexus_explore({name: "validatePayment", type: "symbol"}) - → Callees: verifyCard, fetchRates (external API!) +2. gitnexus_context({name: "validatePayment"}) + → Outgoing calls: verifyCard, fetchRates (external API!) 3. READ gitnexus://repo/my-app/process/CheckoutFlow → Step 3: validatePayment → calls fetchRates (external) diff --git a/.claude/skills/gitnexus/exploring/SKILL.md b/.claude/skills/gitnexus/exploring/SKILL.md index 70a74b0fb..2214c289c 100644 --- a/.claude/skills/gitnexus/exploring/SKILL.md +++ b/.claude/skills/gitnexus/exploring/SKILL.md @@ -17,9 +17,9 @@ description: Navigate unfamiliar code using GitNexus knowledge graph ``` 1. READ gitnexus://repos → Discover indexed repos 2. READ gitnexus://repo/{name}/context → Codebase overview, check staleness -3. READ gitnexus://repo/{name}/clusters → See all functional areas -4. READ gitnexus://repo/{name}/cluster/{name} → Drill into relevant cluster -5. gitnexus_explore({name, type: "symbol"}) → Deep dive on specific symbol +3. gitnexus_query({query: ""}) → Find related execution flows +4. gitnexus_context({name: ""}) → Deep dive on specific symbol +5. READ gitnexus://repo/{name}/process/{name} → Trace full execution flow ``` > If step 2 says "Index is stale" → run `npx gitnexus analyze` in terminal. @@ -27,12 +27,11 @@ description: Navigate unfamiliar code using GitNexus knowledge graph ## Checklist ``` -- [ ] READ gitnexus://repos - [ ] READ gitnexus://repo/{name}/context -- [ ] READ gitnexus://repo/{name}/clusters -- [ ] Identify the relevant cluster -- [ ] READ gitnexus://repo/{name}/cluster/{name} -- [ ] gitnexus_explore for key symbols +- [ ] gitnexus_query for the concept you want to understand +- [ ] Review returned processes (execution flows) +- [ ] gitnexus_context on key symbols for callers/callees +- [ ] READ process resource for full execution traces - [ ] Read source files for implementation details ``` @@ -41,33 +40,36 @@ description: Navigate unfamiliar code using GitNexus knowledge graph | Resource | What you get | |----------|-------------| | `gitnexus://repo/{name}/context` | Stats, staleness warning (~150 tokens) | -| `gitnexus://repo/{name}/clusters` | All clusters with cohesion scores (~300 tokens) | -| `gitnexus://repo/{name}/cluster/{name}` | Cluster members with file paths (~500 tokens) | +| `gitnexus://repo/{name}/clusters` | All functional areas with cohesion scores (~300 tokens) | +| `gitnexus://repo/{name}/cluster/{name}` | Area members with file paths (~500 tokens) | | `gitnexus://repo/{name}/process/{name}` | Step-by-step execution trace (~200 tokens) | ## Tools -**gitnexus_explore** — symbol context with callers/callees: +**gitnexus_query** — find execution flows related to a concept: ``` -gitnexus_explore({name: "validateUser", type: "symbol"}) -→ Callers: loginHandler, apiMiddleware -→ Callees: checkToken, getUserById -→ Cluster: Auth (92% cohesion) +gitnexus_query({query: "payment processing"}) +→ Processes: CheckoutFlow, RefundFlow, WebhookHandler +→ Symbols grouped by flow with file locations ``` -**gitnexus_search** — find code by query when you don't know the cluster: +**gitnexus_context** — 360-degree view of a symbol: ``` -gitnexus_search({query: "payment validation", depth: "full"}) +gitnexus_context({name: "validateUser"}) +→ Incoming calls: loginHandler, apiMiddleware +→ Outgoing calls: checkToken, getUserById +→ Processes: LoginFlow (step 2/5), TokenRefresh (step 1/3) ``` ## Example: "How does payment processing work?" ``` -1. READ gitnexus://repo/my-app/context → 918 symbols, 12 clusters -2. READ gitnexus://repo/my-app/clusters → Auth, Payment, Database, API... -3. READ gitnexus://repo/my-app/cluster/Payment → processPayment, validateCard, PaymentService -4. gitnexus_explore({name: "processPayment", type: "symbol"}) - → Callers: checkoutHandler, webhookHandler - → Callees: validateCard, chargeStripe, saveTransaction -5. Read src/payments/processor.ts for implementation details +1. READ gitnexus://repo/my-app/context → 918 symbols, 45 processes +2. gitnexus_query({query: "payment processing"}) + → CheckoutFlow: processPayment → validateCard → chargeStripe + → RefundFlow: initiateRefund → calculateRefund → processRefund +3. gitnexus_context({name: "processPayment"}) + → Incoming: checkoutHandler, webhookHandler + → Outgoing: validateCard, chargeStripe, saveTransaction +4. Read src/payments/processor.ts for implementation details ``` diff --git a/.claude/skills/gitnexus/impact-analysis/SKILL.md b/.claude/skills/gitnexus/impact-analysis/SKILL.md index 7cedd2b97..bb5f51fcc 100644 --- a/.claude/skills/gitnexus/impact-analysis/SKILL.md +++ b/.claude/skills/gitnexus/impact-analysis/SKILL.md @@ -11,13 +11,14 @@ description: Analyze blast radius before making code changes - "Show me the blast radius" - "Who uses this code?" - Before making non-trivial code changes +- Before committing — to understand what your changes affect ## Workflow ``` 1. gitnexus_impact({target: "X", direction: "upstream"}) → What depends on this -2. READ gitnexus://repo/{name}/clusters → Check which areas are affected -3. READ gitnexus://repo/{name}/processes → Check affected execution flows +2. READ gitnexus://repo/{name}/processes → Check affected execution flows +3. gitnexus_detect_changes() → Map current git changes to affected flows 4. Assess risk and report to user ``` @@ -29,9 +30,8 @@ description: Analyze blast radius before making code changes - [ ] gitnexus_impact({target, direction: "upstream"}) to find dependents - [ ] Review d=1 items first (these WILL BREAK) - [ ] Check high-confidence (>0.8) dependencies -- [ ] READ clusters to understand which areas are affected -- [ ] Count affected clusters (cross-cutting = higher risk) - [ ] READ processes to check affected execution flows +- [ ] gitnexus_detect_changes() for pre-commit check - [ ] Assess risk level and report to user ``` @@ -47,14 +47,14 @@ description: Analyze blast radius before making code changes | Affected | Risk | |----------|------| -| <5 symbols, 1 cluster | LOW | -| 5-15 symbols, 1-2 clusters | MEDIUM | -| >15 symbols or 3+ clusters | HIGH | +| <5 symbols, few processes | LOW | +| 5-15 symbols, 2-5 processes | MEDIUM | +| >15 symbols or many processes | HIGH | | Critical path (auth, payments) | CRITICAL | ## Tools -**gitnexus_impact** — the primary tool: +**gitnexus_impact** — the primary tool for symbol blast radius: ``` gitnexus_impact({ target: "validateUser", @@ -69,9 +69,15 @@ gitnexus_impact({ → d=2 (LIKELY AFFECTED): - authRouter (src/routes/auth.ts:22) [CALLS, 95%] +``` -→ Affected Processes: LoginFlow, TokenRefresh -→ Risk: MEDIUM (3 processes) +**gitnexus_detect_changes** — git-diff based impact analysis: +``` +gitnexus_detect_changes({scope: "staged"}) + +→ Changed: 5 symbols in 3 files +→ Affected: LoginFlow, TokenRefresh, APIMiddlewarePipeline +→ Risk: MEDIUM ``` ## Example: "What breaks if I change validateUser?" @@ -81,8 +87,8 @@ gitnexus_impact({ → d=1: loginHandler, apiMiddleware (WILL BREAK) → d=2: authRouter, sessionManager (LIKELY AFFECTED) -2. READ gitnexus://repo/my-app/clusters - → Auth and API clusters affected (2 clusters) +2. READ gitnexus://repo/my-app/processes + → LoginFlow and TokenRefresh touch validateUser -3. Risk: 2 direct callers, 2 clusters = MEDIUM +3. Risk: 2 direct callers, 2 processes = MEDIUM ``` diff --git a/.claude/skills/gitnexus/refactoring/SKILL.md b/.claude/skills/gitnexus/refactoring/SKILL.md index f98c4b34e..23f4d1130 100644 --- a/.claude/skills/gitnexus/refactoring/SKILL.md +++ b/.claude/skills/gitnexus/refactoring/SKILL.md @@ -16,8 +16,8 @@ description: Plan safe refactors using blast radius and dependency mapping ``` 1. gitnexus_impact({target: "X", direction: "upstream"}) → Map all dependents -2. gitnexus_search({query: "X"}) → Find string/dynamic references -3. READ gitnexus://repo/{name}/cluster/{name} → Check cohesion impact +2. gitnexus_query({query: "X"}) → Find execution flows involving X +3. gitnexus_context({name: "X"}) → See all incoming/outgoing refs 4. Plan update order: interfaces → implementations → callers → tests ``` @@ -27,36 +27,44 @@ description: Plan safe refactors using blast radius and dependency mapping ### Rename Symbol ``` -- [ ] gitnexus_impact({target: oldName, direction: "upstream"}) — find all callers -- [ ] gitnexus_search({query: oldName}) — find string literals and dynamic references -- [ ] Check for reflection/dynamic invocation patterns -- [ ] Plan update order: interface → implementation → callers → tests -- [ ] Update all d=1 (WILL BREAK) items +- [ ] gitnexus_rename({symbol_name: "oldName", new_name: "newName", dry_run: true}) — preview all edits +- [ ] Review graph edits (high confidence) and ast_search edits (review carefully) +- [ ] If satisfied: gitnexus_rename({..., dry_run: false}) — apply edits +- [ ] gitnexus_detect_changes() — verify only expected files changed - [ ] Run tests for affected processes ``` ### Extract Module ``` -- [ ] gitnexus_explore({name: target, type: "symbol"}) — map internal dependencies +- [ ] gitnexus_context({name: target}) — see all incoming/outgoing refs - [ ] gitnexus_impact({target, direction: "upstream"}) — find all external callers -- [ ] READ cluster resource — check if extraction preserves cohesion - [ ] Define new module interface - [ ] Extract code, update imports +- [ ] gitnexus_detect_changes() — verify affected scope - [ ] Run tests for affected processes ``` ### Split Function/Service ``` -- [ ] gitnexus_explore({name: target, type: "symbol"}) — understand all callees -- [ ] Group callees by responsibility/domain +- [ ] gitnexus_context({name: target}) — understand all callees +- [ ] Group callees by responsibility - [ ] gitnexus_impact({target, direction: "upstream"}) — map callers to update - [ ] Create new functions/services - [ ] Update callers +- [ ] gitnexus_detect_changes() — verify affected scope - [ ] Run tests for affected processes ``` ## Tools +**gitnexus_rename** — automated multi-file rename: +``` +gitnexus_rename({symbol_name: "validateUser", new_name: "authenticateUser", dry_run: true}) +→ 12 edits across 8 files +→ 10 graph edits (high confidence), 2 ast_search edits (review) +→ Changes: [{file_path, edits: [{line, old_text, new_text, confidence}]}] +``` + **gitnexus_impact** — map all dependents first: ``` gitnexus_impact({target: "validateUser", direction: "upstream"}) @@ -64,10 +72,12 @@ gitnexus_impact({target: "validateUser", direction: "upstream"}) → Affected Processes: LoginFlow, TokenRefresh ``` -**gitnexus_search** — find string/dynamic references impact() might miss: +**gitnexus_detect_changes** — verify your changes after refactoring: ``` -gitnexus_search({query: "validateUser"}) -→ Found in: config.json (dynamic reference!), test fixtures +gitnexus_detect_changes({scope: "all"}) +→ Changed: 8 files, 12 symbols +→ Affected processes: LoginFlow, TokenRefresh +→ Risk: MEDIUM ``` **gitnexus_cypher** — custom reference queries: @@ -80,23 +90,24 @@ RETURN caller.name, caller.filePath ORDER BY caller.filePath | Risk Factor | Mitigation | |-------------|------------| -| Many callers (>5) | Update in small batches | -| Cross-cluster refs | Coordinate with affected areas | -| String/dynamic refs | `gitnexus_search` to find them | +| Many callers (>5) | Use gitnexus_rename for automated updates | +| Cross-area refs | Use detect_changes after to verify scope | +| String/dynamic refs | gitnexus_query to find them | | External/public API | Version and deprecate properly | ## Example: Rename `validateUser` to `authenticateUser` ``` -1. gitnexus_impact({target: "validateUser", direction: "upstream"}) - → d=1: loginHandler, apiMiddleware, testUtils +1. gitnexus_rename({symbol_name: "validateUser", new_name: "authenticateUser", dry_run: true}) + → 12 edits: 10 graph (safe), 2 ast_search (review) + → Files: validator.ts, login.ts, middleware.ts, config.json... -2. gitnexus_search({query: "validateUser"}) - → Found in: config.json (dynamic reference!) +2. Review ast_search edits (config.json: dynamic reference!) -3. Plan update order: - 1. Update declaration in src/auth/validator.ts - 2. Update config.json string reference - 3. Update loginHandler, apiMiddleware, testUtils - 4. Run tests for LoginFlow, TokenRefresh +3. gitnexus_rename({symbol_name: "validateUser", new_name: "authenticateUser", dry_run: false}) + → Applied 12 edits across 8 files + +4. gitnexus_detect_changes({scope: "all"}) + → Affected: LoginFlow, TokenRefresh + → Risk: MEDIUM — run tests for these flows ``` diff --git a/CLAUDE.md b/CLAUDE.md.bak similarity index 100% rename from CLAUDE.md rename to CLAUDE.md.bak diff --git a/assets/david_ostby_review.png b/assets/david_ostby_review.png new file mode 100644 index 000000000..de336f4d9 Binary files /dev/null and b/assets/david_ostby_review.png differ diff --git a/assets/reddit_thread_61_upvotes.png b/assets/reddit_thread_61_upvotes.png new file mode 100644 index 000000000..688454abf Binary files /dev/null and b/assets/reddit_thread_61_upvotes.png differ diff --git a/assets/user_adoption_cursor.png b/assets/user_adoption_cursor.png new file mode 100644 index 000000000..9d8f71926 Binary files /dev/null and b/assets/user_adoption_cursor.png differ diff --git a/assets/user_feedback_gamechanger.png b/assets/user_feedback_gamechanger.png new file mode 100644 index 000000000..e8f4de866 Binary files /dev/null and b/assets/user_feedback_gamechanger.png differ diff --git a/gitnexus-claude-plugin/.claude-plugin/plugin.json b/gitnexus-claude-plugin/.claude-plugin/plugin.json new file mode 100644 index 000000000..0772c90df --- /dev/null +++ b/gitnexus-claude-plugin/.claude-plugin/plugin.json @@ -0,0 +1,10 @@ +{ + "name": "gitnexus", + "description": "Code intelligence powered by a knowledge graph. Provides execution flow tracing, blast radius analysis, and augmented search across your codebase.", + "version": "1.0.0", + "author": { + "name": "GitNexus" + }, + "homepage": "https://github.com/nicosxt/gitnexus", + "repository": "https://github.com/nicosxt/gitnexus" +} diff --git a/gitnexus-claude-plugin/hooks/gitnexus-hook.js b/gitnexus-claude-plugin/hooks/gitnexus-hook.js new file mode 100644 index 000000000..67c890ff5 --- /dev/null +++ b/gitnexus-claude-plugin/hooks/gitnexus-hook.js @@ -0,0 +1,123 @@ +#!/usr/bin/env node +/** + * GitNexus Claude Code Hook + * + * PreToolUse handler — intercepts Grep/Glob/Bash searches + * and augments with graph context from the GitNexus index. + * + * NOTE: SessionStart hooks are broken on Windows (Claude Code bug). + * Session context is injected via CLAUDE.md / skills instead. + */ + +const fs = require('fs'); +const path = require('path'); +const { execFileSync } = require('child_process'); + +/** + * Read JSON input from stdin synchronously. + */ +function readInput() { + try { + const data = fs.readFileSync(0, 'utf-8'); + return JSON.parse(data); + } catch { + return {}; + } +} + +/** + * Check if a directory (or ancestor) has a .gitnexus index. + */ +function findGitNexusIndex(startDir) { + let dir = startDir || process.cwd(); + for (let i = 0; i < 5; i++) { + if (fs.existsSync(path.join(dir, '.gitnexus'))) { + return true; + } + const parent = path.dirname(dir); + if (parent === dir) break; + dir = parent; + } + return false; +} + +/** + * Extract search pattern from tool input. + */ +function extractPattern(toolName, toolInput) { + if (toolName === 'Grep') { + return toolInput.pattern || null; + } + + if (toolName === 'Glob') { + const raw = toolInput.pattern || ''; + const match = raw.match(/[*\/]([a-zA-Z][a-zA-Z0-9_-]{2,})/); + return match ? match[1] : null; + } + + if (toolName === 'Bash') { + const cmd = toolInput.command || ''; + if (!/\brg\b|\bgrep\b/.test(cmd)) return null; + + const tokens = cmd.split(/\s+/); + let foundCmd = false; + let skipNext = false; + const flagsWithValues = new Set(['-e', '-f', '-m', '-A', '-B', '-C', '-g', '--glob', '-t', '--type', '--include', '--exclude']); + + for (const token of tokens) { + if (skipNext) { skipNext = false; continue; } + if (!foundCmd) { + if (/\brg$|\bgrep$/.test(token)) foundCmd = true; + continue; + } + if (token.startsWith('-')) { + if (flagsWithValues.has(token)) skipNext = true; + continue; + } + const cleaned = token.replace(/['"]/g, ''); + return cleaned.length >= 3 ? cleaned : null; + } + return null; + } + + return null; +} + +function main() { + try { + const input = readInput(); + const hookEvent = input.hook_event_name || ''; + + if (hookEvent !== 'PreToolUse') return; + + const cwd = input.cwd || process.cwd(); + if (!findGitNexusIndex(cwd)) return; + + const toolName = input.tool_name || ''; + const toolInput = input.tool_input || {}; + + if (toolName !== 'Grep' && toolName !== 'Glob' && toolName !== 'Bash') return; + + const pattern = extractPattern(toolName, toolInput); + if (!pattern || pattern.length < 3) return; + + const result = execFileSync( + 'gitnexus', + ['augment', pattern], + { encoding: 'utf-8', timeout: 8000, cwd, stdio: ['pipe', 'pipe', 'pipe'] } + ); + + if (result && result.trim()) { + console.log(JSON.stringify({ + hookSpecificOutput: { + hookEventName: 'PreToolUse', + additionalContext: result.trim() + } + })); + } + } catch { + // Graceful failure + } +} + +main(); diff --git a/gitnexus-claude-plugin/hooks/hooks.json b/gitnexus-claude-plugin/hooks/hooks.json new file mode 100644 index 000000000..2e9cb49b2 --- /dev/null +++ b/gitnexus-claude-plugin/hooks/hooks.json @@ -0,0 +1,17 @@ +{ + "hooks": { + "PreToolUse": [ + { + "matcher": "Grep|Glob|Bash", + "hooks": [ + { + "type": "command", + "command": "node ${CLAUDE_PLUGIN_ROOT}/hooks/gitnexus-hook.js", + "timeout": 10, + "statusMessage": "Enriching with GitNexus graph context..." + } + ] + } + ] + } +} diff --git a/gitnexus-claude-plugin/hooks/pre-tool-use.sh b/gitnexus-claude-plugin/hooks/pre-tool-use.sh new file mode 100644 index 000000000..3c1af3bc0 --- /dev/null +++ b/gitnexus-claude-plugin/hooks/pre-tool-use.sh @@ -0,0 +1,78 @@ +#!/bin/bash +# GitNexus PreToolUse hook for Claude Code +# Intercepts Grep/Glob/Bash searches and augments with graph context. +# Receives JSON on stdin with { tool_name, tool_input, cwd, ... } +# Returns JSON with additionalContext for graph-enriched results. + +INPUT=$(cat) + +TOOL_NAME=$(echo "$INPUT" | jq -r '.tool_name // empty' 2>/dev/null) +CWD=$(echo "$INPUT" | jq -r '.cwd // empty' 2>/dev/null) + +# Extract search pattern based on tool type +PATTERN="" + +case "$TOOL_NAME" in + Grep) + PATTERN=$(echo "$INPUT" | jq -r '.tool_input.pattern // empty' 2>/dev/null) + ;; + Glob) + # Glob patterns are file paths, not search terms — extract meaningful part + RAW=$(echo "$INPUT" | jq -r '.tool_input.pattern // empty' 2>/dev/null) + # Strip glob syntax to get the meaningful name (e.g., "**/*.ts" → skip, "auth*.ts" → "auth") + PATTERN=$(echo "$RAW" | sed -n 's/.*[*\/]\([a-zA-Z][a-zA-Z0-9_-]*\).*/\1/p') + ;; + Bash) + CMD=$(echo "$INPUT" | jq -r '.tool_input.command // empty' 2>/dev/null) + # Only augment grep/rg commands + if echo "$CMD" | grep -qE '\brg\b|\bgrep\b'; then + # Extract pattern from rg/grep + if echo "$CMD" | grep -qE '\brg\b'; then + PATTERN=$(echo "$CMD" | sed -n "s/.*\brg\s\+\(--[^ ]*\s\+\)*['\"]\\?\([^'\";\| >]*\\).*/\2/p") + elif echo "$CMD" | grep -qE '\bgrep\b'; then + PATTERN=$(echo "$CMD" | sed -n "s/.*\bgrep\s\+\(-[^ ]*\s\+\)*['\"]\\?\([^'\";\| >]*\\).*/\2/p") + fi + fi + ;; + *) + # Not a search tool — skip + exit 0 + ;; +esac + +# Skip if pattern too short or empty +if [ -z "$PATTERN" ] || [ ${#PATTERN} -lt 3 ]; then + exit 0 +fi + +# Check if we're in a GitNexus-indexed repo +dir="${CWD:-$PWD}" +found=false +for i in 1 2 3 4 5; do + if [ -d "$dir/.gitnexus" ]; then + found=true + break + fi + parent="$(dirname "$dir")" + [ "$parent" = "$dir" ] && break + dir="$parent" +done + +if [ "$found" = false ]; then + exit 0 +fi + +# Run gitnexus augment — must be fast (<500ms target) +RESULT=$(cd "$CWD" && npx -y gitnexus augment "$PATTERN" 2>/dev/null) + +if [ -n "$RESULT" ]; then + ESCAPED=$(echo "$RESULT" | jq -Rs .) + jq -n --argjson ctx "$ESCAPED" '{ + hookSpecificOutput: { + hookEventName: "PreToolUse", + additionalContext: $ctx + } + }' +else + exit 0 +fi diff --git a/gitnexus-claude-plugin/hooks/session-start.js b/gitnexus-claude-plugin/hooks/session-start.js new file mode 100644 index 000000000..86157d354 --- /dev/null +++ b/gitnexus-claude-plugin/hooks/session-start.js @@ -0,0 +1,41 @@ +// GitNexus SessionStart hook for Claude Code +// Fires on session startup. Stdout is injected into Claude's context. +// Checks if the current directory has a GitNexus index. + +const fs = require('fs'); +const path = require('path'); + +let dir = process.cwd(); +let found = false; +for (let i = 0; i < 5; i++) { + if (fs.existsSync(path.join(dir, '.gitnexus'))) { + found = true; + break; + } + const parent = path.dirname(dir); + if (parent === dir) break; + dir = parent; +} + +if (!found) { + process.exit(0); +} + +process.stdout.write(`## GitNexus Code Intelligence + +This codebase is indexed by GitNexus, providing a knowledge graph with execution flows, relationships, and semantic search. + +**Available MCP Tools:** +- \`query\` — Process-grouped code intelligence (execution flows related to a concept) +- \`context\` — 360-degree symbol view (categorized refs, process participation) +- \`impact\` — Blast radius analysis (what breaks if you change a symbol) +- \`detect_changes\` — Git-diff impact analysis (what do your changes affect) +- \`rename\` — Multi-file coordinated rename with confidence tags +- \`cypher\` — Raw graph queries +- \`list_repos\` — Discover indexed repos + +**Quick Start:** READ \`gitnexus://repo/{name}/context\` for codebase overview, then use \`query\` to find execution flows. + +**Resources:** \`gitnexus://repo/{name}/context\` (overview), \`/processes\` (execution flows), \`/schema\` (for Cypher) +`); +process.exit(0); diff --git a/gitnexus-claude-plugin/hooks/session-start.sh b/gitnexus-claude-plugin/hooks/session-start.sh new file mode 100644 index 000000000..8960dd376 --- /dev/null +++ b/gitnexus-claude-plugin/hooks/session-start.sh @@ -0,0 +1,42 @@ +#!/bin/bash +# GitNexus SessionStart hook for Claude Code +# Fires on session startup. Stdout is injected into Claude's context. +# Checks if the current directory has a GitNexus index. + +dir="$PWD" +found=false +for i in 1 2 3 4 5; do + if [ -d "$dir/.gitnexus" ]; then + found=true + break + fi + parent="$(dirname "$dir")" + [ "$parent" = "$dir" ] && break + dir="$parent" +done + +if [ "$found" = false ]; then + exit 0 +fi + +# Inject GitNexus context — this stdout goes directly into Claude's context +cat << 'EOF' +## GitNexus Code Intelligence + +This codebase is indexed by GitNexus, providing a knowledge graph with execution flows, relationships, and semantic search. + +**Available MCP Tools:** +- `query` — Process-grouped code intelligence (execution flows related to a concept) +- `context` — 360-degree symbol view (categorized refs, process participation) +- `impact` — Blast radius analysis (what breaks if you change a symbol) +- `detect_changes` — Git-diff impact analysis (what do your changes affect) +- `rename` — Multi-file coordinated rename with confidence tags +- `cypher` — Raw graph queries +- `list_repos` — Discover indexed repos + +**Quick Start:** READ `gitnexus://repo/{name}/context` for codebase overview, then use `query` to find execution flows. + +**Resources:** `gitnexus://repo/{name}/context` (overview), `/processes` (execution flows), `/schema` (for Cypher) +EOF + +exit 0 diff --git a/gitnexus-claude-plugin/skills/debugging/SKILL.md b/gitnexus-claude-plugin/skills/debugging/SKILL.md new file mode 100644 index 000000000..3b945835b --- /dev/null +++ b/gitnexus-claude-plugin/skills/debugging/SKILL.md @@ -0,0 +1,85 @@ +--- +name: gitnexus-debugging +description: Trace bugs through call chains using knowledge graph +--- + +# Debugging with GitNexus + +## When to Use +- "Why is this function failing?" +- "Trace where this error comes from" +- "Who calls this method?" +- "This endpoint returns 500" +- Investigating bugs, errors, or unexpected behavior + +## Workflow + +``` +1. gitnexus_query({query: ""}) → Find related execution flows +2. gitnexus_context({name: ""}) → See callers/callees/processes +3. READ gitnexus://repo/{name}/process/{name} → Trace execution flow +4. gitnexus_cypher({query: "MATCH path..."}) → Custom traces if needed +``` + +> If "Index is stale" → run `npx gitnexus analyze` in terminal. + +## Checklist + +``` +- [ ] Understand the symptom (error message, unexpected behavior) +- [ ] gitnexus_query for error text or related code +- [ ] Identify the suspect function from returned processes +- [ ] gitnexus_context to see callers and callees +- [ ] Trace execution flow via process resource if applicable +- [ ] gitnexus_cypher for custom call chain traces if needed +- [ ] Read source files to confirm root cause +``` + +## Debugging Patterns + +| Symptom | GitNexus Approach | +|---------|-------------------| +| Error message | `gitnexus_query` for error text → `context` on throw sites | +| Wrong return value | `context` on the function → trace callees for data flow | +| Intermittent failure | `context` → look for external calls, async deps | +| Performance issue | `context` → find symbols with many callers (hot paths) | +| Recent regression | `detect_changes` to see what your changes affect | + +## Tools + +**gitnexus_query** — find code related to error: +``` +gitnexus_query({query: "payment validation error"}) +→ Processes: CheckoutFlow, ErrorHandling +→ Symbols: validatePayment, handlePaymentError, PaymentException +``` + +**gitnexus_context** — full context for a suspect: +``` +gitnexus_context({name: "validatePayment"}) +→ Incoming calls: processCheckout, webhookHandler +→ Outgoing calls: verifyCard, fetchRates (external API!) +→ Processes: CheckoutFlow (step 3/7) +``` + +**gitnexus_cypher** — custom call chain traces: +```cypher +MATCH path = (a)-[:CodeRelation {type: 'CALLS'}*1..2]->(b:Function {name: "validatePayment"}) +RETURN [n IN nodes(path) | n.name] AS chain +``` + +## Example: "Payment endpoint returns 500 intermittently" + +``` +1. gitnexus_query({query: "payment error handling"}) + → Processes: CheckoutFlow, ErrorHandling + → Symbols: validatePayment, handlePaymentError + +2. gitnexus_context({name: "validatePayment"}) + → Outgoing calls: verifyCard, fetchRates (external API!) + +3. READ gitnexus://repo/my-app/process/CheckoutFlow + → Step 3: validatePayment → calls fetchRates (external) + +4. Root cause: fetchRates calls external API without proper timeout +``` diff --git a/gitnexus-claude-plugin/skills/exploring/SKILL.md b/gitnexus-claude-plugin/skills/exploring/SKILL.md new file mode 100644 index 000000000..2214c289c --- /dev/null +++ b/gitnexus-claude-plugin/skills/exploring/SKILL.md @@ -0,0 +1,75 @@ +--- +name: gitnexus-exploring +description: Navigate unfamiliar code using GitNexus knowledge graph +--- + +# Exploring Codebases with GitNexus + +## When to Use +- "How does authentication work?" +- "What's the project structure?" +- "Show me the main components" +- "Where is the database logic?" +- Understanding code you haven't seen before + +## Workflow + +``` +1. READ gitnexus://repos → Discover indexed repos +2. READ gitnexus://repo/{name}/context → Codebase overview, check staleness +3. gitnexus_query({query: ""}) → Find related execution flows +4. gitnexus_context({name: ""}) → Deep dive on specific symbol +5. READ gitnexus://repo/{name}/process/{name} → Trace full execution flow +``` + +> If step 2 says "Index is stale" → run `npx gitnexus analyze` in terminal. + +## Checklist + +``` +- [ ] READ gitnexus://repo/{name}/context +- [ ] gitnexus_query for the concept you want to understand +- [ ] Review returned processes (execution flows) +- [ ] gitnexus_context on key symbols for callers/callees +- [ ] READ process resource for full execution traces +- [ ] Read source files for implementation details +``` + +## Resources + +| Resource | What you get | +|----------|-------------| +| `gitnexus://repo/{name}/context` | Stats, staleness warning (~150 tokens) | +| `gitnexus://repo/{name}/clusters` | All functional areas with cohesion scores (~300 tokens) | +| `gitnexus://repo/{name}/cluster/{name}` | Area members with file paths (~500 tokens) | +| `gitnexus://repo/{name}/process/{name}` | Step-by-step execution trace (~200 tokens) | + +## Tools + +**gitnexus_query** — find execution flows related to a concept: +``` +gitnexus_query({query: "payment processing"}) +→ Processes: CheckoutFlow, RefundFlow, WebhookHandler +→ Symbols grouped by flow with file locations +``` + +**gitnexus_context** — 360-degree view of a symbol: +``` +gitnexus_context({name: "validateUser"}) +→ Incoming calls: loginHandler, apiMiddleware +→ Outgoing calls: checkToken, getUserById +→ Processes: LoginFlow (step 2/5), TokenRefresh (step 1/3) +``` + +## Example: "How does payment processing work?" + +``` +1. READ gitnexus://repo/my-app/context → 918 symbols, 45 processes +2. gitnexus_query({query: "payment processing"}) + → CheckoutFlow: processPayment → validateCard → chargeStripe + → RefundFlow: initiateRefund → calculateRefund → processRefund +3. gitnexus_context({name: "processPayment"}) + → Incoming: checkoutHandler, webhookHandler + → Outgoing: validateCard, chargeStripe, saveTransaction +4. Read src/payments/processor.ts for implementation details +``` diff --git a/gitnexus-claude-plugin/skills/impact-analysis/SKILL.md b/gitnexus-claude-plugin/skills/impact-analysis/SKILL.md new file mode 100644 index 000000000..bb5f51fcc --- /dev/null +++ b/gitnexus-claude-plugin/skills/impact-analysis/SKILL.md @@ -0,0 +1,94 @@ +--- +name: gitnexus-impact-analysis +description: Analyze blast radius before making code changes +--- + +# Impact Analysis with GitNexus + +## When to Use +- "Is it safe to change this function?" +- "What will break if I modify X?" +- "Show me the blast radius" +- "Who uses this code?" +- Before making non-trivial code changes +- Before committing — to understand what your changes affect + +## Workflow + +``` +1. gitnexus_impact({target: "X", direction: "upstream"}) → What depends on this +2. READ gitnexus://repo/{name}/processes → Check affected execution flows +3. gitnexus_detect_changes() → Map current git changes to affected flows +4. Assess risk and report to user +``` + +> If "Index is stale" → run `npx gitnexus analyze` in terminal. + +## Checklist + +``` +- [ ] gitnexus_impact({target, direction: "upstream"}) to find dependents +- [ ] Review d=1 items first (these WILL BREAK) +- [ ] Check high-confidence (>0.8) dependencies +- [ ] READ processes to check affected execution flows +- [ ] gitnexus_detect_changes() for pre-commit check +- [ ] Assess risk level and report to user +``` + +## Understanding Output + +| Depth | Risk Level | Meaning | +|-------|-----------|---------| +| d=1 | **WILL BREAK** | Direct callers/importers | +| d=2 | LIKELY AFFECTED | Indirect dependencies | +| d=3 | MAY NEED TESTING | Transitive effects | + +## Risk Assessment + +| Affected | Risk | +|----------|------| +| <5 symbols, few processes | LOW | +| 5-15 symbols, 2-5 processes | MEDIUM | +| >15 symbols or many processes | HIGH | +| Critical path (auth, payments) | CRITICAL | + +## Tools + +**gitnexus_impact** — the primary tool for symbol blast radius: +``` +gitnexus_impact({ + target: "validateUser", + direction: "upstream", + minConfidence: 0.8, + maxDepth: 3 +}) + +→ d=1 (WILL BREAK): + - loginHandler (src/auth/login.ts:42) [CALLS, 100%] + - apiMiddleware (src/api/middleware.ts:15) [CALLS, 100%] + +→ d=2 (LIKELY AFFECTED): + - authRouter (src/routes/auth.ts:22) [CALLS, 95%] +``` + +**gitnexus_detect_changes** — git-diff based impact analysis: +``` +gitnexus_detect_changes({scope: "staged"}) + +→ Changed: 5 symbols in 3 files +→ Affected: LoginFlow, TokenRefresh, APIMiddlewarePipeline +→ Risk: MEDIUM +``` + +## Example: "What breaks if I change validateUser?" + +``` +1. gitnexus_impact({target: "validateUser", direction: "upstream"}) + → d=1: loginHandler, apiMiddleware (WILL BREAK) + → d=2: authRouter, sessionManager (LIKELY AFFECTED) + +2. READ gitnexus://repo/my-app/processes + → LoginFlow and TokenRefresh touch validateUser + +3. Risk: 2 direct callers, 2 processes = MEDIUM +``` diff --git a/gitnexus-claude-plugin/skills/refactoring/SKILL.md b/gitnexus-claude-plugin/skills/refactoring/SKILL.md new file mode 100644 index 000000000..23f4d1130 --- /dev/null +++ b/gitnexus-claude-plugin/skills/refactoring/SKILL.md @@ -0,0 +1,113 @@ +--- +name: gitnexus-refactoring +description: Plan safe refactors using blast radius and dependency mapping +--- + +# Refactoring with GitNexus + +## When to Use +- "Rename this function safely" +- "Extract this into a module" +- "Split this service" +- "Move this to a new file" +- Any task involving renaming, extracting, splitting, or restructuring code + +## Workflow + +``` +1. gitnexus_impact({target: "X", direction: "upstream"}) → Map all dependents +2. gitnexus_query({query: "X"}) → Find execution flows involving X +3. gitnexus_context({name: "X"}) → See all incoming/outgoing refs +4. Plan update order: interfaces → implementations → callers → tests +``` + +> If "Index is stale" → run `npx gitnexus analyze` in terminal. + +## Checklists + +### Rename Symbol +``` +- [ ] gitnexus_rename({symbol_name: "oldName", new_name: "newName", dry_run: true}) — preview all edits +- [ ] Review graph edits (high confidence) and ast_search edits (review carefully) +- [ ] If satisfied: gitnexus_rename({..., dry_run: false}) — apply edits +- [ ] gitnexus_detect_changes() — verify only expected files changed +- [ ] Run tests for affected processes +``` + +### Extract Module +``` +- [ ] gitnexus_context({name: target}) — see all incoming/outgoing refs +- [ ] gitnexus_impact({target, direction: "upstream"}) — find all external callers +- [ ] Define new module interface +- [ ] Extract code, update imports +- [ ] gitnexus_detect_changes() — verify affected scope +- [ ] Run tests for affected processes +``` + +### Split Function/Service +``` +- [ ] gitnexus_context({name: target}) — understand all callees +- [ ] Group callees by responsibility +- [ ] gitnexus_impact({target, direction: "upstream"}) — map callers to update +- [ ] Create new functions/services +- [ ] Update callers +- [ ] gitnexus_detect_changes() — verify affected scope +- [ ] Run tests for affected processes +``` + +## Tools + +**gitnexus_rename** — automated multi-file rename: +``` +gitnexus_rename({symbol_name: "validateUser", new_name: "authenticateUser", dry_run: true}) +→ 12 edits across 8 files +→ 10 graph edits (high confidence), 2 ast_search edits (review) +→ Changes: [{file_path, edits: [{line, old_text, new_text, confidence}]}] +``` + +**gitnexus_impact** — map all dependents first: +``` +gitnexus_impact({target: "validateUser", direction: "upstream"}) +→ d=1: loginHandler, apiMiddleware, testUtils +→ Affected Processes: LoginFlow, TokenRefresh +``` + +**gitnexus_detect_changes** — verify your changes after refactoring: +``` +gitnexus_detect_changes({scope: "all"}) +→ Changed: 8 files, 12 symbols +→ Affected processes: LoginFlow, TokenRefresh +→ Risk: MEDIUM +``` + +**gitnexus_cypher** — custom reference queries: +```cypher +MATCH (caller)-[:CodeRelation {type: 'CALLS'}]->(f:Function {name: "validateUser"}) +RETURN caller.name, caller.filePath ORDER BY caller.filePath +``` + +## Risk Rules + +| Risk Factor | Mitigation | +|-------------|------------| +| Many callers (>5) | Use gitnexus_rename for automated updates | +| Cross-area refs | Use detect_changes after to verify scope | +| String/dynamic refs | gitnexus_query to find them | +| External/public API | Version and deprecate properly | + +## Example: Rename `validateUser` to `authenticateUser` + +``` +1. gitnexus_rename({symbol_name: "validateUser", new_name: "authenticateUser", dry_run: true}) + → 12 edits: 10 graph (safe), 2 ast_search (review) + → Files: validator.ts, login.ts, middleware.ts, config.json... + +2. Review ast_search edits (config.json: dynamic reference!) + +3. gitnexus_rename({symbol_name: "validateUser", new_name: "authenticateUser", dry_run: false}) + → Applied 12 edits across 8 files + +4. gitnexus_detect_changes({scope: "all"}) + → Affected: LoginFlow, TokenRefresh + → Risk: MEDIUM — run tests for these flows +``` diff --git a/gitnexus-cursor-integration/hooks/augment-shell.sh b/gitnexus-cursor-integration/hooks/augment-shell.sh new file mode 100644 index 000000000..48ea185b0 --- /dev/null +++ b/gitnexus-cursor-integration/hooks/augment-shell.sh @@ -0,0 +1,50 @@ +#!/bin/bash +# GitNexus beforeShellExecution hook for Cursor +# Receives JSON on stdin with { command, cwd, timeout } +# Returns JSON on stdout with { permission, agent_message } +# +# Extracts search pattern from grep/rg commands, runs gitnexus augment, +# and injects the enriched context via agent_message. + +INPUT=$(cat) + +COMMAND=$(echo "$INPUT" | jq -r '.command // empty' 2>/dev/null) + +if [ -z "$COMMAND" ]; then + echo '{"permission":"allow"}' + exit 0 +fi + +# Skip non-search commands +case "$COMMAND" in + cd\ *|npm\ *|yarn\ *|pnpm\ *|git\ commit*|git\ push*|git\ pull*|mkdir\ *|rm\ *|cp\ *|mv\ *|echo\ *|cat\ *) + echo '{"permission":"allow"}' + exit 0 + ;; +esac + +# Extract search pattern from rg/grep commands +PATTERN="" +if echo "$COMMAND" | grep -qE '\brg\b'; then + PATTERN=$(echo "$COMMAND" | sed -n "s/.*\brg\s\+\(--[^ ]*\s\+\)*['\"]\\?\([^'\";\| >]*\\).*/\2/p") +elif echo "$COMMAND" | grep -qE '\bgrep\b'; then + PATTERN=$(echo "$COMMAND" | sed -n "s/.*\bgrep\s\+\(-[^ ]*\s\+\)*['\"]\\?\([^'\";\| >]*\\).*/\2/p") +fi + +if [ -z "$PATTERN" ] || [ ${#PATTERN} -lt 3 ]; then + echo '{"permission":"allow"}' + exit 0 +fi + +# Run gitnexus augment +RESULT=$(npx -y gitnexus augment "$PATTERN" 2>/dev/null) + +if [ -n "$RESULT" ]; then + # Escape for JSON + ESCAPED=$(echo "$RESULT" | jq -Rs .) + echo "{\"permission\":\"allow\",\"agent_message\":$ESCAPED}" +else + echo '{"permission":"allow"}' +fi + +exit 0 diff --git a/gitnexus-cursor-integration/hooks/hooks.json b/gitnexus-cursor-integration/hooks/hooks.json new file mode 100644 index 000000000..ede0dfbf3 --- /dev/null +++ b/gitnexus-cursor-integration/hooks/hooks.json @@ -0,0 +1,12 @@ +{ + "version": 1, + "hooks": { + "beforeShellExecution": [ + { + "command": "./hooks/augment-shell.sh", + "timeout": 5, + "matcher": "\\brg\\b|\\bgrep\\b" + } + ] + } +} diff --git a/gitnexus-cursor-integration/skills/gitnexus-debugging/SKILL.md b/gitnexus-cursor-integration/skills/gitnexus-debugging/SKILL.md new file mode 100644 index 000000000..3b945835b --- /dev/null +++ b/gitnexus-cursor-integration/skills/gitnexus-debugging/SKILL.md @@ -0,0 +1,85 @@ +--- +name: gitnexus-debugging +description: Trace bugs through call chains using knowledge graph +--- + +# Debugging with GitNexus + +## When to Use +- "Why is this function failing?" +- "Trace where this error comes from" +- "Who calls this method?" +- "This endpoint returns 500" +- Investigating bugs, errors, or unexpected behavior + +## Workflow + +``` +1. gitnexus_query({query: ""}) → Find related execution flows +2. gitnexus_context({name: ""}) → See callers/callees/processes +3. READ gitnexus://repo/{name}/process/{name} → Trace execution flow +4. gitnexus_cypher({query: "MATCH path..."}) → Custom traces if needed +``` + +> If "Index is stale" → run `npx gitnexus analyze` in terminal. + +## Checklist + +``` +- [ ] Understand the symptom (error message, unexpected behavior) +- [ ] gitnexus_query for error text or related code +- [ ] Identify the suspect function from returned processes +- [ ] gitnexus_context to see callers and callees +- [ ] Trace execution flow via process resource if applicable +- [ ] gitnexus_cypher for custom call chain traces if needed +- [ ] Read source files to confirm root cause +``` + +## Debugging Patterns + +| Symptom | GitNexus Approach | +|---------|-------------------| +| Error message | `gitnexus_query` for error text → `context` on throw sites | +| Wrong return value | `context` on the function → trace callees for data flow | +| Intermittent failure | `context` → look for external calls, async deps | +| Performance issue | `context` → find symbols with many callers (hot paths) | +| Recent regression | `detect_changes` to see what your changes affect | + +## Tools + +**gitnexus_query** — find code related to error: +``` +gitnexus_query({query: "payment validation error"}) +→ Processes: CheckoutFlow, ErrorHandling +→ Symbols: validatePayment, handlePaymentError, PaymentException +``` + +**gitnexus_context** — full context for a suspect: +``` +gitnexus_context({name: "validatePayment"}) +→ Incoming calls: processCheckout, webhookHandler +→ Outgoing calls: verifyCard, fetchRates (external API!) +→ Processes: CheckoutFlow (step 3/7) +``` + +**gitnexus_cypher** — custom call chain traces: +```cypher +MATCH path = (a)-[:CodeRelation {type: 'CALLS'}*1..2]->(b:Function {name: "validatePayment"}) +RETURN [n IN nodes(path) | n.name] AS chain +``` + +## Example: "Payment endpoint returns 500 intermittently" + +``` +1. gitnexus_query({query: "payment error handling"}) + → Processes: CheckoutFlow, ErrorHandling + → Symbols: validatePayment, handlePaymentError + +2. gitnexus_context({name: "validatePayment"}) + → Outgoing calls: verifyCard, fetchRates (external API!) + +3. READ gitnexus://repo/my-app/process/CheckoutFlow + → Step 3: validatePayment → calls fetchRates (external) + +4. Root cause: fetchRates calls external API without proper timeout +``` diff --git a/gitnexus-cursor-integration/skills/gitnexus-exploring/SKILL.md b/gitnexus-cursor-integration/skills/gitnexus-exploring/SKILL.md new file mode 100644 index 000000000..2214c289c --- /dev/null +++ b/gitnexus-cursor-integration/skills/gitnexus-exploring/SKILL.md @@ -0,0 +1,75 @@ +--- +name: gitnexus-exploring +description: Navigate unfamiliar code using GitNexus knowledge graph +--- + +# Exploring Codebases with GitNexus + +## When to Use +- "How does authentication work?" +- "What's the project structure?" +- "Show me the main components" +- "Where is the database logic?" +- Understanding code you haven't seen before + +## Workflow + +``` +1. READ gitnexus://repos → Discover indexed repos +2. READ gitnexus://repo/{name}/context → Codebase overview, check staleness +3. gitnexus_query({query: ""}) → Find related execution flows +4. gitnexus_context({name: ""}) → Deep dive on specific symbol +5. READ gitnexus://repo/{name}/process/{name} → Trace full execution flow +``` + +> If step 2 says "Index is stale" → run `npx gitnexus analyze` in terminal. + +## Checklist + +``` +- [ ] READ gitnexus://repo/{name}/context +- [ ] gitnexus_query for the concept you want to understand +- [ ] Review returned processes (execution flows) +- [ ] gitnexus_context on key symbols for callers/callees +- [ ] READ process resource for full execution traces +- [ ] Read source files for implementation details +``` + +## Resources + +| Resource | What you get | +|----------|-------------| +| `gitnexus://repo/{name}/context` | Stats, staleness warning (~150 tokens) | +| `gitnexus://repo/{name}/clusters` | All functional areas with cohesion scores (~300 tokens) | +| `gitnexus://repo/{name}/cluster/{name}` | Area members with file paths (~500 tokens) | +| `gitnexus://repo/{name}/process/{name}` | Step-by-step execution trace (~200 tokens) | + +## Tools + +**gitnexus_query** — find execution flows related to a concept: +``` +gitnexus_query({query: "payment processing"}) +→ Processes: CheckoutFlow, RefundFlow, WebhookHandler +→ Symbols grouped by flow with file locations +``` + +**gitnexus_context** — 360-degree view of a symbol: +``` +gitnexus_context({name: "validateUser"}) +→ Incoming calls: loginHandler, apiMiddleware +→ Outgoing calls: checkToken, getUserById +→ Processes: LoginFlow (step 2/5), TokenRefresh (step 1/3) +``` + +## Example: "How does payment processing work?" + +``` +1. READ gitnexus://repo/my-app/context → 918 symbols, 45 processes +2. gitnexus_query({query: "payment processing"}) + → CheckoutFlow: processPayment → validateCard → chargeStripe + → RefundFlow: initiateRefund → calculateRefund → processRefund +3. gitnexus_context({name: "processPayment"}) + → Incoming: checkoutHandler, webhookHandler + → Outgoing: validateCard, chargeStripe, saveTransaction +4. Read src/payments/processor.ts for implementation details +``` diff --git a/gitnexus-cursor-integration/skills/gitnexus-impact-analysis/SKILL.md b/gitnexus-cursor-integration/skills/gitnexus-impact-analysis/SKILL.md new file mode 100644 index 000000000..bb5f51fcc --- /dev/null +++ b/gitnexus-cursor-integration/skills/gitnexus-impact-analysis/SKILL.md @@ -0,0 +1,94 @@ +--- +name: gitnexus-impact-analysis +description: Analyze blast radius before making code changes +--- + +# Impact Analysis with GitNexus + +## When to Use +- "Is it safe to change this function?" +- "What will break if I modify X?" +- "Show me the blast radius" +- "Who uses this code?" +- Before making non-trivial code changes +- Before committing — to understand what your changes affect + +## Workflow + +``` +1. gitnexus_impact({target: "X", direction: "upstream"}) → What depends on this +2. READ gitnexus://repo/{name}/processes → Check affected execution flows +3. gitnexus_detect_changes() → Map current git changes to affected flows +4. Assess risk and report to user +``` + +> If "Index is stale" → run `npx gitnexus analyze` in terminal. + +## Checklist + +``` +- [ ] gitnexus_impact({target, direction: "upstream"}) to find dependents +- [ ] Review d=1 items first (these WILL BREAK) +- [ ] Check high-confidence (>0.8) dependencies +- [ ] READ processes to check affected execution flows +- [ ] gitnexus_detect_changes() for pre-commit check +- [ ] Assess risk level and report to user +``` + +## Understanding Output + +| Depth | Risk Level | Meaning | +|-------|-----------|---------| +| d=1 | **WILL BREAK** | Direct callers/importers | +| d=2 | LIKELY AFFECTED | Indirect dependencies | +| d=3 | MAY NEED TESTING | Transitive effects | + +## Risk Assessment + +| Affected | Risk | +|----------|------| +| <5 symbols, few processes | LOW | +| 5-15 symbols, 2-5 processes | MEDIUM | +| >15 symbols or many processes | HIGH | +| Critical path (auth, payments) | CRITICAL | + +## Tools + +**gitnexus_impact** — the primary tool for symbol blast radius: +``` +gitnexus_impact({ + target: "validateUser", + direction: "upstream", + minConfidence: 0.8, + maxDepth: 3 +}) + +→ d=1 (WILL BREAK): + - loginHandler (src/auth/login.ts:42) [CALLS, 100%] + - apiMiddleware (src/api/middleware.ts:15) [CALLS, 100%] + +→ d=2 (LIKELY AFFECTED): + - authRouter (src/routes/auth.ts:22) [CALLS, 95%] +``` + +**gitnexus_detect_changes** — git-diff based impact analysis: +``` +gitnexus_detect_changes({scope: "staged"}) + +→ Changed: 5 symbols in 3 files +→ Affected: LoginFlow, TokenRefresh, APIMiddlewarePipeline +→ Risk: MEDIUM +``` + +## Example: "What breaks if I change validateUser?" + +``` +1. gitnexus_impact({target: "validateUser", direction: "upstream"}) + → d=1: loginHandler, apiMiddleware (WILL BREAK) + → d=2: authRouter, sessionManager (LIKELY AFFECTED) + +2. READ gitnexus://repo/my-app/processes + → LoginFlow and TokenRefresh touch validateUser + +3. Risk: 2 direct callers, 2 processes = MEDIUM +``` diff --git a/gitnexus-cursor-integration/skills/gitnexus-refactoring/SKILL.md b/gitnexus-cursor-integration/skills/gitnexus-refactoring/SKILL.md new file mode 100644 index 000000000..23f4d1130 --- /dev/null +++ b/gitnexus-cursor-integration/skills/gitnexus-refactoring/SKILL.md @@ -0,0 +1,113 @@ +--- +name: gitnexus-refactoring +description: Plan safe refactors using blast radius and dependency mapping +--- + +# Refactoring with GitNexus + +## When to Use +- "Rename this function safely" +- "Extract this into a module" +- "Split this service" +- "Move this to a new file" +- Any task involving renaming, extracting, splitting, or restructuring code + +## Workflow + +``` +1. gitnexus_impact({target: "X", direction: "upstream"}) → Map all dependents +2. gitnexus_query({query: "X"}) → Find execution flows involving X +3. gitnexus_context({name: "X"}) → See all incoming/outgoing refs +4. Plan update order: interfaces → implementations → callers → tests +``` + +> If "Index is stale" → run `npx gitnexus analyze` in terminal. + +## Checklists + +### Rename Symbol +``` +- [ ] gitnexus_rename({symbol_name: "oldName", new_name: "newName", dry_run: true}) — preview all edits +- [ ] Review graph edits (high confidence) and ast_search edits (review carefully) +- [ ] If satisfied: gitnexus_rename({..., dry_run: false}) — apply edits +- [ ] gitnexus_detect_changes() — verify only expected files changed +- [ ] Run tests for affected processes +``` + +### Extract Module +``` +- [ ] gitnexus_context({name: target}) — see all incoming/outgoing refs +- [ ] gitnexus_impact({target, direction: "upstream"}) — find all external callers +- [ ] Define new module interface +- [ ] Extract code, update imports +- [ ] gitnexus_detect_changes() — verify affected scope +- [ ] Run tests for affected processes +``` + +### Split Function/Service +``` +- [ ] gitnexus_context({name: target}) — understand all callees +- [ ] Group callees by responsibility +- [ ] gitnexus_impact({target, direction: "upstream"}) — map callers to update +- [ ] Create new functions/services +- [ ] Update callers +- [ ] gitnexus_detect_changes() — verify affected scope +- [ ] Run tests for affected processes +``` + +## Tools + +**gitnexus_rename** — automated multi-file rename: +``` +gitnexus_rename({symbol_name: "validateUser", new_name: "authenticateUser", dry_run: true}) +→ 12 edits across 8 files +→ 10 graph edits (high confidence), 2 ast_search edits (review) +→ Changes: [{file_path, edits: [{line, old_text, new_text, confidence}]}] +``` + +**gitnexus_impact** — map all dependents first: +``` +gitnexus_impact({target: "validateUser", direction: "upstream"}) +→ d=1: loginHandler, apiMiddleware, testUtils +→ Affected Processes: LoginFlow, TokenRefresh +``` + +**gitnexus_detect_changes** — verify your changes after refactoring: +``` +gitnexus_detect_changes({scope: "all"}) +→ Changed: 8 files, 12 symbols +→ Affected processes: LoginFlow, TokenRefresh +→ Risk: MEDIUM +``` + +**gitnexus_cypher** — custom reference queries: +```cypher +MATCH (caller)-[:CodeRelation {type: 'CALLS'}]->(f:Function {name: "validateUser"}) +RETURN caller.name, caller.filePath ORDER BY caller.filePath +``` + +## Risk Rules + +| Risk Factor | Mitigation | +|-------------|------------| +| Many callers (>5) | Use gitnexus_rename for automated updates | +| Cross-area refs | Use detect_changes after to verify scope | +| String/dynamic refs | gitnexus_query to find them | +| External/public API | Version and deprecate properly | + +## Example: Rename `validateUser` to `authenticateUser` + +``` +1. gitnexus_rename({symbol_name: "validateUser", new_name: "authenticateUser", dry_run: true}) + → 12 edits: 10 graph (safe), 2 ast_search (review) + → Files: validator.ts, login.ts, middleware.ts, config.json... + +2. Review ast_search edits (config.json: dynamic reference!) + +3. gitnexus_rename({symbol_name: "validateUser", new_name: "authenticateUser", dry_run: false}) + → Applied 12 edits across 8 files + +4. gitnexus_detect_changes({scope: "all"}) + → Affected: LoginFlow, TokenRefresh + → Risk: MEDIUM — run tests for these flows +``` diff --git a/gitnexus/APPROACH.md b/gitnexus/APPROACH.md new file mode 100644 index 000000000..960ea6d40 --- /dev/null +++ b/gitnexus/APPROACH.md @@ -0,0 +1,339 @@ +# GitNexus: Architecture & Approach + +GitNexus is a code intelligence layer that builds a knowledge graph from your repository's structure — functions, classes, call relationships, execution flows — and delivers that context to AI coding agents through two complementary channels: **MCP tools** for explicit deep dives and **hook-based augmentation** that invisibly enriches every search the agent performs. + +## Architecture + +```mermaid +graph LR + A[gitnexus analyze] -->|parse + index| B[(KuzuDB Graph)] + B --> C[MCP Server] + B --> D[Hook Augmentation] + C -->|tools + resources| E[AI Agent] + D -->|additionalContext on search| E +``` + +`gitnexus analyze` walks the repository, extracts symbols and relationships using tree-sitter, detects communities with the Leiden algorithm, traces execution flows into processes, and stores everything in a KuzuDB graph database under `.gitnexus/kuzu/`. + +The graph is then queryable through two paths: + +1. **MCP tools** — the agent calls `query`, `context`, `impact`, etc. explicitly when it needs structural understanding. +2. **Hook augmentation** — a PreToolUse hook fires on every Grep/Glob/Bash search, runs a fast BM25 lookup against the graph, and injects related symbols, callers, callees, and execution flows as `additionalContext`. The agent never asks for this; it just appears alongside search results. + +## The Graph + +```mermaid +erDiagram + File ||--o{ Function : DEFINES + File ||--o{ Class : DEFINES + File ||--o{ Interface : DEFINES + Class ||--o{ Method : DEFINES + Function ||--o{ Function : CALLS + Method ||--o{ Function : CALLS + Function ||--o{ File : IMPORTS + Class ||--o{ Class : EXTENDS + Class ||--o{ Interface : IMPLEMENTS + Function }o--|| Community : MEMBER_OF + Function }o--|| Process : STEP_IN_PROCESS +``` + +**Nodes** represent code symbols: `File`, `Folder`, `Function`, `Class`, `Interface`, `Method`, `CodeElement`. Language-specific nodes include `Struct`, `Enum`, `Trait`, `Impl`, etc. + +**Edges** use a single `CodeRelation` table with a `type` property: +- `CALLS` — function/method invocation +- `IMPORTS` — file/module import +- `EXTENDS` / `IMPLEMENTS` — inheritance +- `DEFINES` / `CONTAINS` — structural containment +- `MEMBER_OF` — community membership +- `STEP_IN_PROCESS` — participation in an execution flow (with `step` order) + +**Communities** are auto-detected functional areas (Leiden algorithm). Each community has a `heuristicLabel` (e.g., "Authentication", "Database") and a `cohesion` score measuring internal connectivity density. Same-label communities are aggregated for display; clusters with fewer than 5 symbols are filtered out. + +**Processes** are execution flow traces — ordered sequences of symbols that represent a call chain from entry point to terminal. Each process has a `heuristicLabel`, `processType`, and `stepCount`. A single symbol can participate in multiple processes. + +## MCP Tools + +| Tool | Purpose | +|------|---------| +| `list_repos` | Discover indexed repositories and their stats | +| `query` | Search for execution flows related to a concept (hybrid BM25 + semantic, grouped by process) | +| `context` | 360-degree view of a single symbol — callers, callees, imports, process participation | +| `impact` | Blast radius analysis — what breaks if you change a symbol, grouped by depth | +| `detect_changes` | Map uncommitted git changes to affected symbols and execution flows | +| `rename` | Multi-file coordinated rename using graph refs + text search fallback | +| `cypher` | Raw Cypher queries against the knowledge graph | + +### query + +Returns results grouped by process (execution flow), not by file. Hybrid search combines BM25 keyword matching with semantic vector search, merged via Reciprocal Rank Fusion. + +```json +{ + "processes": [ + { "summary": "UserAuthentication", "priority": 0.033, "symbol_count": 4, "process_type": "request_handler", "step_count": 7 } + ], + "process_symbols": [ + { "name": "validateUser", "type": "Function", "filePath": "src/auth/validator.ts", "startLine": 15, "step_index": 3 } + ], + "definitions": [ + { "name": "AuthConfig", "type": "Interface", "filePath": "src/auth/types.ts" } + ] +} +``` + +### context + +Categorized 360-degree view with disambiguation for common names. + +```json +{ + "status": "found", + "symbol": { "uid": "Function:src/auth/validator.ts:validateUser", "name": "validateUser", "kind": "Function", "filePath": "src/auth/validator.ts", "startLine": 15, "endLine": 42 }, + "incoming": { + "calls": [ + { "uid": "Function:src/routes/login.ts:handleLogin", "name": "handleLogin", "filePath": "src/routes/login.ts" } + ], + "imports": [ + { "uid": "File:src/routes/login.ts", "name": "login.ts", "filePath": "src/routes/login.ts" } + ] + }, + "outgoing": { + "calls": [ + { "uid": "Function:src/db/users.ts:findByEmail", "name": "findByEmail", "filePath": "src/db/users.ts" } + ] + }, + "processes": [ + { "id": "proc_auth_login", "name": "UserAuthentication", "step_index": 3, "step_count": 7 } + ] +} +``` + +### impact + +Blast radius grouped by depth: d=1 will break, d=2 likely affected, d=3 may need testing. + +```json +{ + "target": { "name": "validateUser", "type": "Function", "filePath": "src/auth/validator.ts" }, + "direction": "upstream", + "impactedCount": 5, + "byDepth": { + "1": [{ "name": "handleLogin", "type": "Function", "filePath": "src/routes/login.ts", "relationType": "CALLS", "confidence": 1.0 }], + "2": [{ "name": "authRouter", "type": "Function", "filePath": "src/routes/index.ts", "relationType": "CALLS", "confidence": 1.0 }] + } +} +``` + +### detect_changes + +Maps git diff to indexed symbols and traces which execution flows are impacted. + +```json +{ + "summary": { "changed_count": 3, "affected_count": 2, "changed_files": 1, "risk_level": "medium" }, + "changed_symbols": [ + { "name": "validateUser", "type": "Function", "filePath": "src/auth/validator.ts", "change_type": "Modified" } + ], + "affected_processes": [ + { "name": "UserAuthentication", "process_type": "request_handler", "step_count": 7, "changed_steps": [{ "symbol": "validateUser", "step": 3 }] } + ] +} +``` + +### rename + +Graph-based rename with text search fallback. Edits tagged by confidence source. + +```json +{ + "status": "success", + "old_name": "validateUser", + "new_name": "verifyUser", + "files_affected": 4, + "total_edits": 7, + "graph_edits": 5, + "text_search_edits": 2, + "changes": [ + { "file_path": "src/auth/validator.ts", "edits": [{ "line": 15, "old_text": "function validateUser(", "new_text": "function verifyUser(", "confidence": "graph" }] }, + { "file_path": "src/tests/auth.test.ts", "edits": [{ "line": 8, "old_text": "validateUser(testInput)", "new_text": "verifyUser(testInput)", "confidence": "text_search" }] } + ], + "applied": false +} +``` + +## Hook Augmentation + +The hook intercepts every search the agent runs and silently enriches it with graph context. The agent doesn't request this — it just sees richer results. + +### Flow + +```mermaid +sequenceDiagram + participant Agent + participant Claude Code + participant Hook as gitnexus-hook.js + participant CLI as gitnexus augment + participant Graph as KuzuDB + + Agent->>Claude Code: Grep { pattern: "validateUser" } + Claude Code->>Hook: PreToolUse event (stdin JSON) + Hook->>Hook: Extract pattern from tool_input + Hook->>CLI: gitnexus augment "validateUser" + CLI->>Graph: BM25 search → symbol lookup → callers/callees/processes + Graph-->>CLI: enriched results + CLI-->>Hook: structured text (stdout) + Hook-->>Claude Code: { additionalContext: "..." } + Claude Code->>Agent: Grep results + additionalContext +``` + +### What the hook receives + +```json +{ + "hook_event_name": "PreToolUse", + "tool_name": "Grep", + "tool_input": { "pattern": "validateUser", "path": "src/" }, + "cwd": "/home/user/project" +} +``` + +### What the hook returns + +```json +{ + "hookSpecificOutput": { + "hookEventName": "PreToolUse", + "additionalContext": "[GitNexus] 3 related symbols found:\n\nvalidateUser (src/auth/validator.ts)\n Called by: handleLogin, authMiddleware\n Calls: findByEmail, hashPassword\n Flows: UserAuthentication (step 3/7)\n\nauthMiddleware (src/middleware/auth.ts)\n Called by: router.use\n Calls: validateUser, getSession\n Flows: RequestPipeline (step 2/5)" + } +} +``` + +### Before and after + +**Without augmentation** — the agent sees only grep matches: + +``` +src/auth/validator.ts:15: export function validateUser(email: string, password: string) { +src/routes/login.ts:23: const user = await validateUser(req.body.email, req.body.password); +src/tests/auth.test.ts:8: const result = validateUser("test@example.com", "password123"); +``` + +**With augmentation** — the same grep results arrive with additional context appended: + +``` +src/auth/validator.ts:15: export function validateUser(email: string, password: string) { +src/routes/login.ts:23: const user = await validateUser(req.body.email, req.body.password); +src/tests/auth.test.ts:8: const result = validateUser("test@example.com", "password123"); + +[GitNexus] 3 related symbols found: + +validateUser (src/auth/validator.ts) + Called by: handleLogin, authMiddleware + Calls: findByEmail, hashPassword + Flows: UserAuthentication (step 3/7) + +authMiddleware (src/middleware/auth.ts) + Called by: router.use + Calls: validateUser, getSession + Flows: RequestPipeline (step 2/5) + +handleLogin (src/routes/login.ts) + Called by: authRouter + Calls: validateUser, createSession + Flows: UserAuthentication (step 4/7) +``` + +The agent now knows the call chain, related symbols it hasn't searched for yet, and which execution flows are involved — without making any explicit tool call. + +### Pattern extraction + +The hook extracts search patterns from different tool types: +- **Grep** → uses `pattern` directly +- **Glob** → extracts meaningful name segments from glob patterns +- **Bash** → detects `rg`/`grep` commands and extracts the pattern argument, skipping flags + +Patterns shorter than 3 characters are ignored. If no `.gitnexus` index exists in the working directory ancestry, the hook exits silently. + +## Internal Ranking + +### Clusters as a hidden signal + +Communities (clusters) detected by the Leiden algorithm have a `cohesion` score measuring internal connectivity density. This score is used **exclusively as an internal ranking signal** — it is never exposed in tool output or augmentation text. + +In `query`, cluster cohesion provides a subtle ranking boost: +``` +priority = aggregate_rrf_score + (cohesion * 0.1) +``` + +In augmentation, results are sorted by their cluster cohesion before formatting. The effect: symbols from tightly-connected functional areas rank higher, but the agent never sees "cluster" or "cohesion" anywhere — it just gets better-ordered results. + +### Processes as result grouping + +Processes serve as the primary grouping mechanism for `query` results. Instead of returning a flat list of symbol matches, results are organized by execution flow: + +1. Hybrid search finds matching symbols +2. Each symbol is traced to its process(es) via `STEP_IN_PROCESS` edges +3. Processes are ranked by aggregate relevance score (with cohesion boost) +4. Results arrive grouped: "here are the execution flows related to your query, with the symbols in each" + +Symbols not belonging to any process fall into a `definitions` bucket (types, interfaces, standalone declarations). + +## Platform Integration + +```mermaid +graph TD + subgraph "gitnexus setup" + S[setup.ts] + end + + subgraph Claude Code + S --> CC_MCP["claude mcp add gitnexus"] + S --> CC_HOOKS["~/.claude/settings.json
(PreToolUse hooks)"] + S --> CC_SKILLS["~/.claude/skills/
(exploring, debugging, etc.)"] + end + + subgraph "Claude Code Plugin" + P["--plugin-dir gitnexus-claude-plugin/"] + P --> P_HOOKS["hooks/hooks.json
(PreToolUse)"] + P --> P_SKILLS["skills/
(auto-namespaced)"] + end + + subgraph Cursor + S --> CU_MCP["~/.cursor/mcp.json"] + S --> CU_SKILLS["~/.cursor/skills/"] + end + + subgraph OpenCode + S --> OC_MCP["~/.config/opencode/config.json"] + S --> OC_SKILLS["~/.config/opencode/skill/"] + end +``` + +**Two install paths for Claude Code:** + +1. **`gitnexus setup`** — copies hook scripts to `~/.claude/hooks/gitnexus/`, merges PreToolUse config into `~/.claude/settings.json`, installs skills to `~/.claude/skills/`. Works globally across all projects. + +2. **Plugin mode** (`claude --plugin-dir gitnexus-claude-plugin/`) — self-contained directory with `hooks/hooks.json` and skills. Uses `${CLAUDE_PLUGIN_ROOT}` env var for script paths. Skills are auto-namespaced (e.g., `/gitnexus:exploring`). + +Both paths install the same 4 skills: `exploring`, `debugging`, `impact-analysis`, `refactoring`. + +**Cursor** gets MCP config at `~/.cursor/mcp.json` and skills at `~/.cursor/skills/`. + +**OpenCode** gets MCP config at `~/.config/opencode/config.json` and skills at `~/.config/opencode/skill/`. + +All MCP entries point to the same server: `npx -y gitnexus@latest mcp`. + +## UX Perspective + +From the agent's point of view, GitNexus operates on two levels: + +**Invisible enrichment** — Every search the agent runs (grep for a function name, glob for a file pattern, bash with ripgrep) gets silently augmented. The agent sees its normal search results plus a `[GitNexus]` block listing related symbols, their callers/callees, and which execution flows they participate in. This happens on every search without the agent doing anything special. The effect is that the agent naturally discovers related code it wouldn't have found from text search alone. + +**Explicit tools for deep dives** — When the agent needs structural understanding (not just "find where X is used" but "what happens if I change X"), it uses the MCP tools directly: +- `query` to find execution flows related to a concept +- `context` to get the full picture of a specific symbol +- `impact` to assess blast radius before making changes +- `detect_changes` to understand what uncommitted work affects +- `rename` to safely rename across the codebase with graph-backed confidence + +The invisible layer handles the 80% case (richer search context), while the explicit tools handle the 20% (deep structural questions). The agent doesn't need to decide when to use GitNexus for search enrichment — it just happens. diff --git a/gitnexus/hooks/claude/gitnexus-hook.js b/gitnexus/hooks/claude/gitnexus-hook.js new file mode 100644 index 000000000..67c890ff5 --- /dev/null +++ b/gitnexus/hooks/claude/gitnexus-hook.js @@ -0,0 +1,123 @@ +#!/usr/bin/env node +/** + * GitNexus Claude Code Hook + * + * PreToolUse handler — intercepts Grep/Glob/Bash searches + * and augments with graph context from the GitNexus index. + * + * NOTE: SessionStart hooks are broken on Windows (Claude Code bug). + * Session context is injected via CLAUDE.md / skills instead. + */ + +const fs = require('fs'); +const path = require('path'); +const { execFileSync } = require('child_process'); + +/** + * Read JSON input from stdin synchronously. + */ +function readInput() { + try { + const data = fs.readFileSync(0, 'utf-8'); + return JSON.parse(data); + } catch { + return {}; + } +} + +/** + * Check if a directory (or ancestor) has a .gitnexus index. + */ +function findGitNexusIndex(startDir) { + let dir = startDir || process.cwd(); + for (let i = 0; i < 5; i++) { + if (fs.existsSync(path.join(dir, '.gitnexus'))) { + return true; + } + const parent = path.dirname(dir); + if (parent === dir) break; + dir = parent; + } + return false; +} + +/** + * Extract search pattern from tool input. + */ +function extractPattern(toolName, toolInput) { + if (toolName === 'Grep') { + return toolInput.pattern || null; + } + + if (toolName === 'Glob') { + const raw = toolInput.pattern || ''; + const match = raw.match(/[*\/]([a-zA-Z][a-zA-Z0-9_-]{2,})/); + return match ? match[1] : null; + } + + if (toolName === 'Bash') { + const cmd = toolInput.command || ''; + if (!/\brg\b|\bgrep\b/.test(cmd)) return null; + + const tokens = cmd.split(/\s+/); + let foundCmd = false; + let skipNext = false; + const flagsWithValues = new Set(['-e', '-f', '-m', '-A', '-B', '-C', '-g', '--glob', '-t', '--type', '--include', '--exclude']); + + for (const token of tokens) { + if (skipNext) { skipNext = false; continue; } + if (!foundCmd) { + if (/\brg$|\bgrep$/.test(token)) foundCmd = true; + continue; + } + if (token.startsWith('-')) { + if (flagsWithValues.has(token)) skipNext = true; + continue; + } + const cleaned = token.replace(/['"]/g, ''); + return cleaned.length >= 3 ? cleaned : null; + } + return null; + } + + return null; +} + +function main() { + try { + const input = readInput(); + const hookEvent = input.hook_event_name || ''; + + if (hookEvent !== 'PreToolUse') return; + + const cwd = input.cwd || process.cwd(); + if (!findGitNexusIndex(cwd)) return; + + const toolName = input.tool_name || ''; + const toolInput = input.tool_input || {}; + + if (toolName !== 'Grep' && toolName !== 'Glob' && toolName !== 'Bash') return; + + const pattern = extractPattern(toolName, toolInput); + if (!pattern || pattern.length < 3) return; + + const result = execFileSync( + 'gitnexus', + ['augment', pattern], + { encoding: 'utf-8', timeout: 8000, cwd, stdio: ['pipe', 'pipe', 'pipe'] } + ); + + if (result && result.trim()) { + console.log(JSON.stringify({ + hookSpecificOutput: { + hookEventName: 'PreToolUse', + additionalContext: result.trim() + } + })); + } + } catch { + // Graceful failure + } +} + +main(); diff --git a/gitnexus/hooks/claude/pre-tool-use.sh b/gitnexus/hooks/claude/pre-tool-use.sh new file mode 100644 index 000000000..3c1af3bc0 --- /dev/null +++ b/gitnexus/hooks/claude/pre-tool-use.sh @@ -0,0 +1,78 @@ +#!/bin/bash +# GitNexus PreToolUse hook for Claude Code +# Intercepts Grep/Glob/Bash searches and augments with graph context. +# Receives JSON on stdin with { tool_name, tool_input, cwd, ... } +# Returns JSON with additionalContext for graph-enriched results. + +INPUT=$(cat) + +TOOL_NAME=$(echo "$INPUT" | jq -r '.tool_name // empty' 2>/dev/null) +CWD=$(echo "$INPUT" | jq -r '.cwd // empty' 2>/dev/null) + +# Extract search pattern based on tool type +PATTERN="" + +case "$TOOL_NAME" in + Grep) + PATTERN=$(echo "$INPUT" | jq -r '.tool_input.pattern // empty' 2>/dev/null) + ;; + Glob) + # Glob patterns are file paths, not search terms — extract meaningful part + RAW=$(echo "$INPUT" | jq -r '.tool_input.pattern // empty' 2>/dev/null) + # Strip glob syntax to get the meaningful name (e.g., "**/*.ts" → skip, "auth*.ts" → "auth") + PATTERN=$(echo "$RAW" | sed -n 's/.*[*\/]\([a-zA-Z][a-zA-Z0-9_-]*\).*/\1/p') + ;; + Bash) + CMD=$(echo "$INPUT" | jq -r '.tool_input.command // empty' 2>/dev/null) + # Only augment grep/rg commands + if echo "$CMD" | grep -qE '\brg\b|\bgrep\b'; then + # Extract pattern from rg/grep + if echo "$CMD" | grep -qE '\brg\b'; then + PATTERN=$(echo "$CMD" | sed -n "s/.*\brg\s\+\(--[^ ]*\s\+\)*['\"]\\?\([^'\";\| >]*\\).*/\2/p") + elif echo "$CMD" | grep -qE '\bgrep\b'; then + PATTERN=$(echo "$CMD" | sed -n "s/.*\bgrep\s\+\(-[^ ]*\s\+\)*['\"]\\?\([^'\";\| >]*\\).*/\2/p") + fi + fi + ;; + *) + # Not a search tool — skip + exit 0 + ;; +esac + +# Skip if pattern too short or empty +if [ -z "$PATTERN" ] || [ ${#PATTERN} -lt 3 ]; then + exit 0 +fi + +# Check if we're in a GitNexus-indexed repo +dir="${CWD:-$PWD}" +found=false +for i in 1 2 3 4 5; do + if [ -d "$dir/.gitnexus" ]; then + found=true + break + fi + parent="$(dirname "$dir")" + [ "$parent" = "$dir" ] && break + dir="$parent" +done + +if [ "$found" = false ]; then + exit 0 +fi + +# Run gitnexus augment — must be fast (<500ms target) +RESULT=$(cd "$CWD" && npx -y gitnexus augment "$PATTERN" 2>/dev/null) + +if [ -n "$RESULT" ]; then + ESCAPED=$(echo "$RESULT" | jq -Rs .) + jq -n --argjson ctx "$ESCAPED" '{ + hookSpecificOutput: { + hookEventName: "PreToolUse", + additionalContext: $ctx + } + }' +else + exit 0 +fi diff --git a/gitnexus/hooks/claude/session-start.sh b/gitnexus/hooks/claude/session-start.sh new file mode 100644 index 000000000..8960dd376 --- /dev/null +++ b/gitnexus/hooks/claude/session-start.sh @@ -0,0 +1,42 @@ +#!/bin/bash +# GitNexus SessionStart hook for Claude Code +# Fires on session startup. Stdout is injected into Claude's context. +# Checks if the current directory has a GitNexus index. + +dir="$PWD" +found=false +for i in 1 2 3 4 5; do + if [ -d "$dir/.gitnexus" ]; then + found=true + break + fi + parent="$(dirname "$dir")" + [ "$parent" = "$dir" ] && break + dir="$parent" +done + +if [ "$found" = false ]; then + exit 0 +fi + +# Inject GitNexus context — this stdout goes directly into Claude's context +cat << 'EOF' +## GitNexus Code Intelligence + +This codebase is indexed by GitNexus, providing a knowledge graph with execution flows, relationships, and semantic search. + +**Available MCP Tools:** +- `query` — Process-grouped code intelligence (execution flows related to a concept) +- `context` — 360-degree symbol view (categorized refs, process participation) +- `impact` — Blast radius analysis (what breaks if you change a symbol) +- `detect_changes` — Git-diff impact analysis (what do your changes affect) +- `rename` — Multi-file coordinated rename with confidence tags +- `cypher` — Raw graph queries +- `list_repos` — Discover indexed repos + +**Quick Start:** READ `gitnexus://repo/{name}/context` for codebase overview, then use `query` to find execution flows. + +**Resources:** `gitnexus://repo/{name}/context` (overview), `/processes` (execution flows), `/schema` (for Cypher) +EOF + +exit 0 diff --git a/gitnexus/package.json b/gitnexus/package.json index 381fc9749..03ba76d75 100644 --- a/gitnexus/package.json +++ b/gitnexus/package.json @@ -31,6 +31,7 @@ }, "files": [ "dist", + "hooks", "skills", "vendor" ], diff --git a/gitnexus/skills/debugging.md b/gitnexus/skills/debugging.md index 316b0aa3e..3b945835b 100644 --- a/gitnexus/skills/debugging.md +++ b/gitnexus/skills/debugging.md @@ -15,8 +15,8 @@ description: Trace bugs through call chains using knowledge graph ## Workflow ``` -1. gitnexus_search({query: ""}) → Find related code -2. gitnexus_explore({name: "", type: "symbol"}) → See callers/callees +1. gitnexus_query({query: ""}) → Find related execution flows +2. gitnexus_context({name: ""}) → See callers/callees/processes 3. READ gitnexus://repo/{name}/process/{name} → Trace execution flow 4. gitnexus_cypher({query: "MATCH path..."}) → Custom traces if needed ``` @@ -27,9 +27,9 @@ description: Trace bugs through call chains using knowledge graph ``` - [ ] Understand the symptom (error message, unexpected behavior) -- [ ] gitnexus_search for error text or related code -- [ ] Identify the suspect function -- [ ] gitnexus_explore to see callers and callees +- [ ] gitnexus_query for error text or related code +- [ ] Identify the suspect function from returned processes +- [ ] gitnexus_context to see callers and callees - [ ] Trace execution flow via process resource if applicable - [ ] gitnexus_cypher for custom call chain traces if needed - [ ] Read source files to confirm root cause @@ -39,26 +39,27 @@ description: Trace bugs through call chains using knowledge graph | Symptom | GitNexus Approach | |---------|-------------------| -| Error message | `gitnexus_search` for error text → `explore` throw sites | -| Wrong return value | `explore` the function → trace callees for data flow | -| Intermittent failure | `explore` → look for external calls, async deps | -| Performance issue | `explore` → find symbols with many callers (hot paths) | -| Recent regression | `gitnexus_impact` on recently changed symbols | +| Error message | `gitnexus_query` for error text → `context` on throw sites | +| Wrong return value | `context` on the function → trace callees for data flow | +| Intermittent failure | `context` → look for external calls, async deps | +| Performance issue | `context` → find symbols with many callers (hot paths) | +| Recent regression | `detect_changes` to see what your changes affect | ## Tools -**gitnexus_search** — find code related to error: +**gitnexus_query** — find code related to error: ``` -gitnexus_search({query: "payment validation error", depth: "full"}) -→ validatePayment, handlePaymentError, PaymentException +gitnexus_query({query: "payment validation error"}) +→ Processes: CheckoutFlow, ErrorHandling +→ Symbols: validatePayment, handlePaymentError, PaymentException ``` -**gitnexus_explore** — full context for a suspect: +**gitnexus_context** — full context for a suspect: ``` -gitnexus_explore({name: "validatePayment", type: "symbol"}) -→ Callers: processCheckout, webhookHandler -→ Callees: verifyCard, fetchRates (external API!) -→ Cluster: Payment +gitnexus_context({name: "validatePayment"}) +→ Incoming calls: processCheckout, webhookHandler +→ Outgoing calls: verifyCard, fetchRates (external API!) +→ Processes: CheckoutFlow (step 3/7) ``` **gitnexus_cypher** — custom call chain traces: @@ -70,11 +71,12 @@ RETURN [n IN nodes(path) | n.name] AS chain ## Example: "Payment endpoint returns 500 intermittently" ``` -1. gitnexus_search({query: "payment error handling"}) - → validatePayment, handlePaymentError, PaymentException +1. gitnexus_query({query: "payment error handling"}) + → Processes: CheckoutFlow, ErrorHandling + → Symbols: validatePayment, handlePaymentError -2. gitnexus_explore({name: "validatePayment", type: "symbol"}) - → Callees: verifyCard, fetchRates (external API!) +2. gitnexus_context({name: "validatePayment"}) + → Outgoing calls: verifyCard, fetchRates (external API!) 3. READ gitnexus://repo/my-app/process/CheckoutFlow → Step 3: validatePayment → calls fetchRates (external) diff --git a/gitnexus/skills/exploring.md b/gitnexus/skills/exploring.md index 70a74b0fb..2214c289c 100644 --- a/gitnexus/skills/exploring.md +++ b/gitnexus/skills/exploring.md @@ -17,9 +17,9 @@ description: Navigate unfamiliar code using GitNexus knowledge graph ``` 1. READ gitnexus://repos → Discover indexed repos 2. READ gitnexus://repo/{name}/context → Codebase overview, check staleness -3. READ gitnexus://repo/{name}/clusters → See all functional areas -4. READ gitnexus://repo/{name}/cluster/{name} → Drill into relevant cluster -5. gitnexus_explore({name, type: "symbol"}) → Deep dive on specific symbol +3. gitnexus_query({query: ""}) → Find related execution flows +4. gitnexus_context({name: ""}) → Deep dive on specific symbol +5. READ gitnexus://repo/{name}/process/{name} → Trace full execution flow ``` > If step 2 says "Index is stale" → run `npx gitnexus analyze` in terminal. @@ -27,12 +27,11 @@ description: Navigate unfamiliar code using GitNexus knowledge graph ## Checklist ``` -- [ ] READ gitnexus://repos - [ ] READ gitnexus://repo/{name}/context -- [ ] READ gitnexus://repo/{name}/clusters -- [ ] Identify the relevant cluster -- [ ] READ gitnexus://repo/{name}/cluster/{name} -- [ ] gitnexus_explore for key symbols +- [ ] gitnexus_query for the concept you want to understand +- [ ] Review returned processes (execution flows) +- [ ] gitnexus_context on key symbols for callers/callees +- [ ] READ process resource for full execution traces - [ ] Read source files for implementation details ``` @@ -41,33 +40,36 @@ description: Navigate unfamiliar code using GitNexus knowledge graph | Resource | What you get | |----------|-------------| | `gitnexus://repo/{name}/context` | Stats, staleness warning (~150 tokens) | -| `gitnexus://repo/{name}/clusters` | All clusters with cohesion scores (~300 tokens) | -| `gitnexus://repo/{name}/cluster/{name}` | Cluster members with file paths (~500 tokens) | +| `gitnexus://repo/{name}/clusters` | All functional areas with cohesion scores (~300 tokens) | +| `gitnexus://repo/{name}/cluster/{name}` | Area members with file paths (~500 tokens) | | `gitnexus://repo/{name}/process/{name}` | Step-by-step execution trace (~200 tokens) | ## Tools -**gitnexus_explore** — symbol context with callers/callees: +**gitnexus_query** — find execution flows related to a concept: ``` -gitnexus_explore({name: "validateUser", type: "symbol"}) -→ Callers: loginHandler, apiMiddleware -→ Callees: checkToken, getUserById -→ Cluster: Auth (92% cohesion) +gitnexus_query({query: "payment processing"}) +→ Processes: CheckoutFlow, RefundFlow, WebhookHandler +→ Symbols grouped by flow with file locations ``` -**gitnexus_search** — find code by query when you don't know the cluster: +**gitnexus_context** — 360-degree view of a symbol: ``` -gitnexus_search({query: "payment validation", depth: "full"}) +gitnexus_context({name: "validateUser"}) +→ Incoming calls: loginHandler, apiMiddleware +→ Outgoing calls: checkToken, getUserById +→ Processes: LoginFlow (step 2/5), TokenRefresh (step 1/3) ``` ## Example: "How does payment processing work?" ``` -1. READ gitnexus://repo/my-app/context → 918 symbols, 12 clusters -2. READ gitnexus://repo/my-app/clusters → Auth, Payment, Database, API... -3. READ gitnexus://repo/my-app/cluster/Payment → processPayment, validateCard, PaymentService -4. gitnexus_explore({name: "processPayment", type: "symbol"}) - → Callers: checkoutHandler, webhookHandler - → Callees: validateCard, chargeStripe, saveTransaction -5. Read src/payments/processor.ts for implementation details +1. READ gitnexus://repo/my-app/context → 918 symbols, 45 processes +2. gitnexus_query({query: "payment processing"}) + → CheckoutFlow: processPayment → validateCard → chargeStripe + → RefundFlow: initiateRefund → calculateRefund → processRefund +3. gitnexus_context({name: "processPayment"}) + → Incoming: checkoutHandler, webhookHandler + → Outgoing: validateCard, chargeStripe, saveTransaction +4. Read src/payments/processor.ts for implementation details ``` diff --git a/gitnexus/skills/impact-analysis.md b/gitnexus/skills/impact-analysis.md index 7cedd2b97..bb5f51fcc 100644 --- a/gitnexus/skills/impact-analysis.md +++ b/gitnexus/skills/impact-analysis.md @@ -11,13 +11,14 @@ description: Analyze blast radius before making code changes - "Show me the blast radius" - "Who uses this code?" - Before making non-trivial code changes +- Before committing — to understand what your changes affect ## Workflow ``` 1. gitnexus_impact({target: "X", direction: "upstream"}) → What depends on this -2. READ gitnexus://repo/{name}/clusters → Check which areas are affected -3. READ gitnexus://repo/{name}/processes → Check affected execution flows +2. READ gitnexus://repo/{name}/processes → Check affected execution flows +3. gitnexus_detect_changes() → Map current git changes to affected flows 4. Assess risk and report to user ``` @@ -29,9 +30,8 @@ description: Analyze blast radius before making code changes - [ ] gitnexus_impact({target, direction: "upstream"}) to find dependents - [ ] Review d=1 items first (these WILL BREAK) - [ ] Check high-confidence (>0.8) dependencies -- [ ] READ clusters to understand which areas are affected -- [ ] Count affected clusters (cross-cutting = higher risk) - [ ] READ processes to check affected execution flows +- [ ] gitnexus_detect_changes() for pre-commit check - [ ] Assess risk level and report to user ``` @@ -47,14 +47,14 @@ description: Analyze blast radius before making code changes | Affected | Risk | |----------|------| -| <5 symbols, 1 cluster | LOW | -| 5-15 symbols, 1-2 clusters | MEDIUM | -| >15 symbols or 3+ clusters | HIGH | +| <5 symbols, few processes | LOW | +| 5-15 symbols, 2-5 processes | MEDIUM | +| >15 symbols or many processes | HIGH | | Critical path (auth, payments) | CRITICAL | ## Tools -**gitnexus_impact** — the primary tool: +**gitnexus_impact** — the primary tool for symbol blast radius: ``` gitnexus_impact({ target: "validateUser", @@ -69,9 +69,15 @@ gitnexus_impact({ → d=2 (LIKELY AFFECTED): - authRouter (src/routes/auth.ts:22) [CALLS, 95%] +``` -→ Affected Processes: LoginFlow, TokenRefresh -→ Risk: MEDIUM (3 processes) +**gitnexus_detect_changes** — git-diff based impact analysis: +``` +gitnexus_detect_changes({scope: "staged"}) + +→ Changed: 5 symbols in 3 files +→ Affected: LoginFlow, TokenRefresh, APIMiddlewarePipeline +→ Risk: MEDIUM ``` ## Example: "What breaks if I change validateUser?" @@ -81,8 +87,8 @@ gitnexus_impact({ → d=1: loginHandler, apiMiddleware (WILL BREAK) → d=2: authRouter, sessionManager (LIKELY AFFECTED) -2. READ gitnexus://repo/my-app/clusters - → Auth and API clusters affected (2 clusters) +2. READ gitnexus://repo/my-app/processes + → LoginFlow and TokenRefresh touch validateUser -3. Risk: 2 direct callers, 2 clusters = MEDIUM +3. Risk: 2 direct callers, 2 processes = MEDIUM ``` diff --git a/gitnexus/skills/refactoring.md b/gitnexus/skills/refactoring.md index f98c4b34e..23f4d1130 100644 --- a/gitnexus/skills/refactoring.md +++ b/gitnexus/skills/refactoring.md @@ -16,8 +16,8 @@ description: Plan safe refactors using blast radius and dependency mapping ``` 1. gitnexus_impact({target: "X", direction: "upstream"}) → Map all dependents -2. gitnexus_search({query: "X"}) → Find string/dynamic references -3. READ gitnexus://repo/{name}/cluster/{name} → Check cohesion impact +2. gitnexus_query({query: "X"}) → Find execution flows involving X +3. gitnexus_context({name: "X"}) → See all incoming/outgoing refs 4. Plan update order: interfaces → implementations → callers → tests ``` @@ -27,36 +27,44 @@ description: Plan safe refactors using blast radius and dependency mapping ### Rename Symbol ``` -- [ ] gitnexus_impact({target: oldName, direction: "upstream"}) — find all callers -- [ ] gitnexus_search({query: oldName}) — find string literals and dynamic references -- [ ] Check for reflection/dynamic invocation patterns -- [ ] Plan update order: interface → implementation → callers → tests -- [ ] Update all d=1 (WILL BREAK) items +- [ ] gitnexus_rename({symbol_name: "oldName", new_name: "newName", dry_run: true}) — preview all edits +- [ ] Review graph edits (high confidence) and ast_search edits (review carefully) +- [ ] If satisfied: gitnexus_rename({..., dry_run: false}) — apply edits +- [ ] gitnexus_detect_changes() — verify only expected files changed - [ ] Run tests for affected processes ``` ### Extract Module ``` -- [ ] gitnexus_explore({name: target, type: "symbol"}) — map internal dependencies +- [ ] gitnexus_context({name: target}) — see all incoming/outgoing refs - [ ] gitnexus_impact({target, direction: "upstream"}) — find all external callers -- [ ] READ cluster resource — check if extraction preserves cohesion - [ ] Define new module interface - [ ] Extract code, update imports +- [ ] gitnexus_detect_changes() — verify affected scope - [ ] Run tests for affected processes ``` ### Split Function/Service ``` -- [ ] gitnexus_explore({name: target, type: "symbol"}) — understand all callees -- [ ] Group callees by responsibility/domain +- [ ] gitnexus_context({name: target}) — understand all callees +- [ ] Group callees by responsibility - [ ] gitnexus_impact({target, direction: "upstream"}) — map callers to update - [ ] Create new functions/services - [ ] Update callers +- [ ] gitnexus_detect_changes() — verify affected scope - [ ] Run tests for affected processes ``` ## Tools +**gitnexus_rename** — automated multi-file rename: +``` +gitnexus_rename({symbol_name: "validateUser", new_name: "authenticateUser", dry_run: true}) +→ 12 edits across 8 files +→ 10 graph edits (high confidence), 2 ast_search edits (review) +→ Changes: [{file_path, edits: [{line, old_text, new_text, confidence}]}] +``` + **gitnexus_impact** — map all dependents first: ``` gitnexus_impact({target: "validateUser", direction: "upstream"}) @@ -64,10 +72,12 @@ gitnexus_impact({target: "validateUser", direction: "upstream"}) → Affected Processes: LoginFlow, TokenRefresh ``` -**gitnexus_search** — find string/dynamic references impact() might miss: +**gitnexus_detect_changes** — verify your changes after refactoring: ``` -gitnexus_search({query: "validateUser"}) -→ Found in: config.json (dynamic reference!), test fixtures +gitnexus_detect_changes({scope: "all"}) +→ Changed: 8 files, 12 symbols +→ Affected processes: LoginFlow, TokenRefresh +→ Risk: MEDIUM ``` **gitnexus_cypher** — custom reference queries: @@ -80,23 +90,24 @@ RETURN caller.name, caller.filePath ORDER BY caller.filePath | Risk Factor | Mitigation | |-------------|------------| -| Many callers (>5) | Update in small batches | -| Cross-cluster refs | Coordinate with affected areas | -| String/dynamic refs | `gitnexus_search` to find them | +| Many callers (>5) | Use gitnexus_rename for automated updates | +| Cross-area refs | Use detect_changes after to verify scope | +| String/dynamic refs | gitnexus_query to find them | | External/public API | Version and deprecate properly | ## Example: Rename `validateUser` to `authenticateUser` ``` -1. gitnexus_impact({target: "validateUser", direction: "upstream"}) - → d=1: loginHandler, apiMiddleware, testUtils +1. gitnexus_rename({symbol_name: "validateUser", new_name: "authenticateUser", dry_run: true}) + → 12 edits: 10 graph (safe), 2 ast_search (review) + → Files: validator.ts, login.ts, middleware.ts, config.json... -2. gitnexus_search({query: "validateUser"}) - → Found in: config.json (dynamic reference!) +2. Review ast_search edits (config.json: dynamic reference!) -3. Plan update order: - 1. Update declaration in src/auth/validator.ts - 2. Update config.json string reference - 3. Update loginHandler, apiMiddleware, testUtils - 4. Run tests for LoginFlow, TokenRefresh +3. gitnexus_rename({symbol_name: "validateUser", new_name: "authenticateUser", dry_run: false}) + → Applied 12 edits across 8 files + +4. gitnexus_detect_changes({scope: "all"}) + → Affected: LoginFlow, TokenRefresh + → Risk: MEDIUM — run tests for these flows ``` diff --git a/gitnexus/src/cli/ai-context.ts b/gitnexus/src/cli/ai-context.ts index 3ac073e6a..6f8d1ede6 100644 --- a/gitnexus/src/cli/ai-context.ts +++ b/gitnexus/src/cli/ai-context.ts @@ -37,13 +37,12 @@ const GITNEXUS_END_MARKER = ''; * - Tools/Resources sections are labeled "Reference" — agents treat them as lookup, not workflow */ function generateGitNexusContent(projectName: string, stats: RepoStats): string { - const clusterCount = stats.clusters || stats.communities || 0; return `${GITNEXUS_START_MARKER} # GitNexus MCP -This project is indexed by GitNexus as **${projectName}** (${stats.nodes || 0} symbols, ${stats.edges || 0} relationships, ${clusterCount} clusters, ${stats.processes || 0} processes). +This project is indexed by GitNexus as **${projectName}** (${stats.nodes || 0} symbols, ${stats.edges || 0} relationships, ${stats.processes || 0} 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 @@ -68,10 +67,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 | @@ -82,8 +82,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/gitnexus/src/cli/augment.ts b/gitnexus/src/cli/augment.ts new file mode 100644 index 000000000..a3efbef16 --- /dev/null +++ b/gitnexus/src/cli/augment.ts @@ -0,0 +1,32 @@ +/** + * Augment CLI Command + * + * Fast-path command for platform hooks. + * Shells out from Claude Code PreToolUse / Cursor beforeShellExecution hooks. + * + * Usage: gitnexus augment + * Returns enriched text to stdout. + * + * Performance: Must cold-start fast (<500ms). + * Skips unnecessary initialization (no web server, no full DB warmup). + */ + +import { augment } from '../core/augmentation/engine.js'; + +export async function augmentCommand(pattern: string): Promise { + if (!pattern || pattern.length < 3) { + // Too short to be useful — exit silently + process.exit(0); + } + + try { + const result = await augment(pattern, process.cwd()); + + if (result) { + process.stdout.write(result + '\n'); + } + } catch { + // Graceful failure — never break the calling hook + process.exit(0); + } +} diff --git a/gitnexus/src/cli/index.ts b/gitnexus/src/cli/index.ts index 956307d3e..0052ae0b8 100644 --- a/gitnexus/src/cli/index.ts +++ b/gitnexus/src/cli/index.ts @@ -7,6 +7,7 @@ import { statusCommand } from './status.js'; import { mcpCommand } from './mcp.js'; import { cleanCommand } from './clean.js'; import { setupCommand } from './setup.js'; +import { augmentCommand } from './augment.js'; const program = new Command(); program @@ -54,4 +55,9 @@ program .option('--all', 'Clean all indexed repos') .action(cleanCommand); +program + .command('augment ') + .description('Augment a search pattern with knowledge graph context (used by hooks)') + .action(augmentCommand); + program.parse(process.argv); diff --git a/gitnexus/src/cli/setup.ts b/gitnexus/src/cli/setup.ts index 8715eb79c..fd5703a1a 100644 --- a/gitnexus/src/cli/setup.ts +++ b/gitnexus/src/cli/setup.ts @@ -9,8 +9,12 @@ import fs from 'fs/promises'; import path from 'path'; import os from 'os'; +import { fileURLToPath } from 'url'; import { getGlobalDir } from '../storage/repo-manager.js'; +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); + interface SetupResult { configured: string[]; skipped: string[]; @@ -95,8 +99,6 @@ async function setupCursor(result: SetupResult): Promise { } async function setupClaudeCode(result: SetupResult): Promise { - // Claude Code uses `claude mcp add` — we just print the command - // Check for common Claude Code indicators const claudeDir = path.join(os.homedir(), '.claude'); const hasClaude = await dirExists(claudeDir); @@ -107,11 +109,90 @@ async function setupClaudeCode(result: SetupResult): Promise { // Claude Code uses a JSON settings file at ~/.claude.json or claude mcp add console.log(''); - console.log(' Claude Code detected. Run this command to add GitNexus:'); + console.log(' Claude Code detected. Run this command to add GitNexus MCP:'); console.log(''); console.log(' claude mcp add gitnexus -- npx -y gitnexus mcp'); console.log(''); - result.configured.push('Claude Code (manual step printed)'); + result.configured.push('Claude Code (MCP manual step printed)'); +} + +/** + * Install GitNexus skills to ~/.claude/skills/ for Claude Code. + */ +async function installClaudeCodeSkills(result: SetupResult): Promise { + const claudeDir = path.join(os.homedir(), '.claude'); + if (!(await dirExists(claudeDir))) return; + + const skillsDir = path.join(claudeDir, 'skills'); + try { + const installed = await installSkillsTo(skillsDir); + if (installed.length > 0) { + result.configured.push(`Claude Code skills (${installed.length} skills → ~/.claude/skills/)`); + } + } catch (err: any) { + result.errors.push(`Claude Code skills: ${err.message}`); + } +} + +/** + * Install GitNexus hooks to ~/.claude/settings.json for Claude Code. + * Merges hook config without overwriting existing hooks. + */ +async function installClaudeCodeHooks(result: SetupResult): Promise { + const claudeDir = path.join(os.homedir(), '.claude'); + if (!(await dirExists(claudeDir))) return; + + const settingsPath = path.join(claudeDir, 'settings.json'); + + // Source hooks bundled within the gitnexus package (hooks/claude/) + const pluginHooksPath = path.join(__dirname, '..', '..', 'hooks', 'claude'); + + // Copy unified hook script to ~/.claude/hooks/gitnexus/ + const destHooksDir = path.join(claudeDir, 'hooks', 'gitnexus'); + + try { + await fs.mkdir(destHooksDir, { recursive: true }); + + const src = path.join(pluginHooksPath, 'gitnexus-hook.js'); + const dest = path.join(destHooksDir, 'gitnexus-hook.js'); + try { + const content = await fs.readFile(src, 'utf-8'); + await fs.writeFile(dest, content, 'utf-8'); + } catch { + // Script not found in source — skip + } + + const hookCmd = `node "${path.join(destHooksDir, 'gitnexus-hook.js').replace(/\\/g, '/')}"`; + + // Merge hook config into ~/.claude/settings.json + const existing = await readJsonFile(settingsPath) || {}; + if (!existing.hooks) existing.hooks = {}; + + // NOTE: SessionStart hooks are broken on Windows (Claude Code bug #23576). + // Session context is delivered via CLAUDE.md / skills instead. + + // Add PreToolUse hook if not already present + if (!existing.hooks.PreToolUse) existing.hooks.PreToolUse = []; + const hasPreToolHook = existing.hooks.PreToolUse.some( + (h: any) => h.hooks?.some((hh: any) => hh.command?.includes('gitnexus')) + ); + if (!hasPreToolHook) { + existing.hooks.PreToolUse.push({ + matcher: 'Grep|Glob|Bash', + hooks: [{ + type: 'command', + command: hookCmd, + timeout: 10, + statusMessage: 'Enriching with GitNexus graph context...', + }], + }); + } + + await writeJsonFile(settingsPath, existing); + result.configured.push('Claude Code hooks (PreToolUse)'); + } catch (err: any) { + result.errors.push(`Claude Code hooks: ${err.message}`); + } } async function setupOpenCode(result: SetupResult): Promise { @@ -134,6 +215,72 @@ async function setupOpenCode(result: SetupResult): Promise { } } +// ─── Skill Installation ─────────────────────────────────────────── + +const SKILL_NAMES = ['exploring', 'debugging', 'impact-analysis', 'refactoring']; + +/** + * Install GitNexus skills to a target directory. + * Each skill is installed as {targetDir}/gitnexus-{skillName}/SKILL.md + * following the Agent Skills standard (both Cursor and Claude Code). + */ +async function installSkillsTo(targetDir: string): Promise { + const installed: string[] = []; + + for (const skillName of SKILL_NAMES) { + const sourcePath = path.join(__dirname, '..', '..', 'skills', `${skillName}.md`); + const skillDir = path.join(targetDir, `gitnexus-${skillName}`); + const destPath = path.join(skillDir, 'SKILL.md'); + + try { + const content = await fs.readFile(sourcePath, 'utf-8'); + await fs.mkdir(skillDir, { recursive: true }); + await fs.writeFile(destPath, content, 'utf-8'); + installed.push(skillName); + } catch { + // Source skill file not found — skip + } + } + + return installed; +} + +/** + * Install global Cursor skills to ~/.cursor/skills/gitnexus/ + */ +async function installCursorSkills(result: SetupResult): Promise { + const cursorDir = path.join(os.homedir(), '.cursor'); + if (!(await dirExists(cursorDir))) return; + + const skillsDir = path.join(cursorDir, 'skills'); + try { + const installed = await installSkillsTo(skillsDir); + if (installed.length > 0) { + result.configured.push(`Cursor skills (${installed.length} skills → ~/.cursor/skills/)`); + } + } catch (err: any) { + result.errors.push(`Cursor skills: ${err.message}`); + } +} + +/** + * Install global OpenCode skills to ~/.config/opencode/skill/gitnexus/ + */ +async function installOpenCodeSkills(result: SetupResult): Promise { + const opencodeDir = path.join(os.homedir(), '.config', 'opencode'); + if (!(await dirExists(opencodeDir))) return; + + const skillsDir = path.join(opencodeDir, 'skill'); + try { + const installed = await installSkillsTo(skillsDir); + if (installed.length > 0) { + result.configured.push(`OpenCode skills (${installed.length} skills → ~/.config/opencode/skill/)`); + } + } catch (err: any) { + result.errors.push(`OpenCode skills: ${err.message}`); + } +} + // ─── Main command ────────────────────────────────────────────────── export const setupCommand = async () => { @@ -152,10 +299,16 @@ export const setupCommand = async () => { errors: [], }; - // Detect and configure each editor + // Detect and configure each editor's MCP await setupCursor(result); await setupClaudeCode(result); await setupOpenCode(result); + + // Install global skills for platforms that support them + await installClaudeCodeSkills(result); + await installClaudeCodeHooks(result); + await installCursorSkills(result); + await installOpenCodeSkills(result); // Print results if (result.configured.length > 0) { @@ -181,6 +334,10 @@ export const setupCommand = async () => { } } + console.log(''); + console.log(' Summary:'); + console.log(` MCP configured for: ${result.configured.filter(c => !c.includes('skills')).join(', ') || 'none'}`); + console.log(` Skills installed to: ${result.configured.filter(c => c.includes('skills')).length > 0 ? result.configured.filter(c => c.includes('skills')).join(', ') : 'none'}`); console.log(''); console.log(' Next steps:'); console.log(' 1. cd into any git repo'); diff --git a/gitnexus/src/core/augmentation/engine.ts b/gitnexus/src/core/augmentation/engine.ts new file mode 100644 index 000000000..7960c34bf --- /dev/null +++ b/gitnexus/src/core/augmentation/engine.ts @@ -0,0 +1,222 @@ +/** + * Augmentation Engine + * + * Lightweight, fast-path enrichment of search patterns with knowledge graph context. + * Designed to be called from platform hooks (Claude Code PreToolUse, Cursor beforeShellExecution) + * when an agent runs grep/glob/search. + * + * Performance target: <500ms cold start, <200ms warm. + * + * Design decisions: + * - Uses only BM25 search (no semantic/embedding) for speed + * - Clusters used internally for ranking, NEVER in output + * - Output is pure relationships: callers, callees, process participation + * - Graceful failure: any error → return empty string + */ + +import path from 'path'; +import { listRegisteredRepos } from '../../storage/repo-manager.js'; + +/** + * Find the best matching repo for a given working directory. + * Matches by checking if cwd is within the repo's path. + */ +async function findRepoForCwd(cwd: string): Promise<{ + name: string; + storagePath: string; + kuzuPath: string; +} | null> { + try { + const entries = await listRegisteredRepos({ validate: true }); + const resolved = path.resolve(cwd); + + // Find the repo whose path contains (or is) the cwd + for (const entry of entries) { + const repoResolved = path.resolve(entry.path); + if (resolved.startsWith(repoResolved) || repoResolved.startsWith(resolved)) { + return { + name: entry.name, + storagePath: entry.storagePath, + kuzuPath: path.join(entry.storagePath, 'kuzu'), + }; + } + } + return null; + } catch { + return null; + } +} + +/** + * Augment a search pattern with knowledge graph context. + * + * 1. BM25 search for the pattern + * 2. For top matches, fetch callers/callees/processes + * 3. Rank by internal cluster cohesion (not exposed) + * 4. Format as structured text block + * + * Returns empty string on any error (graceful failure). + */ +export async function augment(pattern: string, cwd?: string): Promise { + if (!pattern || pattern.length < 3) return ''; + + const workDir = cwd || process.cwd(); + + try { + const repo = await findRepoForCwd(workDir); + if (!repo) return ''; + + // Lazy-load kuzu adapter (skip unnecessary init) + const { initKuzu, executeQuery, isKuzuReady } = await import('../../mcp/core/kuzu-adapter.js'); + const { searchFTSFromKuzu } = await import('../search/bm25-index.js'); + + const repoId = repo.name.toLowerCase(); + + // Init KuzuDB if not already + if (!isKuzuReady(repoId)) { + await initKuzu(repoId, repo.kuzuPath); + } + + // Step 1: BM25 search (fast, no embeddings) + const bm25Results = await searchFTSFromKuzu(pattern, 10, repoId); + + if (bm25Results.length === 0) return ''; + + // Step 2: Map BM25 file results to symbols + const symbolMatches: Array<{ + nodeId: string; + name: string; + type: string; + filePath: string; + score: number; + }> = []; + + for (const result of bm25Results.slice(0, 5)) { + const escaped = result.filePath.replace(/'/g, "''"); + try { + const symbols = await executeQuery(repoId, ` + MATCH (n) WHERE n.filePath = '${escaped}' + AND n.name CONTAINS '${pattern.replace(/'/g, "''").split(/\s+/)[0]}' + RETURN n.id AS id, n.name AS name, labels(n)[0] AS type, n.filePath AS filePath + LIMIT 3 + `); + for (const sym of symbols) { + symbolMatches.push({ + nodeId: sym.id || sym[0], + name: sym.name || sym[1], + type: sym.type || sym[2], + filePath: sym.filePath || sym[3], + score: result.score, + }); + } + } catch { /* skip */ } + } + + if (symbolMatches.length === 0) return ''; + + // Step 3: For top matches, fetch callers/callees/processes + // Also get cluster cohesion internally for ranking + const enriched: Array<{ + name: string; + filePath: string; + callers: string[]; + callees: string[]; + processes: string[]; + cohesion: number; + }> = []; + + const seen = new Set(); + + for (const sym of symbolMatches.slice(0, 5)) { + if (seen.has(sym.nodeId)) continue; + seen.add(sym.nodeId); + + const escaped = sym.nodeId.replace(/'/g, "''"); + + // Callers + let callers: string[] = []; + try { + const rows = await executeQuery(repoId, ` + MATCH (caller)-[:CodeRelation {type: 'CALLS'}]->(n {id: '${escaped}'}) + RETURN caller.name AS name + LIMIT 3 + `); + callers = rows.map((r: any) => r.name || r[0]).filter(Boolean); + } catch { /* skip */ } + + // Callees + let callees: string[] = []; + try { + const rows = await executeQuery(repoId, ` + MATCH (n {id: '${escaped}'})-[:CodeRelation {type: 'CALLS'}]->(callee) + RETURN callee.name AS name + LIMIT 3 + `); + callees = rows.map((r: any) => r.name || r[0]).filter(Boolean); + } catch { /* skip */ } + + // Processes + let processes: string[] = []; + try { + const rows = await executeQuery(repoId, ` + MATCH (n {id: '${escaped}'})-[r:CodeRelation {type: 'STEP_IN_PROCESS'}]->(p:Process) + RETURN p.heuristicLabel AS label, r.step AS step, p.stepCount AS stepCount + `); + processes = rows.map((r: any) => { + const label = r.label || r[0]; + const step = r.step || r[1]; + const stepCount = r.stepCount || r[2]; + return `${label} (step ${step}/${stepCount})`; + }).filter(Boolean); + } catch { /* skip */ } + + // Cluster cohesion (internal ranking signal) + let cohesion = 0; + try { + const rows = await executeQuery(repoId, ` + MATCH (n {id: '${escaped}'})-[:CodeRelation {type: 'MEMBER_OF'}]->(c:Community) + RETURN c.cohesion AS cohesion + LIMIT 1 + `); + if (rows.length > 0) { + cohesion = (rows[0].cohesion ?? rows[0][0]) || 0; + } + } catch { /* skip */ } + + enriched.push({ + name: sym.name, + filePath: sym.filePath, + callers, + callees, + processes, + cohesion, + }); + } + + if (enriched.length === 0) return ''; + + // Step 4: Rank by cohesion (internal signal) and format + enriched.sort((a, b) => b.cohesion - a.cohesion); + + const lines: string[] = [`[GitNexus] ${enriched.length} related symbols found:`, '']; + + for (const item of enriched) { + lines.push(`${item.name} (${item.filePath})`); + if (item.callers.length > 0) { + lines.push(` Called by: ${item.callers.join(', ')}`); + } + if (item.callees.length > 0) { + lines.push(` Calls: ${item.callees.join(', ')}`); + } + if (item.processes.length > 0) { + lines.push(` Flows: ${item.processes.join(', ')}`); + } + lines.push(''); + } + + return lines.join('\n').trim(); + } catch { + // Graceful failure — never break the original tool + return ''; + } +} diff --git a/gitnexus/src/mcp/local/local-backend.ts b/gitnexus/src/mcp/local/local-backend.ts index d64657619..834a0ca4a 100644 --- a/gitnexus/src/mcp/local/local-backend.ts +++ b/gitnexus/src/mcp/local/local-backend.ts @@ -218,16 +218,25 @@ export class LocalBackend { const repo = this.resolveRepo(params?.repo); switch (method) { - case 'search': - return this.search(repo, params); + case 'query': + return this.query(repo, params); case 'cypher': return this.cypher(repo, params); - case 'overview': - return this.overview(repo, params); - case 'explore': - return this.explore(repo, params); + case 'context': + return this.context(repo, params); case 'impact': return this.impact(repo, params); + case 'detect_changes': + return this.detectChanges(repo, params); + case 'rename': + return this.rename(repo, params); + // Legacy aliases for backwards compatibility + case 'search': + return this.query(repo, params); + case 'explore': + return this.context(repo, { name: params?.name, ...params }); + case 'overview': + return this.overview(repo, params); default: throw new Error(`Unknown tool: ${method}`); } @@ -235,25 +244,39 @@ export class LocalBackend { // ─── Tool Implementations ──────────────────────────────────────── - private async search(repo: RepoHandle, params: { query: string; limit?: number; depth?: string }): Promise { + /** + * Query tool — process-grouped search. + * + * 1. Hybrid search (BM25 + semantic) to find matching symbols + * 2. Trace each match to its process(es) via STEP_IN_PROCESS + * 3. Group by process, rank by aggregate relevance + internal cluster cohesion + * 4. Return: { processes, process_symbols, definitions } + */ + private async query(repo: RepoHandle, params: { + query: string; + task_context?: string; + goal?: string; + limit?: number; + max_symbols?: number; + include_content?: boolean; + }): Promise { await this.ensureInitialized(repo.id); - const limit = params.limit || 10; - const query = params.query; - const depth = params.depth || 'definitions'; + const processLimit = params.limit || 5; + const maxSymbolsPerProcess = params.max_symbols || 10; + const includeContent = params.include_content ?? false; + const searchQuery = params.query; - // Run BM25 and semantic search in parallel + // Step 1: Run hybrid search to get matching symbols + const searchLimit = processLimit * maxSymbolsPerProcess; // fetch enough raw results const [bm25Results, semanticResults] = await Promise.all([ - this.bm25Search(repo, query, limit * 2), - this.semanticSearch(repo, query, limit * 2), + this.bm25Search(repo, searchQuery, searchLimit), + this.semanticSearch(repo, searchQuery, searchLimit), ]); - // Merge and deduplicate results using reciprocal rank fusion - // Key by nodeId (symbol-level) so semantic precision is preserved. - // Fall back to filePath for File-level results that lack a nodeId. - const scoreMap = new Map(); + // Merge via reciprocal rank fusion + const scoreMap = new Map(); - // BM25 results for (let i = 0; i < bm25Results.length; i++) { const result = bm25Results[i]; const key = result.nodeId || result.filePath; @@ -261,13 +284,11 @@ export class LocalBackend { const existing = scoreMap.get(key); if (existing) { existing.score += rrfScore; - existing.source = 'hybrid'; } else { - scoreMap.set(key, { score: rrfScore, source: 'bm25', data: result }); + scoreMap.set(key, { score: rrfScore, data: result }); } } - // Semantic results for (let i = 0; i < semanticResults.length; i++) { const result = semanticResults[i]; const key = result.nodeId || result.filePath; @@ -275,70 +296,156 @@ export class LocalBackend { const existing = scoreMap.get(key); if (existing) { existing.score += rrfScore; - existing.source = 'hybrid'; } else { - scoreMap.set(key, { score: rrfScore, source: 'semantic', data: result }); + scoreMap.set(key, { score: rrfScore, data: result }); } } - // Sort by fused score and take top results const merged = Array.from(scoreMap.entries()) .sort((a, b) => b[1].score - a[1].score) - .slice(0, limit); + .slice(0, searchLimit); - // Enrich with graph data - const results: any[] = []; + // Step 2: For each match with a nodeId, trace to process(es) + const processMap = new Map(); + const definitions: any[] = []; // standalone symbols not in any process for (const [_, item] of merged) { - const result = item.data; - result.searchSource = item.source; - result.fusedScore = item.score; + const sym = item.data; + if (!sym.nodeId) { + // File-level results go to definitions + definitions.push({ + name: sym.name, + type: sym.type || 'File', + filePath: sym.filePath, + }); + continue; + } - // Add cluster membership context for each result with a nodeId - if (result.nodeId) { + const escaped = sym.nodeId.replace(/'/g, "''"); + + // Find processes this symbol participates in + let processRows: any[] = []; + try { + processRows = await executeQuery(repo.id, ` + MATCH (n {id: '${escaped}'})-[r:CodeRelation {type: 'STEP_IN_PROCESS'}]->(p:Process) + RETURN p.id AS pid, p.label AS label, p.heuristicLabel AS heuristicLabel, p.processType AS processType, p.stepCount AS stepCount, r.step AS step + `); + } catch { /* symbol might not be in any process */ } + + // Get cluster cohesion as internal ranking signal (never exposed) + let cohesion = 0; + try { + const cohesionRows = await executeQuery(repo.id, ` + MATCH (n {id: '${escaped}'})-[:CodeRelation {type: 'MEMBER_OF'}]->(c:Community) + RETURN c.cohesion AS cohesion + LIMIT 1 + `); + if (cohesionRows.length > 0) { + cohesion = (cohesionRows[0].cohesion ?? cohesionRows[0][0]) || 0; + } + } catch { /* no cluster info */ } + + // Optionally fetch content + let content: string | undefined; + if (includeContent) { try { - const clusterQuery = ` - MATCH (n {id: '${result.nodeId.replace(/'/g, "''")}'})-[:CodeRelation {type: 'MEMBER_OF'}]->(c:Community) - RETURN c.label AS label, c.heuristicLabel AS heuristicLabel - LIMIT 1 - `; - const clusters = await executeQuery(repo.id, clusterQuery); - if (clusters.length > 0) { - result.cluster = { - label: clusters[0].label || clusters[0][0], - heuristicLabel: clusters[0].heuristicLabel || clusters[0][1], - }; + const contentRows = await executeQuery(repo.id, ` + MATCH (n {id: '${escaped}'}) + RETURN n.content AS content + `); + if (contentRows.length > 0) { + content = contentRows[0].content ?? contentRows[0][0]; } - } catch { - // Cluster lookup failed - continue without it - } + } catch { /* skip */ } } - // Add relationships if depth is 'full' and we have a node ID - // Only include connections with actual name/path data (skip MEMBER_OF, STEP_IN_PROCESS noise) - if (depth === 'full' && result.nodeId) { - try { - const relQuery = ` - MATCH (n {id: '${result.nodeId.replace(/'/g, "''")}'})-[r:CodeRelation]->(m) - WHERE r.type IN ['CALLS', 'IMPORTS', 'DEFINES', 'EXTENDS', 'IMPLEMENTS'] - RETURN r.type AS type, m.name AS targetName, m.filePath AS targetPath - LIMIT 5 - `; - const rels = await executeQuery(repo.id, 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 = []; + const symbolEntry = { + id: sym.nodeId, + name: sym.name, + type: sym.type, + filePath: sym.filePath, + startLine: sym.startLine, + endLine: sym.endLine, + ...(includeContent && content ? { content } : {}), + }; + + if (processRows.length === 0) { + // Symbol not in any process — goes to definitions + definitions.push(symbolEntry); + } else { + // Add to each process it belongs to + for (const row of processRows) { + const pid = row.pid ?? row[0]; + const label = row.label ?? row[1]; + const hLabel = row.heuristicLabel ?? row[2]; + const pType = row.processType ?? row[3]; + const stepCount = row.stepCount ?? row[4]; + const step = row.step ?? row[5]; + + if (!processMap.has(pid)) { + processMap.set(pid, { + id: pid, + label, + heuristicLabel: hLabel, + processType: pType, + stepCount, + totalScore: 0, + cohesionBoost: 0, + symbols: [], + }); + } + + const proc = processMap.get(pid)!; + proc.totalScore += item.score; + proc.cohesionBoost = Math.max(proc.cohesionBoost, cohesion); + proc.symbols.push({ + ...symbolEntry, + process_id: pid, + step_index: step, + }); } } - - results.push(result); } - return results; + // Step 3: Rank processes by aggregate score + internal cohesion boost + const rankedProcesses = Array.from(processMap.values()) + .map(p => ({ + ...p, + priority: p.totalScore + (p.cohesionBoost * 0.1), // cohesion as subtle ranking signal + })) + .sort((a, b) => b.priority - a.priority) + .slice(0, processLimit); + + // Step 4: Build response + const processes = rankedProcesses.map(p => ({ + id: p.id, + summary: p.heuristicLabel || p.label, + priority: Math.round(p.priority * 1000) / 1000, + symbol_count: p.symbols.length, + process_type: p.processType, + step_count: p.stepCount, + })); + + const processSymbols = rankedProcesses.flatMap(p => + p.symbols.slice(0, maxSymbolsPerProcess).map(s => ({ + ...s, + // remove internal fields + })) + ); + + // Deduplicate process_symbols by id + const seen = new Set(); + const dedupedSymbols = processSymbols.filter(s => { + if (seen.has(s.id)) return false; + seen.add(s.id); + return true; + }); + + return { + processes, + process_symbols: dedupedSymbols, + definitions: definitions.slice(0, 20), // cap standalone definitions + }; } /** @@ -568,84 +675,157 @@ export class LocalBackend { return result; } - private async explore(repo: RepoHandle, params: { name: string; type: 'symbol' | 'cluster' | 'process' }): Promise { + /** + * Context tool — 360-degree symbol view with categorized refs. + * Disambiguation when multiple symbols share a name. + * UID-based direct lookup. No cluster in output. + */ + private async context(repo: RepoHandle, params: { + name?: string; + uid?: string; + file_path?: string; + include_content?: boolean; + }): Promise { await this.ensureInitialized(repo.id); + const { name, uid, file_path, include_content } = params; + + if (!name && !uid) { + return { error: 'Either "name" or "uid" parameter is required.' }; + } + + // Step 1: Find the symbol + let symbols: any[]; + + if (uid) { + const escaped = uid.replace(/'/g, "''"); + symbols = await executeQuery(repo.id, ` + MATCH (n {id: '${escaped}'}) + 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${include_content ? ', n.content AS content' : ''} + LIMIT 1 + `); + } else { + const escaped = name!.replace(/'/g, "''"); + const isQualified = name!.includes('/') || name!.includes(':'); + + let whereClause: string; + if (file_path) { + const fpEscaped = file_path.replace(/'/g, "''"); + whereClause = `WHERE n.name = '${escaped}' AND n.filePath CONTAINS '${fpEscaped}'`; + } else if (isQualified) { + whereClause = `WHERE n.id = '${escaped}' OR n.name = '${escaped}'`; + } else { + whereClause = `WHERE n.name = '${escaped}'`; + } + + symbols = await executeQuery(repo.id, ` + MATCH (n) ${whereClause} + 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${include_content ? ', n.content AS content' : ''} + LIMIT 10 + `); + } + + if (symbols.length === 0) { + return { error: `Symbol '${name || uid}' not found` }; + } + + // Step 2: Disambiguation + if (symbols.length > 1 && !uid) { + return { + status: 'ambiguous', + message: `Found ${symbols.length} symbols matching '${name}'. Use uid or file_path to disambiguate.`, + candidates: symbols.map((s: any) => ({ + uid: s.id || s[0], + name: s.name || s[1], + kind: s.type || s[2], + filePath: s.filePath || s[3], + line: s.startLine || s[4], + })), + }; + } + + // Step 3: Build full context + const sym = symbols[0]; + const symId = (sym.id || sym[0]).replace(/'/g, "''"); + + // Categorized incoming refs + const incomingRows = await executeQuery(repo.id, ` + MATCH (caller)-[r:CodeRelation]->(n {id: '${symId}'}) + WHERE r.type IN ['CALLS', 'IMPORTS', 'EXTENDS', 'IMPLEMENTS'] + RETURN r.type AS relType, caller.id AS uid, caller.name AS name, caller.filePath AS filePath, labels(caller)[0] AS kind + LIMIT 30 + `); + + // Categorized outgoing refs + const outgoingRows = await executeQuery(repo.id, ` + MATCH (n {id: '${symId}'})-[r:CodeRelation]->(target) + WHERE r.type IN ['CALLS', 'IMPORTS', 'EXTENDS', 'IMPLEMENTS'] + RETURN r.type AS relType, target.id AS uid, target.name AS name, target.filePath AS filePath, labels(target)[0] AS kind + LIMIT 30 + `); + + // Process participation + let processRows: any[] = []; + try { + processRows = await executeQuery(repo.id, ` + MATCH (n {id: '${symId}'})-[r:CodeRelation {type: 'STEP_IN_PROCESS'}]->(p:Process) + RETURN p.id AS pid, p.heuristicLabel AS label, r.step AS step, p.stepCount AS stepCount + `); + } catch { /* no process info */ } + + // Helper to categorize refs + const categorize = (rows: any[]) => { + const cats: Record = {}; + for (const row of rows) { + const relType = (row.relType || row[0] || '').toLowerCase(); + const entry = { + uid: row.uid || row[1], + name: row.name || row[2], + filePath: row.filePath || row[3], + kind: row.kind || row[4], + }; + if (!cats[relType]) cats[relType] = []; + cats[relType].push(entry); + } + return cats; + }; + + return { + status: 'found', + symbol: { + uid: sym.id || sym[0], + name: sym.name || sym[1], + kind: sym.type || sym[2], + filePath: sym.filePath || sym[3], + startLine: sym.startLine || sym[4], + endLine: sym.endLine || sym[5], + ...(include_content && (sym.content || sym[6]) ? { content: sym.content || sym[6] } : {}), + }, + incoming: categorize(incomingRows), + outgoing: categorize(outgoingRows), + processes: processRows.map((r: any) => ({ + id: r.pid || r[0], + name: r.label || r[1], + step_index: r.step || r[2], + step_count: r.stepCount || r[3], + })), + }; + } + + /** + * Legacy explore — kept for backwards compatibility with resources.ts. + * Routes cluster/process types to direct graph queries. + */ + private async explore(repo: RepoHandle, params: { name: string; type: 'symbol' | 'cluster' | 'process' }): Promise { + await this.ensureInitialized(repo.id); const { name, type } = params; if (type === 'symbol') { - // If name contains a path separator or ':', treat it as a qualified lookup - const isQualified = name.includes('/') || name.includes(':'); - const symbolQuery = isQualified - ? `MATCH (n) WHERE n.id = '${name.replace(/'/g, "''")}' OR (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 5` - : `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 5`; - - const symbols = await executeQuery(repo.id, symbolQuery); - if (symbols.length === 0) return { error: `Symbol '${name}' not found` }; - - // Use the first match for detailed exploration - const sym = symbols[0]; - const symId = sym.id || sym[0]; - - 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(repo.id, callersQuery); - - 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(repo.id, calleesQuery); - - 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(repo.id, communityQuery); - - const result: any = { - 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 multiple symbols share the same name, show alternatives so the agent can disambiguate - if (symbols.length > 1) { - result.alternatives = symbols.slice(1).map((s: any) => ({ - id: s.id || s[0], - type: s.type || s[2], - filePath: s.filePath || s[3], - })); - result.hint = `Multiple symbols named '${name}' found. Showing details for ${result.symbol.filePath}. Use the full node ID to explore a specific alternative.`; - } - - return result; + return this.context(repo, { name }); } if (type === 'cluster') { const escaped = name.replace(/'/g, "''"); - - // Find ALL communities with this label (not just one) const clusterQuery = ` MATCH (c:Community) WHERE c.label = '${escaped}' OR c.heuristicLabel = '${escaped}' @@ -655,30 +835,23 @@ export class LocalBackend { if (clusters.length === 0) return { error: `Cluster '${name}' not found` }; const rawClusters = 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], + 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], })); - // Aggregate: sum symbols, weighted-average cohesion across sub-communities - let totalSymbols = 0; - let weightedCohesion = 0; + let totalSymbols = 0, weightedCohesion = 0; for (const c of rawClusters) { const s = c.symbolCount || 0; totalSymbols += s; weightedCohesion += (c.cohesion || 0) * s; } - // Fetch members from ALL matching sub-communities (DISTINCT to avoid dupes) - const membersQuery = ` + const members = await executeQuery(repo.id, ` MATCH (n)-[:CodeRelation {type: 'MEMBER_OF'}]->(c:Community) WHERE c.label = '${escaped}' OR c.heuristicLabel = '${escaped}' RETURN DISTINCT n.name AS name, labels(n)[0] AS type, n.filePath AS filePath LIMIT 30 - `; - const members = await executeQuery(repo.id, membersQuery); + `); return { cluster: { @@ -690,46 +863,35 @@ export class LocalBackend { subCommunities: rawClusters.length, }, members: members.map((m: any) => ({ - name: m.name || m[0], - type: m.type || m[1], - filePath: m.filePath || m[2], + name: m.name || m[0], type: m.type || m[1], filePath: m.filePath || m[2], })), }; } if (type === 'process') { - const processQuery = ` + const processes = await executeQuery(repo.id, ` 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 + RETURN p.id AS id, p.label AS label, p.heuristicLabel AS heuristicLabel, p.processType AS processType, p.stepCount AS stepCount LIMIT 1 - `; - const processes = await executeQuery(repo.id, processQuery); + `); if (processes.length === 0) return { error: `Process '${name}' not found` }; const proc = processes[0]; const procId = proc.id || proc[0]; - - const stepsQuery = ` + const steps = await executeQuery(repo.id, ` 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(repo.id, 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], + 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], + step: s.step || s[3], name: s.name || s[0], type: s.type || s[1], filePath: s.filePath || s[2], })), }; } @@ -737,6 +899,268 @@ export class LocalBackend { return { error: 'Invalid type. Use: symbol, cluster, or process' }; } + /** + * Detect changes — git-diff based impact analysis. + * Maps changed lines to indexed symbols, then finds affected processes. + */ + private async detectChanges(repo: RepoHandle, params: { + scope?: string; + base_ref?: string; + }): Promise { + await this.ensureInitialized(repo.id); + + const scope = params.scope || 'unstaged'; + const { execSync } = await import('child_process'); + + // Build git diff command based on scope + let diffCmd: string; + switch (scope) { + case 'staged': + diffCmd = 'git diff --staged --name-only'; + break; + case 'all': + diffCmd = 'git diff HEAD --name-only'; + break; + case 'compare': + if (!params.base_ref) return { error: 'base_ref is required for "compare" scope' }; + diffCmd = `git diff ${params.base_ref} --name-only`; + break; + case 'unstaged': + default: + diffCmd = 'git diff --name-only'; + break; + } + + let changedFiles: string[]; + try { + const output = execSync(diffCmd, { cwd: repo.repoPath, encoding: 'utf-8' }); + changedFiles = output.trim().split('\n').filter(f => f.length > 0); + } catch (err: any) { + return { error: `Git diff failed: ${err.message}` }; + } + + if (changedFiles.length === 0) { + return { + summary: { changed_count: 0, affected_count: 0, risk_level: 'none', message: 'No changes detected.' }, + changed_symbols: [], + affected_processes: [], + }; + } + + // Map changed files to indexed symbols + const changedSymbols: any[] = []; + for (const file of changedFiles) { + const escaped = file.replace(/\\/g, '/').replace(/'/g, "''"); + try { + const symbols = await executeQuery(repo.id, ` + MATCH (n) WHERE n.filePath CONTAINS '${escaped}' + RETURN n.id AS id, n.name AS name, labels(n)[0] AS type, n.filePath AS filePath + LIMIT 20 + `); + for (const sym of symbols) { + changedSymbols.push({ + id: sym.id || sym[0], + name: sym.name || sym[1], + type: sym.type || sym[2], + filePath: sym.filePath || sym[3], + change_type: 'Modified', + }); + } + } catch { /* skip */ } + } + + // Find affected processes + const affectedProcesses = new Map(); + for (const sym of changedSymbols) { + const escaped = (sym.id as string).replace(/'/g, "''"); + try { + const procs = await executeQuery(repo.id, ` + MATCH (n {id: '${escaped}'})-[r:CodeRelation {type: 'STEP_IN_PROCESS'}]->(p:Process) + RETURN p.id AS pid, p.heuristicLabel AS label, p.processType AS processType, p.stepCount AS stepCount, r.step AS step + `); + for (const proc of procs) { + const pid = proc.pid || proc[0]; + if (!affectedProcesses.has(pid)) { + affectedProcesses.set(pid, { + id: pid, + name: proc.label || proc[1], + process_type: proc.processType || proc[2], + step_count: proc.stepCount || proc[3], + changed_steps: [], + }); + } + affectedProcesses.get(pid)!.changed_steps.push({ + symbol: sym.name, + step: proc.step || proc[4], + }); + } + } catch { /* skip */ } + } + + const processCount = affectedProcesses.size; + const risk = processCount === 0 ? 'low' : processCount <= 5 ? 'medium' : processCount <= 15 ? 'high' : 'critical'; + + return { + summary: { + changed_count: changedSymbols.length, + affected_count: processCount, + changed_files: changedFiles.length, + risk_level: risk, + }, + changed_symbols: changedSymbols, + affected_processes: Array.from(affectedProcesses.values()), + }; + } + + /** + * Rename tool — multi-file coordinated rename using graph + text search. + * Graph refs are tagged "graph" (high confidence). + * Additional refs found via text search are tagged "text_search" (lower confidence). + */ + private async rename(repo: RepoHandle, params: { + symbol_name?: string; + symbol_uid?: string; + new_name: string; + file_path?: string; + dry_run?: boolean; + }): Promise { + await this.ensureInitialized(repo.id); + + const { new_name, file_path } = params; + const dry_run = params.dry_run ?? true; + + if (!params.symbol_name && !params.symbol_uid) { + return { error: 'Either symbol_name or symbol_uid is required.' }; + } + + // Step 1: Find the target symbol (reuse context's lookup) + const lookupResult = await this.context(repo, { + name: params.symbol_name, + uid: params.symbol_uid, + file_path, + }); + + if (lookupResult.status === 'ambiguous') { + return lookupResult; // pass disambiguation through + } + if (lookupResult.error) { + return lookupResult; + } + + const sym = lookupResult.symbol; + const oldName = sym.name; + + if (oldName === new_name) { + return { error: 'New name is the same as the current name.' }; + } + + // Step 2: Collect edits from graph (high confidence) + const changes = new Map(); + + const addEdit = (filePath: string, line: number, oldText: string, newText: string, confidence: string) => { + if (!changes.has(filePath)) { + changes.set(filePath, { file_path: filePath, edits: [] }); + } + changes.get(filePath)!.edits.push({ line, old_text: oldText, new_text: newText, confidence }); + }; + + // The definition itself + if (sym.filePath && sym.startLine) { + try { + const content = await fs.readFile(path.join(repo.repoPath, sym.filePath), 'utf-8'); + const lines = content.split('\n'); + const lineIdx = sym.startLine - 1; + if (lineIdx >= 0 && lineIdx < lines.length && lines[lineIdx].includes(oldName)) { + addEdit(sym.filePath, sym.startLine, lines[lineIdx].trim(), lines[lineIdx].replace(oldName, new_name).trim(), 'graph'); + } + } catch { /* skip */ } + } + + // All incoming refs from graph (callers, importers, etc.) + const allIncoming = [ + ...(lookupResult.incoming.calls || []), + ...(lookupResult.incoming.imports || []), + ...(lookupResult.incoming.extends || []), + ...(lookupResult.incoming.implements || []), + ]; + + let graphEdits = changes.size > 0 ? 1 : 0; // count definition edit + + for (const ref of allIncoming) { + if (!ref.filePath) continue; + try { + const content = await fs.readFile(path.join(repo.repoPath, ref.filePath), 'utf-8'); + const lines = content.split('\n'); + for (let i = 0; i < lines.length; i++) { + if (lines[i].includes(oldName)) { + addEdit(ref.filePath, i + 1, lines[i].trim(), lines[i].replace(new RegExp(`\\b${oldName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\b`, 'g'), new_name).trim(), 'graph'); + graphEdits++; + break; // one edit per file from graph refs + } + } + } catch { /* skip */ } + } + + // Step 3: Text search for refs the graph might have missed + let astSearchEdits = 0; + const graphFiles = new Set([sym.filePath, ...allIncoming.map(r => r.filePath)].filter(Boolean)); + + // Simple text search across the repo for the old name (in files not already covered by graph) + try { + const { execSync } = await import('child_process'); + const rgCmd = `rg -l --type-add "code:*.{ts,tsx,js,jsx,py,go,rs,java}" -t code "\\b${oldName}\\b" .`; + const output = execSync(rgCmd, { cwd: repo.repoPath, encoding: 'utf-8', timeout: 5000 }); + const files = output.trim().split('\n').filter(f => f.length > 0); + + for (const file of files) { + const normalizedFile = file.replace(/\\/g, '/').replace(/^\.\//, ''); + if (graphFiles.has(normalizedFile)) continue; // already covered by graph + + try { + const content = await fs.readFile(path.join(repo.repoPath, normalizedFile), 'utf-8'); + const lines = content.split('\n'); + const regex = new RegExp(`\\b${oldName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\b`, 'g'); + for (let i = 0; i < lines.length; i++) { + if (regex.test(lines[i])) { + addEdit(normalizedFile, i + 1, lines[i].trim(), lines[i].replace(regex, new_name).trim(), 'text_search'); + astSearchEdits++; + regex.lastIndex = 0; // reset regex + } + } + } catch { /* skip */ } + } + } catch { /* rg not available or no additional matches */ } + + // Step 4: Apply or preview + const allChanges = Array.from(changes.values()); + const totalEdits = allChanges.reduce((sum, c) => sum + c.edits.length, 0); + + if (!dry_run) { + // Apply edits to files + for (const change of allChanges) { + try { + const fullPath = path.join(repo.repoPath, change.file_path); + let content = await fs.readFile(fullPath, 'utf-8'); + const regex = new RegExp(`\\b${oldName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\b`, 'g'); + content = content.replace(regex, new_name); + await fs.writeFile(fullPath, content, 'utf-8'); + } catch { /* skip failed files */ } + } + } + + return { + status: 'success', + old_name: oldName, + new_name, + files_affected: allChanges.length, + total_edits: totalEdits, + graph_edits: graphEdits, + text_search_edits: astSearchEdits, + changes: allChanges, + applied: !dry_run, + }; + } + private async impact(repo: RepoHandle, params: { target: string; direction: 'upstream' | 'downstream'; @@ -828,6 +1252,153 @@ export class LocalBackend { }; } + // ─── Direct Graph Queries (for resources.ts) ──────────────────── + + /** + * Query clusters (communities) directly from graph. + * Used by getClustersResource — avoids legacy overview() dispatch. + */ + async queryClusters(repoName?: string, limit = 100): Promise<{ clusters: any[] }> { + const repo = this.resolveRepo(repoName); + await this.ensureInitialized(repo.id); + + try { + const rawLimit = Math.max(limit * 5, 200); + const clusters = await executeQuery(repo.id, ` + 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 ${rawLimit} + `); + const rawClusters = 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], + })); + return { clusters: this.aggregateClusters(rawClusters).slice(0, limit) }; + } catch { + return { clusters: [] }; + } + } + + /** + * Query processes directly from graph. + * Used by getProcessesResource — avoids legacy overview() dispatch. + */ + async queryProcesses(repoName?: string, limit = 50): Promise<{ processes: any[] }> { + const repo = this.resolveRepo(repoName); + await this.ensureInitialized(repo.id); + + try { + const processes = await executeQuery(repo.id, ` + 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} + `); + return { + 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 { + return { processes: [] }; + } + } + + /** + * Query cluster detail (members) directly from graph. + * Used by getClusterDetailResource. + */ + async queryClusterDetail(name: string, repoName?: string): Promise { + const repo = this.resolveRepo(repoName); + await this.ensureInitialized(repo.id); + + const escaped = name.replace(/'/g, "''"); + const clusterQuery = ` + MATCH (c:Community) + WHERE c.label = '${escaped}' OR c.heuristicLabel = '${escaped}' + RETURN c.id AS id, c.label AS label, c.heuristicLabel AS heuristicLabel, c.cohesion AS cohesion, c.symbolCount AS symbolCount + `; + const clusters = await executeQuery(repo.id, clusterQuery); + if (clusters.length === 0) return { error: `Cluster '${name}' not found` }; + + const rawClusters = 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], + })); + + let totalSymbols = 0, weightedCohesion = 0; + for (const c of rawClusters) { + const s = c.symbolCount || 0; + totalSymbols += s; + weightedCohesion += (c.cohesion || 0) * s; + } + + const members = await executeQuery(repo.id, ` + MATCH (n)-[:CodeRelation {type: 'MEMBER_OF'}]->(c:Community) + WHERE c.label = '${escaped}' OR c.heuristicLabel = '${escaped}' + RETURN DISTINCT n.name AS name, labels(n)[0] AS type, n.filePath AS filePath + LIMIT 30 + `); + + return { + cluster: { + id: rawClusters[0].id, + label: rawClusters[0].heuristicLabel || rawClusters[0].label, + heuristicLabel: rawClusters[0].heuristicLabel || rawClusters[0].label, + cohesion: totalSymbols > 0 ? weightedCohesion / totalSymbols : 0, + symbolCount: totalSymbols, + subCommunities: rawClusters.length, + }, + members: members.map((m: any) => ({ + name: m.name || m[0], type: m.type || m[1], filePath: m.filePath || m[2], + })), + }; + } + + /** + * Query process detail (steps) directly from graph. + * Used by getProcessDetailResource. + */ + async queryProcessDetail(name: string, repoName?: string): Promise { + const repo = this.resolveRepo(repoName); + await this.ensureInitialized(repo.id); + + const escaped = name.replace(/'/g, "''"); + const processes = await executeQuery(repo.id, ` + MATCH (p:Process) + WHERE p.label = '${escaped}' OR p.heuristicLabel = '${escaped}' + RETURN p.id AS id, p.label AS label, p.heuristicLabel AS heuristicLabel, p.processType AS processType, p.stepCount AS stepCount + LIMIT 1 + `); + if (processes.length === 0) return { error: `Process '${name}' not found` }; + + const proc = processes[0]; + const procId = proc.id || proc[0]; + const steps = await executeQuery(repo.id, ` + 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 + `); + + 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], + })), + }; + } + async disconnect(): Promise { await closeKuzu(); // close all connections await disposeEmbedder(); diff --git a/gitnexus/src/mcp/resources.ts b/gitnexus/src/mcp/resources.ts index af6317b1a..ffba25db2 100644 --- a/gitnexus/src/mcp/resources.ts +++ b/gitnexus/src/mcp/resources.ts @@ -25,46 +25,21 @@ export interface ResourceTemplate { /** * Static resources — includes per-repo resources and the global repos list */ -export function getResourceDefinitions(backend: LocalBackend): ResourceDefinition[] { - const resources: ResourceDefinition[] = [ +export function getResourceDefinitions(): ResourceDefinition[] { + return [ { uri: 'gitnexus://repos', name: 'All Indexed Repositories', description: 'List of all indexed repos with stats. Read this first to discover available repos.', mimeType: 'text/yaml', }, + { + uri: 'gitnexus://setup', + name: 'GitNexus Setup Content', + description: 'Returns AGENTS.md content for all indexed repos. Useful for setup/onboarding.', + mimeType: 'text/markdown', + }, ]; - - // Add per-repo context resources - const repos = backend.listRepos(); - for (const repo of repos) { - resources.push({ - uri: `gitnexus://repo/${repo.name}/context`, - name: `${repo.name} Overview`, - description: `Codebase stats and available tools for ${repo.name}`, - mimeType: 'text/yaml', - }); - resources.push({ - uri: `gitnexus://repo/${repo.name}/clusters`, - name: `${repo.name} Clusters`, - description: `All functional clusters for ${repo.name}`, - mimeType: 'text/yaml', - }); - resources.push({ - uri: `gitnexus://repo/${repo.name}/processes`, - name: `${repo.name} Processes`, - description: `All execution flows for ${repo.name}`, - mimeType: 'text/yaml', - }); - resources.push({ - uri: `gitnexus://repo/${repo.name}/schema`, - name: `${repo.name} Schema`, - description: `Graph schema for Cypher queries on ${repo.name}`, - mimeType: 'text/yaml', - }); - } - - return resources; } /** @@ -72,10 +47,34 @@ export function getResourceDefinitions(backend: LocalBackend): ResourceDefinitio */ export function getResourceTemplates(): ResourceTemplate[] { return [ + { + uriTemplate: 'gitnexus://repo/{name}/context', + name: 'Repo Overview', + description: 'Codebase stats, staleness check, and available tools', + mimeType: 'text/yaml', + }, + { + uriTemplate: 'gitnexus://repo/{name}/clusters', + name: 'Repo Modules', + description: 'All functional areas (Leiden clusters)', + mimeType: 'text/yaml', + }, + { + uriTemplate: 'gitnexus://repo/{name}/processes', + name: 'Repo Processes', + description: 'All execution flows', + mimeType: 'text/yaml', + }, + { + uriTemplate: 'gitnexus://repo/{name}/schema', + name: 'Graph Schema', + description: 'Node/edge schema for Cypher queries', + mimeType: 'text/yaml', + }, { uriTemplate: 'gitnexus://repo/{name}/cluster/{clusterName}', - name: 'Cluster Detail', - description: 'Deep dive into a specific cluster', + name: 'Module Detail', + description: 'Deep dive into a specific functional area', mimeType: 'text/yaml', }, { @@ -92,6 +91,7 @@ export function getResourceTemplates(): ResourceTemplate[] { */ function parseUri(uri: string): { repoName?: string; resourceType: string; param?: string } { if (uri === 'gitnexus://repos') return { resourceType: 'repos' }; + if (uri === 'gitnexus://setup') return { resourceType: 'setup' }; // Repo-scoped: gitnexus://repo/{name}/context const repoMatch = uri.match(/^gitnexus:\/\/repo\/([^/]+)\/(.+)$/); @@ -122,6 +122,11 @@ export async function readResource(uri: string, backend: LocalBackend): Promise< if (parsed.resourceType === 'repos') { return getReposResource(backend); } + + // Setup resource — returns AGENTS.md content for all repos + if (parsed.resourceType === 'setup') { + return getSetupResource(backend); + } const repoName = parsed.repoName; @@ -194,15 +199,6 @@ async function getContextResource(backend: LocalBackend, repoName?: string): Pro const repoPath = repo.repoPath; const lastCommit = repo.lastCommit || 'HEAD'; const staleness = repoPath ? checkStaleness(repoPath, lastCommit) : { isStale: false, commitsBehind: 0 }; - - // Get aggregated cluster count (matches what overview/clusters resource shows) - let clusterCount = context.stats.communityCount; - try { - const overview = await backend.callTool('overview', { showClusters: true, showProcesses: false, limit: 100, repo: repoName }); - if (overview.clusters) { - clusterCount = overview.clusters.length; - } - } catch { /* fall back to raw count */ } const lines: string[] = [ `project: ${context.projectName}`, @@ -217,45 +213,44 @@ async function getContextResource(backend: LocalBackend, repoName?: string): Pro lines.push('stats:'); lines.push(` files: ${context.stats.fileCount}`); lines.push(` symbols: ${context.stats.functionCount}`); - lines.push(` clusters: ${clusterCount}`); lines.push(` processes: ${context.stats.processCount}`); lines.push(''); lines.push('tools_available:'); - lines.push(' - list_repos: Discover all indexed repositories'); - lines.push(' - search: Hybrid semantic + keyword search'); - lines.push(' - explore: Deep dive on symbol/cluster/process'); - lines.push(' - impact: Blast radius analysis'); - lines.push(' - overview: List all clusters and processes'); + lines.push(' - query: Process-grouped code intelligence (execution flows related to a concept)'); + lines.push(' - context: 360-degree symbol view (categorized refs, process participation)'); + lines.push(' - impact: Blast radius analysis (what breaks if you change a symbol)'); + lines.push(' - detect_changes: Git-diff impact analysis (what do your changes affect)'); + lines.push(' - rename: Multi-file coordinated rename with confidence tags'); lines.push(' - cypher: Raw graph queries'); + lines.push(' - list_repos: Discover all indexed repositories'); lines.push(''); lines.push('re_index: Run `npx gitnexus analyze` in terminal if data is stale'); lines.push(''); lines.push('resources_available:'); lines.push(' - gitnexus://repos: All indexed repositories'); - lines.push(` - gitnexus://repo/${context.projectName}/clusters: All clusters`); - lines.push(` - gitnexus://repo/${context.projectName}/processes: All processes`); - lines.push(` - gitnexus://repo/${context.projectName}/cluster/{name}: Cluster details`); + lines.push(` - gitnexus://repo/${context.projectName}/clusters: All functional areas`); + lines.push(` - gitnexus://repo/${context.projectName}/processes: All execution flows`); + lines.push(` - gitnexus://repo/${context.projectName}/cluster/{name}: Module details`); lines.push(` - gitnexus://repo/${context.projectName}/process/{name}: Process trace`); return lines.join('\n'); } /** - * Clusters resource + * Clusters resource — queries graph directly via backend.queryClusters() */ async function getClustersResource(backend: LocalBackend, repoName?: string): Promise { try { - // Request more than we display so aggregation has enough raw data - const result = await backend.callTool('overview', { showClusters: true, showProcesses: false, limit: 100, repo: repoName }); - + const result = await backend.queryClusters(repoName, 100); + if (!result.clusters || result.clusters.length === 0) { - return 'clusters: []\n# No clusters detected. Run: gitnexus analyze'; + return 'modules: []\n# No functional areas detected. Run: gitnexus analyze'; } - + const displayLimit = 20; - const lines: string[] = ['clusters:']; + const lines: string[] = ['modules:']; const toShow = result.clusters.slice(0, displayLimit); - + for (const cluster of toShow) { const label = cluster.heuristicLabel || cluster.label || cluster.id; lines.push(` - name: "${label}"`); @@ -263,15 +258,12 @@ async function getClustersResource(backend: LocalBackend, repoName?: string): Pr if (cluster.cohesion) { lines.push(` cohesion: ${(cluster.cohesion * 100).toFixed(0)}%`); } - if (cluster.subCommunities && cluster.subCommunities > 1) { - lines.push(` sub_clusters: ${cluster.subCommunities}`); - } } - + if (result.clusters.length > displayLimit) { - lines.push(`\n# Showing top ${displayLimit} of ${result.clusters.length} clusters. Use gitnexus_search or gitnexus_explore for more.`); + lines.push(`\n# Showing top ${displayLimit} of ${result.clusters.length} modules. Use gitnexus_query for deeper search.`); } - + return lines.join('\n'); } catch (err: any) { return `error: ${err.message}`; @@ -279,31 +271,31 @@ async function getClustersResource(backend: LocalBackend, repoName?: string): Pr } /** - * Processes resource + * Processes resource — queries graph directly via backend.queryProcesses() */ async function getProcessesResource(backend: LocalBackend, repoName?: string): Promise { try { - const result = await backend.callTool('overview', { showClusters: false, showProcesses: true, limit: 50, repo: repoName }); - + const result = await backend.queryProcesses(repoName, 50); + if (!result.processes || result.processes.length === 0) { return 'processes: []\n# No processes detected. Run: gitnexus analyze'; } - + const displayLimit = 20; const lines: string[] = ['processes:']; const toShow = result.processes.slice(0, displayLimit); - + for (const proc of toShow) { const label = proc.heuristicLabel || proc.label || proc.id; lines.push(` - name: "${label}"`); lines.push(` type: ${proc.processType || 'unknown'}`); lines.push(` steps: ${proc.stepCount || 0}`); } - + if (result.processes.length > displayLimit) { - lines.push(`\n# Showing top ${displayLimit} of ${result.processes.length} processes. Use gitnexus_explore for more.`); + lines.push(`\n# Showing top ${displayLimit} of ${result.processes.length} processes. Use gitnexus_query for deeper search.`); } - + return lines.join('\n'); } catch (err: any) { return `error: ${err.message}`; @@ -318,22 +310,29 @@ function getSchemaResource(): string { nodes: - File: Source code files + - Folder: Directory containers - Function: Functions and arrow functions - Class: Class definitions - Interface: Interface/type definitions - Method: Class methods - - Community: Functional cluster (Leiden algorithm) + - CodeElement: Catch-all for other code elements + - Community: Auto-detected functional area (Leiden algorithm) - Process: Execution flow trace +additional_node_types: "Multi-language: Struct, Enum, Macro, Typedef, Union, Namespace, Trait, Impl, TypeAlias, Const, Static, Property, Record, Delegate, Annotation, Constructor, Template, Module (use backticks in queries: \`Struct\`, \`Enum\`, etc.)" + relationships: + - CONTAINS: File/Folder contains child + - DEFINES: File defines a symbol - CALLS: Function/method invocation - IMPORTS: Module imports - EXTENDS: Class inheritance - IMPLEMENTS: Interface implementation - - DEFINES: File defines symbol - MEMBER_OF: Symbol belongs to community - STEP_IN_PROCESS: Symbol is step N in process +relationship_table: "All relationships use a single CodeRelation table with a 'type' property. Properties: type (STRING), confidence (DOUBLE), reason (STRING), step (INT32)" + example_queries: find_callers: | MATCH (caller)-[:CodeRelation {type: 'CALLS'}]->(f:Function {name: "myFunc"}) @@ -353,31 +352,28 @@ example_queries: } /** - * Cluster detail resource + * Cluster detail resource — queries graph directly via backend.queryClusterDetail() */ async function getClusterDetailResource(name: string, backend: LocalBackend, repoName?: string): Promise { try { - const result = await backend.callTool('explore', { name, type: 'cluster', repo: repoName }); - + const result = await backend.queryClusterDetail(name, repoName); + if (result.error) { return `error: ${result.error}`; } - + const cluster = result.cluster; const members = result.members || []; - + const lines: string[] = [ - `name: "${cluster.heuristicLabel || cluster.label || cluster.id}"`, + `module: "${cluster.heuristicLabel || cluster.label || cluster.id}"`, `symbols: ${cluster.symbolCount || members.length}`, ]; - + if (cluster.cohesion) { lines.push(`cohesion: ${(cluster.cohesion * 100).toFixed(0)}%`); } - if (cluster.subCommunities && cluster.subCommunities > 1) { - lines.push(`sub_clusters: ${cluster.subCommunities}`); - } - + if (members.length > 0) { lines.push(''); lines.push('members:'); @@ -390,7 +386,7 @@ async function getClusterDetailResource(name: string, backend: LocalBackend, rep lines.push(` # ... and ${members.length - 20} more`); } } - + return lines.join('\n'); } catch (err: any) { return `error: ${err.message}`; @@ -398,25 +394,25 @@ async function getClusterDetailResource(name: string, backend: LocalBackend, rep } /** - * Process detail resource + * Process detail resource — queries graph directly via backend.queryProcessDetail() */ async function getProcessDetailResource(name: string, backend: LocalBackend, repoName?: string): Promise { try { - const result = await backend.callTool('explore', { name, type: 'process', repo: repoName }); - + const result = await backend.queryProcessDetail(name, repoName); + if (result.error) { return `error: ${result.error}`; } - + const proc = result.process; const steps = result.steps || []; - + const lines: string[] = [ `name: "${proc.heuristicLabel || proc.label || proc.id}"`, `type: ${proc.processType || 'unknown'}`, `step_count: ${proc.stepCount || steps.length}`, ]; - + if (steps.length > 0) { lines.push(''); lines.push('trace:'); @@ -424,9 +420,54 @@ async function getProcessDetailResource(name: string, backend: LocalBackend, rep lines.push(` ${step.step}: ${step.name} (${step.filePath})`); } } - + return lines.join('\n'); } catch (err: any) { return `error: ${err.message}`; } } + +/** + * Setup resource — generates AGENTS.md content for all indexed repos. + * Useful for `gitnexus setup` onboarding or dynamic content injection. + */ +async function getSetupResource(backend: LocalBackend): Promise { + const repos = backend.listRepos(); + + if (repos.length === 0) { + return '# GitNexus\n\nNo repositories indexed. Run: `npx gitnexus analyze` in a repository.'; + } + + const sections: string[] = []; + + for (const repo of repos) { + const stats = repo.stats || {}; + const lines = [ + `# GitNexus MCP — ${repo.name}`, + '', + `This project is indexed by GitNexus as **${repo.name}** (${stats.nodes || 0} symbols, ${stats.edges || 0} relationships, ${stats.processes || 0} execution flows).`, + '', + '## Tools', + '', + '| 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 |', + '| `list_repos` | Discover indexed repos |', + '', + '## Resources', + '', + `- \`gitnexus://repo/${repo.name}/context\` — Stats, staleness check`, + `- \`gitnexus://repo/${repo.name}/clusters\` — All functional areas`, + `- \`gitnexus://repo/${repo.name}/processes\` — All execution flows`, + `- \`gitnexus://repo/${repo.name}/schema\` — Graph schema for Cypher`, + ]; + sections.push(lines.join('\n')); + } + + return sections.join('\n\n---\n\n'); +} diff --git a/gitnexus/src/mcp/server.ts b/gitnexus/src/mcp/server.ts index f676a1260..ae4820acd 100644 --- a/gitnexus/src/mcp/server.ts +++ b/gitnexus/src/mcp/server.ts @@ -7,7 +7,7 @@ * * Supports multiple indexed repositories via the global registry. * - * Tools: list_repos, search, cypher, overview, explore, impact, analyze + * Tools: list_repos, query, cypher, context, impact, detect_changes, rename * Resources: repos, repo/{name}/context, repo/{name}/clusters, ... */ @@ -19,6 +19,8 @@ import { ListResourcesRequestSchema, ReadResourceRequestSchema, ListResourceTemplatesRequestSchema, + ListPromptsRequestSchema, + GetPromptRequestSchema, } from '@modelcontextprotocol/sdk/types.js'; import { GITNEXUS_TOOLS } from './tools.js'; import type { LocalBackend } from './local/local-backend.js'; @@ -42,31 +44,31 @@ function getNextStepHint(toolName: string, args: Record | undefined case 'list_repos': return `\n\n---\n**Next:** READ gitnexus://repo/{name}/context for any repo above to get its overview and check staleness.`; - case 'search': - return `\n\n---\n**Next:** To understand a result in context, use explore({name: "", type: "symbol"${repoParam}}) to see its callers, callees, and cluster membership.`; + case 'query': + return `\n\n---\n**Next:** To understand a specific symbol in depth, use context({name: ""${repoParam}}) to see categorized refs and process participation.`; - case 'explore': { - const exploreType = args?.type || 'symbol'; - if (exploreType === 'symbol') { - return `\n\n---\n**Next:** If planning changes, use impact({target: "${args?.name || ''}", direction: "upstream"${repoParam}}) to check blast radius. To see execution flows, READ gitnexus://repo/${repoPath}/processes.`; - } - if (exploreType === 'cluster') { - return `\n\n---\n**Next:** To drill into a specific symbol, use explore({name: "", type: "symbol"${repoParam}}). To see execution flows, READ gitnexus://repo/${repoPath}/processes.`; - } - if (exploreType === 'process') { - return `\n\n---\n**Next:** To explore any step in detail, use explore({name: "", type: "symbol"${repoParam}}).`; - } - return ''; - } - - case 'overview': - return `\n\n---\n**Next:** To drill into a cluster, READ gitnexus://repo/${repoPath}/cluster/{name} or use explore({name: "", type: "cluster"${repoParam}}).`; + case 'context': + return `\n\n---\n**Next:** If planning changes, use impact({target: "${args?.name || ''}", direction: "upstream"${repoParam}}) to check blast radius. To see execution flows, READ gitnexus://repo/${repoPath}/processes.`; case 'impact': return `\n\n---\n**Next:** Review d=1 items first (WILL BREAK). To check affected execution flows, READ gitnexus://repo/${repoPath}/processes.`; + case 'detect_changes': + return `\n\n---\n**Next:** Review affected processes. Use context() on high-risk changed symbols. READ gitnexus://repo/${repoPath}/process/{name} for full execution traces.`; + + case 'rename': + return `\n\n---\n**Next:** Run detect_changes(${repoParam ? `{repo: "${repo}"}` : ''}) to verify no unexpected side effects from the rename.`; + case 'cypher': - return `\n\n---\n**Next:** To explore a result symbol, use explore({name: "", type: "symbol"${repoParam}}). For schema reference, READ gitnexus://repo/${repoPath}/schema.`; + return `\n\n---\n**Next:** To explore a result symbol, use context({name: ""${repoParam}}). For schema reference, READ gitnexus://repo/${repoPath}/schema.`; + + // Legacy tool names — still return useful hints + case 'search': + return `\n\n---\n**Next:** To understand a result in context, use context({name: ""${repoParam}}).`; + case 'explore': + return `\n\n---\n**Next:** If planning changes, use impact({target: "", direction: "upstream"${repoParam}}).`; + case 'overview': + return `\n\n---\n**Next:** To drill into an area, READ gitnexus://repo/${repoPath}/cluster/{name}. To see execution flows, READ gitnexus://repo/${repoPath}/processes.`; default: return ''; @@ -83,13 +85,14 @@ export async function startMCPServer(backend: LocalBackend): Promise { capabilities: { tools: {}, resources: {}, + prompts: {}, }, } ); // Handle list resources request server.setRequestHandler(ListResourcesRequestSchema, async () => { - const resources = getResourceDefinitions(backend); + const resources = getResourceDefinitions(); return { resources: resources.map(r => ({ uri: r.uri, @@ -182,6 +185,81 @@ export async function startMCPServer(backend: LocalBackend): Promise { } }); + // Handle list prompts request + server.setRequestHandler(ListPromptsRequestSchema, async () => ({ + prompts: [ + { + name: 'detect_impact', + description: 'Analyze the impact of your current changes before committing. Guides through scope selection, change detection, process analysis, and risk assessment.', + arguments: [ + { name: 'scope', description: 'What to analyze: unstaged, staged, all, or compare', required: false }, + { name: 'base_ref', description: 'Branch/commit for compare scope', required: false }, + ], + }, + { + name: 'generate_map', + description: 'Generate architecture documentation from the knowledge graph. Creates a codebase overview with execution flows and mermaid diagrams.', + arguments: [ + { name: 'repo', description: 'Repository name (omit if only one indexed)', required: false }, + ], + }, + ], + })); + + // Handle get prompt request + server.setRequestHandler(GetPromptRequestSchema, async (request) => { + const { name, arguments: args } = request.params; + + if (name === 'detect_impact') { + const scope = args?.scope || 'all'; + const baseRef = args?.base_ref || ''; + return { + messages: [ + { + role: 'user' as const, + content: { + type: 'text' as const, + text: `Analyze the impact of my current code changes before committing. + +Follow these steps: +1. Run \`detect_changes(${JSON.stringify({ scope, ...(baseRef ? { base_ref: baseRef } : {}) })})\` to find what changed and affected processes +2. For each changed symbol in critical processes, run \`context({name: ""})\` to see its full reference graph +3. For any high-risk items (many callers or cross-process), run \`impact({target: "", direction: "upstream"})\` for blast radius +4. Summarize: changes, affected processes, risk level, and recommended actions + +Present the analysis as a clear risk report.`, + }, + }, + ], + }; + } + + if (name === 'generate_map') { + const repo = args?.repo || ''; + return { + messages: [ + { + role: 'user' as const, + content: { + type: 'text' as const, + text: `Generate architecture documentation for this codebase using the knowledge graph. + +Follow these steps: +1. READ \`gitnexus://repo/${repo || '{name}'}/context\` for codebase stats +2. READ \`gitnexus://repo/${repo || '{name}'}/clusters\` to see all functional areas +3. READ \`gitnexus://repo/${repo || '{name}'}/processes\` to see all execution flows +4. For the top 5 most important processes, READ \`gitnexus://repo/${repo || '{name}'}/process/{name}\` for step-by-step traces +5. Generate a mermaid architecture diagram showing the major areas and their connections +6. Write an ARCHITECTURE.md file with: overview, functional areas, key execution flows, and the mermaid diagram`, + }, + }, + ], + }; + } + + throw new Error(`Unknown prompt: ${name}`); + }); + // Connect to stdio transport const transport = new StdioServerTransport(); await server.connect(transport); diff --git a/gitnexus/src/mcp/tools.ts b/gitnexus/src/mcp/tools.ts index c4314638c..20be63278 100644 --- a/gitnexus/src/mcp/tools.ts +++ b/gitnexus/src/mcp/tools.ts @@ -32,7 +32,7 @@ WHEN TO USE: First step when multiple repos are indexed, or to discover availabl AFTER THIS: READ gitnexus://repo/{name}/context for the repo you want to work with. When multiple repos are indexed, you MUST specify the "repo" parameter -on other tools (search, explore, impact, etc.) to target the correct one.`, +on other tools (query, context, impact, etc.) to target the correct one.`, inputSchema: { type: 'object', properties: {}, @@ -40,25 +40,28 @@ on other tools (search, explore, impact, etc.) to target the correct one.`, }, }, { - name: 'search', - description: `Hybrid search (keyword + semantic) across the codebase. -Returns code nodes with cluster context and optional graph connections. + name: 'query', + description: `Query the code knowledge graph for execution flows related to a concept. +Returns processes (call chains) ranked by relevance, each with its symbols and file locations. -WHEN TO USE: Finding code by concept, name, or keyword. Use alongside grep/IDE search for richer results. -AFTER THIS: Use explore() on interesting results to see callers/callees and cluster membership. +WHEN TO USE: Understanding how code works together. Use this when you need execution flows and relationships, not just file matches. Complements grep/IDE search. +AFTER THIS: Use context() on a specific symbol for 360-degree view (callers, callees, categorized refs). -Complements grep/IDE search by adding: -- Cluster context (which functional area each result belongs to) -- Relationship data (callers/callees with depth=full) -- Hybrid ranking (BM25 + semantic via Reciprocal Rank Fusion) +Returns results grouped by process (execution flow): +- processes: ranked execution flows with relevance priority +- process_symbols: all symbols in those flows with file locations +- definitions: standalone types/interfaces not in any process -RETURNS: Array of {name, type, filePath, cluster?, connections[]?, fusedScore, searchSource}`, +Hybrid ranking: BM25 keyword + semantic vector search, ranked by Reciprocal Rank Fusion.`, inputSchema: { type: 'object', 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' }, + task_context: { type: 'string', description: 'What you are working on (e.g., "adding OAuth support"). Helps ranking.' }, + goal: { type: 'string', description: 'What you want to find (e.g., "existing auth validation logic"). Helps ranking.' }, + limit: { type: 'number', description: 'Max processes to return (default: 5)', default: 5 }, + max_symbols: { type: 'number', description: 'Max symbols per process (default: 10)', default: 10 }, + include_content: { type: 'boolean', description: 'Include full symbol source code (default: false)', default: false }, repo: { type: 'string', description: 'Repository name or path. Omit if only one repo is indexed.' }, }, required: ['query'], @@ -69,26 +72,30 @@ RETURNS: Array of {name, type, filePath, cluster?, connections[]?, fusedScore, s description: `Execute Cypher query against the code knowledge graph. WHEN TO USE: Complex structural queries that search/explore can't answer. READ gitnexus://repo/{name}/schema first for the full schema. -AFTER THIS: Use explore() on result symbols for deeper context. +AFTER THIS: Use context() on result symbols for deeper context. SCHEMA: -- Nodes: File, Folder, Function, Class, Interface, Method, Community, Process -- Edges via CodeRelation.type: CALLS, IMPORTS, EXTENDS, IMPLEMENTS, CONTAINS, DEFINES, MEMBER_OF, STEP_IN_PROCESS +- Nodes: File, Folder, Function, Class, Interface, Method, CodeElement, Community, Process +- Multi-language nodes (use backticks): \`Struct\`, \`Enum\`, \`Trait\`, \`Impl\`, etc. +- All edges via single CodeRelation table with 'type' property +- Edge types: CONTAINS, DEFINES, CALLS, IMPORTS, EXTENDS, IMPLEMENTS, MEMBER_OF, STEP_IN_PROCESS +- Edge properties: type (STRING), confidence (DOUBLE), reason (STRING), step (INT32) EXAMPLES: • Find callers of a function: MATCH (a)-[:CodeRelation {type: 'CALLS'}]->(b:Function {name: "validateUser"}) RETURN a.name, a.filePath -• Find all functions in a community: - MATCH (f:Function)-[:CodeRelation {type: 'MEMBER_OF'}]->(c:Community {label: "Auth"}) RETURN f.name +• Find community members: + MATCH (f)-[:CodeRelation {type: 'MEMBER_OF'}]->(c:Community) WHERE c.heuristicLabel = "Auth" RETURN f.name -• Find steps in a process: - MATCH (s)-[r:CodeRelation {type: 'STEP_IN_PROCESS'}]->(p:Process {label: "UserLogin"}) RETURN s.name, r.step ORDER BY r.step +• Trace a process: + MATCH (s)-[r:CodeRelation {type: 'STEP_IN_PROCESS'}]->(p:Process) WHERE p.heuristicLabel = "UserLogin" RETURN s.name, r.step ORDER BY r.step TIPS: -- All relationships use CodeRelation table with 'type' property -- Community = functional cluster detected by Leiden algorithm -- Process = execution flow trace from entry point to terminal`, +- All relationships use single CodeRelation table — filter with {type: 'CALLS'} etc. +- Community = auto-detected functional area (Leiden algorithm) +- Process = execution flow trace from entry point to terminal +- Use heuristicLabel (not label) for human-readable community/process names`, inputSchema: { type: 'object', properties: { @@ -99,72 +106,84 @@ TIPS: }, }, { - name: 'explore', - description: `Deep dive on a symbol, cluster, or process. + name: 'context', + description: `360-degree view of a single code symbol. +Shows categorized incoming/outgoing references (calls, imports, extends, implements), process participation, and file location. -WHEN TO USE: After search() to understand context, or to drill into a specific node. -AFTER THIS (symbol): Use impact() if planning changes, or READ process resource to see execution flows. -AFTER THIS (cluster): Use explore() on specific members, or READ processes resource. -AFTER THIS (process): Use explore() on individual steps for detail. +WHEN TO USE: After query() to understand a specific symbol in depth. When you need to know all callers, callees, and what execution flows a symbol participates in. +AFTER THIS: Use impact() if planning changes, or READ gitnexus://repo/{name}/process/{processName} for full execution trace. -TYPE: symbol | cluster | process - -For SYMBOL: Shows cluster membership, process participation, callers/callees -For CLUSTER: Shows members, cohesion score, processes touching it -For PROCESS: Shows step-by-step trace, clusters traversed, entry/terminal points`, +Handles disambiguation: if multiple symbols share the same name, returns candidates for you to pick from. Use uid param for zero-ambiguity lookup from prior results.`, inputSchema: { type: 'object', properties: { - name: { type: 'string', description: 'Name of symbol, cluster, or process to explore' }, - type: { type: 'string', description: 'Type: symbol, cluster, or process' }, - repo: { type: 'string', description: 'Repository name or path. Omit if only one repo is indexed.' }, - }, - required: ['name', 'type'], - }, - }, - { - name: 'overview', - description: `Get codebase map showing all clusters and processes. - -WHEN TO USE: Understanding overall architecture. Prefer READ gitnexus://repo/{name}/clusters resource for a lighter-weight alternative. -AFTER THIS: Drill into a specific cluster with explore({type: "cluster"}) or search() for specific code. - -Returns: -- All communities (clusters) with member counts and cohesion scores -- All processes with step counts and types (intra/cross-community) -- High-level architectural view`, - inputSchema: { - type: 'object', - properties: { - showProcesses: { type: 'boolean', description: 'Include process list', default: true }, - showClusters: { type: 'boolean', description: 'Include cluster list', default: true }, - limit: { type: 'number', description: 'Max items per category', default: 20 }, + name: { type: 'string', description: 'Symbol name (e.g., "validateUser", "AuthService")' }, + uid: { type: 'string', description: 'Direct symbol UID from prior tool results (zero-ambiguity lookup)' }, + file_path: { type: 'string', description: 'File path to disambiguate common names' }, + include_content: { type: 'boolean', description: 'Include full symbol source code (default: false)', default: false }, repo: { type: 'string', description: 'Repository name or path. Omit if only one repo is indexed.' }, }, required: [], }, }, + { + name: 'detect_changes', + description: `Analyze uncommitted git changes and find affected execution flows. +Maps git diff hunks to indexed symbols, then traces which processes are impacted. + +WHEN TO USE: Before committing — to understand what your changes affect. Pre-commit review, PR preparation. +AFTER THIS: Review affected processes. Use context() on high-risk symbols. READ gitnexus://repo/{name}/process/{name} for full traces. + +Returns: changed symbols, affected processes, and a risk summary.`, + inputSchema: { + type: 'object', + properties: { + scope: { type: 'string', description: 'What to analyze: "unstaged" (default), "staged", "all", or "compare"', enum: ['unstaged', 'staged', 'all', 'compare'], default: 'unstaged' }, + base_ref: { type: 'string', description: 'Branch/commit for "compare" scope (e.g., "main")' }, + repo: { type: 'string', description: 'Repository name or path. Omit if only one repo is indexed.' }, + }, + required: [], + }, + }, + { + name: 'rename', + description: `Multi-file coordinated rename using the knowledge graph + text search. +Finds all references via graph (high confidence) and regex text search (lower confidence). Preview by default. + +WHEN TO USE: Renaming a function, class, method, or variable across the codebase. Safer than find-and-replace. +AFTER THIS: Run detect_changes() to verify no unexpected side effects. + +Each edit is tagged with confidence: +- "graph": found via knowledge graph relationships (high confidence, safe to accept) +- "text_search": found via regex text search (lower confidence, review carefully)`, + inputSchema: { + type: 'object', + properties: { + symbol_name: { type: 'string', description: 'Current symbol name to rename' }, + symbol_uid: { type: 'string', description: 'Direct symbol UID from prior tool results (zero-ambiguity)' }, + new_name: { type: 'string', description: 'The new name for the symbol' }, + file_path: { type: 'string', description: 'File path to disambiguate common names' }, + dry_run: { type: 'boolean', description: 'Preview edits without modifying files (default: true)', default: true }, + repo: { type: 'string', description: 'Repository name or path. Omit if only one repo is indexed.' }, + }, + required: ['new_name'], + }, + }, { name: 'impact', - description: `Analyze the impact of changing a code element. -Returns all nodes affected by modifying the target, with distance, edge type, and confidence. + description: `Analyze the blast radius of changing a code symbol. +Returns all symbols affected by modifying the target, grouped by depth with edge types and confidence. -WHEN TO USE: Before making code changes, especially refactoring, renaming, or modifying shared code. Shows what would be affected. -AFTER THIS: Review d=1 items (WILL BREAK). READ gitnexus://repo/{name}/processes to check affected flows. If risk > MEDIUM, warn the user. - -Output includes: -- Affected processes (with step positions) -- Affected clusters (direct/indirect) -- Risk assessment (critical/high/medium/low) -- Callers/dependents grouped by depth - -EdgeType: CALLS, IMPORTS, EXTENDS, IMPLEMENTS -Confidence: 100% = certain, <80% = fuzzy match +WHEN TO USE: Before making code changes — especially refactoring, renaming, or modifying shared code. Shows what would break. +AFTER THIS: Review d=1 items (WILL BREAK). READ gitnexus://repo/{name}/processes to check affected execution flows. Depth groups: - d=1: WILL BREAK (direct callers/importers) - d=2: LIKELY AFFECTED (indirect) -- d=3: MAY NEED TESTING (transitive)`, +- d=3: MAY NEED TESTING (transitive) + +EdgeType: CALLS, IMPORTS, EXTENDS, IMPLEMENTS +Confidence: 1.0 = certain, <0.8 = fuzzy match`, inputSchema: { type: 'object', properties: {