token-pilot 0.19.2 → 0.23.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.
Files changed (96) hide show
  1. package/.claude-plugin/hooks/hooks.json +30 -0
  2. package/.claude-plugin/plugin.json +2 -2
  3. package/CHANGELOG.md +165 -0
  4. package/README.md +194 -313
  5. package/dist/agents/tp-audit-scanner.md +49 -0
  6. package/dist/agents/tp-commit-writer.md +41 -0
  7. package/dist/agents/tp-dead-code-finder.md +43 -0
  8. package/dist/agents/tp-debugger.md +45 -0
  9. package/dist/agents/tp-history-explorer.md +43 -0
  10. package/dist/agents/tp-impact-analyzer.md +44 -0
  11. package/dist/agents/tp-migration-scout.md +43 -0
  12. package/dist/agents/tp-onboard.md +40 -0
  13. package/dist/agents/tp-pr-reviewer.md +41 -0
  14. package/dist/agents/tp-refactor-planner.md +42 -0
  15. package/dist/agents/tp-run.md +48 -0
  16. package/dist/agents/tp-session-restorer.md +47 -0
  17. package/dist/agents/tp-test-triage.md +40 -0
  18. package/dist/agents/tp-test-writer.md +46 -0
  19. package/dist/cli/agent-frontmatter.d.ts +48 -0
  20. package/dist/cli/agent-frontmatter.js +189 -0
  21. package/dist/cli/bless-agents.d.ts +65 -0
  22. package/dist/cli/bless-agents.js +307 -0
  23. package/dist/cli/claudeignore.d.ts +33 -0
  24. package/dist/cli/claudeignore.js +88 -0
  25. package/dist/cli/claudemd-hygiene.d.ts +26 -0
  26. package/dist/cli/claudemd-hygiene.js +43 -0
  27. package/dist/cli/doctor-drift.d.ts +31 -0
  28. package/dist/cli/doctor-drift.js +130 -0
  29. package/dist/cli/doctor-env-check.d.ts +25 -0
  30. package/dist/cli/doctor-env-check.js +91 -0
  31. package/dist/cli/install-agents.d.ts +108 -0
  32. package/dist/cli/install-agents.js +402 -0
  33. package/dist/cli/save-doc.d.ts +42 -0
  34. package/dist/cli/save-doc.js +145 -0
  35. package/dist/cli/scan-agents.d.ts +46 -0
  36. package/dist/cli/scan-agents.js +227 -0
  37. package/dist/cli/stats.d.ts +36 -0
  38. package/dist/cli/stats.js +131 -0
  39. package/dist/cli/typo-guard.d.ts +27 -0
  40. package/dist/cli/typo-guard.js +119 -0
  41. package/dist/cli/unbless-agents.d.ts +33 -0
  42. package/dist/cli/unbless-agents.js +85 -0
  43. package/dist/cli/uninstall-agents.d.ts +36 -0
  44. package/dist/cli/uninstall-agents.js +117 -0
  45. package/dist/config/defaults.d.ts +1 -1
  46. package/dist/config/defaults.js +14 -8
  47. package/dist/config/loader.d.ts +1 -1
  48. package/dist/config/loader.js +105 -11
  49. package/dist/core/context-registry.d.ts +16 -1
  50. package/dist/core/context-registry.js +60 -28
  51. package/dist/core/event-log.d.ts +79 -0
  52. package/dist/core/event-log.js +190 -0
  53. package/dist/core/session-registry.d.ts +43 -0
  54. package/dist/core/session-registry.js +113 -0
  55. package/dist/core/session-savings.d.ts +19 -0
  56. package/dist/core/session-savings.js +60 -0
  57. package/dist/handlers/session-budget.d.ts +32 -0
  58. package/dist/handlers/session-budget.js +61 -0
  59. package/dist/handlers/session-snapshot-persist.d.ts +22 -0
  60. package/dist/handlers/session-snapshot-persist.js +76 -0
  61. package/dist/hooks/adaptive-threshold.d.ts +27 -0
  62. package/dist/hooks/adaptive-threshold.js +46 -0
  63. package/dist/hooks/format-deny-message.d.ts +21 -0
  64. package/dist/hooks/format-deny-message.js +147 -0
  65. package/dist/hooks/installer.js +130 -31
  66. package/dist/hooks/path-safety.d.ts +16 -0
  67. package/dist/hooks/path-safety.js +34 -0
  68. package/dist/hooks/post-bash.d.ts +46 -0
  69. package/dist/hooks/post-bash.js +77 -0
  70. package/dist/hooks/post-task.d.ts +67 -0
  71. package/dist/hooks/post-task.js +136 -0
  72. package/dist/hooks/session-start.d.ts +45 -0
  73. package/dist/hooks/session-start.js +179 -0
  74. package/dist/hooks/summary-ast-index.d.ts +28 -0
  75. package/dist/hooks/summary-ast-index.js +122 -0
  76. package/dist/hooks/summary-head-tail.d.ts +15 -0
  77. package/dist/hooks/summary-head-tail.js +78 -0
  78. package/dist/hooks/summary-pipeline.d.ts +35 -0
  79. package/dist/hooks/summary-pipeline.js +63 -0
  80. package/dist/hooks/summary-regex.d.ts +14 -0
  81. package/dist/hooks/summary-regex.js +130 -0
  82. package/dist/hooks/summary-types.d.ts +29 -0
  83. package/dist/hooks/summary-types.js +9 -0
  84. package/dist/index.d.ts +15 -3
  85. package/dist/index.js +538 -149
  86. package/dist/integration/context-mode-detector.d.ts +7 -1
  87. package/dist/integration/context-mode-detector.js +51 -15
  88. package/dist/server/tool-definitions.d.ts +149 -0
  89. package/dist/server/tool-definitions.js +424 -202
  90. package/dist/server.d.ts +1 -1
  91. package/dist/server.js +456 -179
  92. package/dist/templates/agent-builder.d.ts +49 -0
  93. package/dist/templates/agent-builder.js +104 -0
  94. package/dist/types.d.ts +38 -4
  95. package/package.json +4 -2
  96. package/skills/stats/SKILL.md +13 -2
package/dist/index.js CHANGED
@@ -1,65 +1,230 @@
1
1
  #!/usr/bin/env node
2
- import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
3
- import { readFileSync, realpathSync, appendFileSync, mkdirSync } from 'node:fs';
4
- import { join } from 'node:path';
5
- import { execFile } from 'node:child_process';
6
- import { promisify } from 'node:util';
7
- import { fileURLToPath } from 'node:url';
8
- import { createServer } from './server.js';
9
- import { installHook, uninstallHook } from './hooks/installer.js';
10
- import { findBinary, installBinary, checkBinaryUpdate, isNewerVersion } from './ast-index/binary-manager.js';
11
- import { loadConfig } from './config/loader.js';
12
- import { isDangerousRoot } from './core/validation.js';
2
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
3
+ import { readFileSync, realpathSync, appendFileSync, mkdirSync } from "node:fs";
4
+ import { join } from "node:path";
5
+ import { homedir } from "node:os";
6
+ import { execFile } from "node:child_process";
7
+ import { promisify } from "node:util";
8
+ import { fileURLToPath } from "node:url";
9
+ import { createServer } from "./server.js";
10
+ import { installHook, uninstallHook } from "./hooks/installer.js";
11
+ import { findBinary, installBinary, checkBinaryUpdate, isNewerVersion, } from "./ast-index/binary-manager.js";
12
+ import { loadConfig } from "./config/loader.js";
13
+ import { isDangerousRoot } from "./core/validation.js";
14
+ import { runSummaryPipeline } from "./hooks/summary-pipeline.js";
15
+ import { formatDenyMessage } from "./hooks/format-deny-message.js";
16
+ import { isPathWithinProject } from "./hooks/path-safety.js";
17
+ import { handleSessionStart } from "./hooks/session-start.js";
18
+ import { computeEffectiveThreshold } from "./hooks/adaptive-threshold.js";
19
+ import { loadSessionSavedTokens } from "./core/session-savings.js";
20
+ import { handleSaveDocCli, handleListDocsCli } from "./cli/save-doc.js";
21
+ import { checkForTypo } from "./cli/typo-guard.js";
22
+ import { processPostTask } from "./hooks/post-task.js";
23
+ import { isContextModeInstalledSync } from "./integration/context-mode-detector.js";
24
+ import { handleBlessAgents } from "./cli/bless-agents.js";
25
+ import { unblessAgents } from "./cli/unbless-agents.js";
26
+ import { detectDrift, formatDriftFinding } from "./cli/doctor-drift.js";
27
+ import { handleInstallAgents, maybeEmitStartupReminder, } from "./cli/install-agents.js";
28
+ import { handleUninstallAgents } from "./cli/uninstall-agents.js";
29
+ import { appendEvent, applyRetention, } from "./core/event-log.js";
30
+ import { handleStats } from "./cli/stats.js";
31
+ import { promptYesNo } from "./cli/install-agents.js";
32
+ import { runClaudeCodeEnvCheck } from "./cli/doctor-env-check.js";
33
+ import { claudeIgnoreStatus, writeDefaultClaudeIgnore, } from "./cli/claudeignore.js";
34
+ import { assessClaudeMd } from "./cli/claudemd-hygiene.js";
35
+ import { decidePostBashAdvice, renderPostBashHookOutput, } from "./hooks/post-bash.js";
13
36
  const execFileAsync = promisify(execFile);
14
37
  export const CODE_EXTENSIONS = new Set([
15
- 'ts', 'tsx', 'js', 'jsx', 'mjs', 'py', 'go', 'rs', 'java', 'kt', 'kts',
16
- 'swift', 'cs', 'cpp', 'cc', 'cxx', 'hpp', 'c', 'h', 'php', 'rb', 'scala',
17
- 'dart', 'lua', 'sh', 'bash', 'sql', 'r', 'vue', 'svelte', 'pl', 'pm',
18
- 'ex', 'exs', 'groovy', 'm', 'proto', 'bsl',
19
- 'lisp', 'lsp', 'cl', 'asd',
38
+ "ts",
39
+ "tsx",
40
+ "js",
41
+ "jsx",
42
+ "mjs",
43
+ "py",
44
+ "go",
45
+ "rs",
46
+ "java",
47
+ "kt",
48
+ "kts",
49
+ "swift",
50
+ "cs",
51
+ "cpp",
52
+ "cc",
53
+ "cxx",
54
+ "hpp",
55
+ "c",
56
+ "h",
57
+ "php",
58
+ "rb",
59
+ "scala",
60
+ "dart",
61
+ "lua",
62
+ "sh",
63
+ "bash",
64
+ "sql",
65
+ "r",
66
+ "vue",
67
+ "svelte",
68
+ "pl",
69
+ "pm",
70
+ "ex",
71
+ "exs",
72
+ "groovy",
73
+ "m",
74
+ "proto",
75
+ "bsl",
76
+ "lisp",
77
+ "lsp",
78
+ "cl",
79
+ "asd",
20
80
  ]);
21
81
  export function getVersion() {
22
82
  try {
23
- const pkgPath = new URL('../package.json', import.meta.url).pathname;
24
- const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'));
83
+ const pkgPath = new URL("../package.json", import.meta.url).pathname;
84
+ const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
25
85
  return pkg.version;
26
86
  }
27
87
  catch {
28
- return '0.0.0';
88
+ return "0.0.0";
29
89
  }
30
90
  }
31
91
  export async function main(cliArgs = process.argv.slice(2)) {
92
+ // Guard against mis-typed commands like `install-aents` silently
93
+ // becoming a projectRoot=install-aents server launch. See TP-v0.22.3.
94
+ const typo = checkForTypo(cliArgs[0]);
95
+ if (typo.kind === "typo") {
96
+ process.stderr.write(`[token-pilot] ${typo.message}\n`);
97
+ process.exit(1);
98
+ }
32
99
  switch (cliArgs[0]) {
33
- case 'hook-read': {
100
+ case "hook-read": {
34
101
  const cfg = await loadConfig(process.cwd());
35
- handleHookRead(cliArgs[1], cfg.hooks.denyThreshold);
102
+ await handleHookRead(cliArgs[1], cfg.hooks.mode, cfg.hooks.denyThreshold, process.cwd(), {
103
+ adaptiveThreshold: cfg.hooks.adaptiveThreshold,
104
+ adaptiveBudgetTokens: cfg.hooks.adaptiveBudgetTokens,
105
+ });
36
106
  return;
37
107
  }
38
- case 'hook-edit':
108
+ case "hook-edit":
39
109
  handleHookEdit();
40
110
  return;
41
- case 'install-hook':
111
+ case "hook-post-bash": {
112
+ try {
113
+ const stdin = readFileSync(0, "utf-8");
114
+ const input = JSON.parse(stdin);
115
+ const advice = decidePostBashAdvice(input, {
116
+ contextModeAvailable: isContextModeInstalledSync(process.cwd()),
117
+ });
118
+ const rendered = renderPostBashHookOutput(advice);
119
+ if (rendered)
120
+ process.stdout.write(rendered);
121
+ }
122
+ catch {
123
+ /* silent — hook must not break */
124
+ }
125
+ process.exit(0);
126
+ return;
127
+ }
128
+ case "hook-post-task": {
129
+ try {
130
+ const stdin = readFileSync(0, "utf-8");
131
+ const input = JSON.parse(stdin);
132
+ const message = await processPostTask(process.cwd(), homedir(), input);
133
+ if (message) {
134
+ process.stdout.write(JSON.stringify({
135
+ hookSpecificOutput: {
136
+ hookEventName: "PostToolUse",
137
+ additionalContext: `[token-pilot] ${message}`,
138
+ },
139
+ }));
140
+ }
141
+ }
142
+ catch {
143
+ /* silent — hook must not break */
144
+ }
145
+ process.exit(0);
146
+ return;
147
+ }
148
+ case "hook-session-start": {
149
+ const cfg = await loadConfig(process.cwd());
150
+ // `sessionStart.enabled` is independent of `hooks.mode` by design —
151
+ // a user may want the Read-blocking hook off (mode:"off") while still
152
+ // getting the tool-rules reminder at session start, or vice versa.
153
+ if (!cfg.sessionStart.enabled) {
154
+ process.exit(0);
155
+ }
156
+ const result = await handleSessionStart({
157
+ projectRoot: process.cwd(),
158
+ homeDir: homedir(),
159
+ sessionStartConfig: cfg.sessionStart,
160
+ });
161
+ if (result) {
162
+ process.stdout.write(result);
163
+ }
164
+ process.exit(0);
165
+ return;
166
+ }
167
+ case "install-hook":
42
168
  await handleInstallHook(cliArgs[1] || process.cwd());
43
169
  return;
44
- case 'uninstall-hook':
170
+ case "uninstall-hook":
45
171
  await handleUninstallHook(cliArgs[1] || process.cwd());
46
172
  return;
47
- case 'install-ast-index':
173
+ case "install-ast-index":
48
174
  await handleInstallAstIndex();
49
175
  return;
50
- case 'doctor':
176
+ case "doctor":
51
177
  await handleDoctor();
52
178
  return;
53
- case 'init':
179
+ case "bless-agents":
180
+ await handleBlessAgents(cliArgs.slice(1));
181
+ return;
182
+ case "unbless-agents": {
183
+ const args = cliArgs.slice(1);
184
+ const all = args.includes("--all");
185
+ const names = args.filter((a) => !a.startsWith("--"));
186
+ if (!all && names.length === 0) {
187
+ process.stderr.write("Usage: token-pilot unbless-agents <name>... | --all\n");
188
+ process.exit(1);
189
+ }
190
+ await unblessAgents({ projectRoot: process.cwd(), names, all });
191
+ return;
192
+ }
193
+ case "install-agents": {
194
+ const code = await handleInstallAgents(cliArgs.slice(1));
195
+ process.exit(code);
196
+ return;
197
+ }
198
+ case "uninstall-agents": {
199
+ const code = await handleUninstallAgents(cliArgs.slice(1));
200
+ process.exit(code);
201
+ return;
202
+ }
203
+ case "stats": {
204
+ const code = await handleStats(cliArgs.slice(1));
205
+ process.exit(code);
206
+ return;
207
+ }
208
+ case "save-doc": {
209
+ const code = await handleSaveDocCli(cliArgs.slice(1));
210
+ process.exit(code);
211
+ return;
212
+ }
213
+ case "list-docs": {
214
+ const code = await handleListDocsCli();
215
+ process.exit(code);
216
+ return;
217
+ }
218
+ case "init":
54
219
  await handleInit(cliArgs[1] || process.cwd());
55
220
  return;
56
- case '--version':
57
- case '-v':
221
+ case "--version":
222
+ case "-v":
58
223
  console.log(getVersion());
59
224
  process.exit(0);
60
225
  return;
61
- case '--help':
62
- case '-h':
226
+ case "--help":
227
+ case "-h":
63
228
  printHelp();
64
229
  return;
65
230
  default:
@@ -76,20 +241,20 @@ export async function startServer(cliArgs = process.argv.slice(2)) {
76
241
  process.env.INIT_CWD, // npm/npx sets this to invoking directory
77
242
  process.env.PWD, // shell working directory (may differ from cwd)
78
243
  process.cwd(), // Node.js working directory
79
- ].filter((c) => !!c && c !== '/');
244
+ ].filter((c) => !!c && c !== "/");
80
245
  let detected = false;
81
246
  for (const candidate of candidates) {
82
247
  if (isDangerousRoot(candidate))
83
248
  continue;
84
249
  try {
85
- const { stdout } = await execFileAsync('git', ['rev-parse', '--show-toplevel'], {
250
+ const { stdout } = await execFileAsync("git", ["rev-parse", "--show-toplevel"], {
86
251
  cwd: candidate,
87
252
  timeout: 3000,
88
253
  });
89
254
  const gitRoot = stdout.trim();
90
255
  if (gitRoot && !isDangerousRoot(gitRoot)) {
91
256
  projectRoot = gitRoot;
92
- console.error(`[token-pilot] project root: ${projectRoot} (git from ${candidate === process.env.INIT_CWD ? 'INIT_CWD' : candidate === process.env.PWD ? 'PWD' : 'cwd'})`);
257
+ console.error(`[token-pilot] project root: ${projectRoot} (git from ${candidate === process.env.INIT_CWD ? "INIT_CWD" : candidate === process.env.PWD ? "PWD" : "cwd"})`);
93
258
  detected = true;
94
259
  break;
95
260
  }
@@ -100,10 +265,10 @@ export async function startServer(cliArgs = process.argv.slice(2)) {
100
265
  }
101
266
  if (!detected) {
102
267
  // Use best non-dangerous candidate as fallback even without git
103
- const fallback = candidates.find(c => !isDangerousRoot(c));
268
+ const fallback = candidates.find((c) => !isDangerousRoot(c));
104
269
  if (fallback) {
105
270
  projectRoot = fallback;
106
- console.error(`[token-pilot] project root: ${projectRoot} (${fallback === process.env.INIT_CWD ? 'INIT_CWD' : 'PWD'}, not a git repo)`);
271
+ console.error(`[token-pilot] project root: ${projectRoot} (${fallback === process.env.INIT_CWD ? "INIT_CWD" : "PWD"}, not a git repo)`);
107
272
  }
108
273
  else {
109
274
  console.error(`[token-pilot] project root: ${projectRoot} (cwd, not a git repo)`);
@@ -120,108 +285,222 @@ export async function startServer(cliArgs = process.argv.slice(2)) {
120
285
  // Non-blocking update check for all components (logs to stderr, never blocks startup)
121
286
  const config = await loadConfig(projectRoot);
122
287
  const binaryStatus = await findBinary(config.astIndex.binaryPath);
123
- checkAllUpdates(config, binaryStatus).catch(() => { });
288
+ checkAllUpdates(config, binaryStatus).catch(() => {
289
+ /* ignore */
290
+ });
291
+ // Phase 5 subtask 5.6 — one-time reminder when no tp-* agents installed.
292
+ // Non-blocking, silent on error, single-fire per process.
293
+ maybeEmitStartupReminder({
294
+ projectRoot,
295
+ homeDir: homedir(),
296
+ configSuppressed: config.agents?.reminder === false,
297
+ }).catch(() => {
298
+ /* ignore */
299
+ });
300
+ // Phase 6 subtask 6.2 — age + size retention on hook-events archives.
301
+ // Fire-and-forget: retention failures must never block startup.
302
+ applyRetention(projectRoot).catch(() => {
303
+ /* ignore */
304
+ });
124
305
  // Auto-install PreToolUse hook (non-blocking, Claude Code only)
125
306
  // Uses absolute paths to node + script so hooks work in /bin/sh (nvm, npx, etc.)
126
307
  let hookOptions;
127
308
  try {
128
- const rawPath = fileURLToPath(new URL('./index.js', import.meta.url));
129
- hookOptions = { scriptPath: realpathSync(rawPath), nodeExecPath: process.execPath };
309
+ const rawPath = fileURLToPath(new URL("./index.js", import.meta.url));
310
+ hookOptions = {
311
+ scriptPath: realpathSync(rawPath),
312
+ nodeExecPath: process.execPath,
313
+ };
130
314
  }
131
315
  catch {
132
316
  // Can't resolve script path (e.g. running from src/ in tests) — fall back to bare command
133
317
  }
134
- installHook(projectRoot, hookOptions).then(result => {
318
+ installHook(projectRoot, hookOptions)
319
+ .then((result) => {
135
320
  if (result.installed) {
136
321
  console.error(`[token-pilot] hook auto-installed: ${result.message}`);
137
322
  }
138
- }).catch(() => { });
323
+ })
324
+ .catch(() => {
325
+ /* ignore — not Claude Code or no .claude dir */
326
+ });
139
327
  const server = await createServer(projectRoot, {
140
328
  skipAstIndex: isDangerousRoot(projectRoot),
141
329
  });
142
330
  const transport = new StdioServerTransport();
143
331
  await server.connect(transport);
144
- process.on('SIGINT', async () => {
332
+ process.on("SIGINT", async () => {
145
333
  await server.close();
146
334
  process.exit(0);
147
335
  });
148
- process.on('SIGTERM', async () => {
336
+ process.on("SIGTERM", async () => {
149
337
  await server.close();
150
338
  process.exit(0);
151
339
  });
152
340
  }
153
- export function handleHookRead(filePathArg, denyThreshold = 300) {
154
- // Parse stdin (Claude Code hook format) to get tool_input
341
+ export async function handleHookRead(filePathArg, mode = "deny-enhanced", denyThreshold = 300, projectRoot = process.cwd(), adaptive = {}) {
342
+ // Mode 'off' hook is inert regardless of input.
343
+ if (mode === "off") {
344
+ process.exit(0);
345
+ }
346
+ const dispatchResult = await runHookReadDispatch(filePathArg, mode, denyThreshold, projectRoot, adaptive);
347
+ if (dispatchResult) {
348
+ process.stdout.write(dispatchResult);
349
+ }
350
+ process.exit(0);
351
+ }
352
+ /**
353
+ * Pure implementation of the hook-read dispatch — returns the JSON payload
354
+ * to write to stdout, or null when we should pass-through (no output).
355
+ * Extracted for testability; the outer handleHookRead adds the process.exit
356
+ * wrapping.
357
+ */
358
+ export async function runHookReadDispatch(filePathArg, mode, denyThresholdArg, projectRootArg, adaptive = {}) {
359
+ const denyThreshold = denyThresholdArg ?? 300;
360
+ const projectRoot = projectRootArg ?? process.cwd();
361
+ return runHookReadDispatchImpl(filePathArg, mode, denyThreshold, projectRoot, adaptive);
362
+ }
363
+ async function runHookReadDispatchImpl(filePathArg, mode, denyThreshold, projectRoot, adaptive = {}) {
364
+ if (mode === "off")
365
+ return null;
366
+ // Parse stdin to get tool_input + session/agent metadata, unless a
367
+ // filePath was supplied directly (tests, --filePath invocation).
155
368
  let filePath = filePathArg;
156
369
  let hasOffset = false;
157
370
  let hasLimit = false;
371
+ let sessionId = "";
372
+ let agentType = null;
373
+ let agentId = null;
158
374
  if (!filePath) {
159
375
  try {
160
- const stdin = readFileSync(0, 'utf-8');
376
+ const stdin = readFileSync(0, "utf-8");
161
377
  const input = JSON.parse(stdin);
162
378
  filePath = input?.tool_input?.file_path;
163
379
  hasOffset = input?.tool_input?.offset != null;
164
380
  hasLimit = input?.tool_input?.limit != null;
381
+ sessionId = typeof input?.session_id === "string" ? input.session_id : "";
382
+ agentType =
383
+ typeof input?.agent_type === "string" ? input.agent_type : null;
384
+ agentId = typeof input?.agent_id === "string" ? input.agent_id : null;
165
385
  }
166
386
  catch {
167
- process.exit(0);
387
+ return null;
168
388
  }
169
389
  }
170
- if (!filePath) {
171
- process.exit(0);
172
- }
173
- const ext = filePath.split('.').pop()?.toLowerCase() ?? '';
174
- // Non-code files — allow Read without interference
175
- if (!CODE_EXTENSIONS.has(ext)) {
176
- process.exit(0);
177
- }
178
- // Bounded Read (has offset or limit) allow, AI is reading a specific section
179
- if (hasOffset || hasLimit) {
180
- process.exit(0);
390
+ if (!filePath)
391
+ return null;
392
+ const ext = filePath.split(".").pop()?.toLowerCase() ?? "";
393
+ if (!CODE_EXTENSIONS.has(ext))
394
+ return null;
395
+ // Bounded Reads are always passed through — the agent already narrowed scope.
396
+ if (hasOffset || hasLimit)
397
+ return null;
398
+ // Path safety: refuse to summarise any file outside the project root
399
+ // (traversal, symlinks pointing outside). Pass-through on failure so the
400
+ // agent is never blocked by a safety reject.
401
+ if (!isPathWithinProject(filePath, projectRoot)) {
402
+ try {
403
+ process.stderr.write(`[token-pilot] refusing to summarise "${filePath}" — outside project root. Hook passing through.\n`);
404
+ }
405
+ catch {
406
+ /* silent — hook must not break */
407
+ }
408
+ return null;
181
409
  }
182
- // Check file size
410
+ // Resolve effective threshold once (cheap if adaptive is off).
411
+ const effectiveThreshold = adaptive.adaptiveThreshold
412
+ ? computeEffectiveThreshold({
413
+ baseThreshold: denyThreshold,
414
+ sessionSavedTokens: loadSessionSavedTokens(projectRoot, sessionId),
415
+ sessionBudgetTokens: adaptive.adaptiveBudgetTokens ?? 100_000,
416
+ enabled: true,
417
+ })
418
+ : denyThreshold;
419
+ // Read file content + line count.
420
+ let fileContent = "";
183
421
  let lineCount = 0;
184
- let fileContent = '';
185
422
  try {
186
- fileContent = readFileSync(filePath, 'utf-8');
187
- lineCount = fileContent.split('\n').length;
188
- if (lineCount <= denyThreshold) {
189
- process.exit(0);
190
- }
423
+ fileContent = readFileSync(filePath, "utf-8");
424
+ lineCount = fileContent.split("\n").length;
425
+ if (lineCount <= effectiveThreshold)
426
+ return null;
191
427
  }
192
428
  catch {
193
- process.exit(0);
429
+ return null;
194
430
  }
195
- // Track denied read for session analytics
431
+ const charEst = Math.ceil(fileContent.length / 4);
432
+ const wsRatio = (fileContent.match(/\s/g)?.length ?? 0) / fileContent.length;
433
+ const estTokens = Math.ceil(charEst * (1 - wsRatio * 0.3));
434
+ // Legacy telemetry (hook-denied.jsonl) — retained for backward compatibility
435
+ // with existing loadDeniedReads() readers in session-analytics. Never block
436
+ // hook dispatch on failure.
196
437
  try {
197
- const charEst = Math.ceil(fileContent.length / 4);
198
- const wsRatio = (fileContent.match(/\s/g)?.length ?? 0) / fileContent.length;
199
- const estTokens = Math.ceil(charEst * (1 - wsRatio * 0.3));
200
- const entry = JSON.stringify({ filePath, lineCount, estimatedTokens: estTokens, timestamp: Date.now() });
201
- const dir = join(process.cwd(), '.token-pilot');
438
+ const entry = JSON.stringify({
439
+ filePath,
440
+ lineCount,
441
+ estimatedTokens: estTokens,
442
+ mode,
443
+ timestamp: Date.now(),
444
+ });
445
+ const dir = join(projectRoot, ".token-pilot");
202
446
  mkdirSync(dir, { recursive: true });
203
- appendFileSync(join(dir, 'hook-denied.jsonl'), entry + '\n');
447
+ appendFileSync(join(dir, "hook-denied.jsonl"), entry + "\n");
204
448
  }
205
449
  catch {
206
- // Silent fail — hook must not break
450
+ /* silent — hook must not break */
451
+ }
452
+ const writeEvent = async (eventKind, summaryTokens) => {
453
+ await appendEvent(projectRoot, {
454
+ ts: Date.now(),
455
+ session_id: sessionId,
456
+ agent_type: agentType,
457
+ agent_id: agentId,
458
+ event: eventKind,
459
+ file: filePath,
460
+ lines: lineCount,
461
+ estTokens,
462
+ summaryTokens,
463
+ savedTokens: Math.max(0, estTokens - summaryTokens),
464
+ });
465
+ };
466
+ if (mode === "advisory") {
467
+ const reason = `File "${filePath}" has ${lineCount} lines. Use mcp__token-pilot__smart_read("${filePath}") ` +
468
+ `for a structural overview, or mcp__token-pilot__read_for_edit("${filePath}", symbol="<name>") ` +
469
+ `for edit context. Bounded Read with offset/limit is still allowed.`;
470
+ await writeEvent("denied", Math.ceil(reason.length / 4));
471
+ return JSON.stringify({
472
+ hookSpecificOutput: {
473
+ hookEventName: "PreToolUse",
474
+ permissionDecision: "deny",
475
+ permissionDecisionReason: reason,
476
+ },
477
+ });
207
478
  }
208
- // Large code file, unbounded Read → DENY
209
- // permissionDecisionReason is shown to Claude (not user) per official docs
210
- const deny = JSON.stringify({
479
+ // mode === 'deny-enhanced'
480
+ const pipelineResult = await runSummaryPipeline(fileContent, filePath);
481
+ if (pipelineResult.kind === "pass-through") {
482
+ await writeEvent("pass-through", 0);
483
+ return null;
484
+ }
485
+ const message = formatDenyMessage({
486
+ filePath,
487
+ summary: pipelineResult.summary,
488
+ tier: pipelineResult.tier,
489
+ });
490
+ await writeEvent("denied", Math.ceil(message.length / 4));
491
+ return JSON.stringify({
211
492
  hookSpecificOutput: {
212
493
  hookEventName: "PreToolUse",
213
494
  permissionDecision: "deny",
214
- permissionDecisionReason: `File "${filePath}" has ${lineCount} lines. Use smart_read("${filePath}") for structural overview, or read_for_edit("${filePath}", symbol="<name>") for edit context. Bounded Read with offset/limit is still allowed.`,
495
+ permissionDecisionReason: message,
215
496
  },
216
497
  });
217
- process.stdout.write(deny);
218
- process.exit(0);
219
498
  }
220
499
  export function handleHookEdit() {
221
500
  // Parse stdin for Edit tool_input
222
501
  let filePath;
223
502
  try {
224
- const stdin = readFileSync(0, 'utf-8');
503
+ const stdin = readFileSync(0, "utf-8");
225
504
  const input = JSON.parse(stdin);
226
505
  filePath = input?.tool_input?.file_path;
227
506
  }
@@ -231,7 +510,7 @@ export function handleHookEdit() {
231
510
  if (!filePath) {
232
511
  process.exit(0);
233
512
  }
234
- const ext = filePath.split('.').pop()?.toLowerCase() ?? '';
513
+ const ext = filePath.split(".").pop()?.toLowerCase() ?? "";
235
514
  // Only add context for code files
236
515
  if (!CODE_EXTENSIONS.has(ext)) {
237
516
  process.exit(0);
@@ -250,8 +529,11 @@ export function handleHookEdit() {
250
529
  export async function handleInstallHook(projectRoot) {
251
530
  let hookOptions;
252
531
  try {
253
- const rawPath = fileURLToPath(new URL('./index.js', import.meta.url));
254
- hookOptions = { scriptPath: realpathSync(rawPath), nodeExecPath: process.execPath };
532
+ const rawPath = fileURLToPath(new URL("./index.js", import.meta.url));
533
+ hookOptions = {
534
+ scriptPath: realpathSync(rawPath),
535
+ nodeExecPath: process.execPath,
536
+ };
255
537
  }
256
538
  catch {
257
539
  // Fall back to bare command
@@ -290,23 +572,23 @@ export async function handleInstallAstIndex() {
290
572
  }
291
573
  export async function handleDoctor() {
292
574
  const version = getVersion();
293
- const { existsSync } = await import('node:fs');
294
- const { join } = await import('node:path');
575
+ const { existsSync } = await import("node:fs");
576
+ const { join } = await import("node:path");
295
577
  const cwd = process.cwd();
296
578
  console.log(`token-pilot doctor v${version}\n`);
297
579
  // ── Environment ──
298
580
  const nodeVersion = process.version;
299
581
  const nodeMajor = parseInt(nodeVersion.slice(1), 10);
300
- console.log(`Node.js: ${nodeVersion} ${nodeMajor >= 18 ? '' : '✗ (requires >=18)'}`);
301
- const configPath = join(cwd, '.token-pilot.json');
302
- console.log(`config: ${existsSync(configPath) ? configPath + '' : 'default (no .token-pilot.json)'}`);
303
- const gitDir = join(cwd, '.git');
304
- console.log(`git repo: ${existsSync(gitDir) ? 'yes ✓' : 'no (read_diff/git features unavailable)'}`);
305
- console.log('');
582
+ console.log(`Node.js: ${nodeVersion} ${nodeMajor >= 18 ? "" : "✗ (requires >=18)"}`);
583
+ const configPath = join(cwd, ".token-pilot.json");
584
+ console.log(`config: ${existsSync(configPath) ? configPath + "" : "default (no .token-pilot.json)"}`);
585
+ const gitDir = join(cwd, ".git");
586
+ console.log(`git repo: ${existsSync(gitDir) ? "yes ✓" : "no (read_diff/git features unavailable)"}`);
587
+ console.log("");
306
588
  // ── token-pilot ──
307
- console.log('── token-pilot ──');
589
+ console.log("── token-pilot ──");
308
590
  console.log(` installed: ${version}`);
309
- const tpLatest = await checkNpmLatest('token-pilot');
591
+ const tpLatest = await checkNpmLatest("token-pilot");
310
592
  if (tpLatest) {
311
593
  if (isNewerVersion(version, tpLatest)) {
312
594
  console.log(` latest: ${tpLatest} (update available!)`);
@@ -319,9 +601,9 @@ export async function handleDoctor() {
319
601
  else {
320
602
  console.log(` latest: could not check (network error)`);
321
603
  }
322
- console.log('');
604
+ console.log("");
323
605
  // ── ast-index ──
324
- console.log('── ast-index ──');
606
+ console.log("── ast-index ──");
325
607
  const astStatus = await findBinary();
326
608
  if (astStatus.available) {
327
609
  console.log(` installed: ${astStatus.version} (${astStatus.source}: ${astStatus.path})`);
@@ -334,45 +616,99 @@ export async function handleDoctor() {
334
616
  console.log(` latest: ${astUpdate.latest} ✓ (up to date)`);
335
617
  }
336
618
  const config = await loadConfig(cwd);
337
- console.log(` auto-update: ${config.updates.autoUpdate ? 'enabled ✓' : 'disabled (set updates.autoUpdate=true in .token-pilot.json)'}`);
619
+ console.log(` auto-update: ${config.updates.autoUpdate ? "enabled ✓" : "disabled (set updates.autoUpdate=true in .token-pilot.json)"}`);
338
620
  }
339
621
  else {
340
622
  console.log(` installed: not found ✗`);
341
623
  console.log(` run: npx token-pilot install-ast-index`);
342
624
  }
343
- console.log('');
625
+ console.log("");
344
626
  // ── context-mode ──
345
- console.log('── context-mode ──');
346
- const { detectContextMode } = await import('./integration/context-mode-detector.js');
627
+ console.log("── context-mode ──");
628
+ const { detectContextMode } = await import("./integration/context-mode-detector.js");
347
629
  const cmStatus = await detectContextMode(cwd);
348
- console.log(` detected: ${cmStatus.detected ? `yes (${cmStatus.source})` : 'no'}`);
349
- const cmLatest = await checkNpmLatest('claude-context-mode');
630
+ console.log(` detected: ${cmStatus.detected ? `yes (${cmStatus.source})` : "no"}`);
631
+ const cmLatest = await checkNpmLatest("claude-context-mode");
350
632
  if (cmLatest) {
351
633
  console.log(` latest npm: ${cmLatest}`);
352
634
  }
353
635
  if (!cmStatus.detected) {
354
636
  console.log(` setup: npx token-pilot init`);
355
637
  }
356
- console.log('');
638
+ console.log("");
639
+ // ── blessed agents drift check ──
640
+ const drift = await detectDrift({ projectRoot: cwd, homeDir: homedir() });
641
+ if (drift.length > 0) {
642
+ console.log("── blessed-agents drift ──");
643
+ for (const finding of drift) {
644
+ console.log(` ${formatDriftFinding(finding)}`);
645
+ }
646
+ console.log("");
647
+ }
648
+ // ── Claude Code env-var savings tips ──
649
+ try {
650
+ const tips = await runClaudeCodeEnvCheck();
651
+ if (tips.length > 0) {
652
+ console.log("── Claude Code env knobs (savings tips) ──");
653
+ for (const t of tips) {
654
+ console.log(` ⚠ ${t}`);
655
+ }
656
+ console.log("");
657
+ }
658
+ }
659
+ catch {
660
+ /* doctor must never crash over an optional check */
661
+ }
662
+ // ── .claudeignore ──
663
+ try {
664
+ const status = await claudeIgnoreStatus(cwd);
665
+ console.log("── .claudeignore ──");
666
+ if (status.kind === "absent") {
667
+ console.log(` not present — run \`npx token-pilot init\` to add sensible defaults`);
668
+ }
669
+ else if (status.kind === "managed") {
670
+ console.log(` present ✓ (managed by token-pilot; safe to edit)`);
671
+ }
672
+ else {
673
+ console.log(` present ✓ (user-owned; token-pilot will not touch it)`);
674
+ }
675
+ console.log("");
676
+ }
677
+ catch {
678
+ /* ignore */
679
+ }
680
+ // ── CLAUDE.md hygiene ──
681
+ try {
682
+ const r = await assessClaudeMd(cwd);
683
+ if (r.kind === "bloated") {
684
+ console.log("── CLAUDE.md hygiene ──");
685
+ console.log(` ⚠ ${r.path} has ${r.nonEmptyLines} non-empty lines (threshold: ${r.threshold}).`);
686
+ console.log(` This file loads into every Claude Code message — splitting into docs/*.md and loading on-demand saves tokens per turn.`);
687
+ console.log("");
688
+ }
689
+ }
690
+ catch {
691
+ /* ignore */
692
+ }
357
693
  process.exit(0);
358
694
  }
359
695
  export async function handleInit(targetDir) {
360
- const { existsSync, readFileSync: readFs, writeFileSync } = await import('node:fs');
361
- const { join } = await import('node:path');
362
- const mcpPath = join(targetDir, '.mcp.json');
696
+ const { existsSync, readFileSync: readFs, writeFileSync, } = await import("node:fs");
697
+ const { join } = await import("node:path");
698
+ const mcpPath = join(targetDir, ".mcp.json");
363
699
  const tokenPilotConfig = {
364
- command: 'npx',
365
- args: ['-y', 'token-pilot'],
700
+ command: "npx",
701
+ args: ["-y", "token-pilot"],
366
702
  };
367
703
  const contextModeConfig = {
368
- command: 'npx',
369
- args: ['-y', 'claude-context-mode'],
704
+ command: "npx",
705
+ args: ["-y", "claude-context-mode"],
370
706
  };
371
707
  let config = { mcpServers: {} };
372
708
  let existed = false;
373
709
  if (existsSync(mcpPath)) {
374
710
  try {
375
- config = JSON.parse(readFs(mcpPath, 'utf-8'));
711
+ config = JSON.parse(readFs(mcpPath, "utf-8"));
376
712
  if (!config.mcpServers)
377
713
  config.mcpServers = {};
378
714
  existed = true;
@@ -383,28 +719,71 @@ export async function handleInit(targetDir) {
383
719
  }
384
720
  }
385
721
  const added = [];
386
- if (!config.mcpServers['token-pilot']) {
387
- config.mcpServers['token-pilot'] = tokenPilotConfig;
388
- added.push('token-pilot');
722
+ if (!config.mcpServers["token-pilot"]) {
723
+ config.mcpServers["token-pilot"] = tokenPilotConfig;
724
+ added.push("token-pilot");
389
725
  }
390
- if (!config.mcpServers['context-mode']) {
391
- config.mcpServers['context-mode'] = contextModeConfig;
392
- added.push('context-mode');
726
+ if (!config.mcpServers["context-mode"]) {
727
+ config.mcpServers["context-mode"] = contextModeConfig;
728
+ added.push("context-mode");
393
729
  }
394
730
  if (added.length === 0) {
395
731
  console.log(`✓ ${mcpPath} already has both token-pilot and context-mode configured`);
396
732
  process.exit(0);
397
733
  }
398
- writeFileSync(mcpPath, JSON.stringify(config, null, 2) + '\n');
734
+ writeFileSync(mcpPath, JSON.stringify(config, null, 2) + "\n");
399
735
  if (existed) {
400
- console.log(`✓ Updated ${mcpPath} — added: ${added.join(', ')}`);
736
+ console.log(`✓ Updated ${mcpPath} — added: ${added.join(", ")}`);
401
737
  }
402
738
  else {
403
739
  console.log(`✓ Created ${mcpPath} with token-pilot + context-mode`);
404
740
  }
405
741
  console.log(`\nConfigured MCP servers:`);
406
- console.log(` • token-pilot — AST-aware code reading (60-80% token savings)`);
742
+ console.log(` • token-pilot — enforcement layer for token-efficient reads (hook + MCP + tp-* subagents)`);
407
743
  console.log(` • context-mode — shell output & large data processing (BM25 sandbox)`);
744
+ // Claude Code users benefit from six token-pilot-native subagents (tp-run,
745
+ // tp-onboard, …). Offer installation now so they don't have to discover
746
+ // `install-agents` from a stderr reminder later.
747
+ const offeredAgents = added.includes("token-pilot") && process.stdin.isTTY === true;
748
+ if (offeredAgents) {
749
+ console.log("");
750
+ const yes = await promptYesNo("Install 6 tp-* subagents now (recommended for Claude Code)?", true);
751
+ if (yes) {
752
+ // Delegate to the full install-agents flow: it will prompt scope,
753
+ // handle idempotence, and persist the choice to .token-pilot.json.
754
+ const code = await handleInstallAgents([], { projectRoot: targetDir });
755
+ if (code !== 0) {
756
+ console.log("\n(install-agents returned non-zero; you can retry with: npx token-pilot install-agents)");
757
+ }
758
+ }
759
+ else {
760
+ console.log("\nSkipping agent install. Run later: npx token-pilot install-agents");
761
+ }
762
+ }
763
+ else if (added.includes("token-pilot")) {
764
+ // Non-TTY path — at minimum surface the next step in the log.
765
+ console.log(`\nNext step (Claude Code): npx token-pilot install-agents --scope=user|project`);
766
+ }
767
+ // Offer `.claudeignore` — small one-time win that compounds per message.
768
+ // Separate TTY check so a script that declined agents can still get ignore,
769
+ // and vice versa.
770
+ const ignoreStatus = await claudeIgnoreStatus(targetDir);
771
+ if (offeredAgents && ignoreStatus.kind !== "user-owned") {
772
+ console.log("");
773
+ const yesIgnore = await promptYesNo(ignoreStatus.kind === "absent"
774
+ ? "Create .claudeignore with sensible defaults (node_modules, dist, lockfiles, …)?"
775
+ : "Refresh .claudeignore defaults (managed by token-pilot)?", true);
776
+ if (yesIgnore) {
777
+ const wrote = await writeDefaultClaudeIgnore(targetDir);
778
+ if (wrote)
779
+ console.log("✓ .claudeignore written");
780
+ else
781
+ console.log("(Skipped — existing .claudeignore is user-owned; not touched.)");
782
+ }
783
+ }
784
+ else if (ignoreStatus.kind === "absent") {
785
+ console.log(`\nTip: \`npx token-pilot init\` offers to create .claudeignore — improves context by skipping node_modules, build artefacts, lockfiles.`);
786
+ }
408
787
  console.log(`\nRestart your AI assistant to activate.`);
409
788
  process.exit(0);
410
789
  }
@@ -421,7 +800,7 @@ export async function checkNpmLatest(packageName) {
421
800
  clearTimeout(timeout);
422
801
  if (!resp.ok)
423
802
  return null;
424
- const data = await resp.json();
803
+ const data = (await resp.json());
425
804
  return data.version ?? null;
426
805
  }
427
806
  catch {
@@ -432,59 +811,69 @@ export async function checkAllUpdates(config, binaryStatus) {
432
811
  if (!config.updates.checkOnStartup)
433
812
  return;
434
813
  const [tpLatest, astUpdate, cmLatest] = await Promise.allSettled([
435
- checkNpmLatest('token-pilot'),
436
- binaryStatus.available ? checkBinaryUpdate(binaryStatus.path) : Promise.resolve(null),
437
- checkNpmLatest('claude-context-mode'),
814
+ checkNpmLatest("token-pilot"),
815
+ binaryStatus.available
816
+ ? checkBinaryUpdate(binaryStatus.path)
817
+ : Promise.resolve(null),
818
+ checkNpmLatest("claude-context-mode"),
438
819
  ]);
439
820
  // token-pilot
440
821
  const tpVersion = getVersion();
441
- if (tpLatest.status === 'fulfilled' && tpLatest.value && isNewerVersion(tpVersion, tpLatest.value)) {
822
+ if (tpLatest.status === "fulfilled" &&
823
+ tpLatest.value &&
824
+ isNewerVersion(tpVersion, tpLatest.value)) {
442
825
  console.error(`[token-pilot] Update available: ${tpVersion} → ${tpLatest.value}. Run: npx token-pilot@latest`);
443
826
  }
444
827
  // ast-index
445
- if (astUpdate.status === 'fulfilled' && astUpdate.value?.updateAvailable) {
828
+ if (astUpdate.status === "fulfilled" && astUpdate.value?.updateAvailable) {
446
829
  const { current, latest } = astUpdate.value;
447
830
  if (config.updates.autoUpdate) {
448
831
  console.error(`[token-pilot] Auto-updating ast-index: ${current} → ${latest}...`);
449
- installBinary(msg => console.error(`[token-pilot] ${msg}`)).catch(() => { });
832
+ installBinary((msg) => console.error(`[token-pilot] ${msg}`)).catch(() => { });
450
833
  }
451
834
  else {
452
835
  console.error(`[token-pilot] ast-index update: ${current} → ${latest}. Run: token-pilot install-ast-index`);
453
836
  }
454
837
  }
455
838
  // context-mode (notification only — runs as separate MCP server)
456
- if (cmLatest.status === 'fulfilled' && cmLatest.value) {
839
+ if (cmLatest.status === "fulfilled" && cmLatest.value) {
457
840
  // We can't reliably detect the currently installed version of context-mode
458
841
  // (it runs as separate process via npx). Just log latest available for doctor.
459
842
  // On startup, we only notify if explicitly useful.
460
843
  }
461
844
  }
462
845
  export function printHelp() {
463
- console.log(`token-pilot v${getVersion()} — MCP server for token-efficient code reading
464
-
465
- Usage:
466
- token-pilot [project-root] Start MCP server (default: cwd)
467
- token-pilot init [dir] Create .mcp.json with token-pilot + context-mode
468
- token-pilot install-hook [root] Install PreToolUse hook (Claude Code only)
469
- token-pilot uninstall-hook [root] Remove PreToolUse hook
470
- token-pilot install-ast-index Download ast-index binary (auto on first run)
471
- token-pilot doctor Run diagnostics (check ast-index, config, updates)
472
- token-pilot --version Show version
473
- token-pilot --help Show this help
474
-
475
- Quick start:
476
- npx token-pilot init Setup .mcp.json (token-pilot + context-mode)
477
-
478
- MCP Tools (18):
479
- smart_read, read_symbol, read_range, read_diff, read_for_edit, smart_read_many,
480
- find_usages, find_unused, related_files, outline, project_overview, session_analytics,
481
- code_audit, module_info, smart_diff, explore_area, smart_log, test_summary
846
+ console.log(`token-pilot v${getVersion()} — MCP server for token-efficient code reading
847
+
848
+ Usage:
849
+ token-pilot [project-root] Start MCP server (default: cwd)
850
+ token-pilot init [dir] Create .mcp.json with token-pilot + context-mode
851
+ token-pilot install-hook [root] Install PreToolUse hook (Claude Code only)
852
+ token-pilot uninstall-hook [root] Remove PreToolUse hook
853
+ token-pilot install-ast-index Download ast-index binary (auto on first run)
854
+ token-pilot doctor Run diagnostics (check ast-index, config, updates)
855
+ token-pilot save-doc <name> Save stdin to .token-pilot/docs/<name>.md
856
+ token-pilot list-docs List saved docs
857
+ token-pilot --version Show version
858
+ token-pilot --help Show this help
859
+
860
+ Quick start:
861
+ npx token-pilot init Setup .mcp.json (token-pilot + context-mode)
862
+
863
+ MCP Tools (22):
864
+ smart_read, read_symbol, read_symbols, read_range, read_section, read_diff,
865
+ read_for_edit, smart_read_many, find_usages, find_unused, related_files,
866
+ outline, project_overview, session_analytics, code_audit, module_info,
867
+ smart_diff, explore_area, smart_log, test_summary, session_snapshot,
868
+ session_budget
482
869
  `);
483
870
  process.exit(0);
484
871
  }
485
- const isDirectRun = process.argv[1] !== undefined && realpathSync(fileURLToPath(import.meta.url)) === realpathSync(process.argv[1]);
872
+ const isDirectRun = process.argv[1] !== undefined &&
873
+ realpathSync(fileURLToPath(import.meta.url)) ===
874
+ realpathSync(process.argv[1]);
486
875
  if (isDirectRun) {
487
- main().catch(err => {
876
+ main().catch((err) => {
488
877
  console.error(`[token-pilot] Fatal: ${err instanceof Error ? err.message : err}`);
489
878
  process.exit(1);
490
879
  });