token-pilot 0.13.0 → 0.14.1

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 (69) hide show
  1. package/.claude-plugin/hooks/hooks.json +9 -0
  2. package/.claude-plugin/marketplace.json +1 -1
  3. package/.claude-plugin/plugin.json +1 -1
  4. package/CHANGELOG.md +29 -0
  5. package/README.md +28 -7
  6. package/dist/config/defaults.js +12 -0
  7. package/dist/core/architecture-fingerprint.d.ts +34 -0
  8. package/dist/core/architecture-fingerprint.js +127 -0
  9. package/dist/core/budget-planner.d.ts +21 -0
  10. package/dist/core/budget-planner.js +68 -0
  11. package/dist/core/confidence.d.ts +31 -0
  12. package/dist/core/confidence.js +99 -0
  13. package/dist/core/context-registry.d.ts +14 -0
  14. package/dist/core/context-registry.js +55 -0
  15. package/dist/core/decision-trace.d.ts +31 -0
  16. package/dist/core/decision-trace.js +45 -0
  17. package/dist/core/intent-classifier.d.ts +13 -0
  18. package/dist/core/intent-classifier.js +44 -0
  19. package/dist/core/policy-engine.d.ts +41 -0
  20. package/dist/core/policy-engine.js +76 -0
  21. package/dist/core/session-analytics.d.ts +8 -0
  22. package/dist/core/session-analytics.js +86 -7
  23. package/dist/core/session-cache.d.ts +74 -0
  24. package/dist/core/session-cache.js +162 -0
  25. package/dist/core/validation.d.ts +3 -0
  26. package/dist/core/validation.js +3 -0
  27. package/dist/git/file-watcher.d.ts +6 -0
  28. package/dist/git/file-watcher.js +18 -2
  29. package/dist/git/watcher.d.ts +3 -0
  30. package/dist/git/watcher.js +6 -0
  31. package/dist/handlers/code-audit.d.ts +7 -2
  32. package/dist/handlers/code-audit.js +19 -5
  33. package/dist/handlers/explore-area.d.ts +10 -0
  34. package/dist/handlers/explore-area.js +39 -13
  35. package/dist/handlers/find-unused.d.ts +3 -0
  36. package/dist/handlers/find-unused.js +3 -2
  37. package/dist/handlers/find-usages.d.ts +7 -0
  38. package/dist/handlers/find-usages.js +36 -5
  39. package/dist/handlers/module-info.d.ts +3 -0
  40. package/dist/handlers/module-info.js +22 -2
  41. package/dist/handlers/project-overview.d.ts +1 -1
  42. package/dist/handlers/project-overview.js +18 -2
  43. package/dist/handlers/read-for-edit.d.ts +3 -0
  44. package/dist/handlers/read-for-edit.js +185 -3
  45. package/dist/handlers/read-range.d.ts +1 -1
  46. package/dist/handlers/read-range.js +16 -1
  47. package/dist/handlers/read-symbol.d.ts +1 -1
  48. package/dist/handlers/read-symbol.js +26 -2
  49. package/dist/handlers/related-files.d.ts +11 -0
  50. package/dist/handlers/related-files.js +178 -42
  51. package/dist/handlers/smart-read-many.js +70 -16
  52. package/dist/handlers/smart-read.js +10 -1
  53. package/dist/handlers/test-summary.js +26 -3
  54. package/dist/hooks/installer.d.ts +12 -8
  55. package/dist/hooks/installer.js +24 -8
  56. package/dist/index.d.ts +16 -1
  57. package/dist/index.js +61 -55
  58. package/dist/server.js +395 -30
  59. package/dist/types.d.ts +12 -0
  60. package/package.json +5 -3
  61. package/start.sh +28 -27
  62. package/dist/handlers/class-hierarchy.d.ts +0 -11
  63. package/dist/handlers/class-hierarchy.js +0 -28
  64. package/dist/handlers/export-ast-index.d.ts +0 -22
  65. package/dist/handlers/export-ast-index.js +0 -175
  66. package/dist/handlers/find-implementations.d.ts +0 -11
  67. package/dist/handlers/find-implementations.js +0 -27
  68. package/dist/handlers/search-code.d.ts +0 -14
  69. package/dist/handlers/search-code.js +0 -32
@@ -1,32 +1,86 @@
1
1
  import { handleSmartRead } from './smart-read.js';
2
- import { estimateTokens } from '../core/token-estimator.js';
2
+ import { estimateTokens, formatSavings } from '../core/token-estimator.js';
3
+ import { readFile } from 'node:fs/promises';
4
+ import { resolveSafePath } from '../core/validation.js';
5
+ const MAX_BATCH_FILES = 20;
6
+ const MAX_BATCH_TOKENS = 1400;
7
+ const MAX_FILE_TOKENS = 220;
8
+ const MAX_FILE_LINES = 24;
9
+ const BATCH_CONCURRENCY = 4;
3
10
  export async function handleSmartReadMany(args, projectRoot, astIndex, fileCache, contextRegistry, config) {
4
11
  if (!args.paths || args.paths.length === 0) {
5
12
  return {
6
13
  content: [{ type: 'text', text: 'No paths provided.' }],
7
14
  };
8
15
  }
9
- if (args.paths.length > 20) {
16
+ if (args.paths.length > MAX_BATCH_FILES) {
10
17
  return {
11
- content: [{ type: 'text', text: `Too many files (${args.paths.length}). Maximum is 20 per batch.` }],
18
+ content: [{ type: 'text', text: `Too many files (${args.paths.length}). Maximum is ${MAX_BATCH_FILES} per batch.` }],
12
19
  };
13
20
  }
14
- const results = [];
15
- let totalTokens = 0;
16
- for (const path of args.paths) {
17
- try {
21
+ const uniquePaths = Array.from(new Set(args.paths));
22
+ const entries = [];
23
+ for (let i = 0; i < uniquePaths.length; i += BATCH_CONCURRENCY) {
24
+ const batch = uniquePaths.slice(i, i + BATCH_CONCURRENCY);
25
+ const settled = await Promise.allSettled(batch.map(async (path) => {
18
26
  const result = await handleSmartRead({ path }, projectRoot, astIndex, fileCache, contextRegistry, config);
19
27
  const text = result.content[0]?.text ?? '';
20
- results.push(text);
21
- totalTokens += estimateTokens(text);
22
- }
23
- catch (err) {
24
- const msg = err instanceof Error ? err.message : String(err);
25
- results.push(`FILE: ${path}\nERROR: ${msg}`);
28
+ const fullTokens = await estimateFullFileTokens(projectRoot, path);
29
+ return { path, text, fullTokens };
30
+ }));
31
+ for (let index = 0; index < settled.length; index++) {
32
+ const outcome = settled[index];
33
+ const path = batch[index];
34
+ if (outcome.status === 'fulfilled') {
35
+ entries.push(outcome.value);
36
+ }
37
+ else {
38
+ const msg = outcome.reason instanceof Error ? outcome.reason.message : String(outcome.reason);
39
+ entries.push({ path, text: `FILE: ${path}\nERROR: ${msg}`, fullTokens: 0 });
40
+ }
26
41
  }
27
42
  }
28
- results.push('');
29
- results.push(`BATCH: ${args.paths.length} files loaded (~${totalTokens} tokens total)`);
30
- return { content: [{ type: 'text', text: results.join('\n\n---\n\n') }] };
43
+ let remainingBudget = MAX_BATCH_TOKENS;
44
+ const renderedEntries = [];
45
+ for (const entry of entries) {
46
+ const compacted = compactBatchEntry(entry, remainingBudget);
47
+ renderedEntries.push(compacted);
48
+ remainingBudget = Math.max(0, remainingBudget - estimateTokens(compacted));
49
+ }
50
+ const body = renderedEntries.join('\n\n---\n\n');
51
+ const actualTokens = estimateTokens(body);
52
+ const fullTokens = entries.reduce((sum, entry) => sum + entry.fullTokens, 0);
53
+ const duplicatesRemoved = args.paths.length - uniquePaths.length;
54
+ const footer = [''];
55
+ footer.push(`BATCH: ${uniquePaths.length} unique files loaded${duplicatesRemoved > 0 ? ` (${duplicatesRemoved} duplicates skipped)` : ''}`);
56
+ footer.push(`OUTPUT: ~${actualTokens} tokens`);
57
+ if (fullTokens > 0) {
58
+ footer.push(formatSavings(actualTokens, fullTokens));
59
+ }
60
+ footer.push('HINT: Re-run smart_read(path) on any compacted file for full detail.');
61
+ return { content: [{ type: 'text', text: body + '\n' + footer.join('\n') }] };
62
+ }
63
+ function compactBatchEntry(entry, remainingBudget) {
64
+ const rawTokens = estimateTokens(entry.text);
65
+ if (remainingBudget <= 60) {
66
+ return `FILE: ${entry.path}\n(compacted in batch mode — use smart_read("${entry.path}") for full detail)`;
67
+ }
68
+ if (rawTokens <= Math.min(MAX_FILE_TOKENS, remainingBudget)) {
69
+ return entry.text;
70
+ }
71
+ const lines = entry.text.split('\n');
72
+ const head = lines.slice(0, MAX_FILE_LINES).join('\n');
73
+ const suffix = `\n\n... compacted for batch mode. Use smart_read("${entry.path}") for full detail.`;
74
+ return head + suffix;
75
+ }
76
+ async function estimateFullFileTokens(projectRoot, relativePath) {
77
+ try {
78
+ const absPath = resolveSafePath(projectRoot, relativePath);
79
+ const content = await readFile(absPath, 'utf-8');
80
+ return estimateTokens(content);
81
+ }
82
+ catch {
83
+ return 0;
84
+ }
31
85
  }
32
86
  //# sourceMappingURL=smart-read-many.js.map
@@ -4,6 +4,7 @@ import { formatOutline } from '../formatters/structure.js';
4
4
  import { estimateTokens, formatSavings } from '../core/token-estimator.js';
5
5
  import { resolveSafePath } from '../core/validation.js';
6
6
  import { isNonCodeStructured, handleNonCodeRead } from './non-code.js';
7
+ import { assessConfidence, formatConfidence } from '../core/confidence.js';
7
8
  export async function handleSmartRead(args, projectRoot, astIndex, fileCache, contextRegistry, config) {
8
9
  const absPath = resolveSafePath(projectRoot, args.path);
9
10
  // 0. Guard: directory passed instead of file
@@ -134,6 +135,14 @@ export async function handleSmartRead(args, projectRoot, astIndex, fileCache, co
134
135
  tokens: structureTokens,
135
136
  });
136
137
  contextRegistry.setContentHash(absPath, cached.hash);
137
- return { content: [{ type: 'text', text: output + savings }] };
138
+ // 9. Confidence metadata
139
+ const confidenceMeta = assessConfidence({
140
+ symbolResolved: (cached.structure.symbols?.length ?? 0) > 0,
141
+ fullFile: false,
142
+ truncated: false,
143
+ astAvailable: true,
144
+ crossFileDeps: cached.structure.imports?.length ?? 0,
145
+ });
146
+ return { content: [{ type: 'text', text: output + savings + formatConfidence(confidenceMeta) }] };
138
147
  }
139
148
  //# sourceMappingURL=smart-read.js.map
@@ -37,7 +37,16 @@ export async function handleTestSummary(args, projectRoot) {
37
37
  const rawTokens = estimateTokens(rawOutput);
38
38
  const runner = args.runner ?? detectRunner(command, rawOutput);
39
39
  const result = parseTestOutput(rawOutput, runner);
40
- const formatted = formatTestSummary(result, command, runner, rawTokens);
40
+ const commandFailed = exitCode !== 0;
41
+ if (commandFailed && result.failed === 0) {
42
+ result.failed = 1;
43
+ result.total = Math.max(result.total, result.passed + result.failed + result.skipped);
44
+ result.failures.unshift({
45
+ name: `Command exited with code ${exitCode}`,
46
+ error: summarizeCommandError(rawOutput),
47
+ });
48
+ }
49
+ const formatted = formatTestSummary(result, command, runner, rawTokens, exitCode, commandFailed);
41
50
  return {
42
51
  content: [{ type: 'text', text: formatted }],
43
52
  rawTokens,
@@ -277,12 +286,23 @@ function parseGeneric(output) {
277
286
  : result.passed + result.failed + result.skipped;
278
287
  return result;
279
288
  }
289
+ function summarizeCommandError(output) {
290
+ const lines = output
291
+ .split('\n')
292
+ .map(line => line.trim())
293
+ .filter(line => line.length > 0)
294
+ .filter(line => !line.startsWith('at ') && !line.startsWith('>'));
295
+ if (lines.length === 0) {
296
+ return 'Command failed without producing output.';
297
+ }
298
+ return lines.slice(0, 3).join('\n').substring(0, 300);
299
+ }
280
300
  // ──────────────────────────────────────────────
281
301
  // Formatter
282
302
  // ──────────────────────────────────────────────
283
- function formatTestSummary(result, command, runner, rawTokens) {
303
+ function formatTestSummary(result, command, runner, rawTokens, exitCode, commandFailed) {
284
304
  const lines = [];
285
- const status = result.failed > 0 ? '❌ FAIL' : '✅ PASS';
305
+ const status = result.failed > 0 || commandFailed ? '❌ FAIL' : '✅ PASS';
286
306
  lines.push(`TEST RESULT: ${status} (${runner})`);
287
307
  lines.push('');
288
308
  // Stats line
@@ -298,6 +318,9 @@ function formatTestSummary(result, command, runner, rawTokens) {
298
318
  if (result.suites)
299
319
  parts.push(`${result.suites} suites`);
300
320
  lines.push(parts.join(' | '));
321
+ if (commandFailed && exitCode != null) {
322
+ lines.push(`Exit code: ${exitCode}`);
323
+ }
301
324
  // Failed tests detail
302
325
  if (result.failures.length > 0) {
303
326
  lines.push('');
@@ -1,16 +1,20 @@
1
+ export interface HookInstallResult {
2
+ installed: boolean;
3
+ fatal: boolean;
4
+ message: string;
5
+ }
6
+ export interface HookUninstallResult {
7
+ removed: boolean;
8
+ fatal: boolean;
9
+ message: string;
10
+ }
1
11
  /**
2
12
  * Install Token Pilot hook into Claude Code settings.
3
13
  * Creates or updates .claude/settings.json with PreToolUse hook.
4
14
  */
5
- export declare function installHook(projectRoot: string): Promise<{
6
- installed: boolean;
7
- message: string;
8
- }>;
15
+ export declare function installHook(projectRoot: string): Promise<HookInstallResult>;
9
16
  /**
10
17
  * Remove Token Pilot hook from Claude Code settings.
11
18
  */
12
- export declare function uninstallHook(projectRoot: string): Promise<{
13
- removed: boolean;
14
- message: string;
15
- }>;
19
+ export declare function uninstallHook(projectRoot: string): Promise<HookUninstallResult>;
16
20
  //# sourceMappingURL=installer.d.ts.map
@@ -42,12 +42,20 @@ export async function installHook(projectRoot) {
42
42
  }
43
43
  catch {
44
44
  // File exists but has invalid JSON — don't destroy it
45
- return { installed: false, message: `Settings file exists but contains invalid JSON: ${settingsPath}. Fix it manually before installing hooks.` };
45
+ return {
46
+ installed: false,
47
+ fatal: true,
48
+ message: `Settings file exists but contains invalid JSON: ${settingsPath}. Fix it manually before installing hooks.`,
49
+ };
46
50
  }
47
51
  }
48
52
  catch (err) {
49
53
  if (err?.code !== 'ENOENT') {
50
- return { installed: false, message: `Cannot read settings: ${err?.message ?? err}` };
54
+ return {
55
+ installed: false,
56
+ fatal: true,
57
+ message: `Cannot read settings: ${err?.message ?? err}`,
58
+ };
51
59
  }
52
60
  // ENOENT — file doesn't exist, start fresh
53
61
  }
@@ -58,7 +66,7 @@ export async function installHook(projectRoot) {
58
66
  const hasRead = existingHooks.some((h) => h.matcher === 'Read' && isTokenPilotHook(h));
59
67
  const hasEdit = existingHooks.some((h) => h.matcher === 'Edit' && isTokenPilotHook(h));
60
68
  if (hasRead && hasEdit) {
61
- return { installed: false, message: 'Token Pilot hooks already installed.' };
69
+ return { installed: false, fatal: false, message: 'Token Pilot hooks already installed.' };
62
70
  }
63
71
  // Add missing hooks
64
72
  for (const hookDef of HOOK_CONFIG.hooks.PreToolUse) {
@@ -77,12 +85,13 @@ export async function installHook(projectRoot) {
77
85
  await writeFile(settingsPath, JSON.stringify(settings, null, 2) + '\n');
78
86
  return {
79
87
  installed: true,
88
+ fatal: false,
80
89
  message: `Hooks installed at ${settingsPath}. Token Pilot will block unbounded Read on large code files and suggest read_for_edit before Edit.`,
81
90
  };
82
91
  }
83
92
  catch (err) {
84
93
  const msg = err instanceof Error ? err.message : String(err);
85
- return { installed: false, message: `Failed to install hook: ${msg}` };
94
+ return { installed: false, fatal: true, message: `Failed to install hook: ${msg}` };
86
95
  }
87
96
  }
88
97
  /**
@@ -94,7 +103,7 @@ export async function uninstallHook(projectRoot) {
94
103
  const raw = await readFile(settingsPath, 'utf-8');
95
104
  const settings = JSON.parse(raw);
96
105
  if (!settings.hooks?.PreToolUse) {
97
- return { removed: false, message: 'No hooks to remove.' };
106
+ return { removed: false, fatal: false, message: 'No hooks to remove.' };
98
107
  }
99
108
  settings.hooks.PreToolUse = settings.hooks.PreToolUse.filter((h) => !h.hooks?.some((hook) => hook.command?.includes('token-pilot')));
100
109
  if (settings.hooks.PreToolUse.length === 0) {
@@ -104,13 +113,20 @@ export async function uninstallHook(projectRoot) {
104
113
  delete settings.hooks;
105
114
  }
106
115
  await writeFile(settingsPath, JSON.stringify(settings, null, 2) + '\n');
107
- return { removed: true, message: 'Token Pilot hook removed.' };
116
+ return { removed: true, fatal: false, message: 'Token Pilot hook removed.' };
108
117
  }
109
118
  catch (err) {
110
119
  if (err?.code === 'ENOENT') {
111
- return { removed: false, message: 'Settings file not found.' };
120
+ return { removed: false, fatal: false, message: 'Settings file not found.' };
112
121
  }
113
- return { removed: false, message: `Failed to process settings: ${err?.message ?? err}` };
122
+ if (err instanceof SyntaxError) {
123
+ return {
124
+ removed: false,
125
+ fatal: true,
126
+ message: `Settings file contains invalid JSON: ${settingsPath}. Fix it manually before uninstalling hooks.`,
127
+ };
128
+ }
129
+ return { removed: false, fatal: true, message: `Failed to process settings: ${err?.message ?? err}` };
114
130
  }
115
131
  }
116
132
  //# sourceMappingURL=installer.js.map
package/dist/index.d.ts CHANGED
@@ -1,3 +1,18 @@
1
1
  #!/usr/bin/env node
2
- export {};
2
+ export declare const CODE_EXTENSIONS: Set<string>;
3
+ export declare function getVersion(): string;
4
+ export declare function main(cliArgs?: string[]): Promise<void>;
5
+ export declare function startServer(cliArgs?: string[]): Promise<void>;
6
+ export declare function handleHookRead(filePathArg?: string): void;
7
+ export declare function handleHookEdit(): void;
8
+ export declare function handleInstallHook(projectRoot: string): Promise<void>;
9
+ export declare function handleUninstallHook(projectRoot: string): Promise<void>;
10
+ export declare function handleInstallAstIndex(): Promise<void>;
11
+ export declare function handleDoctor(): Promise<void>;
12
+ export declare function handleInit(targetDir: string): Promise<void>;
13
+ export declare function checkNpmLatest(packageName: string): Promise<string | null>;
14
+ import type { TokenPilotConfig } from './types.js';
15
+ import type { BinaryStatus } from './ast-index/binary-manager.js';
16
+ export declare function checkAllUpdates(config: TokenPilotConfig, binaryStatus: BinaryStatus): Promise<void>;
17
+ export declare function printHelp(): void;
3
18
  //# sourceMappingURL=index.d.ts.map
package/dist/index.js CHANGED
@@ -3,6 +3,7 @@ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'
3
3
  import { readFileSync } from 'node:fs';
4
4
  import { execFile } from 'node:child_process';
5
5
  import { promisify } from 'node:util';
6
+ import { fileURLToPath } from 'node:url';
6
7
  import { createServer } from './server.js';
7
8
  import { installHook, uninstallHook } from './hooks/installer.js';
8
9
  import { findBinary, installBinary, checkBinaryUpdate, isNewerVersion } from './ast-index/binary-manager.js';
@@ -10,14 +11,14 @@ import { loadConfig } from './config/loader.js';
10
11
  import { isDangerousRoot } from './core/validation.js';
11
12
  const execFileAsync = promisify(execFile);
12
13
  const HOOK_DENY_THRESHOLD = 500;
13
- const CODE_EXTENSIONS = new Set([
14
+ export const CODE_EXTENSIONS = new Set([
14
15
  'ts', 'tsx', 'js', 'jsx', 'mjs', 'py', 'go', 'rs', 'java', 'kt', 'kts',
15
16
  'swift', 'cs', 'cpp', 'cc', 'cxx', 'hpp', 'c', 'h', 'php', 'rb', 'scala',
16
17
  'dart', 'lua', 'sh', 'bash', 'sql', 'r', 'vue', 'svelte', 'pl', 'pm',
17
18
  'ex', 'exs', 'groovy', 'm', 'proto', 'bsl',
18
19
  'lisp', 'lsp', 'cl', 'asd',
19
20
  ]);
20
- function getVersion() {
21
+ export function getVersion() {
21
22
  try {
22
23
  const pkgPath = new URL('../package.json', import.meta.url).pathname;
23
24
  const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'));
@@ -27,50 +28,48 @@ function getVersion() {
27
28
  return '0.0.0';
28
29
  }
29
30
  }
30
- const args = process.argv.slice(2);
31
- switch (args[0]) {
32
- case 'hook-read':
33
- handleHookRead(args[1]);
34
- break;
35
- case 'hook-edit':
36
- handleHookEdit();
37
- break;
38
- case 'install-hook':
39
- handleInstallHook(args[1] || process.cwd());
40
- break;
41
- case 'uninstall-hook':
42
- handleUninstallHook(args[1] || process.cwd());
43
- break;
44
- case 'install-ast-index':
45
- handleInstallAstIndex();
46
- break;
47
- case 'doctor':
48
- handleDoctor();
49
- break;
50
- case 'init':
51
- handleInit(args[1] || process.cwd());
52
- break;
53
- case '--version':
54
- case '-v':
55
- console.log(getVersion());
56
- process.exit(0);
57
- break;
58
- case '--help':
59
- case '-h':
60
- printHelp();
61
- break;
62
- default:
63
- startServer().catch(err => {
64
- console.error(`[token-pilot] Fatal: ${err instanceof Error ? err.message : err}`);
65
- process.exit(1);
66
- });
67
- break;
31
+ export async function main(cliArgs = process.argv.slice(2)) {
32
+ switch (cliArgs[0]) {
33
+ case 'hook-read':
34
+ handleHookRead(cliArgs[1]);
35
+ return;
36
+ case 'hook-edit':
37
+ handleHookEdit();
38
+ return;
39
+ case 'install-hook':
40
+ await handleInstallHook(cliArgs[1] || process.cwd());
41
+ return;
42
+ case 'uninstall-hook':
43
+ await handleUninstallHook(cliArgs[1] || process.cwd());
44
+ return;
45
+ case 'install-ast-index':
46
+ await handleInstallAstIndex();
47
+ return;
48
+ case 'doctor':
49
+ await handleDoctor();
50
+ return;
51
+ case 'init':
52
+ await handleInit(cliArgs[1] || process.cwd());
53
+ return;
54
+ case '--version':
55
+ case '-v':
56
+ console.log(getVersion());
57
+ process.exit(0);
58
+ return;
59
+ case '--help':
60
+ case '-h':
61
+ printHelp();
62
+ return;
63
+ default:
64
+ await startServer(cliArgs);
65
+ return;
66
+ }
68
67
  }
69
- async function startServer() {
70
- let projectRoot = args[0] || process.cwd();
68
+ export async function startServer(cliArgs = process.argv.slice(2)) {
69
+ let projectRoot = cliArgs[0] || process.cwd();
71
70
  // Detect git root for reliable project root
72
71
  // Try multiple sources: args[0] → INIT_CWD (npm/npx invoking dir) → PWD → cwd
73
- if (!args[0]) {
72
+ if (!cliArgs[0]) {
74
73
  const candidates = [
75
74
  process.env.INIT_CWD, // npm/npx sets this to invoking directory
76
75
  process.env.PWD, // shell working directory (may differ from cwd)
@@ -140,7 +139,7 @@ async function startServer() {
140
139
  process.exit(0);
141
140
  });
142
141
  }
143
- function handleHookRead(filePathArg) {
142
+ export function handleHookRead(filePathArg) {
144
143
  // Parse stdin (Claude Code hook format) to get tool_input
145
144
  let filePath = filePathArg;
146
145
  let hasOffset = false;
@@ -193,7 +192,7 @@ function handleHookRead(filePathArg) {
193
192
  process.stdout.write(deny);
194
193
  process.exit(0);
195
194
  }
196
- function handleHookEdit() {
195
+ export function handleHookEdit() {
197
196
  // Parse stdin for Edit tool_input
198
197
  let filePath;
199
198
  try {
@@ -223,17 +222,17 @@ function handleHookEdit() {
223
222
  process.stdout.write(context);
224
223
  process.exit(0);
225
224
  }
226
- async function handleInstallHook(projectRoot) {
225
+ export async function handleInstallHook(projectRoot) {
227
226
  const result = await installHook(projectRoot);
228
227
  console.log(result.message);
229
- process.exit(result.installed ? 0 : 1);
228
+ process.exit(result.fatal ? 1 : 0);
230
229
  }
231
- async function handleUninstallHook(projectRoot) {
230
+ export async function handleUninstallHook(projectRoot) {
232
231
  const result = await uninstallHook(projectRoot);
233
232
  console.log(result.message);
234
- process.exit(result.removed ? 0 : 1);
233
+ process.exit(result.fatal ? 1 : 0);
235
234
  }
236
- async function handleInstallAstIndex() {
235
+ export async function handleInstallAstIndex() {
237
236
  const status = await findBinary();
238
237
  if (status.available) {
239
238
  // Check if update is available
@@ -256,7 +255,7 @@ async function handleInstallAstIndex() {
256
255
  process.exit(1);
257
256
  }
258
257
  }
259
- async function handleDoctor() {
258
+ export async function handleDoctor() {
260
259
  const version = getVersion();
261
260
  const { existsSync } = await import('node:fs');
262
261
  const { join } = await import('node:path');
@@ -324,7 +323,7 @@ async function handleDoctor() {
324
323
  console.log('');
325
324
  process.exit(0);
326
325
  }
327
- async function handleInit(targetDir) {
326
+ export async function handleInit(targetDir) {
328
327
  const { existsSync, readFileSync: readFs, writeFileSync } = await import('node:fs');
329
328
  const { join } = await import('node:path');
330
329
  const mcpPath = join(targetDir, '.mcp.json');
@@ -379,7 +378,7 @@ async function handleInit(targetDir) {
379
378
  // ──────────────────────────────────────────────
380
379
  // Update checking
381
380
  // ──────────────────────────────────────────────
382
- async function checkNpmLatest(packageName) {
381
+ export async function checkNpmLatest(packageName) {
383
382
  try {
384
383
  const controller = new AbortController();
385
384
  const timeout = setTimeout(() => controller.abort(), 3000);
@@ -396,7 +395,7 @@ async function checkNpmLatest(packageName) {
396
395
  return null;
397
396
  }
398
397
  }
399
- async function checkAllUpdates(config, binaryStatus) {
398
+ export async function checkAllUpdates(config, binaryStatus) {
400
399
  if (!config.updates.checkOnStartup)
401
400
  return;
402
401
  const [tpLatest, astUpdate, cmLatest] = await Promise.allSettled([
@@ -427,7 +426,7 @@ async function checkAllUpdates(config, binaryStatus) {
427
426
  // On startup, we only notify if explicitly useful.
428
427
  }
429
428
  }
430
- function printHelp() {
429
+ export function printHelp() {
431
430
  console.log(`token-pilot v${getVersion()} — MCP server for token-efficient code reading
432
431
 
433
432
  Usage:
@@ -450,4 +449,11 @@ MCP Tools (18):
450
449
  `);
451
450
  process.exit(0);
452
451
  }
452
+ const isDirectRun = process.argv[1] !== undefined && fileURLToPath(import.meta.url) === process.argv[1];
453
+ if (isDirectRun) {
454
+ main().catch(err => {
455
+ console.error(`[token-pilot] Fatal: ${err instanceof Error ? err.message : err}`);
456
+ process.exit(1);
457
+ });
458
+ }
453
459
  //# sourceMappingURL=index.js.map