Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 47 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,53 @@

All notable changes to the Toolpath workspace are documented here.

## `path query` — query the local cache with jaq — 2026-06-29

Adds `path query`, a porcelain that loads every step in the local cache
(`~/.toolpath/documents/`) into a single JSON array and transforms it with
an in-process jaq (pure-Rust jq) filter. Selection, projection, ranking,
grouping, and top-N are all just jaq, so one command answers "find every
turn that mentions `RefCell`", "which sessions touched `cmd_resume.rs`?",
"the 10 steps that cost the most tokens", and so on.

- **`path query [scope flags] ['<jq filter>']`** — each array element is a
Toolpath step (`step`/`change`/`meta` verbatim) wrapped with `cache_id`,
`path` (the parent path's `id`/`base`/`meta`), and `dead_end` (whether the
step is off the head's ancestry). With no filter it emits the array; a
filter is the same as piping that array to `jq`. Output mirrors jq:
pretty on a TTY, compact when piped (`-c` forces compact), and `-r`
prints string results unquoted (pipe a column of ids/paths into another
command, or read a turn's text/diff unescaped).
- **Scope flags.** File selection (before parse): `--source <name>`
(cache-id prefix, e.g. `claude`/`git`), `--id <cache-id>` (repeatable),
`--input <file>` (`-` for stdin, repeatable). Content scoping (per path):
`--project <path>` (matches a `file://` base) and `--kind <selector>`
(semver-prefix match, e.g. `agent-coding-session/v1`). Everything else is
a jaq predicate on the real structure.
- **`path kind`** — the cold-start companion: lists the kinds the binary
bundles a spec for; `path kind <kind>` prints that kind's bundled
`schema.json` (the per-field type + semantics reference for writing
filters). A trailing `/<version>` pins a version with the same prefix
rule as `--kind`.
- **Streaming executor (no flag).** So the whole cache needn't sit in memory,
the executor *reads the filter*: it parses the jaq into its AST and, when it
can prove the shape decomposable, runs per document with a bounded merge —
element-wise filters (`.[] | …`, `map(…)`) stream one doc at a time, and
algebraic aggregations split into a per-file partial + combine (top-N
`sort_by(k) | .[:N]`, `length`/`add`/`min`/`max`). A global top-N is a subset
of the per-file top-Ns, so the answer is identical. Anything not provably
decomposable (e.g. `group_by`, `unique`) falls back to the whole-array path,
which is still lean (values held once, no whole-cache byte buffer). The
planner never changes an answer — validated by tests asserting streamed
output equals slurp byte-for-byte. `TOOLPATH_QUERY_EXPLAIN=1` prints the
chosen strategy to stderr.

**Breaking** (pre-1.0): the former `path query` subcommands change.
`ancestors` moves to `path p query ancestors`; `dead-ends` and `filter`
are gone in favor of jaq forms — `path query 'map(select(.dead_end))'` and
`path query 'map(select(.step.actor | startswith("agent:")))'`. `path-cli`
0.14.0 → 0.15.0; adds the `jaq-core`/`jaq-std`/`jaq-json` dependencies.

## Token usage: once per message, with per-step attribution + kind v1.1.0 — 2026-06-17

Fixes token over-counting in derived documents (~3× output-token
Expand Down
17 changes: 11 additions & 6 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,10 +75,10 @@ Requires Rust 1.85+ (edition 2024). Pinned to 1.94.0 via `rust-toolchain.toml`.
The binary is called `path` (package: `path-cli`; the older `toolpath-cli` package is a deprecated shim that still installs the same binary for users running `cargo install toolpath-cli`):

The top-level surface is the porcelain (`show`, `share`, `resume`,
`query`, `auth`, `haiku`). Lower-level building blocks live under
`query`, `kind`, `auth`, `haiku`). Lower-level building blocks live under
`path p …` (plumbing): `p list`, `p import`, `p export`, `p cache`,
`p render`, `p merge`, `p validate`, `p derive`, `p project`,
`p incept`, `p track`.
`p incept`, `p track`, `p query` (graph traversal: `ancestors`).

```bash
# Plumbing: import from external formats into the local toolpath cache
Expand Down Expand Up @@ -118,9 +118,13 @@ cargo run -p path-cli -- p cache rm <cache-id>
# Inspect / analyze
cargo run -p path-cli -- p render dot --input doc.json
cargo run -p path-cli -- p render md --input doc.json --detail full
cargo run -p path-cli -- query dead-ends --input doc.json
cargo run -p path-cli -- query ancestors --input doc.json --step-id step-003
cargo run -p path-cli -- query filter --input doc.json --actor "agent:"
# Query the whole local cache with a jaq (jq) filter over wrapped steps
cargo run -p path-cli -- query 'map(select(.dead_end)) | map(.step.id)'
cargo run -p path-cli -- query --source claude 'map(select(any(.change[].structural.token_usage; .input_tokens > 50000)))'
cargo run -p path-cli -- query --input doc.json 'map(select(.step.actor | startswith("agent:")))'
cargo run -p path-cli -- kind # list bundled kinds
cargo run -p path-cli -- kind agent-coding-session # print a kind's schema (field reference)
cargo run -p path-cli -- p query ancestors --input doc.json --step-id step-003
cargo run -p path-cli -- p merge doc1.json doc2.json --title "Combined"
cargo run -p path-cli -- p list git --repo .
cargo run -p path-cli -- p list github --repo owner/repo
Expand Down Expand Up @@ -186,7 +190,7 @@ Tests live alongside the code (`#[cfg(test)] mod tests`), plus `path-cli` has in
- `toolpath-cursor`: 78 unit + 8 integration round-trip + 1 real-DB sanity + 1 doc test (state.vscdb SQLite reader, bubble store + composer header parsing, content-addressed blob lookup, projector with full TOOL_TABLE coverage, JSONL transcript ingest in `examples/dump_fixture.rs`)
- `toolpath-pi`: 133 unit + 26 integration + 5 doc tests (types, paths, error, reader, io, provider)
- `toolpath-dot`: 30 unit + 2 doc tests (render, visual conventions, escaping)
- `path-cli`: 294 unit + 65 integration tests (import/export/cache, track sessions, merge, validate, roundtrip, render-md snapshots, deprecation aliases, pathbase HTTP mock-server tests, fzf-friendly TSV output, `path resume` orchestration with injectable `ExecStrategy`). For an end-to-end check against a real Pathbase deployment, run `scripts/test-pathbase-live.sh <url>` — it does an anon round-trip in a sandboxed config dir and, if you're logged into that URL, an authed pathstash round-trip too.
- `path-cli`: 317 unit + 92 integration tests (import/export/cache, track sessions, merge, validate, roundtrip, render-md snapshots, deprecation aliases, pathbase HTTP mock-server tests, fzf-friendly TSV output, `path resume` orchestration with injectable `ExecStrategy`, `path query`/`path kind` jaq filters + kind-selector matching + step wrapping over a `$TOOLPATH_CONFIG_DIR` cache sandbox, streaming-planner recognition + streamed-output-equals-slurp equality checks). For an end-to-end check against a real Pathbase deployment, run `scripts/test-pathbase-live.sh <url>` — it does an anon round-trip in a sandboxed config dir and, if you're logged into that URL, an authed pathstash round-trip too.
- `toolpath-cli`: 0 tests (it's a one-line `path_cli::run()` shim crate that exists only so `cargo install toolpath-cli` keeps installing the `path` binary)

Validate example documents: `for f in examples/*.json; do cargo run -p path-cli -- p validate --input "$f"; done`
Expand Down Expand Up @@ -254,3 +258,4 @@ Build the site after changes: `cd site && pnpm run build` (should produce 11 pag
- Conversation metadata title field: `toolpath-claude::ConversationMetadata`, `toolpath-gemini::ConversationMetadata`, and `toolpath-pi::SessionMeta` all expose `first_user_message: Option<String>` — the first non-empty user-prompt text. Populated cheaply during the metadata pass (single-pass for Claude/Gemini; one extra short read for Pi). Used by the picker UI but useful for any "list sessions by topic" surface.
- `path share` is the one-shot equivalent of `path p import <harness> | path p export pathbase`. It probes installed agent harnesses (claude/gemini/codex/opencode/pi), aggregates their sessions into a single fzf picker, and ranks rows whose project (claude/gemini/pi) or recorded cwd (codex/opencode) canonicalizes to the current directory at the top. `--harness` narrows the picker to one provider; `--harness X --session Y` (and `--project P` for keyed providers) skips the picker entirely. Pathbase flags (`--url`, `--anon`, `--repo`, `--slug`, `--public`) match `path export pathbase`. By default the derived doc is written to the cache like `import` does; pass `--no-cache` to skip.
- `path resume <input>` is the inverse of `path share`. It accepts a Pathbase URL, an `owner/repo/slug` shorthand, a local toolpath JSON file, or a cache id; resolves it (caching URL fetches under `~/.toolpath/documents/` unless `--no-cache`); validates that the document is a single agent-bearing `Path`; then opens an `fzf` harness picker (skipped with `--harness X`). The picker pre-selects the source harness inferred from `path.meta.source` (`claude-code`/`gemini-cli`/`codex`/`opencode`/`pi`) when it's installed. After picking, `path resume` projects the session into the harness's on-disk layout under the chosen working directory (default: shell cwd; override with `-C, --cwd P`) and `execvp`'s the harness's resume command (`claude -r <id>` / `gemini --resume <id>` / `codex resume <id>` / `opencode --session <id>` / `pi --session <id>`). On Windows it spawns and waits, propagating the exit code. The exec is mockable via `cmd_resume::ExecStrategy` — production uses `RealExec`; integration tests use `RecordingExec` to capture the recipe without launching a real harness.
- `path query` does not load the whole cache into memory when it can avoid it. `crates/path-cli/src/query/plan.rs` parses the jaq filter into jaq's own AST (`jaq_core::load::parse::Term`) and classifies it into a `Plan`: `PerFileStream` (`.[] | g` / `map`-shaped element-wise work — run per document, print as you go), `Decompose { reduce }` (algebraic aggregations — run the whole filter per file, concatenate the per-file outputs, then run a derived combine: `map`→`add`, top-N `sort_by(k)|.[:N]`→`add | sort_by(k)|.[:N]`, `length`/`add`→`add`, `min`/`max`→themselves), or `Slurp` (the always-correct whole-array fallback). Recognition is conservative (a non-distributive prefix like `unique`/`group_by`, or an unrecognized tail, slurps), so **the planner never changes an answer** — `crates/path-cli/src/query/filter.rs` tests assert streamed output equals slurp byte-for-byte. `filter::execute` compiles the filter once (jaq's compiled `Filter` is fully owned, so it's reused across files) and drives the plan; `mod.rs::stream_files` yields one document's wrapped steps at a time. `TOOLPATH_QUERY_EXPLAIN=1` prints the chosen plan to stderr. No user-facing flag — it's automatic. Tie-break caveat: a streamed top-N matches slurp's *ranking*, but boundary ties may resolve to different specific rows.
193 changes: 192 additions & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ toolpath-github = { version = "0.6.0", path = "crates/toolpath-github" }
toolpath-dot = { version = "0.5.0", path = "crates/toolpath-dot" }
toolpath-md = { version = "0.7.0", path = "crates/toolpath-md" }
toolpath-pi = { version = "0.6.0", path = "crates/toolpath-pi" }
path-cli = { version = "0.14.0", path = "crates/path-cli" }
path-cli = { version = "0.15.0", path = "crates/path-cli" }
pathbase-client = { version = "0.2.0", path = "crates/pathbase-client" }

reqwest = { version = "0.13", default-features = false, features = ["blocking", "json", "rustls"] }
Expand Down
Loading