token-pilot 0.46.1 → 0.47.1

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 (40) hide show
  1. package/.claude-plugin/marketplace.json +3 -3
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/CHANGELOG.md +48 -0
  4. package/agents/tp-api-surface-tracker.md +1 -1
  5. package/agents/tp-audit-scanner.md +1 -1
  6. package/agents/tp-commit-writer.md +1 -1
  7. package/agents/tp-context-engineer.md +1 -1
  8. package/agents/tp-dead-code-finder.md +1 -1
  9. package/agents/tp-debugger.md +1 -1
  10. package/agents/tp-dep-health.md +1 -1
  11. package/agents/tp-doc-writer.md +1 -1
  12. package/agents/tp-history-explorer.md +1 -1
  13. package/agents/tp-impact-analyzer.md +1 -1
  14. package/agents/tp-incident-timeline.md +1 -1
  15. package/agents/tp-incremental-builder.md +1 -1
  16. package/agents/tp-migration-scout.md +1 -1
  17. package/agents/tp-onboard.md +1 -1
  18. package/agents/tp-performance-profiler.md +1 -1
  19. package/agents/tp-pr-reviewer.md +1 -1
  20. package/agents/tp-refactor-planner.md +1 -1
  21. package/agents/tp-review-impact.md +1 -1
  22. package/agents/tp-run.md +1 -1
  23. package/agents/tp-session-restorer.md +1 -1
  24. package/agents/tp-ship-coordinator.md +1 -1
  25. package/agents/tp-spec-writer.md +1 -1
  26. package/agents/tp-test-coverage-gapper.md +1 -1
  27. package/agents/tp-test-triage.md +1 -1
  28. package/agents/tp-test-writer.md +1 -1
  29. package/dist/ast-index/client.d.ts +12 -1
  30. package/dist/ast-index/client.js +121 -18
  31. package/dist/ast-index/types.d.ts +69 -0
  32. package/dist/core/validation.d.ts +6 -0
  33. package/dist/core/validation.js +18 -0
  34. package/dist/handlers/explore.d.ts +17 -0
  35. package/dist/handlers/explore.js +90 -0
  36. package/dist/server/tool-definitions.d.ts +150 -0
  37. package/dist/server/tool-definitions.js +22 -0
  38. package/dist/server/tool-profiles.js +1 -0
  39. package/dist/server.js +36 -1
  40. package/package.json +2 -2
@@ -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.1"
9
+ "version": "0.47.1"
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.1",
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.1",
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.1",
3
+ "version": "0.47.1",
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,54 @@ 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.1] - 2026-06-24
9
+
10
+ ### Fixed — gate AST_INDEX_WALK_UP on the `.git` marker (nested-worktree escape)
11
+
12
+ `exec()` set `AST_INDEX_WALK_UP=1` unconditionally. The flag tells ast-index to
13
+ traverse past nested VCS markers and reuse a parent-level index — it exists for
14
+ bare monorepo subdirs (no `.git`, no local DB). But forcing it when `projectRoot`
15
+ is itself a git repo/worktree root let a worktree nested under the main repo
16
+ (`main-repo/.worktrees/feature`) walk up past its own `.git` and **escape to the
17
+ main repo's index, returning the wrong files**. Now the flag is set only when
18
+ `projectRoot` has no `.git` marker of its own (a `.git` dir or worktree gitlink
19
+ file both count). See `docs/adr/0002-ast-index-multi-root-scoping.md` — also
20
+ records the deferred `--local`/subtree plan for multi-repo parents.
21
+
22
+ ### Fixed — deterministic CLI tests under parallel sharding
23
+
24
+ `index.test.ts` / `installer.test.ts` now neutralise `CLAUDE_PROJECT_DIR` and
25
+ `CLAUDE_PLUGIN_ROOT` in setup so the git-detection and install assertions don't
26
+ flake when another suite leaks those env vars into a shared vitest worker.
27
+
28
+ ## [0.47.0] - 2026-06-24
29
+
30
+ ### Added — `explore` tool: one-shot ranked context + graph blast-radius
31
+
32
+ New MCP tool **`explore`** wraps ast-index 3.48's `explore` command: for a query
33
+ it returns ranked relevant symbols, the source heads of the top files, **graph
34
+ neighbours (callers + subclasses — the blast radius, via RWR over the
35
+ call/inheritance graph)**, and related test files — in a single compact block.
36
+ Replaces the common `find_usages` → `read_symbol` → `call_tree` chain with one
37
+ call. `graph` defaults on; `max_files` caps the source heads. Falls back to a
38
+ clear "requires ast-index >= 3.48" message when an older binary is resolved.
39
+
40
+ ### Changed — bump `@ast-index/cli` to 3.48.1
41
+
42
+ Picks up the upstream TypeScript-indexing fix, the rebuild **swap-and-restore**
43
+ guard (a failed rebuild no longer wipes the index), memory caps, and FUSE-safe
44
+ canonicalisation. `npm audit` stays at 0 vulnerabilities.
45
+
46
+ ### Changed — `buildIndex` trusts swap-and-restore
47
+
48
+ When a rebuild fails, `buildIndex` now checks for the index the binary preserved
49
+ and uses it (instead of throwing and falling back to raw reads). The lock-case
50
+ and generic-failure recovery paths are unified.
51
+
52
+ _Deferred:_ `--local` / subtree query scoping (to re-enable ast-index on
53
+ multi-repo / worktree parents instead of disabling it) needs a rooting-model
54
+ rework and ships separately.
55
+
8
56
  ## [0.46.1] - 2026-06-18
9
57
 
10
58
  ### Fixed — node:test (`node --test`) TAP output parsing
@@ -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.1"
12
+ token_pilot_version: "0.47.1"
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.1"
14
+ token_pilot_version: "0.47.1"
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.1"
11
+ token_pilot_version: "0.47.1"
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.1"
16
+ token_pilot_version: "0.47.1"
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.1"
14
+ token_pilot_version: "0.47.1"
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.1"
15
+ token_pilot_version: "0.47.1"
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.1"
12
+ token_pilot_version: "0.47.1"
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.1"
16
+ token_pilot_version: "0.47.1"
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.1"
13
+ token_pilot_version: "0.47.1"
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.1"
15
+ token_pilot_version: "0.47.1"
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.1"
11
+ token_pilot_version: "0.47.1"
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.1"
16
+ token_pilot_version: "0.47.1"
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.1"
14
+ token_pilot_version: "0.47.1"
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.1"
13
+ token_pilot_version: "0.47.1"
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.1"
14
+ token_pilot_version: "0.47.1"
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.1"
14
+ token_pilot_version: "0.47.1"
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.1"
11
+ token_pilot_version: "0.47.1"
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.1"
12
+ token_pilot_version: "0.47.1"
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.1"
19
+ token_pilot_version: "0.47.1"
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.1"
12
+ token_pilot_version: "0.47.1"
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.1"
14
+ token_pilot_version: "0.47.1"
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.1"
12
+ token_pilot_version: "0.47.1"
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.1"
13
+ token_pilot_version: "0.47.1"
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.1"
11
+ token_pilot_version: "0.47.1"
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.1"
16
+ token_pilot_version: "0.47.1"
17
17
  token_pilot_body_hash: 269f2fe22ff4517c277d3f56ca67d8a5527b93290ab21079a83ba7af22c1b5a9
18
18
  requiredMcpServers:
19
19
  - "token-pilot"
@@ -1,9 +1,16 @@
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
+ /**
4
+ * True when `projectRoot` is itself a git repo or worktree root. A `.git`
5
+ * entry counts whether it's a directory (normal repo) or a file (worktree /
6
+ * submodule gitlink). Used to gate AST_INDEX_WALK_UP — see exec().
7
+ */
8
+ export declare function computeHasGitMarker(projectRoot: string): boolean;
3
9
  export declare class AstIndexClient {
4
10
  private static readonly MAX_INDEX_FILES;
5
11
  private binaryPath;
6
12
  private projectRoot;
13
+ private hasGitMarker;
7
14
  private indexed;
8
15
  private indexOversized;
9
16
  private indexDisabled;
@@ -34,6 +41,10 @@ export declare class AstIndexClient {
34
41
  fuzzy?: boolean;
35
42
  }): Promise<AstIndexSearchResult[]>;
36
43
  usages(symbolName: string): Promise<AstIndexUsageResult[]>;
44
+ explore(query: string, options?: {
45
+ maxFiles?: number;
46
+ graph?: boolean;
47
+ }): Promise<AstIndexExploreResult>;
37
48
  implementations(name: string): Promise<AstIndexImplementation[]>;
38
49
  hierarchy(name: string, options?: {
39
50
  inFile?: string;
@@ -1,4 +1,6 @@
1
1
  import { execFile } from "node:child_process";
2
+ import { existsSync } from "node:fs";
3
+ import { resolve } from "node:path";
2
4
  import { promisify } from "node:util";
3
5
  import { findBinary, installBinary } from "./binary-manager.js";
4
6
  import { parseFileCount, parseOutlineText, parseImportsText, parseImplementationsText, parseHierarchyText, parseAgrepText, parseTodoText, parseDeprecatedText, parseAnnotationsText, parseModuleListText, parseModuleDepText, parseUnusedDepsText, parseModuleApiText, } from "./parser.js";
@@ -8,10 +10,21 @@ import { parsePythonRegex } from "./regex-parser-python.js";
8
10
  const TS_JS_EXTENSIONS = new Set(["ts", "tsx", "js", "jsx", "mjs", "cjs"]);
9
11
  const PYTHON_EXTENSIONS = new Set(["py", "pyw"]);
10
12
  const execFileAsync = promisify(execFile);
13
+ /**
14
+ * True when `projectRoot` is itself a git repo or worktree root. A `.git`
15
+ * entry counts whether it's a directory (normal repo) or a file (worktree /
16
+ * submodule gitlink). Used to gate AST_INDEX_WALK_UP — see exec().
17
+ */
18
+ export function computeHasGitMarker(projectRoot) {
19
+ return existsSync(resolve(projectRoot, ".git"));
20
+ }
11
21
  export class AstIndexClient {
12
22
  static MAX_INDEX_FILES = 50_000;
13
23
  binaryPath = null;
14
24
  projectRoot;
25
+ // True when projectRoot is itself a git repo/worktree root (has a `.git`
26
+ // marker). Gates AST_INDEX_WALK_UP in exec() — see the comment there.
27
+ hasGitMarker;
15
28
  indexed = false;
16
29
  indexOversized = false;
17
30
  indexDisabled = false;
@@ -26,6 +39,7 @@ export class AstIndexClient {
26
39
  periodicUpdateInFlight = false;
27
40
  constructor(projectRoot, timeout = 5000, options) {
28
41
  this.projectRoot = projectRoot;
42
+ this.hasGitMarker = computeHasGitMarker(projectRoot);
29
43
  this.timeout = timeout;
30
44
  this.configBinaryPath = options?.binaryPath ?? null;
31
45
  this.autoInstall = options?.autoInstall ?? true;
@@ -128,16 +142,22 @@ export class AstIndexClient {
128
142
  }
129
143
  catch (buildErr) {
130
144
  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
- }
145
+ // ast-index 3.46+ preserves the previous index when a rebuild aborts
146
+ // (swap-and-restore guard). Before giving up, query stats once: if a
147
+ // usable index survived, use it instead of losing all indexed tools and
148
+ // falling back to raw reads. Covers both the lock / already-running case
149
+ // and any generic rebuild failure the binary recovered from.
150
+ const lockCase = errMsg.includes("lock") || errMsg.includes("already running");
151
+ const count = parseFileCount(await this.exec(["--format", "json", "stats"]).catch(() => ""));
152
+ if (count > 0 && count <= AstIndexClient.MAX_INDEX_FILES) {
153
+ this.indexed = true;
154
+ console.error(lockCase
155
+ ? `[token-pilot] ast-index: using existing index (${count} files, rebuild skipped due to lock)`
156
+ : `[token-pilot] ast-index: rebuild failed but previous index preserved (${count} files) — using it`);
157
+ return;
158
+ }
159
+ if (count > AstIndexClient.MAX_INDEX_FILES) {
160
+ return this.handleOversizedIndex(count);
141
161
  }
142
162
  console.error(`[token-pilot] ast-index: rebuild failed — ${errMsg}`);
143
163
  throw buildErr;
@@ -328,6 +348,80 @@ export class AstIndexClient {
328
348
  return [];
329
349
  }
330
350
  }
351
+ async explore(query, options) {
352
+ await this.ensureIndex();
353
+ const empty = {
354
+ query,
355
+ dominantLanguage: "",
356
+ symbols: [],
357
+ files: [],
358
+ neighbours: [],
359
+ tests: [],
360
+ };
361
+ const args = ["explore", query, "--format", "json"];
362
+ if (options?.maxFiles)
363
+ args.push("-f", String(options.maxFiles));
364
+ // Default graph ON — call/inheritance blast-radius is the value-add;
365
+ // callers opt out with graph: false.
366
+ if (options?.graph !== false)
367
+ args.push("--rwr");
368
+ try {
369
+ const result = await this.exec(args);
370
+ const parsed = JSON.parse(result);
371
+ return {
372
+ query: typeof parsed.query === "string" ? parsed.query : query,
373
+ dominantLanguage: typeof parsed.dominant_language === "string"
374
+ ? parsed.dominant_language
375
+ : "",
376
+ symbols: Array.isArray(parsed.symbols)
377
+ ? parsed.symbols.map((s) => ({
378
+ name: s.name,
379
+ kind: s.kind,
380
+ path: s.path,
381
+ line: s.line,
382
+ score: s.score,
383
+ vendor: s.vendor === true,
384
+ }))
385
+ : [],
386
+ files: Array.isArray(parsed.files)
387
+ ? parsed.files.map((f) => ({
388
+ path: f.path,
389
+ line: f.line,
390
+ source: f.source,
391
+ }))
392
+ : [],
393
+ neighbours: Array.isArray(parsed.neighbours)
394
+ ? parsed.neighbours.map((n) => ({
395
+ name: n.name,
396
+ kind: n.kind,
397
+ path: n.path,
398
+ line: n.line,
399
+ link: n.link,
400
+ }))
401
+ : [],
402
+ tests: Array.isArray(parsed.tests)
403
+ ? parsed.tests.map((t) => ({
404
+ source: t.source,
405
+ tests: Array.isArray(t.tests) ? t.tests : [],
406
+ }))
407
+ : [],
408
+ };
409
+ }
410
+ catch (err) {
411
+ const msg = err instanceof Error ? err.message : String(err);
412
+ console.error(`[token-pilot] ast-index explore failed: ${msg}`);
413
+ // `explore` landed in ast-index 3.48. An older resolved binary reports
414
+ // "unrecognized subcommand 'explore'" — surface that instead of an
415
+ // indistinguishable empty result, so the caller knows to update.
416
+ if (/unrecognized subcommand|unexpected argument/.test(msg)) {
417
+ return {
418
+ ...empty,
419
+ error: "explore requires ast-index >= 3.48 — update the binary",
420
+ };
421
+ }
422
+ return empty;
423
+ }
424
+ }
331
425
  async implementations(name) {
332
426
  await this.ensureIndex();
333
427
  try {
@@ -766,6 +860,7 @@ export class AstIndexClient {
766
860
  }
767
861
  updateProjectRoot(newRoot) {
768
862
  this.projectRoot = newRoot;
863
+ this.hasGitMarker = computeHasGitMarker(newRoot);
769
864
  this.indexed = false;
770
865
  }
771
866
  async exec(args, timeoutMs) {
@@ -791,14 +886,22 @@ export class AstIndexClient {
791
886
  // ast-index v3.39+ honours AST_INDEX_WALK_UP=1 — read-commands then
792
887
  // traverse past nested VCS markers (submodule .git, inner Cargo.toml,
793
888
  // nested settings.gradle) to reuse a parent-level index if one exists.
794
- // Without this, running `search`/`outline` from a monorepo subdir stops
795
- // at the nearest marker and finds nothing when the subdir has no DB.
796
- // Safe default: pure-additive, no effect when projectRoot already sits
797
- // at the index root.
798
- const env = {
799
- ...process.env,
800
- AST_INDEX_WALK_UP: "1",
801
- };
889
+ // We only want this for a BARE SUBDIR that has no VCS marker of its own
890
+ // and no local DB without walk-up, `search`/`outline` from a monorepo
891
+ // subdir stops at the nearest marker and finds nothing.
892
+ //
893
+ // We must NOT force it when projectRoot is itself a git repo/worktree
894
+ // root: a git worktree nested under the main repo (e.g.
895
+ // `main-repo/.worktrees/feature`) would walk up past its own `.git`
896
+ // marker and escape to the MAIN repo's parent index, returning the
897
+ // wrong files. Repo/worktree roots already own their index, so walk-up
898
+ // is at best a no-op and at worst that escape bug — skip it. When the
899
+ // user set AST_INDEX_WALK_UP themselves and a marker is present, we
900
+ // leave their value untouched (we only refrain from forcing it).
901
+ const env = { ...process.env };
902
+ if (!this.hasGitMarker) {
903
+ env.AST_INDEX_WALK_UP = "1";
904
+ }
802
905
  if (this.astGrepBinDir) {
803
906
  env.PATH = `${this.astGrepBinDir}:${process.env.PATH ?? ""}`;
804
907
  }
@@ -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
@@ -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
@@ -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.1",
3
+ "version": "0.47.1",
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,7 +67,7 @@
67
67
  "mcpName": "io.github.Digital-Threads/token-pilot",
68
68
  "license": "MIT",
69
69
  "dependencies": {
70
- "@ast-index/cli": "^3.44.0",
70
+ "@ast-index/cli": "^3.48.1",
71
71
  "@modelcontextprotocol/sdk": "^1.12.0",
72
72
  "chokidar": "^4.0.3"
73
73
  },