@escherize/probe
v0.1.0
Published
Inspect Gleam source files: list top-level declarations with line ranges, types, and signatures.
Maintainers
Readme
gleam-probe
Token-efficient overview of Gleam source files. Built for AI coding agents that need to know what's in a file without reading the whole thing into their context window.
One row per top-level declaration: line range, visibility, kind, signature. A 500-line Gleam file collapses to ~30 lines. The agent reads the index, decides what's relevant, then opens only the lines it needs.
The problem
LLM coding agents waste context reading entire files to figure out
"what's in here." A 2000-line module costs thousands of tokens to skim,
when the agent only needed to know "is there a parse_url function and
what does it return."
gleam-probe ls produces a structural index so the agent can:
- List declarations + signatures (cheap).
- Decide which one to open.
- Read only those lines.
Same idea as clj-surgeon :ls for Clojure or ctags, but emits the kind
of structured signal an LLM actually parses well.
Output
Default (optimized for LLM agents — terse, regular, low-token):
src/probe.gleam (16 decls)
imports: argv, gleam/io, gleam/list, glance, glint, simplifile, probe/decl, probe/line_index, probe/render, probe/scan
L12-25 pub fn main() -> Nil
L27-32 fn rewrite_short_help(arg: String) -> String
L34-45 fn root_command() -> glint.Command(Nil)
L47-77 fn ls_command() -> glint.Command(Nil)
L79-101 fn run_ls(patterns: List(String), as_json: Bool, show_doc: Bool) -> NilRules:
- Imports collapse into one
imports:line. Most files have many; rolling them up saves significant tokens. L12for single-line decls,L12-25for ranges.pubprefix only when public (absence = private).- Signature uses Gleam keyword form:
fn,type,opaque type,const. - Doc preview appended as
-- <first line, truncated>when present. - One decl per line. No padding, no marker columns.
With --doc the full doc expands inline, prefixed with /// to match
Gleam source convention:
L4-6 pub fn greet(name: String) -> String
/// Greets a person by name.
/// Returns the greeting string.JSON (for tool calls, MCP servers, scripts):
[
{
"path": "src/foo.gleam",
"decls": [
{
"kind": "fn",
"name": "greet",
"public": true,
"line_start": 12,
"line_end": 14,
"signature": "fn greet(name: String) -> String",
"params": [{ "label": "", "name": "name", "type": "String" }],
"return": "String"
}
]
}
]Install
npx @escherize/probe ls src/foo.gleamOr:
npm install -g @escherize/probe
gleam-probe ls src/foo.gleamNode >= 18. No Gleam toolchain required - ships precompiled JS.
Usage
gleam-probe ls <file-or-glob> [--json] [--doc]
gleam-probe ls -h
gleam-probe -h<file-or-glob> accepts:
- single file:
src/foo.gleam - directory (recursive):
src/ - glob:
'src/**/*.gleam','*.gleam'
Sorted by line, so output mirrors source order.
JSON (--json) always includes a doc field (empty string when none),
regardless of --doc. @attr lines above a decl are skipped during doc
extraction.
Recipes for agents
Public API of a module:
gleam-probe ls src/my/api.gleam --json \
| jq '.[].decls[] | select(.public) | .signature'Find a function by name across a project:
gleam-probe ls 'src/**/*.gleam' --json \
| jq -r '.[] | .path as $p | .decls[] | select(.name == "parse") | "\($p):\(.line_start)"'Token math (rough): the JSON form of a 1500-line, 60-decl Gleam file is ~3-5k tokens. Reading the file itself is ~15-25k. Roughly 4-6x cheaper, and the agent doesn't burn budget on function bodies it didn't need.
Agent integration
Stable JSON shape, exit code 0 on success, errors to stderr. Suitable for:
- Tool-call wrappers that shell out to the binary
- MCP servers exposing a "list_declarations" tool
- Pre-prompt scaffolding ("here is an index of the codebase: ...")
- Editor integrations that surface symbols without running the LSP
What it parses
Backed by glance, the Gleam parser written in Gleam. Handles: opaque types, type aliases, labeled params, generics, function types, tuple types, attributes.
pub opaque type Box(a) { Box(a) }
pub type Map(k, v) = dict.Dict(k, v)
pub fn map(over list: List(a), with f: fn(a) -> b) -> List(b) { ... }
const default_port: Int = 8080Glance is permissive - may parse files the official compiler rejects.
gleam-probe does no semantic analysis; surfaces what Glance gives it.
Limitations
- Top-level decls only. No nested
lets or expression-level info. - No call graph (planned:
gleam-probe deps). - No source slicing (planned:
gleam-probe show <file>:<name>). - Comments/docstrings not yet attached to decls (planned).
Development
gleam build --target javascript
gleam test
./bin/gleam-probe.mjs ls src/
gleam run -- ls src/Layout:
src/
probe.gleam # CLI entry (glint)
probe/
decl.gleam # glance AST -> Decl record
line_index.gleam # byte offset -> line number
scan.gleam # path/glob -> file list
render.gleam # table + JSON output
type_print.gleam # glance.Type -> string
test/
probe_test.gleam
bin/
gleam-probe.mjs # Node entry shimBuilt on
- glance - Gleam parser
- glint - CLI framework
- simplifile - filesystem
- argv - argv access
Prior art
- clj-surgeon - same idea for Clojure
ctags/etags- same idea, 1980s- LSP
documentSymbol- same idea, heavyweight
License
MIT
