token-pilot 0.8.3 → 0.10.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 +37 -0
- package/README.md +17 -10
- package/dist/ast-index/client.d.ts +15 -1
- package/dist/ast-index/client.js +179 -0
- package/dist/ast-index/types.d.ts +27 -1
- package/dist/ast-index/types.js +1 -1
- package/dist/core/project-detector.d.ts +42 -0
- package/dist/core/project-detector.js +362 -0
- package/dist/core/validation.d.ts +47 -4
- package/dist/core/validation.js +111 -8
- package/dist/handlers/explore-area.d.ts +9 -0
- package/dist/handlers/explore-area.js +280 -0
- package/dist/handlers/find-usages.d.ts +3 -3
- package/dist/handlers/find-usages.js +88 -13
- package/dist/handlers/module-info.d.ts +9 -0
- package/dist/handlers/module-info.js +123 -0
- package/dist/handlers/outline.d.ts +7 -3
- package/dist/handlers/outline.js +52 -21
- package/dist/handlers/project-overview.d.ts +2 -1
- package/dist/handlers/project-overview.js +146 -107
- package/dist/handlers/smart-diff.d.ts +35 -0
- package/dist/handlers/smart-diff.js +257 -0
- package/dist/server.js +98 -7
- package/package.json +1 -1
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
import { execFile } from 'node:child_process';
|
|
2
|
+
import { promisify } from 'node:util';
|
|
3
|
+
import { resolve } from 'node:path';
|
|
4
|
+
import { estimateTokens } from '../core/token-estimator.js';
|
|
5
|
+
const execFileAsync = promisify(execFile);
|
|
6
|
+
// ──────────────────────────────────────────────
|
|
7
|
+
// Handler
|
|
8
|
+
// ──────────────────────────────────────────────
|
|
9
|
+
const SMALL_DIFF_THRESHOLD = 30;
|
|
10
|
+
const MAX_FILES = 50;
|
|
11
|
+
const MAX_OUTPUT_LINES = 500;
|
|
12
|
+
export async function handleSmartDiff(args, projectRoot, astIndex) {
|
|
13
|
+
// 1. Build git command
|
|
14
|
+
const gitArgs = buildGitArgs(args, projectRoot);
|
|
15
|
+
// 2. Execute git diff
|
|
16
|
+
let rawDiff;
|
|
17
|
+
try {
|
|
18
|
+
const { stdout } = await execFileAsync('git', gitArgs, {
|
|
19
|
+
cwd: projectRoot,
|
|
20
|
+
timeout: 10000,
|
|
21
|
+
maxBuffer: 5 * 1024 * 1024,
|
|
22
|
+
});
|
|
23
|
+
rawDiff = stdout;
|
|
24
|
+
}
|
|
25
|
+
catch (err) {
|
|
26
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
27
|
+
if (msg.includes('not a git repository') || msg.includes('fatal:')) {
|
|
28
|
+
return { content: [{ type: 'text', text: 'Not a git repository. smart_diff requires git.' }], rawTokens: 0 };
|
|
29
|
+
}
|
|
30
|
+
return { content: [{ type: 'text', text: `git diff failed: ${msg}` }], rawTokens: 0 };
|
|
31
|
+
}
|
|
32
|
+
const rawTokens = estimateTokens(rawDiff);
|
|
33
|
+
if (!rawDiff.trim()) {
|
|
34
|
+
const scopeLabel = args.scope ?? 'unstaged';
|
|
35
|
+
return {
|
|
36
|
+
content: [{ type: 'text', text: `NO CHANGES (${scopeLabel}): working tree is clean.` }],
|
|
37
|
+
rawTokens: 0,
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
// 3. Parse unified diff
|
|
41
|
+
const fileDiffs = parseUnifiedDiff(rawDiff);
|
|
42
|
+
if (fileDiffs.length === 0) {
|
|
43
|
+
return {
|
|
44
|
+
content: [{ type: 'text', text: 'NO CHANGES: diff parsed but no file changes found.' }],
|
|
45
|
+
rawTokens,
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
// 4. Map hunks to symbols (parallel, capped)
|
|
49
|
+
const filesToProcess = fileDiffs.slice(0, MAX_FILES);
|
|
50
|
+
const symbolChanges = new Map();
|
|
51
|
+
const outlineResults = await Promise.allSettled(filesToProcess
|
|
52
|
+
.filter(f => !f.isBinary && !f.isDeleted)
|
|
53
|
+
.map(async (f) => {
|
|
54
|
+
const absPath = resolve(projectRoot, f.path);
|
|
55
|
+
const structure = await astIndex.outline(absPath);
|
|
56
|
+
return { path: f.path, structure };
|
|
57
|
+
}));
|
|
58
|
+
for (const result of outlineResults) {
|
|
59
|
+
if (result.status === 'fulfilled' && result.value.structure) {
|
|
60
|
+
const { path, structure } = result.value;
|
|
61
|
+
const fd = filesToProcess.find(f => f.path === path);
|
|
62
|
+
if (fd) {
|
|
63
|
+
symbolChanges.set(path, mapHunksToSymbols(fd.hunks, structure));
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
// 5. Format output
|
|
68
|
+
const output = formatSmartDiff(fileDiffs, filesToProcess, symbolChanges, args, rawTokens);
|
|
69
|
+
return { content: [{ type: 'text', text: output }], rawTokens };
|
|
70
|
+
}
|
|
71
|
+
// ──────────────────────────────────────────────
|
|
72
|
+
// Git command builder
|
|
73
|
+
// ──────────────────────────────────────────────
|
|
74
|
+
function buildGitArgs(args, projectRoot) {
|
|
75
|
+
const base = [];
|
|
76
|
+
switch (args.scope) {
|
|
77
|
+
case 'staged':
|
|
78
|
+
base.push('diff', '--cached');
|
|
79
|
+
break;
|
|
80
|
+
case 'commit':
|
|
81
|
+
base.push('show', '--format=', args.ref);
|
|
82
|
+
break;
|
|
83
|
+
case 'branch':
|
|
84
|
+
base.push('diff', `${args.ref}...HEAD`);
|
|
85
|
+
break;
|
|
86
|
+
case 'unstaged':
|
|
87
|
+
default:
|
|
88
|
+
base.push('diff');
|
|
89
|
+
break;
|
|
90
|
+
}
|
|
91
|
+
if (args.path) {
|
|
92
|
+
base.push('--', args.path);
|
|
93
|
+
}
|
|
94
|
+
return base;
|
|
95
|
+
}
|
|
96
|
+
// ──────────────────────────────────────────────
|
|
97
|
+
// Unified diff parser
|
|
98
|
+
// ──────────────────────────────────────────────
|
|
99
|
+
export function parseUnifiedDiff(raw) {
|
|
100
|
+
const files = [];
|
|
101
|
+
let current = null;
|
|
102
|
+
let currentHunk = null;
|
|
103
|
+
for (const line of raw.split('\n')) {
|
|
104
|
+
// New file
|
|
105
|
+
if (line.startsWith('diff --git ')) {
|
|
106
|
+
if (current)
|
|
107
|
+
files.push(current);
|
|
108
|
+
const match = line.match(/diff --git a\/(.+?) b\/(.+)/);
|
|
109
|
+
current = {
|
|
110
|
+
path: match?.[2] ?? '',
|
|
111
|
+
oldPath: match?.[1] !== match?.[2] ? match?.[1] : undefined,
|
|
112
|
+
addedLines: 0,
|
|
113
|
+
removedLines: 0,
|
|
114
|
+
hunks: [],
|
|
115
|
+
isBinary: false,
|
|
116
|
+
isNew: false,
|
|
117
|
+
isDeleted: false,
|
|
118
|
+
};
|
|
119
|
+
currentHunk = null;
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
if (!current)
|
|
123
|
+
continue;
|
|
124
|
+
if (line.startsWith('new file mode')) {
|
|
125
|
+
current.isNew = true;
|
|
126
|
+
}
|
|
127
|
+
else if (line.startsWith('deleted file mode')) {
|
|
128
|
+
current.isDeleted = true;
|
|
129
|
+
}
|
|
130
|
+
else if (line.startsWith('Binary files')) {
|
|
131
|
+
current.isBinary = true;
|
|
132
|
+
}
|
|
133
|
+
else if (line.startsWith('rename from ')) {
|
|
134
|
+
current.oldPath = line.slice(12);
|
|
135
|
+
}
|
|
136
|
+
else if (line.startsWith('@@ ')) {
|
|
137
|
+
const match = line.match(/@@ -\d+(?:,\d+)? \+(\d+)(?:,(\d+))? @@/);
|
|
138
|
+
currentHunk = {
|
|
139
|
+
newStart: match ? parseInt(match[1], 10) : 0,
|
|
140
|
+
newCount: match?.[2] ? parseInt(match[2], 10) : 1,
|
|
141
|
+
lines: [],
|
|
142
|
+
};
|
|
143
|
+
current.hunks.push(currentHunk);
|
|
144
|
+
}
|
|
145
|
+
else if (currentHunk) {
|
|
146
|
+
if (line.startsWith('+') && !line.startsWith('+++')) {
|
|
147
|
+
current.addedLines++;
|
|
148
|
+
currentHunk.lines.push(line);
|
|
149
|
+
}
|
|
150
|
+
else if (line.startsWith('-') && !line.startsWith('---')) {
|
|
151
|
+
current.removedLines++;
|
|
152
|
+
currentHunk.lines.push(line);
|
|
153
|
+
}
|
|
154
|
+
else if (line.startsWith(' ')) {
|
|
155
|
+
currentHunk.lines.push(line);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
if (current)
|
|
160
|
+
files.push(current);
|
|
161
|
+
return files;
|
|
162
|
+
}
|
|
163
|
+
// ──────────────────────────────────────────────
|
|
164
|
+
// Symbol mapping
|
|
165
|
+
// ──────────────────────────────────────────────
|
|
166
|
+
function flattenSymbols(symbols, prefix = '') {
|
|
167
|
+
const result = [];
|
|
168
|
+
for (const sym of symbols) {
|
|
169
|
+
const name = prefix ? `${prefix}.${sym.name}` : sym.name;
|
|
170
|
+
result.push({
|
|
171
|
+
name,
|
|
172
|
+
kind: sym.kind,
|
|
173
|
+
start: sym.location.startLine,
|
|
174
|
+
end: sym.location.endLine,
|
|
175
|
+
});
|
|
176
|
+
if (sym.children.length > 0) {
|
|
177
|
+
result.push(...flattenSymbols(sym.children, sym.kind === 'class' || sym.kind === 'interface' ? sym.name : ''));
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
return result;
|
|
181
|
+
}
|
|
182
|
+
export function mapHunksToSymbols(hunks, structure) {
|
|
183
|
+
const allSymbols = flattenSymbols(structure.symbols);
|
|
184
|
+
const changedSymbols = new Map();
|
|
185
|
+
for (const hunk of hunks) {
|
|
186
|
+
const hunkStart = hunk.newStart;
|
|
187
|
+
const hunkEnd = hunk.newStart + hunk.newCount - 1;
|
|
188
|
+
for (const sym of allSymbols) {
|
|
189
|
+
if (hunkStart <= sym.end && hunkEnd >= sym.start) {
|
|
190
|
+
if (!changedSymbols.has(sym.name)) {
|
|
191
|
+
changedSymbols.set(sym.name, {
|
|
192
|
+
name: sym.name,
|
|
193
|
+
kind: sym.kind,
|
|
194
|
+
changeType: 'MODIFIED',
|
|
195
|
+
lineRange: `[L${sym.start}-${sym.end}]`,
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
return Array.from(changedSymbols.values());
|
|
202
|
+
}
|
|
203
|
+
// ──────────────────────────────────────────────
|
|
204
|
+
// Output formatter
|
|
205
|
+
// ──────────────────────────────────────────────
|
|
206
|
+
function formatSmartDiff(allFiles, processedFiles, symbolChanges, args, rawTokens) {
|
|
207
|
+
const totalAdded = allFiles.reduce((s, f) => s + f.addedLines, 0);
|
|
208
|
+
const totalRemoved = allFiles.reduce((s, f) => s + f.removedLines, 0);
|
|
209
|
+
const scopeLabel = args.scope ?? 'unstaged';
|
|
210
|
+
const lines = [];
|
|
211
|
+
lines.push(`CHANGES: ${allFiles.length} file${allFiles.length !== 1 ? 's' : ''}, +${totalAdded} -${totalRemoved} (${scopeLabel})`);
|
|
212
|
+
lines.push('');
|
|
213
|
+
for (const fd of processedFiles) {
|
|
214
|
+
if (lines.length >= MAX_OUTPUT_LINES) {
|
|
215
|
+
lines.push(`... truncated (${allFiles.length - processedFiles.indexOf(fd)} more files)`);
|
|
216
|
+
break;
|
|
217
|
+
}
|
|
218
|
+
// File header
|
|
219
|
+
const changeLabel = fd.isNew ? ' [NEW]' : fd.isDeleted ? ' [DELETED]' : '';
|
|
220
|
+
const renameLabel = fd.oldPath ? ` (renamed from ${fd.oldPath})` : '';
|
|
221
|
+
const binaryLabel = fd.isBinary ? ' [BINARY]' : '';
|
|
222
|
+
lines.push(`${fd.path} (+${fd.addedLines} -${fd.removedLines})${changeLabel}${renameLabel}${binaryLabel}`);
|
|
223
|
+
if (fd.isBinary) {
|
|
224
|
+
lines.push('');
|
|
225
|
+
continue;
|
|
226
|
+
}
|
|
227
|
+
// Symbol changes
|
|
228
|
+
const symbols = symbolChanges.get(fd.path);
|
|
229
|
+
if (symbols && symbols.length > 0) {
|
|
230
|
+
for (const sc of symbols) {
|
|
231
|
+
lines.push(` ${sc.changeType}: ${sc.name}() ${sc.lineRange}`);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
// Small diff: include actual hunks
|
|
235
|
+
const totalHunkLines = fd.hunks.reduce((s, h) => s + h.lines.length, 0);
|
|
236
|
+
if (totalHunkLines <= SMALL_DIFF_THRESHOLD && totalHunkLines > 0) {
|
|
237
|
+
for (const hunk of fd.hunks) {
|
|
238
|
+
lines.push(` @@ L${hunk.newStart}`);
|
|
239
|
+
for (const hl of hunk.lines) {
|
|
240
|
+
lines.push(` ${hl}`);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
else if (totalHunkLines > SMALL_DIFF_THRESHOLD) {
|
|
245
|
+
lines.push(` (${totalHunkLines} lines changed — use read_symbol for details)`);
|
|
246
|
+
}
|
|
247
|
+
lines.push('');
|
|
248
|
+
}
|
|
249
|
+
if (allFiles.length > MAX_FILES) {
|
|
250
|
+
lines.push(`Showing ${MAX_FILES} of ${allFiles.length} changed files. Use path filter to narrow.`);
|
|
251
|
+
lines.push('');
|
|
252
|
+
}
|
|
253
|
+
lines.push(`HINT: Use read_symbol(path, symbol) to see full changed code, read_diff(path) for line-level diff.`);
|
|
254
|
+
lines.push(`RAW DIFF: ~${rawTokens} tokens → smart_diff: ~${estimateTokens(lines.join('\n'))} tokens`);
|
|
255
|
+
return lines.join('\n');
|
|
256
|
+
}
|
|
257
|
+
//# sourceMappingURL=smart-diff.js.map
|
package/dist/server.js
CHANGED
|
@@ -27,9 +27,12 @@ import { handleReadForEdit } from './handlers/read-for-edit.js';
|
|
|
27
27
|
import { handleRelatedFiles } from './handlers/related-files.js';
|
|
28
28
|
import { handleOutline } from './handlers/outline.js';
|
|
29
29
|
import { handleCodeAudit } from './handlers/code-audit.js';
|
|
30
|
+
import { handleModuleInfo } from './handlers/module-info.js';
|
|
31
|
+
import { handleSmartDiff } from './handlers/smart-diff.js';
|
|
32
|
+
import { handleExploreArea } from './handlers/explore-area.js';
|
|
30
33
|
import { detectContextMode } from './integration/context-mode-detector.js';
|
|
31
34
|
import { estimateTokens } from './core/token-estimator.js';
|
|
32
|
-
import { resolveSafePath, validateSmartReadArgs, validateReadSymbolArgs, validateReadRangeArgs, validateReadDiffArgs, validateFindUsagesArgs, validateSmartReadManyArgs, validateReadForEditArgs, validateRelatedFilesArgs, validateOutlineArgs, validateFindUnusedArgs, validateCodeAuditArgs, } from './core/validation.js';
|
|
35
|
+
import { resolveSafePath, validateSmartReadArgs, validateReadSymbolArgs, validateReadRangeArgs, validateReadDiffArgs, validateFindUsagesArgs, validateSmartReadManyArgs, validateReadForEditArgs, validateRelatedFilesArgs, validateOutlineArgs, validateFindUnusedArgs, validateCodeAuditArgs, validateProjectOverviewArgs, validateModuleInfoArgs, validateSmartDiffArgs, validateExploreAreaArgs, } from './core/validation.js';
|
|
33
36
|
export async function createServer(projectRoot, options) {
|
|
34
37
|
const config = await loadConfig(projectRoot);
|
|
35
38
|
const astIndex = new AstIndexClient(projectRoot, config.astIndex.timeout, {
|
|
@@ -182,6 +185,8 @@ export async function createServer(projectRoot, options) {
|
|
|
182
185
|
'• Reading file again → smart_read (returns compact reminder, not full content)',
|
|
183
186
|
'• Multiple files → smart_read_many (batch, max 20)',
|
|
184
187
|
'• Code quality audit → code_audit (TODOs, deprecated, structural code patterns)',
|
|
188
|
+
'• Reviewing git changes → smart_diff (structural diff with symbol mapping, not raw patch)',
|
|
189
|
+
'• Starting work on an area → explore_area (outline + imports + tests + git log in one call)',
|
|
185
190
|
'',
|
|
186
191
|
'WHEN TO USE DEFAULT TOOLS (Token Pilot adds no value):',
|
|
187
192
|
'• Small files (≤200 lines) → smart_read returns full content anyway, same as Read',
|
|
@@ -198,8 +203,9 @@ export async function createServer(projectRoot, options) {
|
|
|
198
203
|
'• Text pattern search/counting → Grep (regex, count mode)',
|
|
199
204
|
'• Security audit → Grep for: password, token, secret, credential, hardcoded, api_key, TODO.*security',
|
|
200
205
|
'• Deep dive into specific code → read_symbol (after finding issues)',
|
|
206
|
+
'• Module architecture → module_info (deps, dependents, public API, unused deps)',
|
|
201
207
|
'',
|
|
202
|
-
'WORKFLOW: project_overview → smart_read → read_symbol → read_for_edit → edit →
|
|
208
|
+
'WORKFLOW: project_overview → explore_area → smart_read → read_symbol → read_for_edit → edit → smart_diff',
|
|
203
209
|
].join('\n'),
|
|
204
210
|
});
|
|
205
211
|
server.setRequestHandler(ListToolsRequestSchema, () => ({
|
|
@@ -291,21 +297,31 @@ export async function createServer(projectRoot, options) {
|
|
|
291
297
|
// --- Search & navigation ---
|
|
292
298
|
{
|
|
293
299
|
name: 'find_usages',
|
|
294
|
-
description: 'Use INSTEAD OF Grep/ripgrep for finding symbol references. Semantic search across the project — groups results by: definitions, imports, usages.',
|
|
300
|
+
description: 'Use INSTEAD OF Grep/ripgrep for finding symbol references. Semantic search across the project — groups results by: definitions, imports, usages. (v1.1: added scope, kind, limit, lang filters)',
|
|
295
301
|
inputSchema: {
|
|
296
302
|
type: 'object',
|
|
297
303
|
properties: {
|
|
298
304
|
symbol: { type: 'string', description: 'Symbol name to find usages of' },
|
|
305
|
+
scope: { type: 'string', description: 'Filter results by path prefix (e.g., "src/Domain/")' },
|
|
306
|
+
kind: { type: 'string', enum: ['definitions', 'imports', 'usages', 'all'], description: 'Show only specific section (default: "all")' },
|
|
307
|
+
limit: { type: 'number', description: 'Max results per category (default: 50, max: 500)' },
|
|
308
|
+
lang: { type: 'string', description: 'Filter by language/extension (e.g., "php", "typescript")' },
|
|
299
309
|
},
|
|
300
310
|
required: ['symbol'],
|
|
301
311
|
},
|
|
302
312
|
},
|
|
303
313
|
{
|
|
304
314
|
name: 'project_overview',
|
|
305
|
-
description: 'START HERE for unfamiliar codebases. Shows project type, architecture, framework detection,
|
|
315
|
+
description: 'START HERE for unfamiliar codebases. Shows project type (dual-detection: ast-index + config files), architecture, framework detection, quality tools, CI, directory map. (v1.1: added include filter)',
|
|
306
316
|
inputSchema: {
|
|
307
317
|
type: 'object',
|
|
308
|
-
properties: {
|
|
318
|
+
properties: {
|
|
319
|
+
include: {
|
|
320
|
+
type: 'array',
|
|
321
|
+
items: { type: 'string', enum: ['stack', 'ci', 'quality', 'architecture'] },
|
|
322
|
+
description: 'Sections to include (default: all). Use ["stack"] for quick type check, ["quality","ci"] for tooling overview.',
|
|
323
|
+
},
|
|
324
|
+
},
|
|
309
325
|
},
|
|
310
326
|
},
|
|
311
327
|
{
|
|
@@ -321,11 +337,13 @@ export async function createServer(projectRoot, options) {
|
|
|
321
337
|
},
|
|
322
338
|
{
|
|
323
339
|
name: 'outline',
|
|
324
|
-
description: 'Use INSTEAD OF listing dir + reading each file. One call returns all symbols (classes, functions, methods, routes) for every code file in a directory.',
|
|
340
|
+
description: 'Use INSTEAD OF listing dir + reading each file. One call returns all symbols (classes, functions, methods, routes) for every code file in a directory. (v1.1: added recursive, max_depth)',
|
|
325
341
|
inputSchema: {
|
|
326
342
|
type: 'object',
|
|
327
343
|
properties: {
|
|
328
344
|
path: { type: 'string', description: 'Directory path' },
|
|
345
|
+
recursive: { type: 'boolean', description: 'Recursively outline subdirectories (default: false)' },
|
|
346
|
+
max_depth: { type: 'number', description: 'Max recursion depth when recursive=true (default: 2, max: 5)' },
|
|
329
347
|
},
|
|
330
348
|
required: ['path'],
|
|
331
349
|
},
|
|
@@ -371,6 +389,51 @@ export async function createServer(projectRoot, options) {
|
|
|
371
389
|
required: ['check'],
|
|
372
390
|
},
|
|
373
391
|
},
|
|
392
|
+
{
|
|
393
|
+
name: 'module_info',
|
|
394
|
+
description: 'Analyze module dependencies, dependents, public API, and unused deps. Use for architecture understanding and dependency cleanup.',
|
|
395
|
+
inputSchema: {
|
|
396
|
+
type: 'object',
|
|
397
|
+
properties: {
|
|
398
|
+
module: { type: 'string', description: 'Module name or path pattern (e.g., "auth", "src/Domain/")' },
|
|
399
|
+
check: {
|
|
400
|
+
type: 'string',
|
|
401
|
+
enum: ['deps', 'dependents', 'api', 'unused-deps', 'all'],
|
|
402
|
+
description: 'What to check: "deps" (dependencies), "dependents" (who depends on this), "api" (public symbols), "unused-deps" (dead dependencies), "all" (everything). Default: "all"',
|
|
403
|
+
},
|
|
404
|
+
},
|
|
405
|
+
required: ['module'],
|
|
406
|
+
},
|
|
407
|
+
},
|
|
408
|
+
// --- Diff & exploration ---
|
|
409
|
+
{
|
|
410
|
+
name: 'smart_diff',
|
|
411
|
+
description: 'Use INSTEAD OF raw git diff. Shows changed files with AST symbol mapping — which functions/classes were modified/added/removed. Small diffs include hunks, large diffs show summary.',
|
|
412
|
+
inputSchema: {
|
|
413
|
+
type: 'object',
|
|
414
|
+
properties: {
|
|
415
|
+
scope: { type: 'string', enum: ['unstaged', 'staged', 'commit', 'branch'], description: 'Diff scope (default: "unstaged")' },
|
|
416
|
+
path: { type: 'string', description: 'Filter to specific file or directory' },
|
|
417
|
+
ref: { type: 'string', description: 'Git ref — required for scope="commit" (commit hash) or scope="branch" (branch name)' },
|
|
418
|
+
},
|
|
419
|
+
},
|
|
420
|
+
},
|
|
421
|
+
{
|
|
422
|
+
name: 'explore_area',
|
|
423
|
+
description: 'One-call exploration of a directory: outline (all symbols), imports (external deps + who imports this area), tests (matching test files), recent git changes. Use INSTEAD OF separate outline + related_files + git log calls.',
|
|
424
|
+
inputSchema: {
|
|
425
|
+
type: 'object',
|
|
426
|
+
properties: {
|
|
427
|
+
path: { type: 'string', description: 'Directory path (or file path — will use its parent directory)' },
|
|
428
|
+
include: {
|
|
429
|
+
type: 'array',
|
|
430
|
+
items: { type: 'string', enum: ['outline', 'imports', 'tests', 'changes'] },
|
|
431
|
+
description: 'Sections to include (default: all)',
|
|
432
|
+
},
|
|
433
|
+
},
|
|
434
|
+
required: ['path'],
|
|
435
|
+
},
|
|
436
|
+
},
|
|
374
437
|
],
|
|
375
438
|
}));
|
|
376
439
|
// Helper: get real full-file token count for honest analytics
|
|
@@ -476,7 +539,8 @@ export async function createServer(projectRoot, options) {
|
|
|
476
539
|
return usagesResult;
|
|
477
540
|
}
|
|
478
541
|
case 'project_overview': {
|
|
479
|
-
const
|
|
542
|
+
const overviewArgs = validateProjectOverviewArgs(args);
|
|
543
|
+
const overviewResult = await handleProjectOverview(overviewArgs, projectRoot, astIndex);
|
|
480
544
|
const overviewText = overviewResult.content[0]?.text ?? '';
|
|
481
545
|
overviewResult.content[0] = { type: 'text', text: `TOKEN PILOT v${pkgVersion}\n\n${overviewText}` };
|
|
482
546
|
const ovTokens = estimateTokens(overviewResult.content[0].text);
|
|
@@ -513,6 +577,33 @@ export async function createServer(projectRoot, options) {
|
|
|
513
577
|
analytics.record({ tool: 'code_audit', path: auditArgs.check, tokensReturned: estimateTokens(auditText), tokensWouldBe: estimateTokens(auditText), timestamp: Date.now() });
|
|
514
578
|
return auditResult;
|
|
515
579
|
}
|
|
580
|
+
case 'module_info': {
|
|
581
|
+
const moduleArgs = validateModuleInfoArgs(args);
|
|
582
|
+
const moduleResult = await handleModuleInfo(moduleArgs, projectRoot, astIndex);
|
|
583
|
+
const moduleText = moduleResult.content[0]?.text ?? '';
|
|
584
|
+
// Estimate: manual analysis would require reading all module files + grepping deps
|
|
585
|
+
const moduleWouldBe = estimateTokens(moduleText) * 5;
|
|
586
|
+
analytics.record({ tool: 'module_info', path: moduleArgs.module, tokensReturned: estimateTokens(moduleText), tokensWouldBe: moduleWouldBe, timestamp: Date.now() });
|
|
587
|
+
return moduleResult;
|
|
588
|
+
}
|
|
589
|
+
case 'smart_diff': {
|
|
590
|
+
const sdArgs = validateSmartDiffArgs(args);
|
|
591
|
+
const sdResult = await handleSmartDiff(sdArgs, projectRoot, astIndex);
|
|
592
|
+
const sdText = sdResult.content[0]?.text ?? '';
|
|
593
|
+
const sdTokens = estimateTokens(sdText);
|
|
594
|
+
analytics.record({ tool: 'smart_diff', path: sdArgs.path ?? sdArgs.scope ?? 'unstaged', tokensReturned: sdTokens, tokensWouldBe: sdResult.rawTokens || sdTokens, timestamp: Date.now() });
|
|
595
|
+
return { content: sdResult.content };
|
|
596
|
+
}
|
|
597
|
+
case 'explore_area': {
|
|
598
|
+
const eaArgs = validateExploreAreaArgs(args);
|
|
599
|
+
const eaResult = await handleExploreArea(eaArgs, projectRoot, astIndex);
|
|
600
|
+
const eaText = eaResult.content[0]?.text ?? '';
|
|
601
|
+
const eaTokens = estimateTokens(eaText);
|
|
602
|
+
// Without explore_area, agent would call: outline + related_files + git log = ~3-5x tokens
|
|
603
|
+
const eaWouldBe = eaTokens * 4;
|
|
604
|
+
analytics.record({ tool: 'explore_area', path: eaArgs.path, tokensReturned: eaTokens, tokensWouldBe: eaWouldBe, timestamp: Date.now() });
|
|
605
|
+
return eaResult;
|
|
606
|
+
}
|
|
516
607
|
default:
|
|
517
608
|
return {
|
|
518
609
|
content: [{ type: 'text', text: `Unknown tool: ${name}` }],
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "token-pilot",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.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",
|