token-pilot 0.40.0 → 0.42.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/README.md +15 -0
- 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/cli/ecosystem-check.js +4 -12
- package/dist/cli/install-statusline.d.ts +51 -0
- package/dist/cli/install-statusline.js +160 -0
- package/dist/cli/typo-guard.d.ts +1 -1
- package/dist/cli/typo-guard.js +2 -0
- package/dist/hooks/session-start.js +9 -61
- package/dist/hooks/subagent-stop.d.ts +28 -0
- package/dist/hooks/subagent-stop.js +41 -0
- package/dist/index.js +36 -1
- package/package.json +1 -1
|
@@ -6,14 +6,14 @@
|
|
|
6
6
|
},
|
|
7
7
|
"metadata": {
|
|
8
8
|
"description": "Token Pilot \u2014 save 60-90% tokens when AI reads code",
|
|
9
|
-
"version": "0.
|
|
9
|
+
"version": "0.42.0"
|
|
10
10
|
},
|
|
11
11
|
"plugins": [
|
|
12
12
|
{
|
|
13
13
|
"name": "token-pilot",
|
|
14
14
|
"source": "./",
|
|
15
15
|
"description": "Reduces token consumption by 60-90% via AST-aware lazy file reading, structural symbol navigation, and cross-session tool-usage analytics. 22 MCP tools + 19 subagents + budget watchdog hooks.",
|
|
16
|
-
"version": "0.
|
|
16
|
+
"version": "0.42.0",
|
|
17
17
|
"author": {
|
|
18
18
|
"name": "Digital-Threads"
|
|
19
19
|
},
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "token-pilot",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.42.0",
|
|
4
4
|
"description": "Saves 60-90% tokens on AI code reading. AST-aware lazy reads, symbol navigation, find_usages, structural git diff/log, edit-safety guard, Task-routing matcher, cross-session telemetry (errors + diagnostics), 25 tp-* subagents tiered to haiku/sonnet/opus with budget watchdog.",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Digital-Threads",
|
package/README.md
CHANGED
|
@@ -232,6 +232,21 @@ lines instead of bouncing the call. The structural summary still
|
|
|
232
232
|
rides along in `additionalContext`. Default OFF because the field
|
|
233
233
|
is undocumented and may change.
|
|
234
234
|
|
|
235
|
+
### Experimental: SubagentStop budget feedback (CC 2.1.163+)
|
|
236
|
+
|
|
237
|
+
Every subagent completion already lands a task-telemetry row via the
|
|
238
|
+
`SubagentStop` hook (that's how `stats --tasks` knows what you
|
|
239
|
+
dispatched). With `TOKEN_PILOT_SUBAGENT_FEEDBACK=1` the same hook also
|
|
240
|
+
returns `additionalContext` — when a `token-pilot workflow` fan-out is
|
|
241
|
+
at ≥90 % of its token ceiling, each completing agent gets a wind-down
|
|
242
|
+
note so a hundred-agent `/workflow` run stops before blowing the
|
|
243
|
+
budget.
|
|
244
|
+
|
|
245
|
+
**Requires Claude Code 2.1.163+.** Returning `additionalContext` from
|
|
246
|
+
`SubagentStop` is only honoured there; older Claude Code labels it a
|
|
247
|
+
hook error. Default OFF for that reason — enable only once
|
|
248
|
+
`claude --version` reports 2.1.163 or later.
|
|
249
|
+
|
|
235
250
|
## What's new for Claude Code 2.1.151+
|
|
236
251
|
|
|
237
252
|
These notes are about behaviour you'll see automatically once you
|
|
@@ -9,7 +9,7 @@ tools:
|
|
|
9
9
|
- mcp__token-pilot__read_symbol
|
|
10
10
|
- Bash
|
|
11
11
|
model: haiku
|
|
12
|
-
token_pilot_version: "0.
|
|
12
|
+
token_pilot_version: "0.42.0"
|
|
13
13
|
token_pilot_body_hash: dd184501203fa7f3c73f419c4ffbe33c4be75400cb64a7a51733a3fe23f6e085
|
|
14
14
|
requiredMcpServers:
|
|
15
15
|
- "token-pilot"
|
|
@@ -8,7 +8,7 @@ tools:
|
|
|
8
8
|
- mcp__token-pilot__test_summary
|
|
9
9
|
- mcp__token-pilot__outline
|
|
10
10
|
- Bash
|
|
11
|
-
token_pilot_version: "0.
|
|
11
|
+
token_pilot_version: "0.42.0"
|
|
12
12
|
token_pilot_body_hash: de64a406b5176de19f7422619c7de7949b1f28865f225402c9cea9255f377428
|
|
13
13
|
requiredMcpServers:
|
|
14
14
|
- "token-pilot"
|
package/agents/tp-debugger.md
CHANGED
package/agents/tp-dep-health.md
CHANGED
package/agents/tp-doc-writer.md
CHANGED
|
@@ -12,7 +12,7 @@ tools:
|
|
|
12
12
|
- mcp__token-pilot__read_symbols
|
|
13
13
|
- Read
|
|
14
14
|
model: sonnet
|
|
15
|
-
token_pilot_version: "0.
|
|
15
|
+
token_pilot_version: "0.42.0"
|
|
16
16
|
token_pilot_body_hash: 351a987e11eba63852f5431a16d8eb53104f4f689f82fdcc5a2bf4db948ba92f
|
|
17
17
|
requiredMcpServers:
|
|
18
18
|
- "token-pilot"
|
|
@@ -8,7 +8,7 @@ tools:
|
|
|
8
8
|
- mcp__token-pilot__read_symbol
|
|
9
9
|
- Bash
|
|
10
10
|
model: inherit
|
|
11
|
-
token_pilot_version: "0.
|
|
11
|
+
token_pilot_version: "0.42.0"
|
|
12
12
|
token_pilot_body_hash: de5722bfea374eaab096c1ae635c37879e7a91370ee3cd0532f4240be03c91eb
|
|
13
13
|
requiredMcpServers:
|
|
14
14
|
- "token-pilot"
|
package/agents/tp-onboard.md
CHANGED
|
@@ -10,7 +10,7 @@ tools:
|
|
|
10
10
|
- mcp__token-pilot__smart_read
|
|
11
11
|
- mcp__token-pilot__smart_read_many
|
|
12
12
|
- mcp__token-pilot__read_section
|
|
13
|
-
token_pilot_version: "0.
|
|
13
|
+
token_pilot_version: "0.42.0"
|
|
14
14
|
token_pilot_body_hash: 832e95633fbc8e9b0c10f3e540a327d4be062fb4b3f17a6cce6be13f414e2927
|
|
15
15
|
requiredMcpServers:
|
|
16
16
|
- "token-pilot"
|
package/agents/tp-pr-reviewer.md
CHANGED
|
@@ -11,7 +11,7 @@ tools:
|
|
|
11
11
|
- mcp__token-pilot__read_for_edit
|
|
12
12
|
- Read
|
|
13
13
|
model: sonnet
|
|
14
|
-
token_pilot_version: "0.
|
|
14
|
+
token_pilot_version: "0.42.0"
|
|
15
15
|
token_pilot_body_hash: f83f50d05b4f70285ae7afed2b1a406fc436df56e61a0aedbfb31edc7f2b6e66
|
|
16
16
|
requiredMcpServers:
|
|
17
17
|
- "token-pilot"
|
|
@@ -8,7 +8,7 @@ tools:
|
|
|
8
8
|
- mcp__token-pilot__outline
|
|
9
9
|
- mcp__token-pilot__read_symbol
|
|
10
10
|
model: sonnet
|
|
11
|
-
token_pilot_version: "0.
|
|
11
|
+
token_pilot_version: "0.42.0"
|
|
12
12
|
token_pilot_body_hash: c5f6fc122c89e16e5cf774045f92169ee3468555320b898171ba13eca5323550
|
|
13
13
|
requiredMcpServers:
|
|
14
14
|
- "token-pilot"
|
|
@@ -9,7 +9,7 @@ tools:
|
|
|
9
9
|
- mcp__token-pilot__module_info
|
|
10
10
|
- Bash
|
|
11
11
|
model: sonnet
|
|
12
|
-
token_pilot_version: "0.
|
|
12
|
+
token_pilot_version: "0.42.0"
|
|
13
13
|
token_pilot_body_hash: 8ef3c3341cbfed4eb8dd130126a9683edc57e378c92ff0ca764d584fd941c55c
|
|
14
14
|
requiredMcpServers:
|
|
15
15
|
- "token-pilot"
|
package/agents/tp-run.md
CHANGED
package/agents/tp-spec-writer.md
CHANGED
|
@@ -10,7 +10,7 @@ tools:
|
|
|
10
10
|
- mcp__token-pilot__test_summary
|
|
11
11
|
- Glob
|
|
12
12
|
- Grep
|
|
13
|
-
token_pilot_version: "0.
|
|
13
|
+
token_pilot_version: "0.42.0"
|
|
14
14
|
token_pilot_body_hash: be81eed53a3720d146cf89e4a14a7a56577633f7c84c234c412ab70d64c05b11
|
|
15
15
|
requiredMcpServers:
|
|
16
16
|
- "token-pilot"
|
package/agents/tp-test-triage.md
CHANGED
|
@@ -8,7 +8,7 @@ tools:
|
|
|
8
8
|
- mcp__token-pilot__find_usages
|
|
9
9
|
- mcp__token-pilot__read_symbol
|
|
10
10
|
model: sonnet
|
|
11
|
-
token_pilot_version: "0.
|
|
11
|
+
token_pilot_version: "0.42.0"
|
|
12
12
|
token_pilot_body_hash: 362ecf4cb03b059421ea26933473700900073dc38b3a7fe271208dfb1ae14f90
|
|
13
13
|
requiredMcpServers:
|
|
14
14
|
- "token-pilot"
|
package/agents/tp-test-writer.md
CHANGED
|
@@ -225,18 +225,10 @@ export function formatStatuslineHint(result, ecosystemStatuses) {
|
|
|
225
225
|
case "unknown":
|
|
226
226
|
return null;
|
|
227
227
|
case "not-configured": {
|
|
228
|
-
lines.push(` ○ no statusline badge configured —
|
|
229
|
-
lines.push(`
|
|
230
|
-
lines.push(`
|
|
231
|
-
lines.push(
|
|
232
|
-
const command = pluginRoot
|
|
233
|
-
? `bash "${pluginRoot}/hooks/statusline-chain.sh"`
|
|
234
|
-
: `bash "$(ls -t ~/.claude/plugins/cache/token-pilot/token-pilot/*/hooks/statusline-chain.sh 2>/dev/null | head -1)"`;
|
|
235
|
-
lines.push(` Add to ${result.configPath}:`);
|
|
236
|
-
lines.push(` "statusLine": {`);
|
|
237
|
-
lines.push(` "type": "command",`);
|
|
238
|
-
lines.push(` "command": "${command.replace(/"/g, '\\"')}"`);
|
|
239
|
-
lines.push(` }`);
|
|
228
|
+
lines.push(` ○ no statusline badge configured — see token-pilot's live`);
|
|
229
|
+
lines.push(` saved-token count + enforcement mode in your status bar.`);
|
|
230
|
+
lines.push(` one command (non-destructive — never clobbers an existing one):`);
|
|
231
|
+
lines.push(` token-pilot install-statusline`);
|
|
240
232
|
return lines.join("\n");
|
|
241
233
|
}
|
|
242
234
|
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* v0.42.0 — `token-pilot install-statusline`.
|
|
3
|
+
*
|
|
4
|
+
* Convenience installer for the statusline badge. v0.41.1 removed the
|
|
5
|
+
* intrusive sessionTitle overwrite and pointed users at the additive
|
|
6
|
+
* statusline (hooks/statusline-chain.sh) — the caveman-style channel
|
|
7
|
+
* that sits ALONGSIDE the session name and live-updates on every render.
|
|
8
|
+
* This command wires it into `~/.claude/settings.json` without making
|
|
9
|
+
* the user hand-edit JSON.
|
|
10
|
+
*
|
|
11
|
+
* Non-destructive by design (the sessionTitle lesson): we NEVER clobber
|
|
12
|
+
* a third-party statusLine. Decision per current state:
|
|
13
|
+
*
|
|
14
|
+
* not-configured → write our chain command
|
|
15
|
+
* configured-caveman-only → upgrade to chain (shows BOTH badges)
|
|
16
|
+
* configured-tp-only → upgrade to chain (so caveman shows too if present)
|
|
17
|
+
* configured-chain → already ideal, no-op
|
|
18
|
+
* configured-other → leave alone; print how to switch manually
|
|
19
|
+
* unknown → settings.json unreadable; print guidance
|
|
20
|
+
*
|
|
21
|
+
* `--force` overrides the configured-other guard for users who really
|
|
22
|
+
* want to replace a custom statusLine.
|
|
23
|
+
*/
|
|
24
|
+
import type { StatuslineStatus } from "./ecosystem-check.js";
|
|
25
|
+
/** The version-agnostic chain command (auto-picks the newest plugin dir). */
|
|
26
|
+
export declare const CHAIN_COMMAND = "bash \"$(ls -t ~/.claude/plugins/cache/token-pilot/token-pilot/*/hooks/statusline-chain.sh 2>/dev/null | head -1)\"";
|
|
27
|
+
export interface InstallStatuslineResult {
|
|
28
|
+
action: "installed" | "upgraded" | "noop" | "skipped";
|
|
29
|
+
message: string;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Pure decision: given the current statusline status, what should the
|
|
33
|
+
* installer do? Separated from I/O for unit tests.
|
|
34
|
+
*/
|
|
35
|
+
export declare function decideStatuslineAction(status: StatuslineStatus, force: boolean): {
|
|
36
|
+
write: boolean;
|
|
37
|
+
result: InstallStatuslineResult;
|
|
38
|
+
};
|
|
39
|
+
/**
|
|
40
|
+
* Classify the statusLine state of a settings.json at `settingsPath`.
|
|
41
|
+
* Async, path-injectable (so tests point at a tmp file). Mirrors
|
|
42
|
+
* ecosystem-check.checkStatusline but works on any path. Never throws.
|
|
43
|
+
*/
|
|
44
|
+
export declare function classifyStatuslineAt(settingsPath: string): Promise<StatuslineStatus>;
|
|
45
|
+
/**
|
|
46
|
+
* CLI entry. Returns an exit code.
|
|
47
|
+
*/
|
|
48
|
+
export declare function handleInstallStatusline(argv: string[], opts?: {
|
|
49
|
+
settingsPath?: string;
|
|
50
|
+
}): Promise<number>;
|
|
51
|
+
//# sourceMappingURL=install-statusline.d.ts.map
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* v0.42.0 — `token-pilot install-statusline`.
|
|
3
|
+
*
|
|
4
|
+
* Convenience installer for the statusline badge. v0.41.1 removed the
|
|
5
|
+
* intrusive sessionTitle overwrite and pointed users at the additive
|
|
6
|
+
* statusline (hooks/statusline-chain.sh) — the caveman-style channel
|
|
7
|
+
* that sits ALONGSIDE the session name and live-updates on every render.
|
|
8
|
+
* This command wires it into `~/.claude/settings.json` without making
|
|
9
|
+
* the user hand-edit JSON.
|
|
10
|
+
*
|
|
11
|
+
* Non-destructive by design (the sessionTitle lesson): we NEVER clobber
|
|
12
|
+
* a third-party statusLine. Decision per current state:
|
|
13
|
+
*
|
|
14
|
+
* not-configured → write our chain command
|
|
15
|
+
* configured-caveman-only → upgrade to chain (shows BOTH badges)
|
|
16
|
+
* configured-tp-only → upgrade to chain (so caveman shows too if present)
|
|
17
|
+
* configured-chain → already ideal, no-op
|
|
18
|
+
* configured-other → leave alone; print how to switch manually
|
|
19
|
+
* unknown → settings.json unreadable; print guidance
|
|
20
|
+
*
|
|
21
|
+
* `--force` overrides the configured-other guard for users who really
|
|
22
|
+
* want to replace a custom statusLine.
|
|
23
|
+
*/
|
|
24
|
+
import { readFile, writeFile, mkdir, access } from "node:fs/promises";
|
|
25
|
+
import { homedir } from "node:os";
|
|
26
|
+
import { dirname, join } from "node:path";
|
|
27
|
+
/** The version-agnostic chain command (auto-picks the newest plugin dir). */
|
|
28
|
+
export const CHAIN_COMMAND = 'bash "$(ls -t ~/.claude/plugins/cache/token-pilot/token-pilot/*/hooks/statusline-chain.sh 2>/dev/null | head -1)"';
|
|
29
|
+
/**
|
|
30
|
+
* Pure decision: given the current statusline status, what should the
|
|
31
|
+
* installer do? Separated from I/O for unit tests.
|
|
32
|
+
*/
|
|
33
|
+
export function decideStatuslineAction(status, force) {
|
|
34
|
+
switch (status) {
|
|
35
|
+
case "not-configured":
|
|
36
|
+
return {
|
|
37
|
+
write: true,
|
|
38
|
+
result: {
|
|
39
|
+
action: "installed",
|
|
40
|
+
message: "statusLine configured — the [TP] badge will show in your status bar (restart Claude Code).",
|
|
41
|
+
},
|
|
42
|
+
};
|
|
43
|
+
case "configured-caveman-only":
|
|
44
|
+
case "configured-tp-only":
|
|
45
|
+
return {
|
|
46
|
+
write: true,
|
|
47
|
+
result: {
|
|
48
|
+
action: "upgraded",
|
|
49
|
+
message: "statusLine upgraded to the chain wrapper — both caveman and [TP] badges now render side by side.",
|
|
50
|
+
},
|
|
51
|
+
};
|
|
52
|
+
case "configured-chain":
|
|
53
|
+
return {
|
|
54
|
+
write: false,
|
|
55
|
+
result: {
|
|
56
|
+
action: "noop",
|
|
57
|
+
message: "statusLine already uses the token-pilot chain wrapper. Nothing to do.",
|
|
58
|
+
},
|
|
59
|
+
};
|
|
60
|
+
case "configured-other":
|
|
61
|
+
if (force) {
|
|
62
|
+
return {
|
|
63
|
+
write: true,
|
|
64
|
+
result: {
|
|
65
|
+
action: "installed",
|
|
66
|
+
message: "Replaced your custom statusLine with the token-pilot chain wrapper (--force).",
|
|
67
|
+
},
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
return {
|
|
71
|
+
write: false,
|
|
72
|
+
result: {
|
|
73
|
+
action: "skipped",
|
|
74
|
+
message: "You already have a custom statusLine — left untouched. " +
|
|
75
|
+
"To show the [TP] badge too, set statusLine.command to:\n " +
|
|
76
|
+
CHAIN_COMMAND +
|
|
77
|
+
"\nor re-run with --force to replace it.",
|
|
78
|
+
},
|
|
79
|
+
};
|
|
80
|
+
case "unknown":
|
|
81
|
+
default:
|
|
82
|
+
return {
|
|
83
|
+
write: false,
|
|
84
|
+
result: {
|
|
85
|
+
action: "skipped",
|
|
86
|
+
message: "Could not read ~/.claude/settings.json as JSON — not modifying it. " +
|
|
87
|
+
"Add this manually under \"statusLine\":\n " +
|
|
88
|
+
CHAIN_COMMAND,
|
|
89
|
+
},
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Classify the statusLine state of a settings.json at `settingsPath`.
|
|
95
|
+
* Async, path-injectable (so tests point at a tmp file). Mirrors
|
|
96
|
+
* ecosystem-check.checkStatusline but works on any path. Never throws.
|
|
97
|
+
*/
|
|
98
|
+
export async function classifyStatuslineAt(settingsPath) {
|
|
99
|
+
try {
|
|
100
|
+
await access(settingsPath);
|
|
101
|
+
}
|
|
102
|
+
catch {
|
|
103
|
+
return "not-configured";
|
|
104
|
+
}
|
|
105
|
+
let parsed;
|
|
106
|
+
try {
|
|
107
|
+
parsed = JSON.parse(await readFile(settingsPath, "utf-8"));
|
|
108
|
+
}
|
|
109
|
+
catch {
|
|
110
|
+
return "unknown";
|
|
111
|
+
}
|
|
112
|
+
const cmd = parsed
|
|
113
|
+
?.statusLine?.command;
|
|
114
|
+
if (typeof cmd !== "string")
|
|
115
|
+
return "not-configured";
|
|
116
|
+
if (cmd.includes("statusline-chain.sh"))
|
|
117
|
+
return "configured-chain";
|
|
118
|
+
if (cmd.includes("tp-statusline.sh"))
|
|
119
|
+
return "configured-tp-only";
|
|
120
|
+
if (cmd.includes("caveman-statusline.sh"))
|
|
121
|
+
return "configured-caveman-only";
|
|
122
|
+
return "configured-other";
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* CLI entry. Returns an exit code.
|
|
126
|
+
*/
|
|
127
|
+
export async function handleInstallStatusline(argv, opts) {
|
|
128
|
+
const force = argv.includes("--force");
|
|
129
|
+
const settingsPath = opts?.settingsPath ?? join(homedir(), ".claude", "settings.json");
|
|
130
|
+
const status = await classifyStatuslineAt(settingsPath);
|
|
131
|
+
const { write, result } = decideStatuslineAction(status, force);
|
|
132
|
+
if (!write) {
|
|
133
|
+
process.stdout.write(`[token-pilot] ${result.message}\n`);
|
|
134
|
+
return 0;
|
|
135
|
+
}
|
|
136
|
+
// Merge the statusLine field into existing settings (preserve the rest).
|
|
137
|
+
let settings = {};
|
|
138
|
+
try {
|
|
139
|
+
const raw = await readFile(settingsPath, "utf-8");
|
|
140
|
+
const parsed = JSON.parse(raw);
|
|
141
|
+
if (parsed && typeof parsed === "object") {
|
|
142
|
+
settings = parsed;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
catch {
|
|
146
|
+
/* fresh file — start clean (only reached when status was safe) */
|
|
147
|
+
}
|
|
148
|
+
settings.statusLine = { type: "command", command: CHAIN_COMMAND };
|
|
149
|
+
try {
|
|
150
|
+
await mkdir(dirname(settingsPath), { recursive: true });
|
|
151
|
+
await writeFile(settingsPath, JSON.stringify(settings, null, 2) + "\n");
|
|
152
|
+
}
|
|
153
|
+
catch (err) {
|
|
154
|
+
process.stderr.write(`[token-pilot] failed to write ${settingsPath}: ${err instanceof Error ? err.message : String(err)}\n`);
|
|
155
|
+
return 1;
|
|
156
|
+
}
|
|
157
|
+
process.stdout.write(`[token-pilot] ${result.message}\n`);
|
|
158
|
+
return 0;
|
|
159
|
+
}
|
|
160
|
+
//# sourceMappingURL=install-statusline.js.map
|
package/dist/cli/typo-guard.d.ts
CHANGED
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
* Everything else passes through untouched — a real project root like
|
|
18
18
|
* `/home/user/my-project` or `./subdir` goes to startServer as before.
|
|
19
19
|
*/
|
|
20
|
-
export declare const KNOWN_COMMANDS: readonly ["hook-read", "hook-edit", "hook-pre-bash", "hook-pre-grep", "hook-pre-task", "hook-post-bash", "hook-post-task", "hook-session-start", "hook-bootstrap", "hook-subagent-stop", "install-hook", "uninstall-hook", "install-ast-index", "doctor", "bless-agents", "unbless-agents", "install-agents", "uninstall-agents", "stats", "tool-audit", "save-doc", "list-docs", "init", "migrate-hooks", "errors", "workflow", "--version", "-v", "--help", "-h"];
|
|
20
|
+
export declare const KNOWN_COMMANDS: readonly ["hook-read", "hook-edit", "hook-pre-bash", "hook-pre-grep", "hook-pre-task", "hook-post-bash", "hook-post-task", "hook-session-start", "hook-bootstrap", "hook-subagent-stop", "install-statusline", "install-hook", "uninstall-hook", "install-ast-index", "doctor", "bless-agents", "unbless-agents", "install-agents", "uninstall-agents", "stats", "tool-audit", "save-doc", "list-docs", "init", "migrate-hooks", "errors", "workflow", "--version", "-v", "--help", "-h"];
|
|
21
21
|
export interface TypoGuardResult {
|
|
22
22
|
kind: "pass-through" | "typo";
|
|
23
23
|
suggestion?: string;
|
package/dist/cli/typo-guard.js
CHANGED
|
@@ -37,6 +37,8 @@ export const KNOWN_COMMANDS = [
|
|
|
37
37
|
"hook-bootstrap",
|
|
38
38
|
// v0.40.0 — canonical subagent-completion capture
|
|
39
39
|
"hook-subagent-stop",
|
|
40
|
+
// v0.42.0 — one-command statusline badge installer
|
|
41
|
+
"install-statusline",
|
|
40
42
|
"install-hook",
|
|
41
43
|
"uninstall-hook",
|
|
42
44
|
"install-ast-index",
|
|
@@ -274,72 +274,20 @@ export async function handleSessionStart(opts) {
|
|
|
274
274
|
".token-pilot/snapshots/latest.md",
|
|
275
275
|
".token-pilot/hook-events.jsonl",
|
|
276
276
|
];
|
|
277
|
-
// v0.
|
|
278
|
-
//
|
|
279
|
-
//
|
|
280
|
-
// savings
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
// sessionId="" makes loadSessionStats short-circuit; use a tiny
|
|
287
|
-
// dedicated reader instead.
|
|
288
|
-
const { readFileSync } = await import("node:fs");
|
|
289
|
-
const { join } = await import("node:path");
|
|
290
|
-
let total = 0;
|
|
291
|
-
try {
|
|
292
|
-
const raw = readFileSync(join(opts.projectRoot, ".token-pilot", "hook-events.jsonl"), "utf-8");
|
|
293
|
-
for (const line of raw.split("\n")) {
|
|
294
|
-
if (!line)
|
|
295
|
-
continue;
|
|
296
|
-
try {
|
|
297
|
-
const ev = JSON.parse(line);
|
|
298
|
-
if (typeof ev.savedTokens === "number")
|
|
299
|
-
total += ev.savedTokens;
|
|
300
|
-
}
|
|
301
|
-
catch {
|
|
302
|
-
/* skip malformed line */
|
|
303
|
-
}
|
|
304
|
-
}
|
|
305
|
-
}
|
|
306
|
-
catch {
|
|
307
|
-
/* no log file yet */
|
|
308
|
-
}
|
|
309
|
-
// Use the larger of "single-session" (loadSessionStats fallback)
|
|
310
|
-
// and the all-sessions sum so an empty single-session number
|
|
311
|
-
// doesn't override the meaningful project-wide total.
|
|
312
|
-
const surfaced = Math.max(total, stats.savedTokens);
|
|
313
|
-
if (surfaced > 0) {
|
|
314
|
-
const human = surfaced >= 1_000_000
|
|
315
|
-
? `${(surfaced / 1_000_000).toFixed(1)}M`
|
|
316
|
-
: surfaced >= 1000
|
|
317
|
-
? `${Math.round(surfaced / 1000)}k`
|
|
318
|
-
: `${surfaced}`;
|
|
319
|
-
sessionTitle = `[TP] ${human} saved`;
|
|
320
|
-
}
|
|
321
|
-
// v0.38.0 — when a fleet workflow is active, prefer a
|
|
322
|
-
// workflow-progress title so a long fan-out run shows live
|
|
323
|
-
// task count + budget in the window title.
|
|
324
|
-
const { activeWorkflowId, workflowStatus } = await import("../core/workflow.js");
|
|
325
|
-
const wfId = activeWorkflowId();
|
|
326
|
-
if (wfId) {
|
|
327
|
-
const st = await workflowStatus(opts.projectRoot, wfId);
|
|
328
|
-
if (st) {
|
|
329
|
-
const pct = st.pct != null ? ` · ${st.pct}%` : "";
|
|
330
|
-
sessionTitle = `[TP] wf · ${st.task_count} tasks${pct}`;
|
|
331
|
-
}
|
|
332
|
-
}
|
|
333
|
-
}
|
|
334
|
-
catch {
|
|
335
|
-
/* sessionTitle is best-effort decoration; never block startup */
|
|
336
|
-
}
|
|
277
|
+
// v0.41.1 — sessionTitle removed. v0.36.0 set the window/tab title
|
|
278
|
+
// to `[TP] Nk saved`, but `sessionTitle` OVERWRITES Claude Code's
|
|
279
|
+
// own session name — an intrusive clobber users (rightly) disliked.
|
|
280
|
+
// The cumulative-savings display belongs in the additive statusline
|
|
281
|
+
// badge (hooks/tp-statusline.sh), the same non-intrusive channel
|
|
282
|
+
// caveman uses — it sits alongside the session name instead of
|
|
283
|
+
// replacing it. Workflow progress, likewise, can ride the
|
|
284
|
+
// statusline (tp-statusline.sh reads the active workflow) rather
|
|
285
|
+
// than hijacking the title.
|
|
337
286
|
const output = {
|
|
338
287
|
hookSpecificOutput: {
|
|
339
288
|
hookEventName: "SessionStart",
|
|
340
289
|
additionalContext: message,
|
|
341
290
|
watchPaths,
|
|
342
|
-
...(sessionTitle ? { sessionTitle } : {}),
|
|
343
291
|
},
|
|
344
292
|
};
|
|
345
293
|
return JSON.stringify(output);
|
|
@@ -52,4 +52,32 @@ export declare function tokensFromTranscript(path: string | undefined): number;
|
|
|
52
52
|
* can pre-resolve for tests via `tokensOverride`.
|
|
53
53
|
*/
|
|
54
54
|
export declare function buildSubagentTaskEvent(input: SubagentStopInput, now: number, tokensOverride?: number): HookEvent | null;
|
|
55
|
+
export interface SubagentFeedbackContext {
|
|
56
|
+
/** Active workflow budget status, when a fleet workflow is running. */
|
|
57
|
+
workflow?: {
|
|
58
|
+
workflow_id: string;
|
|
59
|
+
budget_tokens: number | null;
|
|
60
|
+
used_tokens: number;
|
|
61
|
+
pct: number | null;
|
|
62
|
+
} | null;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Decide the `additionalContext` feedback to hand back from a
|
|
66
|
+
* SubagentStop hook. Pure — caller resolves the workflow status.
|
|
67
|
+
*
|
|
68
|
+
* Returns a short wind-down note when an active fleet workflow is at or
|
|
69
|
+
* past 90 % of its token ceiling, so a `/workflow`-style fan-out winds
|
|
70
|
+
* down before the budget is blown. Returns null otherwise — we do NOT
|
|
71
|
+
* nag on every completion; broad adoption nudges stay in SessionStart.
|
|
72
|
+
*
|
|
73
|
+
* Emission is the caller's responsibility and is gated behind
|
|
74
|
+
* TOKEN_PILOT_SUBAGENT_FEEDBACK=1 + Claude Code 2.1.163+ (older Claude
|
|
75
|
+
* Code labels a SubagentStop hookSpecificOutput return as a hook error).
|
|
76
|
+
*/
|
|
77
|
+
export declare function decideSubagentFeedback(_input: SubagentStopInput, ctx: SubagentFeedbackContext): string | null;
|
|
78
|
+
/**
|
|
79
|
+
* Render the SubagentStop hook JSON response carrying feedback. Returns
|
|
80
|
+
* null when there is nothing to say (caller writes no stdout).
|
|
81
|
+
*/
|
|
82
|
+
export declare function renderSubagentFeedback(message: string | null): string | null;
|
|
55
83
|
//# sourceMappingURL=subagent-stop.d.ts.map
|
|
@@ -105,4 +105,45 @@ export function buildSubagentTaskEvent(input, now, tokensOverride) {
|
|
|
105
105
|
code: "subagent_stop",
|
|
106
106
|
};
|
|
107
107
|
}
|
|
108
|
+
/**
|
|
109
|
+
* Decide the `additionalContext` feedback to hand back from a
|
|
110
|
+
* SubagentStop hook. Pure — caller resolves the workflow status.
|
|
111
|
+
*
|
|
112
|
+
* Returns a short wind-down note when an active fleet workflow is at or
|
|
113
|
+
* past 90 % of its token ceiling, so a `/workflow`-style fan-out winds
|
|
114
|
+
* down before the budget is blown. Returns null otherwise — we do NOT
|
|
115
|
+
* nag on every completion; broad adoption nudges stay in SessionStart.
|
|
116
|
+
*
|
|
117
|
+
* Emission is the caller's responsibility and is gated behind
|
|
118
|
+
* TOKEN_PILOT_SUBAGENT_FEEDBACK=1 + Claude Code 2.1.163+ (older Claude
|
|
119
|
+
* Code labels a SubagentStop hookSpecificOutput return as a hook error).
|
|
120
|
+
*/
|
|
121
|
+
export function decideSubagentFeedback(_input, ctx) {
|
|
122
|
+
const wf = ctx.workflow;
|
|
123
|
+
if (wf &&
|
|
124
|
+
wf.budget_tokens != null &&
|
|
125
|
+
wf.budget_tokens > 0 &&
|
|
126
|
+
wf.used_tokens >= wf.budget_tokens * 0.9) {
|
|
127
|
+
const pct = wf.pct != null ? `${wf.pct}%` : "~90%";
|
|
128
|
+
return (`[token-pilot] workflow ${wf.workflow_id} is at ${pct} of its ` +
|
|
129
|
+
`${wf.budget_tokens} token ceiling (${wf.used_tokens} used). ` +
|
|
130
|
+
`Wind down the fan-out: finish in-flight branches and report ` +
|
|
131
|
+
`rather than dispatching new agents.`);
|
|
132
|
+
}
|
|
133
|
+
return null;
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Render the SubagentStop hook JSON response carrying feedback. Returns
|
|
137
|
+
* null when there is nothing to say (caller writes no stdout).
|
|
138
|
+
*/
|
|
139
|
+
export function renderSubagentFeedback(message) {
|
|
140
|
+
if (!message)
|
|
141
|
+
return null;
|
|
142
|
+
return JSON.stringify({
|
|
143
|
+
hookSpecificOutput: {
|
|
144
|
+
hookEventName: "SubagentStop",
|
|
145
|
+
additionalContext: message,
|
|
146
|
+
},
|
|
147
|
+
});
|
|
148
|
+
}
|
|
108
149
|
//# sourceMappingURL=subagent-stop.js.map
|
package/dist/index.js
CHANGED
|
@@ -336,12 +336,37 @@ export async function main(cliArgs = process.argv.slice(2)) {
|
|
|
336
336
|
await runHookEntryPoint({ hook: "hook-subagent-stop" }, async () => {
|
|
337
337
|
const stdin = readFileSync(0, "utf-8");
|
|
338
338
|
const input = JSON.parse(stdin);
|
|
339
|
-
const { buildSubagentTaskEvent } = await import("./hooks/subagent-stop.js");
|
|
339
|
+
const { buildSubagentTaskEvent, decideSubagentFeedback, renderSubagentFeedback, } = await import("./hooks/subagent-stop.js");
|
|
340
340
|
const ev = buildSubagentTaskEvent(input, Date.now());
|
|
341
341
|
if (ev) {
|
|
342
342
|
const { appendEvent } = await import("./core/event-log.js");
|
|
343
343
|
await appendEvent(process.cwd(), ev);
|
|
344
344
|
}
|
|
345
|
+
// v0.41.0 — optional SubagentStop feedback. Returning
|
|
346
|
+
// hookSpecificOutput.additionalContext from SubagentStop is a
|
|
347
|
+
// Claude Code 2.1.163+ feature; on older Claude Code it is
|
|
348
|
+
// labelled a hook error (noise). Gate strictly behind
|
|
349
|
+
// TOKEN_PILOT_SUBAGENT_FEEDBACK=1 so the default path (telemetry
|
|
350
|
+
// only) stays safe on every version.
|
|
351
|
+
if (process.env.TOKEN_PILOT_SUBAGENT_FEEDBACK === "1") {
|
|
352
|
+
const { activeWorkflowId, workflowStatus } = await import("./core/workflow.js");
|
|
353
|
+
let wf = null;
|
|
354
|
+
const wfId = activeWorkflowId();
|
|
355
|
+
if (wfId) {
|
|
356
|
+
const st = await workflowStatus(process.cwd(), wfId);
|
|
357
|
+
if (st) {
|
|
358
|
+
wf = {
|
|
359
|
+
workflow_id: st.workflow_id,
|
|
360
|
+
budget_tokens: st.budget_tokens,
|
|
361
|
+
used_tokens: st.used_tokens,
|
|
362
|
+
pct: st.pct,
|
|
363
|
+
};
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
const rendered = renderSubagentFeedback(decideSubagentFeedback(input, { workflow: wf }));
|
|
367
|
+
if (rendered)
|
|
368
|
+
process.stdout.write(rendered);
|
|
369
|
+
}
|
|
345
370
|
});
|
|
346
371
|
return;
|
|
347
372
|
}
|
|
@@ -400,6 +425,16 @@ export async function main(cliArgs = process.argv.slice(2)) {
|
|
|
400
425
|
process.exit(code);
|
|
401
426
|
return;
|
|
402
427
|
}
|
|
428
|
+
case "install-statusline": {
|
|
429
|
+
// v0.42.0 — wire the additive statusline badge into
|
|
430
|
+
// ~/.claude/settings.json so users don't hand-edit JSON. Never
|
|
431
|
+
// clobbers a third-party statusLine (the sessionTitle lesson);
|
|
432
|
+
// upgrades caveman-only / tp-only to the chain wrapper.
|
|
433
|
+
const { handleInstallStatusline } = await import("./cli/install-statusline.js");
|
|
434
|
+
const code = await handleInstallStatusline(cliArgs.slice(1));
|
|
435
|
+
process.exit(code);
|
|
436
|
+
return;
|
|
437
|
+
}
|
|
403
438
|
case "migrate-hooks": {
|
|
404
439
|
// v0.33.0 — clean stale npx-cache / pinned-version token-pilot
|
|
405
440
|
// hook entries from user-level + project-level settings.json so
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "token-pilot",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.42.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",
|