Skip to main content

Sentinel Code Graph CLI

Async CLI that scans a directory or file, extracts structural nodes (files, classes, functions, methods, interfaces, types, imports) and edges (contains, imports, calls) with raw tree-sitter, and stores them in Ladybug (embedded property graph, zero setup).

  • Default database is a known-location Ladybug file (~/.codegraph/graph.lbdb), so query time never guesses where the index lives. Pass --db to any command to override it.
  • --db :memory: indexes ephemerally (lost when the process exits).
  • Scope: Python, TypeScript/JavaScript, Go. (queries.py stays on disk, unwired.)
  • One database holds exactly one index: --overwrite flushes the whole db first, so re-indexing is idempotent.
  • --no-persist dry-runs the pipeline (parse → nodes → edges → print, no DB writes).

Install

From the repo root (workspace member, latest pinned deps in uv.lock):

uv sync

Usage

# index a tree into the default DB (re-runnable; --overwrite flushes first)
uv run --package codegraph python -m codegraph.cli index ./packages/api/src --overwrite

# index a single file (root = its parent dir; must be a supported language)
uv run --package codegraph python -m codegraph.cli index ./packages/api/main.py

# inspect the database
uv run --package codegraph python -m codegraph.cli stats

# index and print the hierarchy tree of the indexed root
uv run --package codegraph python -m codegraph.cli index ./src --output tree

# dump every collected node (kind, lines, parent, children, callees)
uv run --package codegraph python -m codegraph.cli index ./src --output nodes

# list every calls edge (caller -> callee, implementation order)
uv run --package codegraph python -m codegraph.cli index ./src --output calls

# dry-run: print without persisting
uv run --package codegraph python -m codegraph.cli index ./src --no-persist --output tree

# Ephemeral index (no file written)
uv run --package codegraph python -m codegraph.cli index ./src --db :memory: --output tree

# explore the index (read-only; works from anywhere — default DB is known)
uv run --package codegraph python -m codegraph.cli query overview
uv run --package codegraph python -m codegraph.cli query files
uv run --package codegraph python -m codegraph.cli query search --name reviewWorkflowV2
uv run --package codegraph python -m codegraph.cli query callees --name reviewWorkflowV2 --json

Flow

amain parses args, then three linear stages in pipeline.py:

read(target) -> build_graph(root, items) -> out(db, root, graph, ...)
  • read — discover (walk: suffix → language, prune noise dirs, skip oversize/undecodable) + read off the loop → ScannedSources(root, items). I/O.
  • build_graph — pure, two passes: (1) collect — parse_file_input per file (blank source or unsupported language → skip, counted) into nodes + contains / imports plus buffered call sites; (2) link — resolve_call_edges joins sites against the (file, name) definition registry + per-file import maps (alias-aware, joined on a single candidate: the defining-module original when the import carries one, else the bound name; Go dot-imported files are a fallback) into calls with real node ids, dropping unresolvable sites. Merge → BuiltGraph(root, files, skipped, nodes, edges). No I/O, no DB.
  • out — persist (create_all, clear_all when --overwrite, add_all) then print (summary | tree | nodes | calls) → IndexResult. I/O.

cli.py holds only arg parsing + amain wiring + run_stats; __main__.py calls cli.main().

Python emitter

Each Python file is walked once (collect_python_file): Node + Edge(CONTAINS) with base:name:start:end ids for defs, Node(IMPORT) + Edge(IMPORTS) per imported name (keeping the as-alias original, e.g. from utils import helper as h records bound h + original helper), and bare-name calls buffered unresolved with their call-site line. The link phase then joins each site — same-file hit first, else the import map's resolved file + original name — into calls edges with real node ids. Absolute imports tolerate a root-relative prefix on indexed paths (src/app/… satisfies import app.…). Builtins, stdlib, third-party, star-import calls, attribute calls (obj.method(…), self.x(…)), and module-level call sites yield no edges. Decorator applications (@retry, @with_logging(…)) buffer as call sites on the decorated def/class with the @-line and resolve like ordinary calls (attribute decorators such as @app.get(…) are skipped). One edge per caller → callee pair, each stamped with its call-site line. Calls render in implementation order, not alphabetical.

TypeScript / JavaScript collector

Each TS/JS file is walked once (collect_ts_file): classes, functions, methods, interfaces, type aliases, and arrow-bound consts (export wrappers are transparent) → Node + Edge(CONTAINS); imports keep alias originals (import {a as b} → bound b + original a; default imports join on the bound name); bare calls and new C() constructions buffer unresolved. Relative specifiers resolve with extension + /index fallbacks; bare (npm) specifiers never resolve. Member calls (obj.m()), builtins, and module-level sites yield no edges.

Go collector

Each Go file is walked once (collect_go_file): funcs, methods (reparented to their receiver struct when same-file), struct types → class, interface types → interface (with method_elem children as methods), other named types → type. Blank imports bind *, dot imports bind . (their target files are searched for otherwise unresolved bare sites). Import tails match indexed .go stems; selector calls (pkg.Fn()), builtins, and package-level sites yield no edges.

Schema (Ladybug)

  • CodeNode: id (PK), root, file_path, kind (file|class|function|method|interface|type|import), name, language, start_line, end_line, parent_id, is_placeholder
  • Contains / Imports / Calls: rels between CodeNode rows (target_module on Imports, site_line on Calls).

site_line is the 1-based call-site line inside the caller (Calls rels only; NULL = unknown). Callees sort by it.

Node ids (all languages)

  • file node: base (path anchored at the CLI root, e.g. workflows/review.py)
  • def node: base:name:start:end (e.g. workflows/review.py:helper:10:15)
  • import node: base:import:<name>:<line>

Every stored Calls edge points at real node ids — unresolvable call sites are dropped at build time, so no placeholder rows exist (is_placeholder stays on the schema for old databases only). Querier's check:

// 1. file's nodes (find the caller)
MATCH (f:CodeNode {id: '<file>'})-[:Contains]->(n) RETURN n;
// 2. caller's callees, in implementation order
MATCH (c:CodeNode {id: '<caller id>'})-[e:Calls]->(d) RETURN d ORDER BY e.site_line;

Tables are created with create_all (IF NOT EXISTS): after a schema change, delete the .lbdb file and re-index.

Querying (agent ladder)

The query verbs are read-only and built for agents that don't know node id shapes — every step returns the ids the next step needs:

  1. query overview — what's indexed (counts by kind/edge/language).
  2. query files — rel paths (a file node id is its rel path).
  3. query search --name <fragment> [--kind …] [--file …] [--limit N] — substring-match def names → full rows with ids. Zero hits and truncation (truncated: true) tell the agent to rephrase/narrow.
  4. query node|callees|callers|children --id <id> (or --name exact + optional --file; ambiguity is an error listing hits) and query imports --file <rel> — drill down; new names chain back to search.

--root narrows to one indexed root (default: all), --json emits the stable chaining envelope ({root, verb, count, truncated, items[]} with {id, kind, name, file_path, language, start_line, end_line, parent_id} plus site_line on calls items and target_module on imports items). Querying a database that was never indexed exits 1 with a hint instead of an empty result — and read paths never create files or directories (only index persistence creates ~/.codegraph/).

Module map

  • cli.py — arg parsing, amain wiring (read → build_graph → out), main, run_stats, run_query
  • query.py — pure exploration shaping: search_nodes, exact_matches, JSON envelope, human rendering
  • pipeline.py — read / build_graph / out, ScannedSources / BuiltGraph / IndexResult, OutputMode, print_tree / print_nodes / print_calls
  • parser/__init__.py — barrel: re-exports only, no logic
  • parser/rows.py — FileRows, build_file_rows, build_python_file_rows
  • parser/links.py — ImportEntry, build_file_import_map, build_dot_import_targets, resolve_call_edges (+ per-language module-specifier resolvers)
  • parser/lang_python.py — Python collect phase (collect_python_file)
  • parser/lang_typescript.py — TS/JS collect phase (collect_ts_file)
  • parser/lang_go.py — Go collect phase (collect_go_file)
  • parser/queries.py — kept, unwired
  • parser/base.py — ParsedFile / ParsedDefinition / ParsedImport / ParsedCall IR types
  • parser/raw_core.py — tree-sitter parse, walk, span, text helpers
  • models.py — Node / Edge dataclasses (+ NodeKind, EdgeKind)
  • graph_store.py — LadybugStore (UNWIND ingest, clear_all, add_all, count/list queries, narrow get_node / callees / callers / children / file_imports / list_files reads)
  • tree.py — GraphSnapshot, render_tree (nesting by contains, calls in site_line order)
  • walk.py — suffix → language discovery, noise-dir pruning
  • config.py — DEFAULT_DB (~/.codegraph/graph.lbdb) + --db path resolution: file path or :memory: (ResolvedDb)

Design notes

  • Pipeline is functional: read -> build_graph -> out. Extractors and builders are pure; I/O lives only at the edge (discover + read, persist, print).
  • Build output is a simple flat graph: BuiltGraph carries merged nodes + edges tuples only. Every consumer (persist, snapshot, printers) derives what it needs from those two lists.
  • Graph values are frozen dataclasses holding tuples — immutable snapshots, safe to share between build and out.
  • Parsing uses raw tree_sitter_language_pack.get_parser().parse(). Import names are recovered from the statement source text in pure Python.
  • Parsing runs on the event-loop thread (tree-sitter objects are not thread-safe); only file I/O goes through asyncio.to_thread.
  • Ladybug is embedded: one AsyncConnection per store, and out() holds a single store from persist through the snapshot reads, so :memory: databases stay alive for the whole call.
  • Calls are bare-name only (name(…)), alias-aware via the import map (from utils import helper as h + h() resolves to helper; TS import {a as b} + b() resolves to a; TS default imports join on the bound name; Go dot-imported files are a fallback); new C() in TS counts as a call. Attribute / member calls (obj.method(), self.x(), pkg.Fn()), builtins, stdlib, third-party, star-import calls, and module-level call sites yield no edges.
  • Files with unsupported suffixes are discovered, then skipped ((N skipped) in the summary).

Tests

uv run --package codegraph pytest tests

Release files for sentinel-codegraph 0.3.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for sentinel-codegraph 0.3.0
File Size Uploaded
sentinel_codegraph-0.3.0.tar.gz 49.6 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for sentinel-codegraph 0.3.0
File Interpreter ABI Platform
sentinel_codegraph-0.3.0-py3-none-any.whl Python 3 none any Details

Total release size: 101.9 kB

Release files / sentinel_codegraph-0.3.0.tar.gz

Download URL sentinel_codegraph-0.3.0.tar.gz
Size 49.6 kB
Tags Source
SHA-256 checksum
How to use checksums
4aab88d5b141d229dfddda3003f2cbccdd2a37a81bae5d3ddadc72d308c77f00
BLAKE2b-256 checksum
How to use checksums
95fdfcc2a66eea1720f74813e32ef193535190d8a37d1f535526c0455dde625a
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via uv/0.11.25 {"installer":{"name":"uv","version":"0.11.25","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":null,"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}

Release files / sentinel_codegraph-0.3.0-py3-none-any.whl

Download URL sentinel_codegraph-0.3.0-py3-none-any.whl
Size 52.2 kB
Tags Python 3
SHA-256 checksum
How to use checksums
d57f0eb7a97390079eb47b44d3b6764c286cdb8c16c39b85988361a81f2b323c
BLAKE2b-256 checksum
How to use checksums
84fba8955f0e31590f8db5fe7b18fc1bb89ae804093118e6f74b43d4739aaf67
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via uv/0.11.25 {"installer":{"name":"uv","version":"0.11.25","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":null,"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}

Release history Release notifications | RSS feed

This release

0.3.0 This release

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page