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.
Files changed (69) hide show
  1. package/.claude-plugin/hooks/hooks.json +9 -0
  2. package/.claude-plugin/marketplace.json +1 -1
  3. package/.claude-plugin/plugin.json +1 -1
  4. package/CHANGELOG.md +29 -0
  5. package/README.md +36 -15
  6. package/dist/config/defaults.js +12 -0
  7. package/dist/core/architecture-fingerprint.d.ts +34 -0
  8. package/dist/core/architecture-fingerprint.js +127 -0
  9. package/dist/core/budget-planner.d.ts +21 -0
  10. package/dist/core/budget-planner.js +68 -0
  11. package/dist/core/confidence.d.ts +31 -0
  12. package/dist/core/confidence.js +99 -0
  13. package/dist/core/context-registry.d.ts +14 -0
  14. package/dist/core/context-registry.js +55 -0
  15. package/dist/core/decision-trace.d.ts +31 -0
  16. package/dist/core/decision-trace.js +45 -0
  17. package/dist/core/intent-classifier.d.ts +13 -0
  18. package/dist/core/intent-classifier.js +44 -0
  19. package/dist/core/policy-engine.d.ts +41 -0
  20. package/dist/core/policy-engine.js +76 -0
  21. package/dist/core/session-analytics.d.ts +8 -0
  22. package/dist/core/session-analytics.js +86 -7
  23. package/dist/core/session-cache.d.ts +74 -0
  24. package/dist/core/session-cache.js +162 -0
  25. package/dist/core/validation.d.ts +3 -0
  26. package/dist/core/validation.js +3 -0
  27. package/dist/git/file-watcher.d.ts +6 -0
  28. package/dist/git/file-watcher.js +18 -2
  29. package/dist/git/watcher.d.ts +3 -0
  30. package/dist/git/watcher.js +6 -0
  31. package/dist/handlers/code-audit.d.ts +7 -2
  32. package/dist/handlers/code-audit.js +19 -5
  33. package/dist/handlers/explore-area.d.ts +10 -0
  34. package/dist/handlers/explore-area.js +39 -13
  35. package/dist/handlers/find-unused.d.ts +3 -0
  36. package/dist/handlers/find-unused.js +3 -2
  37. package/dist/handlers/find-usages.d.ts +7 -0
  38. package/dist/handlers/find-usages.js +36 -5
  39. package/dist/handlers/module-info.d.ts +3 -0
  40. package/dist/handlers/module-info.js +22 -2
  41. package/dist/handlers/project-overview.d.ts +1 -1
  42. package/dist/handlers/project-overview.js +18 -2
  43. package/dist/handlers/read-for-edit.d.ts +3 -0
  44. package/dist/handlers/read-for-edit.js +185 -3
  45. package/dist/handlers/read-range.d.ts +1 -1
  46. package/dist/handlers/read-range.js +16 -1
  47. package/dist/handlers/read-symbol.d.ts +1 -1
  48. package/dist/handlers/read-symbol.js +26 -2
  49. package/dist/handlers/related-files.d.ts +11 -0
  50. package/dist/handlers/related-files.js +178 -42
  51. package/dist/handlers/smart-read-many.js +70 -16
  52. package/dist/handlers/smart-read.js +10 -1
  53. package/dist/handlers/test-summary.js +26 -3
  54. package/dist/hooks/installer.d.ts +12 -8
  55. package/dist/hooks/installer.js +24 -8
  56. package/dist/index.d.ts +16 -1
  57. package/dist/index.js +62 -56
  58. package/dist/server.js +395 -30
  59. package/dist/types.d.ts +12 -0
  60. package/package.json +18 -14
  61. package/start.sh +28 -27
  62. package/dist/handlers/class-hierarchy.d.ts +0 -11
  63. package/dist/handlers/class-hierarchy.js +0 -28
  64. package/dist/handlers/export-ast-index.d.ts +0 -22
  65. package/dist/handlers/export-ast-index.js +0 -175
  66. package/dist/handlers/find-implementations.d.ts +0 -11
  67. package/dist/handlers/find-implementations.js +0 -27
  68. package/dist/handlers/search-code.d.ts +0 -14
  69. package/dist/handlers/search-code.js +0 -32
@@ -1,5 +1,9 @@
1
- import { basename, extname } from 'node:path';
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 { content: [{ type: 'text', text: 'related_files is disabled: ' + (astIndex.isDisabled()
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 sections = [`RELATED FILES: ${args.path}`, ''];
45
- // 1. Forward imports (what this file imports)
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 specStr = imp.specifiers?.length ? imp.specifiers.join(', ') : '*';
52
- sections.push(` → ${imp.source} (${specStr})`);
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
- // Filter cross-language false positives:
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
- importedBy.push(refPath);
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
- if (importedBy.length > 0) {
110
- sections.push('IMPORTED BY (uses this file):');
111
- for (const p of importedBy) {
112
- sections.push(` → ${p}`);
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
- testFiles.push(f);
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. Check if we found anything useful
145
- if (sections.length <= 2) {
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
- // Suggested reading order
151
- sections.push('HINT: Use smart_read_many(paths=[...]) to read related files at once.');
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 { content: [{ type: 'text', text: sections.join('\n') }] };
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 > 20) {
16
+ if (args.paths.length > MAX_BATCH_FILES) {
10
17
  return {
11
- content: [{ type: 'text', text: `Too many files (${args.paths.length}). Maximum is 20 per batch.` }],
18
+ content: [{ type: 'text', text: `Too many files (${args.paths.length}). Maximum is ${MAX_BATCH_FILES} per batch.` }],
12
19
  };
13
20
  }
14
- const results = [];
15
- let totalTokens = 0;
16
- for (const path of args.paths) {
17
- try {
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
- results.push(text);
21
- totalTokens += estimateTokens(text);
22
- }
23
- catch (err) {
24
- const msg = err instanceof Error ? err.message : String(err);
25
- results.push(`FILE: ${path}\nERROR: ${msg}`);
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
- results.push('');
29
- results.push(`BATCH: ${args.paths.length} files loaded (~${totalTokens} tokens total)`);
30
- return { content: [{ type: 'text', text: results.join('\n\n---\n\n') }] };
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
- return { content: [{ type: 'text', text: output + savings }] };
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 formatted = formatTestSummary(result, command, runner, rawTokens);
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
@@ -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 { installed: false, message: `Settings file exists but contains invalid JSON: ${settingsPath}. Fix it manually before installing hooks.` };
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 { installed: false, message: `Cannot read settings: ${err?.message ?? err}` };
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
- return { removed: false, message: `Failed to process settings: ${err?.message ?? err}` };
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