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.
- package/.claude-plugin/marketplace.json +29 -12
- package/.claude-plugin/plugin.json +23 -5
- package/CHANGELOG.md +182 -0
- package/README.md +128 -15
- 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 +55 -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
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* v0.26.2 — persistent MCP tool-call log.
|
|
3
|
+
*
|
|
4
|
+
* Separate from `hook-events.jsonl` (which records Read-hook outcomes),
|
|
5
|
+
* this file accumulates every MCP tool invocation with its token
|
|
6
|
+
* accounting across ALL sessions. Used by `npx token-pilot tool-audit`
|
|
7
|
+
* to produce a per-tool savings distribution that survives `/clear`,
|
|
8
|
+
* session restarts, and even reboots — i.e. the data-driven base we
|
|
9
|
+
* need before pruning or modifying tools based on "savings".
|
|
10
|
+
*
|
|
11
|
+
* Why not piggy-back on hook-events.jsonl? Different data model: hook
|
|
12
|
+
* events are a denied/allowed bitstream keyed by filepath+lineCount,
|
|
13
|
+
* tool calls are rich records with tokensReturned, wouldBe, category,
|
|
14
|
+
* delegation status. Forcing both into one schema would hurt both
|
|
15
|
+
* readers.
|
|
16
|
+
*
|
|
17
|
+
* File path: `<projectRoot>/.token-pilot/tool-calls.jsonl`. Rotation,
|
|
18
|
+
* retention, and best-effort error handling follow the same contract
|
|
19
|
+
* as event-log.ts — identical 10 MB / 30-day / 100 MB caps so overall
|
|
20
|
+
* .token-pilot/ disk usage stays predictable.
|
|
21
|
+
*/
|
|
22
|
+
import { promises as fs } from "node:fs";
|
|
23
|
+
import { join } from "node:path";
|
|
24
|
+
export const TOOL_LOG_ROTATION_BYTES = 10_000_000;
|
|
25
|
+
export const TOOL_LOG_RETENTION_MAX_AGE_DAYS = 30;
|
|
26
|
+
export const TOOL_LOG_RETENTION_MAX_TOTAL_BYTES = 100_000_000;
|
|
27
|
+
const CURRENT_FILE = "tool-calls.jsonl";
|
|
28
|
+
const ARCHIVE_RE = /^tool-calls\.\d+\.jsonl$/;
|
|
29
|
+
function toolLogDir(projectRoot) {
|
|
30
|
+
return join(projectRoot, ".token-pilot");
|
|
31
|
+
}
|
|
32
|
+
export function currentToolLogPath(projectRoot) {
|
|
33
|
+
return join(toolLogDir(projectRoot), CURRENT_FILE);
|
|
34
|
+
}
|
|
35
|
+
async function ensureDir(projectRoot) {
|
|
36
|
+
await fs.mkdir(toolLogDir(projectRoot), { recursive: true });
|
|
37
|
+
}
|
|
38
|
+
async function rotateIfNeeded(projectRoot, thresholdBytes = TOOL_LOG_ROTATION_BYTES) {
|
|
39
|
+
const current = currentToolLogPath(projectRoot);
|
|
40
|
+
try {
|
|
41
|
+
const s = await fs.stat(current);
|
|
42
|
+
if (s.size < thresholdBytes)
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
return; // no file → nothing to rotate
|
|
47
|
+
}
|
|
48
|
+
const archive = join(toolLogDir(projectRoot), `tool-calls.${Date.now()}.jsonl`);
|
|
49
|
+
try {
|
|
50
|
+
await fs.rename(current, archive);
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
/* raced — next writer will append onto whichever file exists */
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Append one tool call. Never throws — telemetry must not break the
|
|
58
|
+
* tool-response path (the caller awaits this but treats errors as
|
|
59
|
+
* silent via `.catch(() => undefined)` at the call site).
|
|
60
|
+
*/
|
|
61
|
+
export async function appendToolCall(projectRoot, event) {
|
|
62
|
+
try {
|
|
63
|
+
await ensureDir(projectRoot);
|
|
64
|
+
await rotateIfNeeded(projectRoot);
|
|
65
|
+
await fs.appendFile(currentToolLogPath(projectRoot), JSON.stringify(event) + "\n");
|
|
66
|
+
}
|
|
67
|
+
catch {
|
|
68
|
+
/* silent */
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Read every tool-call event from the current file + all archives.
|
|
73
|
+
* Malformed JSONL lines are skipped silently — one bad line should not
|
|
74
|
+
* poison the dataset. Returns events in *insertion order within each
|
|
75
|
+
* file*, which happens to be chronological because append-only.
|
|
76
|
+
*/
|
|
77
|
+
export async function loadAllToolCalls(projectRoot) {
|
|
78
|
+
const dir = toolLogDir(projectRoot);
|
|
79
|
+
let entries;
|
|
80
|
+
try {
|
|
81
|
+
entries = await fs.readdir(dir);
|
|
82
|
+
}
|
|
83
|
+
catch {
|
|
84
|
+
return [];
|
|
85
|
+
}
|
|
86
|
+
const files = [];
|
|
87
|
+
if (entries.includes(CURRENT_FILE))
|
|
88
|
+
files.push(CURRENT_FILE);
|
|
89
|
+
for (const n of entries)
|
|
90
|
+
if (ARCHIVE_RE.test(n))
|
|
91
|
+
files.push(n);
|
|
92
|
+
const out = [];
|
|
93
|
+
for (const name of files) {
|
|
94
|
+
let raw;
|
|
95
|
+
try {
|
|
96
|
+
raw = await fs.readFile(join(dir, name), "utf-8");
|
|
97
|
+
}
|
|
98
|
+
catch {
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
101
|
+
for (const line of raw.split("\n")) {
|
|
102
|
+
if (!line.trim())
|
|
103
|
+
continue;
|
|
104
|
+
try {
|
|
105
|
+
out.push(JSON.parse(line));
|
|
106
|
+
}
|
|
107
|
+
catch {
|
|
108
|
+
/* skip malformed */
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
return out;
|
|
113
|
+
}
|
|
114
|
+
// ─── retention (mirrors event-log) ──────────────────────────────────────────
|
|
115
|
+
export function retentionDeletions(files, now, maxAgeDays = TOOL_LOG_RETENTION_MAX_AGE_DAYS, maxTotalBytes = TOOL_LOG_RETENTION_MAX_TOTAL_BYTES) {
|
|
116
|
+
const toDelete = new Set();
|
|
117
|
+
const maxAgeMs = maxAgeDays * 86_400_000;
|
|
118
|
+
const survivors = [];
|
|
119
|
+
for (const f of files) {
|
|
120
|
+
if (now.getTime() - f.mtime.getTime() > maxAgeMs) {
|
|
121
|
+
toDelete.add(f.path);
|
|
122
|
+
}
|
|
123
|
+
else {
|
|
124
|
+
survivors.push(f);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
const totalSize = survivors.reduce((sum, f) => sum + f.size, 0);
|
|
128
|
+
if (totalSize > maxTotalBytes) {
|
|
129
|
+
const byOldest = [...survivors].sort((a, b) => a.mtime.getTime() - b.mtime.getTime());
|
|
130
|
+
let trimmed = totalSize;
|
|
131
|
+
for (const f of byOldest) {
|
|
132
|
+
if (trimmed <= maxTotalBytes)
|
|
133
|
+
break;
|
|
134
|
+
toDelete.add(f.path);
|
|
135
|
+
trimmed -= f.size;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
return [...toDelete];
|
|
139
|
+
}
|
|
140
|
+
export async function applyRetention(projectRoot, now = new Date()) {
|
|
141
|
+
const dir = toolLogDir(projectRoot);
|
|
142
|
+
let entries;
|
|
143
|
+
try {
|
|
144
|
+
entries = await fs.readdir(dir);
|
|
145
|
+
}
|
|
146
|
+
catch {
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
const archives = [];
|
|
150
|
+
for (const name of entries) {
|
|
151
|
+
if (!ARCHIVE_RE.test(name))
|
|
152
|
+
continue;
|
|
153
|
+
const full = join(dir, name);
|
|
154
|
+
try {
|
|
155
|
+
const s = await fs.stat(full);
|
|
156
|
+
archives.push({ path: full, mtime: s.mtime, size: s.size });
|
|
157
|
+
}
|
|
158
|
+
catch {
|
|
159
|
+
/* unreadable → skip */
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
for (const p of retentionDeletions(archives, now)) {
|
|
163
|
+
try {
|
|
164
|
+
await fs.unlink(p);
|
|
165
|
+
}
|
|
166
|
+
catch {
|
|
167
|
+
/* ignore */
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
//# sourceMappingURL=tool-call-log.js.map
|
|
@@ -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
|
@@ -28,6 +28,7 @@ import { handleInstallAgents, maybeEmitStartupReminder, } from "./cli/install-ag
|
|
|
28
28
|
import { handleUninstallAgents } from "./cli/uninstall-agents.js";
|
|
29
29
|
import { appendEvent, applyRetention, } from "./core/event-log.js";
|
|
30
30
|
import { handleStats } from "./cli/stats.js";
|
|
31
|
+
import { handleToolAudit } from "./cli/tool-audit.js";
|
|
31
32
|
import { promptYesNo } from "./cli/install-agents.js";
|
|
32
33
|
import { runClaudeCodeEnvCheck } from "./cli/doctor-env-check.js";
|
|
33
34
|
import { claudeIgnoreStatus, writeDefaultClaudeIgnore, } from "./cli/claudeignore.js";
|
|
@@ -205,6 +206,11 @@ export async function main(cliArgs = process.argv.slice(2)) {
|
|
|
205
206
|
process.exit(code);
|
|
206
207
|
return;
|
|
207
208
|
}
|
|
209
|
+
case "tool-audit": {
|
|
210
|
+
const code = await handleToolAudit(cliArgs.slice(1));
|
|
211
|
+
process.exit(code);
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
208
214
|
case "save-doc": {
|
|
209
215
|
const code = await handleSaveDocCli(cliArgs.slice(1));
|
|
210
216
|
process.exit(code);
|
|
@@ -527,6 +533,19 @@ export function handleHookEdit() {
|
|
|
527
533
|
process.exit(0);
|
|
528
534
|
}
|
|
529
535
|
export async function handleInstallHook(projectRoot) {
|
|
536
|
+
// v0.26.5 — plugin-aware early-return. If we're running as a Claude
|
|
537
|
+
// Code plugin (CLAUDE_PLUGIN_ROOT set) the hooks are already declared
|
|
538
|
+
// in .claude-plugin/hooks/hooks.json and Claude Code wires them up
|
|
539
|
+
// on install. Calling install-hook in that context would write a
|
|
540
|
+
// duplicate entry to the user's settings.json and emit two hooks for
|
|
541
|
+
// every event. Bail early.
|
|
542
|
+
if (process.env.CLAUDE_PLUGIN_ROOT) {
|
|
543
|
+
console.log("token-pilot is running as a Claude Code plugin — hooks are already\n" +
|
|
544
|
+
"declared in .claude-plugin/hooks/hooks.json and registered by the\n" +
|
|
545
|
+
"plugin installer. `install-hook` is only needed for npm/npx setups.\n" +
|
|
546
|
+
"Skipping to avoid duplicate hook entries.");
|
|
547
|
+
process.exit(0);
|
|
548
|
+
}
|
|
530
549
|
let hookOptions;
|
|
531
550
|
try {
|
|
532
551
|
const rawPath = fileURLToPath(new URL("./index.js", import.meta.url));
|
|
@@ -576,6 +595,22 @@ export async function handleDoctor() {
|
|
|
576
595
|
const { join } = await import("node:path");
|
|
577
596
|
const cwd = process.cwd();
|
|
578
597
|
console.log(`token-pilot doctor v${version}\n`);
|
|
598
|
+
// ── Installation mode ──
|
|
599
|
+
// v0.26.5 — tell the user HOW token-pilot is installed. Matters
|
|
600
|
+
// because plugin users don't need `install-hook` (hooks come from
|
|
601
|
+
// .claude-plugin/hooks/hooks.json); npm users do. dev/worktree users
|
|
602
|
+
// are usually contributors running from a local checkout.
|
|
603
|
+
let installMode;
|
|
604
|
+
if (process.env.CLAUDE_PLUGIN_ROOT) {
|
|
605
|
+
installMode = `plugin (${process.env.CLAUDE_PLUGIN_ROOT})`;
|
|
606
|
+
}
|
|
607
|
+
else if (process.argv[1]?.includes("/.claude/worktrees/")) {
|
|
608
|
+
installMode = "dev / worktree (contributor)";
|
|
609
|
+
}
|
|
610
|
+
else {
|
|
611
|
+
installMode = "npm / npx";
|
|
612
|
+
}
|
|
613
|
+
console.log(`Install mode: ${installMode}`);
|
|
579
614
|
// ── Environment ──
|
|
580
615
|
const nodeVersion = process.version;
|
|
581
616
|
const nodeMajor = parseInt(nodeVersion.slice(1), 10);
|
|
@@ -677,6 +712,26 @@ export async function handleDoctor() {
|
|
|
677
712
|
catch {
|
|
678
713
|
/* ignore */
|
|
679
714
|
}
|
|
715
|
+
// ── profile recommendation ──
|
|
716
|
+
// v0.26.4 — data-driven. Reads cumulative tool-calls.jsonl and suggests
|
|
717
|
+
// the narrowest TOKEN_PILOT_PROFILE that wouldn't hide any tool the
|
|
718
|
+
// user actually invokes. Never auto-applies; doctor just prints the
|
|
719
|
+
// env snippet and why.
|
|
720
|
+
try {
|
|
721
|
+
const { loadAllToolCalls } = await import("./core/tool-call-log.js");
|
|
722
|
+
const { recommendProfile, formatRecommendation } = await import("./server/profile-recommender.js");
|
|
723
|
+
const events = await loadAllToolCalls(cwd);
|
|
724
|
+
const rec = recommendProfile(events);
|
|
725
|
+
// Only print when there's actionable signal OR a clear "stay on full"
|
|
726
|
+
// with enough data — skip the noise when the log is empty.
|
|
727
|
+
if (rec.totalCalls > 0) {
|
|
728
|
+
console.log(formatRecommendation(rec));
|
|
729
|
+
console.log("");
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
catch {
|
|
733
|
+
/* doctor must never crash over an optional check */
|
|
734
|
+
}
|
|
680
735
|
// ── CLAUDE.md hygiene ──
|
|
681
736
|
try {
|
|
682
737
|
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
|