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
@@ -0,0 +1,162 @@
1
+ import { createHash } from 'node:crypto';
2
+ export class SessionCache {
3
+ maxEntries;
4
+ entries = new Map();
5
+ /** Reverse index: file path → set of cache keys that depend on it */
6
+ fileDepsIndex = new Map();
7
+ hits = 0;
8
+ misses = 0;
9
+ invalidations = 0;
10
+ constructor(maxEntries) {
11
+ this.maxEntries = maxEntries;
12
+ }
13
+ /**
14
+ * Generate deterministic cache key from tool name + args.
15
+ * Sorts args keys for consistency regardless of insertion order.
16
+ */
17
+ static makeCacheKey(tool, args) {
18
+ const sorted = JSON.stringify(args, Object.keys(args).sort());
19
+ const hash = createHash('sha256').update(sorted).digest('hex').slice(0, 16);
20
+ return `${tool}:${hash}`;
21
+ }
22
+ /** Try to get a cached result. Returns null on miss. */
23
+ get(tool, args) {
24
+ const key = SessionCache.makeCacheKey(tool, args);
25
+ const entry = this.entries.get(key);
26
+ if (entry) {
27
+ this.hits++;
28
+ return entry;
29
+ }
30
+ this.misses++;
31
+ return null;
32
+ }
33
+ /** Store a result with its dependency metadata. */
34
+ set(tool, args, result, deps, tokenEstimate, tokensWouldBe) {
35
+ // LRU eviction if full
36
+ if (this.entries.size >= this.maxEntries) {
37
+ this.evictOldest();
38
+ }
39
+ const key = SessionCache.makeCacheKey(tool, args);
40
+ const fileDeps = new Set(deps.files ?? []);
41
+ const entry = {
42
+ result,
43
+ fileDeps,
44
+ dependsOnAst: deps.dependsOnAst ?? false,
45
+ dependsOnGit: deps.dependsOnGit ?? false,
46
+ cachedAt: Date.now(),
47
+ tokenEstimate,
48
+ tokensWouldBe,
49
+ };
50
+ this.entries.set(key, entry);
51
+ // Update reverse index
52
+ for (const dep of fileDeps) {
53
+ let keys = this.fileDepsIndex.get(dep);
54
+ if (!keys) {
55
+ keys = new Set();
56
+ this.fileDepsIndex.set(dep, keys);
57
+ }
58
+ keys.add(key);
59
+ }
60
+ }
61
+ /**
62
+ * Invalidate all entries that depend on any of the given files.
63
+ * Checks both exact path match and directory prefix match.
64
+ */
65
+ invalidateByFiles(filePaths) {
66
+ let count = 0;
67
+ const keysToDelete = new Set();
68
+ for (const changedFile of filePaths) {
69
+ // Exact match from reverse index
70
+ const exactKeys = this.fileDepsIndex.get(changedFile);
71
+ if (exactKeys) {
72
+ for (const key of exactKeys)
73
+ keysToDelete.add(key);
74
+ }
75
+ // Directory prefix match: check if changedFile is under any cached dir dep
76
+ for (const [dep, keys] of this.fileDepsIndex) {
77
+ if (dep.endsWith('/') && changedFile.startsWith(dep)) {
78
+ for (const key of keys)
79
+ keysToDelete.add(key);
80
+ }
81
+ }
82
+ }
83
+ for (const key of keysToDelete) {
84
+ this.deleteEntry(key);
85
+ count++;
86
+ }
87
+ this.invalidations += count;
88
+ return count;
89
+ }
90
+ /** Invalidate all entries that depend on AST index state. */
91
+ invalidateByAst() {
92
+ let count = 0;
93
+ for (const [key, entry] of this.entries) {
94
+ if (entry.dependsOnAst) {
95
+ this.deleteEntry(key);
96
+ count++;
97
+ }
98
+ }
99
+ this.invalidations += count;
100
+ return count;
101
+ }
102
+ /** Invalidate all entries that depend on git state. */
103
+ invalidateByGit() {
104
+ let count = 0;
105
+ for (const [key, entry] of this.entries) {
106
+ if (entry.dependsOnGit) {
107
+ this.deleteEntry(key);
108
+ count++;
109
+ }
110
+ }
111
+ this.invalidations += count;
112
+ return count;
113
+ }
114
+ /** Clear all entries. */
115
+ invalidateAll() {
116
+ const count = this.entries.size;
117
+ this.entries.clear();
118
+ this.fileDepsIndex.clear();
119
+ this.invalidations += count;
120
+ }
121
+ /** Cache statistics for analytics. */
122
+ stats() {
123
+ const total = this.hits + this.misses;
124
+ return {
125
+ entries: this.entries.size,
126
+ hits: this.hits,
127
+ misses: this.misses,
128
+ hitRate: total > 0 ? Math.round((this.hits / total) * 100) : 0,
129
+ invalidations: this.invalidations,
130
+ };
131
+ }
132
+ // --- Private helpers ---
133
+ deleteEntry(key) {
134
+ const entry = this.entries.get(key);
135
+ if (!entry)
136
+ return;
137
+ // Clean up reverse index
138
+ for (const dep of entry.fileDeps) {
139
+ const keys = this.fileDepsIndex.get(dep);
140
+ if (keys) {
141
+ keys.delete(key);
142
+ if (keys.size === 0)
143
+ this.fileDepsIndex.delete(dep);
144
+ }
145
+ }
146
+ this.entries.delete(key);
147
+ }
148
+ evictOldest() {
149
+ let oldestKey = null;
150
+ let oldestTime = Infinity;
151
+ for (const [key, entry] of this.entries) {
152
+ if (entry.cachedAt < oldestTime) {
153
+ oldestTime = entry.cachedAt;
154
+ oldestKey = key;
155
+ }
156
+ }
157
+ if (oldestKey) {
158
+ this.deleteEntry(oldestKey);
159
+ }
160
+ }
161
+ }
162
+ //# sourceMappingURL=session-cache.js.map
@@ -64,6 +64,9 @@ export declare function validateReadForEditArgs(args: unknown): {
64
64
  symbol?: string;
65
65
  line?: number;
66
66
  context?: number;
67
+ include_callers?: boolean;
68
+ include_tests?: boolean;
69
+ include_changes?: boolean;
67
70
  };
68
71
  /**
69
72
  * Validate related_files arguments.
@@ -184,6 +184,9 @@ export function validateReadForEditArgs(args) {
184
184
  symbol: optionalString(a.symbol, 'symbol'),
185
185
  line: optionalNumber(a.line, 'line'),
186
186
  context: optionalNumber(a.context, 'context'),
187
+ include_callers: optionalBool(a.include_callers, 'include_callers'),
188
+ include_tests: optionalBool(a.include_tests, 'include_tests'),
189
+ include_changes: optionalBool(a.include_changes, 'include_changes'),
187
190
  };
188
191
  }
189
192
  /**
@@ -18,10 +18,16 @@ export declare class FileWatcher {
18
18
  private watcher;
19
19
  private watchedFiles;
20
20
  private updateTimer;
21
+ private fileChangeCallback;
22
+ private astUpdateCallback;
21
23
  constructor(_projectRoot: string, fileCache: FileCache, contextRegistry: ContextRegistry, _ignore: string[], astIndex?: AstIndexClient);
22
24
  start(): void;
23
25
  /** Debounced ast-index incremental update after file changes */
24
26
  private scheduleIndexUpdate;
27
+ /** Register callback for file change/unlink events. */
28
+ onFileChange(callback: (absPath: string) => void): void;
29
+ /** Register callback for after AST index incremental update completes. */
30
+ onAstUpdate(callback: () => void): void;
25
31
  /** Add a specific file to watch. Called after smart_read/read_symbol loads a file. */
26
32
  watchFile(filePath: string): void;
27
33
  stop(): void;
@@ -17,6 +17,8 @@ export class FileWatcher {
17
17
  watcher = null;
18
18
  watchedFiles = new Set();
19
19
  updateTimer = null;
20
+ fileChangeCallback = null;
21
+ astUpdateCallback = null;
20
22
  constructor(_projectRoot, fileCache, contextRegistry, _ignore, astIndex) {
21
23
  this.fileCache = fileCache;
22
24
  this.contextRegistry = contextRegistry;
@@ -37,6 +39,7 @@ export class FileWatcher {
37
39
  if (this.fileCache.get(absPath)) {
38
40
  this.fileCache.invalidate(absPath);
39
41
  }
42
+ this.fileChangeCallback?.(absPath);
40
43
  this.scheduleIndexUpdate();
41
44
  });
42
45
  this.watcher.on('unlink', (filePath) => {
@@ -44,6 +47,7 @@ export class FileWatcher {
44
47
  this.fileCache.invalidate(absPath);
45
48
  this.contextRegistry.forget(absPath);
46
49
  this.watchedFiles.delete(absPath);
50
+ this.fileChangeCallback?.(absPath);
47
51
  this.scheduleIndexUpdate();
48
52
  });
49
53
  }
@@ -53,10 +57,22 @@ export class FileWatcher {
53
57
  return;
54
58
  if (this.updateTimer)
55
59
  clearTimeout(this.updateTimer);
56
- this.updateTimer = setTimeout(() => {
57
- this.astIndex?.incrementalUpdate().catch(() => { });
60
+ this.updateTimer = setTimeout(async () => {
61
+ try {
62
+ await this.astIndex?.incrementalUpdate();
63
+ this.astUpdateCallback?.();
64
+ }
65
+ catch { /* ignore */ }
58
66
  }, FileWatcher.UPDATE_DEBOUNCE_MS);
59
67
  }
68
+ /** Register callback for file change/unlink events. */
69
+ onFileChange(callback) {
70
+ this.fileChangeCallback = callback;
71
+ }
72
+ /** Register callback for after AST index incremental update completes. */
73
+ onAstUpdate(callback) {
74
+ this.astUpdateCallback = callback;
75
+ }
60
76
  /** Add a specific file to watch. Called after smart_read/read_symbol loads a file. */
61
77
  watchFile(filePath) {
62
78
  const absPath = resolve(filePath);
@@ -7,6 +7,7 @@ export declare class GitWatcher {
7
7
  private watcher;
8
8
  private headRef;
9
9
  private enabled;
10
+ private branchSwitchCallback;
10
11
  constructor(projectRoot: string, fileCache: FileCache, contextRegistry: ContextRegistry, enabled: boolean);
11
12
  start(): Promise<void>;
12
13
  stop(): void;
@@ -19,6 +20,8 @@ export declare class GitWatcher {
19
20
  * Get files changed in the last N commits.
20
21
  */
21
22
  getRecentlyChangedFiles(commits?: number): Promise<string[]>;
23
+ /** Register callback for branch switch events. */
24
+ onBranchSwitchEvent(callback: (changedFiles: string[]) => void): void;
22
25
  private onBranchSwitch;
23
26
  private readHead;
24
27
  }
@@ -11,6 +11,7 @@ export class GitWatcher {
11
11
  watcher = null;
12
12
  headRef = '';
13
13
  enabled;
14
+ branchSwitchCallback = null;
14
15
  constructor(projectRoot, fileCache, contextRegistry, enabled) {
15
16
  this.projectRoot = projectRoot;
16
17
  this.fileCache = fileCache;
@@ -78,6 +79,10 @@ export class GitWatcher {
78
79
  return [];
79
80
  }
80
81
  }
82
+ /** Register callback for branch switch events. */
83
+ onBranchSwitchEvent(callback) {
84
+ this.branchSwitchCallback = callback;
85
+ }
81
86
  async onBranchSwitch() {
82
87
  // On branch switch, get files that differ between old and new branch
83
88
  // and selectively invalidate only those
@@ -85,6 +90,7 @@ export class GitWatcher {
85
90
  if (changed.length > 0) {
86
91
  await this.fileCache.invalidateByGitDiff(changed);
87
92
  this.contextRegistry.invalidateByGitDiff(changed);
93
+ this.branchSwitchCallback?.(changed);
88
94
  }
89
95
  }
90
96
  async readHead() {
@@ -1,9 +1,14 @@
1
1
  import type { AstIndexClient } from '../ast-index/client.js';
2
2
  import type { CodeAuditArgs } from '../core/validation.js';
3
- export declare function handleCodeAudit(args: CodeAuditArgs, projectRoot: string, astIndex: AstIndexClient): Promise<{
3
+ type AuditResult = {
4
4
  content: Array<{
5
5
  type: 'text';
6
6
  text: string;
7
7
  }>;
8
- }>;
8
+ meta: {
9
+ files: string[];
10
+ };
11
+ };
12
+ export declare function handleCodeAudit(args: CodeAuditArgs, projectRoot: string, astIndex: AstIndexClient): Promise<AuditResult>;
13
+ export {};
9
14
  //# sourceMappingURL=code-audit.d.ts.map
@@ -6,6 +6,7 @@ export async function handleCodeAudit(args, projectRoot, astIndex) {
6
6
  type: 'text',
7
7
  text: 'ast-index is not available (project root too broad or index oversized). Use Grep/ripgrep for pattern search.',
8
8
  }],
9
+ meta: { files: [] },
9
10
  };
10
11
  }
11
12
  const limit = args.limit ?? 50;
@@ -27,6 +28,7 @@ export async function handleCodeAudit(args, projectRoot, astIndex) {
27
28
  type: 'text',
28
29
  text: `Unknown check type: "${args.check}". Use: pattern, todo, deprecated, annotations, all`,
29
30
  }],
31
+ meta: { files: [] },
30
32
  };
31
33
  }
32
34
  }
@@ -42,6 +44,7 @@ async function handlePattern(pattern, lang, limit, projectRoot, astIndex) {
42
44
  type: 'text',
43
45
  text: `PATTERN SEARCH: "${pattern}"${lang ? ` (${lang})` : ''}\n\nNo matches found.\n\nHINT: Try Grep/ripgrep for text-based search if the pattern is not structural.`,
44
46
  }],
47
+ meta: { files: [] },
45
48
  };
46
49
  }
47
50
  // Group by file
@@ -64,7 +67,7 @@ async function handlePattern(pattern, lang, limit, projectRoot, astIndex) {
64
67
  lines.push('');
65
68
  }
66
69
  lines.push('HINT: Use read_symbol() to inspect specific matches, or Grep for text-based counting.');
67
- return { content: [{ type: 'text', text: lines.join('\n') }] };
70
+ return { content: [{ type: 'text', text: lines.join('\n') }], meta: { files: [...byFile.keys()] } };
68
71
  }
69
72
  catch (err) {
70
73
  // ast-grep not installed — return the error message
@@ -73,6 +76,7 @@ async function handlePattern(pattern, lang, limit, projectRoot, astIndex) {
73
76
  type: 'text',
74
77
  text: `PATTERN SEARCH ERROR:\n${err instanceof Error ? err.message : String(err)}\n\nFallback: Use Grep/ripgrep for text-based pattern search.`,
75
78
  }],
79
+ meta: { files: [] },
76
80
  };
77
81
  }
78
82
  }
@@ -85,6 +89,7 @@ async function handleTodo(limit, projectRoot, astIndex) {
85
89
  type: 'text',
86
90
  text: 'TODO/FIXME COMMENTS: none found.\n\nHINT: ast-index may not detect all comment formats. Try Grep: grep -rn "TODO\\|FIXME\\|HACK" --include="*.ts"',
87
91
  }],
92
+ meta: { files: [] },
88
93
  };
89
94
  }
90
95
  // Group by kind
@@ -106,7 +111,8 @@ async function handleTodo(limit, projectRoot, astIndex) {
106
111
  }
107
112
  lines.push('');
108
113
  }
109
- return { content: [{ type: 'text', text: lines.join('\n') }] };
114
+ const todoFiles = [...new Set(limited.map(e => e.file))];
115
+ return { content: [{ type: 'text', text: lines.join('\n') }], meta: { files: todoFiles } };
110
116
  }
111
117
  async function handleDeprecated(limit, projectRoot, astIndex) {
112
118
  const entries = await astIndex.deprecated();
@@ -117,6 +123,7 @@ async function handleDeprecated(limit, projectRoot, astIndex) {
117
123
  type: 'text',
118
124
  text: 'DEPRECATED SYMBOLS: none found.\n\nHINT: ast-index detects @Deprecated annotations. Try Grep for other deprecation patterns.',
119
125
  }],
126
+ meta: { files: [] },
120
127
  };
121
128
  }
122
129
  const lines = [
@@ -129,7 +136,8 @@ async function handleDeprecated(limit, projectRoot, astIndex) {
129
136
  }
130
137
  lines.push('');
131
138
  lines.push('HINT: Use read_symbol() to inspect deprecated symbols before removing them.');
132
- return { content: [{ type: 'text', text: lines.join('\n') }] };
139
+ const depFiles = [...new Set(limited.map(e => e.file))];
140
+ return { content: [{ type: 'text', text: lines.join('\n') }], meta: { files: depFiles } };
133
141
  }
134
142
  async function handleAnnotations(name, limit, projectRoot, astIndex) {
135
143
  const entries = await astIndex.annotations(name);
@@ -140,6 +148,7 @@ async function handleAnnotations(name, limit, projectRoot, astIndex) {
140
148
  type: 'text',
141
149
  text: `ANNOTATIONS @${name}: none found.\n\nHINT: Try Grep for text-based search: grep -rn "@${name}" --include="*.ts"`,
142
150
  }],
151
+ meta: { files: [] },
143
152
  };
144
153
  }
145
154
  // Group by file
@@ -161,7 +170,8 @@ async function handleAnnotations(name, limit, projectRoot, astIndex) {
161
170
  }
162
171
  lines.push('');
163
172
  }
164
- return { content: [{ type: 'text', text: lines.join('\n') }] };
173
+ const annFiles = [...new Set(limited.map(e => e.file))];
174
+ return { content: [{ type: 'text', text: lines.join('\n') }], meta: { files: annFiles } };
165
175
  }
166
176
  async function handleAll(limit, projectRoot, astIndex) {
167
177
  // Run todo + deprecated in parallel
@@ -196,6 +206,10 @@ async function handleAll(limit, projectRoot, astIndex) {
196
206
  sections.push('');
197
207
  sections.push('HINT: Use code_audit(check="pattern", pattern="...") for structural pattern search (requires ast-grep).');
198
208
  sections.push(' Use Grep for text-based counting and regex search.');
199
- return { content: [{ type: 'text', text: sections.join('\n') }] };
209
+ const allFiles = [...new Set([
210
+ ...todos.slice(0, limit).map(e => e.file),
211
+ ...deprecated.slice(0, limit).map(e => e.file),
212
+ ])];
213
+ return { content: [{ type: 'text', text: sections.join('\n') }], meta: { files: allFiles } };
200
214
  }
201
215
  //# sourceMappingURL=code-audit.js.map
@@ -1,9 +1,19 @@
1
1
  import type { AstIndexClient } from '../ast-index/client.js';
2
2
  import type { ExploreAreaArgs } from '../core/validation.js';
3
+ export interface ExploreAreaMeta {
4
+ dir: string;
5
+ codeFiles: string[];
6
+ testFiles: string[];
7
+ internalDeps: string[];
8
+ importedBy: string[];
9
+ externalDeps: string[];
10
+ changeCount: number;
11
+ }
3
12
  export declare function handleExploreArea(args: ExploreAreaArgs, projectRoot: string, astIndex: AstIndexClient): Promise<{
4
13
  content: Array<{
5
14
  type: 'text';
6
15
  text: string;
7
16
  }>;
17
+ meta: ExploreAreaMeta;
8
18
  }>;
9
19
  //# sourceMappingURL=explore-area.d.ts.map
@@ -20,6 +20,15 @@ export async function handleExploreArea(args, projectRoot, astIndex) {
20
20
  if (!pathStat) {
21
21
  return {
22
22
  content: [{ type: 'text', text: `Path "${args.path}" not found.` }],
23
+ meta: {
24
+ dir: args.path,
25
+ codeFiles: [],
26
+ testFiles: [],
27
+ internalDeps: [],
28
+ importedBy: [],
29
+ externalDeps: [],
30
+ changeCount: 0,
31
+ },
23
32
  };
24
33
  }
25
34
  if (!pathStat.isDirectory()) {
@@ -49,17 +58,17 @@ export async function handleExploreArea(args, projectRoot, astIndex) {
49
58
  lines.push('');
50
59
  }
51
60
  // Imports
52
- const importLines = extractResult(importsSection);
61
+ const importLines = extractResult(importsSection)?.lines ?? null;
53
62
  if (importLines) {
54
63
  lines.push(...importLines);
55
64
  }
56
65
  // Tests
57
- const testLines = extractResult(testsSection);
66
+ const testLines = extractResult(testsSection)?.lines ?? null;
58
67
  if (testLines) {
59
68
  lines.push(...testLines);
60
69
  }
61
70
  // Changes
62
- const changeLines = extractResult(changesSection);
71
+ const changeLines = extractResult(changesSection)?.lines ?? null;
63
72
  if (changeLines) {
64
73
  lines.push(...changeLines);
65
74
  }
@@ -69,7 +78,18 @@ export async function handleExploreArea(args, projectRoot, astIndex) {
69
78
  lines.push('... truncated. Use outline() on specific subdirectories for details.');
70
79
  }
71
80
  lines.push('HINT: Use smart_read(file) for details, read_symbol(path, symbol) for source code, find_usages(symbol) for references.');
72
- return { content: [{ type: 'text', text: lines.join('\n') }] };
81
+ return {
82
+ content: [{ type: 'text', text: lines.join('\n') }],
83
+ meta: {
84
+ dir: relDir,
85
+ codeFiles: codeFiles.map((file) => relative(projectRoot, file)).sort(),
86
+ testFiles: extractResult(testsSection)?.testFiles ?? [],
87
+ internalDeps: extractResult(importsSection)?.internalDeps ?? [],
88
+ importedBy: extractResult(importsSection)?.importedBy ?? [],
89
+ externalDeps: extractResult(importsSection)?.externalDeps ?? [],
90
+ changeCount: extractResult(changesSection)?.count ?? 0,
91
+ },
92
+ };
73
93
  }
74
94
  // ──────────────────────────────────────────────
75
95
  // Outline section — reuses outlineDir from outline.ts
@@ -84,7 +104,7 @@ async function buildOutlineSection(absPath, projectRoot, astIndex) {
84
104
  // ──────────────────────────────────────────────
85
105
  async function buildImportsSection(codeFiles, absPath, projectRoot, astIndex) {
86
106
  if (!astIndex.isAvailable() || astIndex.isDisabled() || astIndex.isOversized()) {
87
- return [];
107
+ return { lines: [], internalDeps: [], importedBy: [], externalDeps: [] };
88
108
  }
89
109
  const filesToAnalyze = codeFiles.slice(0, MAX_IMPORT_FILES);
90
110
  const externalDeps = new Set();
@@ -150,7 +170,12 @@ async function buildImportsSection(codeFiles, absPath, projectRoot, astIndex) {
150
170
  }
151
171
  if (lines.length > 0)
152
172
  lines.push('');
153
- return lines;
173
+ return {
174
+ lines,
175
+ internalDeps: Array.from(internalDeps).sort(),
176
+ importedBy: Array.from(importedBy).sort(),
177
+ externalDeps: Array.from(externalDeps).sort(),
178
+ };
154
179
  }
155
180
  // ──────────────────────────────────────────────
156
181
  // Tests section — find test/spec files matching area files
@@ -215,11 +240,11 @@ async function buildTestsSection(codeFiles, absPath, projectRoot) {
215
240
  catch { /* skip unreadable dirs */ }
216
241
  }
217
242
  if (testFiles.length === 0)
218
- return [];
243
+ return { lines: [], testFiles: [] };
219
244
  const lines = [];
220
245
  lines.push(`TESTS: ${testFiles.join(', ')}`);
221
246
  lines.push('');
222
- return lines;
247
+ return { lines, testFiles: [...testFiles].sort() };
223
248
  }
224
249
  // ──────────────────────────────────────────────
225
250
  // Changes section — recent git log for this area
@@ -228,24 +253,25 @@ async function buildChangesSection(relDir, projectRoot) {
228
253
  try {
229
254
  const { stdout } = await execFileAsync('git', ['log', '--oneline', '-5', '--', relDir], { cwd: projectRoot, timeout: 5000 });
230
255
  if (!stdout.trim())
231
- return [];
256
+ return { lines: [], count: 0 };
232
257
  const lines = [];
258
+ const commits = stdout.trim().split('\n');
233
259
  lines.push('RECENT CHANGES:');
234
- for (const line of stdout.trim().split('\n')) {
260
+ for (const line of commits) {
235
261
  lines.push(` ${line}`);
236
262
  }
237
263
  lines.push('');
238
- return lines;
264
+ return { lines, count: commits.length };
239
265
  }
240
266
  catch {
241
- return [];
267
+ return { lines: [], count: 0 };
242
268
  }
243
269
  }
244
270
  // ──────────────────────────────────────────────
245
271
  // Helpers
246
272
  // ──────────────────────────────────────────────
247
273
  function extractResult(settled) {
248
- if (settled.status === 'fulfilled' && settled.value && settled.value.length > 0) {
274
+ if (settled.status === 'fulfilled' && settled.value) {
249
275
  return settled.value;
250
276
  }
251
277
  return null;
@@ -9,5 +9,8 @@ export declare function handleFindUnused(args: FindUnusedArgs, astIndex: AstInde
9
9
  type: 'text';
10
10
  text: string;
11
11
  }>;
12
+ meta: {
13
+ files: string[];
14
+ };
12
15
  }>;
13
16
  //# sourceMappingURL=find-unused.d.ts.map
@@ -18,7 +18,7 @@ export async function handleFindUnused(args, astIndex) {
18
18
  return { content: [{ type: 'text', text: 'find_unused is disabled: ' + (astIndex.isDisabled()
19
19
  ? 'project root not detected. Call smart_read() on any project file first — this auto-detects the project root and enables ast-index tools.'
20
20
  : 'ast-index built >50k files (likely includes node_modules). Ensure node_modules is in .gitignore.') +
21
- '\nAlternative: use grep_search to find unused exports manually.' }] };
21
+ '\nAlternative: use grep_search to find unused exports manually.' }], meta: { files: [] } };
22
22
  }
23
23
  const requestLimit = (args.limit ?? 30) + 20; // extra to compensate for filtering
24
24
  const unused = await astIndex.unusedSymbols({
@@ -67,6 +67,7 @@ export async function handleFindUnused(args, astIndex) {
67
67
  ? `No unused symbols found in module "${args.module}".${excluded > 0 ? ` (${excluded} constructors/protocol methods excluded)` : ''}`
68
68
  : `No unused symbols found in the project.${excluded > 0 ? ` (${excluded} constructors/protocol methods excluded)` : ''}`,
69
69
  }],
70
+ meta: { files: [] },
70
71
  };
71
72
  }
72
73
  const lines = [];
@@ -100,7 +101,7 @@ export async function handleFindUnused(args, astIndex) {
100
101
  lines.push(`(${langExcluded} constructors/protocol methods excluded)`);
101
102
  }
102
103
  lines.push('NOTE: Verify before removing — symbols may be used dynamically, in tests, or via framework conventions.');
103
- return { content: [{ type: 'text', text: lines.join('\n') }] };
104
+ return { content: [{ type: 'text', text: lines.join('\n') }], meta: { files: uniqueFiles } };
104
105
  }
105
106
  /**
106
107
  * Find decorators for a symbol by matching name + line in the outline tree.
@@ -15,5 +15,12 @@ export declare function handleFindUsages(args: FindUsagesArgs, astIndex: AstInde
15
15
  type: 'text';
16
16
  text: string;
17
17
  }>;
18
+ meta: {
19
+ files: string[];
20
+ definitions: number;
21
+ imports: number;
22
+ usages: number;
23
+ total: number;
24
+ };
18
25
  }>;
19
26
  //# sourceMappingURL=find-usages.d.ts.map