token-pilot 0.9.0 → 0.12.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.
@@ -0,0 +1,321 @@
1
+ import { execFile } from 'node:child_process';
2
+ import { promisify } from 'node:util';
3
+ import { estimateTokens } from '../core/token-estimator.js';
4
+ const execFileAsync = promisify(execFile);
5
+ // ──────────────────────────────────────────────
6
+ // Handler
7
+ // ──────────────────────────────────────────────
8
+ export async function handleTestSummary(args, projectRoot) {
9
+ const command = args.command;
10
+ const parts = command.match(/(?:[^\s"]+|"[^"]*")+/g) ?? [command];
11
+ const bin = parts[0];
12
+ const binArgs = parts.slice(1).map(a => a.replace(/^"|"$/g, ''));
13
+ let rawOutput;
14
+ let exitCode = 0;
15
+ try {
16
+ const { stdout, stderr } = await execFileAsync(bin, binArgs, {
17
+ cwd: projectRoot,
18
+ timeout: args.timeout ?? 60000,
19
+ maxBuffer: 10 * 1024 * 1024,
20
+ env: { ...process.env, FORCE_COLOR: '0', NO_COLOR: '1', CI: '1' },
21
+ });
22
+ rawOutput = stdout + '\n' + stderr;
23
+ }
24
+ catch (err) {
25
+ // Test runners exit with non-zero when tests fail — that's expected
26
+ const execErr = err;
27
+ rawOutput = (execErr.stdout ?? '') + '\n' + (execErr.stderr ?? '');
28
+ exitCode = execErr.code ?? 1;
29
+ // If no output at all, it's a real error
30
+ if (!rawOutput.trim()) {
31
+ return {
32
+ content: [{ type: 'text', text: `Command failed: ${command}\n${err instanceof Error ? err.message : String(err)}` }],
33
+ rawTokens: 0,
34
+ };
35
+ }
36
+ }
37
+ const rawTokens = estimateTokens(rawOutput);
38
+ const runner = args.runner ?? detectRunner(command, rawOutput);
39
+ const result = parseTestOutput(rawOutput, runner);
40
+ const formatted = formatTestSummary(result, command, runner, rawTokens);
41
+ return {
42
+ content: [{ type: 'text', text: formatted }],
43
+ rawTokens,
44
+ };
45
+ }
46
+ // ──────────────────────────────────────────────
47
+ // Runner detection
48
+ // ──────────────────────────────────────────────
49
+ export function detectRunner(command, output) {
50
+ const cmd = command.toLowerCase();
51
+ if (cmd.includes('vitest'))
52
+ return 'vitest';
53
+ if (cmd.includes('jest'))
54
+ return 'jest';
55
+ if (cmd.includes('pytest') || cmd.includes('python -m pytest'))
56
+ return 'pytest';
57
+ if (cmd.includes('phpunit'))
58
+ return 'phpunit';
59
+ if (cmd.includes('cargo test'))
60
+ return 'cargo';
61
+ if (cmd.includes('go test'))
62
+ return 'go';
63
+ if (cmd.includes('rspec'))
64
+ return 'rspec';
65
+ if (cmd.includes('mocha'))
66
+ return 'mocha';
67
+ // Detect from output
68
+ const lower = output.toLowerCase();
69
+ if (lower.includes('vitest') || lower.includes('vite'))
70
+ return 'vitest';
71
+ if (lower.includes('jest'))
72
+ return 'jest';
73
+ if (lower.includes('pytest') || (lower.includes('=== ') && lower.includes(' passed')))
74
+ return 'pytest';
75
+ if (lower.includes('phpunit'))
76
+ return 'phpunit';
77
+ if (lower.includes('--- fail:') || lower.includes('--- pass:') || lower.includes('ok \t'))
78
+ return 'go';
79
+ return 'generic';
80
+ }
81
+ // ──────────────────────────────────────────────
82
+ // Parsers
83
+ // ──────────────────────────────────────────────
84
+ export function parseTestOutput(output, runner) {
85
+ switch (runner) {
86
+ case 'vitest':
87
+ case 'jest':
88
+ return parseVitestJest(output);
89
+ case 'pytest':
90
+ return parsePytest(output);
91
+ case 'phpunit':
92
+ return parsePhpunit(output);
93
+ case 'go':
94
+ return parseGoTest(output);
95
+ case 'cargo':
96
+ return parseCargoTest(output);
97
+ default:
98
+ return parseGeneric(output);
99
+ }
100
+ }
101
+ function parseVitestJest(output) {
102
+ const result = { total: 0, passed: 0, failed: 0, skipped: 0, failures: [] };
103
+ // Test Files 12 passed (12) OR Tests 170 passed (170) OR Tests 3 failed (3)
104
+ const testsLine = output.match(/Tests?\s+(?:(\d+)\s+failed\s*\|?\s*)?(?:(\d+)\s+passed\s*)?(?:\|?\s*(\d+)\s+skipped)?\s*\((\d+)\)/);
105
+ if (testsLine) {
106
+ result.failed = parseInt(testsLine[1] ?? '0', 10);
107
+ result.passed = parseInt(testsLine[2] ?? '0', 10);
108
+ result.skipped = parseInt(testsLine[3] ?? '0', 10);
109
+ result.total = parseInt(testsLine[4], 10);
110
+ }
111
+ // Test Files count
112
+ const suitesLine = output.match(/Test Files\s+(?:\d+\s+failed\s*\|?\s*)?(\d+)\s+passed\s*\((\d+)\)/);
113
+ if (suitesLine) {
114
+ result.suites = parseInt(suitesLine[2], 10);
115
+ }
116
+ // Duration
117
+ const duration = output.match(/Duration\s+([\d.]+\w?\s*(?:\([^)]+\))?)/);
118
+ if (duration) {
119
+ result.duration = duration[1].trim();
120
+ }
121
+ // Parse failures
122
+ // FAIL tests/foo.test.ts > describe > test name
123
+ const failBlocks = output.split(/(?:FAIL|✕|×)\s+/).slice(1);
124
+ for (const block of failBlocks.slice(0, 10)) {
125
+ const lines = block.split('\n');
126
+ const firstLine = lines[0]?.trim() ?? '';
127
+ const errorLines = [];
128
+ for (let i = 1; i < Math.min(lines.length, 8); i++) {
129
+ const line = lines[i]?.trim();
130
+ if (!line)
131
+ continue;
132
+ if (line.startsWith('at ') || line.startsWith('❯'))
133
+ continue;
134
+ if (line.startsWith('⎯') || line.startsWith('─'))
135
+ break;
136
+ errorLines.push(line);
137
+ }
138
+ if (firstLine) {
139
+ result.failures.push({
140
+ name: firstLine.substring(0, 200),
141
+ error: errorLines.join('\n').substring(0, 300),
142
+ });
143
+ }
144
+ }
145
+ return result;
146
+ }
147
+ function parsePytest(output) {
148
+ const result = { total: 0, passed: 0, failed: 0, skipped: 0, failures: [] };
149
+ // === 5 passed, 1 failed, 2 skipped in 1.23s ===
150
+ const summary = output.match(/=+\s*(.*?)\s*=+\s*$/m);
151
+ if (summary) {
152
+ const parts = summary[1];
153
+ const passed = parts.match(/(\d+)\s+passed/);
154
+ const failed = parts.match(/(\d+)\s+failed/);
155
+ const skipped = parts.match(/(\d+)\s+skipped/);
156
+ const duration = parts.match(/in\s+([\d.]+s)/);
157
+ result.passed = parseInt(passed?.[1] ?? '0', 10);
158
+ result.failed = parseInt(failed?.[1] ?? '0', 10);
159
+ result.skipped = parseInt(skipped?.[1] ?? '0', 10);
160
+ result.total = result.passed + result.failed + result.skipped;
161
+ if (duration)
162
+ result.duration = duration[1];
163
+ }
164
+ // FAILED tests/test_foo.py::test_bar - AssertionError
165
+ const failedPattern = /^FAILED\s+(\S+)\s*-?\s*(.*)/gm;
166
+ let match;
167
+ while ((match = failedPattern.exec(output)) !== null) {
168
+ const [, name, error] = match;
169
+ result.failures.push({
170
+ name: name.substring(0, 200),
171
+ error: (error || '').substring(0, 300),
172
+ });
173
+ }
174
+ return result;
175
+ }
176
+ function parsePhpunit(output) {
177
+ const result = { total: 0, passed: 0, failed: 0, skipped: 0, failures: [] };
178
+ // OK (5 tests, 10 assertions) or FAILURES! Tests: 5, Assertions: 10, Failures: 2, Errors: 1
179
+ const ok = output.match(/OK\s*\((\d+)\s+test/);
180
+ if (ok) {
181
+ result.total = parseInt(ok[1], 10);
182
+ result.passed = result.total;
183
+ }
184
+ const failures = output.match(/Tests:\s*(\d+).*?Failures:\s*(\d+)/);
185
+ if (failures) {
186
+ result.total = parseInt(failures[1], 10);
187
+ result.failed = parseInt(failures[2], 10);
188
+ // PHPUnit also reports Errors separately from Failures
189
+ const errors = output.match(/Errors:\s*(\d+)/);
190
+ if (errors) {
191
+ result.failed += parseInt(errors[1], 10);
192
+ }
193
+ result.passed = result.total - result.failed - result.skipped;
194
+ }
195
+ const duration = output.match(/Time:\s*([\d.:]+\s*\w*)/);
196
+ if (duration)
197
+ result.duration = duration[1].trim();
198
+ // 1) TestClass::testMethod
199
+ const failPattern = /^\d+\)\s+(\S+::\S+)/gm;
200
+ let match;
201
+ while ((match = failPattern.exec(output)) !== null) {
202
+ result.failures.push({
203
+ name: match[1].substring(0, 200),
204
+ error: '',
205
+ });
206
+ }
207
+ return result;
208
+ }
209
+ function parseGoTest(output) {
210
+ const result = { total: 0, passed: 0, failed: 0, skipped: 0, failures: [] };
211
+ const passLines = output.match(/^---\s+PASS:/gm);
212
+ const failLines = output.match(/^---\s+FAIL:/gm);
213
+ const skipLines = output.match(/^---\s+SKIP:/gm);
214
+ result.passed = passLines?.length ?? 0;
215
+ result.failed = failLines?.length ?? 0;
216
+ result.skipped = skipLines?.length ?? 0;
217
+ result.total = result.passed + result.failed + result.skipped;
218
+ // --- FAIL: TestFoo (0.00s)
219
+ const failPattern = /^---\s+FAIL:\s+(\S+)\s+\(([^)]+)\)/gm;
220
+ let match;
221
+ while ((match = failPattern.exec(output)) !== null) {
222
+ result.failures.push({
223
+ name: match[1],
224
+ error: `duration: ${match[2]}`,
225
+ });
226
+ }
227
+ // If zero counted, try "ok" / "FAIL" summary lines
228
+ if (result.total === 0) {
229
+ const okCount = (output.match(/^ok\s+/gm) ?? []).length;
230
+ const failCount = (output.match(/^FAIL\s+/gm) ?? []).length;
231
+ result.passed = okCount;
232
+ result.failed = failCount;
233
+ result.total = okCount + failCount;
234
+ }
235
+ return result;
236
+ }
237
+ function parseCargoTest(output) {
238
+ const result = { total: 0, passed: 0, failed: 0, skipped: 0, failures: [] };
239
+ // test result: ok. 5 passed; 0 failed; 1 ignored; 0 measured; 0 filtered out
240
+ const summary = output.match(/test result:\s*\w+\.\s*(\d+)\s+passed;\s*(\d+)\s+failed;\s*(\d+)\s+ignored/);
241
+ if (summary) {
242
+ result.passed = parseInt(summary[1], 10);
243
+ result.failed = parseInt(summary[2], 10);
244
+ result.skipped = parseInt(summary[3], 10);
245
+ result.total = result.passed + result.failed + result.skipped;
246
+ }
247
+ // Cargo outputs two "failures:" sections:
248
+ // 1. Detail section: "failures:\n\n---- test_name stdout ----\n..."
249
+ // 2. Name-list section: "failures:\n test_name_1\n test_name_2\n"
250
+ // We want the name-list section (the last one before "test result:")
251
+ const failSections = output.split(/^failures:\s*$/m).slice(1);
252
+ for (const section of failSections) {
253
+ // The name-list section has indented test names without "---- ... ----"
254
+ const lines = section.split('\n').filter(l => l.trim());
255
+ const isNameList = lines.length > 0 && lines.every(l => /^\s+\S+/.test(l) && !l.includes('----'));
256
+ if (isNameList) {
257
+ for (const line of lines.slice(0, 10)) {
258
+ result.failures.push({ name: line.trim(), error: '' });
259
+ }
260
+ break;
261
+ }
262
+ }
263
+ return result;
264
+ }
265
+ function parseGeneric(output) {
266
+ const result = { total: 0, passed: 0, failed: 0, skipped: 0, failures: [] };
267
+ // Try common patterns
268
+ const passedMatch = output.match(/(\d+)\s+(?:passed|passing|ok|success)/i);
269
+ const failedMatch = output.match(/(\d+)\s+(?:failed|failing|error|fail)/i);
270
+ const skippedMatch = output.match(/(\d+)\s+(?:skipped|pending|ignored)/i);
271
+ const totalMatch = output.match(/(\d+)\s+(?:total|tests?|specs?)\b/i);
272
+ result.passed = parseInt(passedMatch?.[1] ?? '0', 10);
273
+ result.failed = parseInt(failedMatch?.[1] ?? '0', 10);
274
+ result.skipped = parseInt(skippedMatch?.[1] ?? '0', 10);
275
+ result.total = totalMatch
276
+ ? parseInt(totalMatch[1], 10)
277
+ : result.passed + result.failed + result.skipped;
278
+ return result;
279
+ }
280
+ // ──────────────────────────────────────────────
281
+ // Formatter
282
+ // ──────────────────────────────────────────────
283
+ function formatTestSummary(result, command, runner, rawTokens) {
284
+ const lines = [];
285
+ const status = result.failed > 0 ? '❌ FAIL' : '✅ PASS';
286
+ lines.push(`TEST RESULT: ${status} (${runner})`);
287
+ lines.push('');
288
+ // Stats line
289
+ const parts = [];
290
+ parts.push(`${result.total} total`);
291
+ parts.push(`${result.passed} passed`);
292
+ if (result.failed > 0)
293
+ parts.push(`${result.failed} failed`);
294
+ if (result.skipped > 0)
295
+ parts.push(`${result.skipped} skipped`);
296
+ if (result.duration)
297
+ parts.push(`${result.duration}`);
298
+ if (result.suites)
299
+ parts.push(`${result.suites} suites`);
300
+ lines.push(parts.join(' | '));
301
+ // Failed tests detail
302
+ if (result.failures.length > 0) {
303
+ lines.push('');
304
+ lines.push('FAILURES:');
305
+ for (const f of result.failures.slice(0, 10)) {
306
+ lines.push(` ✗ ${f.name}`);
307
+ if (f.error) {
308
+ for (const errLine of f.error.split('\n').slice(0, 3)) {
309
+ lines.push(` ${errLine}`);
310
+ }
311
+ }
312
+ }
313
+ if (result.failures.length > 10) {
314
+ lines.push(` ... and ${result.failures.length - 10} more failures`);
315
+ }
316
+ }
317
+ lines.push('');
318
+ lines.push(`RAW OUTPUT: ~${rawTokens} tokens → test_summary: ~${estimateTokens(lines.join('\n'))} tokens`);
319
+ return lines.join('\n');
320
+ }
321
+ //# sourceMappingURL=test-summary.js.map
package/dist/index.js CHANGED
@@ -5,7 +5,8 @@ import { execFile } from 'node:child_process';
5
5
  import { promisify } from 'node:util';
6
6
  import { createServer } from './server.js';
7
7
  import { installHook, uninstallHook } from './hooks/installer.js';
8
- import { findBinary, installBinary } from './ast-index/binary-manager.js';
8
+ import { findBinary, installBinary, checkBinaryUpdate, isNewerVersion } from './ast-index/binary-manager.js';
9
+ import { loadConfig } from './config/loader.js';
9
10
  import { isDangerousRoot } from './core/validation.js';
10
11
  const execFileAsync = promisify(execFile);
11
12
  const HOOK_DENY_THRESHOLD = 500;
@@ -14,6 +15,7 @@ const CODE_EXTENSIONS = new Set([
14
15
  'swift', 'cs', 'cpp', 'cc', 'cxx', 'hpp', 'c', 'h', 'php', 'rb', 'scala',
15
16
  'dart', 'lua', 'sh', 'bash', 'sql', 'r', 'vue', 'svelte', 'pl', 'pm',
16
17
  'ex', 'exs', 'groovy', 'm', 'proto', 'bsl',
18
+ 'lisp', 'lsp', 'cl', 'asd',
17
19
  ]);
18
20
  function getVersion() {
19
21
  try {
@@ -114,12 +116,10 @@ async function startServer() {
114
116
  ` Fix: pass project path explicitly — token-pilot /path/to/project\n` +
115
117
  ` Or configure mcpServers with "args": ["/path/to/project"]`);
116
118
  }
117
- // Non-blocking update check (logs to stderr, never blocks startup)
118
- checkLatestVersion().then(latest => {
119
- if (latest && latest !== getVersion()) {
120
- console.error(`[token-pilot] Update available: ${getVersion()} ${latest}. Run: npx token-pilot@latest`);
121
- }
122
- }).catch(() => { });
119
+ // Non-blocking update check for all components (logs to stderr, never blocks startup)
120
+ const config = await loadConfig(projectRoot);
121
+ const binaryStatus = await findBinary(config.astIndex.binaryPath);
122
+ checkAllUpdates(config, binaryStatus).catch(() => { });
123
123
  // Auto-install PreToolUse hook (non-blocking, Claude Code only)
124
124
  installHook(projectRoot).then(result => {
125
125
  if (result.installed) {
@@ -236,8 +236,15 @@ async function handleUninstallHook(projectRoot) {
236
236
  async function handleInstallAstIndex() {
237
237
  const status = await findBinary();
238
238
  if (status.available) {
239
- console.log(`ast-index ${status.version} already available at ${status.path} (${status.source})`);
240
- process.exit(0);
239
+ // Check if update is available
240
+ const update = await checkBinaryUpdate(status.path);
241
+ if (update.updateAvailable) {
242
+ console.log(`ast-index ${update.current} installed, updating to ${update.latest}...`);
243
+ }
244
+ else {
245
+ console.log(`ast-index ${status.version} already up to date at ${status.path} (${status.source})`);
246
+ process.exit(0);
247
+ }
241
248
  }
242
249
  try {
243
250
  const result = await installBinary((msg) => console.log(msg));
@@ -251,43 +258,69 @@ async function handleInstallAstIndex() {
251
258
  }
252
259
  async function handleDoctor() {
253
260
  const version = getVersion();
254
- console.log(`token-pilot v${version}\n`);
255
- // Check Node.js version
261
+ const { existsSync } = await import('node:fs');
262
+ const { join } = await import('node:path');
263
+ const cwd = process.cwd();
264
+ console.log(`token-pilot doctor v${version}\n`);
265
+ // ── Environment ──
256
266
  const nodeVersion = process.version;
257
267
  const nodeMajor = parseInt(nodeVersion.slice(1), 10);
258
- console.log(`Node.js: ${nodeVersion} ${nodeMajor >= 18 ? '✓' : '✗ (requires >=18)'}`);
259
- // Check ast-index
260
- const astStatus = await findBinary();
261
- if (astStatus.available) {
262
- console.log(`ast-index: ${astStatus.version}(${astStatus.source}: ${astStatus.path})`);
268
+ console.log(`Node.js: ${nodeVersion} ${nodeMajor >= 18 ? '✓' : '✗ (requires >=18)'}`);
269
+ const configPath = join(cwd, '.token-pilot.json');
270
+ console.log(`config: ${existsSync(configPath) ? configPath + ' ✓' : 'default (no .token-pilot.json)'}`);
271
+ const gitDir = join(cwd, '.git');
272
+ console.log(`git repo: ${existsSync(gitDir) ? 'yes ' : 'no (read_diff/git features unavailable)'}`);
273
+ console.log('');
274
+ // ── token-pilot ──
275
+ console.log('── token-pilot ──');
276
+ console.log(` installed: ${version}`);
277
+ const tpLatest = await checkNpmLatest('token-pilot');
278
+ if (tpLatest) {
279
+ if (isNewerVersion(version, tpLatest)) {
280
+ console.log(` latest: ${tpLatest} (update available!)`);
281
+ console.log(` run: npx clear-npx-cache && npx -y token-pilot@latest`);
282
+ }
283
+ else {
284
+ console.log(` latest: ${tpLatest} ✓ (up to date)`);
285
+ }
263
286
  }
264
287
  else {
265
- console.log(`ast-index: not found ✗`);
266
- console.log(` Run: npx token-pilot install-ast-index`);
267
- }
268
- // Check for updates
269
- const latest = await checkLatestVersion();
270
- if (latest) {
271
- if (latest !== version) {
272
- console.log(`npm version: ${latest} (current: ${version} — update available!)`);
273
- console.log(` Run: npx clear-npx-cache && npx -y token-pilot@latest`);
288
+ console.log(` latest: could not check (network error)`);
289
+ }
290
+ console.log('');
291
+ // ── ast-index ──
292
+ console.log('── ast-index ──');
293
+ const astStatus = await findBinary();
294
+ if (astStatus.available) {
295
+ console.log(` installed: ${astStatus.version} (${astStatus.source}: ${astStatus.path})`);
296
+ const astUpdate = await checkBinaryUpdate(astStatus.path);
297
+ if (astUpdate.updateAvailable) {
298
+ console.log(` latest: ${astUpdate.latest} (update available!)`);
299
+ console.log(` run: npx token-pilot install-ast-index`);
274
300
  }
275
- else {
276
- console.log(`npm version: ${latest} ✓ (up to date)`);
301
+ else if (astUpdate.latest) {
302
+ console.log(` latest: ${astUpdate.latest} ✓ (up to date)`);
277
303
  }
304
+ const config = await loadConfig(cwd);
305
+ console.log(` auto-update: ${config.updates.autoUpdate ? 'enabled ✓' : 'disabled (set updates.autoUpdate=true in .token-pilot.json)'}`);
278
306
  }
279
307
  else {
280
- console.log(`npm version: could not check (network error)`);
308
+ console.log(` installed: not found ✗`);
309
+ console.log(` run: npx token-pilot install-ast-index`);
310
+ }
311
+ console.log('');
312
+ // ── context-mode ──
313
+ console.log('── context-mode ──');
314
+ const { detectContextMode } = await import('./integration/context-mode-detector.js');
315
+ const cmStatus = await detectContextMode(cwd);
316
+ console.log(` detected: ${cmStatus.detected ? `yes (${cmStatus.source})` : 'no'}`);
317
+ const cmLatest = await checkNpmLatest('claude-context-mode');
318
+ if (cmLatest) {
319
+ console.log(` latest npm: ${cmLatest}`);
320
+ }
321
+ if (!cmStatus.detected) {
322
+ console.log(` setup: npx token-pilot init`);
281
323
  }
282
- // Check config
283
- const { existsSync } = await import('node:fs');
284
- const { join } = await import('node:path');
285
- const cwd = process.cwd();
286
- const configPath = join(cwd, '.token-pilot.json');
287
- console.log(`config: ${existsSync(configPath) ? configPath + ' ✓' : 'default (no .token-pilot.json)'}`);
288
- // Check git
289
- const gitDir = join(cwd, '.git');
290
- console.log(`git repo: ${existsSync(gitDir) ? 'yes ✓' : 'no (read_diff/git features unavailable)'}`);
291
324
  console.log('');
292
325
  process.exit(0);
293
326
  }
@@ -343,11 +376,14 @@ async function handleInit(targetDir) {
343
376
  console.log(`\nRestart your AI assistant to activate.`);
344
377
  process.exit(0);
345
378
  }
346
- async function checkLatestVersion() {
379
+ // ──────────────────────────────────────────────
380
+ // Update checking
381
+ // ──────────────────────────────────────────────
382
+ async function checkNpmLatest(packageName) {
347
383
  try {
348
384
  const controller = new AbortController();
349
385
  const timeout = setTimeout(() => controller.abort(), 3000);
350
- const resp = await fetch('https://registry.npmjs.org/token-pilot/latest', {
386
+ const resp = await fetch(`https://registry.npmjs.org/${packageName}/latest`, {
351
387
  signal: controller.signal,
352
388
  });
353
389
  clearTimeout(timeout);
@@ -360,6 +396,37 @@ async function checkLatestVersion() {
360
396
  return null;
361
397
  }
362
398
  }
399
+ async function checkAllUpdates(config, binaryStatus) {
400
+ if (!config.updates.checkOnStartup)
401
+ return;
402
+ const [tpLatest, astUpdate, cmLatest] = await Promise.allSettled([
403
+ checkNpmLatest('token-pilot'),
404
+ binaryStatus.available ? checkBinaryUpdate(binaryStatus.path) : Promise.resolve(null),
405
+ checkNpmLatest('claude-context-mode'),
406
+ ]);
407
+ // token-pilot
408
+ const tpVersion = getVersion();
409
+ if (tpLatest.status === 'fulfilled' && tpLatest.value && isNewerVersion(tpVersion, tpLatest.value)) {
410
+ console.error(`[token-pilot] Update available: ${tpVersion} → ${tpLatest.value}. Run: npx token-pilot@latest`);
411
+ }
412
+ // ast-index
413
+ if (astUpdate.status === 'fulfilled' && astUpdate.value?.updateAvailable) {
414
+ const { current, latest } = astUpdate.value;
415
+ if (config.updates.autoUpdate) {
416
+ console.error(`[token-pilot] Auto-updating ast-index: ${current} → ${latest}...`);
417
+ installBinary(msg => console.error(`[token-pilot] ${msg}`)).catch(() => { });
418
+ }
419
+ else {
420
+ console.error(`[token-pilot] ast-index update: ${current} → ${latest}. Run: token-pilot install-ast-index`);
421
+ }
422
+ }
423
+ // context-mode (notification only — runs as separate MCP server)
424
+ if (cmLatest.status === 'fulfilled' && cmLatest.value) {
425
+ // We can't reliably detect the currently installed version of context-mode
426
+ // (it runs as separate process via npx). Just log latest available for doctor.
427
+ // On startup, we only notify if explicitly useful.
428
+ }
429
+ }
363
430
  function printHelp() {
364
431
  console.log(`token-pilot v${getVersion()} — MCP server for token-efficient code reading
365
432
 
@@ -376,10 +443,10 @@ Usage:
376
443
  Quick start:
377
444
  npx token-pilot init Setup .mcp.json (token-pilot + context-mode)
378
445
 
379
- MCP Tools (12):
380
- smart_read, read_symbol, read_range, read_diff, smart_read_many, read_for_edit,
381
- find_usages, find_unused, related_files, outline,
382
- project_overview, session_analytics
446
+ MCP Tools (18):
447
+ smart_read, read_symbol, read_range, read_diff, read_for_edit, smart_read_many,
448
+ find_usages, find_unused, related_files, outline, project_overview, session_analytics,
449
+ code_audit, module_info, smart_diff, explore_area, smart_log, test_summary
383
450
  `);
384
451
  process.exit(0);
385
452
  }