Skip to content
Merged
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
80 changes: 71 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,18 +58,79 @@ See [`bench/`](bench/) for the token-savings methodology and an A/B protocol.

## Run the viewer

The viewer is a local-first Next.js app. It indexes a repo with the same engine
and renders the **Atlas** and **City** views.
Once `openvisio` is installed, `view` indexes a repo and opens the bundled
**Atlas** and **City** views in your browser — zero install, served from
`127.0.0.1`:

```bash
npm run build # build the engine + CLI first (the viewer indexes with it)
cd ui
npm install
npm run dev # http://localhost:3000
openvisio view # index the current repo and open the viewer
openvisio view ../other # …or any other local repo
```

Point it at a local folder or a Git URL; everything is indexed and rendered on
your machine.
The viewer ships in the `openvisio-viewer` package: the same React/Three.js
Atlas + City views, as a self-contained static bundle. Toggle between them, click
to focus a file, and re-index any other local repo from the path box. Nothing
leaves your machine.

**Watch your agent think.** `view` defaults to the spotlight port (7077), so it
doubles as the live-highlight hub: leave it running, point your agent at the repo
with `openvisio mcp . --spotlight`, and each tool call focuses the file it's
looking at — in real time.

From a clone, build the workspace first (`npm run build` builds the engine, the
viewer, and the CLI), then `node mcp/dist/cli.js view .`.

---

## Languages

OpenVisio parses these into symbols and import/call edges (tree-sitter grammars).
Any other text file is still scanned as a graph node — templates (Twig, Blade),
Markdown, and EDA/hardware files (KiCad, Gerber) get a language label without
parsed symbols, so nothing in the repo is invisible.

| Language | Extensions |
| ------------------ | ----------------------------------- |
| TypeScript | `.ts`, `.mts`, `.cts` |
| TSX | `.tsx` |
| JavaScript | `.js`, `.jsx`, `.mjs`, `.cjs` |
| Python | `.py`, `.pyi` |
| Go | `.go` |
| Rust | `.rs` |
| Java | `.java` |
| C | `.c`, `.h` |
| C++ | `.cpp`, `.cc`, `.cxx`, `.hpp`, `.hh`|
| C# | `.cs` |
| Kotlin | `.kt`, `.kts` |
| Ruby | `.rb` |
| PHP | `.php` |
| Swift | `.swift` (disabled by default) |
| Scala | `.scala` |
| Dart | `.dart` |
| Zig | `.zig` |
| Lua | `.lua` |
| R | `.r`, `.R` |
| Elixir | `.ex`, `.exs` |
| Elm | `.elm` |
| OCaml | `.ml`, `.mli` |
| ReScript | `.res` |
| Solidity | `.sol` |
| TLA+ | `.tla` |
| Objective-C | `.m`, `.mm` |
| Bash | `.sh`, `.bash` |
| Vue | `.vue` |
| HTML | `.html`, `.htm` |
| CSS | `.css` |
| JSON | `.json` |
| YAML | `.yaml`, `.yml` |
| TOML | `.toml` |
| Embedded Template | `.erb`, `.ejs` |
| SystemRDL | `.rdl` |
| QL | `.ql` |
| Emacs Lisp | `.el` |

> Swift's grammar is heavy enough to crash V8's WASM compiler on some machines,
> so it's off by default — enable it with `OPENVISIO_ENABLE_GRAMMARS=swift`.

---

Expand All @@ -79,7 +140,8 @@ your machine.
|------|------------|
| [`core/`](core/) | `@openvisio/core` — the deterministic code-graph engine (tree-sitter parse, import resolution, PageRank, token-budgeted skeletons). |
| [`mcp/`](mcp/) | `openvisio` — the published MCP server + CLI. Bundles `core` into a single self-contained binary. |
| [`ui/`](ui/) | The local-first viewer (Atlas + City). |
| [`viewer/`](viewer/) | `openvisio-viewer` — the bundled Atlas + City app that `openvisio view` serves (React + Three.js, built to a static bundle). |
| [`ui/`](ui/) | Full Next.js web app (Atlas + City + AI narrator). |
| [`bench/`](bench/) | Token-savings estimator + A/B measurement protocol. |
| [`docs/`](docs/) | Engine, graph, and MCP integration notes. |

Expand Down
8 changes: 6 additions & 2 deletions core/src/parse/grammars/dart.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,12 @@ import type { GrammarConfig } from './index.js'
const posix = path.posix

const DART_SYMBOLS = `
(class_definition (identifier) @name) @def.class
(function_signature (identifier) @name) @def.function
(class_definition name: (identifier) @name) @def.class
(function_signature name: (identifier) @name) @def.function
(getter_signature name: (identifier) @name) @def.function
(setter_signature name: (identifier) @name) @def.function
(enum_declaration name: (identifier) @name) @def.class
(mixin_declaration (identifier) @name) @def.class
`
const DART_IMPORTS = `
(library_import (import_specification (configurable_uri (uri (string_literal) @source))))
Expand Down
10 changes: 5 additions & 5 deletions core/src/parse/grammars/r.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import type { GrammarConfig } from './index.js'

const R_SYMBOLS = `
(function_definition name: (identifier) @name) @def.function
(assignment name: (identifier) @name) @def.const
(binary_operator lhs: (identifier) @name rhs: (function_definition) @def) @def.function
`
const R_IMPORTS = `
(call function: (identifier) @fn arguments: (arguments (string) @source))
(call function: (identifier) @fn arguments: (arguments (argument (identifier) @source)))
(call function: (identifier) @fn arguments: (arguments (argument (string) @source)))
`
const R_CALLS = `
(call function: (identifier) @callee)
Expand All @@ -15,12 +15,12 @@ export const rLanguage: GrammarConfig = {
symbolQuery: R_SYMBOLS,
importQuery: R_IMPORTS,
callQuery: R_CALLS,
keep: (def) => def.parent?.type === 'program' || def.parent?.type === 'brace_list',
keep: (_def, _name) => true,
exported: () => true,
importSpecifier: (n) => {
const s = n.text
if (s.length >= 2 && (s[0] === '"' || s[0] === "'") && s[s.length - 1] === s[0]) return s.slice(1, -1)
return s
},
resolveImport: () => null, // R source/library resolution is limited
resolveImport: () => null,
}
Binary file added core/wasm/tree-sitter-r.wasm
Binary file not shown.
17 changes: 9 additions & 8 deletions mcp/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -124,14 +124,15 @@ openvisio export [path] [--out=.openvisio/graph.json] # emit the graph
- `--task` personalizes the skeleton ranking toward a task description.
- `--spotlight` exposes a local SSE channel so an open OpenVisio viewer lights up
the files the agent is querying.
- `view` indexes the repo and serves a self-contained, dependency-free graph
viewer (bundled in the package — nothing to install) on `127.0.0.1`, then opens
your browser. It draws the same deterministic graph the MCP serves: files
colored by language, import edges, pan/zoom, search, and a per-language
breakdown. The path box re-indexes any other local repo. `--no-open` just
prints the URL. It defaults to the spotlight port (7077) and acts as the
highlight hub: with `view` running, start your agent with
`openvisio mcp . --spotlight` and its tool calls pulse the graph live.
- `view` indexes the repo and serves the OpenVisio **Atlas + City** views (the
same React/Three.js views from the app, shipped in the `openvisio-viewer`
package) on `127.0.0.1`, then opens your browser. City is a 3D treemap
(districts = folders, buildings = files, sized by LOC, colored by language);
Atlas is a file/symbol constellation linked by imports, definitions, and calls.
Toggle between them, click to focus, and re-index any other local repo from the
path box. `--no-open` just prints the URL. It defaults to the spotlight port
(7077) and acts as the highlight hub: with `view` running, start your agent
with `openvisio mcp . --spotlight` and its tool calls focus the graph live.

---

Expand Down
8 changes: 2 additions & 6 deletions mcp/build.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -6,24 +6,20 @@
//
// Requires @openvisio/core to be built first (root: `npm run build`).

import { cpSync, rmSync } from 'node:fs'
import { rmSync } from 'node:fs'
import { build } from 'esbuild'

// Clean: the bundle is the only artifact; stale per-file tsc output must not
// ride along into the published tarball.
rmSync('dist', { recursive: true, force: true })

// Ship the static viewer UI (served by `openvisio view`) beside the bundle so it
// resolves at dist/viewer relative to the compiled cli.js at runtime.
cpSync('viewer', 'dist/viewer', { recursive: true })

const common = {
bundle: true,
platform: 'node',
format: 'esm',
target: 'node18',
sourcemap: false,
external: ['@modelcontextprotocol/sdk', 'zod', 'web-tree-sitter', 'tree-sitter-wasms', 'lmdb'],
external: ['@modelcontextprotocol/sdk', 'zod', 'web-tree-sitter', 'tree-sitter-wasms', 'lmdb', 'openvisio-viewer'],
logLevel: 'info',
}

Expand Down
3 changes: 2 additions & 1 deletion mcp/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "openvisio",
"version": "0.1.4",
"version": "0.1.5",
"description": "See any codebase as a graph. MCP server + CLI that serves coding agents (Claude Code, Codex, Cursor) a token-cheap, ranked, graph-native query surface over a local repo — agents query structure instead of crawling files. Local-first, read-only, no network.",
"type": "module",
"license": "MIT",
Expand Down Expand Up @@ -45,6 +45,7 @@
"dependencies": {
"@modelcontextprotocol/sdk": "^1.29.0",
"lmdb": "^3.2.6",
"openvisio-viewer": "^0.1.0",
"tree-sitter-wasms": "^0.1.13",
"web-tree-sitter": "~0.25.10",
"zod": "^4.4.3"
Expand Down
2 changes: 1 addition & 1 deletion mcp/src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ export async function serveMcp(opts: ServeOptions): Promise<void> {
const badRoot = resolvedRoot === path.parse(resolvedRoot).root || resolvedRoot === path.resolve(os.homedir())

const server = new McpServer(
{ name: 'openvisio', version: '0.1.3' },
{ name: 'openvisio', version: '0.1.5' },
{
instructions:
'MANDATORY workflow for this repository. Before reading, grepping, globbing, ' +
Expand Down
44 changes: 34 additions & 10 deletions mcp/src/viewer.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
// `openvisio view [repo]` — the local, open-source graph viewer. Builds the
// deterministic graph with the SAME engine the MCP serves, then hosts a tiny
// localhost server that pairs the bundled static UI (viewer/) with the existing
// spotlight HTTP surface: GET /api/graph?path=<repo> indexes on demand, and the
// SSE stream is mounted too, so a running `openvisio mcp --spotlight` session on
// the same port lights up the map live. Local-first: binds 127.0.0.1 only.
// localhost server that pairs the bundled Atlas + City UI (the openvisio-viewer
// package) with the existing spotlight HTTP surface: GET /api/graph?path=<repo>
// indexes on demand, and the SSE stream is mounted too, so a running
// `openvisio mcp --spotlight` session on the same port lights up the map live.
// Local-first: binds 127.0.0.1 only.

import { spawn } from 'node:child_process'
import { createRequire } from 'node:module'
import * as fs from 'node:fs'
import * as path from 'node:path'
import { fileURLToPath } from 'node:url'
Expand All @@ -23,16 +25,29 @@ export interface ViewerOptions {
}

/**
* Locate the bundled viewer assets. When running from the published bundle this
* is dist/viewer (copied beside cli.js by build.mjs); under `tsx src/cli.ts` it
* is the source mcp/viewer dir one level up. Falls back to the dist guess.
* Locate the built `openvisio-viewer` assets (the Atlas + City React/Three app).
* Primary path: resolve the installed package via node's resolver — works both
* when openvisio depends on it (node_modules) and in the workspace. Falls back to
* a few relative guesses for source/dev layouts. Returns null if it isn't built.
*/
function resolveViewerDir(): string {
function resolveViewerDir(): string | null {
const here = path.dirname(fileURLToPath(import.meta.url))
for (const cand of [path.join(here, 'viewer'), path.join(here, '..', 'viewer')]) {
const candidates: string[] = []
try {
const req = createRequire(import.meta.url)
candidates.push(path.join(path.dirname(req.resolve('openvisio-viewer/package.json')), 'dist'))
} catch {
// not resolvable (unbuilt workspace, or odd install) — fall through to guesses
}
candidates.push(
path.join(here, '..', '..', 'viewer', 'dist'), // mcp/dist → repo/viewer/dist
path.join(here, '..', '..', '..', 'viewer', 'dist'),
path.join(here, 'viewer'), // legacy bundled copy
)
for (const cand of candidates) {
if (fs.existsSync(path.join(cand, 'index.html'))) return cand
}
return path.join(here, 'viewer')
return null
}

/** Open `url` in the system default browser (best-effort, never throws). */
Expand Down Expand Up @@ -60,6 +75,15 @@ function isAddrInUse(err: unknown): boolean {
export async function serveViewer(opts: ViewerOptions): Promise<void> {
const root = path.resolve(opts.rootPath)
const viewerDir = resolveViewerDir()
if (!viewerDir) {
process.stderr.write(
'openvisio view: viewer assets not found. The `openvisio-viewer` package is missing or unbuilt.\n' +
' • installed globally: reinstall with `npm i -g openvisio@latest`\n' +
' • from the repo: run `npm run build -w openvisio-viewer` first\n',
)
process.exitCode = 1
return
}
// On-demand indexer: empty path (the UI's first load can omit it) falls back
// to the repo the command was launched from.
const onIndex = async (repoPath: string) => toExportPayload(await buildGraph(repoPath || root), Date.now())
Expand Down
Loading
Loading