Skip to main content
base keeps a map of your code built by tree-sitter, and base ast query asks it questions. The difference from grep is that the map knows what a call is, so “everything that calls this” is an answer rather than a text match that also finds the comments.

Map an app once

Run it from anywhere with --target, or from inside the app without it. Tree-sitter covers more than thirty-five languages. Why it works. The extraction walks the directory, parses each file, and writes entities and relationships to that app’s own map at <app>/.base-ast/ast.ttl. One map per app, not one shared index, which is why two apps in one workspace never overwrite each other’s. The gotcha. The hooks also map an app on their own, the first time a session works in it, but that build runs in the background. A query straight after first contact can come back empty until the map lands; base ast list shows when it has. Running the sync yourself is how you get a map you can query right now.

The four questions

Why it works. Each of these is a different edge in the map rather than a different search. --calls is walking call edges, which is why it finds the caller in a file that never mentions the function by the name you typed. Next rung. Everything has a short form: base a q -c "auth" is base ast query --contains "auth". -f, -i and -t cover file, imports and target. --calls has no short form.

Any other relation

The four flags above cover the two relations people ask for most. --relation reaches every other relation the map holds.
That answers both directions at once: what MyClass inherits, and what inherits MyClass. Why it works. The valid relation names are read out of the map itself rather than a fixed list inside base, so a relation the extractor learns to write becomes queryable with no change here. implements, extends, contains and hasMethod all work the same way. When you get the name wrong. An unrecognised relation answers with the ones that are present, so an empty result never has to be guessed at.
Underscores, hyphens and case are all ignored, so imports_from, importsFrom and importsfrom are the same query. The gotcha. Before v0.14.3 only --calls and --imports were reachable, and inherits was missing from every map anyway. An older map answers nothing here until it is rebuilt with base sync --ast.

Querying another app

The gotcha worth remembering. Without --target, the query reads the map belonging to where you are standing. Asking about apps/portal from the workspace root returns nothing rather than an error, which reads exactly like “there is no such function”. If a query comes back empty, check where you are before you conclude anything.
This prints every registered map with its app name, entity count, path and last-synced time. An app with no row has never been mapped.

Staying current

Once an app has a map, it refreshes on its own after each turn: a hook re-runs the sync for the app you are in and any app you edited. The gotcha. The refresh is debounced by twenty seconds and never waited on, so a burst of fast turns can skip one. Nothing is lost when that happens, because skipped apps are queued for the next one, but a map can be a few seconds behind mid-burst. Force it with base sync --ast if that matters. An app with no map gets one on first contact: the first time a session works in it, the hooks start a build in the background (base ast ensure is the same step by hand). They never map your home folder or a workspace folder that only holds other apps, and a tree past the extractor’s file-count threshold waits for you: session start prints the counts and the exact command to run. The 15-minute limit. Since 0.16.0 a build the hooks start stops after 15 minutes, with everything it started, so a stuck build cannot run forever. The next session start says so and keeps the old map if there was one. A tree that genuinely needs longer is built by hand with base sync --ast --yes --no-time-limit --target <folder>. A pnpm workspace whose pnpm-workspace.yaml lists its own root (- .) maps since 0.16.0 instead of crashing the build, and an entry base cannot read is skipped with one notice (#172). When a session searches source code with grep, rg, find, ag, ack, fd or Select-String inside a mapped app, the tool hook suggests base ast query with a plain name taken from the pattern. Since 0.16.0 it stays quiet for ls, git, base and searches of docs, config or data files. In a folder with no map it says what base is doing about it, once per session, and never tells you to map an app base maps by itself.

Where the map lives

<app>/.base-ast/ast.ttl, beside the app, outside both tiers, never merged into graph.nq. .base-ast/ writes its own .gitignore, so the map excludes itself from version control. Whoever clones the repository has no map until they run base sync --ast themselves, which takes seconds and produces paths that are right for their machine.
If you have seen an “AST graph not yet populated” hint on a workspace you know is mapped, that was a real bug: the hint checked the pre-migration path while queries checked the correct one, so it fired forever no matter what you did. It is fixed as of the shipped release, and the check now resolves the same way queries do. Verified from source at 0ace1ba.A current hint about a missing map means the app genuinely has none yet. base ast list settles it either way.

Repairing the graph’s view of a map

Use this when entities exist but their links do not: tasks that do not show up under their project, a decision a domain-scoped search cannot find. It backfills the missing edges and changes nothing else.

Next

Querying the graph

Asking open questions, and walking the graph node by node.

Code reference

Every base ast and base sync flag.