Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ RepoLens MCP is an original TypeScript implementation built around fast local ve
- **Persistent local config**: `config set auto-index incremental` and `config set auto-sync true` store defaults for MCP startup indexing, live-session syncing, root, database path, max file size, labels, and graph-package bootstrap without requiring shell env vars.
- **Watch mode**: keep an indexed graph fresh during active coding with polling-based incremental refreshes, optionally skipping unchanged git polls with `--git-aware`.
- **Portable graph and report artifacts**: export self-contained HTML graph snapshots, architecture reports, and compressed `.rlgz` graph packages from the CLI; first index can bootstrap a missing database from `.repolens/graph.rlgz`.
- **Operational dashboard**: browse graph previews, structural filters, references, semantic/vector search, schema counts, relationship patterns, label property hints, fleet service links, dead-code candidates, review signals, and report links without a frontend build.
- **Operational dashboard**: browse graph previews, Fleet Galaxy cross-repo maps, structural filters, references, semantic/vector search, schema counts, relationship patterns, label property hints, fleet service links, dead-code candidates, review signals, and report links without a frontend build.
- **Graph communities**: detects functional modules from weighted relationships, not just folder names.
- **Code-aware search ranking**: uses SQLite FTS5 BM25 ranking with indexed camelCase and snake_case term expansion, so `create order` can find `createOrder` without scanning files.
- **Reference lookup**: finds exact indexed identifier references and labels definition lines for language-server-style navigation without requiring an external LSP process.
Expand Down Expand Up @@ -62,7 +62,7 @@ node --experimental-sqlite dist/src/cli.js serve

Then open `http://127.0.0.1:9749`.

The dashboard includes code search, reference lookup, semantic/vector search, graph search, graph schema tables with relationship patterns and label property hints, fleet service links, hotspot and boundary summaries, git-history signals, dead-code candidates, and one-click Markdown/HTML architecture reports.
The dashboard includes code search, reference lookup, semantic/vector search, graph search, graph schema tables with relationship patterns and label property hints, Fleet Galaxy cross-repo graphing, fleet service links, hotspot and boundary summaries, git-history signals, dead-code candidates, and one-click Markdown/HTML architecture reports.

From a local clone, the installer runs the same build and Codex checks:

Expand Down
2 changes: 1 addition & 1 deletion docs/agent-guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ On Windows PowerShell, the local installer mirrors the shell installer:
## Before Editing

1. Run `index` or `benchmark` for the target repository.
2. Use `architecture`, `schema`, `communities`, or `fleet-summary` to understand the project shape. `schema` includes relationship patterns and label property hints for safer graph queries.
2. Use `architecture`, `schema`, `communities`, `fleet-summary`, or `fleet-graph` to understand the project shape. `schema` includes relationship patterns and label property hints for safer graph queries.
3. Use `search`, `symbols`, `references`, `trace`, `cycles`, and `context-pack` for focused code context.
4. Use `changes` after edits to map uncommitted files back to graph impact, including per-file blast radius, relationship counts, and risk reasons.

Expand Down
4 changes: 2 additions & 2 deletions docs/research-notes.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ RepoLens MCP is not a fork or a drop-in static C replacement. It is an original
- Incremental indexing skips unchanged files, prunes removed files, and avoids call-edge rebuilds when there is no repository delta.
- Watch mode keeps the graph fresh with polling-based incremental refreshes while preserving deterministic CLI behavior for tests and automation; git-aware auto-sync skips unchanged HEAD/status polls during long-running MCP sessions.
- Browser dashboard without a bundler so the project is easy to build and inspect.
- Dashboard APIs expose architecture, fleet summaries, graph schema relationship/property hints, graph search, semantic search, local vector search, reference lookup, read-only graph queries, source snippets, import-resolved dependency cycles, dead-code candidates, graph previews, code search, and live Markdown/HTML architecture reports from the same local server.
- Dashboard APIs expose architecture, fleet summaries, Fleet Galaxy cross-repo graph data, graph schema relationship/property hints, graph search, semantic search, local vector search, reference lookup, read-only graph queries, source snippets, import-resolved dependency cycles, dead-code candidates, graph previews, code search, and live Markdown/HTML architecture reports from the same local server.
- Self-contained graph and architecture report exports for sharing HTML or Markdown artifacts without running a server, plus compressed checksummed `.rlgz` graph packages for reusing a SQLite graph without reindexing. A successful index can write a fresh package with `--write-package`, and a missing database can bootstrap from `.repolens/graph.rlgz` before the incremental pass.
- CI runs explicit test-skip governance, type-check, tests, production dependency audit, package dry-run, package contents gating, installer dry-run auditing, CycloneDX SBOM generation, self-indexing, and architecture output; separate workflows cover OpenSSF Scorecard and release build-provenance attestations.
- `llms.txt`, `docs/agent-guide.md`, and `docs/BENCHMARK.md` provide concise agent-facing operating instructions, sanitized validation evidence, and local-data boundaries in the npm package.
Expand All @@ -42,7 +42,7 @@ RepoLens MCP is not a fork or a drop-in static C replacement. It is an original
- Codex setup is explicit and reviewable through `doctor` and `install-codex --dry-run`, with safeguards around existing unmanaged MCP entries.
- Multi-agent setup is explicit, reversible, and reviewable through `agent-setup`, `install-agents --dry-run`, and `uninstall-agents --dry-run`, generating project-local instructions and config snippets for Codex, Claude, Gemini, Zed, OpenCode, Antigravity, Aider, KiloCode, VS Code, OpenClaw, and Kiro.
- Built-in ADR memory, not just structural graph search.
- Dashboard API and HTML are included in the same binary entrypoint, avoiding a separate frontend build while still exposing graph exploration, fleet service links, schema counts, relationship patterns, label property hints, review signals, dead-code samples, and report links.
- Dashboard API and HTML are included in the same binary entrypoint, avoiding a separate frontend build while still exposing graph exploration, Fleet Galaxy cross-repo graphing, fleet service links, schema counts, relationship patterns, label property hints, review signals, dead-code samples, and report links.
- Swift extraction and big-repo validation now cover a mixed mobile/web monorepo, not only TypeScript services.
- Structural graph search, BM25 source search with code-aware token expansion, reference lookup, typed inheritance/implementation/use edges, conservative data-flow edges, persistent startup config, redacted secret scanning, context packs for agents, multi-agent setup guidance, graph package bootstrap, optional startup auto-indexing, git-aware auto-sync, repeatable benchmark output, import-resolved local file edges, multi-ecosystem package/dependency nodes, resolved lockfile dependency nodes, project inventory/status, fleet summaries with cross-project service links, cross-repo graphing, runtime trace ingestion, Docker/Kubernetes infrastructure nodes, channel/event edges, first-class HTTP call nodes, GraphQL/gRPC/tRPC/OpenAPI protocol nodes, route-call edges, deterministic graph communities, dependency-free semantic search, persisted local vector search, read-only Cypher-like graph queries, graph schema relationship/property summaries, dependency-cycle detection, dead-code candidates, git-history hotspots, git-diff impact mapping with per-file blast radius, watch indexing, and portable graph/package exports are first-class workflows.
- Indexing now writes local `SIMILAR_TO` and `SEMANTICALLY_RELATED` edges plus deterministic symbol vectors without external embeddings or network calls.
Expand Down
114 changes: 110 additions & 4 deletions src/dashboard/server.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import http from "node:http";
import { architectureReport, findCommunities, findDeadCode, findDependencyCycles, findReferences, fleetSummary, getArchitecture, getCodeSnippet, getGraphSchema, graphSnapshot, queryGraph, searchCode, searchGraph, semanticSearch, searchSymbols, vectorSearch } from "../core/api.js";
import { architectureReport, findCommunities, findDeadCode, findDependencyCycles, findReferences, fleetGraph, fleetSummary, getArchitecture, getCodeSnippet, getGraphSchema, graphSnapshot, queryGraph, searchCode, searchGraph, semanticSearch, searchSymbols, vectorSearch } from "../core/api.js";

export interface DashboardOptions {
dbPath?: string;
Expand Down Expand Up @@ -52,6 +52,15 @@ export async function serveDashboard(options: DashboardOptions): Promise<http.Se
sendJson(response, findDependencyCycles(numberParam(url, "limit") ?? 25, options.dbPath));
} else if (url.pathname === "/api/fleet") {
sendJson(response, await fleetSummary(numberParam(url, "limit") ?? 50));
} else if (url.pathname === "/api/fleet-graph") {
sendJson(
response,
await fleetGraph({
limit: numberParam(url, "limit") ?? 20,
maxNodes: numberParam(url, "maxNodes") ?? 500,
maxEdges: numberParam(url, "maxEdges") ?? 1000
})
);
} else if (url.pathname === "/api/report") {
const format = url.searchParams.get("format") === "html" ? "html" : "markdown";
const body = architectureReport({ format, graphLimit: numberParam(url, "graphLimit") ?? 300 }, options.dbPath);
Expand Down Expand Up @@ -239,6 +248,18 @@ function dashboardHtml(): string {
<h2>Graph Preview</h2>
<canvas id="graph-canvas"></canvas>
</section>
<section>
<h2>Fleet Galaxy</h2>
<div class="sub" id="fleet-galaxy-summary">Loading indexed project graph.</div>
<canvas id="fleet-canvas"></canvas>
<div class="toolbar" style="margin-top:10px">
<span class="pill">Project</span>
<span class="pill">Dependency</span>
<span class="pill">Route</span>
<span class="pill">Language</span>
<span class="pill">Cross-repo call</span>
</div>
</section>
<div class="wide-grid">
<section>
<h2>Languages</h2>
Expand Down Expand Up @@ -307,7 +328,11 @@ function dashboardHtml(): string {
const cypherQuery = document.querySelector('#cypher-query');
const canvas = document.querySelector('#graph-canvas');
const ctx = canvas.getContext('2d');
const fleetCanvas = document.querySelector('#fleet-canvas');
const fleetCtx = fleetCanvas.getContext('2d');
const fleetGalaxySummary = document.querySelector('#fleet-galaxy-summary');
let graphState = { nodes: [], edges: [] };
let fleetState = { nodes: [], edges: [] };

function metric(label, value) { return '<div class="metric"><b>' + fmt.format(value) + '</b><span>' + label + '</span></div>'; }
function item(text, cls='') { return '<div class="' + cls + '">' + text + '</div>'; }
Expand All @@ -321,13 +346,14 @@ function dashboardHtml(): string {
return url.toString();
}
async function load() {
const [arch, schema, graph, dead, communityRows, fleet] = await Promise.all([
const [arch, schema, graph, dead, communityRows, fleet, fleetGalaxy] = await Promise.all([
fetch('/api/architecture').then(r => r.json()),
fetch('/api/schema').then(r => r.json()),
fetch('/api/graph?limit=500').then(r => r.json()),
fetch('/api/dead-code?limit=8').then(r => r.json()),
fetch('/api/communities?limit=8&minSize=4').then(r => r.json()),
fetch('/api/fleet?limit=20').then(r => r.json())
fetch('/api/fleet?limit=20').then(r => r.json()),
fetch('/api/fleet-graph?limit=20&maxNodes=500&maxEdges=1000').then(r => r.json())
]);
indexed.textContent = 'Indexed ' + new Date(arch.indexedAt).toLocaleString();
metrics.innerHTML = metric('files', arch.totals.files) + metric('symbols', arch.totals.symbols) + metric('edges', arch.totals.edges) + metric('lines', arch.totals.lines);
Expand All @@ -345,8 +371,12 @@ function dashboardHtml(): string {
cycles.innerHTML = arch.dependencyCycles.length ? arch.dependencyCycles.map(c => item('<b>' + escapeHtml(c.clusters.join(' -> ')) + '</b><div class="sub">' + fmt.format(c.edges) + ' internal cycle edges</div><div class="path">' + escapeHtml(c.recommendation) + '</div>')).join('') : item('No cross-cluster dependency cycles found.');
communities.innerHTML = communityRows.length ? communityRows.map(c => item('<b>' + escapeHtml(c.label) + '</b><div class="sub">' + fmt.format(c.members) + ' members - cohesion ' + c.cohesion.toFixed(2) + ' - ' + fmt.format(c.internalEdges) + ' internal edges</div><div class="path">' + escapeHtml(c.files.slice(0, 4).join(' | ')) + '</div><div class="sub">' + escapeHtml(c.representativeSymbols.slice(0, 4).map(s => s.name).join(', ')) + '</div>')).join('') : item('No graph communities found.');
graphState = prepareGraph(graph);
fleetState = prepareFleetGraph(fleetGalaxy);
fleetGalaxySummary.textContent = fmt.format(fleetGalaxy.totals.projectNodes) + ' projects, ' + fmt.format(fleetGalaxy.totals.graphNodes) + ' nodes, ' + fmt.format(fleetGalaxy.totals.graphEdges) + ' edges, ' + fmt.format(fleetGalaxy.totals.crossRepoEdges) + ' cross-repo links.';
resizeGraph();
resizeFleetGraph();
drawGraph();
drawFleetGraph();
}
async function doSearch() {
const q = search.value.trim();
Expand Down Expand Up @@ -397,6 +427,12 @@ function dashboardHtml(): string {
canvas.height = Math.max(320, Math.floor(rect.height * devicePixelRatio));
ctx.setTransform(devicePixelRatio, 0, 0, devicePixelRatio, 0, 0);
}
function resizeFleetGraph() {
const rect = fleetCanvas.getBoundingClientRect();
fleetCanvas.width = Math.max(320, Math.floor(rect.width * devicePixelRatio));
fleetCanvas.height = Math.max(320, Math.floor(rect.height * devicePixelRatio));
fleetCtx.setTransform(devicePixelRatio, 0, 0, devicePixelRatio, 0, 0);
}
function drawGraph() {
const w = canvas.clientWidth, h = canvas.clientHeight;
for (let step = 0; step < 2; step += 1) {
Expand Down Expand Up @@ -425,13 +461,83 @@ function dashboardHtml(): string {
}
requestAnimationFrame(drawGraph);
}
function prepareFleetGraph(graph) {
const nodes = graph.nodes.map(node => ({ ...node, x: 0, y: 0 }));
const byId = new Map(nodes.map(node => [node.id, node]));
const projects = nodes.filter(node => node.group === 'project');
const cx = 360, cy = 210;
const ring = Math.max(120, Math.min(230, projects.length * 34));
projects.forEach((node, index) => {
const angle = projects.length === 1 ? -Math.PI / 2 : (Math.PI * 2 * index) / projects.length - Math.PI / 2;
node.x = cx + Math.cos(angle) * ring;
node.y = cy + Math.sin(angle) * ring * 0.62;
});
const satellites = new Map();
for (const edge of graph.edges) {
const source = byId.get(edge.source), target = byId.get(edge.target);
if (!source || !target) continue;
const project = source.group === 'project' ? source : target.group === 'project' ? target : null;
const other = project === source ? target : source;
if (project && other.group !== 'project') {
const list = satellites.get(project.id) || [];
if (!list.includes(other)) list.push(other);
satellites.set(project.id, list);
}
}
for (const [projectId, list] of satellites.entries()) {
const project = byId.get(projectId);
if (!project) continue;
list.forEach((node, index) => {
if (node.x || node.y) return;
const angle = (Math.PI * 2 * index) / Math.max(1, list.length);
const radius = node.group === 'language' ? 42 : node.group === 'route' ? 66 : 88;
node.x = project.x + Math.cos(angle) * radius;
node.y = project.y + Math.sin(angle) * radius;
});
}
nodes.forEach((node, index) => {
if (!node.x && !node.y) {
const angle = (Math.PI * 2 * index) / Math.max(1, nodes.length);
node.x = cx + Math.cos(angle) * 260;
node.y = cy + Math.sin(angle) * 160;
}
});
return { nodes, edges: graph.edges.map(edge => ({ ...edge, sourceNode: byId.get(edge.source), targetNode: byId.get(edge.target) })).filter(edge => edge.sourceNode && edge.targetNode) };
}
function drawFleetGraph() {
const w = fleetCanvas.clientWidth, h = fleetCanvas.clientHeight;
fleetCtx.clearRect(0, 0, w, h);
const scaleX = w / 720, scaleY = h / 430;
const edgeColors = { CROSS_REPO_HTTP_CALLS:'#be123c', ROUTE_OVERLAP:'#b45309', SHARES_DEPENDENCY:'#7c3aed', DEPENDS_ON:'#64748b', USES_LANGUAGE:'#0f766e', PROVIDES_ROUTE:'#2563eb', CALLS_ENDPOINT:'#0891b2' };
for (const edge of fleetState.edges) {
fleetCtx.strokeStyle = edgeColors[edge.type] || 'rgba(71,85,105,.22)';
fleetCtx.lineWidth = edge.type === 'CROSS_REPO_HTTP_CALLS' ? 2.2 : 1;
fleetCtx.beginPath();
fleetCtx.moveTo(edge.sourceNode.x * scaleX, edge.sourceNode.y * scaleY);
fleetCtx.lineTo(edge.targetNode.x * scaleX, edge.targetNode.y * scaleY);
fleetCtx.stroke();
}
const palette = { project:'#111827', language:'#0f766e', dependency:'#6d28d9', route:'#be123c' };
for (const node of fleetState.nodes) {
const x = node.x * scaleX, y = node.y * scaleY;
fleetCtx.fillStyle = palette[node.group] || '#64748b';
fleetCtx.beginPath();
fleetCtx.arc(x, y, node.group === 'project' ? 9 : 5, 0, Math.PI * 2);
fleetCtx.fill();
if (node.group === 'project') {
fleetCtx.fillStyle = '#1f2937';
fleetCtx.font = '12px Inter, system-ui, sans-serif';
fleetCtx.fillText(node.label.slice(0, 28), x + 12, y + 4);
}
}
}
search.addEventListener('input', () => { clearTimeout(window.__t); window.__t = setTimeout(doSearch, 120); });
referencesRun.addEventListener('click', doReferences);
semanticRun.addEventListener('click', doSemanticSearch);
vectorRun.addEventListener('click', doVectorSearch);
graphRun.addEventListener('click', doGraphSearch);
cypherRun.addEventListener('click', doCypherQuery);
window.addEventListener('resize', resizeGraph);
window.addEventListener('resize', () => { resizeGraph(); resizeFleetGraph(); drawFleetGraph(); });
load().catch(err => { document.body.innerHTML = '<pre>' + escapeHtml(err.stack || err) + '</pre>'; });
</script>
</body>
Expand Down
Loading
Loading