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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -36,3 +36,4 @@ Thumbs.db
.idea/
.vscode/
*.swp
.openvisio/
12 changes: 12 additions & 0 deletions .mcp.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"mcpServers": {
"openvisio": {
"command": "openvisio",
"args": [
"mcp",
".",
"--watch"
]
}
}
}
Binary file modified .openvisio/cache/openvisio-oss/data.mdb
Binary file not shown.
Binary file modified .openvisio/cache/openvisio-oss/lock.mdb
Binary file not shown.
28 changes: 28 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,34 @@ viewer, and the CLI), then `node mcp/dist/cli.js view .`.

---

## Share a graph (transport)

`view` keeps everything on `127.0.0.1`. When you want a **shareable link** — the
same Atlas/City map plus an AI **narrator** — without uploading your source,
`transport` does the split: it indexes the repo **locally** (the heavy
tree-sitter scan stays on your machine, private and fast) and ships **only the
computed graph JSON** to a web server that renders it.

```bash
openvisio transport # index the current repo, upload the graph, open the link
openvisio transport ../other # …or any other local repo
openvisio transport --no-open # just print the URL, don't open a browser
openvisio transport --server=https://your-host # send to your own deployment
```

What happens:

1. Index locally → build the deterministic graph.
2. Write a cached copy to `<repo>/.openvisio/graph.json` (auto-added to `.gitignore`).
3. `POST` just that graph JSON to `<server>/api/import`.
4. Open the rendered graph + narrator at `<server>/?g=<id>`.

Your source never leaves your machine — only the symbol/import graph is sent. The
destination defaults to `https://openvisio.io`; override it per-run with
`--server` or globally with the `OPENVISIO_SERVER` environment variable.

---

## Languages

OpenVisio parses these into symbols and import/call edges (tree-sitter grammars).
Expand Down
1 change: 1 addition & 0 deletions core/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
.openvisio/
6 changes: 5 additions & 1 deletion core/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@openvisio/core",
"version": "0.2.0",
"version": "0.2.1",
"description": "Deterministic, local-first code-graph engine for OpenVisio: tree-sitter symbol/import extraction, PageRank ranking, token-budgeted skeletons and slices. No LLM, no network.",
"type": "module",
"license": "MIT",
Expand All @@ -20,6 +20,10 @@
".": {
"types": "./dist/index.d.ts",
"default": "./dist/index.js"
},
"./browser": {
"types": "./dist/browser.d.ts",
"default": "./dist/browser.js"
}
},
"files": [
Expand Down
15 changes: 15 additions & 0 deletions core/src/browser.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
// Browser-safe surface of @openvisio/core. The engine's parse layer is pure
// (tree-sitter queries + per-language resolution logic) — only the scanner,
// graph builder, and wasm *loader* touch Node I/O. This entry re-exports just
// the fs-free pieces, so a browser bundle (the in-browser GitHub indexer) reuses
// the EXACT grammar definitions + import resolution the CLI uses, with no Node
// dependencies dragged in.
//
// What it deliberately does NOT export: scan.ts, build.ts, treesitter.ts,
// store/* (all import node:fs / node:module). The browser provides its own
// in-memory scan + a fetch-based wasm loader instead.

export { GRAMMARS, type GrammarConfig, type TsAliases } from './parse/grammars/index.js'
export { EXT_TO_GRAMMAR, grammarIdFromPath } from './parse/extensions.js'
export type { GrammarId } from './parse/treesitter.js'
export type { Language, SymbolKind, ParseResult } from './types.js'
8 changes: 8 additions & 0 deletions mcp/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,7 @@ every turn, so a fat surface re-creates the bloat we're removing.
openvisio [init] [path] [--global] [--cursor] # register with agents + first index
openvisio mcp [path] [--watch] [--spotlight] # MCP server over stdio
openvisio view [path] [--port=7077] [--no-open] # open the local graph viewer in a browser
openvisio transport [path] [--server=https://openvisio.io] [--no-open] # index locally, ship the graph to a web viewer
openvisio skeleton [path] [--budget=1500] [--task="add oauth"] # print the ranked map
openvisio export [path] [--out=.openvisio/graph.json] # emit the graph as JSON
```
Expand All @@ -142,6 +143,13 @@ openvisio export [path] [--out=.openvisio/graph.json] # emit the graph
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.
- `transport` is the shareable cousin of `view`: it indexes the repo **locally**
(your source stays on your machine), writes a cached copy to
`<repo>/.openvisio/graph.json`, then `POST`s **only the computed graph JSON** to
`<server>/api/import` and opens the rendered Atlas/City + AI narrator at
`<server>/?g=<id>`. The destination defaults to `https://openvisio.io`; override
it with `--server` or the `OPENVISIO_SERVER` env var. `--no-open` just prints the
URL.

---

Expand Down
2 changes: 1 addition & 1 deletion mcp/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "openvisio",
"version": "0.2.0",
"version": "0.2.1",
"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
16 changes: 16 additions & 0 deletions mcp/src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { toExportPayload } from './adapter.js'
import { serveMcp } from './server.js'
import { serveAgent } from './agent.js'
import { serveViewer } from './viewer.js'
import { runTransport } from './transport.js'
import { startSpotlightServer } from './spotlight.js'
import { runInit, runGlobalInit } from './init.js'

Expand Down Expand Up @@ -88,6 +89,12 @@ function printUsage(): void {
' Build the graph and write it where the viewer reads it (default:',
' <repo>/.openvisio/graph.json). Prints a one-line summary, not the JSON.',
'',
' openvisio transport [path] [--server=https://openvisio.io] [--no-open]',
' Index the repo LOCALLY, then ship just the graph JSON to the web',
' server and open the rendered graph + narrator in your browser. Your',
' source never leaves your machine — only the computed graph is sent.',
' Override the destination with --server or OPENVISIO_SERVER.',
'',
' openvisio view [path] [--port=7077] [--no-open]',
' Index a local repo and open the bundled graph viewer in your browser.',
' A self-contained UI (no install) that draws the same graph the MCP',
Expand Down Expand Up @@ -316,6 +323,15 @@ async function main(): Promise<number> {
)
return 0
}
case 'transport': {
// Index locally, then ship the graph JSON to a web server that renders it.
const root = args.positional[0] ?? process.cwd()
const serverRaw = args.flags.get('server')
const server = serverRaw && serverRaw !== 'true' ? serverRaw : process.env.OPENVISIO_SERVER || 'https://openvisio.io'
const outRaw = args.flags.get('out')
const out = outRaw && outRaw !== 'true' ? outRaw : undefined
return runTransport({ rootPath: root, server, out, open: !args.flags.has('no-open') })
}
case 'help':
case '--help':
printUsage()
Expand Down
115 changes: 115 additions & 0 deletions mcp/src/transport.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
// `openvisio transport [path]` — index a repo LOCALLY, then ship just the graph
// JSON to a web server that renders it. This is the local-repo half of the hosted
// viewer: the heavy work (clone-free filesystem scan + tree-sitter parse) happens
// on your machine where it's fast and private; the server only stores + renders a
// pre-computed graph (no git, no indexing, no source upload).
//
// Flow: build the graph → write .openvisio/graph.json → POST it to
// <server>/api/import → open <server>/?g=<id> (the rendered graph + narrator).

import { spawn } from 'node:child_process'
import * as fs from 'node:fs'
import * as path from 'node:path'
import { buildGraph } from '@openvisio/core'
import { toExportPayload } from './adapter.js'

export interface TransportOptions {
/** Repo to index. */
rootPath: string
/** Web server base URL, e.g. https://openvisio.io (no trailing slash). */
server: string
/** Also write the graph to this file (default <repo>/.openvisio/graph.json). */
out?: string
/** Open the system browser at the rendered URL (default true). */
open: boolean
}

/** Add `.openvisio/` to the repo's .gitignore once (best-effort). */
function ensureGitIgnore(root: string): void {
const gitignorePath = path.join(root, '.gitignore')
const entry = '.openvisio/'
let content = ''
try {
content = fs.readFileSync(gitignorePath, 'utf8')
} catch {
/* no .gitignore — will create */
}
if (content.split(/\r?\n/).some((l) => l.trim() === entry)) return
const sep = content.length === 0 || content.endsWith('\n') ? '' : '\n'
try {
fs.writeFileSync(gitignorePath, content + sep + entry + '\n')
} catch {
/* read-only fs — fine */
}
}

/** Open `url` in the system default browser (best-effort, never throws). */
function openBrowser(url: string): void {
const cmd = process.platform === 'darwin' ? 'open' : process.platform === 'win32' ? 'cmd' : 'xdg-open'
const args = process.platform === 'win32' ? ['/c', 'start', '', url] : [url]
try {
const child = spawn(cmd, args, { stdio: 'ignore', detached: true })
child.on('error', () => {})
child.unref()
} catch {
/* headless box — the URL is printed for the user */
}
}

export async function runTransport(opts: TransportOptions): Promise<number> {
const root = path.resolve(opts.rootPath)
const server = opts.server.replace(/\/+$/, '')

// 1. Index locally.
process.stderr.write(`openvisio transport: indexing ${root} …\n`)
const started = process.hrtime.bigint()
const graph = await buildGraph(root)
const payload = toExportPayload(graph, Date.now())
const json = JSON.stringify(payload)
const buildMs = Number(process.hrtime.bigint() - started) / 1e6
process.stderr.write(
`openvisio transport: indexed ${graph.files.length} files, ${graph.symbols.length} symbols, ` +
`${graph.edges.length} edges in ${buildMs.toFixed(0)}ms (${(Buffer.byteLength(json) / 1024 / 1024).toFixed(1)} MB)\n`,
)

// 2. Write the graph to the local .openvisio folder (cache + offline copy).
const outFile = opts.out ?? path.join(root, '.openvisio', 'graph.json')
try {
fs.mkdirSync(path.dirname(outFile), { recursive: true })
fs.writeFileSync(outFile, json)
ensureGitIgnore(root)
process.stderr.write(`openvisio transport: wrote ${outFile}\n`)
} catch (err) {
process.stderr.write(`openvisio transport: could not write ${outFile} (${err instanceof Error ? err.message : String(err)})\n`)
}

// 3. Upload the graph JSON to the server.
process.stderr.write(`openvisio transport: uploading to ${server}/api/import …\n`)
let res: Response
try {
res = await fetch(`${server}/api/import`, {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'X-Openvisio-Name': graph.name },
body: json,
})
} catch (err) {
process.stderr.write(`openvisio transport: upload failed — could not reach ${server} (${err instanceof Error ? err.message : String(err)})\n`)
return 1
}
if (!res.ok) {
const detail = await res.text().catch(() => '')
process.stderr.write(`openvisio transport: upload rejected (${res.status}) ${detail.slice(0, 200)}\n`)
return 1
}
const body = (await res.json().catch(() => ({}))) as { id?: string; url?: string }
if (!body.id && !body.url) {
process.stderr.write('openvisio transport: server did not return an id/url\n')
return 1
}

// 4/5. Open the rendered graph + narrator.
const viewUrl = body.url ?? `${server}/?g=${encodeURIComponent(body.id!)}`
process.stderr.write(`openvisio transport: live at\n ${viewUrl}\n`)
if (opts.open) openBrowser(viewUrl)
return 0
}
33 changes: 27 additions & 6 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "openvisio",
"version": "0.2.0",
"version": "0.2.1",
"private": true,
"description": "OpenVisio monorepo. The deterministic code-graph core, the standalone MCP CLI published to npm as `openvisio`, and a local-first viewer (Atlas + City). See any codebase as a graph; give coding agents a token-cheap, graph-native query surface.",
"license": "MIT",
Expand Down
Loading
Loading