token-pilot 0.16.2 → 0.18.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 +33 -0
- package/dist/config/defaults.js +5 -0
- package/dist/core/context-registry.d.ts +4 -0
- package/dist/core/context-registry.js +13 -0
- package/dist/core/validation.d.ts +6 -0
- package/dist/core/validation.js +25 -2
- package/dist/formatters/structure.d.ts +1 -0
- package/dist/formatters/structure.js +48 -2
- package/dist/handlers/csv-sections.d.ts +35 -0
- package/dist/handlers/csv-sections.js +129 -0
- package/dist/handlers/find-usages.js +65 -13
- package/dist/handlers/json-sections.d.ts +17 -0
- package/dist/handlers/json-sections.js +66 -0
- package/dist/handlers/markdown-sections.d.ts +15 -0
- package/dist/handlers/markdown-sections.js +49 -0
- package/dist/handlers/non-code.js +48 -11
- package/dist/handlers/read-for-edit.d.ts +4 -1
- package/dist/handlers/read-for-edit.js +113 -2
- package/dist/handlers/read-section.d.ts +12 -0
- package/dist/handlers/read-section.js +96 -0
- package/dist/handlers/read-symbol.d.ts +1 -0
- package/dist/handlers/read-symbol.js +20 -4
- package/dist/handlers/smart-read-many.js +9 -0
- package/dist/handlers/smart-read.d.ts +1 -0
- package/dist/handlers/smart-read.js +57 -0
- package/dist/handlers/symbol-display-constants.d.ts +10 -0
- package/dist/handlers/symbol-display-constants.js +10 -0
- package/dist/handlers/yaml-sections.d.ts +17 -0
- package/dist/handlers/yaml-sections.js +46 -0
- package/dist/server/tool-definitions.d.ts +161 -16
- package/dist/server/tool-definitions.js +53 -16
- package/dist/server.js +19 -2
- package/dist/types.d.ts +6 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,39 @@ 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.18.0] - 2026-04-05
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
- **`read_section` tool** — read a specific section from Markdown, YAML, JSON, or CSV files. Markdown: by heading name. YAML/JSON: by top-level key. CSV: by row range (`rows:1-50`). Much cheaper than reading the whole file.
|
|
12
|
+
- **`read_for_edit` section parameter** — prepare edit context for non-code file sections. Works with all 4 formats.
|
|
13
|
+
- **Markdown outline with line ranges** — `smart_read` on `.md` files now shows `[L5-20]` ranges and hints for `read_section`.
|
|
14
|
+
- **YAML/JSON section ranges** — `smart_read` on `.yaml`/`.json` shows top-level key ranges.
|
|
15
|
+
- **CSV smart_read** — shows columns, row count, sample rows, and hints for row-range reading.
|
|
16
|
+
- **4 section parsers** — `markdown-sections.ts`, `yaml-sections.ts`, `json-sections.ts`, `csv-sections.ts`.
|
|
17
|
+
|
|
18
|
+
### Changed
|
|
19
|
+
- **20 tools** (was 19) — added `read_section`.
|
|
20
|
+
- **492 tests** (was 441).
|
|
21
|
+
|
|
22
|
+
### Fixed
|
|
23
|
+
- `npm audit` — resolved brace-expansion, path-to-regexp, picomatch vulnerabilities.
|
|
24
|
+
|
|
25
|
+
## [0.17.0] - 2026-04-02
|
|
26
|
+
|
|
27
|
+
### Added
|
|
28
|
+
- **`smart_read` scope parameter** — `scope="nav"` returns names + line ranges only (2-3x smaller), `scope="exports"` shows only public API. Default `scope="full"` unchanged.
|
|
29
|
+
- **`smart_read` auto-delta** — when a file changed since last load (within 120s), shows ADDED/REMOVED/UNCHANGED symbols instead of full re-read. Config: `smartRead.autoDelta.enabled`.
|
|
30
|
+
- **`read_symbol` include_edit_context** — optional `include_edit_context=true` appends raw code block (max 60 lines) to save a separate `read_for_edit` call. Large symbols fall back to `read_for_edit`.
|
|
31
|
+
- **`find_usages` mode=list** — compact `file:line` output for initial discovery, 5-10x smaller than full mode.
|
|
32
|
+
- **`smart_read_many` per-file dedup** — skips files already in context and unchanged, returns compact reminder instead.
|
|
33
|
+
- **Actionable hints** — `read_for_edit` suggests `read_diff` after editing. Config: `display.actionableHints`.
|
|
34
|
+
- **`symbol-display-constants.ts`** — shared display constants for symbol rendering.
|
|
35
|
+
|
|
36
|
+
### Changed
|
|
37
|
+
- **441 tests** (was 427) — new tests for scope, list mode, include_edit_context, dedup.
|
|
38
|
+
- **MCP instructions** updated with scope/mode/include_edit_context guidance.
|
|
39
|
+
- **find_usages context rendering** — sequential instead of concurrent to prevent shared cache race condition.
|
|
40
|
+
|
|
8
41
|
## [0.16.1] - 2026-03-21
|
|
9
42
|
|
|
10
43
|
### Added
|
package/dist/config/defaults.js
CHANGED
|
@@ -12,6 +12,10 @@ export const DEFAULT_CONFIG = {
|
|
|
12
12
|
smallFileThreshold: 200,
|
|
13
13
|
showDependencyHints: true,
|
|
14
14
|
advisoryReminders: true,
|
|
15
|
+
autoDelta: {
|
|
16
|
+
enabled: true,
|
|
17
|
+
maxAgeSec: 120,
|
|
18
|
+
},
|
|
15
19
|
},
|
|
16
20
|
git: {
|
|
17
21
|
watchHead: true,
|
|
@@ -33,6 +37,7 @@ export const DEFAULT_CONFIG = {
|
|
|
33
37
|
showReferences: false,
|
|
34
38
|
maxDepth: 2,
|
|
35
39
|
showTokenSavings: true,
|
|
40
|
+
actionableHints: true,
|
|
36
41
|
},
|
|
37
42
|
contextMode: {
|
|
38
43
|
enabled: 'auto',
|
|
@@ -40,6 +40,10 @@ export declare class ContextRegistry {
|
|
|
40
40
|
entries: ContextEntry[];
|
|
41
41
|
};
|
|
42
42
|
estimateTokens(): number;
|
|
43
|
+
trackStructureSymbols(path: string, symbolNames: string[]): void;
|
|
44
|
+
getSymbolNames(path: string): string[] | undefined;
|
|
45
|
+
/** Get the timestamp when a file was last loaded into context. */
|
|
46
|
+
getLoadedAt(path: string): number | undefined;
|
|
43
47
|
invalidateByGitDiff(changedFiles: string[]): void;
|
|
44
48
|
}
|
|
45
49
|
//# sourceMappingURL=context-registry.d.ts.map
|
|
@@ -182,6 +182,19 @@ export class ContextRegistry {
|
|
|
182
182
|
}
|
|
183
183
|
return total;
|
|
184
184
|
}
|
|
185
|
+
trackStructureSymbols(path, symbolNames) {
|
|
186
|
+
const entry = this.entries.get(path);
|
|
187
|
+
if (entry) {
|
|
188
|
+
entry.symbolNames = symbolNames;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
getSymbolNames(path) {
|
|
192
|
+
return this.entries.get(path)?.symbolNames;
|
|
193
|
+
}
|
|
194
|
+
/** Get the timestamp when a file was last loaded into context. */
|
|
195
|
+
getLoadedAt(path) {
|
|
196
|
+
return this.entries.get(path)?.loadedAt;
|
|
197
|
+
}
|
|
185
198
|
invalidateByGitDiff(changedFiles) {
|
|
186
199
|
for (const file of changedFiles) {
|
|
187
200
|
this.entries.delete(file);
|
|
@@ -59,6 +59,7 @@ export interface FindUsagesArgs {
|
|
|
59
59
|
limit?: number;
|
|
60
60
|
lang?: string;
|
|
61
61
|
context_lines?: number;
|
|
62
|
+
mode?: 'full' | 'list';
|
|
62
63
|
}
|
|
63
64
|
export declare function validateFindUsagesArgs(args: unknown): FindUsagesArgs;
|
|
64
65
|
/**
|
|
@@ -79,6 +80,7 @@ export declare function validateReadForEditArgs(args: unknown): {
|
|
|
79
80
|
include_callers?: boolean;
|
|
80
81
|
include_tests?: boolean;
|
|
81
82
|
include_changes?: boolean;
|
|
83
|
+
section?: string;
|
|
82
84
|
};
|
|
83
85
|
/**
|
|
84
86
|
* Validate related_files arguments.
|
|
@@ -154,6 +156,10 @@ export interface TestSummaryArgs {
|
|
|
154
156
|
timeout?: number;
|
|
155
157
|
}
|
|
156
158
|
export declare function validateTestSummaryArgs(args: unknown): TestSummaryArgs;
|
|
159
|
+
export declare function validateReadSectionArgs(args: unknown): {
|
|
160
|
+
path: string;
|
|
161
|
+
heading: string;
|
|
162
|
+
};
|
|
157
163
|
/** Detect roots that would cause ast-index to scan the entire filesystem */
|
|
158
164
|
export declare function isDangerousRoot(root: string): boolean;
|
|
159
165
|
//# sourceMappingURL=validation.d.ts.map
|
package/dist/core/validation.js
CHANGED
|
@@ -160,6 +160,14 @@ export function validateFindUsagesArgs(args) {
|
|
|
160
160
|
if (context_lines !== undefined && (context_lines < 0 || context_lines > 10)) {
|
|
161
161
|
throw new Error('"context_lines" must be between 0 and 10.');
|
|
162
162
|
}
|
|
163
|
+
let mode;
|
|
164
|
+
if (a.mode !== undefined && a.mode !== null) {
|
|
165
|
+
const validModes = ['full', 'list'];
|
|
166
|
+
if (typeof a.mode !== 'string' || !validModes.includes(a.mode)) {
|
|
167
|
+
throw new Error(`"mode" must be one of: ${validModes.join(', ')}`);
|
|
168
|
+
}
|
|
169
|
+
mode = a.mode;
|
|
170
|
+
}
|
|
163
171
|
return {
|
|
164
172
|
symbol: a.symbol,
|
|
165
173
|
scope: optionalString(a.scope, 'scope'),
|
|
@@ -167,6 +175,7 @@ export function validateFindUsagesArgs(args) {
|
|
|
167
175
|
limit,
|
|
168
176
|
lang: optionalString(a.lang, 'lang'),
|
|
169
177
|
context_lines,
|
|
178
|
+
mode,
|
|
170
179
|
};
|
|
171
180
|
}
|
|
172
181
|
/**
|
|
@@ -219,8 +228,8 @@ export function validateReadForEditArgs(args) {
|
|
|
219
228
|
if (typeof a.path !== 'string' || a.path.length === 0) {
|
|
220
229
|
throw new Error('Required parameter "path" must be a non-empty string.');
|
|
221
230
|
}
|
|
222
|
-
if (!a.symbol && !a.line && (!Array.isArray(a.symbols) || a.symbols.length === 0)) {
|
|
223
|
-
throw new Error('Either "symbol", "symbols", or "
|
|
231
|
+
if (!a.symbol && !a.line && (!Array.isArray(a.symbols) || a.symbols.length === 0) && !a.section) {
|
|
232
|
+
throw new Error('Either "symbol", "symbols", "line", or "section" must be provided.');
|
|
224
233
|
}
|
|
225
234
|
// Validate symbols array (batch mode)
|
|
226
235
|
let symbols;
|
|
@@ -247,6 +256,7 @@ export function validateReadForEditArgs(args) {
|
|
|
247
256
|
include_callers: optionalBool(a.include_callers, 'include_callers'),
|
|
248
257
|
include_tests: optionalBool(a.include_tests, 'include_tests'),
|
|
249
258
|
include_changes: optionalBool(a.include_changes, 'include_changes'),
|
|
259
|
+
section: optionalString(a.section, 'section'),
|
|
250
260
|
};
|
|
251
261
|
}
|
|
252
262
|
/**
|
|
@@ -444,6 +454,19 @@ export function validateTestSummaryArgs(args) {
|
|
|
444
454
|
}
|
|
445
455
|
return { command: a.command, runner, timeout };
|
|
446
456
|
}
|
|
457
|
+
export function validateReadSectionArgs(args) {
|
|
458
|
+
if (!args || typeof args !== 'object') {
|
|
459
|
+
throw new Error('Arguments must be an object.');
|
|
460
|
+
}
|
|
461
|
+
const a = args;
|
|
462
|
+
if (typeof a.path !== 'string' || a.path.length === 0) {
|
|
463
|
+
throw new Error('Required parameter "path" must be a non-empty string.');
|
|
464
|
+
}
|
|
465
|
+
if (typeof a.heading !== 'string' || a.heading.length === 0) {
|
|
466
|
+
throw new Error('Required parameter "heading" must be a non-empty string.');
|
|
467
|
+
}
|
|
468
|
+
return { path: a.path, heading: a.heading };
|
|
469
|
+
}
|
|
447
470
|
/** Detect roots that would cause ast-index to scan the entire filesystem */
|
|
448
471
|
export function isDangerousRoot(root) {
|
|
449
472
|
const normalized = root.replace(/\/+$/, '') || '/';
|
|
@@ -2,13 +2,48 @@
|
|
|
2
2
|
* Format a FileStructure as token-optimized text for LLMs.
|
|
3
3
|
*/
|
|
4
4
|
export function formatOutline(structure, options = {}) {
|
|
5
|
-
const { showImports = true, showDocs = true, showDependencyHints = true, maxDepth = 2, } = options;
|
|
5
|
+
const { showImports = true, showDocs = true, showDependencyHints = true, maxDepth = 2, scope = 'full', } = options;
|
|
6
6
|
const lines = [];
|
|
7
7
|
// Header
|
|
8
8
|
const sizeKB = (structure.meta.bytes / 1024).toFixed(1);
|
|
9
|
-
|
|
9
|
+
const scopeLabel = scope !== 'full' ? `, ${scope} mode` : '';
|
|
10
|
+
lines.push(`FILE: ${structure.path} (${structure.meta.lines} lines, ${sizeKB}KB${scopeLabel})`);
|
|
10
11
|
lines.push(`LANGUAGE: ${structure.language}`);
|
|
11
12
|
lines.push('');
|
|
13
|
+
// NAV scope: compact names + line ranges only
|
|
14
|
+
if (scope === 'nav') {
|
|
15
|
+
lines.push('SYMBOLS:');
|
|
16
|
+
for (const sym of structure.symbols) {
|
|
17
|
+
formatSymbolNav(sym, lines, 1, maxDepth);
|
|
18
|
+
}
|
|
19
|
+
lines.push('');
|
|
20
|
+
lines.push('HINT: Use scope="full" for type signatures and imports. Use read_symbol(path, symbol) to load code.');
|
|
21
|
+
return lines.join('\n');
|
|
22
|
+
}
|
|
23
|
+
// EXPORTS scope: only exported symbols
|
|
24
|
+
if (scope === 'exports') {
|
|
25
|
+
const exportNames = new Set(structure.exports.map(e => e.name));
|
|
26
|
+
const exportedSymbols = structure.symbols.filter(s => exportNames.has(s.name));
|
|
27
|
+
const hiddenCount = structure.symbols.length - exportedSymbols.length;
|
|
28
|
+
lines.push('EXPORTS:');
|
|
29
|
+
for (const exp of structure.exports) {
|
|
30
|
+
const defaultLabel = exp.isDefault ? ' (default)' : '';
|
|
31
|
+
lines.push(` ${exp.kind} ${exp.name}${defaultLabel}`);
|
|
32
|
+
}
|
|
33
|
+
lines.push('');
|
|
34
|
+
lines.push('STRUCTURE:');
|
|
35
|
+
for (const sym of exportedSymbols) {
|
|
36
|
+
formatSymbolTree(sym, lines, 1, maxDepth, showDocs, showDependencyHints);
|
|
37
|
+
}
|
|
38
|
+
lines.push('');
|
|
39
|
+
if (hiddenCount > 0) {
|
|
40
|
+
lines.push(`HINT: ${hiddenCount} non-exported symbol(s) hidden. Use scope="full" to see all symbols.`);
|
|
41
|
+
}
|
|
42
|
+
else {
|
|
43
|
+
lines.push('HINT: Use read_symbol(path, symbol) to load a specific symbol\'s code.');
|
|
44
|
+
}
|
|
45
|
+
return lines.join('\n');
|
|
46
|
+
}
|
|
12
47
|
// Imports
|
|
13
48
|
if (showImports && structure.imports.length > 0) {
|
|
14
49
|
lines.push('IMPORTS:');
|
|
@@ -111,6 +146,17 @@ function formatSymbolTree(sym, lines, depth, maxDepth, showDocs, showDeps, paren
|
|
|
111
146
|
lines.push(`${indent} (${sym.children.length} members — increase depth to see)`);
|
|
112
147
|
}
|
|
113
148
|
}
|
|
149
|
+
function formatSymbolNav(sym, lines, depth, maxDepth) {
|
|
150
|
+
const indent = ' '.repeat(depth);
|
|
151
|
+
const loc = `[L${sym.location.startLine}-${sym.location.endLine}]`;
|
|
152
|
+
const callable = sym.kind === 'function' || sym.kind === 'method' ? '()' : '';
|
|
153
|
+
lines.push(`${indent}${sym.name}${callable} ${loc}`);
|
|
154
|
+
if (depth < maxDepth && sym.children.length > 0) {
|
|
155
|
+
for (const child of sym.children) {
|
|
156
|
+
formatSymbolNav(child, lines, depth + 1, maxDepth);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
114
160
|
/**
|
|
115
161
|
* Detect HTTP route decorators and format as "METHOD /path".
|
|
116
162
|
* Uses standard HTTP verbs — not framework-specific.
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CSV parser — column-aware reading with row/column subsetting.
|
|
3
|
+
*/
|
|
4
|
+
export interface CsvOutline {
|
|
5
|
+
columns: string[];
|
|
6
|
+
rowCount: number;
|
|
7
|
+
sampleRows: string[][];
|
|
8
|
+
}
|
|
9
|
+
export interface CsvSection {
|
|
10
|
+
heading: string;
|
|
11
|
+
startLine: number;
|
|
12
|
+
endLine: number;
|
|
13
|
+
lineCount: number;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Parse CSV into an outline: columns, row count, sample.
|
|
17
|
+
* Simple parser — handles quoted fields with commas.
|
|
18
|
+
*/
|
|
19
|
+
export declare function parseCsvOutline(content: string): CsvOutline;
|
|
20
|
+
/**
|
|
21
|
+
* Parse a row range specification into a CsvSection.
|
|
22
|
+
* Supported formats:
|
|
23
|
+
* "rows:1-50" — row range (1-indexed, refers to data rows, not header)
|
|
24
|
+
* "rows:1-50" with column filter isn't supported at section level
|
|
25
|
+
*/
|
|
26
|
+
export declare function parseCsvSectionSpec(heading: string, totalDataRows: number): CsvSection | null;
|
|
27
|
+
/**
|
|
28
|
+
* Extract CSV rows for a section. Returns header + requested rows.
|
|
29
|
+
*/
|
|
30
|
+
export declare function extractCsvSectionContent(lines: string[], section: CsvSection): string;
|
|
31
|
+
/**
|
|
32
|
+
* Format CSV outline for smart_read output.
|
|
33
|
+
*/
|
|
34
|
+
export declare function formatCsvOutline(filePath: string, outline: CsvOutline, lineCount: number): string;
|
|
35
|
+
//# sourceMappingURL=csv-sections.d.ts.map
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CSV parser — column-aware reading with row/column subsetting.
|
|
3
|
+
*/
|
|
4
|
+
/**
|
|
5
|
+
* Parse CSV into an outline: columns, row count, sample.
|
|
6
|
+
* Simple parser — handles quoted fields with commas.
|
|
7
|
+
*/
|
|
8
|
+
export function parseCsvOutline(content) {
|
|
9
|
+
const lines = content.split('\n').filter(l => l.trim());
|
|
10
|
+
if (lines.length === 0)
|
|
11
|
+
return { columns: [], rowCount: 0, sampleRows: [] };
|
|
12
|
+
const columns = parseCsvRow(lines[0]);
|
|
13
|
+
const dataLines = lines.slice(1);
|
|
14
|
+
const sampleRows = dataLines.slice(0, 5).map(parseCsvRow);
|
|
15
|
+
return {
|
|
16
|
+
columns,
|
|
17
|
+
rowCount: dataLines.length,
|
|
18
|
+
sampleRows,
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Parse a row range specification into a CsvSection.
|
|
23
|
+
* Supported formats:
|
|
24
|
+
* "rows:1-50" — row range (1-indexed, refers to data rows, not header)
|
|
25
|
+
* "rows:1-50" with column filter isn't supported at section level
|
|
26
|
+
*/
|
|
27
|
+
export function parseCsvSectionSpec(heading, totalDataRows) {
|
|
28
|
+
// rows:N-M format
|
|
29
|
+
const rowMatch = heading.match(/^rows?:\s*(\d+)\s*-\s*(\d+)$/i);
|
|
30
|
+
if (rowMatch) {
|
|
31
|
+
const start = Math.max(1, parseInt(rowMatch[1], 10));
|
|
32
|
+
const end = Math.min(totalDataRows, parseInt(rowMatch[2], 10));
|
|
33
|
+
if (start > end || start > totalDataRows)
|
|
34
|
+
return null;
|
|
35
|
+
// +1 for header line offset
|
|
36
|
+
return {
|
|
37
|
+
heading: `rows ${start}-${end}`,
|
|
38
|
+
startLine: start + 1, // +1 because line 1 is header
|
|
39
|
+
endLine: end + 1,
|
|
40
|
+
lineCount: end - start + 1,
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
// Single row number
|
|
44
|
+
const singleMatch = heading.match(/^rows?:\s*(\d+)$/i);
|
|
45
|
+
if (singleMatch) {
|
|
46
|
+
const row = parseInt(singleMatch[1], 10);
|
|
47
|
+
if (row < 1 || row > totalDataRows)
|
|
48
|
+
return null;
|
|
49
|
+
return {
|
|
50
|
+
heading: `row ${row}`,
|
|
51
|
+
startLine: row + 1,
|
|
52
|
+
endLine: row + 1,
|
|
53
|
+
lineCount: 1,
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Extract CSV rows for a section. Returns header + requested rows.
|
|
60
|
+
*/
|
|
61
|
+
export function extractCsvSectionContent(lines, section) {
|
|
62
|
+
const header = lines[0]; // always include header
|
|
63
|
+
const dataRows = lines.slice(section.startLine - 1, section.endLine);
|
|
64
|
+
return [header, ...dataRows].join('\n');
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Parse a single CSV row handling quoted fields.
|
|
68
|
+
*/
|
|
69
|
+
function parseCsvRow(line) {
|
|
70
|
+
const fields = [];
|
|
71
|
+
let current = '';
|
|
72
|
+
let inQuote = false;
|
|
73
|
+
for (let i = 0; i < line.length; i++) {
|
|
74
|
+
const ch = line[i];
|
|
75
|
+
if (inQuote) {
|
|
76
|
+
if (ch === '"') {
|
|
77
|
+
if (i + 1 < line.length && line[i + 1] === '"') {
|
|
78
|
+
current += '"';
|
|
79
|
+
i++; // skip escaped quote
|
|
80
|
+
}
|
|
81
|
+
else {
|
|
82
|
+
inQuote = false;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
else {
|
|
86
|
+
current += ch;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
else {
|
|
90
|
+
if (ch === '"') {
|
|
91
|
+
inQuote = true;
|
|
92
|
+
}
|
|
93
|
+
else if (ch === ',') {
|
|
94
|
+
fields.push(current.trim());
|
|
95
|
+
current = '';
|
|
96
|
+
}
|
|
97
|
+
else {
|
|
98
|
+
current += ch;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
fields.push(current.trim());
|
|
103
|
+
return fields;
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Format CSV outline for smart_read output.
|
|
107
|
+
*/
|
|
108
|
+
export function formatCsvOutline(filePath, outline, lineCount) {
|
|
109
|
+
const lines = [
|
|
110
|
+
`FILE: ${filePath} (${lineCount} lines, CSV)`,
|
|
111
|
+
'',
|
|
112
|
+
`COLUMNS (${outline.columns.length}): ${outline.columns.join(', ')}`,
|
|
113
|
+
`ROWS: ${outline.rowCount}`,
|
|
114
|
+
'',
|
|
115
|
+
];
|
|
116
|
+
if (outline.sampleRows.length > 0) {
|
|
117
|
+
lines.push(`SAMPLE (first ${outline.sampleRows.length} rows):`);
|
|
118
|
+
for (const row of outline.sampleRows) {
|
|
119
|
+
// Format as: col1=val1, col2=val2, ...
|
|
120
|
+
const pairs = outline.columns.map((col, i) => `${col}=${row[i] ?? ''}`);
|
|
121
|
+
lines.push(` ${pairs.join(', ')}`);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
lines.push('');
|
|
125
|
+
lines.push(`HINT: Use read_section("${filePath}", heading="rows:1-50") to load specific rows.`);
|
|
126
|
+
lines.push(` Use read_section("${filePath}", heading="rows:${outline.rowCount}") for last row.`);
|
|
127
|
+
return lines.join('\n');
|
|
128
|
+
}
|
|
129
|
+
//# sourceMappingURL=csv-sections.js.map
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { readFile } from 'node:fs/promises';
|
|
1
|
+
import { readFile, stat } from 'node:fs/promises';
|
|
2
2
|
import { resolve } from 'node:path';
|
|
3
3
|
import { assessConfidence, formatConfidence } from '../core/confidence.js';
|
|
4
4
|
/**
|
|
@@ -53,10 +53,15 @@ function renderSection(title, items) {
|
|
|
53
53
|
lines.push('');
|
|
54
54
|
return lines;
|
|
55
55
|
}
|
|
56
|
+
/** Max unique files to read for context (prevents unbounded I/O). */
|
|
57
|
+
const MAX_CONTEXT_FILES = 30;
|
|
58
|
+
/** Max file size (bytes) to read for context lines. */
|
|
59
|
+
const MAX_CONTEXT_FILE_SIZE = 500_000;
|
|
56
60
|
/**
|
|
57
61
|
* Render a section with surrounding source context lines.
|
|
62
|
+
* Uses shared fileCache to avoid re-reading the same file across sections.
|
|
58
63
|
*/
|
|
59
|
-
async function renderSectionWithContext(title, items, contextLines, projectRoot) {
|
|
64
|
+
async function renderSectionWithContext(title, items, contextLines, projectRoot, fileCache) {
|
|
60
65
|
if (items.length === 0)
|
|
61
66
|
return [];
|
|
62
67
|
const lines = [`${title}:`];
|
|
@@ -66,17 +71,30 @@ async function renderSectionWithContext(title, items, contextLines, projectRoot)
|
|
|
66
71
|
arr.push({ line: item.line, text: item.text });
|
|
67
72
|
byFile.set(item.file, arr);
|
|
68
73
|
}
|
|
74
|
+
let filesRead = 0;
|
|
69
75
|
for (const [file, matches] of byFile) {
|
|
70
76
|
matches.sort((a, b) => a.line - b.line);
|
|
71
77
|
lines.push(` ${file}:`);
|
|
72
|
-
// Read file for context
|
|
78
|
+
// Read file for context (with shared cache and limits)
|
|
73
79
|
let fileLines = null;
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
fileLines = content.split('\n');
|
|
80
|
+
if (fileCache.has(file)) {
|
|
81
|
+
fileLines = fileCache.get(file);
|
|
77
82
|
}
|
|
78
|
-
|
|
79
|
-
|
|
83
|
+
else if (filesRead < MAX_CONTEXT_FILES) {
|
|
84
|
+
try {
|
|
85
|
+
const fileStat = await stat(resolve(projectRoot, file));
|
|
86
|
+
if (fileStat.size <= MAX_CONTEXT_FILE_SIZE) {
|
|
87
|
+
const content = await readFile(resolve(projectRoot, file), 'utf-8');
|
|
88
|
+
fileLines = content.split('\n');
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
catch {
|
|
92
|
+
// File unreadable
|
|
93
|
+
}
|
|
94
|
+
fileCache.set(file, fileLines);
|
|
95
|
+
filesRead++;
|
|
96
|
+
}
|
|
97
|
+
if (!fileLines) {
|
|
80
98
|
for (const m of matches) {
|
|
81
99
|
lines.push(` :${m.line} ${m.text}`);
|
|
82
100
|
}
|
|
@@ -218,6 +236,36 @@ export async function handleFindUsages(args, astIndex, projectRoot) {
|
|
|
218
236
|
meta: { files: [], definitions: 0, imports: 0, usages: 0, total: 0 },
|
|
219
237
|
};
|
|
220
238
|
}
|
|
239
|
+
// ─── List mode — compact file:line output ───
|
|
240
|
+
if (args.mode === 'list') {
|
|
241
|
+
const allItems = [...definitions, ...allImports, ...allUsages];
|
|
242
|
+
const byFile = new Map();
|
|
243
|
+
for (const item of allItems) {
|
|
244
|
+
const arr = byFile.get(item.file) ?? [];
|
|
245
|
+
arr.push(item.line);
|
|
246
|
+
byFile.set(item.file, arr);
|
|
247
|
+
}
|
|
248
|
+
const listLines = [
|
|
249
|
+
`USAGES OF "${args.symbol}" (${allItems.length} matches in ${byFile.size} files):`,
|
|
250
|
+
'',
|
|
251
|
+
];
|
|
252
|
+
for (const [file, fileLines] of byFile) {
|
|
253
|
+
const sorted = [...new Set(fileLines)].sort((a, b) => a - b);
|
|
254
|
+
listLines.push(` ${file}: L${sorted.join(', L')}`);
|
|
255
|
+
}
|
|
256
|
+
listLines.push('');
|
|
257
|
+
listLines.push(`HINT: Use find_usages("${args.symbol}", path="specific_dir/") to narrow, or read_symbol() on specific matches.`);
|
|
258
|
+
return {
|
|
259
|
+
content: [{ type: 'text', text: listLines.join('\n') }],
|
|
260
|
+
meta: {
|
|
261
|
+
files: Array.from(byFile.keys()),
|
|
262
|
+
definitions: definitions.length,
|
|
263
|
+
imports: allImports.length,
|
|
264
|
+
usages: allUsages.length,
|
|
265
|
+
total: allItems.length,
|
|
266
|
+
},
|
|
267
|
+
};
|
|
268
|
+
}
|
|
221
269
|
// Build header with active filters
|
|
222
270
|
const filterHints = [];
|
|
223
271
|
if (args.scope)
|
|
@@ -232,11 +280,11 @@ export async function handleFindUsages(args, astIndex, projectRoot) {
|
|
|
232
280
|
'',
|
|
233
281
|
];
|
|
234
282
|
if (args.context_lines !== undefined && args.context_lines > 0 && projectRoot) {
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
283
|
+
// Shared file cache across sections — sequential to avoid concurrent Map writes
|
|
284
|
+
const contextFileCache = new Map();
|
|
285
|
+
const defSection = await renderSectionWithContext('DEFINITIONS', definitions, args.context_lines, projectRoot, contextFileCache);
|
|
286
|
+
const impSection = await renderSectionWithContext('IMPORTS', allImports, args.context_lines, projectRoot, contextFileCache);
|
|
287
|
+
const useSection = await renderSectionWithContext('USAGES', allUsages, args.context_lines, projectRoot, contextFileCache);
|
|
240
288
|
lines.push(...defSection);
|
|
241
289
|
lines.push(...impSection);
|
|
242
290
|
lines.push(...useSection);
|
|
@@ -247,6 +295,10 @@ export async function handleFindUsages(args, astIndex, projectRoot) {
|
|
|
247
295
|
lines.push(...renderSection('USAGES', allUsages));
|
|
248
296
|
}
|
|
249
297
|
lines.push('HINT: Use read_symbol() or read_range() to load specific results.');
|
|
298
|
+
if (totalCount > 20) {
|
|
299
|
+
lines.push('');
|
|
300
|
+
lines.push(`NARROW: ${totalCount} matches found. Use find_usages("${args.symbol}", path="specific_dir/") to filter by location.`);
|
|
301
|
+
}
|
|
250
302
|
// Confidence metadata
|
|
251
303
|
const confidenceMeta = assessConfidence({
|
|
252
304
|
refsFound: totalCount > 0,
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* JSON section parser — parses top-level keys with line ranges.
|
|
3
|
+
*/
|
|
4
|
+
export interface JsonSection {
|
|
5
|
+
heading: string;
|
|
6
|
+
startLine: number;
|
|
7
|
+
endLine: number;
|
|
8
|
+
lineCount: number;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Parse JSON into sections based on top-level keys.
|
|
12
|
+
* Works with formatted JSON (pretty-printed). For minified JSON, returns empty.
|
|
13
|
+
*/
|
|
14
|
+
export declare function parseJsonSections(content: string): JsonSection[];
|
|
15
|
+
export declare function findJsonSection(sections: JsonSection[], heading: string): JsonSection | undefined;
|
|
16
|
+
export declare function extractJsonSectionContent(lines: string[], section: JsonSection): string;
|
|
17
|
+
//# sourceMappingURL=json-sections.d.ts.map
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* JSON section parser — parses top-level keys with line ranges.
|
|
3
|
+
*/
|
|
4
|
+
/**
|
|
5
|
+
* Parse JSON into sections based on top-level keys.
|
|
6
|
+
* Works with formatted JSON (pretty-printed). For minified JSON, returns empty.
|
|
7
|
+
*/
|
|
8
|
+
export function parseJsonSections(content) {
|
|
9
|
+
if (!content.trim())
|
|
10
|
+
return [];
|
|
11
|
+
const lines = content.split('\n');
|
|
12
|
+
if (lines.length < 3)
|
|
13
|
+
return []; // minified or trivial
|
|
14
|
+
// Find top-level keys: lines matching /^\s{0,2}"key":/ (0-2 spaces indent = top level)
|
|
15
|
+
const topKeys = [];
|
|
16
|
+
// Track brace depth to identify top-level
|
|
17
|
+
let depth = 0;
|
|
18
|
+
let inString = false;
|
|
19
|
+
let lineIdx = 0;
|
|
20
|
+
for (lineIdx = 0; lineIdx < lines.length; lineIdx++) {
|
|
21
|
+
const line = lines[lineIdx];
|
|
22
|
+
// Simple top-level key detection: at depth 1 (inside root object)
|
|
23
|
+
// Match: "key": or "key" : at the beginning of a line (with indent)
|
|
24
|
+
if (depth === 1) {
|
|
25
|
+
const keyMatch = line.match(/^\s*"([^"]+)"\s*:/);
|
|
26
|
+
if (keyMatch) {
|
|
27
|
+
topKeys.push({ key: keyMatch[1], line: lineIdx + 1 });
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
// Track depth (simplified — doesn't handle strings perfectly but good enough for formatted JSON)
|
|
31
|
+
for (let ci = 0; ci < line.length; ci++) {
|
|
32
|
+
const ch = line[ci];
|
|
33
|
+
if (ch === '"' && (ci === 0 || line[ci - 1] !== '\\')) {
|
|
34
|
+
inString = !inString;
|
|
35
|
+
}
|
|
36
|
+
if (!inString) {
|
|
37
|
+
if (ch === '{' || ch === '[')
|
|
38
|
+
depth++;
|
|
39
|
+
if (ch === '}' || ch === ']')
|
|
40
|
+
depth--;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
if (topKeys.length === 0)
|
|
45
|
+
return [];
|
|
46
|
+
const sections = [];
|
|
47
|
+
for (let i = 0; i < topKeys.length; i++) {
|
|
48
|
+
const start = topKeys[i].line;
|
|
49
|
+
const end = i + 1 < topKeys.length ? topKeys[i + 1].line - 1 : lines.length - 1; // -1 to exclude closing }
|
|
50
|
+
sections.push({
|
|
51
|
+
heading: topKeys[i].key,
|
|
52
|
+
startLine: start,
|
|
53
|
+
endLine: end,
|
|
54
|
+
lineCount: end - start + 1,
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
return sections;
|
|
58
|
+
}
|
|
59
|
+
export function findJsonSection(sections, heading) {
|
|
60
|
+
const normalized = heading.trim().toLowerCase();
|
|
61
|
+
return sections.find(s => s.heading.toLowerCase() === normalized);
|
|
62
|
+
}
|
|
63
|
+
export function extractJsonSectionContent(lines, section) {
|
|
64
|
+
return lines.slice(section.startLine - 1, section.endLine).join('\n');
|
|
65
|
+
}
|
|
66
|
+
//# sourceMappingURL=json-sections.js.map
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Markdown section parser — shared helper for section-aware tools.
|
|
3
|
+
* Parses heading structure with line ranges for targeted reading.
|
|
4
|
+
*/
|
|
5
|
+
export interface MarkdownSection {
|
|
6
|
+
heading: string;
|
|
7
|
+
level: number;
|
|
8
|
+
startLine: number;
|
|
9
|
+
endLine: number;
|
|
10
|
+
lineCount: number;
|
|
11
|
+
}
|
|
12
|
+
export declare function parseMarkdownSections(content: string): MarkdownSection[];
|
|
13
|
+
export declare function findSection(sections: MarkdownSection[], heading: string): MarkdownSection | undefined;
|
|
14
|
+
export declare function extractSectionContent(lines: string[], section: MarkdownSection): string;
|
|
15
|
+
//# sourceMappingURL=markdown-sections.d.ts.map
|