token-pilot 0.7.6 → 0.8.1
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 +20 -0
- package/README.md +12 -4
- package/dist/ast-index/client.d.ts +22 -2
- package/dist/ast-index/client.js +181 -9
- package/dist/ast-index/types.d.ts +29 -0
- package/dist/core/validation.d.ts +8 -0
- package/dist/core/validation.js +27 -0
- package/dist/git/file-watcher.d.ts +10 -1
- package/dist/git/file-watcher.js +23 -1
- package/dist/handlers/code-audit.d.ts +9 -0
- package/dist/handlers/code-audit.js +200 -0
- package/dist/handlers/smart-read.js +10 -0
- package/dist/server.js +41 -3
- package/package.json +21 -12
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,26 @@ 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.1] - 2026-03-08
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
- **ast-grep auto-install** — `@ast-grep/cli` added as optional dependency. `code_audit(check="pattern")` now works out-of-the-box without manual `brew install ast-grep`.
|
|
12
|
+
- **MCP instructions: security audit guidance** — instructions now recommend Grep for security patterns (password, token, secret, credential) and `find_unused` for dead code detection.
|
|
13
|
+
|
|
14
|
+
### Changed
|
|
15
|
+
- **ast-index stats → JSON parsing** — `--format json` for reliable file count extraction instead of regex on text output.
|
|
16
|
+
|
|
17
|
+
## [0.8.0] - 2026-03-07
|
|
18
|
+
|
|
19
|
+
### Added
|
|
20
|
+
- **`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"`).
|
|
21
|
+
- **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.
|
|
22
|
+
- **ast-index client methods** — `agrep()`, `todo()`, `deprecated()`, `annotations()`, `incrementalUpdate()`.
|
|
23
|
+
|
|
24
|
+
### Fixed
|
|
25
|
+
- **smart_read on directories** — now returns helpful message instead of EISDIR crash.
|
|
26
|
+
- **MCP instructions** — added "COMBINE BOTH" section for audit tasks (Token Pilot + Grep).
|
|
27
|
+
|
|
8
28
|
## [0.7.6] - 2026-03-07
|
|
9
29
|
|
|
10
30
|
### 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: **
|
|
58
|
+
Works with: **Claude Code**, **Cursor**, **Codex**, **Antigravity**, **Cline**, and any MCP-compatible client.
|
|
57
59
|
|
|
58
60
|
#### Cursor
|
|
59
61
|
|
|
@@ -105,6 +107,10 @@ brew tap defendend/ast-index && brew install ast-index
|
|
|
105
107
|
npx token-pilot install-ast-index
|
|
106
108
|
```
|
|
107
109
|
|
|
110
|
+
### ast-grep (bundled)
|
|
111
|
+
|
|
112
|
+
[ast-grep](https://ast-grep.github.io/) (`sg`) is included as optional dependency for structural code pattern search via `code_audit(check="pattern")`. Installs automatically with `npm i -g token-pilot`.
|
|
113
|
+
|
|
108
114
|
### PreToolUse Hook (Claude Code only)
|
|
109
115
|
|
|
110
116
|
Optional hook that intercepts `Read` calls for large code files (>500 lines) and suggests `smart_read`. Claude Code only.
|
|
@@ -143,7 +149,7 @@ For more control, you can add rules to your project:
|
|
|
143
149
|
- **Cursor** → `.cursorrules` in project root
|
|
144
150
|
- **Codex** → `AGENTS.md` in project root
|
|
145
151
|
|
|
146
|
-
## MCP Tools (
|
|
152
|
+
## MCP Tools (13)
|
|
147
153
|
|
|
148
154
|
### Core Reading
|
|
149
155
|
|
|
@@ -165,6 +171,7 @@ For more control, you can add rules to your project:
|
|
|
165
171
|
| `related_files` | manual explore | Import graph: what a file imports, what imports it, test files. |
|
|
166
172
|
| `outline` | multiple `smart_read` | Compact overview of all code files in a directory. One call instead of 5-6. |
|
|
167
173
|
| `find_unused` | manual | Detect dead code — unused functions, classes, variables. |
|
|
174
|
+
| `code_audit` | multiple `Grep` | Code quality issues in one call: TODO/FIXME comments, deprecated symbols, structural code patterns (via ast-grep), decorator search. |
|
|
168
175
|
|
|
169
176
|
### Analytics
|
|
170
177
|
|
|
@@ -177,6 +184,7 @@ For more control, you can add rules to your project:
|
|
|
177
184
|
```bash
|
|
178
185
|
token-pilot # Start MCP server (uses cwd as project root)
|
|
179
186
|
token-pilot /path/to/project # Start with specific project root
|
|
187
|
+
token-pilot init [dir] # Create/update .mcp.json (token-pilot + context-mode)
|
|
180
188
|
token-pilot install-ast-index # Download ast-index binary (auto on first run)
|
|
181
189
|
token-pilot install-hook [root] # Install PreToolUse hook
|
|
182
190
|
token-pilot uninstall-hook # Remove hook
|
|
@@ -193,7 +201,7 @@ Create `.token-pilot.json` in your project root to customize behavior:
|
|
|
193
201
|
```json
|
|
194
202
|
{
|
|
195
203
|
"smartRead": {
|
|
196
|
-
"smallFileThreshold":
|
|
204
|
+
"smallFileThreshold": 200,
|
|
197
205
|
"advisoryReminders": true
|
|
198
206
|
},
|
|
199
207
|
"cache": {
|
|
@@ -251,7 +259,6 @@ When both are configured, Token Pilot automatically:
|
|
|
251
259
|
- Detects context-mode via `.mcp.json`
|
|
252
260
|
- Suggests context-mode for large non-code files
|
|
253
261
|
- Shows combined architecture info in `session_analytics`
|
|
254
|
-
- Provides `export_ast_index` to feed AST data into context-mode's BM25 index
|
|
255
262
|
|
|
256
263
|
**Combined savings: 60-80%** in a typical coding session.
|
|
257
264
|
|
|
@@ -339,6 +346,7 @@ src/
|
|
|
339
346
|
related-files.ts — related_files handler (import graph)
|
|
340
347
|
outline.ts — outline handler (directory overview)
|
|
341
348
|
find-unused.ts — find_unused handler
|
|
349
|
+
code-audit.ts — code_audit handler (TODOs, deprecated, patterns)
|
|
342
350
|
project-overview.ts — project_overview (via ast-index map + conventions)
|
|
343
351
|
non-code.ts — JSON/YAML/MD/TOML structural summaries
|
|
344
352
|
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;
|
|
@@ -20,7 +21,7 @@ export declare class AstIndexClient {
|
|
|
20
21
|
private buildIndex;
|
|
21
22
|
/** Mark index as oversized — disables index-dependent tools, outline still works */
|
|
22
23
|
private handleOversizedIndex;
|
|
23
|
-
/** Extract file count from stats output */
|
|
24
|
+
/** Extract file count from stats output (JSON or text) */
|
|
24
25
|
private parseFileCount;
|
|
25
26
|
outline(filePath: string): Promise<FileStructure | null>;
|
|
26
27
|
/**
|
|
@@ -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;
|
package/dist/ast-index/client.js
CHANGED
|
@@ -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;
|
|
@@ -74,9 +75,8 @@ export class AstIndexClient {
|
|
|
74
75
|
// Check if index already exists and has files
|
|
75
76
|
let existingFileCount = 0;
|
|
76
77
|
try {
|
|
77
|
-
const stats = await this.exec(['stats']);
|
|
78
|
-
|
|
79
|
-
existingFileCount = filesMatch ? parseInt(filesMatch[1], 10) : 0;
|
|
78
|
+
const stats = await this.exec(['--format', 'json', 'stats']);
|
|
79
|
+
existingFileCount = this.parseFileCount(stats);
|
|
80
80
|
}
|
|
81
81
|
catch { /* no index yet */ }
|
|
82
82
|
// Guard: existing index is oversized (node_modules leak from previous build)
|
|
@@ -96,9 +96,7 @@ export class AstIndexClient {
|
|
|
96
96
|
await this.exec(['update'], 30000);
|
|
97
97
|
// Re-check count after update
|
|
98
98
|
try {
|
|
99
|
-
|
|
100
|
-
const filesMatch = statsText.match(/Files:\s*(\d+)/);
|
|
101
|
-
existingFileCount = filesMatch ? parseInt(filesMatch[1], 10) : existingFileCount;
|
|
99
|
+
existingFileCount = this.parseFileCount(await this.exec(['--format', 'json', 'stats']));
|
|
102
100
|
}
|
|
103
101
|
catch { /* keep previous count */ }
|
|
104
102
|
// Guard: update may have grown index beyond limit
|
|
@@ -117,7 +115,7 @@ export class AstIndexClient {
|
|
|
117
115
|
console.error('[token-pilot] ast-index: building index (this may take a moment)...');
|
|
118
116
|
try {
|
|
119
117
|
await this.exec(['rebuild'], 120000);
|
|
120
|
-
const fileCount = this.parseFileCount(await this.exec(['stats']).catch(() => ''));
|
|
118
|
+
const fileCount = this.parseFileCount(await this.exec(['--format', 'json', 'stats']).catch(() => ''));
|
|
121
119
|
// Guard: rebuild produced oversized index
|
|
122
120
|
if (fileCount > AstIndexClient.MAX_INDEX_FILES) {
|
|
123
121
|
return this.handleOversizedIndex(fileCount);
|
|
@@ -129,7 +127,7 @@ export class AstIndexClient {
|
|
|
129
127
|
// If rebuild failed due to lock, check if index is usable anyway
|
|
130
128
|
const errMsg = buildErr instanceof Error ? buildErr.message : String(buildErr);
|
|
131
129
|
if (errMsg.includes('lock') || errMsg.includes('already running')) {
|
|
132
|
-
const count = this.parseFileCount(await this.exec(['stats']).catch(() => ''));
|
|
130
|
+
const count = this.parseFileCount(await this.exec(['--format', 'json', 'stats']).catch(() => ''));
|
|
133
131
|
if (count > 0 && count <= AstIndexClient.MAX_INDEX_FILES) {
|
|
134
132
|
this.indexed = true;
|
|
135
133
|
console.error(`[token-pilot] ast-index: using existing index (${count} files, rebuild skipped due to lock)`);
|
|
@@ -157,8 +155,16 @@ export class AstIndexClient {
|
|
|
157
155
|
` → Tools disabled: find_unused, find_usages, related_files, project_overview\n` +
|
|
158
156
|
` → Tools still working: outline, smart_read, smart_read_many, read_symbol`);
|
|
159
157
|
}
|
|
160
|
-
/** Extract file count from stats output */
|
|
158
|
+
/** Extract file count from stats output (JSON or text) */
|
|
161
159
|
parseFileCount(statsText) {
|
|
160
|
+
// Try JSON first (--format json)
|
|
161
|
+
try {
|
|
162
|
+
const json = JSON.parse(statsText);
|
|
163
|
+
if (json?.stats?.file_count !== undefined)
|
|
164
|
+
return json.stats.file_count;
|
|
165
|
+
}
|
|
166
|
+
catch { /* not JSON, fall through */ }
|
|
167
|
+
// Fallback: text format
|
|
162
168
|
const match = statsText.match(/Files:\s*(\d+)/);
|
|
163
169
|
return match ? parseInt(match[1], 10) : 0;
|
|
164
170
|
}
|
|
@@ -636,6 +642,172 @@ export class AstIndexClient {
|
|
|
636
642
|
}
|
|
637
643
|
return entries;
|
|
638
644
|
}
|
|
645
|
+
// --- Code audit commands ---
|
|
646
|
+
/** Check if ast-grep (sg) is available for structural pattern search */
|
|
647
|
+
async checkAstGrep() {
|
|
648
|
+
if (this.astGrepAvailable !== null)
|
|
649
|
+
return this.astGrepAvailable;
|
|
650
|
+
try {
|
|
651
|
+
await execFileAsync('sg', ['--version'], { timeout: 3000 });
|
|
652
|
+
this.astGrepAvailable = true;
|
|
653
|
+
}
|
|
654
|
+
catch {
|
|
655
|
+
this.astGrepAvailable = false;
|
|
656
|
+
}
|
|
657
|
+
return this.astGrepAvailable;
|
|
658
|
+
}
|
|
659
|
+
/** Structural pattern search via ast-grep. Requires ast-grep (sg) installed. */
|
|
660
|
+
async agrep(pattern, options) {
|
|
661
|
+
if (this.indexDisabled || this.indexOversized)
|
|
662
|
+
return [];
|
|
663
|
+
await this.ensureIndex();
|
|
664
|
+
const available = await this.checkAstGrep();
|
|
665
|
+
if (!available) {
|
|
666
|
+
throw new Error('ast-grep (sg) not installed — required for structural pattern search.\n' +
|
|
667
|
+
'Install: brew install ast-grep OR npm i -g @ast-grep/cli\n' +
|
|
668
|
+
'Alternative: use Grep/ripgrep for text-based pattern search.');
|
|
669
|
+
}
|
|
670
|
+
const limit = options?.limit ?? 50;
|
|
671
|
+
const args = ['agrep', pattern];
|
|
672
|
+
if (options?.lang)
|
|
673
|
+
args.push('--lang', options.lang);
|
|
674
|
+
args.push('--limit', String(limit));
|
|
675
|
+
try {
|
|
676
|
+
const result = await this.exec(args, 15000);
|
|
677
|
+
return this.parseAgrepText(result).slice(0, limit);
|
|
678
|
+
}
|
|
679
|
+
catch (err) {
|
|
680
|
+
console.error(`[token-pilot] ast-index agrep failed: ${err instanceof Error ? err.message : err}`);
|
|
681
|
+
return [];
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
parseAgrepText(text) {
|
|
685
|
+
const results = [];
|
|
686
|
+
for (const line of text.split('\n')) {
|
|
687
|
+
if (!line.trim())
|
|
688
|
+
continue;
|
|
689
|
+
// Format: file:line:matched_text OR file:line: matched_text
|
|
690
|
+
const match = line.match(/^(.+?):(\d+):(.*)$/);
|
|
691
|
+
if (match) {
|
|
692
|
+
results.push({
|
|
693
|
+
file: match[1],
|
|
694
|
+
line: parseInt(match[2], 10),
|
|
695
|
+
text: match[3].trim(),
|
|
696
|
+
});
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
return results;
|
|
700
|
+
}
|
|
701
|
+
/** Find TODO/FIXME/HACK comments in the project */
|
|
702
|
+
async todo() {
|
|
703
|
+
if (this.indexDisabled || this.indexOversized)
|
|
704
|
+
return [];
|
|
705
|
+
await this.ensureIndex();
|
|
706
|
+
try {
|
|
707
|
+
const result = await this.exec(['todo'], 15000);
|
|
708
|
+
return this.parseTodoText(result);
|
|
709
|
+
}
|
|
710
|
+
catch (err) {
|
|
711
|
+
console.error(`[token-pilot] ast-index todo failed: ${err instanceof Error ? err.message : err}`);
|
|
712
|
+
return [];
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
parseTodoText(text) {
|
|
716
|
+
const results = [];
|
|
717
|
+
for (const line of text.split('\n')) {
|
|
718
|
+
if (!line.trim())
|
|
719
|
+
continue;
|
|
720
|
+
// Try format: file:line: KIND: message OR file:line: KIND message
|
|
721
|
+
const match = line.match(/^(.+?):(\d+):\s*(TODO|FIXME|HACK|XXX|NOTE|WARN(?:ING)?)[:\s]+(.*)$/i);
|
|
722
|
+
if (match) {
|
|
723
|
+
results.push({
|
|
724
|
+
file: match[1],
|
|
725
|
+
line: parseInt(match[2], 10),
|
|
726
|
+
kind: match[3].toUpperCase(),
|
|
727
|
+
text: match[4].trim(),
|
|
728
|
+
});
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
return results;
|
|
732
|
+
}
|
|
733
|
+
/** Find @Deprecated symbols in the project */
|
|
734
|
+
async deprecated() {
|
|
735
|
+
if (this.indexDisabled || this.indexOversized)
|
|
736
|
+
return [];
|
|
737
|
+
await this.ensureIndex();
|
|
738
|
+
try {
|
|
739
|
+
const result = await this.exec(['deprecated'], 15000);
|
|
740
|
+
return this.parseDeprecatedText(result);
|
|
741
|
+
}
|
|
742
|
+
catch (err) {
|
|
743
|
+
console.error(`[token-pilot] ast-index deprecated failed: ${err instanceof Error ? err.message : err}`);
|
|
744
|
+
return [];
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
parseDeprecatedText(text) {
|
|
748
|
+
const results = [];
|
|
749
|
+
for (const line of text.split('\n')) {
|
|
750
|
+
if (!line.trim())
|
|
751
|
+
continue;
|
|
752
|
+
// Try format: kind name (file:line) - message OR kind name (file:line)
|
|
753
|
+
const match = line.match(/^(\w+)\s+(\S+)\s+\((.+?):(\d+)\)(?:\s*-\s*(.+))?$/);
|
|
754
|
+
if (match) {
|
|
755
|
+
results.push({
|
|
756
|
+
kind: match[1],
|
|
757
|
+
name: match[2],
|
|
758
|
+
file: match[3],
|
|
759
|
+
line: parseInt(match[4], 10),
|
|
760
|
+
message: match[5]?.trim(),
|
|
761
|
+
});
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
return results;
|
|
765
|
+
}
|
|
766
|
+
/** Find symbols with a specific annotation/decorator */
|
|
767
|
+
async annotations(name) {
|
|
768
|
+
if (this.indexDisabled || this.indexOversized)
|
|
769
|
+
return [];
|
|
770
|
+
await this.ensureIndex();
|
|
771
|
+
try {
|
|
772
|
+
const result = await this.exec(['annotations', name], 15000);
|
|
773
|
+
return this.parseAnnotationsText(result, name);
|
|
774
|
+
}
|
|
775
|
+
catch (err) {
|
|
776
|
+
console.error(`[token-pilot] ast-index annotations failed: ${err instanceof Error ? err.message : err}`);
|
|
777
|
+
return [];
|
|
778
|
+
}
|
|
779
|
+
}
|
|
780
|
+
parseAnnotationsText(text, annotationName) {
|
|
781
|
+
const results = [];
|
|
782
|
+
for (const line of text.split('\n')) {
|
|
783
|
+
if (!line.trim())
|
|
784
|
+
continue;
|
|
785
|
+
// Try format: kind name (file:line) OR @Annotation kind name (file:line)
|
|
786
|
+
const match = line.match(/^(?:@\S+\s+)?(\w+)\s+(\S+)\s+\((.+?):(\d+)\)$/);
|
|
787
|
+
if (match) {
|
|
788
|
+
results.push({
|
|
789
|
+
kind: match[1],
|
|
790
|
+
name: match[2],
|
|
791
|
+
file: match[3],
|
|
792
|
+
line: parseInt(match[4], 10),
|
|
793
|
+
annotation: annotationName,
|
|
794
|
+
});
|
|
795
|
+
}
|
|
796
|
+
}
|
|
797
|
+
return results;
|
|
798
|
+
}
|
|
799
|
+
/** Trigger incremental index update (called by file watcher after edits) */
|
|
800
|
+
async incrementalUpdate() {
|
|
801
|
+
if (!this.indexed || this.indexDisabled || this.indexOversized)
|
|
802
|
+
return;
|
|
803
|
+
try {
|
|
804
|
+
await this.exec(['update'], 15000);
|
|
805
|
+
}
|
|
806
|
+
catch (err) {
|
|
807
|
+
console.error(`[token-pilot] ast-index incremental update failed: ${err instanceof Error ? err.message : err}`);
|
|
808
|
+
}
|
|
809
|
+
}
|
|
810
|
+
// --- Utility methods ---
|
|
639
811
|
isAvailable() {
|
|
640
812
|
return this.binaryPath !== null;
|
|
641
813
|
}
|
|
@@ -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
|
package/dist/core/validation.js
CHANGED
|
@@ -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
|
-
|
|
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;
|
package/dist/git/file-watcher.js
CHANGED
|
@@ -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
|
-
|
|
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,24 @@ 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
|
|
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
|
+
'• Dead code detection → find_unused (finds unreferenced symbols)',
|
|
197
|
+
'• Code issues → code_audit (TODOs, deprecated, structural patterns like bare except:)',
|
|
198
|
+
'• Text pattern search/counting → Grep (regex, count mode)',
|
|
199
|
+
'• Security audit → Grep for: password, token, secret, credential, hardcoded, api_key, TODO.*security',
|
|
200
|
+
'• Deep dive into specific code → read_symbol (after finding issues)',
|
|
201
|
+
'',
|
|
190
202
|
'WORKFLOW: project_overview → smart_read → read_symbol → read_for_edit → edit → read_diff',
|
|
191
203
|
].join('\n'),
|
|
192
204
|
});
|
|
@@ -340,6 +352,25 @@ export async function createServer(projectRoot, options) {
|
|
|
340
352
|
},
|
|
341
353
|
},
|
|
342
354
|
},
|
|
355
|
+
{
|
|
356
|
+
name: 'code_audit',
|
|
357
|
+
description: 'Find code quality issues: TODO/FIXME comments, deprecated symbols, structural code patterns (bare except:, print() calls). Use for project-wide audits.',
|
|
358
|
+
inputSchema: {
|
|
359
|
+
type: 'object',
|
|
360
|
+
properties: {
|
|
361
|
+
check: {
|
|
362
|
+
type: 'string',
|
|
363
|
+
enum: ['pattern', 'todo', 'deprecated', 'annotations', 'all'],
|
|
364
|
+
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)',
|
|
365
|
+
},
|
|
366
|
+
pattern: { type: 'string', description: 'Code pattern for check="pattern". ast-grep syntax: "except:" finds bare excepts, "print($$$ARGS)" finds print calls.' },
|
|
367
|
+
name: { type: 'string', description: 'Decorator/annotation name for check="annotations". Example: "Deprecated", "Controller"' },
|
|
368
|
+
lang: { type: 'string', description: 'Language filter for check="pattern" (e.g., "python", "typescript")' },
|
|
369
|
+
limit: { type: 'number', description: 'Max results (default: 50)' },
|
|
370
|
+
},
|
|
371
|
+
required: ['check'],
|
|
372
|
+
},
|
|
373
|
+
},
|
|
343
374
|
],
|
|
344
375
|
}));
|
|
345
376
|
// Helper: get real full-file token count for honest analytics
|
|
@@ -475,6 +506,13 @@ export async function createServer(projectRoot, options) {
|
|
|
475
506
|
analytics.record({ tool: 'find_unused', path: unusedArgs.module ?? 'all', tokensReturned: estimateTokens(unusedText), tokensWouldBe: estimateTokens(unusedText), timestamp: Date.now() });
|
|
476
507
|
return unusedResult;
|
|
477
508
|
}
|
|
509
|
+
case 'code_audit': {
|
|
510
|
+
const auditArgs = validateCodeAuditArgs(args);
|
|
511
|
+
const auditResult = await handleCodeAudit(auditArgs, projectRoot, astIndex);
|
|
512
|
+
const auditText = auditResult.content[0]?.text ?? '';
|
|
513
|
+
analytics.record({ tool: 'code_audit', path: auditArgs.check, tokensReturned: estimateTokens(auditText), tokensWouldBe: estimateTokens(auditText), timestamp: Date.now() });
|
|
514
|
+
return auditResult;
|
|
515
|
+
}
|
|
478
516
|
default:
|
|
479
517
|
return {
|
|
480
518
|
content: [{ type: 'text', text: `Unknown tool: ${name}` }],
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "token-pilot",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.8.1",
|
|
4
|
+
"description": "Save 80% tokens when AI reads code — MCP server for token-efficient code navigation, AST-aware structural reading instead of dumping full files into context window",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
7
7
|
"bin": {
|
|
@@ -28,18 +28,24 @@
|
|
|
28
28
|
"prepublishOnly": "npm run build && chmod +x dist/index.js"
|
|
29
29
|
},
|
|
30
30
|
"keywords": [
|
|
31
|
-
"
|
|
32
|
-
"token",
|
|
33
|
-
"
|
|
34
|
-
"
|
|
35
|
-
"
|
|
36
|
-
"
|
|
37
|
-
"
|
|
38
|
-
"ai",
|
|
39
|
-
"coding-assistant",
|
|
31
|
+
"token-savings",
|
|
32
|
+
"token-reduction",
|
|
33
|
+
"context-window",
|
|
34
|
+
"save-tokens",
|
|
35
|
+
"reduce-tokens",
|
|
36
|
+
"token-efficient",
|
|
37
|
+
"token-economy",
|
|
40
38
|
"context-optimization",
|
|
39
|
+
"fewer-tokens",
|
|
40
|
+
"mcp",
|
|
41
|
+
"mcp-server",
|
|
41
42
|
"model-context-protocol",
|
|
42
|
-
"
|
|
43
|
+
"ast",
|
|
44
|
+
"code-reading",
|
|
45
|
+
"code-navigation",
|
|
46
|
+
"smart-read",
|
|
47
|
+
"ai-coding",
|
|
48
|
+
"llm-tools"
|
|
43
49
|
],
|
|
44
50
|
"repository": {
|
|
45
51
|
"type": "git",
|
|
@@ -69,5 +75,8 @@
|
|
|
69
75
|
"ast-index": {
|
|
70
76
|
"optional": true
|
|
71
77
|
}
|
|
78
|
+
},
|
|
79
|
+
"optionalDependencies": {
|
|
80
|
+
"@ast-grep/cli": "^0.41.0"
|
|
72
81
|
}
|
|
73
82
|
}
|