@psnext/lscg
v0.1.6
Published
Local source context graphs for repositories, exposed as a CLI and MCP server.
Maintainers
Readme
lscg
Local source context graphs for repositories. lscg parses code with
Tree-sitter, stores discovered nodes and edges in SQLite, and exposes the graph
through both a CLI and an MCP server.
Requirements
- Node.js 24+
- npm install scripts allowed for Tree-sitter native packages
Install
npm installCLI
Use global help to list commands, or ask any operational command for focused usage, supported options, and examples with --help or -h:
lscg --help
lscg context --help
lscg callgraph -hThe positional form lscg <command> help is not a command-help alias; help remains available as a normal command argument.
Common option aliases are -r/--root, -s/--scope, -k/--kind, -d/--depth, -l/--limit, -f/--file, and -o/--output where the command supports the corresponding option. callgraph and context use text output by default; pass --output json when machine-readable JSON is needed.
# Create repo-local and home-level graph databases
npm start -- init --scope both
# Scan the current repository into both graph scopes
npm start -- scan --scope both
# Watch the current repository and rescan on scanner-relevant changes
npm run watch
npm run scan:watch
npm start -- watch
npm start -- scan --watch
# Retrieve bounded context around a symbol (JSON envelope)
npm start -- context greet --kind symbol
npm start -- context greet --depth 2 --limit 25
npm start -- context greet --file src/app.ts --excerpts --excerpt-lines 40 --excerpt-bytes 8000
npm start -- context greet --output text
npm start -- context greet --output svg > /tmp/context.svg
# Inspect graph contents
npm start -- status --scope repo
npm start -- nodes --limit 20
npm start -- nodes greet --kind symbol --limit 20
npm start -- nodes greet --output text
npm start -- edges --kind calls --limit 20
npm start -- edges --kind attributed_to --limit 20
npm start -- edges --output text
npm start -- neighbors <node-id> --depth 2
# Visualize the graph
npm start -- view --scope repo
npm start -- view --scope home --anchor greet --search util
npm start -- view --display icon --output /tmp/lscg-view.svg
# Run a read-only SQL query
npm start -- query "select kind, count(*) as n from nodes group by kind"
# Start the MCP server over stdio
npm start -- mcpWhen installed as a package, the binary names are lscg and context-graph.
MCP tools
The MCP server exposes:
context_graph_scancontext_graph_statuscontext_graph_nodescontext_graph_edgescontext_graph_neighborscontext_graph_query
Storage
- Repo database:
.sling/graph.sqlite - Home database:
~/.sling/graph.sqlite
Use --scope repo, --scope home, or --scope both on commands that read or
write graph data.
lscg view opens a temporary interactive HTML page by default and shows the
anchor-centered subgraph for the selected scope when no --anchor is provided.
The browser view includes a toggle to switch between the subgraph and the full
graph. Pass --search to filter within the active graph mode, or --output
<path> to write a static SVG snapshot of the default subgraph instead of
opening a browser. Use --display simple, --display shape (the default), or
--display icon to choose how nodes are rendered. Simple mode uses colored
circles, shape mode uses kind-specific shapes, and icon mode loads SVG icons
from src/view/templates/icons.
Examples:
lscg view --anchor greet --search util
lscg view --display simple --output graph.svglscg context <symbol> is the agent-oriented retrieval workflow. It returns a
versioned JSON envelope with one result per selected scope by default. Use
--output text for a concise human-readable summary or --output svg for a
static graph of the returned context slice. Results distinguish
ok, ambiguous, not_found, refresh_failed, and stale states from an
empty context. Anchor matching prefers qualified/path and exact names before
prefix and substring matches; an ambiguous match returns ranked candidates
instead of merging unrelated symbols. Use --kind and repository-relative
--file to disambiguate. Results are references by default; --excerpts opts
into bounded source text with --excerpt-lines and --excerpt-bytes limits.
Graph-reading commands never start a scan. Their Freshness Report reflects the
stored graph state: fresh after a successful explicit scan, stale when no
compatible graph exists or a known invalidation remains, and degraded when a
retained failed-file or plugin contribution is being served. Run scan or keep
watch active after source changes; a stored fresh result is not a live
filesystem content-verification guarantee.
Impact contains only syntax-grounded calls edges (with caller/callee
direction), and dependencies contain imports/exports; parser confidence
and provenance are included so callers can verify relationships in source.
Substring fallback is intentionally reported as a bounded capability warning.
Use rg or grep for literal discovery when a term is not indexed—context
does not replace arbitrary text search or claim complete semantic resolution.
lscg scan performs an Explicit Scan. After the first scan, its default mode
is incremental: it retains unchanged file graphs and reprocesses files whose
path, size, or mtime metadata changed. Use lscg scan --full when you need a
complete rebuild and content verification (including same-size edits with
restored mtimes).
lscg watch keeps the selected repository current in the foreground. It runs
one scan immediately, then watches scanner-relevant files and prints compact
status updates between rescans. lscg scan --watch provides the same Watch
Session from the scan command. Use lscg scan when you want the one-shot scan
output.
Pass --progress to scan, watch, or scan --watch for line-oriented,
human-readable lifecycle messages on stderr while the existing JSON summary or
Watch Session events remain on stdout. For example:
npm start -- scan --progress >summary.json
npm start -- watch --progress >watch-events.jsonl
npm start -- scan --watch --progress >watch-events.jsonlProgress labels discovery/planning, plugin work, file processing, reconciliation, and optional enrichment. A normal scan may report structural completion with enrichment deferred or degraded; a Full Scan reports retries and only reports stabilized completion after its enrichment loop succeeds. Progress is informational rather than a stable machine-readable protocol, and it does not provide cancellation or animated progress bars.
Scan/watch benchmark
Run the non-blocking benchmark with:
npm run benchmark:scan-watchThe versioned JSON report separates two cold timings for this lscg checkout:
coldNormalStructuralReady: a normal scan's return after its structural graph is persisted, before optional enrichment completes.coldFullEnrichmentComplete: a coldscan --fullcompletion, including its required enrichment drain.
Every cold sample uses an isolated copy with no pre-existing graph database;
source and Git metadata are copied so the normal and full scans exercise the
same source graph and optional attribution path without changing this checkout.
The report records SHA-256 fingerprints plus structural node/edge counts before
normal-scan enrichment and after that scan's pending enrichment work drains.
structuralEquivalence must be true: user nodes, attributed_to edges, and
per-node attribution are
intentionally excluded because they are optional enrichment rather than source
structure.
coldNormalStructuralReadyTarget reports the 3.43-second baseline, the
2.06-second median target (the required 40% reduction), the measured p50, and
whether that run met the target. Timing values and target status are benchmark
results for the documented machine/run, not unit-test thresholds. Use multiple
cold samples for review (the default is 10); pass -- --repetitions 5 to change
the sample count.
The report also retains deterministic work counters and informational p50/p95 timings for the isolated representative fixture's initial full scan, no-change incremental scan, and one-file Watch Session update.
Git user attribution is optional. Enable it with --attribution on scan,
watch, or scan --watch; normal scans return structural data with attribution
pending, while --full and active watch sessions drain the selected enrichment.
Without the flag, scans report attribution as disabled and do not invoke Git
or create user nodes. Existing attribution remains readable if a later scan omits
the flag. When enabled, contributor user nodes are keyed by normalized email and
the most recent contributor is tracked for each discovered node.
Scanner plugins
The library accepts trusted, versioned scanner plugins through the plugins
option on scanRepository. A plugin receives a read-only repository context
with the resolved repoPath, discovered files, file contents, and a readFile
helper, then returns semantic node and edge facts. Edges refer to node factKey values within their emitting plugin namespace and
may connect facts from different files. Use the additive sourcePlugin and
targetPlugin fields when an endpoint belongs to another plugin; raw fact keys
are never parsed as namespaces. Nodes and edges may also carry plugin-defined
type values; edge type defaults to its structural kind when omitted.
import { AttributionPlugin, scanRepository, type ScannerPlugin } from '@psnext/lscg';
const plugin: ScannerPlugin = {
name: 'my-analyzer',
apiVersion: 1,
capabilities: ['repository-analyzer'],
async scan({ repoPath, files }) {
return {
nodes: files.map((file) => ({
factKey: `artifact:${file.path}`,
filePath: file.path,
kind: 'symbol',
type: 'artifact',
name: file.path,
metadata: { repoPath }
})),
edges: []
};
}
};
await scanRepository({ root: '/path/to/repo', plugins: [plugin] });
// Or opt into the bundled Git user attribution enrichment:
await scanRepository({ root: '/path/to/repo', plugins: [AttributionPlugin] });
// The same bundled plugin can also be selected as a convenience option:
await scanRepository({ root: '/path/to/repo', attribution: true });
// The CLI equivalent is: lscg scan --attributionA separately packaged plugin can be passed to a host application as an imported ScannerPlugin. The copyable external scanner plugin example demonstrates this with a TODO/FIXME analyzer and a host script.
The CLI can load the default export of an external module with the repeatable --plugin option:
lscg scan --root /path/to/repo --plugin @acme/my-scanner-plugin
lscg scan --root /path/to/repo --plugin ./my-scanner-plugin.mjs
lscg watch --root /path/to/repo --plugin ./my-scanner-plugin.mjsRelative module specifiers are resolved from the current working directory. The module must default-export one ScannerPlugin (an array is also accepted). External plugins are trusted code and run in-process; only load modules you trust. The supplied plugins are used for one-shot scans, watch, and scan --watch.
Plugins may implement initialize, scan, finalize, and dispose hooks.
The core assigns deterministic IDs, validates API compatibility, persists
plugin facts, and reports plugin diagnostics in the scan summary. Plugins run
in-process and must be treated as trusted code.
Plugin output is bounded by conservative host-selected resource limits. The
limits cover node and edge fact counts, diagnostics, metadata bytes, and total
serialized output bytes. Callers can override them with pluginLimits on
scanRepository; an oversized plugin contribution is rejected atomically and
reported in the scan summary, while later plugins can still contribute normally.
The host keeps semantic factKey values separate from graph identity. Node
facts conflict by (node, factKey) and edge facts by (edge, factKey) across
plugins. merge retains both under distinct plugin-qualified identities,
replace uses priority (then plugin-name order for ties), and reject keeps
the incumbent and reports a diagnostic. The host assigns deterministic IDs;
plugins do not provide database IDs. Use lscg edges --type <edge-type> to filter
persisted edge types.
Every scan also runs the bundled PackagePlugin. It reads the repository-root
package.json dependency sections (dependencies, devDependencies,
peerDependencies, and optionalDependencies) and creates repository-scoped
package nodes only for package roots used by source imports. A provides edge
connects each package node to its matching import node. Undeclared bare
external imports create implied package nodes; relative, absolute, URL, and
Node built-in imports are excluded. The plugin is de-duplicated if explicitly
listed in plugins.
Current parser support
The scanner supports JavaScript, JSX, TypeScript, TSX, Python, and Java via
Tree-sitter. Python is handled by the bundled PythonScannerPlugin, and Java
by JavaScannerPlugin using the pinned native [email protected]
grammar. Java records syntax-grounded classes, interfaces, enums, records,
annotation types, methods, constructors, fields, imports, method invocations,
containment, and explicit-public export approximations. Java calls/imports are
occurrences (no type, overload, constructor, or cross-file resolution), and
inherited/module APIs are not modeled. A Java parse error retains the prior
file contribution and marks freshness degraded until the file recovers.
Repositories with root or immediate-module pom.xml, build.gradle, or
build.gradle.kts files also receive java-dependency package nodes from
JavaDependencyPlugin. Manifest reading is bounded and non-executing; only
literal Maven coordinates/properties and literal Gradle dependency calls are
modeled. Dependencies remain independent from Java imports: no heuristic
coordinate matching or dependency-to-import edges are emitted. Native
Tree-sitter install scripts must remain allowlisted for clean installs.
Cross-file symbol resolution remains a language-specific semantic-enricher
concern.
