token-pilot 0.46.0 → 0.47.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. package/.claude-plugin/marketplace.json +3 -3
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/CHANGELOG.md +54 -0
  4. package/README.md +1 -1
  5. package/agents/tp-api-surface-tracker.md +1 -1
  6. package/agents/tp-audit-scanner.md +1 -1
  7. package/agents/tp-commit-writer.md +1 -1
  8. package/agents/tp-context-engineer.md +1 -1
  9. package/agents/tp-dead-code-finder.md +1 -1
  10. package/agents/tp-debugger.md +1 -1
  11. package/agents/tp-dep-health.md +1 -1
  12. package/agents/tp-doc-writer.md +1 -1
  13. package/agents/tp-history-explorer.md +1 -1
  14. package/agents/tp-impact-analyzer.md +1 -1
  15. package/agents/tp-incident-timeline.md +1 -1
  16. package/agents/tp-incremental-builder.md +1 -1
  17. package/agents/tp-migration-scout.md +1 -1
  18. package/agents/tp-onboard.md +1 -1
  19. package/agents/tp-performance-profiler.md +1 -1
  20. package/agents/tp-pr-reviewer.md +1 -1
  21. package/agents/tp-refactor-planner.md +1 -1
  22. package/agents/tp-review-impact.md +1 -1
  23. package/agents/tp-run.md +1 -1
  24. package/agents/tp-session-restorer.md +1 -1
  25. package/agents/tp-ship-coordinator.md +1 -1
  26. package/agents/tp-spec-writer.md +1 -1
  27. package/agents/tp-test-coverage-gapper.md +1 -1
  28. package/agents/tp-test-triage.md +1 -1
  29. package/agents/tp-test-writer.md +1 -1
  30. package/dist/ast-index/client.d.ts +5 -1
  31. package/dist/ast-index/client.js +90 -10
  32. package/dist/ast-index/types.d.ts +69 -0
  33. package/dist/core/event-log.d.ts +7 -0
  34. package/dist/core/event-log.js +9 -1
  35. package/dist/core/validation.d.ts +6 -0
  36. package/dist/core/validation.js +18 -0
  37. package/dist/handlers/explore.d.ts +17 -0
  38. package/dist/handlers/explore.js +90 -0
  39. package/dist/handlers/test-summary.js +42 -0
  40. package/dist/server/tool-definitions.d.ts +150 -0
  41. package/dist/server/tool-definitions.js +22 -0
  42. package/dist/server/tool-profiles.js +1 -0
  43. package/dist/server.js +36 -1
  44. package/package.json +4 -4
@@ -6,14 +6,14 @@
6
6
  },
7
7
  "metadata": {
8
8
  "description": "Token Pilot — save 60-90% tokens when AI reads code",
9
- "version": "0.46.0"
9
+ "version": "0.47.0"
10
10
  },
11
11
  "plugins": [
12
12
  {
13
13
  "name": "token-pilot",
14
14
  "source": "./",
15
- "description": "Reduces token consumption by 60-90% via AST-aware lazy file reading, structural symbol navigation, and cross-session tool-usage analytics. 23 MCP tools + 25 subagents + budget watchdog hooks.",
16
- "version": "0.46.0",
15
+ "description": "Reduces token consumption by 60-90% via AST-aware lazy file reading, structural symbol navigation, and cross-session tool-usage analytics. 24 MCP tools + 25 subagents + budget watchdog hooks.",
16
+ "version": "0.47.0",
17
17
  "author": {
18
18
  "name": "Digital-Threads"
19
19
  },
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "token-pilot",
3
- "version": "0.46.0",
3
+ "version": "0.47.0",
4
4
  "description": "Saves 60-90% tokens on AI code reading. AST-aware lazy reads, symbol navigation, find_usages, structural git diff/log, edit-safety guard, Task-routing matcher, cross-session telemetry (errors + diagnostics), 25 tp-* subagents tiered to haiku/sonnet/opus with budget watchdog.",
5
5
  "author": {
6
6
  "name": "Digital-Threads",
package/CHANGELOG.md CHANGED
@@ -5,6 +5,60 @@ All notable changes to Token Pilot will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [0.47.0] - 2026-06-24
9
+
10
+ ### Added — `explore` tool: one-shot ranked context + graph blast-radius
11
+
12
+ New MCP tool **`explore`** wraps ast-index 3.48's `explore` command: for a query
13
+ it returns ranked relevant symbols, the source heads of the top files, **graph
14
+ neighbours (callers + subclasses — the blast radius, via RWR over the
15
+ call/inheritance graph)**, and related test files — in a single compact block.
16
+ Replaces the common `find_usages` → `read_symbol` → `call_tree` chain with one
17
+ call. `graph` defaults on; `max_files` caps the source heads. Falls back to a
18
+ clear "requires ast-index >= 3.48" message when an older binary is resolved.
19
+
20
+ ### Changed — bump `@ast-index/cli` to 3.48.1
21
+
22
+ Picks up the upstream TypeScript-indexing fix, the rebuild **swap-and-restore**
23
+ guard (a failed rebuild no longer wipes the index), memory caps, and FUSE-safe
24
+ canonicalisation. `npm audit` stays at 0 vulnerabilities.
25
+
26
+ ### Changed — `buildIndex` trusts swap-and-restore
27
+
28
+ When a rebuild fails, `buildIndex` now checks for the index the binary preserved
29
+ and uses it (instead of throwing and falling back to raw reads). The lock-case
30
+ and generic-failure recovery paths are unified.
31
+
32
+ _Deferred:_ `--local` / subtree query scoping (to re-enable ast-index on
33
+ multi-repo / worktree parents instead of disabling it) needs a rooting-model
34
+ rework and ships separately.
35
+
36
+ ## [0.46.1] - 2026-06-18
37
+
38
+ ### Fixed — node:test (`node --test`) TAP output parsing
39
+
40
+ `test_summary` did not recognise `node --test`: `detectRunner` had no `node`
41
+ case, so node:test output fell through to the generic parser. node:test emits a
42
+ TAP footer (`# pass N` / `# fail N`, number after the word) and `ok N - name`
43
+ points (`ok` before the number), which the generic `<N> passed` regex never
44
+ matches — a green 2/2 run was reported as 0 passed. Added a `node` runner:
45
+ detected by command (`node --test` / `node:test`) or by the TAP footer, parsing
46
+ pass/fail/skipped/tests from the footer with a fallback to counting `ok` /
47
+ `not ok` lines; failure names come from `not ok N - name`. Purely additive — no
48
+ existing parser touched.
49
+
50
+ ### Changed — dev-dependency security + decoupled registry publish (no shipped change)
51
+
52
+ - Bump `vitest` / `@vitest/coverage-v8` to 4.x — clears the 6 remaining
53
+ dev-only high advisories in the vitest/vite/esbuild chain. `npm audit` now
54
+ reports **0 vulnerabilities** (dev + prod). No runtime/package change: dev
55
+ deps are not shipped to npm consumers; the full 1402-test suite is green on
56
+ vitest 4.
57
+ - `publish-mcp.yml`: the MCP Registry job no longer hard-depends on npm-job
58
+ **success** (`if: !cancelled()`). A failed npm publish (e.g. EOTP on a
59
+ manual-token release) no longer blocks the registry update; re-run the job
60
+ via `workflow_dispatch` after a manual `npm publish`.
61
+
8
62
  ## [0.46.0] - 2026-06-13
9
63
 
10
64
  ### Added — UserPromptSubmit per-turn reinforcement (caveman-style awareness)
package/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  **Token-efficient AI coding, enforced.** Cuts context consumption in AI coding assistants by up to **90%** without changing the way you work.
4
4
 
5
- > **Why it matters more now:** as frontier models move up in price — Claude's Fable 5 is the most capable (and most expensive-per-token) tier yet — the tokens you *don't* spend reading code are worth more, not less. The savings are in tokens; the value is in tokens × price. Token Pilot keeps the expensive main thread lean so the premium model spends its budget on reasoning, not on re-reading files.
5
+ > **Why it matters more now:** as frontier models move up in price, the tokens you *don't* spend reading code are worth more, not less. The savings are in tokens; the value is in tokens × price. Token Pilot keeps the expensive main thread lean so the premium model spends its budget on reasoning, not on re-reading files.
6
6
 
7
7
  Three layers, each useful on its own, stronger together:
8
8
 
@@ -9,7 +9,7 @@ tools:
9
9
  - mcp__token-pilot__read_symbol
10
10
  - Bash
11
11
  model: haiku
12
- token_pilot_version: "0.46.0"
12
+ token_pilot_version: "0.47.0"
13
13
  token_pilot_body_hash: dd184501203fa7f3c73f419c4ffbe33c4be75400cb64a7a51733a3fe23f6e085
14
14
  requiredMcpServers:
15
15
  - "token-pilot"
@@ -11,7 +11,7 @@ tools:
11
11
  - Grep
12
12
  - Read
13
13
  model: sonnet
14
- token_pilot_version: "0.46.0"
14
+ token_pilot_version: "0.47.0"
15
15
  token_pilot_body_hash: d172f600bf32277ea6eb4cbbee4542ddd698a986dcd96997d33930561964569b
16
16
  requiredMcpServers:
17
17
  - "token-pilot"
@@ -8,7 +8,7 @@ tools:
8
8
  - mcp__token-pilot__test_summary
9
9
  - mcp__token-pilot__outline
10
10
  - Bash
11
- token_pilot_version: "0.46.0"
11
+ token_pilot_version: "0.47.0"
12
12
  token_pilot_body_hash: de64a406b5176de19f7422619c7de7949b1f28865f225402c9cea9255f377428
13
13
  requiredMcpServers:
14
14
  - "token-pilot"
@@ -13,7 +13,7 @@ tools:
13
13
  - Edit
14
14
  - Glob
15
15
  model: sonnet
16
- token_pilot_version: "0.46.0"
16
+ token_pilot_version: "0.47.0"
17
17
  token_pilot_body_hash: 68b32af2dacd82ebe52c4eec93edb903d452688274c3065218270627c564d8b0
18
18
  requiredMcpServers:
19
19
  - "token-pilot"
@@ -11,7 +11,7 @@ tools:
11
11
  - Grep
12
12
  - Read
13
13
  model: sonnet
14
- token_pilot_version: "0.46.0"
14
+ token_pilot_version: "0.47.0"
15
15
  token_pilot_body_hash: d9b7f5b7ae6f4ae21305c775361bcab097cc774370a6d976c093571d46d55021
16
16
  requiredMcpServers:
17
17
  - "token-pilot"
@@ -12,7 +12,7 @@ tools:
12
12
  - Read
13
13
  - Bash
14
14
  model: sonnet
15
- token_pilot_version: "0.46.0"
15
+ token_pilot_version: "0.47.0"
16
16
  token_pilot_body_hash: 052413de8d92377edcde6ae5c823f5378db304baccfa29e8866467f42553a500
17
17
  requiredMcpServers:
18
18
  - "token-pilot"
@@ -9,7 +9,7 @@ tools:
9
9
  - Bash
10
10
  - Read
11
11
  model: haiku
12
- token_pilot_version: "0.46.0"
12
+ token_pilot_version: "0.47.0"
13
13
  token_pilot_body_hash: e14dc57493d816f8c2e017963e2ef5f66bea50fd0b805a80e8a0d97c968427e7
14
14
  requiredMcpServers:
15
15
  - "token-pilot"
@@ -13,7 +13,7 @@ tools:
13
13
  - Edit
14
14
  - Glob
15
15
  model: haiku
16
- token_pilot_version: "0.46.0"
16
+ token_pilot_version: "0.47.0"
17
17
  token_pilot_body_hash: 57d741794ab40e31a7ac49c68ea39a9088f5827cdef866ce81bfca1b7c9180cf
18
18
  requiredMcpServers:
19
19
  - "token-pilot"
@@ -10,7 +10,7 @@ tools:
10
10
  - Bash
11
11
  - Read
12
12
  model: haiku
13
- token_pilot_version: "0.46.0"
13
+ token_pilot_version: "0.47.0"
14
14
  token_pilot_body_hash: 7b70fa76a60e3c58a1de4f56c32c0f166424137e203a0cf1c8654e7c9235d904
15
15
  requiredMcpServers:
16
16
  - "token-pilot"
@@ -12,7 +12,7 @@ tools:
12
12
  - mcp__token-pilot__read_symbols
13
13
  - Read
14
14
  model: sonnet
15
- token_pilot_version: "0.46.0"
15
+ token_pilot_version: "0.47.0"
16
16
  token_pilot_body_hash: 351a987e11eba63852f5431a16d8eb53104f4f689f82fdcc5a2bf4db948ba92f
17
17
  requiredMcpServers:
18
18
  - "token-pilot"
@@ -8,7 +8,7 @@ tools:
8
8
  - mcp__token-pilot__read_symbol
9
9
  - Bash
10
10
  model: inherit
11
- token_pilot_version: "0.46.0"
11
+ token_pilot_version: "0.47.0"
12
12
  token_pilot_body_hash: de5722bfea374eaab096c1ae635c37879e7a91370ee3cd0532f4240be03c91eb
13
13
  requiredMcpServers:
14
14
  - "token-pilot"
@@ -13,7 +13,7 @@ tools:
13
13
  - Edit
14
14
  - Bash
15
15
  model: sonnet
16
- token_pilot_version: "0.46.0"
16
+ token_pilot_version: "0.47.0"
17
17
  token_pilot_body_hash: 375a824d0d847bb5453ec594c7a62ad566ee7e4d92717b0473f771f1a0477c60
18
18
  requiredMcpServers:
19
19
  - "token-pilot"
@@ -11,7 +11,7 @@ tools:
11
11
  - Grep
12
12
  - Glob
13
13
  model: sonnet
14
- token_pilot_version: "0.46.0"
14
+ token_pilot_version: "0.47.0"
15
15
  token_pilot_body_hash: 0334de1bf99b431b65359637d125cda7c44c6f780eb92c57cc538715b1939536
16
16
  requiredMcpServers:
17
17
  - "token-pilot"
@@ -10,7 +10,7 @@ tools:
10
10
  - mcp__token-pilot__smart_read
11
11
  - mcp__token-pilot__smart_read_many
12
12
  - mcp__token-pilot__read_section
13
- token_pilot_version: "0.46.0"
13
+ token_pilot_version: "0.47.0"
14
14
  token_pilot_body_hash: 832e95633fbc8e9b0c10f3e540a327d4be062fb4b3f17a6cce6be13f414e2927
15
15
  requiredMcpServers:
16
16
  - "token-pilot"
@@ -11,7 +11,7 @@ tools:
11
11
  - Bash
12
12
  - Read
13
13
  model: sonnet
14
- token_pilot_version: "0.46.0"
14
+ token_pilot_version: "0.47.0"
15
15
  token_pilot_body_hash: b61f06380d80798fa2e49d37bcba0653495bee04dd6bdbc1feff9a75607b0508
16
16
  requiredMcpServers:
17
17
  - "token-pilot"
@@ -11,7 +11,7 @@ tools:
11
11
  - mcp__token-pilot__read_for_edit
12
12
  - Read
13
13
  model: sonnet
14
- token_pilot_version: "0.46.0"
14
+ token_pilot_version: "0.47.0"
15
15
  token_pilot_body_hash: f83f50d05b4f70285ae7afed2b1a406fc436df56e61a0aedbfb31edc7f2b6e66
16
16
  requiredMcpServers:
17
17
  - "token-pilot"
@@ -8,7 +8,7 @@ tools:
8
8
  - mcp__token-pilot__outline
9
9
  - mcp__token-pilot__read_symbol
10
10
  model: sonnet
11
- token_pilot_version: "0.46.0"
11
+ token_pilot_version: "0.47.0"
12
12
  token_pilot_body_hash: c5f6fc122c89e16e5cf774045f92169ee3468555320b898171ba13eca5323550
13
13
  requiredMcpServers:
14
14
  - "token-pilot"
@@ -9,7 +9,7 @@ tools:
9
9
  - mcp__token-pilot__module_info
10
10
  - Bash
11
11
  model: sonnet
12
- token_pilot_version: "0.46.0"
12
+ token_pilot_version: "0.47.0"
13
13
  token_pilot_body_hash: 8ef3c3341cbfed4eb8dd130126a9683edc57e378c92ff0ca764d584fd941c55c
14
14
  requiredMcpServers:
15
15
  - "token-pilot"
package/agents/tp-run.md CHANGED
@@ -16,7 +16,7 @@ tools:
16
16
  - Glob
17
17
  - Bash
18
18
  model: haiku
19
- token_pilot_version: "0.46.0"
19
+ token_pilot_version: "0.47.0"
20
20
  token_pilot_body_hash: 2b08618d34a61f00aafccbda9fed6d83243296dedb83440edbd2d5c28bb6dbc4
21
21
  requiredMcpServers:
22
22
  - "token-pilot"
@@ -9,7 +9,7 @@ tools:
9
9
  - mcp__token-pilot__session_budget
10
10
  - Bash
11
11
  - Read
12
- token_pilot_version: "0.46.0"
12
+ token_pilot_version: "0.47.0"
13
13
  token_pilot_body_hash: 529374ed728f5eed5b758b3be3da65624783c0bf0c1a253d7d661a843eb5f767
14
14
  requiredMcpServers:
15
15
  - "token-pilot"
@@ -11,7 +11,7 @@ tools:
11
11
  - Read
12
12
  - Grep
13
13
  model: sonnet
14
- token_pilot_version: "0.46.0"
14
+ token_pilot_version: "0.47.0"
15
15
  token_pilot_body_hash: a60f6ae110eb3138064bce074e8ba26fa0ce5f4659df1624a9d9d3646803391b
16
16
  requiredMcpServers:
17
17
  - "token-pilot"
@@ -9,7 +9,7 @@ tools:
9
9
  - Read
10
10
  - Write
11
11
  model: sonnet
12
- token_pilot_version: "0.46.0"
12
+ token_pilot_version: "0.47.0"
13
13
  token_pilot_body_hash: c7a4e8b39228fd5158528f389c924c5ff2d98c4b9b05ee0106d54a26c5dc1350
14
14
  requiredMcpServers:
15
15
  - "token-pilot"
@@ -10,7 +10,7 @@ tools:
10
10
  - mcp__token-pilot__test_summary
11
11
  - Glob
12
12
  - Grep
13
- token_pilot_version: "0.46.0"
13
+ token_pilot_version: "0.47.0"
14
14
  token_pilot_body_hash: be81eed53a3720d146cf89e4a14a7a56577633f7c84c234c412ab70d64c05b11
15
15
  requiredMcpServers:
16
16
  - "token-pilot"
@@ -8,7 +8,7 @@ tools:
8
8
  - mcp__token-pilot__find_usages
9
9
  - mcp__token-pilot__read_symbol
10
10
  model: sonnet
11
- token_pilot_version: "0.46.0"
11
+ token_pilot_version: "0.47.0"
12
12
  token_pilot_body_hash: 362ecf4cb03b059421ea26933473700900073dc38b3a7fe271208dfb1ae14f90
13
13
  requiredMcpServers:
14
14
  - "token-pilot"
@@ -13,7 +13,7 @@ tools:
13
13
  - Edit
14
14
  - Bash
15
15
  model: sonnet
16
- token_pilot_version: "0.46.0"
16
+ token_pilot_version: "0.47.0"
17
17
  token_pilot_body_hash: 269f2fe22ff4517c277d3f56ca67d8a5527b93290ab21079a83ba7af22c1b5a9
18
18
  requiredMcpServers:
19
19
  - "token-pilot"
@@ -1,5 +1,5 @@
1
1
  import type { FileStructure } from "../types.js";
2
- import type { AstIndexSymbolDetail, AstIndexSearchResult, AstIndexUsageResult, AstIndexImplementation, AstIndexHierarchyNode, AstIndexRefsResponse, AstIndexMapResponse, AstIndexConventionsResponse, AstIndexCallerEntry, AstIndexCallTreeNode, AstIndexChangedEntry, AstIndexUnusedSymbol, AstIndexImportEntry, AstIndexAgrepMatch, AstIndexTodoEntry, AstIndexDeprecatedEntry, AstIndexAnnotationEntry, AstIndexModuleEntry, AstIndexModuleDep, AstIndexUnusedDep, AstIndexModuleApi } from "./types.js";
2
+ import type { AstIndexSymbolDetail, AstIndexSearchResult, AstIndexUsageResult, AstIndexImplementation, AstIndexHierarchyNode, AstIndexRefsResponse, AstIndexMapResponse, AstIndexConventionsResponse, AstIndexCallerEntry, AstIndexCallTreeNode, AstIndexChangedEntry, AstIndexUnusedSymbol, AstIndexImportEntry, AstIndexAgrepMatch, AstIndexTodoEntry, AstIndexDeprecatedEntry, AstIndexAnnotationEntry, AstIndexModuleEntry, AstIndexModuleDep, AstIndexUnusedDep, AstIndexModuleApi, AstIndexExploreResult } from "./types.js";
3
3
  export declare class AstIndexClient {
4
4
  private static readonly MAX_INDEX_FILES;
5
5
  private binaryPath;
@@ -34,6 +34,10 @@ export declare class AstIndexClient {
34
34
  fuzzy?: boolean;
35
35
  }): Promise<AstIndexSearchResult[]>;
36
36
  usages(symbolName: string): Promise<AstIndexUsageResult[]>;
37
+ explore(query: string, options?: {
38
+ maxFiles?: number;
39
+ graph?: boolean;
40
+ }): Promise<AstIndexExploreResult>;
37
41
  implementations(name: string): Promise<AstIndexImplementation[]>;
38
42
  hierarchy(name: string, options?: {
39
43
  inFile?: string;
@@ -128,16 +128,22 @@ export class AstIndexClient {
128
128
  }
129
129
  catch (buildErr) {
130
130
  const errMsg = buildErr instanceof Error ? buildErr.message : String(buildErr);
131
- if (errMsg.includes("lock") || errMsg.includes("already running")) {
132
- const count = parseFileCount(await this.exec(["--format", "json", "stats"]).catch(() => ""));
133
- if (count > 0 && count <= AstIndexClient.MAX_INDEX_FILES) {
134
- this.indexed = true;
135
- console.error(`[token-pilot] ast-index: using existing index (${count} files, rebuild skipped due to lock)`);
136
- return;
137
- }
138
- if (count > AstIndexClient.MAX_INDEX_FILES) {
139
- return this.handleOversizedIndex(count);
140
- }
131
+ // ast-index 3.46+ preserves the previous index when a rebuild aborts
132
+ // (swap-and-restore guard). Before giving up, query stats once: if a
133
+ // usable index survived, use it instead of losing all indexed tools and
134
+ // falling back to raw reads. Covers both the lock / already-running case
135
+ // and any generic rebuild failure the binary recovered from.
136
+ const lockCase = errMsg.includes("lock") || errMsg.includes("already running");
137
+ const count = parseFileCount(await this.exec(["--format", "json", "stats"]).catch(() => ""));
138
+ if (count > 0 && count <= AstIndexClient.MAX_INDEX_FILES) {
139
+ this.indexed = true;
140
+ console.error(lockCase
141
+ ? `[token-pilot] ast-index: using existing index (${count} files, rebuild skipped due to lock)`
142
+ : `[token-pilot] ast-index: rebuild failed but previous index preserved (${count} files) — using it`);
143
+ return;
144
+ }
145
+ if (count > AstIndexClient.MAX_INDEX_FILES) {
146
+ return this.handleOversizedIndex(count);
141
147
  }
142
148
  console.error(`[token-pilot] ast-index: rebuild failed — ${errMsg}`);
143
149
  throw buildErr;
@@ -328,6 +334,80 @@ export class AstIndexClient {
328
334
  return [];
329
335
  }
330
336
  }
337
+ async explore(query, options) {
338
+ await this.ensureIndex();
339
+ const empty = {
340
+ query,
341
+ dominantLanguage: "",
342
+ symbols: [],
343
+ files: [],
344
+ neighbours: [],
345
+ tests: [],
346
+ };
347
+ const args = ["explore", query, "--format", "json"];
348
+ if (options?.maxFiles)
349
+ args.push("-f", String(options.maxFiles));
350
+ // Default graph ON — call/inheritance blast-radius is the value-add;
351
+ // callers opt out with graph: false.
352
+ if (options?.graph !== false)
353
+ args.push("--rwr");
354
+ try {
355
+ const result = await this.exec(args);
356
+ const parsed = JSON.parse(result);
357
+ return {
358
+ query: typeof parsed.query === "string" ? parsed.query : query,
359
+ dominantLanguage: typeof parsed.dominant_language === "string"
360
+ ? parsed.dominant_language
361
+ : "",
362
+ symbols: Array.isArray(parsed.symbols)
363
+ ? parsed.symbols.map((s) => ({
364
+ name: s.name,
365
+ kind: s.kind,
366
+ path: s.path,
367
+ line: s.line,
368
+ score: s.score,
369
+ vendor: s.vendor === true,
370
+ }))
371
+ : [],
372
+ files: Array.isArray(parsed.files)
373
+ ? parsed.files.map((f) => ({
374
+ path: f.path,
375
+ line: f.line,
376
+ source: f.source,
377
+ }))
378
+ : [],
379
+ neighbours: Array.isArray(parsed.neighbours)
380
+ ? parsed.neighbours.map((n) => ({
381
+ name: n.name,
382
+ kind: n.kind,
383
+ path: n.path,
384
+ line: n.line,
385
+ link: n.link,
386
+ }))
387
+ : [],
388
+ tests: Array.isArray(parsed.tests)
389
+ ? parsed.tests.map((t) => ({
390
+ source: t.source,
391
+ tests: Array.isArray(t.tests) ? t.tests : [],
392
+ }))
393
+ : [],
394
+ };
395
+ }
396
+ catch (err) {
397
+ const msg = err instanceof Error ? err.message : String(err);
398
+ console.error(`[token-pilot] ast-index explore failed: ${msg}`);
399
+ // `explore` landed in ast-index 3.48. An older resolved binary reports
400
+ // "unrecognized subcommand 'explore'" — surface that instead of an
401
+ // indistinguishable empty result, so the caller knows to update.
402
+ if (/unrecognized subcommand|unexpected argument/.test(msg)) {
403
+ return {
404
+ ...empty,
405
+ error: "explore requires ast-index >= 3.48 — update the binary",
406
+ };
407
+ }
408
+ return empty;
409
+ }
410
+ }
331
411
  async implementations(name) {
332
412
  await this.ensureIndex();
333
413
  try {
@@ -208,4 +208,73 @@ export interface AstIndexModuleApi {
208
208
  file: string;
209
209
  line: number;
210
210
  }
211
+ /** ast-index explore — one ranked symbol */
212
+ export interface AstIndexExploreSymbol {
213
+ name: string;
214
+ kind: string;
215
+ path: string;
216
+ line: number;
217
+ score: number;
218
+ vendor: boolean;
219
+ }
220
+ /** ast-index explore — one ranked file head (source is line-numbered) */
221
+ export interface AstIndexExploreFile {
222
+ path: string;
223
+ line: number;
224
+ source: string;
225
+ }
226
+ /** ast-index explore — one graph neighbour (blast radius, requires --rwr) */
227
+ export interface AstIndexExploreNeighbour {
228
+ name: string;
229
+ kind: string;
230
+ path: string;
231
+ line: number;
232
+ /** "caller" | "subclass" | string */
233
+ link: string;
234
+ }
235
+ /** ast-index explore — tests grouped by source file */
236
+ export interface AstIndexExploreTestGroup {
237
+ source: string;
238
+ tests: string[];
239
+ }
240
+ /** ast-index explore — mapped result */
241
+ export interface AstIndexExploreResult {
242
+ query: string;
243
+ dominantLanguage: string;
244
+ symbols: AstIndexExploreSymbol[];
245
+ files: AstIndexExploreFile[];
246
+ neighbours: AstIndexExploreNeighbour[];
247
+ tests: AstIndexExploreTestGroup[];
248
+ /** Set when the run failed (e.g. binary too old for `explore`). */
249
+ error?: string;
250
+ }
251
+ /** ast-index explore — raw json shape from the binary (snake_case) */
252
+ export interface AstIndexExploreRaw {
253
+ query?: string;
254
+ dominant_language?: string;
255
+ symbols?: Array<{
256
+ name: string;
257
+ kind: string;
258
+ path: string;
259
+ line: number;
260
+ score: number;
261
+ vendor?: boolean;
262
+ }>;
263
+ files?: Array<{
264
+ path: string;
265
+ line: number;
266
+ source: string;
267
+ }>;
268
+ neighbours?: Array<{
269
+ name: string;
270
+ kind: string;
271
+ path: string;
272
+ line: number;
273
+ link: string;
274
+ }>;
275
+ tests?: Array<{
276
+ source: string;
277
+ tests?: string[];
278
+ }>;
279
+ }
211
280
  //# sourceMappingURL=types.d.ts.map
@@ -56,6 +56,13 @@ export interface HookEvent {
56
56
  * Optional — absent when no workflow is active.
57
57
  */
58
58
  workflow_id?: string;
59
+ /**
60
+ * Loom spine link — id of the Loom task this event belongs to, when the
61
+ * session was launched by Loom (LOOM_TASK_ID set). Lets Loom attribute token
62
+ * savings to a task exactly. Optional — absent outside Loom, so standalone
63
+ * token-pilot events stay byte-identical to before.
64
+ */
65
+ task_id?: string;
59
66
  event: "denied" | "allowed" | "bypass" | "pass-through" | "task" | "diagnostic" | string;
60
67
  file: string;
61
68
  lines: number;
@@ -115,8 +115,16 @@ export async function appendEvent(projectRoot, event) {
115
115
  const wf = event.workflow_id ??
116
116
  process.env.TOKEN_PILOT_WORKFLOW_ID ??
117
117
  process.env.CLAUDE_CODE_WORKFLOW_ID ??
118
+ process.env.LOOM_WORKFLOW_ID ??
118
119
  undefined;
119
- const tagged = wf ? { ...event, workflow_id: wf } : event;
120
+ // Loom spine: tag the task id when the session was launched by Loom.
121
+ // Env-driven so call sites stay unchanged; absent outside Loom → no field.
122
+ const taskId = event.task_id ?? process.env.LOOM_TASK_ID ?? undefined;
123
+ let tagged = event;
124
+ if (wf)
125
+ tagged = { ...tagged, workflow_id: wf };
126
+ if (taskId)
127
+ tagged = { ...tagged, task_id: taskId };
120
128
  await ensureLogDir(projectRoot);
121
129
  await rotateIfNeeded(projectRoot);
122
130
  const line = JSON.stringify(tagged) + "\n";
@@ -179,6 +179,12 @@ export interface ExploreAreaArgs {
179
179
  include?: Array<"outline" | "imports" | "tests" | "changes">;
180
180
  }
181
181
  export declare function validateExploreAreaArgs(args: unknown): ExploreAreaArgs;
182
+ export interface ExploreArgs {
183
+ query: string;
184
+ max_files?: number;
185
+ graph?: boolean;
186
+ }
187
+ export declare function validateExploreArgs(args: unknown): ExploreArgs;
182
188
  export interface SmartLogArgs {
183
189
  path?: string;
184
190
  count?: number;
@@ -533,6 +533,24 @@ export function validateExploreAreaArgs(args) {
533
533
  }
534
534
  return { path: a.path };
535
535
  }
536
+ export function validateExploreArgs(args) {
537
+ if (!args || typeof args !== "object") {
538
+ throw new Error('Arguments must be an object with a "query" parameter.');
539
+ }
540
+ const a = args;
541
+ if (typeof a.query !== "string" || a.query.length === 0) {
542
+ throw new Error('Required parameter "query" must be a non-empty string.');
543
+ }
544
+ const max_files = optionalNumber(a.max_files, "max_files");
545
+ if (max_files !== undefined && max_files < 1) {
546
+ throw new Error('"max_files" must be at least 1.');
547
+ }
548
+ return {
549
+ query: a.query,
550
+ max_files,
551
+ graph: optionalBool(a.graph, "graph"),
552
+ };
553
+ }
536
554
  export function validateSmartLogArgs(args) {
537
555
  if (!args || typeof args !== "object")
538
556
  return {};
@@ -0,0 +1,17 @@
1
+ import type { AstIndexClient } from "../ast-index/client.js";
2
+ import type { ExploreArgs } from "../core/validation.js";
3
+ export interface ExploreMeta {
4
+ query: string;
5
+ symbolCount: number;
6
+ fileCount: number;
7
+ neighbourCount: number;
8
+ testCount: number;
9
+ }
10
+ export declare function handleExplore(args: ExploreArgs, projectRoot: string, astIndex: AstIndexClient): Promise<{
11
+ content: Array<{
12
+ type: "text";
13
+ text: string;
14
+ }>;
15
+ meta: ExploreMeta;
16
+ }>;
17
+ //# sourceMappingURL=explore.d.ts.map
@@ -0,0 +1,90 @@
1
+ // ──────────────────────────────────────────────
2
+ // Constants
3
+ // ──────────────────────────────────────────────
4
+ const MAX_RANKED_SYMBOLS = 12;
5
+ // ──────────────────────────────────────────────
6
+ // Handler — one-shot ranked context + graph blast-radius.
7
+ // Mirrors the shape of handleExploreArea: build a compact, token-efficient
8
+ // text block and return it with lightweight meta.
9
+ // ──────────────────────────────────────────────
10
+ export async function handleExplore(args, projectRoot, astIndex) {
11
+ void projectRoot; // explore runs against the index root, not a path
12
+ const result = await astIndex.explore(args.query, {
13
+ maxFiles: args.max_files,
14
+ graph: args.graph,
15
+ });
16
+ const lines = [];
17
+ lines.push(`# explore: "${result.query}" (lang: ${result.dominantLanguage || "?"})`);
18
+ // Ranked symbols
19
+ if (result.symbols.length > 0) {
20
+ lines.push("");
21
+ lines.push("## Ranked symbols");
22
+ for (const s of result.symbols.slice(0, MAX_RANKED_SYMBOLS)) {
23
+ const vendorTag = s.vendor ? " [vendor]" : "";
24
+ lines.push(`${Math.round(s.score)} ${s.kind} ${s.name} ${s.path}:${s.line}${vendorTag}`);
25
+ }
26
+ }
27
+ // Source — file heads (source is already line-numbered)
28
+ if (result.files.length > 0) {
29
+ lines.push("");
30
+ lines.push("## Source");
31
+ for (const f of result.files) {
32
+ lines.push(`${f.path}:${f.line}`);
33
+ lines.push("```");
34
+ lines.push(f.source.replace(/\n+$/, ""));
35
+ lines.push("```");
36
+ }
37
+ }
38
+ // Graph neighbours (blast radius) — only with --rwr
39
+ if (result.neighbours.length > 0) {
40
+ lines.push("");
41
+ lines.push("## Graph neighbours (blast radius)");
42
+ for (const n of result.neighbours) {
43
+ lines.push(`${n.link} ${n.kind} ${n.name} ${n.path}:${n.line}`);
44
+ }
45
+ }
46
+ // Tests grouped by source
47
+ if (result.tests.length > 0) {
48
+ lines.push("");
49
+ lines.push("## Tests");
50
+ for (const t of result.tests) {
51
+ lines.push(`${t.source}:`);
52
+ for (const test of t.tests) {
53
+ lines.push(` ${test}`);
54
+ }
55
+ }
56
+ }
57
+ const empty = result.symbols.length === 0 &&
58
+ result.files.length === 0 &&
59
+ result.neighbours.length === 0 &&
60
+ result.tests.length === 0;
61
+ if (empty) {
62
+ const reason = result.error ?? "No results — index unavailable or query matched nothing.";
63
+ return {
64
+ content: [
65
+ {
66
+ type: "text",
67
+ text: `# explore: "${result.query}"\n\n${reason}`,
68
+ },
69
+ ],
70
+ meta: {
71
+ query: result.query,
72
+ symbolCount: 0,
73
+ fileCount: 0,
74
+ neighbourCount: 0,
75
+ testCount: 0,
76
+ },
77
+ };
78
+ }
79
+ return {
80
+ content: [{ type: "text", text: lines.join("\n") }],
81
+ meta: {
82
+ query: result.query,
83
+ symbolCount: result.symbols.length,
84
+ fileCount: result.files.length,
85
+ neighbourCount: result.neighbours.length,
86
+ testCount: result.tests.reduce((n, t) => n + t.tests.length, 0),
87
+ },
88
+ };
89
+ }
90
+ //# sourceMappingURL=explore.js.map
@@ -73,6 +73,8 @@ export function detectRunner(command, output) {
73
73
  return 'rspec';
74
74
  if (cmd.includes('mocha'))
75
75
  return 'mocha';
76
+ if (cmd.includes('node --test') || cmd.includes('node:test'))
77
+ return 'node';
76
78
  // Detect from output
77
79
  const lower = output.toLowerCase();
78
80
  if (lower.includes('vitest') || lower.includes('vite'))
@@ -85,6 +87,9 @@ export function detectRunner(command, output) {
85
87
  return 'phpunit';
86
88
  if (lower.includes('--- fail:') || lower.includes('--- pass:') || lower.includes('ok \t'))
87
89
  return 'go';
90
+ // node:test prints a TAP summary footer: "# tests N" + "# pass N" + "# fail N".
91
+ if (/^#\s*tests\s+\d+/m.test(output) && /^#\s*pass\s+\d+/m.test(output))
92
+ return 'node';
88
93
  return 'generic';
89
94
  }
90
95
  // ──────────────────────────────────────────────
@@ -103,6 +108,8 @@ export function parseTestOutput(output, runner) {
103
108
  return parseGoTest(output);
104
109
  case 'cargo':
105
110
  return parseCargoTest(output);
111
+ case 'node':
112
+ return parseNodeTest(output);
106
113
  default:
107
114
  return parseGeneric(output);
108
115
  }
@@ -243,6 +250,41 @@ function parseGoTest(output) {
243
250
  }
244
251
  return result;
245
252
  }
253
+ function parseNodeTest(output) {
254
+ const result = { total: 0, passed: 0, failed: 0, skipped: 0, failures: [] };
255
+ // node:test (`node --test`) prints a TAP summary footer:
256
+ // # tests 2
257
+ // # pass 2
258
+ // # fail 0
259
+ // # skipped 0
260
+ const num = (re) => {
261
+ const m = output.match(re);
262
+ return m ? parseInt(m[1], 10) : null;
263
+ };
264
+ const pass = num(/^#\s*pass\s+(\d+)/m);
265
+ const fail = num(/^#\s*fail\s+(\d+)/m);
266
+ const skip = num(/^#\s*skipped\s+(\d+)/m);
267
+ const tests = num(/^#\s*tests\s+(\d+)/m);
268
+ if (pass !== null || fail !== null) {
269
+ result.passed = pass ?? 0;
270
+ result.failed = fail ?? 0;
271
+ result.skipped = skip ?? 0;
272
+ result.total = tests ?? result.passed + result.failed + result.skipped;
273
+ }
274
+ else {
275
+ // No footer (truncated output) — count the TAP point lines instead.
276
+ result.passed = (output.match(/^ok\s+\d+/gm) ?? []).length;
277
+ result.failed = (output.match(/^not ok\s+\d+/gm) ?? []).length;
278
+ result.total = result.passed + result.failed + result.skipped;
279
+ }
280
+ // Failure names come from the TAP point: "not ok 3 - the test name".
281
+ const failPattern = /^not ok\s+\d+\s*-\s*(.+)$/gm;
282
+ let match;
283
+ while ((match = failPattern.exec(output)) !== null) {
284
+ result.failures.push({ name: match[1].trim(), error: '' });
285
+ }
286
+ return result;
287
+ }
246
288
  function parseCargoTest(output) {
247
289
  const result = { total: 0, passed: 0, failed: 0, skipped: 0, failures: [] };
248
290
  // test result: ok. 5 passed; 0 failed; 1 ignored; 0 measured; 0 filtered out
@@ -100,6 +100,9 @@ export declare const TOOL_DEFINITIONS: ({
100
100
  viaKind?: undefined;
101
101
  format?: undefined;
102
102
  ref?: undefined;
103
+ query?: undefined;
104
+ max_files?: undefined;
105
+ graph?: undefined;
103
106
  count?: undefined;
104
107
  command?: undefined;
105
108
  runner?: undefined;
@@ -191,6 +194,9 @@ export declare const TOOL_DEFINITIONS: ({
191
194
  viaKind?: undefined;
192
195
  format?: undefined;
193
196
  ref?: undefined;
197
+ query?: undefined;
198
+ max_files?: undefined;
199
+ graph?: undefined;
194
200
  count?: undefined;
195
201
  command?: undefined;
196
202
  runner?: undefined;
@@ -276,6 +282,9 @@ export declare const TOOL_DEFINITIONS: ({
276
282
  viaKind?: undefined;
277
283
  format?: undefined;
278
284
  ref?: undefined;
285
+ query?: undefined;
286
+ max_files?: undefined;
287
+ graph?: undefined;
279
288
  count?: undefined;
280
289
  command?: undefined;
281
290
  runner?: undefined;
@@ -357,6 +366,9 @@ export declare const TOOL_DEFINITIONS: ({
357
366
  viaKind?: undefined;
358
367
  format?: undefined;
359
368
  ref?: undefined;
369
+ query?: undefined;
370
+ max_files?: undefined;
371
+ graph?: undefined;
360
372
  count?: undefined;
361
373
  command?: undefined;
362
374
  runner?: undefined;
@@ -429,6 +441,9 @@ export declare const TOOL_DEFINITIONS: ({
429
441
  viaKind?: undefined;
430
442
  format?: undefined;
431
443
  ref?: undefined;
444
+ query?: undefined;
445
+ max_files?: undefined;
446
+ graph?: undefined;
432
447
  count?: undefined;
433
448
  command?: undefined;
434
449
  runner?: undefined;
@@ -501,6 +516,9 @@ export declare const TOOL_DEFINITIONS: ({
501
516
  viaKind?: undefined;
502
517
  format?: undefined;
503
518
  ref?: undefined;
519
+ query?: undefined;
520
+ max_files?: undefined;
521
+ graph?: undefined;
504
522
  count?: undefined;
505
523
  command?: undefined;
506
524
  runner?: undefined;
@@ -597,6 +615,9 @@ export declare const TOOL_DEFINITIONS: ({
597
615
  viaKind?: undefined;
598
616
  format?: undefined;
599
617
  ref?: undefined;
618
+ query?: undefined;
619
+ max_files?: undefined;
620
+ graph?: undefined;
600
621
  count?: undefined;
601
622
  command?: undefined;
602
623
  runner?: undefined;
@@ -678,6 +699,9 @@ export declare const TOOL_DEFINITIONS: ({
678
699
  viaKind?: undefined;
679
700
  format?: undefined;
680
701
  ref?: undefined;
702
+ query?: undefined;
703
+ max_files?: undefined;
704
+ graph?: undefined;
681
705
  count?: undefined;
682
706
  command?: undefined;
683
707
  runner?: undefined;
@@ -768,6 +792,9 @@ export declare const TOOL_DEFINITIONS: ({
768
792
  viaKind?: undefined;
769
793
  format?: undefined;
770
794
  ref?: undefined;
795
+ query?: undefined;
796
+ max_files?: undefined;
797
+ graph?: undefined;
771
798
  count?: undefined;
772
799
  command?: undefined;
773
800
  runner?: undefined;
@@ -841,6 +868,9 @@ export declare const TOOL_DEFINITIONS: ({
841
868
  viaKind?: undefined;
842
869
  format?: undefined;
843
870
  ref?: undefined;
871
+ query?: undefined;
872
+ max_files?: undefined;
873
+ graph?: undefined;
844
874
  count?: undefined;
845
875
  command?: undefined;
846
876
  runner?: undefined;
@@ -910,6 +940,9 @@ export declare const TOOL_DEFINITIONS: ({
910
940
  viaKind?: undefined;
911
941
  format?: undefined;
912
942
  ref?: undefined;
943
+ query?: undefined;
944
+ max_files?: undefined;
945
+ graph?: undefined;
913
946
  count?: undefined;
914
947
  command?: undefined;
915
948
  runner?: undefined;
@@ -985,6 +1018,9 @@ export declare const TOOL_DEFINITIONS: ({
985
1018
  viaKind?: undefined;
986
1019
  format?: undefined;
987
1020
  ref?: undefined;
1021
+ query?: undefined;
1022
+ max_files?: undefined;
1023
+ graph?: undefined;
988
1024
  count?: undefined;
989
1025
  command?: undefined;
990
1026
  runner?: undefined;
@@ -1054,6 +1090,9 @@ export declare const TOOL_DEFINITIONS: ({
1054
1090
  viaKind?: undefined;
1055
1091
  format?: undefined;
1056
1092
  ref?: undefined;
1093
+ query?: undefined;
1094
+ max_files?: undefined;
1095
+ graph?: undefined;
1057
1096
  count?: undefined;
1058
1097
  command?: undefined;
1059
1098
  runner?: undefined;
@@ -1126,6 +1165,9 @@ export declare const TOOL_DEFINITIONS: ({
1126
1165
  viaKind?: undefined;
1127
1166
  format?: undefined;
1128
1167
  ref?: undefined;
1168
+ query?: undefined;
1169
+ max_files?: undefined;
1170
+ graph?: undefined;
1129
1171
  count?: undefined;
1130
1172
  command?: undefined;
1131
1173
  runner?: undefined;
@@ -1201,6 +1243,9 @@ export declare const TOOL_DEFINITIONS: ({
1201
1243
  viaKind?: undefined;
1202
1244
  format?: undefined;
1203
1245
  ref?: undefined;
1246
+ query?: undefined;
1247
+ max_files?: undefined;
1248
+ graph?: undefined;
1204
1249
  count?: undefined;
1205
1250
  command?: undefined;
1206
1251
  runner?: undefined;
@@ -1283,6 +1328,9 @@ export declare const TOOL_DEFINITIONS: ({
1283
1328
  viaKind?: undefined;
1284
1329
  format?: undefined;
1285
1330
  ref?: undefined;
1331
+ query?: undefined;
1332
+ max_files?: undefined;
1333
+ graph?: undefined;
1286
1334
  count?: undefined;
1287
1335
  command?: undefined;
1288
1336
  runner?: undefined;
@@ -1356,6 +1404,9 @@ export declare const TOOL_DEFINITIONS: ({
1356
1404
  viaKind?: undefined;
1357
1405
  format?: undefined;
1358
1406
  ref?: undefined;
1407
+ query?: undefined;
1408
+ max_files?: undefined;
1409
+ graph?: undefined;
1359
1410
  count?: undefined;
1360
1411
  command?: undefined;
1361
1412
  runner?: undefined;
@@ -1445,6 +1496,9 @@ export declare const TOOL_DEFINITIONS: ({
1445
1496
  pattern?: undefined;
1446
1497
  name?: undefined;
1447
1498
  ref?: undefined;
1499
+ query?: undefined;
1500
+ max_files?: undefined;
1501
+ graph?: undefined;
1448
1502
  count?: undefined;
1449
1503
  command?: undefined;
1450
1504
  runner?: undefined;
@@ -1521,6 +1575,9 @@ export declare const TOOL_DEFINITIONS: ({
1521
1575
  maxDepth?: undefined;
1522
1576
  viaKind?: undefined;
1523
1577
  format?: undefined;
1578
+ query?: undefined;
1579
+ max_files?: undefined;
1580
+ graph?: undefined;
1524
1581
  count?: undefined;
1525
1582
  command?: undefined;
1526
1583
  runner?: undefined;
@@ -1597,6 +1654,87 @@ export declare const TOOL_DEFINITIONS: ({
1597
1654
  viaKind?: undefined;
1598
1655
  format?: undefined;
1599
1656
  ref?: undefined;
1657
+ query?: undefined;
1658
+ max_files?: undefined;
1659
+ graph?: undefined;
1660
+ count?: undefined;
1661
+ command?: undefined;
1662
+ runner?: undefined;
1663
+ timeout?: undefined;
1664
+ goal?: undefined;
1665
+ decisions?: undefined;
1666
+ confirmed?: undefined;
1667
+ files?: undefined;
1668
+ blocked?: undefined;
1669
+ next?: undefined;
1670
+ sessionId?: undefined;
1671
+ };
1672
+ required: string[];
1673
+ };
1674
+ } | {
1675
+ name: string;
1676
+ description: string;
1677
+ inputSchema: {
1678
+ type: "object";
1679
+ properties: {
1680
+ query: {
1681
+ type: string;
1682
+ description: string;
1683
+ };
1684
+ max_files: {
1685
+ type: string;
1686
+ description: string;
1687
+ };
1688
+ graph: {
1689
+ type: string;
1690
+ description: string;
1691
+ };
1692
+ path?: undefined;
1693
+ show_imports?: undefined;
1694
+ show_docs?: undefined;
1695
+ depth?: undefined;
1696
+ scope?: undefined;
1697
+ max_tokens?: undefined;
1698
+ session_id?: undefined;
1699
+ force?: undefined;
1700
+ symbol?: undefined;
1701
+ context_before?: undefined;
1702
+ context_after?: undefined;
1703
+ show?: undefined;
1704
+ include_edit_context?: undefined;
1705
+ symbols?: undefined;
1706
+ start_line?: undefined;
1707
+ end_line?: undefined;
1708
+ heading?: undefined;
1709
+ context_lines?: undefined;
1710
+ line?: undefined;
1711
+ context?: undefined;
1712
+ include_callers?: undefined;
1713
+ include_tests?: undefined;
1714
+ include_changes?: undefined;
1715
+ section?: undefined;
1716
+ paths?: undefined;
1717
+ kind?: undefined;
1718
+ limit?: undefined;
1719
+ lang?: undefined;
1720
+ mode?: undefined;
1721
+ include?: undefined;
1722
+ recursive?: undefined;
1723
+ max_depth?: undefined;
1724
+ verbose?: undefined;
1725
+ module?: undefined;
1726
+ export_only?: undefined;
1727
+ check?: undefined;
1728
+ pattern?: undefined;
1729
+ name?: undefined;
1730
+ from?: undefined;
1731
+ to?: undefined;
1732
+ all?: undefined;
1733
+ maxPaths?: undefined;
1734
+ maxDepth?: undefined;
1735
+ viaKind?: undefined;
1736
+ format?: undefined;
1737
+ ref?: undefined;
1600
1738
  count?: undefined;
1601
1739
  command?: undefined;
1602
1740
  runner?: undefined;
@@ -1673,6 +1811,9 @@ export declare const TOOL_DEFINITIONS: ({
1673
1811
  maxDepth?: undefined;
1674
1812
  viaKind?: undefined;
1675
1813
  format?: undefined;
1814
+ query?: undefined;
1815
+ max_files?: undefined;
1816
+ graph?: undefined;
1676
1817
  command?: undefined;
1677
1818
  runner?: undefined;
1678
1819
  timeout?: undefined;
@@ -1751,6 +1892,9 @@ export declare const TOOL_DEFINITIONS: ({
1751
1892
  viaKind?: undefined;
1752
1893
  format?: undefined;
1753
1894
  ref?: undefined;
1895
+ query?: undefined;
1896
+ max_files?: undefined;
1897
+ graph?: undefined;
1754
1898
  count?: undefined;
1755
1899
  goal?: undefined;
1756
1900
  decisions?: undefined;
@@ -1847,6 +1991,9 @@ export declare const TOOL_DEFINITIONS: ({
1847
1991
  viaKind?: undefined;
1848
1992
  format?: undefined;
1849
1993
  ref?: undefined;
1994
+ query?: undefined;
1995
+ max_files?: undefined;
1996
+ graph?: undefined;
1850
1997
  count?: undefined;
1851
1998
  command?: undefined;
1852
1999
  runner?: undefined;
@@ -1911,6 +2058,9 @@ export declare const TOOL_DEFINITIONS: ({
1911
2058
  viaKind?: undefined;
1912
2059
  format?: undefined;
1913
2060
  ref?: undefined;
2061
+ query?: undefined;
2062
+ max_files?: undefined;
2063
+ graph?: undefined;
1914
2064
  count?: undefined;
1915
2065
  command?: undefined;
1916
2066
  runner?: undefined;
@@ -670,6 +670,28 @@ export const TOOL_DEFINITIONS = [
670
670
  required: ["path"],
671
671
  },
672
672
  },
673
+ {
674
+ name: "explore",
675
+ description: "One-shot ranked context + call/inheritance graph blast-radius for a query. Returns ranked symbols, the source heads of the top-ranked files, graph neighbours (callers + subclasses — the blast radius), and related test files in a single compact block. Use INSTEAD OF separate find_usages + read_symbol + call_tree when you need to understand an area fast — cheaper than chaining those three.",
676
+ inputSchema: {
677
+ type: "object",
678
+ properties: {
679
+ query: {
680
+ type: "string",
681
+ description: "Search terms (the binary splits the string into terms itself), e.g. \"AstIndexClient buildIndex\"",
682
+ },
683
+ max_files: {
684
+ type: "number",
685
+ description: "Cap on the number of source file heads returned (default: binary's own limit)",
686
+ },
687
+ graph: {
688
+ type: "boolean",
689
+ description: "Include call/inheritance graph neighbours (blast radius). Default: true. Set false to skip the graph walk.",
690
+ },
691
+ },
692
+ required: ["query"],
693
+ },
694
+ },
673
695
  {
674
696
  name: "smart_log",
675
697
  description: "Use INSTEAD OF raw git log. Structured commit history with category detection (feat/fix/refactor/docs), file stats, author breakdown. Filters by path and ref. HEADS UP: two verification runs measured this tool at ~39% token reduction (borderline — vs 95-99% for outline/smart_diff). Cumulative data being gathered — tool may be dropped or redesigned in v0.30.0 if numbers don't improve. Prefer scoping with `path` or `count` to tighten savings.",
@@ -60,6 +60,7 @@ export const NAV_TOOLS = new Set([
60
60
  "module_info",
61
61
  "related_files",
62
62
  "explore_area",
63
+ "explore",
63
64
  "smart_log",
64
65
  "smart_diff",
65
66
  "read_section", // v0.30.0: section reading is nav-class (read-only, no edit prep)
package/dist/server.js CHANGED
@@ -38,6 +38,7 @@ import { handleModuleInfo } from "./handlers/module-info.js";
38
38
  import { handleModuleRoute } from "./handlers/module-route.js";
39
39
  import { handleSmartDiff } from "./handlers/smart-diff.js";
40
40
  import { handleExploreArea } from "./handlers/explore-area.js";
41
+ import { handleExplore } from "./handlers/explore.js";
41
42
  import { handleSmartLog } from "./handlers/smart-log.js";
42
43
  import { handleTestSummary } from "./handlers/test-summary.js";
43
44
  import { handleSessionSnapshot } from "./handlers/session-snapshot.js";
@@ -52,7 +53,7 @@ import { getMcpInstructions, TOOL_DEFINITIONS, } from "./server/tool-definitions
52
53
  import { filterToolsByProfile, parseProfileEnv, } from "./server/tool-profiles.js";
53
54
  import { STRICT_SMART_READ_MAX_TOKENS, STRICT_EXPLORE_AREA_INCLUDE, } from "./server/enforcement-mode.js";
54
55
  import { createTokenEstimates } from "./server/token-estimates.js";
55
- import { validateSmartReadArgs, validateReadSymbolArgs, validateReadSymbolsArgs, validateReadRangeArgs, validateReadDiffArgs, validateFindUsagesArgs, validateSmartReadManyArgs, validateReadForEditArgs, validateRelatedFilesArgs, validateOutlineArgs, validateFindUnusedArgs, validateCallTreeArgs, validateCodeAuditArgs, validateProjectOverviewArgs, validateModuleInfoArgs, validateModuleRouteArgs, validateSmartDiffArgs, validateExploreAreaArgs, validateSmartLogArgs, validateTestSummaryArgs, validateReadSectionArgs, } from "./core/validation.js";
56
+ import { validateSmartReadArgs, validateReadSymbolArgs, validateReadSymbolsArgs, validateReadRangeArgs, validateReadDiffArgs, validateFindUsagesArgs, validateSmartReadManyArgs, validateReadForEditArgs, validateRelatedFilesArgs, validateOutlineArgs, validateFindUnusedArgs, validateCallTreeArgs, validateCodeAuditArgs, validateProjectOverviewArgs, validateModuleInfoArgs, validateModuleRouteArgs, validateSmartDiffArgs, validateExploreAreaArgs, validateExploreArgs, validateSmartLogArgs, validateTestSummaryArgs, validateReadSectionArgs, } from "./core/validation.js";
56
57
  export async function createServer(projectRoot, options) {
57
58
  const mode = options?.enforcementMode ?? "deny";
58
59
  // v0.43.0 — the real Claude Code session id. CC exports it to every
@@ -967,6 +968,40 @@ export async function createServer(projectRoot, options) {
967
968
  }
968
969
  return eaResult;
969
970
  }
971
+ case "explore": {
972
+ const exArgs = validateExploreArgs(args);
973
+ const cachedEx = sessionCache?.get("explore", exArgs);
974
+ if (cachedEx) {
975
+ recordWithTrace({
976
+ tool: "explore",
977
+ path: exArgs.query,
978
+ tokensReturned: cachedEx.tokenEstimate,
979
+ tokensWouldBe: cachedEx.tokensWouldBe ?? cachedEx.tokenEstimate,
980
+ timestamp: Date.now(),
981
+ sessionCacheHit: true,
982
+ savingsCategory: "cache",
983
+ args: exArgs,
984
+ });
985
+ return cachedEx.result;
986
+ }
987
+ const exResult = await handleExplore(exArgs, projectRoot, astIndex);
988
+ const exText = exResult.content[0]?.text ?? "";
989
+ const exTokens = estimateTokens(exText);
990
+ // explore replaces a find_usages + read_symbol + call_tree chain;
991
+ // approximate that baseline as ~3x the compacted output.
992
+ const exWouldBe = exTokens * 3;
993
+ sessionCache?.set("explore", exArgs, exResult, { dependsOnAst: true }, exTokens, exWouldBe || exTokens);
994
+ recordWithTrace({
995
+ tool: "explore",
996
+ path: exArgs.query,
997
+ tokensReturned: exTokens,
998
+ tokensWouldBe: exWouldBe || exTokens,
999
+ timestamp: Date.now(),
1000
+ savingsCategory: "compression",
1001
+ args: exArgs,
1002
+ });
1003
+ return exResult;
1004
+ }
970
1005
  case "smart_log": {
971
1006
  const slArgs = validateSmartLogArgs(args);
972
1007
  // v0.30.0 strict mode: bound count to 20 when caller didn't set it.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "token-pilot",
3
- "version": "0.46.0",
3
+ "version": "0.47.0",
4
4
  "description": "Save up to 80% tokens when AI reads code — MCP server for token-efficient code navigation, AST-aware structural reading instead of dumping full files into context window",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -67,15 +67,15 @@
67
67
  "mcpName": "io.github.Digital-Threads/token-pilot",
68
68
  "license": "MIT",
69
69
  "dependencies": {
70
+ "@ast-index/cli": "^3.48.1",
70
71
  "@modelcontextprotocol/sdk": "^1.12.0",
71
- "@ast-index/cli": "^3.44.0",
72
72
  "chokidar": "^4.0.3"
73
73
  },
74
74
  "devDependencies": {
75
- "@vitest/coverage-v8": "^3.2.4",
76
75
  "@types/node": "^22.0.0",
76
+ "@vitest/coverage-v8": "^4.1.8",
77
77
  "typescript": "^5.7.0",
78
- "vitest": "^3.0.0"
78
+ "vitest": "^4.1.8"
79
79
  },
80
80
  "engines": {
81
81
  "node": ">=18.0.0"