token-pilot 0.27.0 → 0.28.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/hooks/hooks.json +18 -0
- package/.claude-plugin/marketplace.json +2 -2
- package/.claude-plugin/plugin.json +2 -3
- package/CHANGELOG.md +52 -0
- package/{dist/agents → agents}/tp-api-surface-tracker.md +1 -1
- package/{dist/agents → agents}/tp-audit-scanner.md +1 -1
- package/{dist/agents → agents}/tp-commit-writer.md +1 -1
- package/{dist/agents → agents}/tp-context-engineer.md +1 -1
- package/{dist/agents → agents}/tp-dead-code-finder.md +1 -1
- package/{dist/agents → agents}/tp-debugger.md +1 -1
- package/{dist/agents → agents}/tp-dep-health.md +1 -1
- package/{dist/agents → agents}/tp-doc-writer.md +1 -1
- package/{dist/agents → agents}/tp-history-explorer.md +1 -1
- package/{dist/agents → agents}/tp-impact-analyzer.md +1 -1
- package/{dist/agents → agents}/tp-incident-timeline.md +1 -1
- package/{dist/agents → agents}/tp-incremental-builder.md +1 -1
- package/{dist/agents → agents}/tp-migration-scout.md +1 -1
- package/{dist/agents → agents}/tp-onboard.md +1 -1
- package/{dist/agents → agents}/tp-performance-profiler.md +1 -1
- package/{dist/agents → agents}/tp-pr-reviewer.md +1 -1
- package/{dist/agents → agents}/tp-refactor-planner.md +1 -1
- package/{dist/agents → agents}/tp-review-impact.md +1 -1
- package/{dist/agents → agents}/tp-run.md +1 -1
- package/{dist/agents → agents}/tp-session-restorer.md +1 -1
- package/{dist/agents → agents}/tp-ship-coordinator.md +1 -1
- package/{dist/agents → agents}/tp-spec-writer.md +1 -1
- package/{dist/agents → agents}/tp-test-coverage-gapper.md +1 -1
- package/{dist/agents → agents}/tp-test-triage.md +1 -1
- package/{dist/agents → agents}/tp-test-writer.md +1 -1
- package/dist/cli/install-agents.d.ts +6 -4
- package/dist/cli/install-agents.js +8 -7
- package/dist/cli/typo-guard.d.ts +1 -1
- package/dist/cli/typo-guard.js +2 -0
- package/dist/hooks/installer.js +18 -0
- package/dist/hooks/pre-bash.d.ts +43 -0
- package/dist/hooks/pre-bash.js +112 -0
- package/dist/hooks/pre-grep.d.ts +59 -0
- package/dist/hooks/pre-grep.js +98 -0
- package/dist/index.js +38 -0
- package/package.json +2 -2
|
@@ -18,6 +18,24 @@
|
|
|
18
18
|
"command": "node ${CLAUDE_PLUGIN_ROOT}/dist/index.js hook-edit"
|
|
19
19
|
}
|
|
20
20
|
]
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
"matcher": "Bash",
|
|
24
|
+
"hooks": [
|
|
25
|
+
{
|
|
26
|
+
"type": "command",
|
|
27
|
+
"command": "node ${CLAUDE_PLUGIN_ROOT}/dist/index.js hook-pre-bash"
|
|
28
|
+
}
|
|
29
|
+
]
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
"matcher": "Grep",
|
|
33
|
+
"hooks": [
|
|
34
|
+
{
|
|
35
|
+
"type": "command",
|
|
36
|
+
"command": "node ${CLAUDE_PLUGIN_ROOT}/dist/index.js hook-pre-grep"
|
|
37
|
+
}
|
|
38
|
+
]
|
|
21
39
|
}
|
|
22
40
|
],
|
|
23
41
|
"SessionStart": [
|
|
@@ -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.28.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.28.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.28.0",
|
|
4
4
|
"description": "Saves 60-90% tokens when AI reads code. AST-aware lazy reading, symbol navigation, cross-session tool-usage analytics, 22 subagents (haiku/sonnet/opus-tiered) with budget watchdog.",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Digital-Threads",
|
|
@@ -26,6 +26,5 @@
|
|
|
26
26
|
]
|
|
27
27
|
}
|
|
28
28
|
},
|
|
29
|
-
"skills": "./skills/"
|
|
30
|
-
"agents": "./dist/agents/"
|
|
29
|
+
"skills": "./skills/"
|
|
31
30
|
}
|
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,58 @@ All notable changes to Token Pilot will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [0.28.0] - 2026-04-19
|
|
9
|
+
|
|
10
|
+
### Added — passive pre-intercept hooks for Grep and Bash
|
|
11
|
+
|
|
12
|
+
Field observation from the author's own session: over 12 hours of work on token-pilot, the main-thread agent called **zero** MCP code-reading tools. Everything went through raw `Read`, `Bash` (`awk`/`grep`), and `Edit`. Advisory hooks (PostToolUse `additionalContext`) didn't change the behaviour — by the time they fire, the big output is already in the context.
|
|
13
|
+
|
|
14
|
+
Fix: push enforcement upstream to `PreToolUse`. When the agent is about to invoke a heavy pattern, deny the call and suggest a cheaper MCP equivalent. This is the same lever the Read hook already uses — it's the one that actually works in production.
|
|
15
|
+
|
|
16
|
+
**`PreToolUse:Grep` (new matcher)** — denies Grep when the pattern looks like a code identifier (camelCase, PascalCase, snake_case, CONSTANT_CASE, kebab-case; length ≥4; no regex metacharacters). Suggests `mcp__token-pilot__find_usages(symbol=...)` — semantic search, 5-10× cheaper than line-oriented grep output. Regex-shaped patterns, short generic terms (`id`, `err`, `db`), and patterns with spaces still pass through unchanged.
|
|
17
|
+
|
|
18
|
+
**`PreToolUse:Bash` (new matcher)** — denies five heavy patterns and suggests the cheaper path:
|
|
19
|
+
|
|
20
|
+
| Pattern | Redirect |
|
|
21
|
+
|---|---|
|
|
22
|
+
| `grep -r` without `-m N` | `find_usages` or bounded grep |
|
|
23
|
+
| `find /` / `find ~` without `-maxdepth` | Glob tool or bounded find |
|
|
24
|
+
| `cat <code-file>` (TypeScript/Python/etc) | `smart_read` or `Read` with offset/limit |
|
|
25
|
+
| `git log` without `-n` / `--max-count` / `| head` | `smart_log` or bounded log |
|
|
26
|
+
| Bare `git diff` (no path, no `--stat`) | `smart_diff` or scoped diff |
|
|
27
|
+
|
|
28
|
+
Every deny message includes an explicit bypass instruction (`add -m N to re-run`, `use regex-shaped pattern`, etc.) so legitimate use-cases aren't blocked — just made deliberate.
|
|
29
|
+
|
|
30
|
+
**Hook installer + plugin manifest** now register all four `PreToolUse` matchers (Read, Edit, Bash, Grep). Per-matcher idempotence from v0.25.0 means existing users who re-run `install-hook` or reinstall the plugin pick up the new matchers without duplicates. 43 new unit tests on the pure decision logic.
|
|
31
|
+
|
|
32
|
+
### Why not truncate output post-factum
|
|
33
|
+
|
|
34
|
+
Investigated for v0.27 but Claude Code's `PostToolUse` can't modify `tool_response` for Bash — the `updatedMCPToolOutput` field is MCP-only, documented in our existing `post-bash.ts` comment. Blocking upfront is the only mechanism that actually saves tokens on heavy Bash / Grep calls.
|
|
35
|
+
|
|
36
|
+
### Noted
|
|
37
|
+
|
|
38
|
+
The author's session that motivated this release will be re-measured after v0.28.0 is published and the plugin reinstalled. If `find_usages` and `smart_*` adoption rises from 0 to double digits per session, we keep the aggressive default. If agents bypass via regex or `-m 1` to escape the block, we soften back to advisory.
|
|
39
|
+
|
|
40
|
+
1018 tests passing (+43 new).
|
|
41
|
+
|
|
42
|
+
## [0.27.1] - 2026-04-19
|
|
43
|
+
|
|
44
|
+
### Fixed — plugin install failed on v0.27.0 with "agents: Invalid input"
|
|
45
|
+
|
|
46
|
+
First field report after v0.27.0 hit npm: `claude plugin install token-pilot@token-pilot` failed with a schema validation error because `plugin.json` declared `"agents": "./dist/agents/"` — but `agents` is not a valid field in the Claude Code plugin-manifest schema. Agents are discovered by convention from `./agents/` at the repo root (the same way addyosmani/agent-skills ships 3 of them).
|
|
47
|
+
|
|
48
|
+
Fixed:
|
|
49
|
+
- Removed `"agents"` field from `plugin.json`. Only `"skills"` stays as an explicit path.
|
|
50
|
+
- Moved composed tp-* files from `dist/agents/` to repo-root `agents/` (Claude Code convention).
|
|
51
|
+
- Updated `scripts/build-agents.mjs` to write to `./agents/` by default.
|
|
52
|
+
- Updated `package.json` `files` to ship `agents/*.md` instead of `dist/agents/*.md`.
|
|
53
|
+
- Updated `src/cli/install-agents.ts` `resolveDistAgentsDir` to walk `dist/cli/../../agents/` for npm-installed users.
|
|
54
|
+
- `.gitignore`: removed the `!dist/agents/` exceptions; `agents/` at root is now versioned directly.
|
|
55
|
+
|
|
56
|
+
No behaviour change to agents themselves or any MCP tool. Pure path/schema fix so the plugin path actually works.
|
|
57
|
+
|
|
58
|
+
975 tests still passing.
|
|
59
|
+
|
|
8
60
|
## [0.27.0] - 2026-04-19
|
|
9
61
|
|
|
10
62
|
Big release motivated by Opus 4.7's +35% tokenizer tax over 4.6 — token savings no longer optional. Two interlocking moves.
|
|
@@ -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.28.0"
|
|
14
14
|
token_pilot_body_hash: ae0b86eaffaf34bf283b94b5572481fa8c2d6a2a25193f1173b70bef0fbe1919
|
|
15
15
|
---
|
|
16
16
|
|
|
@@ -47,10 +47,12 @@ export interface InstallResult {
|
|
|
47
47
|
}
|
|
48
48
|
export declare function installAgents(opts: InstallOptions): Promise<InstallResult>;
|
|
49
49
|
/**
|
|
50
|
-
* Resolve the
|
|
51
|
-
* entry. Works for both `npm run start`
|
|
52
|
-
* users
|
|
53
|
-
* agents
|
|
50
|
+
* Resolve the composed agents directory relative to the running
|
|
51
|
+
* `dist/index.js` entry. Works for both `npm run start` and `npm pack`-
|
|
52
|
+
* installed users. As of v0.27.1 the composed agents live at the repo-root
|
|
53
|
+
* `./agents/` (Claude Code plugin convention — see scripts/build-agents.mjs).
|
|
54
|
+
* The compiled JS lives at `dist/cli/install-agents.js`, so we walk two
|
|
55
|
+
* levels up to reach the repo/package root, then into `agents/`.
|
|
54
56
|
*/
|
|
55
57
|
export declare function resolveDistAgentsDir(scriptUrl: string): string;
|
|
56
58
|
/**
|
|
@@ -177,16 +177,17 @@ export async function installAgents(opts) {
|
|
|
177
177
|
}
|
|
178
178
|
// ─── CLI wrapper ─────────────────────────────────────────────────────────────
|
|
179
179
|
/**
|
|
180
|
-
* Resolve the
|
|
181
|
-
* entry. Works for both `npm run start`
|
|
182
|
-
* users
|
|
183
|
-
* agents
|
|
180
|
+
* Resolve the composed agents directory relative to the running
|
|
181
|
+
* `dist/index.js` entry. Works for both `npm run start` and `npm pack`-
|
|
182
|
+
* installed users. As of v0.27.1 the composed agents live at the repo-root
|
|
183
|
+
* `./agents/` (Claude Code plugin convention — see scripts/build-agents.mjs).
|
|
184
|
+
* The compiled JS lives at `dist/cli/install-agents.js`, so we walk two
|
|
185
|
+
* levels up to reach the repo/package root, then into `agents/`.
|
|
184
186
|
*/
|
|
185
187
|
export function resolveDistAgentsDir(scriptUrl) {
|
|
186
|
-
// Compiled layout: dist/cli/install-agents.js →
|
|
187
|
-
// One level up from our own file, then into agents/.
|
|
188
|
+
// Compiled layout: dist/cli/install-agents.js → ../../agents/
|
|
188
189
|
const here = dirname(fileURLToPath(scriptUrl));
|
|
189
|
-
return join(here, "..", "agents");
|
|
190
|
+
return join(here, "..", "..", "agents");
|
|
190
191
|
}
|
|
191
192
|
/** Read one line from an interactive TTY prompt. */
|
|
192
193
|
async function promptLine(question) {
|
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-post-bash", "hook-post-task", "hook-session-start", "install-hook", "uninstall-hook", "install-ast-index", "doctor", "bless-agents", "unbless-agents", "install-agents", "uninstall-agents", "stats", "tool-audit", "save-doc", "list-docs", "init", "--version", "-v", "--help", "-h"];
|
|
20
|
+
export declare const KNOWN_COMMANDS: readonly ["hook-read", "hook-edit", "hook-pre-bash", "hook-pre-grep", "hook-post-bash", "hook-post-task", "hook-session-start", "install-hook", "uninstall-hook", "install-ast-index", "doctor", "bless-agents", "unbless-agents", "install-agents", "uninstall-agents", "stats", "tool-audit", "save-doc", "list-docs", "init", "--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
package/dist/hooks/installer.js
CHANGED
|
@@ -34,6 +34,24 @@ function createHookConfig(options) {
|
|
|
34
34
|
},
|
|
35
35
|
],
|
|
36
36
|
},
|
|
37
|
+
{
|
|
38
|
+
matcher: "Bash",
|
|
39
|
+
hooks: [
|
|
40
|
+
{
|
|
41
|
+
type: "command",
|
|
42
|
+
command: buildHookCommand("hook-pre-bash", options),
|
|
43
|
+
},
|
|
44
|
+
],
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
matcher: "Grep",
|
|
48
|
+
hooks: [
|
|
49
|
+
{
|
|
50
|
+
type: "command",
|
|
51
|
+
command: buildHookCommand("hook-pre-grep", options),
|
|
52
|
+
},
|
|
53
|
+
],
|
|
54
|
+
},
|
|
37
55
|
],
|
|
38
56
|
SessionStart: [
|
|
39
57
|
{
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* v0.28.0 — PreToolUse:Bash advisor / blocker.
|
|
3
|
+
*
|
|
4
|
+
* Intercepts heavy Bash commands BEFORE they run and redirects the
|
|
5
|
+
* agent to cheaper alternatives. Why before? Claude Code's PostToolUse
|
|
6
|
+
* hook cannot truncate the Bash `tool_response` (verified; the
|
|
7
|
+
* updatedMCPToolOutput field is MCP-only). Post-factum advice means
|
|
8
|
+
* the full stdout already sits in the agent's context. We save real
|
|
9
|
+
* tokens only by refusing the heavy call up front.
|
|
10
|
+
*
|
|
11
|
+
* Patterns we block (in order, first match wins):
|
|
12
|
+
*
|
|
13
|
+
* 1. `grep -r <pattern>` → suggest find_usages
|
|
14
|
+
* 2. `find /` / `find ~` → suggest Glob or bounded find
|
|
15
|
+
* 3. `cat <code-file>` → suggest smart_read
|
|
16
|
+
* 4. `git log` without -n/-N → suggest smart_log
|
|
17
|
+
* 5. `git diff` without path → suggest smart_diff
|
|
18
|
+
*
|
|
19
|
+
* For anything not matching → allow. We err on the side of false
|
|
20
|
+
* negatives: a missed heavy command stays annoying (tokens wasted)
|
|
21
|
+
* but a false-positive block blocks legitimate work and erodes trust.
|
|
22
|
+
*
|
|
23
|
+
* Strictly lexical — we don't shell-parse. Users running `grep` inside
|
|
24
|
+
* `bash -c`, heredocs, or eval'd strings slip through. Acceptable for
|
|
25
|
+
* v0.28.0; tighten only if tool-audit shows repeated escapes.
|
|
26
|
+
*/
|
|
27
|
+
export interface PreBashInput {
|
|
28
|
+
tool_name?: string;
|
|
29
|
+
tool_input?: {
|
|
30
|
+
command?: string;
|
|
31
|
+
[k: string]: unknown;
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
export type PreBashDecision = {
|
|
35
|
+
kind: "allow";
|
|
36
|
+
} | {
|
|
37
|
+
kind: "deny";
|
|
38
|
+
reason: string;
|
|
39
|
+
};
|
|
40
|
+
export declare function detectHeavyPattern(command: string): PreBashDecision;
|
|
41
|
+
export declare function decidePreBash(input: PreBashInput): PreBashDecision;
|
|
42
|
+
export declare function renderPreBashOutput(decision: PreBashDecision): string | null;
|
|
43
|
+
//# sourceMappingURL=pre-bash.d.ts.map
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* v0.28.0 — PreToolUse:Bash advisor / blocker.
|
|
3
|
+
*
|
|
4
|
+
* Intercepts heavy Bash commands BEFORE they run and redirects the
|
|
5
|
+
* agent to cheaper alternatives. Why before? Claude Code's PostToolUse
|
|
6
|
+
* hook cannot truncate the Bash `tool_response` (verified; the
|
|
7
|
+
* updatedMCPToolOutput field is MCP-only). Post-factum advice means
|
|
8
|
+
* the full stdout already sits in the agent's context. We save real
|
|
9
|
+
* tokens only by refusing the heavy call up front.
|
|
10
|
+
*
|
|
11
|
+
* Patterns we block (in order, first match wins):
|
|
12
|
+
*
|
|
13
|
+
* 1. `grep -r <pattern>` → suggest find_usages
|
|
14
|
+
* 2. `find /` / `find ~` → suggest Glob or bounded find
|
|
15
|
+
* 3. `cat <code-file>` → suggest smart_read
|
|
16
|
+
* 4. `git log` without -n/-N → suggest smart_log
|
|
17
|
+
* 5. `git diff` without path → suggest smart_diff
|
|
18
|
+
*
|
|
19
|
+
* For anything not matching → allow. We err on the side of false
|
|
20
|
+
* negatives: a missed heavy command stays annoying (tokens wasted)
|
|
21
|
+
* but a false-positive block blocks legitimate work and erodes trust.
|
|
22
|
+
*
|
|
23
|
+
* Strictly lexical — we don't shell-parse. Users running `grep` inside
|
|
24
|
+
* `bash -c`, heredocs, or eval'd strings slip through. Acceptable for
|
|
25
|
+
* v0.28.0; tighten only if tool-audit shows repeated escapes.
|
|
26
|
+
*/
|
|
27
|
+
const CODE_EXT_RE = /\.(ts|tsx|js|jsx|mjs|cjs|py|rb|go|rs|java|kt|swift|php|cs|cpp|c|h|hpp|scala|clj|ex|exs|elm|ml|fs|dart|lua|sh|bash|zsh)(\s|$|;|\||&|>|<)/;
|
|
28
|
+
/** Check whether the command contains a specific utility invocation at
|
|
29
|
+
* top level (not inside a quoted string). Cheap lexical match. */
|
|
30
|
+
function invokes(command, utility) {
|
|
31
|
+
// Match `<utility> ` at start, after `; `, `&& `, `|| `, `| `, or newline.
|
|
32
|
+
const re = new RegExp(`(^|[;&|\\n]\\s*)${utility}(\\s|$)`, "m");
|
|
33
|
+
return re.test(command);
|
|
34
|
+
}
|
|
35
|
+
export function detectHeavyPattern(command) {
|
|
36
|
+
const cmd = command.trim();
|
|
37
|
+
if (!cmd)
|
|
38
|
+
return { kind: "allow" };
|
|
39
|
+
// 1. grep -r / grep -R without -m and with a bareword pattern
|
|
40
|
+
if (/\bgrep\s+[^|]*-[rR]\b/.test(cmd) && !/\s-m\s+\d+/.test(cmd)) {
|
|
41
|
+
return {
|
|
42
|
+
kind: "deny",
|
|
43
|
+
reason: "Recursive `grep -r` can dump huge output into your context. " +
|
|
44
|
+
"Use mcp__token-pilot__find_usages(symbol=...) for identifier searches " +
|
|
45
|
+
"(semantic, grouped by definition/import/usage), or add `-m 20` to " +
|
|
46
|
+
"bound the match count. Re-run through grep with `-m` to bypass.",
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
// 2. find / | find ~ | find . without bounds
|
|
50
|
+
if (/\bfind\s+(\/|~|\$HOME)/.test(cmd) && !/-maxdepth\s+\d+/.test(cmd)) {
|
|
51
|
+
return {
|
|
52
|
+
kind: "deny",
|
|
53
|
+
reason: "Unbounded `find /` walks the whole filesystem and dumps every match. " +
|
|
54
|
+
"Use the Glob tool for pattern matching, or add `-maxdepth N -type f -name <glob>` " +
|
|
55
|
+
"to bound the walk. Re-run with `-maxdepth` to bypass.",
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
// 3. cat <code-file> at top level
|
|
59
|
+
if (invokes(cmd, "cat") && CODE_EXT_RE.test(cmd) && !cmd.includes("|")) {
|
|
60
|
+
return {
|
|
61
|
+
kind: "deny",
|
|
62
|
+
reason: "`cat` on a code file dumps the whole thing into context. " +
|
|
63
|
+
"Use mcp__token-pilot__smart_read(path) for a structural overview, " +
|
|
64
|
+
"or Read(path, offset, limit) for a bounded slice. " +
|
|
65
|
+
"For head/tail access use `head -n N` or `tail -n N`.",
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
// 4. git log without -n / -N
|
|
69
|
+
if (invokes(cmd, "git") &&
|
|
70
|
+
/\bgit\s+log\b/.test(cmd) &&
|
|
71
|
+
!/-n\s*\d+|-N\s*\d+|--max-count=\d+/.test(cmd) &&
|
|
72
|
+
!/\|\s*head/.test(cmd)) {
|
|
73
|
+
return {
|
|
74
|
+
kind: "deny",
|
|
75
|
+
reason: "Unbounded `git log` can return thousands of commits. " +
|
|
76
|
+
"Use mcp__token-pilot__smart_log for structured history, or add " +
|
|
77
|
+
"`-n 20` / `| head -20` to bound. Re-run with a limit to bypass.",
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
// 5. git diff with no path argument (common mistake on large repos)
|
|
81
|
+
if (/\bgit\s+diff\b/.test(cmd) &&
|
|
82
|
+
!/\bgit\s+diff\s+[^\s|]*--stat/.test(cmd) &&
|
|
83
|
+
/\bgit\s+diff\s*($|[|;&])/.test(cmd)) {
|
|
84
|
+
return {
|
|
85
|
+
kind: "deny",
|
|
86
|
+
reason: "Bare `git diff` on a big working tree is huge. " +
|
|
87
|
+
"Use mcp__token-pilot__smart_diff for per-symbol change summary, " +
|
|
88
|
+
"or `git diff --stat` / `git diff <path>` to scope. Re-run scoped to bypass.",
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
return { kind: "allow" };
|
|
92
|
+
}
|
|
93
|
+
export function decidePreBash(input) {
|
|
94
|
+
if (input.tool_name !== "Bash")
|
|
95
|
+
return { kind: "allow" };
|
|
96
|
+
const cmd = input.tool_input?.command;
|
|
97
|
+
if (typeof cmd !== "string")
|
|
98
|
+
return { kind: "allow" };
|
|
99
|
+
return detectHeavyPattern(cmd);
|
|
100
|
+
}
|
|
101
|
+
export function renderPreBashOutput(decision) {
|
|
102
|
+
if (decision.kind === "allow")
|
|
103
|
+
return null;
|
|
104
|
+
return JSON.stringify({
|
|
105
|
+
hookSpecificOutput: {
|
|
106
|
+
hookEventName: "PreToolUse",
|
|
107
|
+
permissionDecision: "deny",
|
|
108
|
+
permissionDecisionReason: decision.reason,
|
|
109
|
+
},
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
//# sourceMappingURL=pre-bash.js.map
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* v0.28.0 — PreToolUse:Grep advisor / blocker.
|
|
3
|
+
*
|
|
4
|
+
* When the main-thread agent uses Grep to search for a symbol-like
|
|
5
|
+
* identifier (camelCase, snake_case, PascalCase, ≥4 chars, no regex
|
|
6
|
+
* metacharacters) we suggest `mcp__token-pilot__find_usages` instead.
|
|
7
|
+
* The MCP tool does semantic search — groups results into definitions /
|
|
8
|
+
* imports / usages — and for real symbols is 5-10× cheaper than Grep's
|
|
9
|
+
* line-oriented output.
|
|
10
|
+
*
|
|
11
|
+
* For anything that LOOKS like regex (special chars, quantifiers,
|
|
12
|
+
* alternation) or short generic terms (≤3 chars) we allow Grep through
|
|
13
|
+
* unchanged. Those are cases where semantic search doesn't apply or
|
|
14
|
+
* Grep is genuinely cheaper.
|
|
15
|
+
*
|
|
16
|
+
* This is strictly advisory in the v0.28.0 first pass: we emit
|
|
17
|
+
* permissionDecision: "deny" so the agent sees the suggestion on the
|
|
18
|
+
* first attempt, but the user mandate is passive-as-possible — if
|
|
19
|
+
* adoption data (tool-audit) shows agents actually re-run through
|
|
20
|
+
* find_usages after the block, we keep it. If they bypass via `-E` or
|
|
21
|
+
* raw shell, we soften to advisory.
|
|
22
|
+
*/
|
|
23
|
+
export interface PreGrepInput {
|
|
24
|
+
tool_name?: string;
|
|
25
|
+
tool_input?: {
|
|
26
|
+
pattern?: string;
|
|
27
|
+
path?: string;
|
|
28
|
+
type?: string;
|
|
29
|
+
[k: string]: unknown;
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
export type PreGrepDecision = {
|
|
33
|
+
kind: "allow";
|
|
34
|
+
} | {
|
|
35
|
+
kind: "deny";
|
|
36
|
+
reason: string;
|
|
37
|
+
};
|
|
38
|
+
/**
|
|
39
|
+
* Heuristic: does `pattern` look like a code identifier worth sending
|
|
40
|
+
* through find_usages?
|
|
41
|
+
*
|
|
42
|
+
* - Length ≥ 4 (avoid `id`, `err`, `db` — Grep wins there)
|
|
43
|
+
* - No regex metacharacters (`.` `*` `+` `?` `|` `(` `)` `[` `]`
|
|
44
|
+
* `{` `}` `^` `$` `\`) — if present we assume real regex
|
|
45
|
+
* - Not purely lowercase words (those look like prose search)
|
|
46
|
+
* - Matches `camelCase`, `PascalCase`, `snake_case`, `CONSTANT_CASE`,
|
|
47
|
+
* or `kebab-case` shapes
|
|
48
|
+
*/
|
|
49
|
+
export declare function isSymbolLikePattern(pattern: string): boolean;
|
|
50
|
+
/**
|
|
51
|
+
* Pure decision function. Given a PreToolUse hook input for Grep,
|
|
52
|
+
* return whether to allow or deny (with a suggestion).
|
|
53
|
+
*/
|
|
54
|
+
export declare function decidePreGrep(input: PreGrepInput): PreGrepDecision;
|
|
55
|
+
/**
|
|
56
|
+
* Render the Claude Code hook JSON response.
|
|
57
|
+
*/
|
|
58
|
+
export declare function renderPreGrepOutput(decision: PreGrepDecision): string | null;
|
|
59
|
+
//# sourceMappingURL=pre-grep.d.ts.map
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* v0.28.0 — PreToolUse:Grep advisor / blocker.
|
|
3
|
+
*
|
|
4
|
+
* When the main-thread agent uses Grep to search for a symbol-like
|
|
5
|
+
* identifier (camelCase, snake_case, PascalCase, ≥4 chars, no regex
|
|
6
|
+
* metacharacters) we suggest `mcp__token-pilot__find_usages` instead.
|
|
7
|
+
* The MCP tool does semantic search — groups results into definitions /
|
|
8
|
+
* imports / usages — and for real symbols is 5-10× cheaper than Grep's
|
|
9
|
+
* line-oriented output.
|
|
10
|
+
*
|
|
11
|
+
* For anything that LOOKS like regex (special chars, quantifiers,
|
|
12
|
+
* alternation) or short generic terms (≤3 chars) we allow Grep through
|
|
13
|
+
* unchanged. Those are cases where semantic search doesn't apply or
|
|
14
|
+
* Grep is genuinely cheaper.
|
|
15
|
+
*
|
|
16
|
+
* This is strictly advisory in the v0.28.0 first pass: we emit
|
|
17
|
+
* permissionDecision: "deny" so the agent sees the suggestion on the
|
|
18
|
+
* first attempt, but the user mandate is passive-as-possible — if
|
|
19
|
+
* adoption data (tool-audit) shows agents actually re-run through
|
|
20
|
+
* find_usages after the block, we keep it. If they bypass via `-E` or
|
|
21
|
+
* raw shell, we soften to advisory.
|
|
22
|
+
*/
|
|
23
|
+
/**
|
|
24
|
+
* Heuristic: does `pattern` look like a code identifier worth sending
|
|
25
|
+
* through find_usages?
|
|
26
|
+
*
|
|
27
|
+
* - Length ≥ 4 (avoid `id`, `err`, `db` — Grep wins there)
|
|
28
|
+
* - No regex metacharacters (`.` `*` `+` `?` `|` `(` `)` `[` `]`
|
|
29
|
+
* `{` `}` `^` `$` `\`) — if present we assume real regex
|
|
30
|
+
* - Not purely lowercase words (those look like prose search)
|
|
31
|
+
* - Matches `camelCase`, `PascalCase`, `snake_case`, `CONSTANT_CASE`,
|
|
32
|
+
* or `kebab-case` shapes
|
|
33
|
+
*/
|
|
34
|
+
export function isSymbolLikePattern(pattern) {
|
|
35
|
+
if (pattern.length < 4)
|
|
36
|
+
return false;
|
|
37
|
+
// Regex metacharacters — if any present, assume user means regex.
|
|
38
|
+
if (/[.*+?|()[\]{}^$\\]/.test(pattern))
|
|
39
|
+
return false;
|
|
40
|
+
// Spaces or control chars — not a single symbol.
|
|
41
|
+
if (/\s/.test(pattern))
|
|
42
|
+
return false;
|
|
43
|
+
// Must contain at least one letter (so "12345" or "-->" don't trip).
|
|
44
|
+
if (!/[a-zA-Z]/.test(pattern))
|
|
45
|
+
return false;
|
|
46
|
+
// Shapes we consider symbol-like:
|
|
47
|
+
// camelCase → foo(Bar)+
|
|
48
|
+
// PascalCase → (Foo)+
|
|
49
|
+
// snake_case → at least one underscore
|
|
50
|
+
// CONSTANT_CASE → all upper with underscore
|
|
51
|
+
// kebab-case → at least one hyphen
|
|
52
|
+
const hasUpperInMiddle = /[a-z][A-Z]/.test(pattern);
|
|
53
|
+
const hasUnderscore = /_/.test(pattern);
|
|
54
|
+
const hasHyphen = /-/.test(pattern);
|
|
55
|
+
const isPureUppercase = /^[A-Z][A-Z0-9]+$/.test(pattern);
|
|
56
|
+
const isPascalCase = /^[A-Z][a-zA-Z0-9]+$/.test(pattern);
|
|
57
|
+
return (hasUpperInMiddle ||
|
|
58
|
+
hasUnderscore ||
|
|
59
|
+
hasHyphen ||
|
|
60
|
+
isPureUppercase ||
|
|
61
|
+
isPascalCase);
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Pure decision function. Given a PreToolUse hook input for Grep,
|
|
65
|
+
* return whether to allow or deny (with a suggestion).
|
|
66
|
+
*/
|
|
67
|
+
export function decidePreGrep(input) {
|
|
68
|
+
if (input.tool_name !== "Grep")
|
|
69
|
+
return { kind: "allow" };
|
|
70
|
+
const pattern = input.tool_input?.pattern;
|
|
71
|
+
if (typeof pattern !== "string" || pattern.length === 0) {
|
|
72
|
+
return { kind: "allow" };
|
|
73
|
+
}
|
|
74
|
+
if (!isSymbolLikePattern(pattern))
|
|
75
|
+
return { kind: "allow" };
|
|
76
|
+
const reason = `Grep pattern "${pattern}" looks like a code identifier. ` +
|
|
77
|
+
`Use mcp__token-pilot__find_usages(symbol="${pattern}") for semantic ` +
|
|
78
|
+
`search — groups results into definitions / imports / usages, typically ` +
|
|
79
|
+
`5-10× cheaper than Grep's line-oriented output. ` +
|
|
80
|
+
`If you really need a raw text search (regex, comment hunt, string ` +
|
|
81
|
+
`literal) re-run Grep with -E or a regex-shaped pattern to bypass.`;
|
|
82
|
+
return { kind: "deny", reason };
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Render the Claude Code hook JSON response.
|
|
86
|
+
*/
|
|
87
|
+
export function renderPreGrepOutput(decision) {
|
|
88
|
+
if (decision.kind === "allow")
|
|
89
|
+
return null;
|
|
90
|
+
return JSON.stringify({
|
|
91
|
+
hookSpecificOutput: {
|
|
92
|
+
hookEventName: "PreToolUse",
|
|
93
|
+
permissionDecision: "deny",
|
|
94
|
+
permissionDecisionReason: decision.reason,
|
|
95
|
+
},
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
//# sourceMappingURL=pre-grep.js.map
|
package/dist/index.js
CHANGED
|
@@ -50,6 +50,8 @@ import { runClaudeCodeEnvCheck } from "./cli/doctor-env-check.js";
|
|
|
50
50
|
import { claudeIgnoreStatus, writeDefaultClaudeIgnore, } from "./cli/claudeignore.js";
|
|
51
51
|
import { assessClaudeMd } from "./cli/claudemd-hygiene.js";
|
|
52
52
|
import { decidePostBashAdvice, renderPostBashHookOutput, } from "./hooks/post-bash.js";
|
|
53
|
+
import { decidePreBash, renderPreBashOutput } from "./hooks/pre-bash.js";
|
|
54
|
+
import { decidePreGrep, renderPreGrepOutput } from "./hooks/pre-grep.js";
|
|
53
55
|
const execFileAsync = promisify(execFile);
|
|
54
56
|
export const CODE_EXTENSIONS = new Set([
|
|
55
57
|
"ts",
|
|
@@ -142,6 +144,42 @@ export async function main(cliArgs = process.argv.slice(2)) {
|
|
|
142
144
|
process.exit(0);
|
|
143
145
|
return;
|
|
144
146
|
}
|
|
147
|
+
case "hook-pre-bash": {
|
|
148
|
+
// v0.28.0 — passive pre-intercept for heavy Bash commands.
|
|
149
|
+
// PostToolUse can't truncate tool_response (API limit), so the only
|
|
150
|
+
// way to actually save tokens is to deny the call up front and
|
|
151
|
+
// nudge the agent to the cheaper MCP equivalent.
|
|
152
|
+
try {
|
|
153
|
+
const stdin = readFileSync(0, "utf-8");
|
|
154
|
+
const input = JSON.parse(stdin);
|
|
155
|
+
const decision = decidePreBash(input);
|
|
156
|
+
const rendered = renderPreBashOutput(decision);
|
|
157
|
+
if (rendered)
|
|
158
|
+
process.stdout.write(rendered);
|
|
159
|
+
}
|
|
160
|
+
catch {
|
|
161
|
+
/* silent — hook must not break */
|
|
162
|
+
}
|
|
163
|
+
process.exit(0);
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
case "hook-pre-grep": {
|
|
167
|
+
// v0.28.0 — passive pre-intercept for symbol-like Grep patterns.
|
|
168
|
+
// Redirects identifier searches to mcp__token-pilot__find_usages.
|
|
169
|
+
try {
|
|
170
|
+
const stdin = readFileSync(0, "utf-8");
|
|
171
|
+
const input = JSON.parse(stdin);
|
|
172
|
+
const decision = decidePreGrep(input);
|
|
173
|
+
const rendered = renderPreGrepOutput(decision);
|
|
174
|
+
if (rendered)
|
|
175
|
+
process.stdout.write(rendered);
|
|
176
|
+
}
|
|
177
|
+
catch {
|
|
178
|
+
/* silent — hook must not break */
|
|
179
|
+
}
|
|
180
|
+
process.exit(0);
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
145
183
|
case "hook-post-task": {
|
|
146
184
|
try {
|
|
147
185
|
const stdin = readFileSync(0, "utf-8");
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "token-pilot",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.28.0",
|
|
4
4
|
"description": "Save up to 80% tokens when AI reads code \u2014 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",
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
"files": [
|
|
11
11
|
"dist/**/*.js",
|
|
12
12
|
"dist/**/*.d.ts",
|
|
13
|
-
"
|
|
13
|
+
"agents/*.md",
|
|
14
14
|
"docs/*.md",
|
|
15
15
|
"scripts/postinstall.mjs",
|
|
16
16
|
"start.sh",
|