token-pilot 0.24.1 → 0.26.5

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 (47) hide show
  1. package/.claude-plugin/marketplace.json +29 -12
  2. package/.claude-plugin/plugin.json +23 -5
  3. package/CHANGELOG.md +182 -0
  4. package/README.md +128 -15
  5. package/dist/agents/tp-api-surface-tracker.md +4 -3
  6. package/dist/agents/tp-audit-scanner.md +1 -1
  7. package/dist/agents/tp-commit-writer.md +1 -1
  8. package/dist/agents/tp-dead-code-finder.md +22 -7
  9. package/dist/agents/tp-debugger.md +1 -1
  10. package/dist/agents/tp-dep-health.md +3 -2
  11. package/dist/agents/tp-history-explorer.md +1 -1
  12. package/dist/agents/tp-impact-analyzer.md +1 -1
  13. package/dist/agents/tp-incident-timeline.md +1 -1
  14. package/dist/agents/tp-migration-scout.md +1 -1
  15. package/dist/agents/tp-onboard.md +1 -1
  16. package/dist/agents/tp-pr-reviewer.md +1 -1
  17. package/dist/agents/tp-refactor-planner.md +1 -1
  18. package/dist/agents/tp-review-impact.md +1 -1
  19. package/dist/agents/tp-run.md +1 -1
  20. package/dist/agents/tp-session-restorer.md +1 -1
  21. package/dist/agents/tp-test-coverage-gapper.md +1 -1
  22. package/dist/agents/tp-test-triage.md +1 -1
  23. package/dist/agents/tp-test-writer.md +1 -1
  24. package/dist/cli/detect-client.d.ts +39 -0
  25. package/dist/cli/detect-client.js +106 -0
  26. package/dist/cli/install-agents.d.ts +1 -0
  27. package/dist/cli/install-agents.js +31 -1
  28. package/dist/cli/tool-audit.d.ts +58 -0
  29. package/dist/cli/tool-audit.js +123 -0
  30. package/dist/cli/typo-guard.d.ts +1 -1
  31. package/dist/cli/typo-guard.js +1 -0
  32. package/dist/core/tool-call-log.d.ts +63 -0
  33. package/dist/core/tool-call-log.js +171 -0
  34. package/dist/handlers/read-symbols.js +23 -1
  35. package/dist/hooks/installer.js +27 -12
  36. package/dist/index.js +55 -0
  37. package/dist/server/profile-recommender.d.ts +48 -0
  38. package/dist/server/profile-recommender.js +102 -0
  39. package/dist/server/token-estimates.d.ts +17 -3
  40. package/dist/server/token-estimates.js +77 -45
  41. package/dist/server/tool-definitions.js +1 -1
  42. package/dist/server/tool-profiles.d.ts +46 -0
  43. package/dist/server/tool-profiles.js +81 -0
  44. package/dist/server.js +38 -1
  45. package/package.json +1 -1
  46. package/start.sh +0 -0
  47. package/.mcp.json +0 -8
@@ -2,9 +2,31 @@
2
2
  * Token estimation functions for analytics.
3
3
  * Used to calculate "tokens would be" for honest savings reporting.
4
4
  */
5
- import { estimateTokens } from '../core/token-estimator.js';
6
- import { resolveSafePath } from '../core/validation.js';
7
- import { CODE_EXTENSIONS } from '../handlers/outline.js';
5
+ import { estimateTokens } from "../core/token-estimator.js";
6
+ import { resolveSafePath } from "../core/validation.js";
7
+ import { CODE_EXTENSIONS } from "../handlers/outline.js";
8
+ /**
9
+ * Honest savings classification for a tool's text output.
10
+ *
11
+ * Standalone (pure) so it can be unit-tested without spinning up the
12
+ * full token-estimates closure. Kept here because it's semantically
13
+ * tied to how this module reports wouldBe/returned pairs.
14
+ *
15
+ * v0.26.1 adds the 'none' branch: smart_read's small-file pass-through
16
+ * returns the file verbatim with a tiny header. Claiming wouldBe =
17
+ * fullFile for those calls was the root cause of the -2% "negative
18
+ * savings" line Opus 4.7 reported. With 'none', the recorder sets
19
+ * wouldBe = returned → 0% savings, no ghost overhead.
20
+ */
21
+ export function detectSavingsCategoryPure(text) {
22
+ if (text.startsWith("REMINDER:") || text.startsWith("DEDUP:"))
23
+ return "dedup";
24
+ if (text.includes("returned in full, below threshold") ||
25
+ text.includes("returned in full, outline not smaller")) {
26
+ return "none";
27
+ }
28
+ return "compression";
29
+ }
8
30
  /**
9
31
  * Creates token estimation functions bound to a project context.
10
32
  * Uses getter for projectRoot since it may change on auto-detect.
@@ -16,8 +38,8 @@ export function createTokenEstimates(getProjectRoot, fileCache) {
16
38
  const cached = fileCache.get(absPath);
17
39
  if (cached)
18
40
  return estimateTokens(cached.content);
19
- const { readFile: readFileAsync } = await import('node:fs/promises');
20
- const content = await readFileAsync(absPath, 'utf-8');
41
+ const { readFile: readFileAsync } = await import("node:fs/promises");
42
+ const content = await readFileAsync(absPath, "utf-8");
21
43
  return estimateTokens(content);
22
44
  }
23
45
  catch {
@@ -26,34 +48,46 @@ export function createTokenEstimates(getProjectRoot, fileCache) {
26
48
  }
27
49
  async function estimateProjectOverviewWorkflowTokens(includeSections) {
28
50
  const sectionFiles = {
29
- stack: ['package.json', 'composer.json', 'Cargo.toml', 'pyproject.toml', 'go.mod'],
30
- ci: ['.gitlab-ci.yml', 'Jenkinsfile', '.circleci/config.yml', 'bitbucket-pipelines.yml', '.travis.yml'],
51
+ stack: [
52
+ "package.json",
53
+ "composer.json",
54
+ "Cargo.toml",
55
+ "pyproject.toml",
56
+ "go.mod",
57
+ ],
58
+ ci: [
59
+ ".gitlab-ci.yml",
60
+ "Jenkinsfile",
61
+ ".circleci/config.yml",
62
+ "bitbucket-pipelines.yml",
63
+ ".travis.yml",
64
+ ],
31
65
  quality: [
32
- 'tsconfig.json',
33
- 'vitest.config.ts',
34
- 'vitest.config.js',
35
- 'vitest.config.mts',
36
- 'jest.config.js',
37
- 'jest.config.ts',
38
- 'jest.config.mjs',
39
- 'eslint.config.js',
40
- 'eslint.config.mjs',
41
- '.eslintrc',
42
- '.eslintrc.js',
43
- '.eslintrc.json',
44
- '.eslintrc.yml',
45
- 'biome.json',
46
- 'biome.jsonc',
47
- '.prettierrc',
48
- '.prettierrc.js',
49
- '.prettierrc.json',
50
- 'prettier.config.js',
51
- 'phpunit.xml',
52
- 'phpunit.xml.dist',
53
- 'phpstan.neon',
54
- 'phpstan.neon.dist',
66
+ "tsconfig.json",
67
+ "vitest.config.ts",
68
+ "vitest.config.js",
69
+ "vitest.config.mts",
70
+ "jest.config.js",
71
+ "jest.config.ts",
72
+ "jest.config.mjs",
73
+ "eslint.config.js",
74
+ "eslint.config.mjs",
75
+ ".eslintrc",
76
+ ".eslintrc.js",
77
+ ".eslintrc.json",
78
+ ".eslintrc.yml",
79
+ "biome.json",
80
+ "biome.jsonc",
81
+ ".prettierrc",
82
+ ".prettierrc.js",
83
+ ".prettierrc.json",
84
+ "prettier.config.js",
85
+ "phpunit.xml",
86
+ "phpunit.xml.dist",
87
+ "phpstan.neon",
88
+ "phpstan.neon.dist",
55
89
  ],
56
- architecture: ['README.md'],
90
+ architecture: ["README.md"],
57
91
  };
58
92
  let total = 0;
59
93
  const seen = new Set();
@@ -65,15 +99,17 @@ export function createTokenEstimates(getProjectRoot, fileCache) {
65
99
  total += await fullFileTokens(file);
66
100
  }
67
101
  }
68
- if (includeSections.includes('ci')) {
102
+ if (includeSections.includes("ci")) {
69
103
  try {
70
- const { readdir: readDirAsync } = await import('node:fs/promises');
71
- const workflowDir = resolveSafePath(getProjectRoot(), '.github/workflows');
72
- const workflowFiles = await readDirAsync(workflowDir, { withFileTypes: true });
104
+ const { readdir: readDirAsync } = await import("node:fs/promises");
105
+ const workflowDir = resolveSafePath(getProjectRoot(), ".github/workflows");
106
+ const workflowFiles = await readDirAsync(workflowDir, {
107
+ withFileTypes: true,
108
+ });
73
109
  for (const file of workflowFiles) {
74
110
  if (!file.isFile())
75
111
  continue;
76
- if (!file.name.endsWith('.yml') && !file.name.endsWith('.yaml'))
112
+ if (!file.name.endsWith(".yml") && !file.name.endsWith(".yaml"))
77
113
  continue;
78
114
  total += await fullFileTokens(`.github/workflows/${file.name}`);
79
115
  }
@@ -82,7 +118,7 @@ export function createTokenEstimates(getProjectRoot, fileCache) {
82
118
  // ignore missing workflows dir
83
119
  }
84
120
  }
85
- if (includeSections.includes('architecture')) {
121
+ if (includeSections.includes("architecture")) {
86
122
  total += 200;
87
123
  }
88
124
  return total;
@@ -90,8 +126,8 @@ export function createTokenEstimates(getProjectRoot, fileCache) {
90
126
  async function estimateOutlineWorkflowTokens(relativePath, recursive, maxDepth) {
91
127
  const SAMPLE_LIMIT = 30;
92
128
  try {
93
- const { readdir: readDirAsync } = await import('node:fs/promises');
94
- const { resolve: resolvePath } = await import('node:path');
129
+ const { readdir: readDirAsync } = await import("node:fs/promises");
130
+ const { resolve: resolvePath } = await import("node:path");
95
131
  const absDir = resolveSafePath(getProjectRoot(), relativePath);
96
132
  const sampledFiles = [];
97
133
  let totalFiles = 0;
@@ -99,7 +135,7 @@ export function createTokenEstimates(getProjectRoot, fileCache) {
99
135
  const entries = await readDirAsync(dirPath, { withFileTypes: true });
100
136
  for (const entry of entries) {
101
137
  if (entry.isFile()) {
102
- const ext = entry.name.split('.').pop()?.toLowerCase() ?? '';
138
+ const ext = entry.name.split(".").pop()?.toLowerCase() ?? "";
103
139
  if (!CODE_EXTENSIONS.has(ext))
104
140
  continue;
105
141
  totalFiles++;
@@ -186,11 +222,7 @@ export function createTokenEstimates(getProjectRoot, fileCache) {
186
222
  total += (meta.changeCount ?? 0) * 40;
187
223
  return total;
188
224
  }
189
- function detectSavingsCategory(text) {
190
- if (text.startsWith('REMINDER:') || text.startsWith('DEDUP:'))
191
- return 'dedup';
192
- return 'compression';
193
- }
225
+ const detectSavingsCategory = detectSavingsCategoryPure;
194
226
  return {
195
227
  fullFileTokens,
196
228
  estimateProjectOverviewWorkflowTokens,
@@ -287,7 +287,7 @@ export const TOOL_DEFINITIONS = [
287
287
  // --- Search & navigation ---
288
288
  {
289
289
  name: "find_usages",
290
- description: "Use INSTEAD OF Grep for finding symbol references. Semantic search — groups by: definitions, imports, usages. Supports scope, kind, limit, lang filters. Use context_lines to include surrounding code.",
290
+ description: "Use INSTEAD OF Grep for finding symbol references. Semantic search — groups by: definitions, imports, usages. Supports scope, kind, limit, lang filters. Use context_lines to include surrounding code. HINT: for very short / generic symbols (≤4 chars like `id`, `err`, `Cmd`, `db`) Grep is usually cheaper than find_usages — the semantic grouping doesn't pay off when the symbol resolves ambiguously across thousands of files.",
291
291
  inputSchema: {
292
292
  type: "object",
293
293
  properties: {
@@ -0,0 +1,46 @@
1
+ /**
2
+ * v0.26.3 — tool profiles.
3
+ *
4
+ * Idea lifted honestly from Token Savior's TOKEN_SAVIOR_PROFILE. When an
5
+ * MCP server advertises 22 tools, every tools/list response costs the
6
+ * agent ~3 k tokens before it does anything. Most sessions don't need
7
+ * every tool — a code-review agent uses smart_read + find_usages +
8
+ * outline and nothing else. A profile lets the user ship a narrower
9
+ * tools/list while keeping the handlers live (so a subagent or another
10
+ * user in the same server can still reach the full set if they know
11
+ * the name).
12
+ *
13
+ * Three profiles:
14
+ * - full (default): everything, same as pre-v0.26.3.
15
+ * - nav : read-only exploration. smart_read, outline, find_usages,
16
+ * read_symbol, project_overview, module_info, related_files,
17
+ * explore_area, smart_log, smart_diff.
18
+ * - edit : nav + batch reads + everything Edit needs to hit a symbol
19
+ * precisely. Adds read_symbols, read_range, read_section,
20
+ * read_diff, read_for_edit, smart_read_many.
21
+ *
22
+ * Selection: TOKEN_PILOT_PROFILE=nav|edit|full env var. Unknown values
23
+ * fall back to full with a stderr warning. Silent on missing env.
24
+ */
25
+ export type ToolProfile = "full" | "nav" | "edit";
26
+ export declare const PROFILE_NAMES: readonly ToolProfile[];
27
+ /** Minimum nav profile — exploration only, no editing support. */
28
+ export declare const NAV_TOOLS: ReadonlySet<string>;
29
+ /** Edit profile adds batch reads + edit-preparation tools. */
30
+ export declare const EDIT_EXTRAS: ReadonlySet<string>;
31
+ /**
32
+ * Decide which tools the LLM sees in tools/list given a profile.
33
+ * Pure — safe to unit-test without spinning up the server.
34
+ *
35
+ * Tool names NOT matched by any profile rule (e.g. future additions)
36
+ * fall into 'full' only, to stay conservative by default.
37
+ */
38
+ export declare function filterToolsByProfile<T extends {
39
+ name: string;
40
+ }>(tools: readonly T[], profile: ToolProfile): T[];
41
+ /**
42
+ * Parse the TOKEN_PILOT_PROFILE env value. Unknown values get a warning
43
+ * and fall back to full — we never silently apply a guess.
44
+ */
45
+ export declare function parseProfileEnv(envValue: string | undefined, warn?: (msg: string) => void): ToolProfile;
46
+ //# sourceMappingURL=tool-profiles.d.ts.map
@@ -0,0 +1,81 @@
1
+ /**
2
+ * v0.26.3 — tool profiles.
3
+ *
4
+ * Idea lifted honestly from Token Savior's TOKEN_SAVIOR_PROFILE. When an
5
+ * MCP server advertises 22 tools, every tools/list response costs the
6
+ * agent ~3 k tokens before it does anything. Most sessions don't need
7
+ * every tool — a code-review agent uses smart_read + find_usages +
8
+ * outline and nothing else. A profile lets the user ship a narrower
9
+ * tools/list while keeping the handlers live (so a subagent or another
10
+ * user in the same server can still reach the full set if they know
11
+ * the name).
12
+ *
13
+ * Three profiles:
14
+ * - full (default): everything, same as pre-v0.26.3.
15
+ * - nav : read-only exploration. smart_read, outline, find_usages,
16
+ * read_symbol, project_overview, module_info, related_files,
17
+ * explore_area, smart_log, smart_diff.
18
+ * - edit : nav + batch reads + everything Edit needs to hit a symbol
19
+ * precisely. Adds read_symbols, read_range, read_section,
20
+ * read_diff, read_for_edit, smart_read_many.
21
+ *
22
+ * Selection: TOKEN_PILOT_PROFILE=nav|edit|full env var. Unknown values
23
+ * fall back to full with a stderr warning. Silent on missing env.
24
+ */
25
+ export const PROFILE_NAMES = [
26
+ "full",
27
+ "nav",
28
+ "edit",
29
+ ];
30
+ /** Minimum nav profile — exploration only, no editing support. */
31
+ export const NAV_TOOLS = new Set([
32
+ "smart_read",
33
+ "read_symbol",
34
+ "outline",
35
+ "find_usages",
36
+ "project_overview",
37
+ "module_info",
38
+ "related_files",
39
+ "explore_area",
40
+ "smart_log",
41
+ "smart_diff",
42
+ ]);
43
+ /** Edit profile adds batch reads + edit-preparation tools. */
44
+ export const EDIT_EXTRAS = new Set([
45
+ "read_symbols",
46
+ "read_range",
47
+ "read_section",
48
+ "read_diff",
49
+ "read_for_edit",
50
+ "smart_read_many",
51
+ ]);
52
+ /**
53
+ * Decide which tools the LLM sees in tools/list given a profile.
54
+ * Pure — safe to unit-test without spinning up the server.
55
+ *
56
+ * Tool names NOT matched by any profile rule (e.g. future additions)
57
+ * fall into 'full' only, to stay conservative by default.
58
+ */
59
+ export function filterToolsByProfile(tools, profile) {
60
+ if (profile === "full")
61
+ return [...tools];
62
+ if (profile === "nav")
63
+ return tools.filter((t) => NAV_TOOLS.has(t.name));
64
+ // edit = nav + extras
65
+ return tools.filter((t) => NAV_TOOLS.has(t.name) || EDIT_EXTRAS.has(t.name));
66
+ }
67
+ /**
68
+ * Parse the TOKEN_PILOT_PROFILE env value. Unknown values get a warning
69
+ * and fall back to full — we never silently apply a guess.
70
+ */
71
+ export function parseProfileEnv(envValue, warn = () => { }) {
72
+ if (!envValue)
73
+ return "full";
74
+ const lower = envValue.trim().toLowerCase();
75
+ if (lower === "full" || lower === "nav" || lower === "edit") {
76
+ return lower;
77
+ }
78
+ warn(`[token-pilot] Unknown TOKEN_PILOT_PROFILE="${envValue}". Expected full|nav|edit. Falling back to full.`);
79
+ return "full";
80
+ }
81
+ //# sourceMappingURL=tool-profiles.js.map
package/dist/server.js CHANGED
@@ -44,7 +44,9 @@ import { handleReadSection } from "./handlers/read-section.js";
44
44
  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
+ import { appendToolCall } from "./core/tool-call-log.js";
47
48
  import { MCP_INSTRUCTIONS, TOOL_DEFINITIONS, } from "./server/tool-definitions.js";
49
+ import { filterToolsByProfile, parseProfileEnv, } from "./server/tool-profiles.js";
48
50
  import { createTokenEstimates } from "./server/token-estimates.js";
49
51
  import { validateSmartReadArgs, validateReadSymbolArgs, validateReadSymbolsArgs, validateReadRangeArgs, validateReadDiffArgs, validateFindUsagesArgs, validateSmartReadManyArgs, validateReadForEditArgs, validateRelatedFilesArgs, validateOutlineArgs, validateFindUnusedArgs, validateCodeAuditArgs, validateProjectOverviewArgs, validateModuleInfoArgs, validateSmartDiffArgs, validateExploreAreaArgs, validateSmartLogArgs, validateTestSummaryArgs, validateReadSectionArgs, } from "./core/validation.js";
50
52
  export async function createServer(projectRoot, options) {
@@ -240,14 +242,33 @@ export async function createServer(projectRoot, options) {
240
242
  capabilities: { tools: {} },
241
243
  instructions: MCP_INSTRUCTIONS,
242
244
  });
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
+ 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`);
253
+ }
243
254
  server.setRequestHandler(ListToolsRequestSchema, () => ({
244
- tools: TOOL_DEFINITIONS,
255
+ tools: advertisedTools,
245
256
  }));
246
257
  // Token estimation functions (extracted to server/token-estimates.ts)
247
258
  const { fullFileTokens, estimateProjectOverviewWorkflowTokens, estimateOutlineWorkflowTokens, estimateRelatedFilesWorkflowTokens, estimateFindUsagesWorkflowTokens, estimateExploreAreaWorkflowTokens, detectSavingsCategory, } = createTokenEstimates(() => projectRoot, fileCache);
248
259
  /** Record analytics with intent classification and decision trace. Returns policy advisory if any. */
249
260
  function recordWithTrace(call) {
250
261
  const { absPath, args, recentlyEdited, ...rest } = call;
262
+ // v0.26.1 — honest accounting. When a handler signals 'none' as
263
+ // the savings category (e.g. smart_read small-file pass-through),
264
+ // we weren't compressing anything — the caller got the file back
265
+ // verbatim plus a tiny header. Claiming wouldBe = fullFile here
266
+ // produced the -2% "negative savings" line on Opus 4.7's
267
+ // session_analytics. Zero the delta: 0% savings claimed, no ghost
268
+ // overhead.
269
+ if (rest.savingsCategory === "none") {
270
+ rest.tokensWouldBe = rest.tokensReturned;
271
+ }
251
272
  analytics.record({
252
273
  ...rest,
253
274
  intent: classifyIntent(rest.tool),
@@ -279,6 +300,22 @@ export async function createServer(projectRoot, options) {
279
300
  totalCallCount,
280
301
  totalTokensReturned,
281
302
  });
303
+ // v0.26.2 — persist for cumulative tool-audit. Fire-and-forget;
304
+ // disk failures must not block the tool-response path. The audit
305
+ // CLI reads all archives + current to build a per-tool savings
306
+ // distribution across sessions, which is the foundation for any
307
+ // future prune/fix decision.
308
+ void appendToolCall(projectRoot, {
309
+ ts: rest.timestamp,
310
+ session_id: call.sessionId ?? "",
311
+ tool: rest.tool,
312
+ path: rest.path,
313
+ tokensReturned: rest.tokensReturned,
314
+ tokensWouldBe: rest.tokensWouldBe,
315
+ savingsCategory: rest.savingsCategory ?? "compression",
316
+ sessionCacheHit: rest.sessionCacheHit,
317
+ delegatedToContextMode: rest.delegatedToContextMode,
318
+ });
282
319
  return advisory ? `\n${advisory.message}` : null;
283
320
  }
284
321
  // Handle tool calls with validated arguments
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "token-pilot",
3
- "version": "0.24.1",
3
+ "version": "0.26.5",
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",
package/start.sh CHANGED
File without changes
package/.mcp.json DELETED
@@ -1,8 +0,0 @@
1
- {
2
- "mcpServers": {
3
- "token-pilot": {
4
- "command": "sh",
5
- "args": ["${CLAUDE_PLUGIN_ROOT}/start.sh"]
6
- }
7
- }
8
- }