token-pilot 0.24.2 → 0.26.6
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.
- package/.claude-plugin/marketplace.json +29 -12
- package/.claude-plugin/plugin.json +23 -5
- package/CHANGELOG.md +179 -0
- package/README.md +54 -3
- package/dist/agents/tp-api-surface-tracker.md +4 -3
- package/dist/agents/tp-audit-scanner.md +1 -1
- package/dist/agents/tp-commit-writer.md +1 -1
- package/dist/agents/tp-dead-code-finder.md +22 -7
- package/dist/agents/tp-debugger.md +1 -1
- package/dist/agents/tp-dep-health.md +3 -2
- package/dist/agents/tp-history-explorer.md +1 -1
- package/dist/agents/tp-impact-analyzer.md +1 -1
- package/dist/agents/tp-incident-timeline.md +1 -1
- package/dist/agents/tp-migration-scout.md +1 -1
- package/dist/agents/tp-onboard.md +1 -1
- package/dist/agents/tp-pr-reviewer.md +1 -1
- package/dist/agents/tp-refactor-planner.md +1 -1
- package/dist/agents/tp-review-impact.md +1 -1
- package/dist/agents/tp-run.md +1 -1
- package/dist/agents/tp-session-restorer.md +1 -1
- package/dist/agents/tp-test-coverage-gapper.md +1 -1
- package/dist/agents/tp-test-triage.md +1 -1
- package/dist/agents/tp-test-writer.md +1 -1
- package/dist/cli/detect-client.d.ts +39 -0
- package/dist/cli/detect-client.js +106 -0
- package/dist/cli/install-agents.d.ts +1 -0
- package/dist/cli/install-agents.js +31 -1
- package/dist/cli/tool-audit.d.ts +58 -0
- package/dist/cli/tool-audit.js +123 -0
- package/dist/cli/typo-guard.d.ts +1 -1
- package/dist/cli/typo-guard.js +1 -0
- package/dist/core/tool-call-log.d.ts +63 -0
- package/dist/core/tool-call-log.js +171 -0
- package/dist/handlers/read-symbols.js +23 -1
- package/dist/hooks/installer.js +27 -12
- package/dist/index.js +71 -0
- package/dist/server/profile-recommender.d.ts +48 -0
- package/dist/server/profile-recommender.js +102 -0
- package/dist/server/token-estimates.d.ts +17 -3
- package/dist/server/token-estimates.js +77 -45
- package/dist/server/tool-definitions.js +1 -1
- package/dist/server/tool-profiles.d.ts +46 -0
- package/dist/server/tool-profiles.js +81 -0
- package/dist/server.js +38 -1
- package/package.json +1 -1
- package/start.sh +0 -0
- package/.mcp.json +0 -8
|
@@ -67,6 +67,15 @@ export async function handleReadSymbols(args, projectRoot, symbolResolver, fileC
|
|
|
67
67
|
let anyTruncated = false;
|
|
68
68
|
let anyResolved = false;
|
|
69
69
|
let totalTokens = 0;
|
|
70
|
+
// v0.26.1 — overlap dedupe. The ast-index parser occasionally resolves
|
|
71
|
+
// two requested symbols to the exact same line range (arrow-function
|
|
72
|
+
// exports, Vue SFCs, type-vs-function ambiguity). Before this fix the
|
|
73
|
+
// handler emitted the same body N× — a 4× blow-up on Opus 4.7's field
|
|
74
|
+
// report, directly negating batch savings. We key by "startLine:endLine"
|
|
75
|
+
// and emit a short dedupe note instead of repeating the source. Caller
|
|
76
|
+
// still sees *which* names they asked for; token budget stays sane.
|
|
77
|
+
const seenRanges = new Map(); // "start:end" -> first name
|
|
78
|
+
let dedupedCount = 0;
|
|
70
79
|
for (let i = 0; i < N; i++) {
|
|
71
80
|
const symbolName = args.symbols[i];
|
|
72
81
|
const idx = i + 1;
|
|
@@ -78,6 +87,16 @@ export async function handleReadSymbols(args, projectRoot, symbolResolver, fileC
|
|
|
78
87
|
continue;
|
|
79
88
|
}
|
|
80
89
|
anyResolved = true;
|
|
90
|
+
const rangeKey = `${resolved.startLine}:${resolved.endLine}`;
|
|
91
|
+
const firstName = seenRanges.get(rangeKey);
|
|
92
|
+
if (firstName) {
|
|
93
|
+
dedupedCount++;
|
|
94
|
+
sections.push(`SYMBOL ${idx}/${N}: ${symbolName} — DEDUPED\n` +
|
|
95
|
+
`Same [L${resolved.startLine}-${resolved.endLine}] range as "${firstName}" ` +
|
|
96
|
+
`(ast-index parser overlap). See that section above for the source.`);
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
seenRanges.set(rangeKey, symbolName);
|
|
81
100
|
const source = symbolResolver.extractSource(resolved, lines, {
|
|
82
101
|
contextBefore: args.context_before ?? 2,
|
|
83
102
|
contextAfter: args.context_after ?? 0,
|
|
@@ -164,7 +183,10 @@ export async function handleReadSymbols(args, projectRoot, symbolResolver, fileC
|
|
|
164
183
|
if (cached?.hash) {
|
|
165
184
|
contextRegistry.setContentHash(absPath, cached.hash);
|
|
166
185
|
}
|
|
167
|
-
const header = `FILE: ${args.path} | SYMBOLS: ${N} requested
|
|
186
|
+
const header = `FILE: ${args.path} | SYMBOLS: ${N} requested` +
|
|
187
|
+
(dedupedCount > 0
|
|
188
|
+
? ` | DEDUPED: ${dedupedCount} (parser overlap — saved ~${dedupedCount}× body tokens)`
|
|
189
|
+
: "");
|
|
168
190
|
const body = sections.join("\n\n---\n\n");
|
|
169
191
|
const footer = "CONTEXT TRACKED: These symbols are now in your context.";
|
|
170
192
|
const output = [header, "", body, "", footer].join("\n");
|
package/dist/hooks/installer.js
CHANGED
|
@@ -130,9 +130,18 @@ export async function installHook(projectRoot, options) {
|
|
|
130
130
|
const hasEdit = existingHooks.some((h) => h.matcher === "Edit" && isTokenPilotHook(h));
|
|
131
131
|
const hasSessionStart = Array.isArray(settings.hooks?.SessionStart) &&
|
|
132
132
|
settings.hooks.SessionStart.some(isTokenPilotHook);
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
133
|
+
// v0.25.0: check each PostToolUse matcher separately. Previously
|
|
134
|
+
// "any token-pilot hook in PostToolUse" counted the whole section
|
|
135
|
+
// as installed, so v0.21 users (Bash matcher only) missed the
|
|
136
|
+
// Task matcher added in v0.23 and their budget watchdog stayed
|
|
137
|
+
// silent. The required matchers are exactly what createHookConfig
|
|
138
|
+
// ships — derive from there so this stays in sync automatically.
|
|
139
|
+
const requiredPostMatchers = hookConfig.hooks.PostToolUse.map((h) => h.matcher);
|
|
140
|
+
const postMatchers = Array.isArray(settings.hooks?.PostToolUse)
|
|
141
|
+
? settings.hooks.PostToolUse.filter(isTokenPilotHook).map((h) => h.matcher)
|
|
142
|
+
: [];
|
|
143
|
+
const hasAllPostMatchers = requiredPostMatchers.every((m) => postMatchers.includes(m));
|
|
144
|
+
if (hasRead && hasEdit && hasSessionStart && hasAllPostMatchers) {
|
|
136
145
|
return {
|
|
137
146
|
installed: false,
|
|
138
147
|
fatal: false,
|
|
@@ -166,16 +175,22 @@ export async function installHook(projectRoot, options) {
|
|
|
166
175
|
}
|
|
167
176
|
settings.hooks.SessionStart.push(...hookConfig.hooks.SessionStart);
|
|
168
177
|
}
|
|
169
|
-
// Install PostToolUse
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
178
|
+
// Install PostToolUse hooks idempotently — per-matcher check.
|
|
179
|
+
// v0.25.0: earlier code treated the whole section as one unit, which
|
|
180
|
+
// meant users installed in v0.21 (only Bash matcher) never received
|
|
181
|
+
// the Task matcher added in v0.23. That silently broke the budget
|
|
182
|
+
// watchdog for anyone upgrading from an older version. Now we check
|
|
183
|
+
// each matcher individually, same as PreToolUse.
|
|
184
|
+
if (!settings.hooks)
|
|
185
|
+
settings.hooks = {};
|
|
186
|
+
if (!Array.isArray(settings.hooks.PostToolUse)) {
|
|
187
|
+
settings.hooks.PostToolUse = [];
|
|
188
|
+
}
|
|
189
|
+
for (const hookDef of hookConfig.hooks.PostToolUse) {
|
|
190
|
+
const exists = settings.hooks.PostToolUse.some((h) => h.matcher === hookDef.matcher && isTokenPilotHook(h));
|
|
191
|
+
if (!exists) {
|
|
192
|
+
settings.hooks.PostToolUse.push(hookDef);
|
|
177
193
|
}
|
|
178
|
-
settings.hooks.PostToolUse.push(...hookConfig.hooks.PostToolUse);
|
|
179
194
|
}
|
|
180
195
|
await writeFile(settingsPath, JSON.stringify(settings, null, 2) + "\n");
|
|
181
196
|
return {
|
package/dist/index.js
CHANGED
|
@@ -1,4 +1,20 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
// v0.26.6 — handle EPIPE silently. Piping `token-pilot doctor | head -5`
|
|
3
|
+
// causes EPIPE once head closes stdin. Classic Node.js CLI wart. Default
|
|
4
|
+
// behaviour is a red "throw er; // Unhandled 'error' event" stacktrace,
|
|
5
|
+
// which scares users who just wanted a quick look. Standard fix: swallow
|
|
6
|
+
// EPIPE on stdout/stderr and exit 0 — any CLI piped to head|less|grep
|
|
7
|
+
// behaves this way.
|
|
8
|
+
process.stdout.on("error", (err) => {
|
|
9
|
+
if (err.code === "EPIPE")
|
|
10
|
+
process.exit(0);
|
|
11
|
+
throw err;
|
|
12
|
+
});
|
|
13
|
+
process.stderr.on("error", (err) => {
|
|
14
|
+
if (err.code === "EPIPE")
|
|
15
|
+
process.exit(0);
|
|
16
|
+
throw err;
|
|
17
|
+
});
|
|
2
18
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
3
19
|
import { readFileSync, realpathSync, appendFileSync, mkdirSync } from "node:fs";
|
|
4
20
|
import { join } from "node:path";
|
|
@@ -28,6 +44,7 @@ import { handleInstallAgents, maybeEmitStartupReminder, } from "./cli/install-ag
|
|
|
28
44
|
import { handleUninstallAgents } from "./cli/uninstall-agents.js";
|
|
29
45
|
import { appendEvent, applyRetention, } from "./core/event-log.js";
|
|
30
46
|
import { handleStats } from "./cli/stats.js";
|
|
47
|
+
import { handleToolAudit } from "./cli/tool-audit.js";
|
|
31
48
|
import { promptYesNo } from "./cli/install-agents.js";
|
|
32
49
|
import { runClaudeCodeEnvCheck } from "./cli/doctor-env-check.js";
|
|
33
50
|
import { claudeIgnoreStatus, writeDefaultClaudeIgnore, } from "./cli/claudeignore.js";
|
|
@@ -205,6 +222,11 @@ export async function main(cliArgs = process.argv.slice(2)) {
|
|
|
205
222
|
process.exit(code);
|
|
206
223
|
return;
|
|
207
224
|
}
|
|
225
|
+
case "tool-audit": {
|
|
226
|
+
const code = await handleToolAudit(cliArgs.slice(1));
|
|
227
|
+
process.exit(code);
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
208
230
|
case "save-doc": {
|
|
209
231
|
const code = await handleSaveDocCli(cliArgs.slice(1));
|
|
210
232
|
process.exit(code);
|
|
@@ -527,6 +549,19 @@ export function handleHookEdit() {
|
|
|
527
549
|
process.exit(0);
|
|
528
550
|
}
|
|
529
551
|
export async function handleInstallHook(projectRoot) {
|
|
552
|
+
// v0.26.5 — plugin-aware early-return. If we're running as a Claude
|
|
553
|
+
// Code plugin (CLAUDE_PLUGIN_ROOT set) the hooks are already declared
|
|
554
|
+
// in .claude-plugin/hooks/hooks.json and Claude Code wires them up
|
|
555
|
+
// on install. Calling install-hook in that context would write a
|
|
556
|
+
// duplicate entry to the user's settings.json and emit two hooks for
|
|
557
|
+
// every event. Bail early.
|
|
558
|
+
if (process.env.CLAUDE_PLUGIN_ROOT) {
|
|
559
|
+
console.log("token-pilot is running as a Claude Code plugin — hooks are already\n" +
|
|
560
|
+
"declared in .claude-plugin/hooks/hooks.json and registered by the\n" +
|
|
561
|
+
"plugin installer. `install-hook` is only needed for npm/npx setups.\n" +
|
|
562
|
+
"Skipping to avoid duplicate hook entries.");
|
|
563
|
+
process.exit(0);
|
|
564
|
+
}
|
|
530
565
|
let hookOptions;
|
|
531
566
|
try {
|
|
532
567
|
const rawPath = fileURLToPath(new URL("./index.js", import.meta.url));
|
|
@@ -576,6 +611,22 @@ export async function handleDoctor() {
|
|
|
576
611
|
const { join } = await import("node:path");
|
|
577
612
|
const cwd = process.cwd();
|
|
578
613
|
console.log(`token-pilot doctor v${version}\n`);
|
|
614
|
+
// ── Installation mode ──
|
|
615
|
+
// v0.26.5 — tell the user HOW token-pilot is installed. Matters
|
|
616
|
+
// because plugin users don't need `install-hook` (hooks come from
|
|
617
|
+
// .claude-plugin/hooks/hooks.json); npm users do. dev/worktree users
|
|
618
|
+
// are usually contributors running from a local checkout.
|
|
619
|
+
let installMode;
|
|
620
|
+
if (process.env.CLAUDE_PLUGIN_ROOT) {
|
|
621
|
+
installMode = `plugin (${process.env.CLAUDE_PLUGIN_ROOT})`;
|
|
622
|
+
}
|
|
623
|
+
else if (process.argv[1]?.includes("/.claude/worktrees/")) {
|
|
624
|
+
installMode = "dev / worktree (contributor)";
|
|
625
|
+
}
|
|
626
|
+
else {
|
|
627
|
+
installMode = "npm / npx";
|
|
628
|
+
}
|
|
629
|
+
console.log(`Install mode: ${installMode}`);
|
|
579
630
|
// ── Environment ──
|
|
580
631
|
const nodeVersion = process.version;
|
|
581
632
|
const nodeMajor = parseInt(nodeVersion.slice(1), 10);
|
|
@@ -677,6 +728,26 @@ export async function handleDoctor() {
|
|
|
677
728
|
catch {
|
|
678
729
|
/* ignore */
|
|
679
730
|
}
|
|
731
|
+
// ── profile recommendation ──
|
|
732
|
+
// v0.26.4 — data-driven. Reads cumulative tool-calls.jsonl and suggests
|
|
733
|
+
// the narrowest TOKEN_PILOT_PROFILE that wouldn't hide any tool the
|
|
734
|
+
// user actually invokes. Never auto-applies; doctor just prints the
|
|
735
|
+
// env snippet and why.
|
|
736
|
+
try {
|
|
737
|
+
const { loadAllToolCalls } = await import("./core/tool-call-log.js");
|
|
738
|
+
const { recommendProfile, formatRecommendation } = await import("./server/profile-recommender.js");
|
|
739
|
+
const events = await loadAllToolCalls(cwd);
|
|
740
|
+
const rec = recommendProfile(events);
|
|
741
|
+
// Only print when there's actionable signal OR a clear "stay on full"
|
|
742
|
+
// with enough data — skip the noise when the log is empty.
|
|
743
|
+
if (rec.totalCalls > 0) {
|
|
744
|
+
console.log(formatRecommendation(rec));
|
|
745
|
+
console.log("");
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
catch {
|
|
749
|
+
/* doctor must never crash over an optional check */
|
|
750
|
+
}
|
|
680
751
|
// ── CLAUDE.md hygiene ──
|
|
681
752
|
try {
|
|
682
753
|
const r = await assessClaudeMd(cwd);
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* v0.26.4 — data-driven profile recommendation.
|
|
3
|
+
*
|
|
4
|
+
* The user mandate: existing users who don't read CHANGELOG shouldn't
|
|
5
|
+
* be left carrying the full 22-tool tools/list payload forever just
|
|
6
|
+
* because we were afraid of a breaking default change.
|
|
7
|
+
*
|
|
8
|
+
* Approach: the doctor command reads cumulative .token-pilot/tool-calls.jsonl
|
|
9
|
+
* (introduced in v0.26.2) and classifies the user's actual usage:
|
|
10
|
+
*
|
|
11
|
+
* - Every tool they used fits in NAV_TOOLS → recommend `nav`
|
|
12
|
+
* - Uses NAV + a subset of EDIT_EXTRAS → recommend `edit`
|
|
13
|
+
* - Uses full-only tools (test_summary,
|
|
14
|
+
* code_audit, find_unused, session_*) → recommend `full`
|
|
15
|
+
*
|
|
16
|
+
* The recommendation is *advisory*. We never auto-flip anyone's default;
|
|
17
|
+
* a user running through a rare but real tool like code_audit would
|
|
18
|
+
* lose it silently. Doctor prints the recommendation + the exact env
|
|
19
|
+
* snippet to paste into .mcp.json, and the copy nudges the user to act.
|
|
20
|
+
*/
|
|
21
|
+
import type { ToolCallEvent } from "../core/tool-call-log.js";
|
|
22
|
+
import { type ToolProfile } from "./tool-profiles.js";
|
|
23
|
+
export interface ProfileRecommendation {
|
|
24
|
+
/** The profile we'd pick if the user asked "what fits me?". */
|
|
25
|
+
recommended: ToolProfile;
|
|
26
|
+
/** One-sentence explanation printable by doctor. */
|
|
27
|
+
reason: string;
|
|
28
|
+
/** Number of distinct tools the user has ever called. */
|
|
29
|
+
uniqueToolsSeen: number;
|
|
30
|
+
/** Total calls in the log (across all sessions/projects). */
|
|
31
|
+
totalCalls: number;
|
|
32
|
+
/** Tools that would be filtered out under `recommended`. Empty when
|
|
33
|
+
* recommended=full. */
|
|
34
|
+
wouldHide: string[];
|
|
35
|
+
/** True when we don't have enough data to claim more than "full". */
|
|
36
|
+
lowConfidence: boolean;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Pure analysis — takes raw tool-call events, returns a recommendation.
|
|
40
|
+
* Safe to unit-test without filesystem, network, or config.
|
|
41
|
+
*/
|
|
42
|
+
export declare function recommendProfile(events: readonly ToolCallEvent[]): ProfileRecommendation;
|
|
43
|
+
/**
|
|
44
|
+
* Render a doctor-style multi-line block the caller can print verbatim.
|
|
45
|
+
* Pure.
|
|
46
|
+
*/
|
|
47
|
+
export declare function formatRecommendation(rec: ProfileRecommendation): string;
|
|
48
|
+
//# sourceMappingURL=profile-recommender.d.ts.map
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* v0.26.4 — data-driven profile recommendation.
|
|
3
|
+
*
|
|
4
|
+
* The user mandate: existing users who don't read CHANGELOG shouldn't
|
|
5
|
+
* be left carrying the full 22-tool tools/list payload forever just
|
|
6
|
+
* because we were afraid of a breaking default change.
|
|
7
|
+
*
|
|
8
|
+
* Approach: the doctor command reads cumulative .token-pilot/tool-calls.jsonl
|
|
9
|
+
* (introduced in v0.26.2) and classifies the user's actual usage:
|
|
10
|
+
*
|
|
11
|
+
* - Every tool they used fits in NAV_TOOLS → recommend `nav`
|
|
12
|
+
* - Uses NAV + a subset of EDIT_EXTRAS → recommend `edit`
|
|
13
|
+
* - Uses full-only tools (test_summary,
|
|
14
|
+
* code_audit, find_unused, session_*) → recommend `full`
|
|
15
|
+
*
|
|
16
|
+
* The recommendation is *advisory*. We never auto-flip anyone's default;
|
|
17
|
+
* a user running through a rare but real tool like code_audit would
|
|
18
|
+
* lose it silently. Doctor prints the recommendation + the exact env
|
|
19
|
+
* snippet to paste into .mcp.json, and the copy nudges the user to act.
|
|
20
|
+
*/
|
|
21
|
+
import { NAV_TOOLS, EDIT_EXTRAS } from "./tool-profiles.js";
|
|
22
|
+
/** Below this the sample is too small to make a confident recommendation. */
|
|
23
|
+
const MIN_SAMPLE_CALLS = 20;
|
|
24
|
+
/**
|
|
25
|
+
* Pure analysis — takes raw tool-call events, returns a recommendation.
|
|
26
|
+
* Safe to unit-test without filesystem, network, or config.
|
|
27
|
+
*/
|
|
28
|
+
export function recommendProfile(events) {
|
|
29
|
+
const used = new Set();
|
|
30
|
+
for (const e of events)
|
|
31
|
+
used.add(e.tool);
|
|
32
|
+
const totalCalls = events.length;
|
|
33
|
+
const uniqueToolsSeen = used.size;
|
|
34
|
+
if (totalCalls < MIN_SAMPLE_CALLS) {
|
|
35
|
+
return {
|
|
36
|
+
recommended: "full",
|
|
37
|
+
reason: `Only ${totalCalls} call(s) logged — need ≥${MIN_SAMPLE_CALLS} to recommend a narrower profile.`,
|
|
38
|
+
uniqueToolsSeen,
|
|
39
|
+
totalCalls,
|
|
40
|
+
wouldHide: [],
|
|
41
|
+
lowConfidence: true,
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
const allInNav = [...used].every((t) => NAV_TOOLS.has(t));
|
|
45
|
+
if (allInNav) {
|
|
46
|
+
return {
|
|
47
|
+
recommended: "nav",
|
|
48
|
+
reason: `Every tool you've used (${uniqueToolsSeen} distinct) is part of the nav subset. You're a read-only explorer.`,
|
|
49
|
+
uniqueToolsSeen,
|
|
50
|
+
totalCalls,
|
|
51
|
+
wouldHide: [
|
|
52
|
+
...[...EDIT_EXTRAS].filter((t) => !used.has(t)),
|
|
53
|
+
/* full-only — we don't enumerate here, keep the list short */
|
|
54
|
+
],
|
|
55
|
+
lowConfidence: false,
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
const allInEditOrBelow = [...used].every((t) => NAV_TOOLS.has(t) || EDIT_EXTRAS.has(t));
|
|
59
|
+
if (allInEditOrBelow) {
|
|
60
|
+
return {
|
|
61
|
+
recommended: "edit",
|
|
62
|
+
reason: `You use edit-preparation tools (read_for_edit, batch reads) but never reach for full-only tools like code_audit/test_summary/find_unused.`,
|
|
63
|
+
uniqueToolsSeen,
|
|
64
|
+
totalCalls,
|
|
65
|
+
wouldHide: [],
|
|
66
|
+
lowConfidence: false,
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
// Uses at least one full-only tool — stay on full.
|
|
70
|
+
const fullOnlyUsed = [...used].filter((t) => !NAV_TOOLS.has(t) && !EDIT_EXTRAS.has(t));
|
|
71
|
+
return {
|
|
72
|
+
recommended: "full",
|
|
73
|
+
reason: `You actually use full-only tools (${fullOnlyUsed.slice(0, 3).join(", ")}${fullOnlyUsed.length > 3 ? ", …" : ""}). Don't trim.`,
|
|
74
|
+
uniqueToolsSeen,
|
|
75
|
+
totalCalls,
|
|
76
|
+
wouldHide: [],
|
|
77
|
+
lowConfidence: false,
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Render a doctor-style multi-line block the caller can print verbatim.
|
|
82
|
+
* Pure.
|
|
83
|
+
*/
|
|
84
|
+
export function formatRecommendation(rec) {
|
|
85
|
+
const lines = [];
|
|
86
|
+
lines.push(`── profile recommendation ──`);
|
|
87
|
+
lines.push(` data: ${rec.totalCalls} calls, ${rec.uniqueToolsSeen} distinct tools`);
|
|
88
|
+
lines.push(` recommend: TOKEN_PILOT_PROFILE=${rec.recommended}`);
|
|
89
|
+
lines.push(` why: ${rec.reason}`);
|
|
90
|
+
if (rec.recommended !== "full") {
|
|
91
|
+
lines.push(` savings: ~${rec.recommended === "nav" ? "2200 tokens (−54%)" : "1000 tokens (−25%)"} on every tools/list response`);
|
|
92
|
+
lines.push(` apply: add "env": { "TOKEN_PILOT_PROFILE": "${rec.recommended}" } to your token-pilot entry in .mcp.json`);
|
|
93
|
+
}
|
|
94
|
+
else if (rec.lowConfidence) {
|
|
95
|
+
lines.push(` action: keep default (full). Re-run \`token-pilot doctor\` after a few real sessions for a data-backed suggestion.`);
|
|
96
|
+
}
|
|
97
|
+
else {
|
|
98
|
+
lines.push(` action: keep default (full). You're using what you have.`);
|
|
99
|
+
}
|
|
100
|
+
return lines.join("\n");
|
|
101
|
+
}
|
|
102
|
+
//# sourceMappingURL=profile-recommender.js.map
|
|
@@ -2,8 +2,22 @@
|
|
|
2
2
|
* Token estimation functions for analytics.
|
|
3
3
|
* Used to calculate "tokens would be" for honest savings reporting.
|
|
4
4
|
*/
|
|
5
|
-
import type { FileCache } from
|
|
6
|
-
import type { SavingsCategory } from
|
|
5
|
+
import type { FileCache } from "../core/file-cache.js";
|
|
6
|
+
import type { SavingsCategory } from "../core/session-analytics.js";
|
|
7
|
+
/**
|
|
8
|
+
* Honest savings classification for a tool's text output.
|
|
9
|
+
*
|
|
10
|
+
* Standalone (pure) so it can be unit-tested without spinning up the
|
|
11
|
+
* full token-estimates closure. Kept here because it's semantically
|
|
12
|
+
* tied to how this module reports wouldBe/returned pairs.
|
|
13
|
+
*
|
|
14
|
+
* v0.26.1 adds the 'none' branch: smart_read's small-file pass-through
|
|
15
|
+
* returns the file verbatim with a tiny header. Claiming wouldBe =
|
|
16
|
+
* fullFile for those calls was the root cause of the -2% "negative
|
|
17
|
+
* savings" line Opus 4.7 reported. With 'none', the recorder sets
|
|
18
|
+
* wouldBe = returned → 0% savings, no ghost overhead.
|
|
19
|
+
*/
|
|
20
|
+
export declare function detectSavingsCategoryPure(text: string): SavingsCategory;
|
|
7
21
|
/**
|
|
8
22
|
* Creates token estimation functions bound to a project context.
|
|
9
23
|
* Uses getter for projectRoot since it may change on auto-detect.
|
|
@@ -26,6 +40,6 @@ export declare function createTokenEstimates(getProjectRoot: () => string, fileC
|
|
|
26
40
|
externalDeps?: string[];
|
|
27
41
|
changeCount?: number;
|
|
28
42
|
}) => Promise<number>;
|
|
29
|
-
detectSavingsCategory:
|
|
43
|
+
detectSavingsCategory: typeof detectSavingsCategoryPure;
|
|
30
44
|
};
|
|
31
45
|
//# sourceMappingURL=token-estimates.d.ts.map
|
|
@@ -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
|
|
6
|
-
import { resolveSafePath } from
|
|
7
|
-
import { CODE_EXTENSIONS } from
|
|
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(
|
|
20
|
-
const content = await readFileAsync(absPath,
|
|
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: [
|
|
30
|
-
|
|
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
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
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: [
|
|
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(
|
|
102
|
+
if (includeSections.includes("ci")) {
|
|
69
103
|
try {
|
|
70
|
-
const { readdir: readDirAsync } = await import(
|
|
71
|
-
const workflowDir = resolveSafePath(getProjectRoot(),
|
|
72
|
-
const workflowFiles = await readDirAsync(workflowDir, {
|
|
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(
|
|
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(
|
|
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(
|
|
94
|
-
const { resolve: resolvePath } = await import(
|
|
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(
|
|
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
|
-
|
|
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
|