Skills gitnexus
install
source · Clone the upstream repo
git clone https://github.com/TerminalSkills/skills
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/TerminalSkills/skills "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/gitnexus" ~/.claude/skills/terminalskills-skills-gitnexus && rm -rf "$T"
manifest:
skills/gitnexus/SKILL.mdsource content
GitNexus
Overview
Build client-side code knowledge graphs that run entirely in the browser — no server required. Parse code with tree-sitter WASM, construct a graph of files, functions, classes, and dependencies, visualize it with force-directed layouts, and query it with Graph RAG for natural-language code exploration.
Instructions
When a user asks to build a code knowledge graph, browser-based code explorer, or Graph RAG for code:
- Set up tree-sitter WASM — Load language grammars for the target languages
- Parse the codebase — Extract AST nodes (functions, classes, imports, exports)
- Build the graph — Create nodes and edges representing code relationships
- Visualize — Render with force-directed graph (D3 or force-graph library)
- Enable Graph RAG — Embed graph nodes, allow natural-language queries
Code Parsing with Tree-sitter WASM
import Parser from "web-tree-sitter"; interface CodeNode { id: string; type: "file" | "function" | "class" | "method" | "import" | "export"; name: string; filePath: string; startLine: number; endLine: number; code: string; } interface CodeEdge { source: string; target: string; type: "contains" | "calls" | "imports" | "extends" | "implements"; } async function initParser(language: string): Promise<Parser> { await Parser.init(); const parser = new Parser(); const lang = await Parser.Language.load(`/tree-sitter-${language}.wasm`); parser.setLanguage(lang); return parser; } function extractNodes(tree: Parser.Tree, filePath: string): CodeNode[] { const nodes: CodeNode[] = []; nodes.push({ id: `file:${filePath}`, type: "file", name: filePath.split("/").pop()!, filePath, startLine: 0, endLine: tree.rootNode.endPosition.row, code: "" }); function walk(node: Parser.SyntaxNode) { const nameNode = node.childForFieldName("name"); if ((node.type === "function_declaration" || node.type === "arrow_function") && nameNode) { nodes.push({ id: `fn:${filePath}:${nameNode.text}`, type: "function", name: nameNode.text, filePath, startLine: node.startPosition.row, endLine: node.endPosition.row, code: node.text.slice(0, 500) }); } if (node.type === "class_declaration" && nameNode) { nodes.push({ id: `class:${filePath}:${nameNode.text}`, type: "class", name: nameNode.text, filePath, startLine: node.startPosition.row, endLine: node.endPosition.row, code: node.text.slice(0, 500) }); } if (node.type === "import_statement") { const source = node.descendantsOfType("string")[0]; if (source) nodes.push({ id: `import:${filePath}:${source.text}`, type: "import", name: source.text.replace(/['"]/g, ""), filePath, startLine: node.startPosition.row, endLine: node.endPosition.row, code: node.text }); } for (const child of node.children) walk(child); } walk(tree.rootNode); return nodes; }
Graph Construction
function buildGraph(fileNodes: Map<string, CodeNode[]>): { nodes: CodeNode[]; edges: CodeEdge[] } { const allNodes: CodeNode[] = []; const edges: CodeEdge[] = []; const functionIndex = new Map<string, string>(); for (const [, nodes] of fileNodes) { allNodes.push(...nodes); for (const node of nodes) { if (node.type === "function" || node.type === "method") functionIndex.set(node.name, node.id); } } for (const node of allNodes) { const fileId = `file:${node.filePath}`; if (node.type !== "file") edges.push({ source: fileId, target: node.id, type: "contains" }); if (node.type === "import") { const targetFile = resolveImport(node.name, node.filePath); if (targetFile) edges.push({ source: fileId, target: `file:${targetFile}`, type: "imports" }); } if (node.type === "function" || node.type === "method") { for (const [fnName, fnId] of functionIndex) { if (fnId !== node.id && node.code.includes(fnName + "(")) edges.push({ source: node.id, target: fnId, type: "calls" }); } } } return { nodes: allNodes, edges }; } function resolveImport(importPath: string, fromFile: string): string | null { if (importPath.startsWith(".")) { return `${fromFile.split("/").slice(0, -1).join("/")}/${importPath.replace(/^\.\//, "")}.ts`; } return null; }
Visualization with Force-Graph
import ForceGraph from "force-graph"; function renderGraph(container: HTMLElement, graph: { nodes: CodeNode[]; edges: CodeEdge[] }) { const colorMap: Record<string, string> = { file: "#4a9eff", function: "#50c878", class: "#ff6b6b", method: "#ffa500", import: "#888888", export: "#dda0dd" }; ForceGraph()(container) .graphData({ nodes: graph.nodes.map((n) => ({ id: n.id, name: n.name, type: n.type, val: n.type === "file" ? 8 : n.type === "class" ? 5 : 3 })), links: graph.edges.map((e) => ({ source: e.source, target: e.target, type: e.type })), }) .nodeColor((node: any) => colorMap[node.type] || "#999") .nodeLabel((node: any) => `${node.type}: ${node.name}`) .linkDirectionalArrowLength(4); }
Graph RAG Query
import { pipeline } from "@xenova/transformers"; async function embedNodes(graph: { nodes: CodeNode[] }): Promise<Map<string, number[]>> { const embeddings = new Map<string, number[]>(); const embedder = await pipeline("feature-extraction", "Xenova/all-MiniLM-L6-v2"); for (const node of graph.nodes) { const text = `${node.type} "${node.name}" in ${node.filePath}: ${node.code.slice(0, 200)}`; const result = await embedder(text, { pooling: "mean", normalize: true }); embeddings.set(node.id, Array.from(result.data)); } return embeddings; } function searchGraph(query: number[], embeddings: Map<string, number[]>, topK = 10): string[] { const scores: [string, number][] = []; for (const [id, emb] of embeddings) { let dot = 0, magA = 0, magB = 0; for (let i = 0; i < query.length; i++) { dot += query[i] * emb[i]; magA += query[i] ** 2; magB += emb[i] ** 2; } scores.push([id, dot / (Math.sqrt(magA) * Math.sqrt(magB))]); } return scores.sort((a, b) => b[1] - a[1]).slice(0, topK).map(([id]) => id); }
Examples
Example 1: Build a Browser-Based Code Explorer for a React Project
npm create vite@latest code-nexus -- --template vanilla-ts cd code-nexus npm install web-tree-sitter force-graph @xenova/transformers
// main.ts — Parse a GitHub repo and render its knowledge graph const parser = await initParser("typescript"); const files = await fetchRepoFiles("facebook/react", "packages/react/src"); const fileNodes = new Map<string, CodeNode[]>(); for (const file of files) { const tree = parser.parse(file.content); fileNodes.set(file.path, extractNodes(tree, file.path)); } const graph = buildGraph(fileNodes); renderGraph(document.getElementById("graph")!, graph); // Result: interactive force-directed graph showing React's internal module structure
Example 2: Natural-Language Code Query with Graph RAG
// After building the graph, embed all nodes and query const graph = buildGraph(fileNodes); const embeddings = await embedNodes(graph); // User asks a question about the codebase const embedder = await pipeline("feature-extraction", "Xenova/all-MiniLM-L6-v2"); const qEmb = Array.from((await embedder("How does the authentication middleware work?", { pooling: "mean", normalize: true })).data); const relevantIds = searchGraph(qEmb, embeddings, 5); const context = relevantIds .map((id) => graph.nodes.find((n) => n.id === id)) .filter(Boolean) .map((n) => `[${n!.type}] ${n!.name} (${n!.filePath})\n${n!.code.slice(0, 300)}`) .join("\n---\n"); // Pass context to LLM for a grounded answer about the codebase const response = await fetch("/api/chat", { method: "POST", body: JSON.stringify({ messages: [ { role: "system", content: `Answer using this code context:\n${context}` }, { role: "user", content: "How does the authentication middleware work?" }, ]}), });
Guidelines
- Lazy-load grammars — Only load tree-sitter WASM grammars for languages present in the repo
- OPFS for large repos — Store cloned files in Origin Private File System for persistence
- Incremental parsing — Re-parse only changed files, not the entire repo
- Limit graph size — For repos with 1000+ files, allow filtering by directory or file type
- Web Workers — Run parsing and embedding in Web Workers to keep the UI responsive
- Cache embeddings — Store in IndexedDB so you don't re-embed on every page load