token-pilot 0.13.0 → 0.14.2
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/hooks/hooks.json +9 -0
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/CHANGELOG.md +29 -0
- package/README.md +36 -15
- package/dist/config/defaults.js +12 -0
- package/dist/core/architecture-fingerprint.d.ts +34 -0
- package/dist/core/architecture-fingerprint.js +127 -0
- package/dist/core/budget-planner.d.ts +21 -0
- package/dist/core/budget-planner.js +68 -0
- package/dist/core/confidence.d.ts +31 -0
- package/dist/core/confidence.js +99 -0
- package/dist/core/context-registry.d.ts +14 -0
- package/dist/core/context-registry.js +55 -0
- package/dist/core/decision-trace.d.ts +31 -0
- package/dist/core/decision-trace.js +45 -0
- package/dist/core/intent-classifier.d.ts +13 -0
- package/dist/core/intent-classifier.js +44 -0
- package/dist/core/policy-engine.d.ts +41 -0
- package/dist/core/policy-engine.js +76 -0
- package/dist/core/session-analytics.d.ts +8 -0
- package/dist/core/session-analytics.js +86 -7
- package/dist/core/session-cache.d.ts +74 -0
- package/dist/core/session-cache.js +162 -0
- package/dist/core/validation.d.ts +3 -0
- package/dist/core/validation.js +3 -0
- package/dist/git/file-watcher.d.ts +6 -0
- package/dist/git/file-watcher.js +18 -2
- package/dist/git/watcher.d.ts +3 -0
- package/dist/git/watcher.js +6 -0
- package/dist/handlers/code-audit.d.ts +7 -2
- package/dist/handlers/code-audit.js +19 -5
- package/dist/handlers/explore-area.d.ts +10 -0
- package/dist/handlers/explore-area.js +39 -13
- package/dist/handlers/find-unused.d.ts +3 -0
- package/dist/handlers/find-unused.js +3 -2
- package/dist/handlers/find-usages.d.ts +7 -0
- package/dist/handlers/find-usages.js +36 -5
- package/dist/handlers/module-info.d.ts +3 -0
- package/dist/handlers/module-info.js +22 -2
- package/dist/handlers/project-overview.d.ts +1 -1
- package/dist/handlers/project-overview.js +18 -2
- package/dist/handlers/read-for-edit.d.ts +3 -0
- package/dist/handlers/read-for-edit.js +185 -3
- package/dist/handlers/read-range.d.ts +1 -1
- package/dist/handlers/read-range.js +16 -1
- package/dist/handlers/read-symbol.d.ts +1 -1
- package/dist/handlers/read-symbol.js +26 -2
- package/dist/handlers/related-files.d.ts +11 -0
- package/dist/handlers/related-files.js +178 -42
- package/dist/handlers/smart-read-many.js +70 -16
- package/dist/handlers/smart-read.js +10 -1
- package/dist/handlers/test-summary.js +26 -3
- package/dist/hooks/installer.d.ts +12 -8
- package/dist/hooks/installer.js +24 -8
- package/dist/index.d.ts +16 -1
- package/dist/index.js +62 -56
- package/dist/server.js +395 -30
- package/dist/types.d.ts +12 -0
- package/package.json +18 -14
- package/start.sh +28 -27
- package/dist/handlers/class-hierarchy.d.ts +0 -11
- package/dist/handlers/class-hierarchy.js +0 -28
- package/dist/handlers/export-ast-index.d.ts +0 -22
- package/dist/handlers/export-ast-index.js +0 -175
- package/dist/handlers/find-implementations.d.ts +0 -11
- package/dist/handlers/find-implementations.js +0 -27
- package/dist/handlers/search-code.d.ts +0 -14
- package/dist/handlers/search-code.js +0 -32
|
@@ -1,5 +1,9 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { existsSync } from 'node:fs';
|
|
2
|
+
import { execFile } from 'node:child_process';
|
|
3
|
+
import { promisify } from 'node:util';
|
|
4
|
+
import { basename, dirname, extname, relative, resolve } from 'node:path';
|
|
2
5
|
import { resolveSafePath } from '../core/validation.js';
|
|
6
|
+
const execFileAsync = promisify(execFile);
|
|
3
7
|
/**
|
|
4
8
|
* Language families — files with extensions in the same family are considered related.
|
|
5
9
|
* This prevents cross-language false positives (e.g. Python files showing as importers of TS).
|
|
@@ -32,36 +36,64 @@ const TEST_PATTERNS = [
|
|
|
32
36
|
/\/tests?\//,
|
|
33
37
|
];
|
|
34
38
|
export async function handleRelatedFiles(args, projectRoot, astIndex) {
|
|
39
|
+
const emptyMeta = { imports: [], importedBy: [], tests: [], ranked: { high: [], medium: [], low: [] } };
|
|
35
40
|
if (astIndex.isDisabled() || astIndex.isOversized()) {
|
|
36
|
-
return {
|
|
41
|
+
return {
|
|
42
|
+
content: [{
|
|
43
|
+
type: 'text',
|
|
44
|
+
text: 'related_files is disabled: ' + (astIndex.isDisabled()
|
|
37
45
|
? 'project root not detected. Call smart_read() on any project file first — this auto-detects the project root and enables ast-index tools.'
|
|
38
|
-
: 'ast-index built >50k files (likely includes node_modules). Ensure node_modules is in .gitignore.')
|
|
39
|
-
'\nAlternative: use smart_read() to see file imports in the outline.'
|
|
46
|
+
: 'ast-index built >50k files (likely includes node_modules). Ensure node_modules is in .gitignore.')
|
|
47
|
+
+ '\nAlternative: use smart_read() to see file imports in the outline.',
|
|
48
|
+
}],
|
|
49
|
+
meta: emptyMeta,
|
|
50
|
+
};
|
|
40
51
|
}
|
|
41
52
|
const absPath = resolveSafePath(projectRoot, args.path);
|
|
42
53
|
const fileName = basename(absPath);
|
|
43
54
|
const fileBase = fileName.replace(/\.\w+$/, '');
|
|
44
|
-
const
|
|
45
|
-
//
|
|
55
|
+
const fileDir = dirname(absPath);
|
|
56
|
+
// Scoring map: relPath → RankedFile
|
|
57
|
+
const fileScores = new Map();
|
|
58
|
+
function addScore(relPath, points, tag) {
|
|
59
|
+
const existing = fileScores.get(relPath);
|
|
60
|
+
if (existing) {
|
|
61
|
+
existing.score += points;
|
|
62
|
+
if (!existing.tags.includes(tag))
|
|
63
|
+
existing.tags.push(tag);
|
|
64
|
+
}
|
|
65
|
+
else {
|
|
66
|
+
fileScores.set(relPath, { relPath, score: points, tags: [tag] });
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
// Track original categories for backwards-compatible meta
|
|
70
|
+
const importPaths = new Set();
|
|
71
|
+
const importedByPaths = [];
|
|
72
|
+
const testPaths = [];
|
|
73
|
+
// 1. Forward imports (what this file imports) → +4 per file
|
|
46
74
|
try {
|
|
47
75
|
const imports = await astIndex.fileImports(absPath);
|
|
48
76
|
if (imports && imports.length > 0) {
|
|
49
|
-
sections.push('IMPORTS (this file uses):');
|
|
50
77
|
for (const imp of imports) {
|
|
51
|
-
const
|
|
52
|
-
|
|
78
|
+
const resolvedImport = resolveImportPath(absPath, imp.source, projectRoot);
|
|
79
|
+
if (resolvedImport) {
|
|
80
|
+
const relPath = relative(projectRoot, resolvedImport);
|
|
81
|
+
importPaths.add(relPath);
|
|
82
|
+
addScore(relPath, 4, 'import');
|
|
83
|
+
// Same directory bonus
|
|
84
|
+
if (dirname(resolvedImport) === fileDir) {
|
|
85
|
+
addScore(relPath, 2, 'same-dir');
|
|
86
|
+
}
|
|
87
|
+
}
|
|
53
88
|
}
|
|
54
|
-
sections.push('');
|
|
55
89
|
}
|
|
56
90
|
}
|
|
57
91
|
catch {
|
|
58
92
|
// fileImports not available — skip silently
|
|
59
93
|
}
|
|
60
|
-
// 2. Reverse imports (what imports this file)
|
|
61
|
-
const importedBy = [];
|
|
94
|
+
// 2. Reverse imports (what imports this file) → +3 per file, +1 per extra ref
|
|
62
95
|
const sourceLang = getLangFamily(absPath);
|
|
63
96
|
try {
|
|
64
|
-
// Get structure to find exported symbol names
|
|
65
97
|
const structure = await astIndex.outline(absPath);
|
|
66
98
|
const exportNames = [];
|
|
67
99
|
if (structure) {
|
|
@@ -71,85 +103,189 @@ export async function handleRelatedFiles(args, projectRoot, astIndex) {
|
|
|
71
103
|
break;
|
|
72
104
|
}
|
|
73
105
|
}
|
|
74
|
-
// Also try the file base name as a symbol
|
|
75
106
|
if (!exportNames.includes(fileBase)) {
|
|
76
107
|
exportNames.push(fileBase);
|
|
77
108
|
}
|
|
78
|
-
// Search refs for each exported symbol (check imports + usages)
|
|
79
109
|
const seenFiles = new Set();
|
|
80
110
|
seenFiles.add(absPath);
|
|
111
|
+
// Track ref count per file for multi-ref bonus
|
|
112
|
+
const refCounts = new Map();
|
|
81
113
|
for (const name of exportNames) {
|
|
82
114
|
try {
|
|
83
115
|
const refs = await astIndex.refs(name, 30);
|
|
84
|
-
// Check both imports and usages — imports catch direct `import X from`,
|
|
85
|
-
// usages catch re-exports, function calls, type references from other files
|
|
86
116
|
const refEntries = [
|
|
87
117
|
...(refs?.imports ?? []),
|
|
88
118
|
...(refs?.usages ?? []),
|
|
89
119
|
];
|
|
90
120
|
for (const ref of refEntries) {
|
|
91
121
|
const refPath = ref.path;
|
|
92
|
-
if (!refPath || seenFiles.has(refPath))
|
|
122
|
+
if (!refPath || seenFiles.has(refPath)) {
|
|
123
|
+
// Still count extra refs for already-seen files
|
|
124
|
+
if (refPath && refPath !== absPath) {
|
|
125
|
+
const rp = relative(projectRoot, refPath);
|
|
126
|
+
refCounts.set(rp, (refCounts.get(rp) ?? 0) + 1);
|
|
127
|
+
}
|
|
93
128
|
continue;
|
|
94
|
-
|
|
95
|
-
// only include files from the same language family
|
|
129
|
+
}
|
|
96
130
|
if (sourceLang) {
|
|
97
131
|
const refLang = getLangFamily(refPath);
|
|
98
132
|
if (refLang && refLang !== sourceLang)
|
|
99
133
|
continue;
|
|
100
134
|
}
|
|
101
135
|
seenFiles.add(refPath);
|
|
102
|
-
|
|
136
|
+
const relPath = relative(projectRoot, refPath);
|
|
137
|
+
importedByPaths.push(relPath);
|
|
138
|
+
refCounts.set(relPath, (refCounts.get(relPath) ?? 0) + 1);
|
|
139
|
+
addScore(relPath, 3, 'importer');
|
|
140
|
+
// Same directory bonus
|
|
141
|
+
if (dirname(refPath) === fileDir) {
|
|
142
|
+
addScore(relPath, 2, 'same-dir');
|
|
143
|
+
}
|
|
103
144
|
}
|
|
104
145
|
}
|
|
105
146
|
catch {
|
|
106
147
|
// skip symbol
|
|
107
148
|
}
|
|
108
149
|
}
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
150
|
+
// Apply multi-ref bonus: +1 per extra ref beyond the first
|
|
151
|
+
for (const [relPath, count] of refCounts) {
|
|
152
|
+
if (count > 1) {
|
|
153
|
+
addScore(relPath, count - 1, 'multi-ref');
|
|
113
154
|
}
|
|
114
|
-
sections.push('');
|
|
115
155
|
}
|
|
116
156
|
}
|
|
117
157
|
catch {
|
|
118
158
|
// refs not available — skip silently
|
|
119
159
|
}
|
|
120
|
-
// 3. Test files
|
|
160
|
+
// 3. Test files → +5 per file
|
|
121
161
|
try {
|
|
122
162
|
const allFiles = await astIndex.listFiles();
|
|
123
|
-
const testFiles = [];
|
|
124
163
|
if (allFiles && allFiles.length > 0) {
|
|
125
164
|
for (const f of allFiles) {
|
|
126
|
-
// Match test files for this module
|
|
127
165
|
const fBase = basename(f);
|
|
128
166
|
if (fBase.includes(fileBase) && TEST_PATTERNS.some(p => p.test(f))) {
|
|
129
|
-
|
|
167
|
+
const relPath = relative(projectRoot, f);
|
|
168
|
+
testPaths.push(relPath);
|
|
169
|
+
addScore(relPath, 5, 'test');
|
|
130
170
|
}
|
|
131
171
|
}
|
|
132
172
|
}
|
|
133
|
-
if (testFiles.length > 0) {
|
|
134
|
-
sections.push('TESTS:');
|
|
135
|
-
for (const t of testFiles) {
|
|
136
|
-
sections.push(` → ${t}`);
|
|
137
|
-
}
|
|
138
|
-
sections.push('');
|
|
139
|
-
}
|
|
140
173
|
}
|
|
141
174
|
catch {
|
|
142
175
|
// listFiles not available — skip silently
|
|
143
176
|
}
|
|
144
|
-
// 4.
|
|
145
|
-
|
|
177
|
+
// 4. Recently changed files → +2 boost
|
|
178
|
+
const changedFiles = await getRecentlyChangedFiles(projectRoot);
|
|
179
|
+
for (const [, ranked] of fileScores) {
|
|
180
|
+
if (changedFiles.has(ranked.relPath)) {
|
|
181
|
+
addScore(ranked.relPath, 2, 'changed');
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
// 5. Sort by score and bucket into high/medium/low
|
|
185
|
+
const allRanked = Array.from(fileScores.values()).sort((a, b) => b.score - a.score);
|
|
186
|
+
const high = [];
|
|
187
|
+
const medium = [];
|
|
188
|
+
const low = [];
|
|
189
|
+
for (const r of allRanked) {
|
|
190
|
+
if (r.score >= 5)
|
|
191
|
+
high.push(r);
|
|
192
|
+
else if (r.score >= 3)
|
|
193
|
+
medium.push(r);
|
|
194
|
+
else
|
|
195
|
+
low.push(r);
|
|
196
|
+
}
|
|
197
|
+
// 6. Build output
|
|
198
|
+
const sections = [`RELATED FILES: ${args.path}`, ''];
|
|
199
|
+
if (high.length > 0) {
|
|
200
|
+
sections.push(`HIGH VALUE (${high.length} file${high.length > 1 ? 's' : ''} — read these first):`);
|
|
201
|
+
for (const r of high) {
|
|
202
|
+
sections.push(` ★ ${r.relPath} [${r.tags.join(', ')}]`);
|
|
203
|
+
}
|
|
204
|
+
sections.push('');
|
|
205
|
+
}
|
|
206
|
+
if (medium.length > 0) {
|
|
207
|
+
sections.push(`MEDIUM (${medium.length} file${medium.length > 1 ? 's' : ''}):`);
|
|
208
|
+
for (const r of medium) {
|
|
209
|
+
sections.push(` · ${r.relPath} [${r.tags.join(', ')}]`);
|
|
210
|
+
}
|
|
211
|
+
sections.push('');
|
|
212
|
+
}
|
|
213
|
+
if (low.length > 0) {
|
|
214
|
+
sections.push(`LOW (${low.length} file${low.length > 1 ? 's' : ''} — read only if needed):`);
|
|
215
|
+
for (const r of low) {
|
|
216
|
+
sections.push(` · ${r.relPath} [${r.tags.join(', ')}]`);
|
|
217
|
+
}
|
|
218
|
+
sections.push('');
|
|
219
|
+
}
|
|
220
|
+
if (allRanked.length === 0) {
|
|
146
221
|
sections.push('No related files found. AST index may not cover this file.');
|
|
147
222
|
sections.push('HINT: Use smart_read() to explore the file structure.');
|
|
148
223
|
}
|
|
149
224
|
else {
|
|
150
|
-
|
|
151
|
-
|
|
225
|
+
const highPaths = high.map(r => `"${r.relPath}"`).join(', ');
|
|
226
|
+
if (high.length > 0) {
|
|
227
|
+
sections.push(`HINT: Use smart_read_many(paths=[${highPaths}]) to read the most relevant files.`);
|
|
228
|
+
}
|
|
229
|
+
else {
|
|
230
|
+
sections.push('HINT: Use smart_read_many(paths=[...]) to read related files at once.');
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
return {
|
|
234
|
+
content: [{ type: 'text', text: sections.join('\n') }],
|
|
235
|
+
meta: {
|
|
236
|
+
imports: Array.from(importPaths).sort(),
|
|
237
|
+
importedBy: Array.from(new Set(importedByPaths)).sort(),
|
|
238
|
+
tests: Array.from(new Set(testPaths)).sort(),
|
|
239
|
+
ranked: {
|
|
240
|
+
high: high.map(r => r.relPath),
|
|
241
|
+
medium: medium.map(r => r.relPath),
|
|
242
|
+
low: low.map(r => r.relPath),
|
|
243
|
+
},
|
|
244
|
+
},
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
/** Get files changed in the last 5 commits (single git call). */
|
|
248
|
+
async function getRecentlyChangedFiles(projectRoot) {
|
|
249
|
+
try {
|
|
250
|
+
const { stdout } = await execFileAsync('git', ['diff', '--name-only', 'HEAD~5'], {
|
|
251
|
+
cwd: projectRoot,
|
|
252
|
+
timeout: 5000,
|
|
253
|
+
});
|
|
254
|
+
const files = stdout.trim().split('\n').filter(Boolean);
|
|
255
|
+
return new Set(files);
|
|
256
|
+
}
|
|
257
|
+
catch {
|
|
258
|
+
// git not available, not a repo, or <5 commits — try smaller range
|
|
259
|
+
try {
|
|
260
|
+
const { stdout } = await execFileAsync('git', ['diff', '--name-only', 'HEAD~1'], {
|
|
261
|
+
cwd: projectRoot,
|
|
262
|
+
timeout: 5000,
|
|
263
|
+
});
|
|
264
|
+
const files = stdout.trim().split('\n').filter(Boolean);
|
|
265
|
+
return new Set(files);
|
|
266
|
+
}
|
|
267
|
+
catch {
|
|
268
|
+
return new Set();
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
function resolveImportPath(sourceFile, importSource, projectRoot) {
|
|
273
|
+
if (!importSource.startsWith('.') && !importSource.startsWith('/')) {
|
|
274
|
+
return null;
|
|
275
|
+
}
|
|
276
|
+
const basePath = importSource.startsWith('/')
|
|
277
|
+
? resolve(projectRoot, '.' + importSource)
|
|
278
|
+
: resolve(dirname(sourceFile), importSource);
|
|
279
|
+
const candidates = [
|
|
280
|
+
basePath,
|
|
281
|
+
...['.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs', '.py', '.php', '.go', '.rs', '.java', '.kt', '.swift']
|
|
282
|
+
.flatMap((ext) => [`${basePath}${ext}`, resolve(basePath, `index${ext}`)]),
|
|
283
|
+
];
|
|
284
|
+
for (const candidate of candidates) {
|
|
285
|
+
if (candidate.startsWith(projectRoot) && existsSync(candidate)) {
|
|
286
|
+
return candidate;
|
|
287
|
+
}
|
|
152
288
|
}
|
|
153
|
-
return
|
|
289
|
+
return null;
|
|
154
290
|
}
|
|
155
291
|
//# sourceMappingURL=related-files.js.map
|
|
@@ -1,32 +1,86 @@
|
|
|
1
1
|
import { handleSmartRead } from './smart-read.js';
|
|
2
|
-
import { estimateTokens } from '../core/token-estimator.js';
|
|
2
|
+
import { estimateTokens, formatSavings } from '../core/token-estimator.js';
|
|
3
|
+
import { readFile } from 'node:fs/promises';
|
|
4
|
+
import { resolveSafePath } from '../core/validation.js';
|
|
5
|
+
const MAX_BATCH_FILES = 20;
|
|
6
|
+
const MAX_BATCH_TOKENS = 1400;
|
|
7
|
+
const MAX_FILE_TOKENS = 220;
|
|
8
|
+
const MAX_FILE_LINES = 24;
|
|
9
|
+
const BATCH_CONCURRENCY = 4;
|
|
3
10
|
export async function handleSmartReadMany(args, projectRoot, astIndex, fileCache, contextRegistry, config) {
|
|
4
11
|
if (!args.paths || args.paths.length === 0) {
|
|
5
12
|
return {
|
|
6
13
|
content: [{ type: 'text', text: 'No paths provided.' }],
|
|
7
14
|
};
|
|
8
15
|
}
|
|
9
|
-
if (args.paths.length >
|
|
16
|
+
if (args.paths.length > MAX_BATCH_FILES) {
|
|
10
17
|
return {
|
|
11
|
-
content: [{ type: 'text', text: `Too many files (${args.paths.length}). Maximum is
|
|
18
|
+
content: [{ type: 'text', text: `Too many files (${args.paths.length}). Maximum is ${MAX_BATCH_FILES} per batch.` }],
|
|
12
19
|
};
|
|
13
20
|
}
|
|
14
|
-
const
|
|
15
|
-
|
|
16
|
-
for (
|
|
17
|
-
|
|
21
|
+
const uniquePaths = Array.from(new Set(args.paths));
|
|
22
|
+
const entries = [];
|
|
23
|
+
for (let i = 0; i < uniquePaths.length; i += BATCH_CONCURRENCY) {
|
|
24
|
+
const batch = uniquePaths.slice(i, i + BATCH_CONCURRENCY);
|
|
25
|
+
const settled = await Promise.allSettled(batch.map(async (path) => {
|
|
18
26
|
const result = await handleSmartRead({ path }, projectRoot, astIndex, fileCache, contextRegistry, config);
|
|
19
27
|
const text = result.content[0]?.text ?? '';
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
const
|
|
25
|
-
|
|
28
|
+
const fullTokens = await estimateFullFileTokens(projectRoot, path);
|
|
29
|
+
return { path, text, fullTokens };
|
|
30
|
+
}));
|
|
31
|
+
for (let index = 0; index < settled.length; index++) {
|
|
32
|
+
const outcome = settled[index];
|
|
33
|
+
const path = batch[index];
|
|
34
|
+
if (outcome.status === 'fulfilled') {
|
|
35
|
+
entries.push(outcome.value);
|
|
36
|
+
}
|
|
37
|
+
else {
|
|
38
|
+
const msg = outcome.reason instanceof Error ? outcome.reason.message : String(outcome.reason);
|
|
39
|
+
entries.push({ path, text: `FILE: ${path}\nERROR: ${msg}`, fullTokens: 0 });
|
|
40
|
+
}
|
|
26
41
|
}
|
|
27
42
|
}
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
43
|
+
let remainingBudget = MAX_BATCH_TOKENS;
|
|
44
|
+
const renderedEntries = [];
|
|
45
|
+
for (const entry of entries) {
|
|
46
|
+
const compacted = compactBatchEntry(entry, remainingBudget);
|
|
47
|
+
renderedEntries.push(compacted);
|
|
48
|
+
remainingBudget = Math.max(0, remainingBudget - estimateTokens(compacted));
|
|
49
|
+
}
|
|
50
|
+
const body = renderedEntries.join('\n\n---\n\n');
|
|
51
|
+
const actualTokens = estimateTokens(body);
|
|
52
|
+
const fullTokens = entries.reduce((sum, entry) => sum + entry.fullTokens, 0);
|
|
53
|
+
const duplicatesRemoved = args.paths.length - uniquePaths.length;
|
|
54
|
+
const footer = [''];
|
|
55
|
+
footer.push(`BATCH: ${uniquePaths.length} unique files loaded${duplicatesRemoved > 0 ? ` (${duplicatesRemoved} duplicates skipped)` : ''}`);
|
|
56
|
+
footer.push(`OUTPUT: ~${actualTokens} tokens`);
|
|
57
|
+
if (fullTokens > 0) {
|
|
58
|
+
footer.push(formatSavings(actualTokens, fullTokens));
|
|
59
|
+
}
|
|
60
|
+
footer.push('HINT: Re-run smart_read(path) on any compacted file for full detail.');
|
|
61
|
+
return { content: [{ type: 'text', text: body + '\n' + footer.join('\n') }] };
|
|
62
|
+
}
|
|
63
|
+
function compactBatchEntry(entry, remainingBudget) {
|
|
64
|
+
const rawTokens = estimateTokens(entry.text);
|
|
65
|
+
if (remainingBudget <= 60) {
|
|
66
|
+
return `FILE: ${entry.path}\n(compacted in batch mode — use smart_read("${entry.path}") for full detail)`;
|
|
67
|
+
}
|
|
68
|
+
if (rawTokens <= Math.min(MAX_FILE_TOKENS, remainingBudget)) {
|
|
69
|
+
return entry.text;
|
|
70
|
+
}
|
|
71
|
+
const lines = entry.text.split('\n');
|
|
72
|
+
const head = lines.slice(0, MAX_FILE_LINES).join('\n');
|
|
73
|
+
const suffix = `\n\n... compacted for batch mode. Use smart_read("${entry.path}") for full detail.`;
|
|
74
|
+
return head + suffix;
|
|
75
|
+
}
|
|
76
|
+
async function estimateFullFileTokens(projectRoot, relativePath) {
|
|
77
|
+
try {
|
|
78
|
+
const absPath = resolveSafePath(projectRoot, relativePath);
|
|
79
|
+
const content = await readFile(absPath, 'utf-8');
|
|
80
|
+
return estimateTokens(content);
|
|
81
|
+
}
|
|
82
|
+
catch {
|
|
83
|
+
return 0;
|
|
84
|
+
}
|
|
31
85
|
}
|
|
32
86
|
//# sourceMappingURL=smart-read-many.js.map
|
|
@@ -4,6 +4,7 @@ import { formatOutline } from '../formatters/structure.js';
|
|
|
4
4
|
import { estimateTokens, formatSavings } from '../core/token-estimator.js';
|
|
5
5
|
import { resolveSafePath } from '../core/validation.js';
|
|
6
6
|
import { isNonCodeStructured, handleNonCodeRead } from './non-code.js';
|
|
7
|
+
import { assessConfidence, formatConfidence } from '../core/confidence.js';
|
|
7
8
|
export async function handleSmartRead(args, projectRoot, astIndex, fileCache, contextRegistry, config) {
|
|
8
9
|
const absPath = resolveSafePath(projectRoot, args.path);
|
|
9
10
|
// 0. Guard: directory passed instead of file
|
|
@@ -134,6 +135,14 @@ export async function handleSmartRead(args, projectRoot, astIndex, fileCache, co
|
|
|
134
135
|
tokens: structureTokens,
|
|
135
136
|
});
|
|
136
137
|
contextRegistry.setContentHash(absPath, cached.hash);
|
|
137
|
-
|
|
138
|
+
// 9. Confidence metadata
|
|
139
|
+
const confidenceMeta = assessConfidence({
|
|
140
|
+
symbolResolved: (cached.structure.symbols?.length ?? 0) > 0,
|
|
141
|
+
fullFile: false,
|
|
142
|
+
truncated: false,
|
|
143
|
+
astAvailable: true,
|
|
144
|
+
crossFileDeps: cached.structure.imports?.length ?? 0,
|
|
145
|
+
});
|
|
146
|
+
return { content: [{ type: 'text', text: output + savings + formatConfidence(confidenceMeta) }] };
|
|
138
147
|
}
|
|
139
148
|
//# sourceMappingURL=smart-read.js.map
|
|
@@ -37,7 +37,16 @@ export async function handleTestSummary(args, projectRoot) {
|
|
|
37
37
|
const rawTokens = estimateTokens(rawOutput);
|
|
38
38
|
const runner = args.runner ?? detectRunner(command, rawOutput);
|
|
39
39
|
const result = parseTestOutput(rawOutput, runner);
|
|
40
|
-
const
|
|
40
|
+
const commandFailed = exitCode !== 0;
|
|
41
|
+
if (commandFailed && result.failed === 0) {
|
|
42
|
+
result.failed = 1;
|
|
43
|
+
result.total = Math.max(result.total, result.passed + result.failed + result.skipped);
|
|
44
|
+
result.failures.unshift({
|
|
45
|
+
name: `Command exited with code ${exitCode}`,
|
|
46
|
+
error: summarizeCommandError(rawOutput),
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
const formatted = formatTestSummary(result, command, runner, rawTokens, exitCode, commandFailed);
|
|
41
50
|
return {
|
|
42
51
|
content: [{ type: 'text', text: formatted }],
|
|
43
52
|
rawTokens,
|
|
@@ -277,12 +286,23 @@ function parseGeneric(output) {
|
|
|
277
286
|
: result.passed + result.failed + result.skipped;
|
|
278
287
|
return result;
|
|
279
288
|
}
|
|
289
|
+
function summarizeCommandError(output) {
|
|
290
|
+
const lines = output
|
|
291
|
+
.split('\n')
|
|
292
|
+
.map(line => line.trim())
|
|
293
|
+
.filter(line => line.length > 0)
|
|
294
|
+
.filter(line => !line.startsWith('at ') && !line.startsWith('>'));
|
|
295
|
+
if (lines.length === 0) {
|
|
296
|
+
return 'Command failed without producing output.';
|
|
297
|
+
}
|
|
298
|
+
return lines.slice(0, 3).join('\n').substring(0, 300);
|
|
299
|
+
}
|
|
280
300
|
// ──────────────────────────────────────────────
|
|
281
301
|
// Formatter
|
|
282
302
|
// ──────────────────────────────────────────────
|
|
283
|
-
function formatTestSummary(result, command, runner, rawTokens) {
|
|
303
|
+
function formatTestSummary(result, command, runner, rawTokens, exitCode, commandFailed) {
|
|
284
304
|
const lines = [];
|
|
285
|
-
const status = result.failed > 0 ? '❌ FAIL' : '✅ PASS';
|
|
305
|
+
const status = result.failed > 0 || commandFailed ? '❌ FAIL' : '✅ PASS';
|
|
286
306
|
lines.push(`TEST RESULT: ${status} (${runner})`);
|
|
287
307
|
lines.push('');
|
|
288
308
|
// Stats line
|
|
@@ -298,6 +318,9 @@ function formatTestSummary(result, command, runner, rawTokens) {
|
|
|
298
318
|
if (result.suites)
|
|
299
319
|
parts.push(`${result.suites} suites`);
|
|
300
320
|
lines.push(parts.join(' | '));
|
|
321
|
+
if (commandFailed && exitCode != null) {
|
|
322
|
+
lines.push(`Exit code: ${exitCode}`);
|
|
323
|
+
}
|
|
301
324
|
// Failed tests detail
|
|
302
325
|
if (result.failures.length > 0) {
|
|
303
326
|
lines.push('');
|
|
@@ -1,16 +1,20 @@
|
|
|
1
|
+
export interface HookInstallResult {
|
|
2
|
+
installed: boolean;
|
|
3
|
+
fatal: boolean;
|
|
4
|
+
message: string;
|
|
5
|
+
}
|
|
6
|
+
export interface HookUninstallResult {
|
|
7
|
+
removed: boolean;
|
|
8
|
+
fatal: boolean;
|
|
9
|
+
message: string;
|
|
10
|
+
}
|
|
1
11
|
/**
|
|
2
12
|
* Install Token Pilot hook into Claude Code settings.
|
|
3
13
|
* Creates or updates .claude/settings.json with PreToolUse hook.
|
|
4
14
|
*/
|
|
5
|
-
export declare function installHook(projectRoot: string): Promise<
|
|
6
|
-
installed: boolean;
|
|
7
|
-
message: string;
|
|
8
|
-
}>;
|
|
15
|
+
export declare function installHook(projectRoot: string): Promise<HookInstallResult>;
|
|
9
16
|
/**
|
|
10
17
|
* Remove Token Pilot hook from Claude Code settings.
|
|
11
18
|
*/
|
|
12
|
-
export declare function uninstallHook(projectRoot: string): Promise<
|
|
13
|
-
removed: boolean;
|
|
14
|
-
message: string;
|
|
15
|
-
}>;
|
|
19
|
+
export declare function uninstallHook(projectRoot: string): Promise<HookUninstallResult>;
|
|
16
20
|
//# sourceMappingURL=installer.d.ts.map
|
package/dist/hooks/installer.js
CHANGED
|
@@ -42,12 +42,20 @@ export async function installHook(projectRoot) {
|
|
|
42
42
|
}
|
|
43
43
|
catch {
|
|
44
44
|
// File exists but has invalid JSON — don't destroy it
|
|
45
|
-
return {
|
|
45
|
+
return {
|
|
46
|
+
installed: false,
|
|
47
|
+
fatal: true,
|
|
48
|
+
message: `Settings file exists but contains invalid JSON: ${settingsPath}. Fix it manually before installing hooks.`,
|
|
49
|
+
};
|
|
46
50
|
}
|
|
47
51
|
}
|
|
48
52
|
catch (err) {
|
|
49
53
|
if (err?.code !== 'ENOENT') {
|
|
50
|
-
return {
|
|
54
|
+
return {
|
|
55
|
+
installed: false,
|
|
56
|
+
fatal: true,
|
|
57
|
+
message: `Cannot read settings: ${err?.message ?? err}`,
|
|
58
|
+
};
|
|
51
59
|
}
|
|
52
60
|
// ENOENT — file doesn't exist, start fresh
|
|
53
61
|
}
|
|
@@ -58,7 +66,7 @@ export async function installHook(projectRoot) {
|
|
|
58
66
|
const hasRead = existingHooks.some((h) => h.matcher === 'Read' && isTokenPilotHook(h));
|
|
59
67
|
const hasEdit = existingHooks.some((h) => h.matcher === 'Edit' && isTokenPilotHook(h));
|
|
60
68
|
if (hasRead && hasEdit) {
|
|
61
|
-
return { installed: false, message: 'Token Pilot hooks already installed.' };
|
|
69
|
+
return { installed: false, fatal: false, message: 'Token Pilot hooks already installed.' };
|
|
62
70
|
}
|
|
63
71
|
// Add missing hooks
|
|
64
72
|
for (const hookDef of HOOK_CONFIG.hooks.PreToolUse) {
|
|
@@ -77,12 +85,13 @@ export async function installHook(projectRoot) {
|
|
|
77
85
|
await writeFile(settingsPath, JSON.stringify(settings, null, 2) + '\n');
|
|
78
86
|
return {
|
|
79
87
|
installed: true,
|
|
88
|
+
fatal: false,
|
|
80
89
|
message: `Hooks installed at ${settingsPath}. Token Pilot will block unbounded Read on large code files and suggest read_for_edit before Edit.`,
|
|
81
90
|
};
|
|
82
91
|
}
|
|
83
92
|
catch (err) {
|
|
84
93
|
const msg = err instanceof Error ? err.message : String(err);
|
|
85
|
-
return { installed: false, message: `Failed to install hook: ${msg}` };
|
|
94
|
+
return { installed: false, fatal: true, message: `Failed to install hook: ${msg}` };
|
|
86
95
|
}
|
|
87
96
|
}
|
|
88
97
|
/**
|
|
@@ -94,7 +103,7 @@ export async function uninstallHook(projectRoot) {
|
|
|
94
103
|
const raw = await readFile(settingsPath, 'utf-8');
|
|
95
104
|
const settings = JSON.parse(raw);
|
|
96
105
|
if (!settings.hooks?.PreToolUse) {
|
|
97
|
-
return { removed: false, message: 'No hooks to remove.' };
|
|
106
|
+
return { removed: false, fatal: false, message: 'No hooks to remove.' };
|
|
98
107
|
}
|
|
99
108
|
settings.hooks.PreToolUse = settings.hooks.PreToolUse.filter((h) => !h.hooks?.some((hook) => hook.command?.includes('token-pilot')));
|
|
100
109
|
if (settings.hooks.PreToolUse.length === 0) {
|
|
@@ -104,13 +113,20 @@ export async function uninstallHook(projectRoot) {
|
|
|
104
113
|
delete settings.hooks;
|
|
105
114
|
}
|
|
106
115
|
await writeFile(settingsPath, JSON.stringify(settings, null, 2) + '\n');
|
|
107
|
-
return { removed: true, message: 'Token Pilot hook removed.' };
|
|
116
|
+
return { removed: true, fatal: false, message: 'Token Pilot hook removed.' };
|
|
108
117
|
}
|
|
109
118
|
catch (err) {
|
|
110
119
|
if (err?.code === 'ENOENT') {
|
|
111
|
-
return { removed: false, message: 'Settings file not found.' };
|
|
120
|
+
return { removed: false, fatal: false, message: 'Settings file not found.' };
|
|
112
121
|
}
|
|
113
|
-
|
|
122
|
+
if (err instanceof SyntaxError) {
|
|
123
|
+
return {
|
|
124
|
+
removed: false,
|
|
125
|
+
fatal: true,
|
|
126
|
+
message: `Settings file contains invalid JSON: ${settingsPath}. Fix it manually before uninstalling hooks.`,
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
return { removed: false, fatal: true, message: `Failed to process settings: ${err?.message ?? err}` };
|
|
114
130
|
}
|
|
115
131
|
}
|
|
116
132
|
//# sourceMappingURL=installer.js.map
|
package/dist/index.d.ts
CHANGED
|
@@ -1,3 +1,18 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
export
|
|
2
|
+
export declare const CODE_EXTENSIONS: Set<string>;
|
|
3
|
+
export declare function getVersion(): string;
|
|
4
|
+
export declare function main(cliArgs?: string[]): Promise<void>;
|
|
5
|
+
export declare function startServer(cliArgs?: string[]): Promise<void>;
|
|
6
|
+
export declare function handleHookRead(filePathArg?: string): void;
|
|
7
|
+
export declare function handleHookEdit(): void;
|
|
8
|
+
export declare function handleInstallHook(projectRoot: string): Promise<void>;
|
|
9
|
+
export declare function handleUninstallHook(projectRoot: string): Promise<void>;
|
|
10
|
+
export declare function handleInstallAstIndex(): Promise<void>;
|
|
11
|
+
export declare function handleDoctor(): Promise<void>;
|
|
12
|
+
export declare function handleInit(targetDir: string): Promise<void>;
|
|
13
|
+
export declare function checkNpmLatest(packageName: string): Promise<string | null>;
|
|
14
|
+
import type { TokenPilotConfig } from './types.js';
|
|
15
|
+
import type { BinaryStatus } from './ast-index/binary-manager.js';
|
|
16
|
+
export declare function checkAllUpdates(config: TokenPilotConfig, binaryStatus: BinaryStatus): Promise<void>;
|
|
17
|
+
export declare function printHelp(): void;
|
|
3
18
|
//# sourceMappingURL=index.d.ts.map
|