gitnexus
$
npx mdskill add TerminalSkills/skills/gitnexusConstruct browser-based code knowledge graphs for zero-server exploration.
- Enables interactive codebase analysis without external server dependencies.
- Integrates tree-sitter WASM for parsing and force-directed layouts for visualization.
- Employs Graph RAG to answer natural language queries about code structure.
- Delivers rendered graph visualizations directly within the browser environment.
SKILL.md
.github/skills/gitnexusView on GitHub ↗
---
name: gitnexus
description: >-
Build client-side code knowledge graphs with built-in Graph RAG for code exploration.
Use when: analyzing large codebases in the browser, building zero-server code intelligence
tools, creating interactive code exploration UIs.
license: MIT
compatibility: "Browser, TypeScript"
metadata:
author: terminal-skills
version: "1.0.0"
category: development
tags: [knowledge-graph, code-analysis, graph-rag, browser, visualization]
use-cases:
- "Build a browser-based code exploration tool from a GitHub repo URL"
- "Create an interactive knowledge graph of any codebase without a server"
- "Ask questions about code using Graph RAG in the browser"
agents: [claude-code, openai-codex, gemini-cli, cursor]
---
# 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:
1. **Set up tree-sitter WASM** — Load language grammars for the target languages
2. **Parse the codebase** — Extract AST nodes (functions, classes, imports, exports)
3. **Build the graph** — Create nodes and edges representing code relationships
4. **Visualize** — Render with force-directed graph (D3 or force-graph library)
5. **Enable Graph RAG** — Embed graph nodes, allow natural-language queries
### Code Parsing with Tree-sitter WASM
```typescript
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
```typescript
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
```typescript
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
```typescript
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
```bash
npm create vite@latest code-nexus -- --template vanilla-ts
cd code-nexus
npm install web-tree-sitter force-graph @xenova/transformers
```
```typescript
// 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
```typescript
// 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
1. **Lazy-load grammars** — Only load tree-sitter WASM grammars for languages present in the repo
2. **OPFS for large repos** — Store cloned files in Origin Private File System for persistence
3. **Incremental parsing** — Re-parse only changed files, not the entire repo
4. **Limit graph size** — For repos with 1000+ files, allow filtering by directory or file type
5. **Web Workers** — Run parsing and embedding in Web Workers to keep the UI responsive
6. **Cache embeddings** — Store in IndexedDB so you don't re-embed on every page load