token-pilot 0.10.0 → 0.13.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 +35 -0
- package/README.md +4 -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 +12 -0
- package/dist/core/validation.js +44 -0
- package/dist/handlers/explore-area.js +2 -2
- package/dist/handlers/outline.js +1 -0
- package/dist/handlers/smart-diff.js +21 -9
- 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 +46 -1
- package/dist/types.d.ts +4 -0
- package/package.json +1 -1
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "token-pilot",
|
|
3
3
|
"displayName": "Token Pilot",
|
|
4
|
-
"description": "Reduces token consumption by 80-95% via AST-aware lazy file reading.
|
|
5
|
-
"version": "0.
|
|
4
|
+
"description": "Reduces token consumption by 80-95% via AST-aware lazy file reading. 18 MCP tools for structural code reading, symbol navigation, and cross-file search.",
|
|
5
|
+
"version": "0.13.0",
|
|
6
6
|
"author": "Digital-Threads",
|
|
7
7
|
"repository": "https://github.com/Digital-Threads/token-pilot",
|
|
8
8
|
"license": "MIT",
|
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,41 @@ All notable changes to Token Pilot will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [0.13.0] - 2026-03-14
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
- **Version check for all components** — on startup, checks token-pilot (npm), ast-index (GitHub releases), and context-mode (npm) in parallel. Non-blocking, fire-and-forget. Shows update notifications in stderr.
|
|
12
|
+
- **`autoUpdate` config flag** — `updates.autoUpdate: true` in `.token-pilot.json` auto-downloads new ast-index binary on startup. Default: `false` (notify only). token-pilot and context-mode only notify (separate processes).
|
|
13
|
+
- **`checkBinaryUpdate()`** — compares installed ast-index version vs latest GitHub release.
|
|
14
|
+
- **`isNewerVersion()` utility** — semver comparison: strip `v` prefix, compare segments. Handles different lengths (`1.0` vs `1.0.1`).
|
|
15
|
+
- **Common Lisp extensions** — `.lisp`, `.lsp`, `.cl`, `.asd` added to `CODE_EXTENSIONS` for ast-index v3.28+ compatibility.
|
|
16
|
+
- **9 new tests** — `isNewerVersion()` covering major/minor/patch, same version, older, `v` prefix, different segment lengths, large numbers, real-world versions. Total: 217 tests.
|
|
17
|
+
|
|
18
|
+
### Changed
|
|
19
|
+
- **`doctor` command** — now shows 3 sections: token-pilot (installed/latest), ast-index (installed/latest/auto-update status), context-mode (detected/latest npm). Previously only showed ast-index binary status.
|
|
20
|
+
- **`install-ast-index` command** — now also updates existing binary if newer version available on GitHub.
|
|
21
|
+
- **`printHelp()`** — fixed tool count: 18 (was incorrectly showing 12 since v0.8.0).
|
|
22
|
+
- **Startup update check** — replaced single `checkLatestVersion()` with `checkAllUpdates()` covering all 3 components via `Promise.allSettled`.
|
|
23
|
+
|
|
24
|
+
### Fixed
|
|
25
|
+
- **`test_summary` PHPUnit parser** — now counts both `Failures:` and `Errors:` (was only counting failures).
|
|
26
|
+
- **`test_summary` cargo parser** — correctly identifies failure name-list section (no `----` markers) vs detail section.
|
|
27
|
+
- **`test_summary` token estimation** — uses shared `estimateTokens()` instead of local duplicate.
|
|
28
|
+
- **`smart_log` category detection** — `documentation` now matches docs pattern, `tests` (plural) matches test pattern, `optimize`/`optimization` match perf pattern.
|
|
29
|
+
- **`explore_area` path boundary** — `startsWith(path + '/')` prevents `src/auth` matching `src/authorize/`.
|
|
30
|
+
- **Validation consistency** — `validateSmartLogArgs` and `validateTestSummaryArgs` now use `optionalString`/`optionalNumber` helpers, reject empty strings, check integers.
|
|
31
|
+
|
|
32
|
+
## [0.11.0] - 2026-03-14
|
|
33
|
+
|
|
34
|
+
### Added
|
|
35
|
+
- **`smart_log` tool** — structured git log with commit category detection (feat/fix/refactor/docs/test/chore/style/perf). Shows author breakdown, file stats (+/-), per-commit file list. Filters by path and ref. Raw git log → compact summary.
|
|
36
|
+
- **`test_summary` tool** — runs test command and returns structured summary: total/passed/failed/skipped + failure details. Parsers for vitest, jest, pytest, phpunit, go test, cargo test, rspec, mocha + generic fallback. 200 lines of raw output → 10-15 lines.
|
|
37
|
+
- **38 new tests** — smart_log parser (5), categorizer (4), test_summary parsers (17), runner detection (8), validation (4). Total: 208 tests (was 170).
|
|
38
|
+
|
|
39
|
+
### Changed
|
|
40
|
+
- **18 tools** (was 16) — added `smart_log`, `test_summary`
|
|
41
|
+
- **MCP instructions** — added smart_log and test_summary to workflow guidance
|
|
42
|
+
|
|
8
43
|
## [0.10.0] - 2026-03-14
|
|
9
44
|
|
|
10
45
|
### Added
|
package/README.md
CHANGED
|
@@ -175,6 +175,8 @@ For more control, you can add rules to your project:
|
|
|
175
175
|
| `module_info` | manual analysis | Module dependency analysis: dependencies, dependents, public API, unused deps. Use for architecture understanding and dependency cleanup. |
|
|
176
176
|
| `smart_diff` | raw `git diff` | Structural diff with AST symbol mapping — shows which functions/classes changed instead of raw patch. Scopes: unstaged, staged, commit, branch. |
|
|
177
177
|
| `explore_area` | outline + related_files + git log | One-call directory exploration: structure, imports, tests, recent changes. Replaces 3-5 separate calls. |
|
|
178
|
+
| `smart_log` | raw `git log` | Structured commit history with category detection (feat/fix/refactor/docs), file stats, author breakdown. Filters by path and ref. |
|
|
179
|
+
| `test_summary` | raw test output | Run tests and get structured summary: total/passed/failed + failure details. Supports vitest, jest, pytest, phpunit, go, cargo, rspec, mocha. |
|
|
178
180
|
|
|
179
181
|
### Analytics
|
|
180
182
|
|
|
@@ -355,6 +357,8 @@ src/
|
|
|
355
357
|
module-info.ts — module_info handler (deps, dependents, API, unused)
|
|
356
358
|
smart-diff.ts — smart_diff handler (structural git diff + symbol mapping)
|
|
357
359
|
explore-area.ts — explore_area handler (outline + imports + tests + changes)
|
|
360
|
+
smart-log.ts — smart_log handler (structured git log + category detection)
|
|
361
|
+
test-summary.ts — test_summary handler (run tests + parse output)
|
|
358
362
|
non-code.ts — JSON/YAML/MD/TOML structural summaries
|
|
359
363
|
export-ast-index.ts — AST export for context-mode BM25
|
|
360
364
|
git/
|
|
@@ -12,4 +12,17 @@ export declare function findBinary(configPath?: string | null): Promise<BinarySt
|
|
|
12
12
|
* Download and install ast-index binary from GitHub releases.
|
|
13
13
|
*/
|
|
14
14
|
export declare function installBinary(onProgress?: (msg: string) => void): Promise<BinaryStatus>;
|
|
15
|
+
/**
|
|
16
|
+
* Check if a newer version of ast-index is available on GitHub.
|
|
17
|
+
* Non-blocking, returns null values on any error.
|
|
18
|
+
*/
|
|
19
|
+
export declare function checkBinaryUpdate(currentPath: string | null): Promise<{
|
|
20
|
+
current: string | null;
|
|
21
|
+
latest: string | null;
|
|
22
|
+
updateAvailable: boolean;
|
|
23
|
+
}>;
|
|
24
|
+
/**
|
|
25
|
+
* Compare two semver strings. Returns true if `latest` is newer than `current`.
|
|
26
|
+
*/
|
|
27
|
+
export declare function isNewerVersion(current: string, latest: string): boolean;
|
|
15
28
|
//# sourceMappingURL=binary-manager.d.ts.map
|
|
@@ -86,6 +86,49 @@ export async function installBinary(onProgress) {
|
|
|
86
86
|
throw err;
|
|
87
87
|
}
|
|
88
88
|
}
|
|
89
|
+
/**
|
|
90
|
+
* Check if a newer version of ast-index is available on GitHub.
|
|
91
|
+
* Non-blocking, returns null values on any error.
|
|
92
|
+
*/
|
|
93
|
+
export async function checkBinaryUpdate(currentPath) {
|
|
94
|
+
if (!currentPath) {
|
|
95
|
+
return { current: null, latest: null, updateAvailable: false };
|
|
96
|
+
}
|
|
97
|
+
try {
|
|
98
|
+
const [current, release] = await Promise.all([
|
|
99
|
+
getBinaryVersion(currentPath),
|
|
100
|
+
fetchLatestRelease(),
|
|
101
|
+
]);
|
|
102
|
+
const latest = release.tag.replace(/^v/, '');
|
|
103
|
+
if (!current) {
|
|
104
|
+
return { current: null, latest, updateAvailable: false };
|
|
105
|
+
}
|
|
106
|
+
return {
|
|
107
|
+
current,
|
|
108
|
+
latest,
|
|
109
|
+
updateAvailable: isNewerVersion(current, latest),
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
catch {
|
|
113
|
+
return { current: null, latest: null, updateAvailable: false };
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Compare two semver strings. Returns true if `latest` is newer than `current`.
|
|
118
|
+
*/
|
|
119
|
+
export function isNewerVersion(current, latest) {
|
|
120
|
+
const c = current.replace(/^v/, '').split('.').map(Number);
|
|
121
|
+
const l = latest.replace(/^v/, '').split('.').map(Number);
|
|
122
|
+
for (let i = 0; i < Math.max(c.length, l.length); i++) {
|
|
123
|
+
const cv = c[i] ?? 0;
|
|
124
|
+
const lv = l[i] ?? 0;
|
|
125
|
+
if (lv > cv)
|
|
126
|
+
return true;
|
|
127
|
+
if (lv < cv)
|
|
128
|
+
return false;
|
|
129
|
+
}
|
|
130
|
+
return false;
|
|
131
|
+
}
|
|
89
132
|
// --- Internal helpers ---
|
|
90
133
|
function getPlatform() {
|
|
91
134
|
switch (platform()) {
|
package/dist/config/defaults.js
CHANGED
|
@@ -127,6 +127,18 @@ export interface ExploreAreaArgs {
|
|
|
127
127
|
include?: Array<'outline' | 'imports' | 'tests' | 'changes'>;
|
|
128
128
|
}
|
|
129
129
|
export declare function validateExploreAreaArgs(args: unknown): ExploreAreaArgs;
|
|
130
|
+
export interface SmartLogArgs {
|
|
131
|
+
path?: string;
|
|
132
|
+
count?: number;
|
|
133
|
+
ref?: string;
|
|
134
|
+
}
|
|
135
|
+
export declare function validateSmartLogArgs(args: unknown): SmartLogArgs;
|
|
136
|
+
export interface TestSummaryArgs {
|
|
137
|
+
command: string;
|
|
138
|
+
runner?: string;
|
|
139
|
+
timeout?: number;
|
|
140
|
+
}
|
|
141
|
+
export declare function validateTestSummaryArgs(args: unknown): TestSummaryArgs;
|
|
130
142
|
/** Detect roots that would cause ast-index to scan the entire filesystem */
|
|
131
143
|
export declare function isDangerousRoot(root: string): boolean;
|
|
132
144
|
//# sourceMappingURL=validation.d.ts.map
|
package/dist/core/validation.js
CHANGED
|
@@ -337,6 +337,50 @@ export function validateExploreAreaArgs(args) {
|
|
|
337
337
|
}
|
|
338
338
|
return { path: a.path };
|
|
339
339
|
}
|
|
340
|
+
export function validateSmartLogArgs(args) {
|
|
341
|
+
if (!args || typeof args !== 'object')
|
|
342
|
+
return {};
|
|
343
|
+
const a = args;
|
|
344
|
+
const path = optionalString(a.path, 'path');
|
|
345
|
+
if (path !== undefined && path.length === 0) {
|
|
346
|
+
throw new Error('"path" must be a non-empty string.');
|
|
347
|
+
}
|
|
348
|
+
const count = optionalNumber(a.count, 'count');
|
|
349
|
+
if (count !== undefined) {
|
|
350
|
+
if (!Number.isInteger(count) || count < 1 || count > 50) {
|
|
351
|
+
throw new Error('"count" must be an integer between 1 and 50.');
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
const ref = optionalString(a.ref, 'ref');
|
|
355
|
+
if (ref !== undefined && ref.length === 0) {
|
|
356
|
+
throw new Error('"ref" must be a non-empty string.');
|
|
357
|
+
}
|
|
358
|
+
return { path, count, ref };
|
|
359
|
+
}
|
|
360
|
+
const VALID_RUNNERS = ['vitest', 'jest', 'pytest', 'phpunit', 'go', 'cargo', 'rspec', 'mocha'];
|
|
361
|
+
export function validateTestSummaryArgs(args) {
|
|
362
|
+
if (!args || typeof args !== 'object') {
|
|
363
|
+
throw new Error('Arguments must be an object with a "command" parameter.');
|
|
364
|
+
}
|
|
365
|
+
const a = args;
|
|
366
|
+
if (typeof a.command !== 'string' || a.command.length === 0) {
|
|
367
|
+
throw new Error('Required parameter "command" must be a non-empty string.');
|
|
368
|
+
}
|
|
369
|
+
let runner;
|
|
370
|
+
if (a.runner !== undefined && a.runner !== null) {
|
|
371
|
+
if (typeof a.runner !== 'string' || !VALID_RUNNERS.includes(a.runner)) {
|
|
372
|
+
throw new Error(`"runner" must be one of: ${VALID_RUNNERS.join(', ')}`);
|
|
373
|
+
}
|
|
374
|
+
runner = a.runner;
|
|
375
|
+
}
|
|
376
|
+
const timeout = optionalNumber(a.timeout, 'timeout');
|
|
377
|
+
if (timeout !== undefined) {
|
|
378
|
+
if (!Number.isInteger(timeout) || timeout < 1000 || timeout > 300000) {
|
|
379
|
+
throw new Error('"timeout" must be an integer between 1000 and 300000 (ms).');
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
return { command: a.command, runner, timeout };
|
|
383
|
+
}
|
|
340
384
|
/** Detect roots that would cause ast-index to scan the entire filesystem */
|
|
341
385
|
export function isDangerousRoot(root) {
|
|
342
386
|
const normalized = root.replace(/\/+$/, '') || '/';
|
|
@@ -102,7 +102,7 @@ async function buildImportsSection(codeFiles, absPath, projectRoot, astIndex) {
|
|
|
102
102
|
if (source.startsWith('.') || source.startsWith('/')) {
|
|
103
103
|
// Internal import — track if it's outside this area
|
|
104
104
|
const resolved = resolve(absPath, source);
|
|
105
|
-
if (!resolved.startsWith(absPath)) {
|
|
105
|
+
if (!resolved.startsWith(absPath + '/') && resolved !== absPath) {
|
|
106
106
|
const relImport = relative(projectRoot, resolved).replace(/\.[^.]+$/, '');
|
|
107
107
|
internalDeps.add(relImport);
|
|
108
108
|
}
|
|
@@ -129,7 +129,7 @@ async function buildImportsSection(codeFiles, absPath, projectRoot, astIndex) {
|
|
|
129
129
|
continue;
|
|
130
130
|
const relFile = relative(projectRoot, impFile);
|
|
131
131
|
// Only include files outside this area
|
|
132
|
-
if (!relFile.startsWith(relDir)) {
|
|
132
|
+
if (!relFile.startsWith(relDir + '/') && relFile !== relDir) {
|
|
133
133
|
importedBy.add(relFile.replace(/\.[^.]+$/, ''));
|
|
134
134
|
}
|
|
135
135
|
}
|
package/dist/handlers/outline.js
CHANGED
|
@@ -6,6 +6,7 @@ export const CODE_EXTENSIONS = new Set([
|
|
|
6
6
|
'swift', 'cs', 'cpp', 'cc', 'cxx', 'hpp', 'c', 'h', 'php', 'rb', 'scala',
|
|
7
7
|
'dart', 'lua', 'sh', 'bash', 'sql', 'r', 'vue', 'svelte', 'pl', 'pm',
|
|
8
8
|
'ex', 'exs', 'groovy', 'm', 'proto', 'bsl',
|
|
9
|
+
'lisp', 'lsp', 'cl', 'asd',
|
|
9
10
|
]);
|
|
10
11
|
// Standard HTTP verbs — protocol-level, not framework-specific
|
|
11
12
|
const HTTP_VERBS = ['Get', 'Post', 'Put', 'Delete', 'Patch', 'Head', 'Options', 'All'];
|
|
@@ -11,7 +11,7 @@ const MAX_FILES = 50;
|
|
|
11
11
|
const MAX_OUTPUT_LINES = 500;
|
|
12
12
|
export async function handleSmartDiff(args, projectRoot, astIndex) {
|
|
13
13
|
// 1. Build git command
|
|
14
|
-
const gitArgs = buildGitArgs(args
|
|
14
|
+
const gitArgs = buildGitArgs(args);
|
|
15
15
|
// 2. Execute git diff
|
|
16
16
|
let rawDiff;
|
|
17
17
|
try {
|
|
@@ -71,21 +71,21 @@ export async function handleSmartDiff(args, projectRoot, astIndex) {
|
|
|
71
71
|
// ──────────────────────────────────────────────
|
|
72
72
|
// Git command builder
|
|
73
73
|
// ──────────────────────────────────────────────
|
|
74
|
-
function buildGitArgs(args
|
|
74
|
+
function buildGitArgs(args) {
|
|
75
75
|
const base = [];
|
|
76
76
|
switch (args.scope) {
|
|
77
77
|
case 'staged':
|
|
78
|
-
base.push('diff', '--cached');
|
|
78
|
+
base.push('diff', '--cached', '--no-color');
|
|
79
79
|
break;
|
|
80
80
|
case 'commit':
|
|
81
|
-
base.push('show', '--format=', args.ref);
|
|
81
|
+
base.push('show', '--format=', '--no-color', args.ref);
|
|
82
82
|
break;
|
|
83
83
|
case 'branch':
|
|
84
|
-
base.push('diff', `${args.ref}...HEAD`);
|
|
84
|
+
base.push('diff', '--no-color', `${args.ref}...HEAD`);
|
|
85
85
|
break;
|
|
86
86
|
case 'unstaged':
|
|
87
87
|
default:
|
|
88
|
-
base.push('diff');
|
|
88
|
+
base.push('diff', '--no-color');
|
|
89
89
|
break;
|
|
90
90
|
}
|
|
91
91
|
if (args.path) {
|
|
@@ -182,16 +182,27 @@ function flattenSymbols(symbols, prefix = '') {
|
|
|
182
182
|
export function mapHunksToSymbols(hunks, structure) {
|
|
183
183
|
const allSymbols = flattenSymbols(structure.symbols);
|
|
184
184
|
const changedSymbols = new Map();
|
|
185
|
+
// Classify hunks: all-added, all-removed, or mixed
|
|
186
|
+
const hasAdded = hunks.some(h => h.lines.some(l => l.startsWith('+')));
|
|
187
|
+
const hasRemoved = hunks.some(h => h.lines.some(l => l.startsWith('-')));
|
|
185
188
|
for (const hunk of hunks) {
|
|
186
189
|
const hunkStart = hunk.newStart;
|
|
187
|
-
const hunkEnd = hunk.
|
|
190
|
+
const hunkEnd = hunk.newCount > 0
|
|
191
|
+
? hunk.newStart + hunk.newCount - 1
|
|
192
|
+
: hunk.newStart; // pure deletion: use newStart as point
|
|
188
193
|
for (const sym of allSymbols) {
|
|
189
194
|
if (hunkStart <= sym.end && hunkEnd >= sym.start) {
|
|
190
195
|
if (!changedSymbols.has(sym.name)) {
|
|
196
|
+
// Determine changeType from hunk content
|
|
197
|
+
let changeType = 'MODIFIED';
|
|
198
|
+
if (hasAdded && !hasRemoved)
|
|
199
|
+
changeType = 'ADDED';
|
|
200
|
+
else if (hasRemoved && !hasAdded)
|
|
201
|
+
changeType = 'REMOVED';
|
|
191
202
|
changedSymbols.set(sym.name, {
|
|
192
203
|
name: sym.name,
|
|
193
204
|
kind: sym.kind,
|
|
194
|
-
changeType
|
|
205
|
+
changeType,
|
|
195
206
|
lineRange: `[L${sym.start}-${sym.end}]`,
|
|
196
207
|
});
|
|
197
208
|
}
|
|
@@ -228,7 +239,8 @@ function formatSmartDiff(allFiles, processedFiles, symbolChanges, args, rawToken
|
|
|
228
239
|
const symbols = symbolChanges.get(fd.path);
|
|
229
240
|
if (symbols && symbols.length > 0) {
|
|
230
241
|
for (const sc of symbols) {
|
|
231
|
-
|
|
242
|
+
const parens = ['function', 'method'].includes(sc.kind) ? '()' : '';
|
|
243
|
+
lines.push(` ${sc.changeType}: ${sc.name}${parens} ${sc.lineRange}`);
|
|
232
244
|
}
|
|
233
245
|
}
|
|
234
246
|
// Small diff: include actual hunks
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { SmartLogArgs } from '../core/validation.js';
|
|
2
|
+
export interface LogEntry {
|
|
3
|
+
hash: string;
|
|
4
|
+
date: string;
|
|
5
|
+
author: string;
|
|
6
|
+
message: string;
|
|
7
|
+
category: 'feat' | 'fix' | 'refactor' | 'docs' | 'test' | 'chore' | 'style' | 'perf' | 'other';
|
|
8
|
+
files: string[];
|
|
9
|
+
insertions: number;
|
|
10
|
+
deletions: number;
|
|
11
|
+
}
|
|
12
|
+
export declare function handleSmartLog(args: SmartLogArgs, projectRoot: string): Promise<{
|
|
13
|
+
content: Array<{
|
|
14
|
+
type: 'text';
|
|
15
|
+
text: string;
|
|
16
|
+
}>;
|
|
17
|
+
rawTokens: number;
|
|
18
|
+
}>;
|
|
19
|
+
export declare function parseGitLog(raw: string): LogEntry[];
|
|
20
|
+
export declare function categorizeCommit(message: string): LogEntry['category'];
|
|
21
|
+
//# sourceMappingURL=smart-log.d.ts.map
|
|
@@ -0,0 +1,200 @@
|
|
|
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
|
+
// Constants
|
|
7
|
+
// ──────────────────────────────────────────────
|
|
8
|
+
const RECORD_SEPARATOR = '<<<SEP>>>';
|
|
9
|
+
const FIELD_SEPARATOR = '<<<F>>>';
|
|
10
|
+
const MAX_COUNT = 50;
|
|
11
|
+
// ──────────────────────────────────────────────
|
|
12
|
+
// Handler
|
|
13
|
+
// ──────────────────────────────────────────────
|
|
14
|
+
export async function handleSmartLog(args, projectRoot) {
|
|
15
|
+
const count = Math.min(args.count ?? 10, MAX_COUNT);
|
|
16
|
+
const ref = args.ref ?? 'HEAD';
|
|
17
|
+
// Build git log command with --numstat for file stats
|
|
18
|
+
const gitArgs = [
|
|
19
|
+
'log',
|
|
20
|
+
`--format=${RECORD_SEPARATOR}%h${FIELD_SEPARATOR}%ad${FIELD_SEPARATOR}%an${FIELD_SEPARATOR}%s`,
|
|
21
|
+
'--date=short',
|
|
22
|
+
'--numstat',
|
|
23
|
+
`-n`, `${count}`,
|
|
24
|
+
'--', ref,
|
|
25
|
+
];
|
|
26
|
+
if (args.path) {
|
|
27
|
+
gitArgs.push('--', args.path);
|
|
28
|
+
}
|
|
29
|
+
let rawOutput;
|
|
30
|
+
try {
|
|
31
|
+
const { stdout } = await execFileAsync('git', gitArgs, {
|
|
32
|
+
cwd: projectRoot,
|
|
33
|
+
timeout: 10000,
|
|
34
|
+
maxBuffer: 5 * 1024 * 1024,
|
|
35
|
+
});
|
|
36
|
+
rawOutput = stdout;
|
|
37
|
+
}
|
|
38
|
+
catch (err) {
|
|
39
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
40
|
+
return {
|
|
41
|
+
content: [{ type: 'text', text: `git log failed: ${msg}` }],
|
|
42
|
+
rawTokens: 0,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
if (!rawOutput.trim()) {
|
|
46
|
+
return {
|
|
47
|
+
content: [{ type: 'text', text: 'No commits found.' }],
|
|
48
|
+
rawTokens: 0,
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
const rawTokens = estimateTokens(rawOutput);
|
|
52
|
+
const entries = parseGitLog(rawOutput);
|
|
53
|
+
const formatted = formatSmartLog(entries, args.path);
|
|
54
|
+
return {
|
|
55
|
+
content: [{ type: 'text', text: formatted }],
|
|
56
|
+
rawTokens,
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
// ──────────────────────────────────────────────
|
|
60
|
+
// Parser
|
|
61
|
+
// ──────────────────────────────────────────────
|
|
62
|
+
export function parseGitLog(raw) {
|
|
63
|
+
const records = raw.split(RECORD_SEPARATOR).filter(r => r.trim());
|
|
64
|
+
const entries = [];
|
|
65
|
+
for (const record of records) {
|
|
66
|
+
const lines = record.trim().split('\n');
|
|
67
|
+
if (lines.length === 0)
|
|
68
|
+
continue;
|
|
69
|
+
const headerLine = lines[0];
|
|
70
|
+
const fields = headerLine.split(FIELD_SEPARATOR);
|
|
71
|
+
if (fields.length < 4)
|
|
72
|
+
continue;
|
|
73
|
+
const [hash, date, author, message] = fields;
|
|
74
|
+
// Parse numstat lines (insertions\tdeletions\tfile)
|
|
75
|
+
const files = [];
|
|
76
|
+
let insertions = 0;
|
|
77
|
+
let deletions = 0;
|
|
78
|
+
for (let i = 1; i < lines.length; i++) {
|
|
79
|
+
const line = lines[i].trim();
|
|
80
|
+
if (!line)
|
|
81
|
+
continue;
|
|
82
|
+
const parts = line.split('\t');
|
|
83
|
+
if (parts.length >= 3) {
|
|
84
|
+
const ins = parts[0] === '-' ? 0 : parseInt(parts[0], 10) || 0;
|
|
85
|
+
const del = parts[1] === '-' ? 0 : parseInt(parts[1], 10) || 0;
|
|
86
|
+
insertions += ins;
|
|
87
|
+
deletions += del;
|
|
88
|
+
files.push(parts.slice(2).join('\t'));
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
entries.push({
|
|
92
|
+
hash,
|
|
93
|
+
date,
|
|
94
|
+
author,
|
|
95
|
+
message,
|
|
96
|
+
category: categorizeCommit(message),
|
|
97
|
+
files,
|
|
98
|
+
insertions,
|
|
99
|
+
deletions,
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
return entries;
|
|
103
|
+
}
|
|
104
|
+
// ──────────────────────────────────────────────
|
|
105
|
+
// Categorizer
|
|
106
|
+
// ──────────────────────────────────────────────
|
|
107
|
+
export function categorizeCommit(message) {
|
|
108
|
+
const lower = message.toLowerCase();
|
|
109
|
+
// Conventional commits prefix
|
|
110
|
+
if (/^feat[:(!\s]/.test(lower))
|
|
111
|
+
return 'feat';
|
|
112
|
+
if (/^fix[:(!\s]/.test(lower))
|
|
113
|
+
return 'fix';
|
|
114
|
+
if (/^refactor[:(!\s]/.test(lower))
|
|
115
|
+
return 'refactor';
|
|
116
|
+
if (/^docs?[:(!\s]/.test(lower))
|
|
117
|
+
return 'docs';
|
|
118
|
+
if (/^tests?[:(!\s]/.test(lower))
|
|
119
|
+
return 'test';
|
|
120
|
+
if (/^chore[:(!\s]/.test(lower))
|
|
121
|
+
return 'chore';
|
|
122
|
+
if (/^style[:(!\s]/.test(lower))
|
|
123
|
+
return 'style';
|
|
124
|
+
if (/^perf[:(!\s]/.test(lower))
|
|
125
|
+
return 'perf';
|
|
126
|
+
// Version bumps
|
|
127
|
+
if (/^v?\d+\.\d+/.test(lower))
|
|
128
|
+
return 'feat';
|
|
129
|
+
// Keyword heuristics with word boundaries (order matters — more specific first)
|
|
130
|
+
if (/\b(fix|bug|hotfix)\b/.test(lower) || /\bpatch\b/.test(lower))
|
|
131
|
+
return 'fix';
|
|
132
|
+
if (/\b(tests?|specs?|coverage)\b/.test(lower))
|
|
133
|
+
return 'test';
|
|
134
|
+
if (/\b(refactor|restructure|rename|extract)\b/.test(lower) || /\bmove\b/.test(lower))
|
|
135
|
+
return 'refactor';
|
|
136
|
+
if (/\b(docs?|documentation|readme|changelog)\b/.test(lower))
|
|
137
|
+
return 'docs';
|
|
138
|
+
if (/\b(add|new|implement|feature)\b/.test(lower))
|
|
139
|
+
return 'feat';
|
|
140
|
+
if (/\b(style|format|lint)\b/.test(lower))
|
|
141
|
+
return 'style';
|
|
142
|
+
if (/\b(perf|optimiz\w*|speed|faster?)\b/.test(lower))
|
|
143
|
+
return 'perf';
|
|
144
|
+
if (/\b(chore|bump|deps)\b/.test(lower) || /\bci\b/.test(lower) || /\bbuild\b/.test(lower))
|
|
145
|
+
return 'chore';
|
|
146
|
+
return 'other';
|
|
147
|
+
}
|
|
148
|
+
// ──────────────────────────────────────────────
|
|
149
|
+
// Formatter
|
|
150
|
+
// ──────────────────────────────────────────────
|
|
151
|
+
function formatSmartLog(entries, pathFilter) {
|
|
152
|
+
if (entries.length === 0)
|
|
153
|
+
return 'No commits found.';
|
|
154
|
+
const lines = [];
|
|
155
|
+
// Header
|
|
156
|
+
const filterInfo = pathFilter ? ` (filtered: ${pathFilter})` : '';
|
|
157
|
+
lines.push(`GIT LOG: ${entries.length} commits${filterInfo}`);
|
|
158
|
+
lines.push('');
|
|
159
|
+
// Summary stats
|
|
160
|
+
const authors = new Map();
|
|
161
|
+
const categories = new Map();
|
|
162
|
+
let totalIns = 0;
|
|
163
|
+
let totalDel = 0;
|
|
164
|
+
for (const e of entries) {
|
|
165
|
+
authors.set(e.author, (authors.get(e.author) ?? 0) + 1);
|
|
166
|
+
categories.set(e.category, (categories.get(e.category) ?? 0) + 1);
|
|
167
|
+
totalIns += e.insertions;
|
|
168
|
+
totalDel += e.deletions;
|
|
169
|
+
}
|
|
170
|
+
// Category summary
|
|
171
|
+
const catParts = [];
|
|
172
|
+
for (const [cat, count] of Array.from(categories.entries()).sort((a, b) => b[1] - a[1])) {
|
|
173
|
+
catParts.push(`${cat}:${count}`);
|
|
174
|
+
}
|
|
175
|
+
lines.push(`BREAKDOWN: ${catParts.join(', ')} | +${totalIns}/-${totalDel} lines`);
|
|
176
|
+
// Authors
|
|
177
|
+
const authorParts = Array.from(authors.entries())
|
|
178
|
+
.sort((a, b) => b[1] - a[1])
|
|
179
|
+
.slice(0, 5)
|
|
180
|
+
.map(([name, cnt]) => authors.size > 1 ? `${name} (${cnt})` : name);
|
|
181
|
+
lines.push(`AUTHORS: ${authorParts.join(', ')}`);
|
|
182
|
+
lines.push('');
|
|
183
|
+
// Entries
|
|
184
|
+
for (const e of entries) {
|
|
185
|
+
const filesSummary = e.files.length <= 3
|
|
186
|
+
? e.files.join(', ')
|
|
187
|
+
: `${e.files.slice(0, 3).join(', ')} +${e.files.length - 3} more`;
|
|
188
|
+
const stats = e.insertions + e.deletions > 0
|
|
189
|
+
? ` (+${e.insertions}/-${e.deletions})`
|
|
190
|
+
: '';
|
|
191
|
+
lines.push(`${e.hash} ${e.date} [${e.category}] ${e.message}${stats}`);
|
|
192
|
+
if (e.files.length > 0) {
|
|
193
|
+
lines.push(` → ${filesSummary}`);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
lines.push('');
|
|
197
|
+
lines.push('HINT: Use smart_diff(scope="commit", ref="<hash>") to see structural changes for a specific commit.');
|
|
198
|
+
return lines.join('\n');
|
|
199
|
+
}
|
|
200
|
+
//# sourceMappingURL=smart-log.js.map
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { TestSummaryArgs } from '../core/validation.js';
|
|
2
|
+
export interface TestResult {
|
|
3
|
+
total: number;
|
|
4
|
+
passed: number;
|
|
5
|
+
failed: number;
|
|
6
|
+
skipped: number;
|
|
7
|
+
duration?: string;
|
|
8
|
+
failures: FailedTest[];
|
|
9
|
+
suites?: number;
|
|
10
|
+
}
|
|
11
|
+
export interface FailedTest {
|
|
12
|
+
name: string;
|
|
13
|
+
file?: string;
|
|
14
|
+
error: string;
|
|
15
|
+
}
|
|
16
|
+
export declare function handleTestSummary(args: TestSummaryArgs, projectRoot: string): Promise<{
|
|
17
|
+
content: Array<{
|
|
18
|
+
type: 'text';
|
|
19
|
+
text: string;
|
|
20
|
+
}>;
|
|
21
|
+
rawTokens: number;
|
|
22
|
+
}>;
|
|
23
|
+
export declare function detectRunner(command: string, output: string): string;
|
|
24
|
+
export declare function parseTestOutput(output: string, runner: string): TestResult;
|
|
25
|
+
//# sourceMappingURL=test-summary.d.ts.map
|
|
@@ -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
|
}
|
package/dist/server.js
CHANGED
|
@@ -30,9 +30,11 @@ import { handleCodeAudit } from './handlers/code-audit.js';
|
|
|
30
30
|
import { handleModuleInfo } from './handlers/module-info.js';
|
|
31
31
|
import { handleSmartDiff } from './handlers/smart-diff.js';
|
|
32
32
|
import { handleExploreArea } from './handlers/explore-area.js';
|
|
33
|
+
import { handleSmartLog } from './handlers/smart-log.js';
|
|
34
|
+
import { handleTestSummary } from './handlers/test-summary.js';
|
|
33
35
|
import { detectContextMode } from './integration/context-mode-detector.js';
|
|
34
36
|
import { estimateTokens } from './core/token-estimator.js';
|
|
35
|
-
import { resolveSafePath, validateSmartReadArgs, validateReadSymbolArgs, validateReadRangeArgs, validateReadDiffArgs, validateFindUsagesArgs, validateSmartReadManyArgs, validateReadForEditArgs, validateRelatedFilesArgs, validateOutlineArgs, validateFindUnusedArgs, validateCodeAuditArgs, validateProjectOverviewArgs, validateModuleInfoArgs, validateSmartDiffArgs, validateExploreAreaArgs, } from './core/validation.js';
|
|
37
|
+
import { resolveSafePath, validateSmartReadArgs, validateReadSymbolArgs, validateReadRangeArgs, validateReadDiffArgs, validateFindUsagesArgs, validateSmartReadManyArgs, validateReadForEditArgs, validateRelatedFilesArgs, validateOutlineArgs, validateFindUnusedArgs, validateCodeAuditArgs, validateProjectOverviewArgs, validateModuleInfoArgs, validateSmartDiffArgs, validateExploreAreaArgs, validateSmartLogArgs, validateTestSummaryArgs, } from './core/validation.js';
|
|
36
38
|
export async function createServer(projectRoot, options) {
|
|
37
39
|
const config = await loadConfig(projectRoot);
|
|
38
40
|
const astIndex = new AstIndexClient(projectRoot, config.astIndex.timeout, {
|
|
@@ -187,6 +189,8 @@ export async function createServer(projectRoot, options) {
|
|
|
187
189
|
'• Code quality audit → code_audit (TODOs, deprecated, structural code patterns)',
|
|
188
190
|
'• Reviewing git changes → smart_diff (structural diff with symbol mapping, not raw patch)',
|
|
189
191
|
'• Starting work on an area → explore_area (outline + imports + tests + git log in one call)',
|
|
192
|
+
'• Understanding commit history → smart_log (structured git log with categories, not raw output)',
|
|
193
|
+
'• Running tests → test_summary (structured pass/fail summary, not 200 lines of raw output)',
|
|
190
194
|
'',
|
|
191
195
|
'WHEN TO USE DEFAULT TOOLS (Token Pilot adds no value):',
|
|
192
196
|
'• Small files (≤200 lines) → smart_read returns full content anyway, same as Read',
|
|
@@ -434,6 +438,31 @@ export async function createServer(projectRoot, options) {
|
|
|
434
438
|
required: ['path'],
|
|
435
439
|
},
|
|
436
440
|
},
|
|
441
|
+
{
|
|
442
|
+
name: 'smart_log',
|
|
443
|
+
description: 'Use INSTEAD OF raw git log. Structured commit history with category detection (feat/fix/refactor/docs), file stats, author breakdown. Filters by path and ref.',
|
|
444
|
+
inputSchema: {
|
|
445
|
+
type: 'object',
|
|
446
|
+
properties: {
|
|
447
|
+
path: { type: 'string', description: 'Filter to specific file or directory' },
|
|
448
|
+
count: { type: 'number', description: 'Number of commits (default: 10, max: 50)' },
|
|
449
|
+
ref: { type: 'string', description: 'Git ref — branch, tag, or commit (default: HEAD)' },
|
|
450
|
+
},
|
|
451
|
+
},
|
|
452
|
+
},
|
|
453
|
+
{
|
|
454
|
+
name: 'test_summary',
|
|
455
|
+
description: 'Run tests and return structured summary: total/passed/failed/skipped + failure details. 200 lines of raw output → 10-15 lines. Supports vitest, jest, pytest, phpunit, go test, cargo test.',
|
|
456
|
+
inputSchema: {
|
|
457
|
+
type: 'object',
|
|
458
|
+
properties: {
|
|
459
|
+
command: { type: 'string', description: 'Test command to run (e.g., "npm test", "pytest", "go test ./...")' },
|
|
460
|
+
runner: { type: 'string', enum: ['vitest', 'jest', 'pytest', 'phpunit', 'go', 'cargo', 'rspec', 'mocha'], description: 'Force specific parser (auto-detected if omitted)' },
|
|
461
|
+
timeout: { type: 'number', description: 'Timeout in ms (default: 60000, max: 300000)' },
|
|
462
|
+
},
|
|
463
|
+
required: ['command'],
|
|
464
|
+
},
|
|
465
|
+
},
|
|
437
466
|
],
|
|
438
467
|
}));
|
|
439
468
|
// Helper: get real full-file token count for honest analytics
|
|
@@ -604,6 +633,22 @@ export async function createServer(projectRoot, options) {
|
|
|
604
633
|
analytics.record({ tool: 'explore_area', path: eaArgs.path, tokensReturned: eaTokens, tokensWouldBe: eaWouldBe, timestamp: Date.now() });
|
|
605
634
|
return eaResult;
|
|
606
635
|
}
|
|
636
|
+
case 'smart_log': {
|
|
637
|
+
const slArgs = validateSmartLogArgs(args);
|
|
638
|
+
const slResult = await handleSmartLog(slArgs, projectRoot);
|
|
639
|
+
const slText = slResult.content[0]?.text ?? '';
|
|
640
|
+
const slTokens = estimateTokens(slText);
|
|
641
|
+
analytics.record({ tool: 'smart_log', path: slArgs.path ?? 'all', tokensReturned: slTokens, tokensWouldBe: slResult.rawTokens || slTokens, timestamp: Date.now() });
|
|
642
|
+
return { content: slResult.content };
|
|
643
|
+
}
|
|
644
|
+
case 'test_summary': {
|
|
645
|
+
const tsArgs = validateTestSummaryArgs(args);
|
|
646
|
+
const tsResult = await handleTestSummary(tsArgs, projectRoot);
|
|
647
|
+
const tsText = tsResult.content[0]?.text ?? '';
|
|
648
|
+
const tsTokens = estimateTokens(tsText);
|
|
649
|
+
analytics.record({ tool: 'test_summary', path: tsArgs.command, tokensReturned: tsTokens, tokensWouldBe: tsResult.rawTokens || tsTokens, timestamp: Date.now() });
|
|
650
|
+
return { content: tsResult.content };
|
|
651
|
+
}
|
|
607
652
|
default:
|
|
608
653
|
return {
|
|
609
654
|
content: [{ type: 'text', text: `Unknown tool: ${name}` }],
|
package/dist/types.d.ts
CHANGED
|
@@ -115,6 +115,10 @@ export interface TokenPilotConfig {
|
|
|
115
115
|
adviseDelegation: boolean;
|
|
116
116
|
largeNonCodeThreshold: number;
|
|
117
117
|
};
|
|
118
|
+
updates: {
|
|
119
|
+
checkOnStartup: boolean;
|
|
120
|
+
autoUpdate: boolean;
|
|
121
|
+
};
|
|
118
122
|
ignore: string[];
|
|
119
123
|
}
|
|
120
124
|
//# sourceMappingURL=types.d.ts.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "token-pilot",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.13.0",
|
|
4
4
|
"description": "Save 60-80% tokens when AI reads code — MCP server for token-efficient code navigation, AST-aware structural reading instead of dumping full files into context window",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|