token-pilot 0.7.6 → 0.8.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/CHANGELOG.md CHANGED
@@ -5,6 +5,17 @@ All notable changes to Token Pilot will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [0.8.0] - 2026-03-07
9
+
10
+ ### Added
11
+ - **`code_audit` tool** — find code quality issues in one call: TODO/FIXME comments (`check="todo"`), deprecated symbols (`check="deprecated"`), structural code patterns via ast-grep (`check="pattern"`), decorator search (`check="annotations"`), or combined audit (`check="all"`).
12
+ - **Incremental index update on file changes** — file watcher now triggers `ast-index update` (debounced 2s) after edits. Keeps index fresh for find_usages, find_unused, code_audit.
13
+ - **ast-index client methods** — `agrep()`, `todo()`, `deprecated()`, `annotations()`, `incrementalUpdate()`.
14
+
15
+ ### Fixed
16
+ - **smart_read on directories** — now returns helpful message instead of EISDIR crash.
17
+ - **MCP instructions** — added "COMBINE BOTH" section for audit tasks (Token Pilot + Grep).
18
+
8
19
  ## [0.7.6] - 2026-03-07
9
20
 
10
21
  ### Added
package/README.md CHANGED
@@ -25,6 +25,8 @@ One command creates `.mcp.json` with token-pilot + context-mode:
25
25
  npx -y token-pilot init
26
26
  ```
27
27
 
28
+ Safe to run in any project — if `.mcp.json` already exists, only adds missing servers without overwriting existing config.
29
+
28
30
  This generates:
29
31
 
30
32
  ```json
@@ -53,7 +55,7 @@ Add to your `.mcp.json` (project-level or `~/.mcp.json` for global):
53
55
  }
54
56
  ```
55
57
 
56
- Works with: **Cursor**, **Cline**, **Continue**, **Codex**, **Antigravity**, and any MCP-compatible client.
58
+ Works with: **Claude Code**, **Cursor**, **Codex**, **Antigravity**, **Cline**, and any MCP-compatible client.
57
59
 
58
60
  #### Cursor
59
61
 
@@ -143,7 +145,7 @@ For more control, you can add rules to your project:
143
145
  - **Cursor** → `.cursorrules` in project root
144
146
  - **Codex** → `AGENTS.md` in project root
145
147
 
146
- ## MCP Tools (12)
148
+ ## MCP Tools (13)
147
149
 
148
150
  ### Core Reading
149
151
 
@@ -165,6 +167,7 @@ For more control, you can add rules to your project:
165
167
  | `related_files` | manual explore | Import graph: what a file imports, what imports it, test files. |
166
168
  | `outline` | multiple `smart_read` | Compact overview of all code files in a directory. One call instead of 5-6. |
167
169
  | `find_unused` | manual | Detect dead code — unused functions, classes, variables. |
170
+ | `code_audit` | multiple `Grep` | Code quality issues in one call: TODO/FIXME comments, deprecated symbols, structural code patterns (via ast-grep), decorator search. |
168
171
 
169
172
  ### Analytics
170
173
 
@@ -177,6 +180,7 @@ For more control, you can add rules to your project:
177
180
  ```bash
178
181
  token-pilot # Start MCP server (uses cwd as project root)
179
182
  token-pilot /path/to/project # Start with specific project root
183
+ token-pilot init [dir] # Create/update .mcp.json (token-pilot + context-mode)
180
184
  token-pilot install-ast-index # Download ast-index binary (auto on first run)
181
185
  token-pilot install-hook [root] # Install PreToolUse hook
182
186
  token-pilot uninstall-hook # Remove hook
@@ -193,7 +197,7 @@ Create `.token-pilot.json` in your project root to customize behavior:
193
197
  ```json
194
198
  {
195
199
  "smartRead": {
196
- "smallFileThreshold": 80,
200
+ "smallFileThreshold": 200,
197
201
  "advisoryReminders": true
198
202
  },
199
203
  "cache": {
@@ -251,7 +255,6 @@ When both are configured, Token Pilot automatically:
251
255
  - Detects context-mode via `.mcp.json`
252
256
  - Suggests context-mode for large non-code files
253
257
  - Shows combined architecture info in `session_analytics`
254
- - Provides `export_ast_index` to feed AST data into context-mode's BM25 index
255
258
 
256
259
  **Combined savings: 60-80%** in a typical coding session.
257
260
 
@@ -339,6 +342,7 @@ src/
339
342
  related-files.ts — related_files handler (import graph)
340
343
  outline.ts — outline handler (directory overview)
341
344
  find-unused.ts — find_unused handler
345
+ code-audit.ts — code_audit handler (TODOs, deprecated, patterns)
342
346
  project-overview.ts — project_overview (via ast-index map + conventions)
343
347
  non-code.ts — JSON/YAML/MD/TOML structural summaries
344
348
  export-ast-index.ts — AST export for context-mode BM25
@@ -1,5 +1,5 @@
1
1
  import type { FileStructure } from '../types.js';
2
- import type { AstIndexSymbolDetail, AstIndexSearchResult, AstIndexUsageResult, AstIndexImplementation, AstIndexHierarchyNode, AstIndexRefsResponse, AstIndexMapResponse, AstIndexConventionsResponse, AstIndexCallerEntry, AstIndexCallTreeNode, AstIndexChangedEntry, AstIndexUnusedSymbol, AstIndexImportEntry } from './types.js';
2
+ import type { AstIndexSymbolDetail, AstIndexSearchResult, AstIndexUsageResult, AstIndexImplementation, AstIndexHierarchyNode, AstIndexRefsResponse, AstIndexMapResponse, AstIndexConventionsResponse, AstIndexCallerEntry, AstIndexCallTreeNode, AstIndexChangedEntry, AstIndexUnusedSymbol, AstIndexImportEntry, AstIndexAgrepMatch, AstIndexTodoEntry, AstIndexDeprecatedEntry, AstIndexAnnotationEntry } from './types.js';
3
3
  export declare class AstIndexClient {
4
4
  private static readonly MAX_INDEX_FILES;
5
5
  private binaryPath;
@@ -11,6 +11,7 @@ export declare class AstIndexClient {
11
11
  private timeout;
12
12
  private configBinaryPath;
13
13
  private autoInstall;
14
+ private astGrepAvailable;
14
15
  constructor(projectRoot: string, timeout?: number, options?: {
15
16
  binaryPath?: string | null;
16
17
  autoInstall?: boolean;
@@ -92,6 +93,25 @@ export declare class AstIndexClient {
92
93
  */
93
94
  fileImports(filePath: string): Promise<AstIndexImportEntry[]>;
94
95
  private parseImportsText;
96
+ /** Check if ast-grep (sg) is available for structural pattern search */
97
+ private checkAstGrep;
98
+ /** Structural pattern search via ast-grep. Requires ast-grep (sg) installed. */
99
+ agrep(pattern: string, options?: {
100
+ lang?: string;
101
+ limit?: number;
102
+ }): Promise<AstIndexAgrepMatch[]>;
103
+ private parseAgrepText;
104
+ /** Find TODO/FIXME/HACK comments in the project */
105
+ todo(): Promise<AstIndexTodoEntry[]>;
106
+ private parseTodoText;
107
+ /** Find @Deprecated symbols in the project */
108
+ deprecated(): Promise<AstIndexDeprecatedEntry[]>;
109
+ private parseDeprecatedText;
110
+ /** Find symbols with a specific annotation/decorator */
111
+ annotations(name: string): Promise<AstIndexAnnotationEntry[]>;
112
+ private parseAnnotationsText;
113
+ /** Trigger incremental index update (called by file watcher after edits) */
114
+ incrementalUpdate(): Promise<void>;
95
115
  isAvailable(): boolean;
96
116
  /** Returns true if the index was built but found >50k files (node_modules leak) */
97
117
  isOversized(): boolean;
@@ -16,6 +16,7 @@ export class AstIndexClient {
16
16
  timeout;
17
17
  configBinaryPath;
18
18
  autoInstall;
19
+ astGrepAvailable = null;
19
20
  constructor(projectRoot, timeout = 5000, options) {
20
21
  this.projectRoot = projectRoot;
21
22
  this.timeout = timeout;
@@ -636,6 +637,172 @@ export class AstIndexClient {
636
637
  }
637
638
  return entries;
638
639
  }
640
+ // --- Code audit commands ---
641
+ /** Check if ast-grep (sg) is available for structural pattern search */
642
+ async checkAstGrep() {
643
+ if (this.astGrepAvailable !== null)
644
+ return this.astGrepAvailable;
645
+ try {
646
+ await execFileAsync('sg', ['--version'], { timeout: 3000 });
647
+ this.astGrepAvailable = true;
648
+ }
649
+ catch {
650
+ this.astGrepAvailable = false;
651
+ }
652
+ return this.astGrepAvailable;
653
+ }
654
+ /** Structural pattern search via ast-grep. Requires ast-grep (sg) installed. */
655
+ async agrep(pattern, options) {
656
+ if (this.indexDisabled || this.indexOversized)
657
+ return [];
658
+ await this.ensureIndex();
659
+ const available = await this.checkAstGrep();
660
+ if (!available) {
661
+ throw new Error('ast-grep (sg) not installed — required for structural pattern search.\n' +
662
+ 'Install: brew install ast-grep OR npm i -g @ast-grep/cli\n' +
663
+ 'Alternative: use Grep/ripgrep for text-based pattern search.');
664
+ }
665
+ const limit = options?.limit ?? 50;
666
+ const args = ['agrep', pattern];
667
+ if (options?.lang)
668
+ args.push('--lang', options.lang);
669
+ args.push('--limit', String(limit));
670
+ try {
671
+ const result = await this.exec(args, 15000);
672
+ return this.parseAgrepText(result).slice(0, limit);
673
+ }
674
+ catch (err) {
675
+ console.error(`[token-pilot] ast-index agrep failed: ${err instanceof Error ? err.message : err}`);
676
+ return [];
677
+ }
678
+ }
679
+ parseAgrepText(text) {
680
+ const results = [];
681
+ for (const line of text.split('\n')) {
682
+ if (!line.trim())
683
+ continue;
684
+ // Format: file:line:matched_text OR file:line: matched_text
685
+ const match = line.match(/^(.+?):(\d+):(.*)$/);
686
+ if (match) {
687
+ results.push({
688
+ file: match[1],
689
+ line: parseInt(match[2], 10),
690
+ text: match[3].trim(),
691
+ });
692
+ }
693
+ }
694
+ return results;
695
+ }
696
+ /** Find TODO/FIXME/HACK comments in the project */
697
+ async todo() {
698
+ if (this.indexDisabled || this.indexOversized)
699
+ return [];
700
+ await this.ensureIndex();
701
+ try {
702
+ const result = await this.exec(['todo'], 15000);
703
+ return this.parseTodoText(result);
704
+ }
705
+ catch (err) {
706
+ console.error(`[token-pilot] ast-index todo failed: ${err instanceof Error ? err.message : err}`);
707
+ return [];
708
+ }
709
+ }
710
+ parseTodoText(text) {
711
+ const results = [];
712
+ for (const line of text.split('\n')) {
713
+ if (!line.trim())
714
+ continue;
715
+ // Try format: file:line: KIND: message OR file:line: KIND message
716
+ const match = line.match(/^(.+?):(\d+):\s*(TODO|FIXME|HACK|XXX|NOTE|WARN(?:ING)?)[:\s]+(.*)$/i);
717
+ if (match) {
718
+ results.push({
719
+ file: match[1],
720
+ line: parseInt(match[2], 10),
721
+ kind: match[3].toUpperCase(),
722
+ text: match[4].trim(),
723
+ });
724
+ }
725
+ }
726
+ return results;
727
+ }
728
+ /** Find @Deprecated symbols in the project */
729
+ async deprecated() {
730
+ if (this.indexDisabled || this.indexOversized)
731
+ return [];
732
+ await this.ensureIndex();
733
+ try {
734
+ const result = await this.exec(['deprecated'], 15000);
735
+ return this.parseDeprecatedText(result);
736
+ }
737
+ catch (err) {
738
+ console.error(`[token-pilot] ast-index deprecated failed: ${err instanceof Error ? err.message : err}`);
739
+ return [];
740
+ }
741
+ }
742
+ parseDeprecatedText(text) {
743
+ const results = [];
744
+ for (const line of text.split('\n')) {
745
+ if (!line.trim())
746
+ continue;
747
+ // Try format: kind name (file:line) - message OR kind name (file:line)
748
+ const match = line.match(/^(\w+)\s+(\S+)\s+\((.+?):(\d+)\)(?:\s*-\s*(.+))?$/);
749
+ if (match) {
750
+ results.push({
751
+ kind: match[1],
752
+ name: match[2],
753
+ file: match[3],
754
+ line: parseInt(match[4], 10),
755
+ message: match[5]?.trim(),
756
+ });
757
+ }
758
+ }
759
+ return results;
760
+ }
761
+ /** Find symbols with a specific annotation/decorator */
762
+ async annotations(name) {
763
+ if (this.indexDisabled || this.indexOversized)
764
+ return [];
765
+ await this.ensureIndex();
766
+ try {
767
+ const result = await this.exec(['annotations', name], 15000);
768
+ return this.parseAnnotationsText(result, name);
769
+ }
770
+ catch (err) {
771
+ console.error(`[token-pilot] ast-index annotations failed: ${err instanceof Error ? err.message : err}`);
772
+ return [];
773
+ }
774
+ }
775
+ parseAnnotationsText(text, annotationName) {
776
+ const results = [];
777
+ for (const line of text.split('\n')) {
778
+ if (!line.trim())
779
+ continue;
780
+ // Try format: kind name (file:line) OR @Annotation kind name (file:line)
781
+ const match = line.match(/^(?:@\S+\s+)?(\w+)\s+(\S+)\s+\((.+?):(\d+)\)$/);
782
+ if (match) {
783
+ results.push({
784
+ kind: match[1],
785
+ name: match[2],
786
+ file: match[3],
787
+ line: parseInt(match[4], 10),
788
+ annotation: annotationName,
789
+ });
790
+ }
791
+ }
792
+ return results;
793
+ }
794
+ /** Trigger incremental index update (called by file watcher after edits) */
795
+ async incrementalUpdate() {
796
+ if (!this.indexed || this.indexDisabled || this.indexOversized)
797
+ return;
798
+ try {
799
+ await this.exec(['update'], 15000);
800
+ }
801
+ catch (err) {
802
+ console.error(`[token-pilot] ast-index incremental update failed: ${err instanceof Error ? err.message : err}`);
803
+ }
804
+ }
805
+ // --- Utility methods ---
639
806
  isAvailable() {
640
807
  return this.binaryPath !== null;
641
808
  }
@@ -153,4 +153,33 @@ export interface AstIndexImportEntry {
153
153
  isDefault?: boolean;
154
154
  isNamespace?: boolean;
155
155
  }
156
+ /** ast-index agrep — structural pattern search via ast-grep */
157
+ export interface AstIndexAgrepMatch {
158
+ file: string;
159
+ line: number;
160
+ text: string;
161
+ }
162
+ /** ast-index todo — TODO/FIXME/HACK comments */
163
+ export interface AstIndexTodoEntry {
164
+ file: string;
165
+ line: number;
166
+ kind: string;
167
+ text: string;
168
+ }
169
+ /** ast-index deprecated — @Deprecated symbols */
170
+ export interface AstIndexDeprecatedEntry {
171
+ name: string;
172
+ kind: string;
173
+ file: string;
174
+ line: number;
175
+ message?: string;
176
+ }
177
+ /** ast-index annotations — symbols with specific decorator/annotation */
178
+ export interface AstIndexAnnotationEntry {
179
+ name: string;
180
+ kind: string;
181
+ file: string;
182
+ line: number;
183
+ annotation: string;
184
+ }
156
185
  //# sourceMappingURL=types.d.ts.map
@@ -76,6 +76,14 @@ export declare function validateFindUnusedArgs(args: unknown): {
76
76
  export_only?: boolean;
77
77
  limit?: number;
78
78
  };
79
+ export interface CodeAuditArgs {
80
+ check: 'pattern' | 'todo' | 'deprecated' | 'annotations' | 'all';
81
+ pattern?: string;
82
+ name?: string;
83
+ lang?: string;
84
+ limit?: number;
85
+ }
86
+ export declare function validateCodeAuditArgs(args: unknown): CodeAuditArgs;
79
87
  /** Detect roots that would cause ast-index to scan the entire filesystem */
80
88
  export declare function isDangerousRoot(root: string): boolean;
81
89
  //# sourceMappingURL=validation.d.ts.map
@@ -207,6 +207,33 @@ export function validateFindUnusedArgs(args) {
207
207
  limit: optionalNumber(a.limit, 'limit'),
208
208
  };
209
209
  }
210
+ export function validateCodeAuditArgs(args) {
211
+ if (!args || typeof args !== 'object') {
212
+ throw new Error('Arguments must be an object with a "check" parameter.');
213
+ }
214
+ const a = args;
215
+ const validChecks = ['pattern', 'todo', 'deprecated', 'annotations', 'all'];
216
+ if (typeof a.check !== 'string' || !validChecks.includes(a.check)) {
217
+ throw new Error(`Required parameter "check" must be one of: ${validChecks.join(', ')}`);
218
+ }
219
+ if (a.check === 'pattern') {
220
+ if (typeof a.pattern !== 'string' || a.pattern.length === 0) {
221
+ throw new Error('Parameter "pattern" is required when check="pattern". Example: "except:" or "print($$$ARGS)"');
222
+ }
223
+ }
224
+ if (a.check === 'annotations') {
225
+ if (typeof a.name !== 'string' || a.name.length === 0) {
226
+ throw new Error('Parameter "name" is required when check="annotations". Example: "Deprecated" or "Controller"');
227
+ }
228
+ }
229
+ return {
230
+ check: a.check,
231
+ pattern: optionalString(a.pattern, 'pattern'),
232
+ name: optionalString(a.name, 'name'),
233
+ lang: optionalString(a.lang, 'lang'),
234
+ limit: optionalNumber(a.limit, 'limit'),
235
+ };
236
+ }
210
237
  /** Detect roots that would cause ast-index to scan the entire filesystem */
211
238
  export function isDangerousRoot(root) {
212
239
  const normalized = root.replace(/\/+$/, '') || '/';
@@ -1,18 +1,27 @@
1
1
  import type { FileCache } from '../core/file-cache.js';
2
2
  import type { ContextRegistry } from '../core/context-registry.js';
3
+ import type { AstIndexClient } from '../ast-index/client.js';
3
4
  /**
4
5
  * Watches individual files for changes and auto-invalidates cache.
5
6
  * Only watches files that have been explicitly added (via watchFile()),
6
7
  * NOT the entire project root — avoids scanning thousands of files
7
8
  * and permission errors on Docker volumes, restricted dirs, etc.
9
+ *
10
+ * Also triggers debounced ast-index incremental update on file changes
11
+ * to keep the index fresh for find_usages, find_unused, code_audit.
8
12
  */
9
13
  export declare class FileWatcher {
14
+ private static readonly UPDATE_DEBOUNCE_MS;
10
15
  private fileCache;
11
16
  private contextRegistry;
17
+ private astIndex;
12
18
  private watcher;
13
19
  private watchedFiles;
14
- constructor(_projectRoot: string, fileCache: FileCache, contextRegistry: ContextRegistry, _ignore: string[]);
20
+ private updateTimer;
21
+ constructor(_projectRoot: string, fileCache: FileCache, contextRegistry: ContextRegistry, _ignore: string[], astIndex?: AstIndexClient);
15
22
  start(): void;
23
+ /** Debounced ast-index incremental update after file changes */
24
+ private scheduleIndexUpdate;
16
25
  /** Add a specific file to watch. Called after smart_read/read_symbol loads a file. */
17
26
  watchFile(filePath: string): void;
18
27
  stop(): void;
@@ -5,15 +5,22 @@ import { resolve } from 'node:path';
5
5
  * Only watches files that have been explicitly added (via watchFile()),
6
6
  * NOT the entire project root — avoids scanning thousands of files
7
7
  * and permission errors on Docker volumes, restricted dirs, etc.
8
+ *
9
+ * Also triggers debounced ast-index incremental update on file changes
10
+ * to keep the index fresh for find_usages, find_unused, code_audit.
8
11
  */
9
12
  export class FileWatcher {
13
+ static UPDATE_DEBOUNCE_MS = 2000;
10
14
  fileCache;
11
15
  contextRegistry;
16
+ astIndex;
12
17
  watcher = null;
13
18
  watchedFiles = new Set();
14
- constructor(_projectRoot, fileCache, contextRegistry, _ignore) {
19
+ updateTimer = null;
20
+ constructor(_projectRoot, fileCache, contextRegistry, _ignore, astIndex) {
15
21
  this.fileCache = fileCache;
16
22
  this.contextRegistry = contextRegistry;
23
+ this.astIndex = astIndex ?? null;
17
24
  }
18
25
  start() {
19
26
  // Start with an empty watcher — files are added on demand via watchFile()
@@ -30,14 +37,26 @@ export class FileWatcher {
30
37
  if (this.fileCache.get(absPath)) {
31
38
  this.fileCache.invalidate(absPath);
32
39
  }
40
+ this.scheduleIndexUpdate();
33
41
  });
34
42
  this.watcher.on('unlink', (filePath) => {
35
43
  const absPath = resolve(filePath);
36
44
  this.fileCache.invalidate(absPath);
37
45
  this.contextRegistry.forget(absPath);
38
46
  this.watchedFiles.delete(absPath);
47
+ this.scheduleIndexUpdate();
39
48
  });
40
49
  }
50
+ /** Debounced ast-index incremental update after file changes */
51
+ scheduleIndexUpdate() {
52
+ if (!this.astIndex)
53
+ return;
54
+ if (this.updateTimer)
55
+ clearTimeout(this.updateTimer);
56
+ this.updateTimer = setTimeout(() => {
57
+ this.astIndex?.incrementalUpdate().catch(() => { });
58
+ }, FileWatcher.UPDATE_DEBOUNCE_MS);
59
+ }
41
60
  /** Add a specific file to watch. Called after smart_read/read_symbol loads a file. */
42
61
  watchFile(filePath) {
43
62
  const absPath = resolve(filePath);
@@ -49,6 +68,9 @@ export class FileWatcher {
49
68
  this.watchedFiles.add(absPath);
50
69
  }
51
70
  stop() {
71
+ if (this.updateTimer)
72
+ clearTimeout(this.updateTimer);
73
+ this.updateTimer = null;
52
74
  this.watcher?.close();
53
75
  this.watcher = null;
54
76
  this.watchedFiles.clear();
@@ -0,0 +1,9 @@
1
+ import type { AstIndexClient } from '../ast-index/client.js';
2
+ import type { CodeAuditArgs } from '../core/validation.js';
3
+ export declare function handleCodeAudit(args: CodeAuditArgs, projectRoot: string, astIndex: AstIndexClient): Promise<{
4
+ content: Array<{
5
+ type: 'text';
6
+ text: string;
7
+ }>;
8
+ }>;
9
+ //# sourceMappingURL=code-audit.d.ts.map
@@ -0,0 +1,200 @@
1
+ import { relative } from 'node:path';
2
+ export async function handleCodeAudit(args, projectRoot, astIndex) {
3
+ if (astIndex.isDisabled() || astIndex.isOversized()) {
4
+ return {
5
+ content: [{
6
+ type: 'text',
7
+ text: 'ast-index is not available (project root too broad or index oversized). Use Grep/ripgrep for pattern search.',
8
+ }],
9
+ };
10
+ }
11
+ const limit = args.limit ?? 50;
12
+ switch (args.check) {
13
+ case 'pattern':
14
+ return handlePattern(args.pattern, args.lang, limit, projectRoot, astIndex);
15
+ case 'todo':
16
+ return handleTodo(limit, projectRoot, astIndex);
17
+ case 'deprecated':
18
+ return handleDeprecated(limit, projectRoot, astIndex);
19
+ case 'annotations':
20
+ return handleAnnotations(args.name, limit, projectRoot, astIndex);
21
+ case 'all':
22
+ return handleAll(limit, projectRoot, astIndex);
23
+ default:
24
+ return {
25
+ content: [{
26
+ type: 'text',
27
+ text: `Unknown check type: "${args.check}". Use: pattern, todo, deprecated, annotations, all`,
28
+ }],
29
+ };
30
+ }
31
+ }
32
+ function rel(projectRoot, absPath) {
33
+ return relative(projectRoot, absPath) || absPath;
34
+ }
35
+ async function handlePattern(pattern, lang, limit, projectRoot, astIndex) {
36
+ try {
37
+ const matches = await astIndex.agrep(pattern, { lang, limit });
38
+ if (matches.length === 0) {
39
+ return {
40
+ content: [{
41
+ type: 'text',
42
+ 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.`,
43
+ }],
44
+ };
45
+ }
46
+ // Group by file
47
+ const byFile = new Map();
48
+ for (const m of matches) {
49
+ const key = rel(projectRoot, m.file);
50
+ if (!byFile.has(key))
51
+ byFile.set(key, []);
52
+ byFile.get(key).push({ line: m.line, text: m.text });
53
+ }
54
+ const lines = [
55
+ `PATTERN SEARCH: "${pattern}"${lang ? ` (${lang})` : ''} — ${matches.length} matches in ${byFile.size} files`,
56
+ '',
57
+ ];
58
+ for (const [file, items] of byFile) {
59
+ lines.push(`${file}:`);
60
+ for (const item of items) {
61
+ lines.push(` L${item.line}: ${item.text}`);
62
+ }
63
+ lines.push('');
64
+ }
65
+ lines.push('HINT: Use read_symbol() to inspect specific matches, or Grep for text-based counting.');
66
+ return { content: [{ type: 'text', text: lines.join('\n') }] };
67
+ }
68
+ catch (err) {
69
+ // ast-grep not installed — return the error message
70
+ return {
71
+ content: [{
72
+ type: 'text',
73
+ text: `PATTERN SEARCH ERROR:\n${err instanceof Error ? err.message : String(err)}\n\nFallback: Use Grep/ripgrep for text-based pattern search.`,
74
+ }],
75
+ };
76
+ }
77
+ }
78
+ async function handleTodo(limit, projectRoot, astIndex) {
79
+ const entries = await astIndex.todo();
80
+ const limited = entries.slice(0, limit);
81
+ if (limited.length === 0) {
82
+ return {
83
+ content: [{
84
+ type: 'text',
85
+ 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"',
86
+ }],
87
+ };
88
+ }
89
+ // Group by kind
90
+ const byKind = new Map();
91
+ for (const e of limited) {
92
+ const kind = e.kind;
93
+ if (!byKind.has(kind))
94
+ byKind.set(kind, []);
95
+ byKind.get(kind).push({ file: rel(projectRoot, e.file), line: e.line, text: e.text });
96
+ }
97
+ const lines = [
98
+ `TODO/FIXME COMMENTS: ${limited.length} found${entries.length > limit ? ` (showing ${limit} of ${entries.length})` : ''}`,
99
+ '',
100
+ ];
101
+ for (const [kind, items] of byKind) {
102
+ lines.push(`${kind} (${items.length}):`);
103
+ for (const item of items) {
104
+ lines.push(` ${item.file}:${item.line} — ${item.text}`);
105
+ }
106
+ lines.push('');
107
+ }
108
+ return { content: [{ type: 'text', text: lines.join('\n') }] };
109
+ }
110
+ async function handleDeprecated(limit, projectRoot, astIndex) {
111
+ const entries = await astIndex.deprecated();
112
+ const limited = entries.slice(0, limit);
113
+ if (limited.length === 0) {
114
+ return {
115
+ content: [{
116
+ type: 'text',
117
+ text: 'DEPRECATED SYMBOLS: none found.\n\nHINT: ast-index detects @Deprecated annotations. Try Grep for other deprecation patterns.',
118
+ }],
119
+ };
120
+ }
121
+ const lines = [
122
+ `DEPRECATED SYMBOLS: ${limited.length} found${entries.length > limit ? ` (showing ${limit} of ${entries.length})` : ''}`,
123
+ '',
124
+ ];
125
+ for (const e of limited) {
126
+ const loc = `${rel(projectRoot, e.file)}:${e.line}`;
127
+ lines.push(` ${e.kind} ${e.name} ${loc}${e.message ? ` — ${e.message}` : ''}`);
128
+ }
129
+ lines.push('');
130
+ lines.push('HINT: Use read_symbol() to inspect deprecated symbols before removing them.');
131
+ return { content: [{ type: 'text', text: lines.join('\n') }] };
132
+ }
133
+ async function handleAnnotations(name, limit, projectRoot, astIndex) {
134
+ const entries = await astIndex.annotations(name);
135
+ const limited = entries.slice(0, limit);
136
+ if (limited.length === 0) {
137
+ return {
138
+ content: [{
139
+ type: 'text',
140
+ text: `ANNOTATIONS @${name}: none found.\n\nHINT: Try Grep for text-based search: grep -rn "@${name}" --include="*.ts"`,
141
+ }],
142
+ };
143
+ }
144
+ // Group by file
145
+ const byFile = new Map();
146
+ for (const e of limited) {
147
+ const key = rel(projectRoot, e.file);
148
+ if (!byFile.has(key))
149
+ byFile.set(key, []);
150
+ byFile.get(key).push({ name: e.name, kind: e.kind, line: e.line });
151
+ }
152
+ const lines = [
153
+ `ANNOTATIONS @${name}: ${limited.length} found in ${byFile.size} files${entries.length > limit ? ` (showing ${limit} of ${entries.length})` : ''}`,
154
+ '',
155
+ ];
156
+ for (const [file, items] of byFile) {
157
+ lines.push(`${file}:`);
158
+ for (const item of items) {
159
+ lines.push(` L${item.line}: ${item.kind} ${item.name}`);
160
+ }
161
+ lines.push('');
162
+ }
163
+ return { content: [{ type: 'text', text: lines.join('\n') }] };
164
+ }
165
+ async function handleAll(limit, projectRoot, astIndex) {
166
+ // Run todo + deprecated in parallel
167
+ const [todos, deprecated] = await Promise.all([
168
+ astIndex.todo(),
169
+ astIndex.deprecated(),
170
+ ]);
171
+ const sections = ['CODE AUDIT SUMMARY', ''];
172
+ // TODOs
173
+ const todoLimited = todos.slice(0, limit);
174
+ if (todoLimited.length === 0) {
175
+ sections.push('TODO/FIXME: none found');
176
+ }
177
+ else {
178
+ sections.push(`TODO/FIXME: ${todoLimited.length} comments${todos.length > limit ? ` (${todos.length} total)` : ''}`);
179
+ for (const e of todoLimited) {
180
+ sections.push(` ${rel(projectRoot, e.file)}:${e.line} [${e.kind}] ${e.text}`);
181
+ }
182
+ }
183
+ sections.push('');
184
+ // Deprecated
185
+ const depLimited = deprecated.slice(0, limit);
186
+ if (depLimited.length === 0) {
187
+ sections.push('DEPRECATED: none found');
188
+ }
189
+ else {
190
+ sections.push(`DEPRECATED: ${depLimited.length} symbols${deprecated.length > limit ? ` (${deprecated.length} total)` : ''}`);
191
+ for (const e of depLimited) {
192
+ sections.push(` ${rel(projectRoot, e.file)}:${e.line} ${e.kind} ${e.name}${e.message ? ` — ${e.message}` : ''}`);
193
+ }
194
+ }
195
+ sections.push('');
196
+ sections.push('HINT: Use code_audit(check="pattern", pattern="...") for structural pattern search (requires ast-grep).');
197
+ sections.push(' Use Grep for text-based counting and regex search.');
198
+ return { content: [{ type: 'text', text: sections.join('\n') }] };
199
+ }
200
+ //# sourceMappingURL=code-audit.js.map
@@ -6,6 +6,16 @@ import { resolveSafePath } from '../core/validation.js';
6
6
  import { isNonCodeStructured, handleNonCodeRead } from './non-code.js';
7
7
  export async function handleSmartRead(args, projectRoot, astIndex, fileCache, contextRegistry, config) {
8
8
  const absPath = resolveSafePath(projectRoot, args.path);
9
+ // 0. Guard: directory passed instead of file
10
+ const fileStat0 = await stat(absPath).catch(() => null);
11
+ if (fileStat0?.isDirectory()) {
12
+ return {
13
+ content: [{
14
+ type: 'text',
15
+ text: `"${args.path}" is a directory. Use outline("${args.path}") for directory overview, or smart_read a specific file inside it.`,
16
+ }],
17
+ };
18
+ }
9
19
  // 1. Read file content
10
20
  const content = await readFile(absPath, 'utf-8');
11
21
  const lines = content.split('\n');
package/dist/server.js CHANGED
@@ -26,9 +26,10 @@ import { handleFindUnused } from './handlers/find-unused.js';
26
26
  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
+ import { handleCodeAudit } from './handlers/code-audit.js';
29
30
  import { detectContextMode } from './integration/context-mode-detector.js';
30
31
  import { estimateTokens } from './core/token-estimator.js';
31
- import { resolveSafePath, validateSmartReadArgs, validateReadSymbolArgs, validateReadRangeArgs, validateReadDiffArgs, validateFindUsagesArgs, validateSmartReadManyArgs, validateReadForEditArgs, validateRelatedFilesArgs, validateOutlineArgs, validateFindUnusedArgs, } from './core/validation.js';
32
+ import { resolveSafePath, validateSmartReadArgs, validateReadSymbolArgs, validateReadRangeArgs, validateReadDiffArgs, validateFindUsagesArgs, validateSmartReadManyArgs, validateReadForEditArgs, validateRelatedFilesArgs, validateOutlineArgs, validateFindUnusedArgs, validateCodeAuditArgs, } from './core/validation.js';
32
33
  export async function createServer(projectRoot, options) {
33
34
  const config = await loadConfig(projectRoot);
34
35
  const astIndex = new AstIndexClient(projectRoot, config.astIndex.timeout, {
@@ -152,7 +153,7 @@ export async function createServer(projectRoot, options) {
152
153
  // Watches only files that have been loaded — NOT the entire project root
153
154
  let fileWatcher = null;
154
155
  if (config.cache.watchFiles) {
155
- fileWatcher = new FileWatcher(projectRoot, fileCache, contextRegistry, config.ignore);
156
+ fileWatcher = new FileWatcher(projectRoot, fileCache, contextRegistry, config.ignore, astIndex);
156
157
  fileWatcher.start();
157
158
  fileCache.onSet((filePath) => fileWatcher?.watchFile(filePath));
158
159
  }
@@ -180,13 +181,22 @@ export async function createServer(projectRoot, options) {
180
181
  '• New codebase → project_overview first',
181
182
  '• Reading file again → smart_read (returns compact reminder, not full content)',
182
183
  '• Multiple files → smart_read_many (batch, max 20)',
184
+ '• Code quality audit → code_audit (TODOs, deprecated, structural code patterns)',
183
185
  '',
184
186
  'WHEN TO USE DEFAULT TOOLS (Token Pilot adds no value):',
185
187
  '• Small files (≤200 lines) → smart_read returns full content anyway, same as Read',
186
- '• Regex/pattern search (e.g. TODO.*fix) → use Grep/ripgrep, NOT find_usages',
188
+ '• Regex text search (e.g. TODO.*fix) → use Grep/ripgrep',
189
+ '• Counting occurrences (e.g. how many `any` types?) → use Grep count mode',
190
+ '• Finding code duplication → use Grep to search for repeated patterns',
187
191
  '• Non-code files (JSON, YAML, Markdown, configs) → smart_read handles these but default Read works too',
188
192
  '• You need exact raw content for copy-paste → use Read',
189
193
  '',
194
+ 'COMBINE BOTH for audits and code review:',
195
+ '• Structure/navigation → Token Pilot (project_overview, outline, smart_read)',
196
+ '• Code issues → code_audit (TODOs, deprecated, structural patterns like bare except:)',
197
+ '• Text pattern search/counting → Grep (regex, count mode)',
198
+ '• Deep dive into specific code → read_symbol (after finding issues)',
199
+ '',
190
200
  'WORKFLOW: project_overview → smart_read → read_symbol → read_for_edit → edit → read_diff',
191
201
  ].join('\n'),
192
202
  });
@@ -340,6 +350,25 @@ export async function createServer(projectRoot, options) {
340
350
  },
341
351
  },
342
352
  },
353
+ {
354
+ name: 'code_audit',
355
+ description: 'Find code quality issues: TODO/FIXME comments, deprecated symbols, structural code patterns (bare except:, print() calls). Use for project-wide audits.',
356
+ inputSchema: {
357
+ type: 'object',
358
+ properties: {
359
+ check: {
360
+ type: 'string',
361
+ enum: ['pattern', 'todo', 'deprecated', 'annotations', 'all'],
362
+ description: 'What to check: "pattern" (structural search via ast-grep, e.g. "except:", "print($$$ARGS)"), "todo" (TODO/FIXME comments), "deprecated" (deprecated symbols), "annotations" (find by decorator name), "all" (todo + deprecated summary)',
363
+ },
364
+ pattern: { type: 'string', description: 'Code pattern for check="pattern". ast-grep syntax: "except:" finds bare excepts, "print($$$ARGS)" finds print calls.' },
365
+ name: { type: 'string', description: 'Decorator/annotation name for check="annotations". Example: "Deprecated", "Controller"' },
366
+ lang: { type: 'string', description: 'Language filter for check="pattern" (e.g., "python", "typescript")' },
367
+ limit: { type: 'number', description: 'Max results (default: 50)' },
368
+ },
369
+ required: ['check'],
370
+ },
371
+ },
343
372
  ],
344
373
  }));
345
374
  // Helper: get real full-file token count for honest analytics
@@ -475,6 +504,13 @@ export async function createServer(projectRoot, options) {
475
504
  analytics.record({ tool: 'find_unused', path: unusedArgs.module ?? 'all', tokensReturned: estimateTokens(unusedText), tokensWouldBe: estimateTokens(unusedText), timestamp: Date.now() });
476
505
  return unusedResult;
477
506
  }
507
+ case 'code_audit': {
508
+ const auditArgs = validateCodeAuditArgs(args);
509
+ const auditResult = await handleCodeAudit(auditArgs, projectRoot, astIndex);
510
+ const auditText = auditResult.content[0]?.text ?? '';
511
+ analytics.record({ tool: 'code_audit', path: auditArgs.check, tokensReturned: estimateTokens(auditText), tokensWouldBe: estimateTokens(auditText), timestamp: Date.now() });
512
+ return auditResult;
513
+ }
478
514
  default:
479
515
  return {
480
516
  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.7.6",
3
+ "version": "0.8.0",
4
4
  "description": "MCP server that reduces token consumption in AI coding assistants via AST-aware lazy file reading",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",