token-pilot 0.31.0 → 0.32.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.
- package/.claude-plugin/marketplace.json +2 -2
- package/.claude-plugin/plugin.json +1 -1
- package/agents/tp-api-surface-tracker.md +1 -1
- package/agents/tp-audit-scanner.md +1 -1
- package/agents/tp-commit-writer.md +1 -1
- package/agents/tp-context-engineer.md +1 -1
- package/agents/tp-dead-code-finder.md +1 -1
- package/agents/tp-debugger.md +1 -1
- package/agents/tp-dep-health.md +1 -1
- package/agents/tp-doc-writer.md +1 -1
- package/agents/tp-history-explorer.md +1 -1
- package/agents/tp-impact-analyzer.md +1 -1
- package/agents/tp-incident-timeline.md +1 -1
- package/agents/tp-incremental-builder.md +1 -1
- package/agents/tp-migration-scout.md +1 -1
- package/agents/tp-onboard.md +1 -1
- package/agents/tp-performance-profiler.md +1 -1
- package/agents/tp-pr-reviewer.md +1 -1
- package/agents/tp-refactor-planner.md +1 -1
- package/agents/tp-review-impact.md +1 -1
- package/agents/tp-run.md +1 -1
- package/agents/tp-session-restorer.md +1 -1
- package/agents/tp-ship-coordinator.md +1 -1
- package/agents/tp-spec-writer.md +1 -1
- package/agents/tp-test-coverage-gapper.md +1 -1
- package/agents/tp-test-triage.md +1 -1
- package/agents/tp-test-writer.md +1 -1
- package/dist/core/validation.d.ts +13 -9
- package/dist/core/validation.js +180 -134
- package/dist/handlers/call-tree.d.ts +35 -0
- package/dist/handlers/call-tree.js +70 -0
- package/dist/hooks/session-start.d.ts +2 -0
- package/dist/hooks/session-start.js +49 -0
- package/dist/server/tool-definitions.d.ts +65 -0
- package/dist/server/tool-definitions.js +18 -0
- package/dist/server.js +36 -1
- package/package.json +1 -1
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* v0.32.0 — call_tree MCP tool.
|
|
3
|
+
*
|
|
4
|
+
* Thin wrapper over `AstIndexClient.callTree`. Produces a text tree of
|
|
5
|
+
* callers (depth-N) for one function. Complements `find_usages` which
|
|
6
|
+
* is flat (one level of refs): call_tree is recursive, so you see the
|
|
7
|
+
* full chain from leaves → entry points.
|
|
8
|
+
*
|
|
9
|
+
* Typical use cases:
|
|
10
|
+
* - debugging: "who eventually calls this helper"
|
|
11
|
+
* - refactor planning: "what breaks if I change this function's
|
|
12
|
+
* signature"
|
|
13
|
+
* - dead-code verification: "does anything actually reach this
|
|
14
|
+
* branch"
|
|
15
|
+
*
|
|
16
|
+
* Output shape is indented tree text, not JSON — the MCP-consuming
|
|
17
|
+
* model needs to read it, not diff it.
|
|
18
|
+
*/
|
|
19
|
+
import type { AstIndexClient } from "../ast-index/client.js";
|
|
20
|
+
export interface CallTreeArgs {
|
|
21
|
+
/** Function / method name (unqualified, e.g. `fetchUser`). */
|
|
22
|
+
symbol: string;
|
|
23
|
+
/** Walk-up depth. Default 3, max 6 (anything deeper is overwhelming). */
|
|
24
|
+
depth?: number;
|
|
25
|
+
}
|
|
26
|
+
export declare function handleCallTree(args: CallTreeArgs, astIndex: AstIndexClient): Promise<{
|
|
27
|
+
content: Array<{
|
|
28
|
+
type: "text";
|
|
29
|
+
text: string;
|
|
30
|
+
}>;
|
|
31
|
+
meta: {
|
|
32
|
+
files: string[];
|
|
33
|
+
};
|
|
34
|
+
}>;
|
|
35
|
+
//# sourceMappingURL=call-tree.d.ts.map
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
const MAX_DEPTH = 6;
|
|
2
|
+
function renderNode(node, indent, out) {
|
|
3
|
+
const loc = node.file && node.line != null
|
|
4
|
+
? ` — ${node.file}:${node.line}`
|
|
5
|
+
: node.file
|
|
6
|
+
? ` — ${node.file}`
|
|
7
|
+
: "";
|
|
8
|
+
out.push(`${indent}${node.name}${loc}`);
|
|
9
|
+
if (node.callers && node.callers.length > 0) {
|
|
10
|
+
for (const child of node.callers) {
|
|
11
|
+
renderNode(child, indent + " ", out);
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
export async function handleCallTree(args, astIndex) {
|
|
16
|
+
if (astIndex.isDisabled() || astIndex.isOversized()) {
|
|
17
|
+
return {
|
|
18
|
+
content: [
|
|
19
|
+
{
|
|
20
|
+
type: "text",
|
|
21
|
+
text: "call_tree is disabled: " +
|
|
22
|
+
(astIndex.isDisabled()
|
|
23
|
+
? "project root not detected. Call smart_read() on any project file first."
|
|
24
|
+
: "ast-index indexed >50k files (likely includes node_modules). Ensure node_modules is in .gitignore.") +
|
|
25
|
+
"\nAlternative: use find_usages(symbol) iteratively.",
|
|
26
|
+
},
|
|
27
|
+
],
|
|
28
|
+
meta: { files: [] },
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
const symbol = args.symbol?.trim();
|
|
32
|
+
if (!symbol) {
|
|
33
|
+
return {
|
|
34
|
+
content: [{ type: "text", text: "call_tree: `symbol` is required." }],
|
|
35
|
+
meta: { files: [] },
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
const depth = Math.min(Math.max(1, Math.floor(args.depth ?? 3)), MAX_DEPTH);
|
|
39
|
+
const tree = await astIndex.callTree(symbol, depth);
|
|
40
|
+
if (!tree) {
|
|
41
|
+
return {
|
|
42
|
+
content: [
|
|
43
|
+
{
|
|
44
|
+
type: "text",
|
|
45
|
+
text: `No call-tree found for \`${symbol}\`. The symbol may be uncalled, unindexed, or ambiguous. Try find_usages("${symbol}") for a flat cross-reference list.`,
|
|
46
|
+
},
|
|
47
|
+
],
|
|
48
|
+
meta: { files: [] },
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
const lines = [];
|
|
52
|
+
lines.push(`CALL TREE for \`${symbol}\` (depth ${depth}, callers of callers…):`);
|
|
53
|
+
lines.push("");
|
|
54
|
+
renderNode(tree, " ", lines);
|
|
55
|
+
lines.push("");
|
|
56
|
+
lines.push("Read bottom-up: indented entries call the parent. Root is the symbol you asked for.");
|
|
57
|
+
// Collect files for meta so downstream consumers can open them.
|
|
58
|
+
const files = new Set();
|
|
59
|
+
const collect = (n) => {
|
|
60
|
+
if (n.file)
|
|
61
|
+
files.add(n.file);
|
|
62
|
+
n.callers?.forEach(collect);
|
|
63
|
+
};
|
|
64
|
+
collect(tree);
|
|
65
|
+
return {
|
|
66
|
+
content: [{ type: "text", text: lines.join("\n") }],
|
|
67
|
+
meta: { files: [...files] },
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
//# sourceMappingURL=call-tree.js.map
|
|
@@ -7,6 +7,8 @@
|
|
|
7
7
|
*
|
|
8
8
|
* Output contract: one JSON line on stdout, or exit 0 silent.
|
|
9
9
|
*/
|
|
10
|
+
import { type HookEvent } from "../core/event-log.js";
|
|
11
|
+
export declare function buildSubagentAdoptionNudge(events: HookEvent[], now: number, windowDays?: number, minSample?: number, threshold?: number): string | null;
|
|
10
12
|
export interface AgentEntry {
|
|
11
13
|
name: string;
|
|
12
14
|
description: string;
|
|
@@ -10,7 +10,43 @@
|
|
|
10
10
|
import { readdir, readFile } from "node:fs/promises";
|
|
11
11
|
import { join, basename } from "node:path";
|
|
12
12
|
import { loadLatestSnapshot } from "./../handlers/session-snapshot-persist.js";
|
|
13
|
+
import { loadEvents } from "../core/event-log.js";
|
|
13
14
|
const SNAPSHOT_FRESH_MS = 2 * 3600 * 1000; // 2h — enough to cover compaction/restart, tight enough that a new day's unrelated work doesn't inherit yesterday's thread
|
|
15
|
+
// ─── subagent adoption nudge (v0.32.0) ──────────────────────────────
|
|
16
|
+
// Pure function: takes the event log + current time, returns either a
|
|
17
|
+
// one-liner nudge string or null when there's nothing useful to say.
|
|
18
|
+
// Thresholds are module-level constants so tests can reference them.
|
|
19
|
+
const NUDGE_WINDOW_DAYS = 7;
|
|
20
|
+
/** Minimum Task events in window before we consider the sample big enough. */
|
|
21
|
+
const NUDGE_MIN_SAMPLE = 5;
|
|
22
|
+
/** Miss-rate (routable general-purpose dispatches / total) above which we nudge. */
|
|
23
|
+
const NUDGE_THRESHOLD = 0.5;
|
|
24
|
+
export function buildSubagentAdoptionNudge(events, now, windowDays = NUDGE_WINDOW_DAYS, minSample = NUDGE_MIN_SAMPLE, threshold = NUDGE_THRESHOLD) {
|
|
25
|
+
const cutoff = now - windowDays * 86_400_000;
|
|
26
|
+
const tasks = events.filter((e) => e.event === "task" && e.ts >= cutoff);
|
|
27
|
+
if (tasks.length < minSample)
|
|
28
|
+
return null;
|
|
29
|
+
const misses = tasks.filter((e) => typeof e.matched_tp_agent === "string" &&
|
|
30
|
+
e.matched_tp_agent.length > 0 &&
|
|
31
|
+
e.subagent_type !== e.matched_tp_agent);
|
|
32
|
+
if (misses.length === 0)
|
|
33
|
+
return null;
|
|
34
|
+
const rate = misses.length / tasks.length;
|
|
35
|
+
if (rate < threshold)
|
|
36
|
+
return null;
|
|
37
|
+
const pct = Math.round(rate * 100);
|
|
38
|
+
// Surface the top routing miss pair so the nudge is concrete, not abstract.
|
|
39
|
+
const pairCounts = new Map();
|
|
40
|
+
for (const m of misses) {
|
|
41
|
+
const key = `${m.subagent_type} → ${m.matched_tp_agent}`;
|
|
42
|
+
pairCounts.set(key, (pairCounts.get(key) ?? 0) + 1);
|
|
43
|
+
}
|
|
44
|
+
const topPair = [...pairCounts.entries()].sort((a, b) => b[1] - a[1])[0]?.[0];
|
|
45
|
+
const pairClause = topPair ? ` Top miss: ${topPair}.` : "";
|
|
46
|
+
return (`[token-pilot] subagent miss-rate ${pct}% over last ${windowDays}d ` +
|
|
47
|
+
`(${misses.length}/${tasks.length} Task calls could have used a tp-* specialist).${pairClause} ` +
|
|
48
|
+
`Run \`token-pilot stats --tasks\` for details, or set TOKEN_PILOT_FORCE_SUBAGENTS=1 to hard-block.`);
|
|
49
|
+
}
|
|
14
50
|
function extractSnapshotGoal(body) {
|
|
15
51
|
const m = body.match(/\*\*Goal:\*\*\s*(.+?)(?:\n|$)/);
|
|
16
52
|
return m ? m[1].trim().slice(0, 100) : null;
|
|
@@ -215,6 +251,19 @@ export async function handleSessionStart(opts) {
|
|
|
215
251
|
const goalClause = goal ? ` (goal: "${goal}")` : "";
|
|
216
252
|
message += `\n\n[token-pilot] session_snapshot from ${age}${goalClause}. Read .token-pilot/snapshots/latest.md to resume — or ignore if unrelated.`;
|
|
217
253
|
}
|
|
254
|
+
// v0.32.0 — subagent adoption nudge. Reads recent Task telemetry
|
|
255
|
+
// from hook-events.jsonl; when the main thread is picking
|
|
256
|
+
// general-purpose on routable work, surface a one-liner so the
|
|
257
|
+
// user / agent sees the miss rate without needing `stats --tasks`.
|
|
258
|
+
try {
|
|
259
|
+
const events = await loadEvents(opts.projectRoot);
|
|
260
|
+
const nudge = buildSubagentAdoptionNudge(events, Date.now());
|
|
261
|
+
if (nudge)
|
|
262
|
+
message += `\n\n${nudge}`;
|
|
263
|
+
}
|
|
264
|
+
catch {
|
|
265
|
+
/* silent — telemetry nudge is strictly opt-in */
|
|
266
|
+
}
|
|
218
267
|
const output = {
|
|
219
268
|
hookSpecificOutput: {
|
|
220
269
|
hookEventName: "SessionStart",
|
|
@@ -977,6 +977,71 @@ export declare const TOOL_DEFINITIONS: ({
|
|
|
977
977
|
};
|
|
978
978
|
required?: undefined;
|
|
979
979
|
};
|
|
980
|
+
} | {
|
|
981
|
+
name: string;
|
|
982
|
+
description: string;
|
|
983
|
+
inputSchema: {
|
|
984
|
+
type: "object";
|
|
985
|
+
properties: {
|
|
986
|
+
symbol: {
|
|
987
|
+
type: string;
|
|
988
|
+
description: string;
|
|
989
|
+
};
|
|
990
|
+
depth: {
|
|
991
|
+
type: string;
|
|
992
|
+
description: string;
|
|
993
|
+
};
|
|
994
|
+
path?: undefined;
|
|
995
|
+
show_imports?: undefined;
|
|
996
|
+
show_docs?: undefined;
|
|
997
|
+
scope?: undefined;
|
|
998
|
+
max_tokens?: undefined;
|
|
999
|
+
session_id?: undefined;
|
|
1000
|
+
force?: undefined;
|
|
1001
|
+
context_before?: undefined;
|
|
1002
|
+
context_after?: undefined;
|
|
1003
|
+
show?: undefined;
|
|
1004
|
+
include_edit_context?: undefined;
|
|
1005
|
+
symbols?: undefined;
|
|
1006
|
+
start_line?: undefined;
|
|
1007
|
+
end_line?: undefined;
|
|
1008
|
+
heading?: undefined;
|
|
1009
|
+
context_lines?: undefined;
|
|
1010
|
+
line?: undefined;
|
|
1011
|
+
context?: undefined;
|
|
1012
|
+
include_callers?: undefined;
|
|
1013
|
+
include_tests?: undefined;
|
|
1014
|
+
include_changes?: undefined;
|
|
1015
|
+
section?: undefined;
|
|
1016
|
+
paths?: undefined;
|
|
1017
|
+
kind?: undefined;
|
|
1018
|
+
limit?: undefined;
|
|
1019
|
+
lang?: undefined;
|
|
1020
|
+
mode?: undefined;
|
|
1021
|
+
include?: undefined;
|
|
1022
|
+
recursive?: undefined;
|
|
1023
|
+
max_depth?: undefined;
|
|
1024
|
+
verbose?: undefined;
|
|
1025
|
+
module?: undefined;
|
|
1026
|
+
export_only?: undefined;
|
|
1027
|
+
check?: undefined;
|
|
1028
|
+
pattern?: undefined;
|
|
1029
|
+
name?: undefined;
|
|
1030
|
+
ref?: undefined;
|
|
1031
|
+
count?: undefined;
|
|
1032
|
+
command?: undefined;
|
|
1033
|
+
runner?: undefined;
|
|
1034
|
+
timeout?: undefined;
|
|
1035
|
+
goal?: undefined;
|
|
1036
|
+
decisions?: undefined;
|
|
1037
|
+
confirmed?: undefined;
|
|
1038
|
+
files?: undefined;
|
|
1039
|
+
blocked?: undefined;
|
|
1040
|
+
next?: undefined;
|
|
1041
|
+
sessionId?: undefined;
|
|
1042
|
+
};
|
|
1043
|
+
required: string[];
|
|
1044
|
+
};
|
|
980
1045
|
} | {
|
|
981
1046
|
name: string;
|
|
982
1047
|
description: string;
|
|
@@ -502,6 +502,24 @@ export const TOOL_DEFINITIONS = [
|
|
|
502
502
|
},
|
|
503
503
|
},
|
|
504
504
|
// --- Analysis ---
|
|
505
|
+
{
|
|
506
|
+
name: "call_tree",
|
|
507
|
+
description: "Recursive depth-N call hierarchy for a function. Shows who calls who transitively — complements find_usages (flat one-level refs) by revealing full chains from leaf helpers to entry points. Use for debugging, refactor impact, and verifying reachability.",
|
|
508
|
+
inputSchema: {
|
|
509
|
+
type: "object",
|
|
510
|
+
properties: {
|
|
511
|
+
symbol: {
|
|
512
|
+
type: "string",
|
|
513
|
+
description: "Function / method name, unqualified (e.g. `fetchUser`).",
|
|
514
|
+
},
|
|
515
|
+
depth: {
|
|
516
|
+
type: "number",
|
|
517
|
+
description: "Walk-up depth. Default 3, max 6.",
|
|
518
|
+
},
|
|
519
|
+
},
|
|
520
|
+
required: ["symbol"],
|
|
521
|
+
},
|
|
522
|
+
},
|
|
505
523
|
{
|
|
506
524
|
name: "find_unused",
|
|
507
525
|
description: "Find dead code — functions, classes, and variables with no references across the project. Use for cleanup and refactoring.",
|
package/dist/server.js
CHANGED
|
@@ -28,6 +28,7 @@ import { handleSmartReadMany } from "./handlers/smart-read-many.js";
|
|
|
28
28
|
import { handleProjectOverview } from "./handlers/project-overview.js";
|
|
29
29
|
import { handleNonCodeRead, isNonCodeStructured } from "./handlers/non-code.js";
|
|
30
30
|
import { handleFindUnused } from "./handlers/find-unused.js";
|
|
31
|
+
import { handleCallTree } from "./handlers/call-tree.js";
|
|
31
32
|
import { handleReadForEdit } from "./handlers/read-for-edit.js";
|
|
32
33
|
import { handleRelatedFiles } from "./handlers/related-files.js";
|
|
33
34
|
import { handleOutline } from "./handlers/outline.js";
|
|
@@ -49,7 +50,7 @@ import { getMcpInstructions, TOOL_DEFINITIONS, } from "./server/tool-definitions
|
|
|
49
50
|
import { filterToolsByProfile, parseProfileEnv, } from "./server/tool-profiles.js";
|
|
50
51
|
import { STRICT_SMART_READ_MAX_TOKENS, STRICT_EXPLORE_AREA_INCLUDE, } from "./server/enforcement-mode.js";
|
|
51
52
|
import { createTokenEstimates } from "./server/token-estimates.js";
|
|
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";
|
|
53
|
+
import { validateSmartReadArgs, validateReadSymbolArgs, validateReadSymbolsArgs, validateReadRangeArgs, validateReadDiffArgs, validateFindUsagesArgs, validateSmartReadManyArgs, validateReadForEditArgs, validateRelatedFilesArgs, validateOutlineArgs, validateFindUnusedArgs, validateCallTreeArgs, validateCodeAuditArgs, validateProjectOverviewArgs, validateModuleInfoArgs, validateSmartDiffArgs, validateExploreAreaArgs, validateSmartLogArgs, validateTestSummaryArgs, validateReadSectionArgs, } from "./core/validation.js";
|
|
53
54
|
export async function createServer(projectRoot, options) {
|
|
54
55
|
const mode = options?.enforcementMode ?? "deny";
|
|
55
56
|
const config = await loadConfig(projectRoot);
|
|
@@ -726,6 +727,40 @@ export async function createServer(projectRoot, options) {
|
|
|
726
727
|
],
|
|
727
728
|
};
|
|
728
729
|
}
|
|
730
|
+
case "call_tree": {
|
|
731
|
+
const callTreeArgs = validateCallTreeArgs(args);
|
|
732
|
+
const cachedTree = sessionCache?.get("call_tree", callTreeArgs);
|
|
733
|
+
if (cachedTree) {
|
|
734
|
+
recordWithTrace({
|
|
735
|
+
tool: "call_tree",
|
|
736
|
+
path: callTreeArgs.symbol,
|
|
737
|
+
tokensReturned: cachedTree.tokenEstimate,
|
|
738
|
+
tokensWouldBe: cachedTree.tokensWouldBe ?? cachedTree.tokenEstimate,
|
|
739
|
+
timestamp: Date.now(),
|
|
740
|
+
sessionCacheHit: true,
|
|
741
|
+
savingsCategory: "cache",
|
|
742
|
+
args: callTreeArgs,
|
|
743
|
+
});
|
|
744
|
+
return cachedTree.result;
|
|
745
|
+
}
|
|
746
|
+
const treeResult = await handleCallTree(callTreeArgs, astIndex);
|
|
747
|
+
const treeText = treeResult.content[0]?.text ?? "";
|
|
748
|
+
const treeTokens = estimateTokens(treeText);
|
|
749
|
+
// No good "wouldBe" baseline for call_tree (you'd otherwise
|
|
750
|
+
// run find_usages in a loop). Use a conservative N× multiplier.
|
|
751
|
+
const treeWouldBe = treeTokens * 3;
|
|
752
|
+
sessionCache?.set("call_tree", callTreeArgs, treeResult, { dependsOnAst: true }, treeTokens, treeWouldBe);
|
|
753
|
+
recordWithTrace({
|
|
754
|
+
tool: "call_tree",
|
|
755
|
+
path: callTreeArgs.symbol,
|
|
756
|
+
tokensReturned: treeTokens,
|
|
757
|
+
tokensWouldBe: treeWouldBe,
|
|
758
|
+
timestamp: Date.now(),
|
|
759
|
+
savingsCategory: "compression",
|
|
760
|
+
args: callTreeArgs,
|
|
761
|
+
});
|
|
762
|
+
return treeResult;
|
|
763
|
+
}
|
|
729
764
|
case "find_unused": {
|
|
730
765
|
const unusedArgs = validateFindUnusedArgs(args);
|
|
731
766
|
const cachedUnused = sessionCache?.get("find_unused", unusedArgs);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "token-pilot",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.32.0",
|
|
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",
|