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.
- package/.claude-plugin/marketplace.json +2 -2
- package/.claude-plugin/plugin.json +1 -1
- package/CHANGELOG.md +47 -0
- package/README.md +8 -0
- package/dist/ast-index/binary-manager.d.ts +13 -0
- package/dist/ast-index/binary-manager.js +43 -0
- package/dist/config/defaults.js +4 -0
- package/dist/core/validation.d.ts +29 -0
- package/dist/core/validation.js +88 -0
- package/dist/handlers/explore-area.d.ts +9 -0
- package/dist/handlers/explore-area.js +280 -0
- package/dist/handlers/outline.d.ts +6 -0
- package/dist/handlers/outline.js +3 -2
- package/dist/handlers/smart-diff.d.ts +35 -0
- package/dist/handlers/smart-diff.js +269 -0
- package/dist/handlers/smart-log.d.ts +21 -0
- package/dist/handlers/smart-log.js +200 -0
- package/dist/handlers/test-summary.d.ts +25 -0
- package/dist/handlers/test-summary.js +321 -0
- package/dist/index.js +110 -43
- package/dist/server.js +98 -2
- package/dist/types.d.ts +4 -0
- package/package.json +1 -1
|
@@ -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
|
-
|
|
119
|
-
|
|
120
|
-
|
|
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
|
-
|
|
240
|
-
|
|
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
|
-
|
|
255
|
-
|
|
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:
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
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(`
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
//
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
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(`
|
|
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(`
|
|
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
|
-
|
|
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(
|
|
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 (
|
|
380
|
-
smart_read, read_symbol, read_range, read_diff,
|
|
381
|
-
find_usages, find_unused, related_files, outline,
|
|
382
|
-
|
|
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
|
}
|