token-pilot 0.28.3 → 0.30.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 (52) hide show
  1. package/.claude-plugin/marketplace.json +2 -2
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/CHANGELOG.md +75 -0
  4. package/README.md +39 -390
  5. package/agents/tp-api-surface-tracker.md +4 -2
  6. package/agents/tp-audit-scanner.md +4 -2
  7. package/agents/tp-commit-writer.md +4 -2
  8. package/agents/tp-context-engineer.md +4 -2
  9. package/agents/tp-dead-code-finder.md +4 -2
  10. package/agents/tp-debugger.md +4 -2
  11. package/agents/tp-dep-health.md +4 -2
  12. package/agents/tp-doc-writer.md +4 -2
  13. package/agents/tp-history-explorer.md +4 -2
  14. package/agents/tp-impact-analyzer.md +4 -2
  15. package/agents/tp-incident-timeline.md +4 -2
  16. package/agents/tp-incremental-builder.md +4 -2
  17. package/agents/tp-migration-scout.md +4 -2
  18. package/agents/tp-onboard.md +4 -2
  19. package/agents/tp-performance-profiler.md +4 -2
  20. package/agents/tp-pr-reviewer.md +4 -2
  21. package/agents/tp-refactor-planner.md +4 -2
  22. package/agents/tp-review-impact.md +4 -2
  23. package/agents/tp-run.md +4 -2
  24. package/agents/tp-session-restorer.md +4 -2
  25. package/agents/tp-ship-coordinator.md +4 -2
  26. package/agents/tp-spec-writer.md +4 -2
  27. package/agents/tp-test-coverage-gapper.md +4 -2
  28. package/agents/tp-test-triage.md +4 -2
  29. package/agents/tp-test-writer.md +4 -2
  30. package/dist/cli/tool-audit.d.ts +5 -0
  31. package/dist/cli/tool-audit.js +9 -1
  32. package/dist/core/policy-engine.d.ts +1 -5
  33. package/dist/core/policy-engine.js +9 -24
  34. package/dist/hooks/pre-bash.d.ts +13 -1
  35. package/dist/hooks/pre-bash.js +56 -1
  36. package/dist/hooks/pre-grep.d.ts +2 -1
  37. package/dist/hooks/pre-grep.js +3 -1
  38. package/dist/index.js +4 -2
  39. package/dist/server/enforcement-mode.d.ts +47 -0
  40. package/dist/server/enforcement-mode.js +59 -0
  41. package/dist/server/tool-definitions.d.ts +20 -0
  42. package/dist/server/tool-definitions.js +113 -10
  43. package/dist/server/tool-profiles.d.ts +19 -1
  44. package/dist/server/tool-profiles.js +38 -4
  45. package/dist/server.d.ts +2 -0
  46. package/dist/server.js +68 -16
  47. package/docs/agents.md +82 -0
  48. package/docs/configuration.md +117 -0
  49. package/docs/hooks.md +99 -0
  50. package/docs/installation.md +143 -0
  51. package/docs/tools.md +61 -0
  52. package/package.json +2 -2
@@ -25,6 +25,7 @@ export const PROFILE_NAMES = [
25
25
  "full",
26
26
  "nav",
27
27
  "edit",
28
+ "minimal",
28
29
  ];
29
30
  /**
30
31
  * Meta-tools — diagnostic / self-observation tools that must be visible
@@ -37,6 +38,18 @@ export const META_TOOLS = new Set([
37
38
  "session_budget",
38
39
  "session_snapshot",
39
40
  ]);
41
+ /**
42
+ * Minimal profile — 5 core tools for emergency / context-constrained sessions.
43
+ * Token overhead: tools/list is tiny; instructions are ~80 tokens vs ~350 for full.
44
+ * Use TOKEN_PILOT_PROFILE=minimal when the agent's context budget is nearly full.
45
+ */
46
+ export const MINIMAL_TOOLS = new Set([
47
+ "smart_read",
48
+ "read_symbol",
49
+ "find_usages",
50
+ "smart_diff",
51
+ "smart_log",
52
+ ]);
40
53
  /** Minimum nav profile — exploration only, no editing support. */
41
54
  export const NAV_TOOLS = new Set([
42
55
  "smart_read",
@@ -49,6 +62,7 @@ export const NAV_TOOLS = new Set([
49
62
  "explore_area",
50
63
  "smart_log",
51
64
  "smart_diff",
65
+ "read_section", // v0.30.0: section reading is nav-class (read-only, no edit prep)
52
66
  ]);
53
67
  /** Edit profile adds batch reads + edit-preparation tools. */
54
68
  export const EDIT_EXTRAS = new Set([
@@ -73,6 +87,11 @@ export function filterToolsByProfile(tools, profile) {
73
87
  // session_snapshot are the instruments for verifying the profile is
74
88
  // doing its job. Hiding them would turn "did this save tokens?" into
75
89
  // a guess.
90
+ if (profile === "minimal") {
91
+ // Minimal: 5 core tools only. META excluded — keep the footprint tiny.
92
+ // The agent can still call session_analytics by name if it knows it.
93
+ return tools.filter((t) => MINIMAL_TOOLS.has(t.name));
94
+ }
76
95
  if (profile === "nav") {
77
96
  return tools.filter((t) => NAV_TOOLS.has(t.name) || META_TOOLS.has(t.name));
78
97
  }
@@ -85,14 +104,29 @@ export function filterToolsByProfile(tools, profile) {
85
104
  * Parse the TOKEN_PILOT_PROFILE env value. Unknown values get a warning
86
105
  * and fall back to full — we never silently apply a guess.
87
106
  */
107
+ /**
108
+ * Parse the TOKEN_PILOT_PROFILE env value.
109
+ *
110
+ * Default changed in v0.30.0: full → edit.
111
+ * Rationale: 'full' was exposing 22 tools + full instruction set on every
112
+ * session, burning ~3 k tokens before any work. 'edit' covers 99% of
113
+ * development workflows (reading + writing code). Switch to 'full' only
114
+ * when you need audit tools (code_audit, find_unused, test_summary).
115
+ *
116
+ * Unknown values fall back to 'edit' with a stderr warning — we never
117
+ * silently apply a guess.
118
+ */
88
119
  export function parseProfileEnv(envValue, warn = () => { }) {
89
120
  if (!envValue)
90
- return "full";
121
+ return "edit";
91
122
  const lower = envValue.trim().toLowerCase();
92
- if (lower === "full" || lower === "nav" || lower === "edit") {
123
+ if (lower === "full" ||
124
+ lower === "nav" ||
125
+ lower === "edit" ||
126
+ lower === "minimal") {
93
127
  return lower;
94
128
  }
95
- warn(`[token-pilot] Unknown TOKEN_PILOT_PROFILE="${envValue}". Expected full|nav|edit. Falling back to full.`);
96
- return "full";
129
+ warn(`[token-pilot] Unknown TOKEN_PILOT_PROFILE="${envValue}". Expected full|nav|edit|minimal. Falling back to edit.`);
130
+ return "edit";
97
131
  }
98
132
  //# sourceMappingURL=tool-profiles.js.map
package/dist/server.d.ts CHANGED
@@ -1,6 +1,8 @@
1
1
  import { Server } from "@modelcontextprotocol/sdk/server/index.js";
2
+ import { type EnforcementMode } from "./server/enforcement-mode.js";
2
3
  export declare function createServer(projectRoot: string, options?: {
3
4
  skipAstIndex?: boolean;
5
+ enforcementMode?: EnforcementMode;
4
6
  }): Promise<Server<{
5
7
  method: string;
6
8
  params?: {
package/dist/server.js CHANGED
@@ -45,11 +45,13 @@ import { detectContextMode } from "./integration/context-mode-detector.js";
45
45
  import { estimateTokens } from "./core/token-estimator.js";
46
46
  import { checkPolicy, isFullReadTool } from "./core/policy-engine.js";
47
47
  import { appendToolCall } from "./core/tool-call-log.js";
48
- import { MCP_INSTRUCTIONS, TOOL_DEFINITIONS, } from "./server/tool-definitions.js";
48
+ import { getMcpInstructions, TOOL_DEFINITIONS, } from "./server/tool-definitions.js";
49
49
  import { filterToolsByProfile, parseProfileEnv, } from "./server/tool-profiles.js";
50
+ import { STRICT_SMART_READ_MAX_TOKENS, STRICT_EXPLORE_AREA_INCLUDE, } from "./server/enforcement-mode.js";
50
51
  import { createTokenEstimates } from "./server/token-estimates.js";
51
52
  import { validateSmartReadArgs, validateReadSymbolArgs, validateReadSymbolsArgs, validateReadRangeArgs, validateReadDiffArgs, validateFindUsagesArgs, validateSmartReadManyArgs, validateReadForEditArgs, validateRelatedFilesArgs, validateOutlineArgs, validateFindUnusedArgs, validateCodeAuditArgs, validateProjectOverviewArgs, validateModuleInfoArgs, validateSmartDiffArgs, validateExploreAreaArgs, validateSmartLogArgs, validateTestSummaryArgs, validateReadSectionArgs, } from "./core/validation.js";
52
53
  export async function createServer(projectRoot, options) {
54
+ const mode = options?.enforcementMode ?? "deny";
53
55
  const config = await loadConfig(projectRoot);
54
56
  const astIndex = new AstIndexClient(projectRoot, config.astIndex.timeout, {
55
57
  binaryPath: config.astIndex.binaryPath,
@@ -193,7 +195,6 @@ export async function createServer(projectRoot, options) {
193
195
  let fullFileReadsCount = 0;
194
196
  let totalCallCount = 0;
195
197
  let totalTokensReturned = 0;
196
- const readForEditCalled = new Set();
197
198
  // Detect context-mode companion
198
199
  const cmEnabled = config.contextMode.enabled;
199
200
  const contextModeStatus = await detectContextMode(projectRoot, cmEnabled === "auto" ? undefined : cmEnabled);
@@ -238,18 +239,20 @@ export async function createServer(projectRoot, options) {
238
239
  catch {
239
240
  /* fallback to hardcoded */
240
241
  }
242
+ // v0.26.3 — tool profiles. TOKEN_PILOT_PROFILE=nav|edit|full|minimal
243
+ // (default: edit since v0.30.0) trims the advertised tools/list payload.
244
+ // Handlers stay live, so a subagent that explicitly names a filtered-out
245
+ // tool still gets a response — we just don't brag about every tool upfront.
246
+ // v0.30.0 — profile also selects matching MCP instructions so the agent
247
+ // doesn't see rules for tools that aren't in its tools/list.
248
+ const activeProfile = parseProfileEnv(process.env.TOKEN_PILOT_PROFILE, (m) => process.stderr.write(m + "\n"));
241
249
  const server = new Server({ name: "token-pilot", version: pkgVersion }, {
242
250
  capabilities: { tools: {} },
243
- instructions: MCP_INSTRUCTIONS,
251
+ instructions: getMcpInstructions(activeProfile),
244
252
  });
245
- // v0.26.3 — tool profiles. TOKEN_PILOT_PROFILE=nav|edit|full (default
246
- // full) trims the advertised tools/list payload. Handlers stay live,
247
- // so a subagent that explicitly names a filtered-out tool still gets
248
- // a response — we just don't brag about every tool upfront.
249
- const activeProfile = parseProfileEnv(process.env.TOKEN_PILOT_PROFILE, (m) => process.stderr.write(m + "\n"));
250
253
  const advertisedTools = filterToolsByProfile(TOOL_DEFINITIONS, activeProfile);
251
- if (activeProfile !== "full") {
252
- process.stderr.write(`[token-pilot] Profile: ${activeProfile} — advertising ${advertisedTools.length}/${TOOL_DEFINITIONS.length} tools. Unset TOKEN_PILOT_PROFILE for the full set.\n`);
254
+ if (activeProfile !== "edit") {
255
+ process.stderr.write(`[token-pilot] Profile: ${activeProfile} — advertising ${advertisedTools.length}/${TOOL_DEFINITIONS.length} tools. Set TOKEN_PILOT_PROFILE=edit for the default set.\n`);
253
256
  }
254
257
  server.setRequestHandler(ListToolsRequestSchema, () => ({
255
258
  tools: advertisedTools,
@@ -289,14 +292,10 @@ export async function createServer(projectRoot, options) {
289
292
  if (isFullReadTool(rest.tool)) {
290
293
  fullFileReadsCount++;
291
294
  }
292
- if (rest.tool === "read_for_edit" && call.path) {
293
- readForEditCalled.add(call.path);
294
- }
295
295
  // Policy check
296
296
  const advisory = checkPolicy(config.policies, rest.tool, {
297
297
  fullFileReadsCount,
298
298
  tokensReturned: rest.tokensReturned,
299
- readForEditCalled,
300
299
  totalCallCount,
301
300
  totalTokensReturned,
302
301
  });
@@ -331,6 +330,14 @@ export async function createServer(projectRoot, options) {
331
330
  switch (name) {
332
331
  case "smart_read": {
333
332
  const validArgs = validateSmartReadArgs(args);
333
+ // v0.30.0 strict mode: cap max_tokens when caller didn't set it.
334
+ let strictReadCapNote;
335
+ if (mode === "strict" && validArgs.max_tokens === undefined) {
336
+ validArgs.max_tokens = STRICT_SMART_READ_MAX_TOKENS;
337
+ strictReadCapNote =
338
+ `\n\n[token-pilot strict] Output capped at ${STRICT_SMART_READ_MAX_TOKENS} tokens ` +
339
+ `(TOKEN_PILOT_MODE=strict). Pass max_tokens explicitly to override.`;
340
+ }
334
341
  const picked = pickRegistry(args);
335
342
  // Try non-code handler for JSON/YAML/MD etc.
336
343
  if (isNonCodeStructured(validArgs.path)) {
@@ -373,8 +380,9 @@ export async function createServer(projectRoot, options) {
373
380
  absPath: resolve(projectRoot, validArgs.path),
374
381
  args: validArgs,
375
382
  });
376
- if (policyAdv)
377
- result.content[0] = { type: "text", text: text + policyAdv };
383
+ const srSuffix = (policyAdv ?? "") + (strictReadCapNote ?? "");
384
+ if (srSuffix)
385
+ result.content[0] = { type: "text", text: text + srSuffix };
378
386
  return result;
379
387
  }
380
388
  case "read_symbol": {
@@ -527,6 +535,15 @@ export async function createServer(projectRoot, options) {
527
535
  }
528
536
  case "find_usages": {
529
537
  const usagesArgs = validateFindUsagesArgs(args);
538
+ // v0.30.0 strict mode: default mode to "list" when caller didn't set it.
539
+ // Injected before cache lookup so the key matches strict-mode cached results.
540
+ let strictFuNote;
541
+ if (mode === "strict" && usagesArgs.mode === undefined) {
542
+ usagesArgs.mode = "list";
543
+ strictFuNote =
544
+ `\n\n[token-pilot strict] find_usages mode defaulted to "list" ` +
545
+ `(TOKEN_PILOT_MODE=strict). Pass mode explicitly to override.`;
546
+ }
530
547
  const cachedUsages = sessionCache?.get("find_usages", usagesArgs);
531
548
  if (cachedUsages) {
532
549
  recordWithTrace({
@@ -558,6 +575,12 @@ export async function createServer(projectRoot, options) {
558
575
  savingsCategory: "compression",
559
576
  args: usagesArgs,
560
577
  });
578
+ if (strictFuNote && usagesResult.content[0]) {
579
+ usagesResult.content[0] = {
580
+ type: "text",
581
+ text: usagesText + strictFuNote,
582
+ };
583
+ }
561
584
  return usagesResult;
562
585
  }
563
586
  case "project_overview": {
@@ -801,6 +824,15 @@ export async function createServer(projectRoot, options) {
801
824
  }
802
825
  case "explore_area": {
803
826
  const eaArgs = validateExploreAreaArgs(args);
827
+ // v0.30.0 strict mode: default include to outline-only when caller didn't set it.
828
+ // Injected before cache lookup so the key matches strict-mode cached results.
829
+ let strictEaCapNote;
830
+ if (mode === "strict" && eaArgs.include === undefined) {
831
+ eaArgs.include = STRICT_EXPLORE_AREA_INCLUDE;
832
+ strictEaCapNote =
833
+ `\n\n[token-pilot strict] include defaulted to ["outline"] ` +
834
+ `(TOKEN_PILOT_MODE=strict). Pass include explicitly to override.`;
835
+ }
804
836
  const cachedEa = sessionCache?.get("explore_area", eaArgs);
805
837
  if (cachedEa) {
806
838
  recordWithTrace({
@@ -833,10 +865,24 @@ export async function createServer(projectRoot, options) {
833
865
  savingsCategory: "compression",
834
866
  args: eaArgs,
835
867
  });
868
+ if (strictEaCapNote && eaResult.content[0]) {
869
+ eaResult.content[0] = {
870
+ type: "text",
871
+ text: eaText + strictEaCapNote,
872
+ };
873
+ }
836
874
  return eaResult;
837
875
  }
838
876
  case "smart_log": {
839
877
  const slArgs = validateSmartLogArgs(args);
878
+ // v0.30.0 strict mode: bound count to 20 when caller didn't set it.
879
+ let strictSlNote;
880
+ if (mode === "strict" && slArgs.count === undefined) {
881
+ slArgs.count = 20;
882
+ strictSlNote =
883
+ `\n\n[token-pilot strict] smart_log count defaulted to 20 ` +
884
+ `(TOKEN_PILOT_MODE=strict). Pass count explicitly to override.`;
885
+ }
840
886
  const slResult = await handleSmartLog(slArgs, projectRoot);
841
887
  const slText = slResult.content[0]?.text ?? "";
842
888
  const slTokens = estimateTokens(slText);
@@ -849,6 +895,12 @@ export async function createServer(projectRoot, options) {
849
895
  savingsCategory: "compression",
850
896
  args: slArgs,
851
897
  });
898
+ if (strictSlNote && slResult.content[0]) {
899
+ slResult.content[0] = {
900
+ type: "text",
901
+ text: slText + strictSlNote,
902
+ };
903
+ }
852
904
  return { content: slResult.content };
853
905
  }
854
906
  case "test_summary": {
package/docs/agents.md ADDED
@@ -0,0 +1,82 @@
1
+ # tp-* Subagents (Claude Code only)
2
+
3
+ `tp-*` subagents are a Claude Code feature. Other clients get the MCP tools + hooks but cannot invoke subagents. Each agent carries an explicit `model:` field in its frontmatter; the budget is enforced post-response — overshoots beyond 10% land in `.token-pilot/over-budget.log`.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npx token-pilot install-agents --scope=user # all projects
9
+ npx token-pilot install-agents --scope=project # this repo only
10
+ npx token-pilot install-agents --scope=user --force # re-apply after an update
11
+ npx token-pilot uninstall-agents --scope=user|project
12
+ ```
13
+
14
+ `init` offers to install these; to add them to another project run `npx token-pilot install-agents`.
15
+
16
+ ## Tier 1 — Workhorses (invoke proactively)
17
+
18
+ | Agent | When to invoke | Budget |
19
+ |-------|---------------|-------:|
20
+ | `tp-run` | General MCP-first workhorse; use when no specialised agent fits | 800 |
21
+ | `tp-onboard` | Orient to an unfamiliar repo (layout, entry points, modules) | 600 |
22
+ | `tp-pr-reviewer` | Review a diff / PR / changeset; verdict-first, Critical/Important tiers | 600 |
23
+ | `tp-impact-analyzer` | Trace blast-radius of a change (callers, transitive deps) | 400 |
24
+ | `tp-refactor-planner` | Plan a refactor with exact edit context per step | 500 |
25
+ | `tp-test-triage` | Investigate test failures → root cause → minimal fix | 500 |
26
+
27
+ ## Tier 2 — Specialists
28
+
29
+ | Agent | When to invoke | Budget |
30
+ |-------|---------------|-------:|
31
+ | `tp-debugger` | Stack trace / error → root-cause line via call-tree traversal | 700 |
32
+ | `tp-migration-scout` | Pre-migration impact map grouped by effort class | 800 |
33
+ | `tp-test-writer` | Write tests for ONE symbol, mirrors project style, runs tests | 900 |
34
+ | `tp-dead-code-finder` | Cross-checked dead-code detection, output-only (never deletes) | 600 |
35
+ | `tp-commit-writer` | Draft Conventional-Commit from staged diff; refuses failing tests | 400 |
36
+ | `tp-history-explorer` | "Why is this like this?" — minimum commit chain explaining current state | 600 |
37
+ | `tp-audit-scanner` | Read-only security / quality audit; Critical / Important / Minor findings | 800 |
38
+ | `tp-session-restorer` | Rehydrate state after /clear or compaction from latest snapshot | 400 |
39
+
40
+ ## Tier 3 — Combo / Workflow
41
+
42
+ | Agent | When to invoke | Budget |
43
+ |-------|---------------|-------:|
44
+ | `tp-review-impact` | Pre-merge blast-radius review (diff × dependents × API surface) | 700 |
45
+ | `tp-test-coverage-gapper` | Find symbols with zero test references, prioritised | 500 |
46
+ | `tp-api-surface-tracker` | Public API diff vs last release → MAJOR / MINOR / PATCH verdict | 600 |
47
+ | `tp-dep-health` | Dep audit: stale × heavily-used × removable | 600 |
48
+ | `tp-incident-timeline` | Correlate an incident window with commits, rank likely culprits | 700 |
49
+
50
+ ## Tier 4 — Methodology
51
+
52
+ | Agent | When to invoke | Budget |
53
+ |-------|---------------|-------:|
54
+ | `tp-context-engineer` | Audit / write CLAUDE.md / AGENTS.md rules files per project | 800 |
55
+ | `tp-spec-writer` | Pre-code spec with gated workflow; surfaces assumptions before code | 900 |
56
+ | `tp-performance-profiler` | Measure → identify → fix → verify → guard; refuses to optimise without data | 800 |
57
+ | `tp-incremental-builder` | Multi-file feature work in thin vertical slices, test between each | 900 |
58
+ | `tp-doc-writer` | ADRs + READMEs + API docs; documents *why* not *what* | 700 |
59
+ | `tp-ship-coordinator` | 5-pillar pre-launch checklist (quality / security / observability / rollback / rollout) | 800 |
60
+
61
+ ## Model Tiers
62
+
63
+ Every agent carries an explicit `model:` field:
64
+
65
+ | Model | Count | Used for |
66
+ |-------|------:|---------|
67
+ | `haiku` | 9 | Structured / format-bound output (commit messages, onboarding maps, ADRs, session briefings) |
68
+ | `sonnet` | 15 | Reasoning tasks (review, debug, test, plan, audit, spec, profile, ship) |
69
+ | `inherit` | 1 | Deep correlation needing the main thread's model (`tp-incident-timeline`) |
70
+
71
+ Under Opus 4.7's +35% tokenizer tax, keeping the majority of agent spawns on haiku/sonnet saves 5–10× model cost vs an all-Opus baseline.
72
+
73
+ ## Third-party Agent Integration (bless-agents)
74
+
75
+ For third-party agents (e.g. `acc-*` plugins) whose tool allowlist excludes token-pilot MCP:
76
+
77
+ ```bash
78
+ npx token-pilot bless-agents # add token-pilot MCP to project-level overrides
79
+ npx token-pilot unbless-agents <name>... | --all
80
+ ```
81
+
82
+ `doctor` warns when the original agent has changed since blessing.
@@ -0,0 +1,117 @@
1
+ # Configuration & Tool Profiles
2
+
3
+ ## .token-pilot.json
4
+
5
+ Drop `.token-pilot.json` in your project root. All fields are optional.
6
+
7
+ ```json
8
+ {
9
+ "hooks": { "mode": "deny-enhanced", "denyThreshold": 300 },
10
+ "sessionStart": { "enabled": true, "showStats": false, "maxReminderTokens": 250 },
11
+ "agents": { "scope": null, "reminder": true },
12
+ "smartRead": { "smallFileThreshold": 200 },
13
+ "cache": { "maxSizeMB": 100, "watchFiles": true },
14
+ "policies": { "maxFullFileReads": 10, "largeReadThreshold": 2000 },
15
+ "astIndex": { "binaryPath": null },
16
+ "updates": { "checkOnStartup": true, "autoUpdate": false },
17
+ "ignore": ["node_modules/**", "dist/**", ".git/**"]
18
+ }
19
+ ```
20
+
21
+ | Option | Default | What it does |
22
+ |--------|---------|--------------|
23
+ | `hooks.mode` | `"deny-enhanced"` | Read hook mode: `off` / `advisory` / `deny-enhanced` |
24
+ | `hooks.denyThreshold` | `300` | Line count above which the hook intervenes on unbounded `Read` |
25
+ | `sessionStart.enabled` | `true` | Re-inject MCP-rules reminder at every new session / `/clear` / `/compact` |
26
+ | `agents.scope` | `null` | Persisted scope of last `install-agents` run; reused silently |
27
+ | `agents.reminder` | `true` | Show the "agents not installed" startup nudge |
28
+ | `smartRead.smallFileThreshold` | `200` | Files with fewer lines bypass AST overhead and are returned in full |
29
+ | `cache.maxSizeMB` | `100` | File cache ceiling (LRU eviction) |
30
+ | `policies.maxFullFileReads` | `10` | Warn after N full-file reads in session |
31
+ | `policies.largeReadThreshold` | `2000` | Token threshold above which a read is flagged as "large" in analytics |
32
+
33
+ ## Tool Profiles
34
+
35
+ Trim the advertised `tools/list` to save ~2 k tokens per session. Set via `TOKEN_PILOT_PROFILE` in your MCP server env block.
36
+
37
+ | Profile | Tools | ~Tokens | Use when |
38
+ |---------|------:|--------:|----------|
39
+ | `full` *(default)* | 22 | ~4 150 | All capabilities |
40
+ | `edit` | 16 | ~3 120 | Code-change workflows (nav + batch reads + `read_for_edit`) |
41
+ | `nav` | 10 | ~1 910 | Read-only exploration / subagents that only navigate |
42
+
43
+ Handlers remain active regardless of profile — a subagent that explicitly names a filtered-out tool still gets served. The profile only controls what appears in `tools/list` at session start.
44
+
45
+ ### Setting a profile
46
+
47
+ **In `.mcp.json`:**
48
+ ```json
49
+ {
50
+ "mcpServers": {
51
+ "token-pilot": {
52
+ "command": "npx",
53
+ "args": ["-y", "token-pilot"],
54
+ "env": { "TOKEN_PILOT_PROFILE": "nav" }
55
+ }
56
+ }
57
+ }
58
+ ```
59
+
60
+ **Via shell:**
61
+ ```bash
62
+ TOKEN_PILOT_PROFILE=edit npx token-pilot
63
+ ```
64
+
65
+ ## CLI Reference
66
+
67
+ ```bash
68
+ token-pilot # start MCP server
69
+ token-pilot init # create/merge .mcp.json; prompt about subagents
70
+ token-pilot install-agents [--scope=user|project] [--force]
71
+ token-pilot uninstall-agents --scope=user|project
72
+ token-pilot bless-agents # extend third-party agents with token-pilot MCP
73
+ token-pilot unbless-agents <name>... | --all
74
+ token-pilot install-hook # install PreToolUse hooks
75
+ token-pilot uninstall-hook
76
+ token-pilot stats # totals + top files from hook-events.jsonl
77
+ token-pilot stats --session[=<id>] | --by-agent
78
+ token-pilot tool-audit # per-tool savings distribution
79
+ token-pilot tool-audit --json
80
+ token-pilot doctor # diagnostics (ast-index, config, upstream drift)
81
+ token-pilot doctor --check=env # env var check only
82
+ token-pilot install-ast-index # download ast-index binary (auto on first run)
83
+ ```
84
+
85
+ ## Architecture
86
+
87
+ ```
88
+ src/
89
+ index.ts — CLI entry + MCP server bootstrap
90
+ server.ts — MCP server: 22 tool definitions + enforcement mode
91
+ server/
92
+ enforcement-mode.ts — TOKEN_PILOT_MODE parsing (advisory / deny / strict)
93
+ ast-index/ — ast-index binary client + auto-install
94
+ core/
95
+ event-log.ts — hook-events.jsonl + rotation + retention
96
+ session-analytics.ts, policy-engine.ts, intent-classifier.ts
97
+ hooks/
98
+ installer.ts — hook install/uninstall for Claude Code
99
+ pre-bash.ts — PreToolUse:Bash advisor (Bash/sh/eval/loop patterns)
100
+ pre-grep.ts — PreToolUse:Grep advisor (symbol-like pattern detection)
101
+ session-start.ts — SessionStart reminder handler
102
+ summary-pipeline.ts — ast-index → regex → head+tail → pass-through
103
+ cli/
104
+ install-agents.ts, uninstall-agents.ts
105
+ bless-agents.ts, unbless-agents.ts, doctor-drift.ts
106
+ stats.ts, tool-audit.ts
107
+ templates/agent-builder.ts
108
+ config/loader.ts, defaults.ts
109
+ handlers/ — 22 MCP tool handlers
110
+ git/ — HEAD + file watchers (cache invalidation)
111
+
112
+ scripts/
113
+ build-agents.mjs — render templates/ → dist/agents/
114
+ bench-hook.mjs — hook latency benchmark
115
+
116
+ templates/agents/ — source for tp-* family + shared preamble + contract
117
+ ```
package/docs/hooks.md ADDED
@@ -0,0 +1,99 @@
1
+ # Hooks & Enforcement Modes
2
+
3
+ Token Pilot installs two categories of PreToolUse hooks in Claude Code:
4
+
5
+ 1. **Read hook** — intercepts large `Read` calls (configurable threshold, default 300 lines) and returns a structural summary in the denial reason.
6
+ 2. **Grep / Bash hooks** — block heavy recursive patterns (`grep -r`, `find /`, `cat <file.ts>`, unbounded `git log`, bare `git diff`) and redirect to token-pilot MCP equivalents.
7
+
8
+ ## TOKEN_PILOT_MODE — Enforcement Mode
9
+
10
+ Controls how aggressively both hook categories behave:
11
+
12
+ | Value | Grep/Bash hooks | MCP output |
13
+ |-------|----------------|------------|
14
+ | `advisory` | Pass all through (no blocking) | No caps |
15
+ | `deny` *(default)* | Block heavy patterns, allow bounded variants | No caps |
16
+ | `strict` | Same as deny, plus auto-cap MCP output (see below) | Capped |
17
+
18
+ ```bash
19
+ # Set in your MCP server env block or shell profile:
20
+ TOKEN_PILOT_MODE=strict npx token-pilot
21
+ ```
22
+
23
+ ### Strict-mode MCP output caps
24
+
25
+ When `TOKEN_PILOT_MODE=strict` and the caller has not set the parameter explicitly:
26
+
27
+ | Tool | Auto-injected default | Note appended |
28
+ |------|-----------------------|---------------|
29
+ | `smart_read` | `max_tokens: 2000` | Yes |
30
+ | `explore_area` | `include: ["outline"]` | Yes |
31
+ | `find_usages` | `mode: "list"` | Yes |
32
+ | `smart_log` | `count: 20` | Yes |
33
+
34
+ Pass the parameter explicitly to override the cap.
35
+
36
+ ## Read Hook Modes
37
+
38
+ The PreToolUse:Read hook has its own mode (separate from enforcement mode). Set in `.token-pilot.json`:
39
+
40
+ | Mode | Behaviour |
41
+ |------|-----------|
42
+ | `off` | Hook is inert — all `Read` calls pass through |
43
+ | `advisory` | Denies unbounded Read with a short tip pointing at `smart_read` / `read_for_edit` |
44
+ | `deny-enhanced` *(default)* | Denies the Read and returns a full structural summary (imports, exports, declarations) **inside** the denial reason. Works for subagents that lack MCP access. |
45
+
46
+ ```json
47
+ { "hooks": { "mode": "deny-enhanced", "denyThreshold": 300 } }
48
+ ```
49
+
50
+ ## Grep / Bash Hook Rules
51
+
52
+ The Grep hook redirects symbol-like patterns to `find_usages`. The Bash hook blocks:
53
+
54
+ | Pattern | Blocked when | Allowed when |
55
+ |---------|-------------|--------------|
56
+ | `grep -r`/`-R` | Always (unbounded) | Has `-m N` bound |
57
+ | `find /`, `find ~` | No `-maxdepth` | Has `-maxdepth N` |
58
+ | `cat <file.ts>` | Code file, no pipeline | In pipeline (`cat … \| head`) or non-code file |
59
+ | `git log` | No count limit | Has `-n N`, `--max-count`, or `\| head` |
60
+ | `git diff` | Bare (no path/flag) | Has path arg or `--stat` |
61
+ | `bash -c "…"`, `eval "…"` | Inner command is heavy | Inner command is benign |
62
+
63
+ ## Installing / Removing Hooks
64
+
65
+ ```bash
66
+ npx token-pilot install-hook # register PreToolUse hooks in Claude Code
67
+ npx token-pilot uninstall-hook # remove hooks
68
+ ```
69
+
70
+ Hooks are auto-installed on first server start inside Claude Code. The Claude Code plugin path installs hooks automatically:
71
+
72
+ ```bash
73
+ claude plugin marketplace add https://github.com/Digital-Threads/token-pilot
74
+ claude plugin install token-pilot@token-pilot
75
+ ```
76
+
77
+ ## Environment Variables
78
+
79
+ | Var | Effect |
80
+ |-----|--------|
81
+ | `TOKEN_PILOT_MODE` | `advisory` / `deny` (default) / `strict` — enforcement level for Grep/Bash hooks and MCP output caps |
82
+ | `TOKEN_PILOT_BYPASS=1` | Pass every Read through (Read hook only) |
83
+ | `TOKEN_PILOT_DENY_THRESHOLD=<n>` | Override `hooks.denyThreshold` (default 300) |
84
+ | `TOKEN_PILOT_ADAPTIVE_THRESHOLD=true` | Enable adaptive curve as session burns |
85
+ | `TOKEN_PILOT_DEBUG=1` | Verbose hook logging to stderr |
86
+ | `TOKEN_PILOT_NO_AGENT_REMINDER=1` | Suppress the "tp-* not installed" stderr nudge |
87
+ | `TOKEN_PILOT_SUBAGENT=1` | Mark the MCP server as running inside a subagent |
88
+
89
+ ## Analytics & Audit
90
+
91
+ ```bash
92
+ token-pilot stats # totals + top files from hook-events.jsonl
93
+ token-pilot stats --session[=<id>] # filter by session
94
+ token-pilot stats --by-agent # grouped by agent
95
+ token-pilot tool-audit # per-tool savings distribution (cumulative)
96
+ token-pilot tool-audit --json # machine-readable output
97
+ ```
98
+
99
+ Hook events accumulate in `.token-pilot/hook-events.jsonl`. The `session_analytics` MCP tool provides per-tool breakdown within the current session.