Quickstart
cants points at a TypeScript or JavaScript project and produces one typed artifact — its symbol table, call graph, and external symbols. You choose where that artifact lands: a single analysis.json file, or a Neo4j property graph you can query with Cypher and read back from the CLDK Python SDK. Three steps below: install it, run it against a project, and read the result.
-
Install the CLI.
Grab the prebuilt
cantsbinary the fast way — pip, Homebrew, or the shell installer. No toolchain required.Terminal window pip install codeanalyzer-typescriptTerminal window brew install codellm-devkit/homebrew-tap/cantsTerminal window curl -fsSL https://raw.githubusercontent.com/codellm-devkit/codeanalyzer-typescript/main/cants-installer.sh | sh# installs to ~/.local/bin/cantsTerminal window cants --help -
Run it against a project.
Point
--inputat any TypeScript/JavaScript project root and--outputat a directory for the result.Terminal window cants --input ./my-ts-project --output ./outBy default the analyzer first materializes the project’s
node_modules(so imported library calls resolve), walks every source file, builds the symbol table and call graph, and writes./out/analysis.json. Intermediate state is cached under.codeanalyzer/in the project. -
Read the result.
analysis.jsonis a singleTSApplicationobject with three top-level keys.Terminal window jq 'keys' ./out/analysis.json# [ "call_graph", "external_symbols", "symbol_table" ]jq '.symbol_table | length' ./out/analysis.json # modules analyzedjq '.call_graph | length' ./out/analysis.json # call edgesjq '.external_symbols | length' ./out/analysis.json # phantom library targetsThat’s it — a directory of source files is now a typed, queryable model of the program.
Load it into a graph
Section titled “Load it into a graph”The call graph is a flat list of source -> target edges keyed by callable signature, so it drops straight into any graph library — here, Python’s networkx:
import jsonimport networkx as nx
app = json.load(open("./out/analysis.json"))
g = nx.DiGraph()for edge in app["call_graph"]: g.add_edge(edge["source"], edge["target"])
print(g.number_of_nodes(), "nodes,", g.number_of_edges(), "edges")# Is a sink reachable from some caller? A graph query, not a guess.# print(nx.has_path(g, caller_sig, sink_sig))Edge endpoints are either a real TSCallable.signature from the symbol table or a TSExternalSymbol.signature for a call into a library — both are plain strings, so external boundaries show up as nodes in the graph rather than disappearing.
Emit a Neo4j property graph
Section titled “Emit a Neo4j property graph”analysis.json is one file per project: you load it whole into memory, and it doesn’t compose across a portfolio. When you want a queryable, persistent system of record — many applications in one database, cross-service traversals, a model your tools depend on — project the analysis into Neo4j instead. Same in-memory analysis, different output target: --emit neo4j replaces the JSON entirely.
There are two ways to emit it, chosen by whether you point at a live database.
With no --neo4j-uri, cants writes a self-contained Cypher script — DDL constraints and indexes, a scoped wipe of this app’s prior subgraph, then batched MERGE for nodes and relationships.
cants --input ./my-ts-project --emit neo4j --app-name my-ts-app --output ./out# -> ./out/graph.cypherLoad it into any Neo4j with cypher-shell:
cypher-shell -u neo4j -p "$NEO4J_PASSWORD" < ./out/graph.cypherA snapshot is portable and reproducible, but it’s not incremental — a static script has no view of the live database, so it rewrites this application’s whole subgraph each time.
Pass --neo4j-uri and cants pushes over Bolt incrementally: it diffs each module’s content_hash against the database and only re-pushes what changed. This is the path for a CI job or a Kubernetes Job/CronJob that keeps a shared graph current.
Prefer the NEO4J_PASSWORD environment variable over --neo4j-password — a flag value is visible in your shell history and the process list.
export NEO4J_PASSWORD='…'
cants \ --input ./my-ts-project \ --emit neo4j \ --app-name my-ts-app \ --neo4j-uri bolt://localhost:7687 \ --neo4j-user neo4j \ --neo4j-database neo4j--neo4j-uri, --neo4j-user, --neo4j-password, and --neo4j-database also read from NEO4J_URI, NEO4J_USERNAME, NEO4J_PASSWORD, and NEO4J_DATABASE — a flag wins when both are set.
Your first Cypher query
Section titled “Your first Cypher query”Once the graph is loaded, the model is a graph traversal instead of a parse-the-whole-file problem. How many modules did this application contribute?
MATCH (a:TSApplication {name: $app})-[:TS_HAS_MODULE]->(m:TSModule)RETURN count(m) AS modulesWalk one hop further to list the call edges out of a callable:
MATCH (a:TSApplication {name: $app})-[:TS_HAS_MODULE]->(:TSModule) -[:TS_DECLARES]->(c:TSCallable)-[r:TS_CALLS]->(callee)RETURN c.signature, callee.signature, r.weightORDER BY r.weight DESCLIMIT 10See the graph schema reference for every node label, relationship type, and property.
Read the graph from Python — no re-analysis
Section titled “Read the graph from Python — no re-analysis”The big enterprise unlock: analysis is produced once, centrally, and read cheaply everywhere. CLDK has a read-only Neo4j backend — point it at the Bolt URI and it reconstructs the same typed model objects and the same networkx call graph the in-process analyzer builds, with no toolchain, no cants binary, and no project source on the consumer. It only needs the graph and read-only credentials.
Install the driver extra, then select the backend by passing a Neo4jConnectionConfig:
pip install "cldk[neo4j]"from cldk import CLDKfrom cldk.analysis.commons.backend_config import Neo4jConnectionConfig
analysis = CLDK.typescript( backend=Neo4jConnectionConfig( uri="bolt://localhost:7687", username="neo4j", password="neo4j", application_name="my-ts-app", # == the --app-name the graph was loaded with ),)
classes = analysis.get_classes() # Dict[str, TSClass]externals = analysis.get_external_symbols() # phantom library targets, for source->sink reachabilitycg = analysis.get_call_graph() # networkx.DiGraphapplication_name must equal the --app-name the graph was loaded with — it scopes every query to one application’s subgraph. Read-only credentials are sufficient; the backend never writes. Other methods on the facade include get_call_graph(), get_interfaces(), get_functions(), get_callers(), get_callees(), get_class_hierarchy(), and get_decorators() — the same surface as the in-process backend.
Go deeper with level 2
Section titled “Go deeper with level 2”The default run is level 1: the TypeScript checker resolves the call graph, RTA expands virtual dispatch, and phantom nodes capture external calls — fast, no extra tooling. Level 2 adds CodeQL enrichment for the dynamic cases the checker can’t reach.
cants --input ./my-ts-project --output ./out --analysis-level 2