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 +11 -0
- package/README.md +8 -4
- package/dist/ast-index/client.d.ts +21 -1
- package/dist/ast-index/client.js +167 -0
- 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 +39 -3
- package/package.json +1 -1
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: **
|
|
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 (
|
|
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":
|
|
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;
|
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;
|
|
@@ -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
|
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,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
|
|
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}` }],
|