From d932980c1a7d9b83b7dac7552824192d73fdd635 Mon Sep 17 00:00:00 2001 From: Teknium <127238744+teknium1@users.noreply.github.com> Date: Sun, 5 Apr 2026 03:00:19 -0700 Subject: [PATCH] Add gitnexus-explorer optional skill (#5208) Index codebases with GitNexus and serve an interactive knowledge graph web UI via Cloudflare tunnel. No sudo required. Includes: - Full setup/build/serve/tunnel pipeline - Zero-dependency Node.js reverse proxy script - Pitfalls section covering cloudflared config conflicts, Vite allowedHosts, Claude Code artifact cleanup, and browser memory limits for large repos --- .../research/gitnexus-explorer/SKILL.md | 213 ++++++++++++++++++ .../gitnexus-explorer/scripts/proxy.mjs | 92 ++++++++ 2 files changed, 305 insertions(+) create mode 100644 optional-skills/research/gitnexus-explorer/SKILL.md create mode 100644 optional-skills/research/gitnexus-explorer/scripts/proxy.mjs diff --git a/optional-skills/research/gitnexus-explorer/SKILL.md b/optional-skills/research/gitnexus-explorer/SKILL.md new file mode 100644 index 000000000..d57c896ed --- /dev/null +++ b/optional-skills/research/gitnexus-explorer/SKILL.md @@ -0,0 +1,213 @@ +--- +name: gitnexus-explorer +description: Index a codebase with GitNexus and serve an interactive knowledge graph via web UI + Cloudflare tunnel. +version: 1.0.0 +author: Hermes Agent + Teknium +license: MIT +metadata: + hermes: + tags: [gitnexus, code-intelligence, knowledge-graph, visualization] + related_skills: [native-mcp, codebase-inspection] +--- + +# GitNexus Explorer + +Index any codebase into a knowledge graph and serve an interactive web UI for exploring +symbols, call chains, clusters, and execution flows. Tunneled via Cloudflare for remote access. + +## When to Use + +- User wants to visually explore a codebase's architecture +- User asks for a knowledge graph / dependency graph of a repo +- User wants to share an interactive codebase explorer with someone + +## Prerequisites + +- **Node.js** (v18+) — required for GitNexus and the proxy +- **git** — repo must have a `.git` directory +- **cloudflared** — for tunneling (auto-installed to ~/.local/bin if missing) + +## Size Warning + +The web UI renders all nodes in the browser. Repos under ~5,000 files work well. Large +repos (30k+ nodes) will be sluggish or crash the browser tab. The CLI/MCP tools work +at any scale — only the web visualization has this limit. + +## Steps + +### 1. Clone and Build GitNexus (one-time setup) + +```bash +GITNEXUS_DIR="${GITNEXUS_DIR:-$HOME/.local/share/gitnexus}" + +if [ ! -d "$GITNEXUS_DIR/gitnexus-web/dist" ]; then + git clone https://github.com/abhigyanpatwari/GitNexus.git "$GITNEXUS_DIR" + cd "$GITNEXUS_DIR/gitnexus-shared" && npm install && npm run build + cd "$GITNEXUS_DIR/gitnexus-web" && npm install +fi +``` + +### 2. Patch the Web UI for Remote Access + +The web UI defaults to `localhost:4747` for API calls. Patch it to use same-origin +so it works through a tunnel/proxy: + +**File: `$GITNEXUS_DIR/gitnexus-web/src/config/ui-constants.ts`** +Change: +```typescript +export const DEFAULT_BACKEND_URL = 'http://localhost:4747'; +``` +To: +```typescript +export const DEFAULT_BACKEND_URL = typeof window !== 'undefined' && window.location.hostname !== 'localhost' ? window.location.origin : 'http://localhost:4747'; +``` + +**File: `$GITNEXUS_DIR/gitnexus-web/vite.config.ts`** +Add `allowedHosts: true` inside the `server: { }` block (only needed if running dev +mode instead of production build): +```typescript +server: { + allowedHosts: true, + // ... existing config +}, +``` + +Then build the production bundle: +```bash +cd "$GITNEXUS_DIR/gitnexus-web" && npx vite build +``` + +### 3. Index the Target Repo + +```bash +cd /path/to/target-repo +npx gitnexus analyze --skip-agents-md +rm -rf .claude/ # remove Claude Code-specific artifacts +``` + +Add `--embeddings` for semantic search (slower — minutes instead of seconds). + +The index lives in `.gitnexus/` inside the repo (auto-gitignored). + +### 4. Create the Proxy Script + +Write this to a file (e.g., `$GITNEXUS_DIR/proxy.mjs`). It serves the production +web UI and proxies `/api/*` to the GitNexus backend — same origin, no CORS issues, +no sudo, no nginx. + +```javascript +import http from 'node:http'; +import fs from 'node:fs'; +import path from 'node:path'; + +const API_PORT = parseInt(process.env.API_PORT || '4747'); +const DIST_DIR = process.argv[2] || './dist'; +const PORT = parseInt(process.argv[3] || '8888'); + +const MIME = { + '.html': 'text/html', '.js': 'application/javascript', '.css': 'text/css', + '.json': 'application/json', '.png': 'image/png', '.svg': 'image/svg+xml', + '.ico': 'image/x-icon', '.woff2': 'font/woff2', '.woff': 'font/woff', + '.wasm': 'application/wasm', +}; + +function proxyToApi(req, res) { + const opts = { + hostname: '127.0.0.1', port: API_PORT, + path: req.url, method: req.method, headers: req.headers, + }; + const proxy = http.request(opts, (upstream) => { + res.writeHead(upstream.statusCode, upstream.headers); + upstream.pipe(res, { end: true }); + }); + proxy.on('error', () => { res.writeHead(502); res.end('Backend unavailable'); }); + req.pipe(proxy, { end: true }); +} + +function serveStatic(req, res) { + let filePath = path.join(DIST_DIR, req.url === '/' ? 'index.html' : req.url.split('?')[0]); + if (!fs.existsSync(filePath)) filePath = path.join(DIST_DIR, 'index.html'); + const ext = path.extname(filePath); + const mime = MIME[ext] || 'application/octet-stream'; + try { + const data = fs.readFileSync(filePath); + res.writeHead(200, { 'Content-Type': mime, 'Cache-Control': 'public, max-age=3600' }); + res.end(data); + } catch { res.writeHead(404); res.end('Not found'); } +} + +http.createServer((req, res) => { + if (req.url.startsWith('/api')) proxyToApi(req, res); + else serveStatic(req, res); +}).listen(PORT, () => console.log(`GitNexus proxy on http://localhost:${PORT}`)); +``` + +### 5. Start the Services + +```bash +# Terminal 1: GitNexus backend API +npx gitnexus serve & + +# Terminal 2: Proxy (web UI + API on one port) +node "$GITNEXUS_DIR/proxy.mjs" "$GITNEXUS_DIR/gitnexus-web/dist" 8888 & +``` + +Verify: `curl -s http://localhost:8888/api/repos` should return the indexed repo(s). + +### 6. Tunnel with Cloudflare (optional — for remote access) + +```bash +# Install cloudflared if needed (no sudo) +if ! command -v cloudflared &>/dev/null; then + mkdir -p ~/.local/bin + curl -sL https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-amd64 \ + -o ~/.local/bin/cloudflared + chmod +x ~/.local/bin/cloudflared + export PATH="$HOME/.local/bin:$PATH" +fi + +# Start tunnel (--config /dev/null avoids conflicts with existing named tunnels) +cloudflared tunnel --config /dev/null --url http://localhost:8888 --no-autoupdate --protocol http2 +``` + +The tunnel URL (e.g., `https://random-words.trycloudflare.com`) is printed to stderr. +Share it — anyone with the link can explore the graph. + +### 7. Cleanup + +```bash +# Stop services +pkill -f "gitnexus serve" +pkill -f "proxy.mjs" +pkill -f cloudflared + +# Remove index from the target repo +cd /path/to/target-repo +npx gitnexus clean +rm -rf .claude/ +``` + +## Pitfalls + +- **`--config /dev/null` is required for cloudflared** if the user has an existing + named tunnel config at `~/.cloudflared/config.yml`. Without it, the catch-all + ingress rule in the config returns 404 for all quick tunnel requests. + +- **Production build is mandatory for tunneling.** The Vite dev server blocks + non-localhost hosts by default (`allowedHosts`). The production build + Node + proxy avoids this entirely. + +- **The web UI does NOT create `.claude/` or `CLAUDE.md`.** Those are created by + `npx gitnexus analyze`. Use `--skip-agents-md` to suppress the markdown files, + then `rm -rf .claude/` for the rest. These are Claude Code integrations that + hermes-agent users don't need. + +- **Browser memory limit.** The web UI loads the entire graph into browser memory. + Repos with 5k+ files may be sluggish. 30k+ files will likely crash the tab. + +- **Embeddings are optional.** `--embeddings` enables semantic search but takes + minutes on large repos. Skip it for quick exploration; add it if you want + natural language queries via the AI chat panel. + +- **Multiple repos.** `gitnexus serve` serves ALL indexed repos. Index several + repos, start serve once, and the web UI lets you switch between them. diff --git a/optional-skills/research/gitnexus-explorer/scripts/proxy.mjs b/optional-skills/research/gitnexus-explorer/scripts/proxy.mjs new file mode 100644 index 000000000..65b34e745 --- /dev/null +++ b/optional-skills/research/gitnexus-explorer/scripts/proxy.mjs @@ -0,0 +1,92 @@ +/** + * GitNexus reverse proxy — serves production web UI + proxies /api/* to backend. + * Zero dependencies, Node.js built-ins only. + * + * Usage: node proxy.mjs [port] + * dist-dir: path to gitnexus-web/dist (production build) + * port: listen port (default: 8888) + * + * Environment: + * API_PORT: GitNexus serve backend port (default: 4747) + */ +import http from 'node:http'; +import fs from 'node:fs'; +import path from 'node:path'; + +const API_PORT = parseInt(process.env.API_PORT || '4747'); +const DIST_DIR = process.argv[2] || './dist'; +const PORT = parseInt(process.argv[3] || '8888'); + +const MIME = { + '.html': 'text/html', + '.js': 'application/javascript', + '.css': 'text/css', + '.json': 'application/json', + '.png': 'image/png', + '.svg': 'image/svg+xml', + '.ico': 'image/x-icon', + '.woff2': 'font/woff2', + '.woff': 'font/woff', + '.wasm': 'application/wasm', + '.ttf': 'font/ttf', + '.map': 'application/json', +}; + +function proxyToApi(req, res) { + const opts = { + hostname: '127.0.0.1', + port: API_PORT, + path: req.url, + method: req.method, + headers: { ...req.headers, host: `127.0.0.1:${API_PORT}` }, + }; + const proxy = http.request(opts, (upstream) => { + res.writeHead(upstream.statusCode, upstream.headers); + upstream.pipe(res, { end: true }); + }); + proxy.on('error', () => { + res.writeHead(502, { 'Content-Type': 'text/plain' }); + res.end('GitNexus backend unavailable — is `npx gitnexus serve` running?'); + }); + req.pipe(proxy, { end: true }); +} + +function serveStatic(req, res) { + const urlPath = req.url.split('?')[0]; + let filePath = path.join(DIST_DIR, urlPath === '/' ? 'index.html' : urlPath); + + // SPA fallback: if file doesn't exist and isn't a static asset, serve index.html + if (!fs.existsSync(filePath) && !path.extname(filePath)) { + filePath = path.join(DIST_DIR, 'index.html'); + } + + const ext = path.extname(filePath); + const mime = MIME[ext] || 'application/octet-stream'; + + try { + const data = fs.readFileSync(filePath); + res.writeHead(200, { + 'Content-Type': mime, + 'Cache-Control': ext === '.html' ? 'no-cache' : 'public, max-age=86400', + }); + res.end(data); + } catch { + res.writeHead(404, { 'Content-Type': 'text/plain' }); + res.end('Not found'); + } +} + +const server = http.createServer((req, res) => { + if (req.url.startsWith('/api')) { + proxyToApi(req, res); + } else { + serveStatic(req, res); + } +}); + +server.listen(PORT, () => { + console.log(`GitNexus proxy listening on http://localhost:${PORT}`); + console.log(` Web UI: http://localhost:${PORT}/`); + console.log(` API: http://localhost:${PORT}/api/repos`); + console.log(` Backend: http://127.0.0.1:${API_PORT}`); +});