Skip to content
21 changes: 21 additions & 0 deletions .claude-plugin/marketplace.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
{
"$schema": "https://anthropic.com/claude-code/marketplace.schema.json",
"name": "toolpath",
"version": "0.1.0",
"description": "Toolpath — provenance for AI coding sessions. Share agent sessions to Pathbase and query your local session history via the path CLI",
"owner": {
"name": "Empathic"
},
"plugins": [
{
"name": "path",
"description": "Slash commands for the Toolpath path CLI: /path:share uploads an agent session to Pathbase, /path:query runs jaq queries over your local session cache. Installs the path binary globally on first use",
"version": "0.1.4",
"author": {
"name": "Empathic"
},
"source": "./plugins/claude-code",
"category": "productivity"
}
]
}
36 changes: 36 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,42 @@ API. Together they broke resume for most projected sessions.
resume, and without one the first appended entry landed on the same
line as the last projected entry, corrupting the JSONL.

## Claude Code plugin — `/path:share` and `/path:query` — 2026-07-29

Adds a Claude Code plugin so users get the `path` CLI without a manual
install: `/plugin marketplace add empathic/toolpath`, then
`/plugin install path@toolpath`.

- **`.claude-plugin/marketplace.json`** (marketplace `toolpath`) +
**`plugins/claude-code/`** (plugin `path` 0.1.4). Future harness
integrations land as siblings under `plugins/`.
- Two slash commands: `/path:share` uploads an agent session to Pathbase
(defaults to the current conversation, identified exactly via
`$CLAUDE_CODE_SESSION_ID`; takes a session hint, `--harness`, and the
`share` pass-through flags) and `/path:query` answers questions about
the local session cache, translating plain English into jaq filters.
Session listings are fetched lazily as tool output, never embedded in
the command context: eager embedding re-injects old session titles as
prompt text, where the harness re-expands `@path` file mentions.
- No committed binaries: both commands run through
`plugins/claude-code/scripts/ensure-path.sh`, which prefers an
existing Toolpath install, else downloads the latest GitHub release
(sha256-verified, same logic as `install.sh`) and installs globally to
`~/.local/bin` (`~/.toolpath/bin` when a foreign `path` binary claims
the name). Warns when the resolved CLI predates the plugin's
`MIN_VERSION`.
- Command docs encode two discovered constraints: inline `!` context
commands and model-issued Bash must avoid `$PWD` (Claude Code's
permission checker rejects non-statically-analyzable commands — the
`sessions` helper mode exists for this), and `--project` must be
absolute (path-cli does not canonicalize relative values; `.` silently
matches nothing).
- Tests: `scripts/test-plugin.sh` — manifest consistency plus offline
bootstrap tests against a stubbed curl/release (resolution order, exec
and sessions modes, min-version warning, clean install, foreign-name
fallback, checksum rejection) — wired into `quality_gates.sh` as the
new `plugin` gate; plugin shell scripts join the shellcheck gate.

## `path p cache sync` — incremental session ingestion — 2026-07-16

Adds `path p cache sync [types…]`, the first step toward a cache that
Expand Down
6 changes: 6 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,11 @@ crates/
toolpath-cli/ # deprecated shim that re-exports path-cli (excluded from the workspace; see below)
pathbase-client/ # progenitor-derived client for the Pathbase HTTP API
# (spec at crates/pathbase-client/openapi.json; refresh via scripts/refresh-pathbase-openapi.sh)
.claude-plugin/
marketplace.json # Claude Code plugin marketplace (add with `/plugin marketplace add empathic/toolpath`)
plugins/
claude-code/ # Claude Code plugin "path": /path:share + /path:query, bundles the CLI
# (future harness integrations land as siblings: plugins/gemini-cli/, plugins/codex/, ...)
schema/toolpath.schema.json # JSON Schema for the toolpath format
examples/*.json # 12 example documents (step, path, graph)
RFC.md # full format specification
Expand Down Expand Up @@ -281,4 +286,5 @@ Build the site after changes: `cd site && pnpm run build` (should produce 11 pag
- `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` 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` (array concat), top-N `sort_by(k)|.[:N]`→`add | sort_by(k)|.[:N]`, `length`→`add` over exact integer counts), or `Slurp` (the always-correct whole-array fallback). Recognition is conservative — a non-distributive prefix like `unique`/`group_by` slurps, and so do scalar `add` (float sums re-associate across per-file partials), `min`/`max` (`[] | min == null` poisons the merge), and any unrecognized tail — 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.
- Cache sync: `path p cache sync [types…]` (`crates/path-cli/src/artifact.rs`: `ArtifactType` + `ArtifactRef` + the stamp helpers; `sync/engine.rs`: manifest + ingestion loop, no UI — it reports through a `SyncObserver` trait, `&mut ()` for a silent sync; `sync/sources.rs`: an `ArtifactSource` trait — enumerate / stamp / derive — with one impl per provider, so the engine never matches on artifact type; `cmd_cache.rs`: the stderr progress line + summary) incrementally ingests artifacts into the cache — no args syncs every artifact type. Change detection is **stat-level**: each artifact is enumerated as an `ArtifactRef` whose fingerprint is the source file's mtime + size (claude: the *whole session chain* — max segment mtime + summed segment sizes via `claude_chain_stamp`, because Claude Code rotates to a new file on continuation while the chain keeps its oldest segment's id, so appends land in the newest file, not the head; the chain comes from the same cached index `list_conversations` builds; codex: rollout file, id from the stem's trailing UUID; pi: session file, id from a one-line header peek; copilot: `session-state/<id>/events.jsonl`, pure read-dir + stat) or the DB row's updated-at (opencode: header-only `SELECT time_updated`; cursor: composer headers' `lastUpdatedAt`, bubble-less drafts skipped, workspace-less composers *included* unlike `share`). Gemini enumerates via `PathResolver::list_session_entries` (`toolpath-gemini` 0.6.1), whose identity peek is bounded to the first 4 KiB of a main file. Deciding "nothing changed" reads no session bodies — a no-op sync is milliseconds. Changed/new artifacts derive through the same provider managers (each source calls the `derive_*_session_with` helpers in `derive.rs`). Manifest at `~/.toolpath/manifest.json`: artifact type → artifact id → `{path?, cache_id, modified?, size?, synced_at}`; atomic temp+rename writes, `0600`, checkpointed every 10 writes (interruption-safe: a killed run keeps nearly everything it derived, and derives run newest-first so partial progress covers the sessions that matter most); writers serialize on an advisory lock (`manifest.json.lock`) and every write is a locked read-merge-save — checkpoints merge only the records the run wrote — so concurrent invocations (query auto-syncs, imports) union their records instead of clobbering each other. Pending work reports progress on stderr (`\r`-updating `<type> done/total` on a TTY, a plain line every 25 items otherwise; no-op syncs stay silent). Sync always writes the cache with force — refresh semantics — and never deletes: artifacts removed upstream keep their cache docs and manifest records (archive, not mirror). Derivation failures warn and tally, they don't abort. A record's `cache_id` is *optional*: a record without one is "known, not materialized" — created when `p cache rm` evicts a doc (rm downgrades the record; the next sync re-materializes it, and sync also verifies the doc file actually exists before skipping, so even out-of-band deletions self-heal). Claude derives leave `DeriveConfig.project_path` unset so `path.base` comes from the session's own recorded cwd rather than the lossy slug. `path query` runs this sync implicitly before reading, scoped to its flags (`--source X` → that type; `--id`s → their prefixes; bare query → all types; `--input`-only → none), quiet unless something was ingested, degrading to the cache as-is if sync fails; `--no-sync` opts out. `p import` and `share` record what they write: every session derive carries a provenance `ArtifactRef` (stamped *before* the source is read, in `DerivedDoc.provenance`), and the cache-write sites call `sync::record_artifact` so the next sync sees those artifacts as unchanged instead of re-deriving them. Every import flow — explicit `--session`, picker multi-select, `--all`, and the most-recent fallbacks — loops the per-session helpers, so every session write is recorded; there is no bulk `derive_project` path in the CLI anymore, and `p import pi --all` now emits one Path per session like every other provider (it used to emit a single combined Graph). `--no-cache` paths record nothing: the manifest describes the cache.
- Claude Code plugin: `.claude-plugin/marketplace.json` (marketplace `toolpath`) + `plugins/claude-code/` (plugin `path`, so commands are `/path:share` and `/path:query`). The plugin does **not** commit binaries — both commands invoke the CLI through `plugins/claude-code/scripts/ensure-path.sh`, which prefers an existing Toolpath `path` on PATH (identity-checked via `--help`), else `~/.local/bin/path`, else `~/.toolpath/bin/path`, else downloads the latest GitHub release (sha256-verified, same logic as `scripts/install.sh`) and installs globally to `~/.local/bin` — falling back to `~/.toolpath/bin` when a foreign binary named `path` claims the name. Two hard-won constraints baked into the command docs: slash-command inline `!` context commands and model-issued Bash must not contain `$PWD`/variables (Claude Code's permission checker rejects commands it can't statically analyze — hence the `sessions` and `current-session` helper modes, the latter reading `$CLAUDE_CODE_SESSION_ID` so no-arg `/path:share` shares exactly the running session), and `--project` must always be an absolute path (path-cli does not canonicalize relative `--project` values; `.` silently matches nothing). Tests: `scripts/test-plugin.sh` (manifest consistency + offline bootstrap tests against a stubbed curl/release), wired in as the `plugin` quality gate; plugin shell scripts are shellchecked. Dev loop: `claude --plugin-dir ./plugins/claude-code`. Future harness integrations go under `plugins/<harness>/` (only Claude Code plugins are marketplace entries; other harnesses distribute their own way). Version bumps: keep `plugins/claude-code/.claude-plugin/plugin.json` and the matching entry in `.claude-plugin/marketplace.json` in lockstep (test-plugin.sh asserts this); the binary is unpinned (latest release) with `MIN_VERSION` in ensure-path.sh naming the oldest CLI the command docs support.
- `ArtifactType` (`crates/path-cli/src/artifact.rs`) is the general enum naming artifact sources — the seven agent harnesses (incl. copilot) plus `Git` (8 variants). Git artifacts are *recorded* in the manifest by `p import git` (id `<repo-tag>-<graph-id>`, `path` = the repo directory) but never *discovered* — there is no machine-wide registry of repos — so sync reports them and leaves them alone. Github and pathbase are deliberately not artifact types: they are remote services, not local artifact sources, and their imports stay out of the manifest. It derives `clap::ValueEnum` and is used by `p cache sync` types, the sync manifest keys, `ArtifactRow.artifact_type`, and `cmd_import`'s cache-id prefixes (`name()` is both the manifest key and the `make_id` source string). The deliberately parallel `Harness` enum (`crates/path-cli/src/harness.rs`, alongside `HarnessBundle`) names the seven agent *runtimes* — things sessions can be shared from and resumed into — and is what `share`/`resume` `--harness` take, so future non-harness artifact types stay unrepresentable there (you can't resume into a git repo). `Harness::artifact_type()` maps into the general enum; `ArtifactType::harness()` is the partial inverse. Keep new code on `ArtifactType` unless it's genuinely harness-only.
11 changes: 11 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,17 @@ cargo install path-cli

Both install a binary called `path`.

Claude Code users can skip the manual install entirely — the Toolpath plugin
bundles the CLI (downloaded and installed globally on first use) and adds
`/path:share` and `/path:query` slash commands:

```
/plugin marketplace add empathic/toolpath
/plugin install path@toolpath
```

See [plugins/claude-code](plugins/claude-code/) for details.

> The older `toolpath-cli` crate name still works — `cargo install toolpath-cli` is now a thin shim that pulls in `path-cli` and installs the same binary. New users should reach for `path-cli` directly; the shim will eventually be retired.

## Workspace
Expand Down
16 changes: 16 additions & 0 deletions plugins/claude-code/.claude-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
{
"name": "path",
"version": "0.1.4",
"description": "Toolpath for Claude Code — /path:share uploads an agent session to Pathbase, /path:query answers questions about your local session history. Bundles the path CLI, installed globally on first use",
"author": {
"name": "Empathic"
},
"keywords": [
"toolpath",
"provenance",
"sessions",
"pathbase",
"share",
"query"
]
}
55 changes: 55 additions & 0 deletions plugins/claude-code/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
# Toolpath plugin for Claude Code

Slash commands for the [Toolpath](https://toolpath.net) `path` CLI, with no
manual install step: the plugin resolves the binary on first use — preferring
an existing install, otherwise downloading the latest release and installing
it globally to `~/.local/bin`.

## Install

Inside Claude Code:

```
/plugin marketplace add empathic/toolpath
/plugin install path@toolpath
```

## Commands

| Command | Description |
|---------|-------------|
| `/path:share` | Share an agent session to Pathbase and get a link. With no arguments it shares the current conversation; pass a hint to pick another session, `--harness <name>` for another harness, and `--anon` / `--public` / `--repo` / `--name` / `--url` to control the upload. |
| `/path:query` | Ask questions about your local agent-session history. Takes plain English (translated to a jaq filter) or a jaq filter verbatim, plus `--source` / `--project` scoping. |

## How the binary is bundled

Both commands run the CLI through `scripts/ensure-path.sh`, which resolves in
order:

1. `path` already on `PATH`, if it identifies as the Toolpath CLI (an
existing cargo / Homebrew / install.sh install is always preferred)
2. `~/.local/bin/path` (or `$TOOLPATH_INSTALL_DIR/path`)
3. `~/.toolpath/bin/path` (or `$TOOLPATH_CONFIG_DIR/bin/path`)
4. Otherwise it downloads the latest GitHub release for your platform,
verifies the sha256 checksum, and installs to `~/.local/bin` — falling
back to `~/.toolpath/bin` if an unrelated binary named `path` already
claims the `~/.local/bin` name.

The install is global: after first use, `path` works from your own shell too
(the script prints a PATH hint if `~/.local/bin` isn't on it). Platforms
without prebuilt binaries (Intel Mac, native Windows) get a
`cargo install path-cli` recipe instead.

## Development

Run Claude Code with the local plugin:

```bash
claude --plugin-dir ./plugins/claude-code
```

Validate manifests and exercise the bootstrap logic offline:

```bash
scripts/test-plugin.sh # from the repo root
```
Loading
Loading