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
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Markdown section parser — shared helper for section-aware tools.
|
|
3
|
+
* Parses heading structure with line ranges for targeted reading.
|
|
4
|
+
*/
|
|
5
|
+
export function parseMarkdownSections(content) {
|
|
6
|
+
if (!content.trim())
|
|
7
|
+
return [];
|
|
8
|
+
const lines = content.split('\n');
|
|
9
|
+
const headings = [];
|
|
10
|
+
for (let i = 0; i < lines.length; i++) {
|
|
11
|
+
const match = lines[i].match(/^(#{1,6})\s+(.+)/);
|
|
12
|
+
if (match) {
|
|
13
|
+
headings.push({
|
|
14
|
+
heading: match[2].trim(),
|
|
15
|
+
level: match[1].length,
|
|
16
|
+
line: i + 1,
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
if (headings.length === 0)
|
|
21
|
+
return [];
|
|
22
|
+
const sections = [];
|
|
23
|
+
for (let i = 0; i < headings.length; i++) {
|
|
24
|
+
const current = headings[i];
|
|
25
|
+
let endLine = lines.length;
|
|
26
|
+
for (let j = i + 1; j < headings.length; j++) {
|
|
27
|
+
if (headings[j].level <= current.level) {
|
|
28
|
+
endLine = headings[j].line - 1;
|
|
29
|
+
break;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
sections.push({
|
|
33
|
+
heading: current.heading,
|
|
34
|
+
level: current.level,
|
|
35
|
+
startLine: current.line,
|
|
36
|
+
endLine,
|
|
37
|
+
lineCount: endLine - current.line + 1,
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
return sections;
|
|
41
|
+
}
|
|
42
|
+
export function findSection(sections, heading) {
|
|
43
|
+
const normalized = heading.replace(/^#+\s*/, '').trim().toLowerCase();
|
|
44
|
+
return sections.find(s => s.heading.toLowerCase() === normalized);
|
|
45
|
+
}
|
|
46
|
+
export function extractSectionContent(lines, section) {
|
|
47
|
+
return lines.slice(section.startLine - 1, section.endLine).join('\n');
|
|
48
|
+
}
|
|
49
|
+
//# sourceMappingURL=markdown-sections.js.map
|
|
@@ -2,12 +2,16 @@ import { readFile } from 'node:fs/promises';
|
|
|
2
2
|
import { extname } from 'node:path';
|
|
3
3
|
import { estimateTokens } from '../core/token-estimator.js';
|
|
4
4
|
import { resolveSafePath } from '../core/validation.js';
|
|
5
|
+
import { parseMarkdownSections } from './markdown-sections.js';
|
|
6
|
+
import { parseYamlSections } from './yaml-sections.js';
|
|
7
|
+
import { parseJsonSections } from './json-sections.js';
|
|
8
|
+
import { parseCsvOutline, formatCsvOutline } from './csv-sections.js';
|
|
5
9
|
/**
|
|
6
10
|
* Detect if a file is a non-code structured file (JSON, YAML, Markdown, etc.)
|
|
7
11
|
*/
|
|
8
12
|
export function isNonCodeStructured(filePath) {
|
|
9
13
|
const ext = extname(filePath).toLowerCase();
|
|
10
|
-
return ['.json', '.yaml', '.yml', '.md', '.markdown', '.toml'].includes(ext);
|
|
14
|
+
return ['.json', '.yaml', '.yml', '.md', '.markdown', '.toml', '.csv'].includes(ext);
|
|
11
15
|
}
|
|
12
16
|
/**
|
|
13
17
|
* Generate a structural summary for non-code files.
|
|
@@ -37,6 +41,9 @@ export async function handleNonCodeRead(filePath, projectRoot, contextRegistry,
|
|
|
37
41
|
case '.toml':
|
|
38
42
|
summary = summarizeToml(filePath, content, lines.length);
|
|
39
43
|
break;
|
|
44
|
+
case '.csv':
|
|
45
|
+
summary = summarizeCsv(filePath, content, lines.length);
|
|
46
|
+
break;
|
|
40
47
|
default:
|
|
41
48
|
return null;
|
|
42
49
|
}
|
|
@@ -87,14 +94,34 @@ function summarizeJson(filePath, content, lineCount) {
|
|
|
87
94
|
catch {
|
|
88
95
|
lines.push('(Invalid JSON — parse error)');
|
|
89
96
|
}
|
|
97
|
+
// Add section line ranges for navigation
|
|
98
|
+
const jsonSections = parseJsonSections(content);
|
|
99
|
+
if (jsonSections.length > 0) {
|
|
100
|
+
lines.push('');
|
|
101
|
+
lines.push('SECTION RANGES:');
|
|
102
|
+
for (const sec of jsonSections) {
|
|
103
|
+
lines.push(` "${sec.heading}" [L${sec.startLine}-${sec.endLine}] (${sec.lineCount} lines)`);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
lines.push('');
|
|
107
|
+
lines.push(`HINT: Use read_section("${filePath}", heading="<key>") to load a specific section.`);
|
|
90
108
|
return lines.join('\n');
|
|
91
109
|
}
|
|
92
110
|
function summarizeYaml(filePath, content, lineCount) {
|
|
93
111
|
const lines = [
|
|
94
112
|
`FILE: ${filePath} (${lineCount} lines, YAML)`,
|
|
95
113
|
'',
|
|
96
|
-
'STRUCTURE:',
|
|
97
114
|
];
|
|
115
|
+
// Add top-level section overview with line ranges
|
|
116
|
+
const yamlSections = parseYamlSections(content);
|
|
117
|
+
if (yamlSections.length > 0) {
|
|
118
|
+
lines.push('SECTIONS:');
|
|
119
|
+
for (const sec of yamlSections) {
|
|
120
|
+
lines.push(` ${sec.heading}: [L${sec.startLine}-${sec.endLine}] (${sec.lineCount} lines)`);
|
|
121
|
+
}
|
|
122
|
+
lines.push('');
|
|
123
|
+
}
|
|
124
|
+
lines.push('STRUCTURE:');
|
|
98
125
|
const rawLines = content.split('\n');
|
|
99
126
|
const roots = [];
|
|
100
127
|
// Stack tracks parent nodes at each indent level
|
|
@@ -159,6 +186,8 @@ function summarizeYaml(filePath, content, lineCount) {
|
|
|
159
186
|
formatYamlNode(root, lines, 1, 3); // max 3 levels deep
|
|
160
187
|
}
|
|
161
188
|
}
|
|
189
|
+
lines.push('');
|
|
190
|
+
lines.push(`HINT: Use read_section("${filePath}", heading="<key>") to load a specific section.`);
|
|
162
191
|
return lines.join('\n');
|
|
163
192
|
}
|
|
164
193
|
function formatYamlNode(node, lines, depth, maxDepth) {
|
|
@@ -194,16 +223,17 @@ function summarizeMarkdown(filePath, content, lineCount) {
|
|
|
194
223
|
const lines = [
|
|
195
224
|
`FILE: ${filePath} (${lineCount} lines, Markdown)`,
|
|
196
225
|
'',
|
|
197
|
-
'TABLE OF CONTENTS:',
|
|
198
226
|
];
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
227
|
+
const sections = parseMarkdownSections(content);
|
|
228
|
+
if (sections.length === 0) {
|
|
229
|
+
lines.push('(No headings found)');
|
|
230
|
+
return lines.join('\n');
|
|
231
|
+
}
|
|
232
|
+
lines.push('SECTIONS:');
|
|
233
|
+
for (const sec of sections) {
|
|
234
|
+
const indent = ' '.repeat(sec.level);
|
|
235
|
+
const hashes = '#'.repeat(sec.level);
|
|
236
|
+
lines.push(`${indent}${hashes} ${sec.heading} [L${sec.startLine}-${sec.endLine}] (${sec.lineCount} lines)`);
|
|
207
237
|
}
|
|
208
238
|
// Count code blocks
|
|
209
239
|
const codeBlocks = (content.match(/```/g) || []).length / 2;
|
|
@@ -211,6 +241,9 @@ function summarizeMarkdown(filePath, content, lineCount) {
|
|
|
211
241
|
lines.push('');
|
|
212
242
|
lines.push(`Code blocks: ${Math.floor(codeBlocks)}`);
|
|
213
243
|
}
|
|
244
|
+
lines.push('');
|
|
245
|
+
lines.push(`HINT: Use read_section("${filePath}", heading="<name>") to load a specific section.`);
|
|
246
|
+
lines.push(` Use read_for_edit("${filePath}", section="<name>") for edit context.`);
|
|
214
247
|
return lines.join('\n');
|
|
215
248
|
}
|
|
216
249
|
function summarizeToml(filePath, content, lineCount) {
|
|
@@ -228,4 +261,8 @@ function summarizeToml(filePath, content, lineCount) {
|
|
|
228
261
|
}
|
|
229
262
|
return lines.join('\n');
|
|
230
263
|
}
|
|
264
|
+
function summarizeCsv(filePath, content, lineCount) {
|
|
265
|
+
const outline = parseCsvOutline(content);
|
|
266
|
+
return formatCsvOutline(filePath, outline, lineCount);
|
|
267
|
+
}
|
|
231
268
|
//# sourceMappingURL=non-code.js.map
|
|
@@ -11,8 +11,11 @@ export interface ReadForEditArgs {
|
|
|
11
11
|
include_callers?: boolean;
|
|
12
12
|
include_tests?: boolean;
|
|
13
13
|
include_changes?: boolean;
|
|
14
|
+
section?: string;
|
|
14
15
|
}
|
|
15
|
-
export declare function handleReadForEdit(args: ReadForEditArgs, projectRoot: string, symbolResolver: SymbolResolver, fileCache: FileCache, contextRegistry: ContextRegistry, astIndex: AstIndexClient
|
|
16
|
+
export declare function handleReadForEdit(args: ReadForEditArgs, projectRoot: string, symbolResolver: SymbolResolver, fileCache: FileCache, contextRegistry: ContextRegistry, astIndex: AstIndexClient, options?: {
|
|
17
|
+
actionableHints?: boolean;
|
|
18
|
+
}): Promise<{
|
|
16
19
|
content: Array<{
|
|
17
20
|
type: 'text';
|
|
18
21
|
text: string;
|
|
@@ -2,15 +2,121 @@ import { readFile, stat, access } from 'node:fs/promises';
|
|
|
2
2
|
import { execFile } from 'node:child_process';
|
|
3
3
|
import { promisify } from 'node:util';
|
|
4
4
|
import { createHash } from 'node:crypto';
|
|
5
|
-
import { relative, join } from 'node:path';
|
|
5
|
+
import { relative, join, extname } from 'node:path';
|
|
6
|
+
import { parseMarkdownSections, findSection, extractSectionContent } from './markdown-sections.js';
|
|
7
|
+
import { parseYamlSections, findYamlSection, extractYamlSectionContent } from './yaml-sections.js';
|
|
8
|
+
import { parseJsonSections, findJsonSection, extractJsonSectionContent } from './json-sections.js';
|
|
9
|
+
import { parseCsvOutline, parseCsvSectionSpec, extractCsvSectionContent } from './csv-sections.js';
|
|
6
10
|
import { estimateTokens } from '../core/token-estimator.js';
|
|
7
11
|
import { resolveSafePath } from '../core/validation.js';
|
|
8
12
|
import { assessConfidence, formatConfidence } from '../core/confidence.js';
|
|
9
13
|
const execFileAsync = promisify(execFile);
|
|
10
14
|
const DEFAULT_CONTEXT = 5;
|
|
11
|
-
export async function handleReadForEdit(args, projectRoot, symbolResolver, fileCache, contextRegistry, astIndex) {
|
|
15
|
+
export async function handleReadForEdit(args, projectRoot, symbolResolver, fileCache, contextRegistry, astIndex, options) {
|
|
12
16
|
const absPath = resolveSafePath(projectRoot, args.path);
|
|
13
17
|
const ctx = args.context ?? DEFAULT_CONTEXT;
|
|
18
|
+
// Section mode: markdown/YAML section extraction for edit
|
|
19
|
+
if (args.section) {
|
|
20
|
+
const ext = extname(absPath).toLowerCase();
|
|
21
|
+
const supportedExts = new Set(['.md', '.markdown', '.yaml', '.yml', '.json', '.csv']);
|
|
22
|
+
if (!supportedExts.has(ext)) {
|
|
23
|
+
return {
|
|
24
|
+
content: [{
|
|
25
|
+
type: 'text',
|
|
26
|
+
text: `"section" parameter only works with Markdown, YAML, or JSON files. Got: ${ext}. Use "symbol" for code files.`,
|
|
27
|
+
}],
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
const fileContent = await readFile(absPath, 'utf-8');
|
|
31
|
+
const fileLines = fileContent.split('\n');
|
|
32
|
+
// Cache file in fileCache for read_diff baseline
|
|
33
|
+
if (!fileCache.get(absPath)) {
|
|
34
|
+
const fileStat = await stat(absPath);
|
|
35
|
+
const hash = createHash('sha256').update(fileContent).digest('hex');
|
|
36
|
+
const language = ext === '.csv' ? 'csv' : ext === '.json' ? 'json' : (ext === '.md' || ext === '.markdown') ? 'markdown' : 'yaml';
|
|
37
|
+
fileCache.set(absPath, {
|
|
38
|
+
structure: { path: absPath, language, meta: { lines: fileLines.length, bytes: fileContent.length, lastModified: fileStat.mtimeMs, contentHash: hash }, imports: [], exports: [], symbols: [] },
|
|
39
|
+
content: fileContent, lines: fileLines, mtime: fileStat.mtimeMs, hash, lastAccess: Date.now(),
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
let sectionResult = null;
|
|
43
|
+
if (ext === '.md' || ext === '.markdown') {
|
|
44
|
+
const sections = parseMarkdownSections(fileContent);
|
|
45
|
+
const section = findSection(sections, args.section);
|
|
46
|
+
if (!section) {
|
|
47
|
+
const available = sections.map(s => s.heading).join(', ');
|
|
48
|
+
return {
|
|
49
|
+
content: [{
|
|
50
|
+
type: 'text',
|
|
51
|
+
text: `Section "${args.section}" not found in ${args.path}.\nAvailable: ${available}`,
|
|
52
|
+
}],
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
const hashes = '#'.repeat(section.level);
|
|
56
|
+
sectionResult = { ...section, rawContent: extractSectionContent(fileLines, section), label: `${hashes} ${section.heading}` };
|
|
57
|
+
}
|
|
58
|
+
else if (ext === '.yaml' || ext === '.yml') {
|
|
59
|
+
const sections = parseYamlSections(fileContent);
|
|
60
|
+
const section = findYamlSection(sections, args.section);
|
|
61
|
+
if (!section) {
|
|
62
|
+
const available = sections.map(s => s.heading).join(', ');
|
|
63
|
+
return {
|
|
64
|
+
content: [{
|
|
65
|
+
type: 'text',
|
|
66
|
+
text: `Section "${args.section}" not found in ${args.path}.\nAvailable: ${available}`,
|
|
67
|
+
}],
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
sectionResult = { ...section, rawContent: extractYamlSectionContent(fileLines, section), label: section.heading };
|
|
71
|
+
}
|
|
72
|
+
else if (ext === '.json') {
|
|
73
|
+
const sections = parseJsonSections(fileContent);
|
|
74
|
+
const section = findJsonSection(sections, args.section);
|
|
75
|
+
if (!section) {
|
|
76
|
+
const available = sections.map(s => s.heading).join(', ');
|
|
77
|
+
return {
|
|
78
|
+
content: [{
|
|
79
|
+
type: 'text',
|
|
80
|
+
text: `Section "${args.section}" not found in ${args.path}.\nAvailable: ${available}`,
|
|
81
|
+
}],
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
sectionResult = { ...section, rawContent: extractJsonSectionContent(fileLines, section), label: section.heading };
|
|
85
|
+
}
|
|
86
|
+
else if (ext === '.csv') {
|
|
87
|
+
const outline = parseCsvOutline(fileContent);
|
|
88
|
+
const section = parseCsvSectionSpec(args.section, outline.rowCount);
|
|
89
|
+
if (!section) {
|
|
90
|
+
return {
|
|
91
|
+
content: [{
|
|
92
|
+
type: 'text',
|
|
93
|
+
text: `Invalid section "${args.section}" for CSV. Use: rows:1-50 or row:5\nTotal rows: ${outline.rowCount}`,
|
|
94
|
+
}],
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
sectionResult = { ...section, rawContent: extractCsvSectionContent(fileLines, section), label: section.heading };
|
|
98
|
+
}
|
|
99
|
+
if (!sectionResult) {
|
|
100
|
+
return { content: [{ type: 'text', text: `Unsupported file type: ${ext}` }] };
|
|
101
|
+
}
|
|
102
|
+
const outputLines = [
|
|
103
|
+
`FILE: ${args.path}`,
|
|
104
|
+
`EDIT SECTION: ${sectionResult.label} [L${sectionResult.startLine}-${sectionResult.endLine}] (${sectionResult.lineCount} lines)`,
|
|
105
|
+
'',
|
|
106
|
+
sectionResult.rawContent,
|
|
107
|
+
'',
|
|
108
|
+
`AFTER EDIT: Use read_diff("${args.path}") to verify changes (90% cheaper than re-reading).`,
|
|
109
|
+
];
|
|
110
|
+
const output = outputLines.join('\n');
|
|
111
|
+
const tokens = estimateTokens(output);
|
|
112
|
+
contextRegistry.trackLoad(absPath, {
|
|
113
|
+
type: 'range',
|
|
114
|
+
startLine: sectionResult.startLine,
|
|
115
|
+
endLine: sectionResult.endLine,
|
|
116
|
+
tokens,
|
|
117
|
+
});
|
|
118
|
+
return { content: [{ type: 'text', text: output }] };
|
|
119
|
+
}
|
|
14
120
|
// Get file content — also cache for read_diff baseline
|
|
15
121
|
const cached = fileCache.get(absPath);
|
|
16
122
|
let lines;
|
|
@@ -220,6 +326,11 @@ export async function handleReadForEdit(args, projectRoot, symbolResolver, fileC
|
|
|
220
326
|
astAvailable: true,
|
|
221
327
|
});
|
|
222
328
|
outputLines.push(formatConfidence(confidenceMeta));
|
|
329
|
+
// Add post-edit hint (config-gated)
|
|
330
|
+
if (options?.actionableHints !== false) {
|
|
331
|
+
outputLines.push('');
|
|
332
|
+
outputLines.push(`AFTER EDIT: Use read_diff("${args.path}") to verify changes (90% cheaper than re-reading the file).`);
|
|
333
|
+
}
|
|
223
334
|
const output = outputLines.join('\n');
|
|
224
335
|
const tokens = estimateTokens(output);
|
|
225
336
|
// Track in context
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { ContextRegistry } from '../core/context-registry.js';
|
|
2
|
+
export interface ReadSectionArgs {
|
|
3
|
+
path: string;
|
|
4
|
+
heading: string;
|
|
5
|
+
}
|
|
6
|
+
export declare function handleReadSection(args: ReadSectionArgs, projectRoot: string, contextRegistry: ContextRegistry): Promise<{
|
|
7
|
+
content: Array<{
|
|
8
|
+
type: 'text';
|
|
9
|
+
text: string;
|
|
10
|
+
}>;
|
|
11
|
+
}>;
|
|
12
|
+
//# sourceMappingURL=read-section.d.ts.map
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { readFile } from 'node:fs/promises';
|
|
2
|
+
import { extname } from 'node:path';
|
|
3
|
+
import { estimateTokens } from '../core/token-estimator.js';
|
|
4
|
+
import { resolveSafePath } from '../core/validation.js';
|
|
5
|
+
import { parseMarkdownSections, findSection, extractSectionContent } from './markdown-sections.js';
|
|
6
|
+
import { parseYamlSections, findYamlSection, extractYamlSectionContent } from './yaml-sections.js';
|
|
7
|
+
import { parseJsonSections, findJsonSection, extractJsonSectionContent } from './json-sections.js';
|
|
8
|
+
import { parseCsvOutline, parseCsvSectionSpec, extractCsvSectionContent } from './csv-sections.js';
|
|
9
|
+
export async function handleReadSection(args, projectRoot, contextRegistry) {
|
|
10
|
+
const absPath = resolveSafePath(projectRoot, args.path);
|
|
11
|
+
const ext = extname(absPath).toLowerCase();
|
|
12
|
+
const content = await readFile(absPath, 'utf-8');
|
|
13
|
+
const lines = content.split('\n');
|
|
14
|
+
// Dispatch to format-specific parser
|
|
15
|
+
let sectionData = null;
|
|
16
|
+
if (ext === '.md' || ext === '.markdown') {
|
|
17
|
+
const sections = parseMarkdownSections(content);
|
|
18
|
+
const section = findSection(sections, args.heading);
|
|
19
|
+
if (!section) {
|
|
20
|
+
return {
|
|
21
|
+
content: [{
|
|
22
|
+
type: 'text',
|
|
23
|
+
text: `Section "${args.heading}" not found in ${args.path}.\nAvailable sections: ${sections.map(s => s.heading).join(', ')}`,
|
|
24
|
+
}],
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
const hashes = '#'.repeat(section.level);
|
|
28
|
+
sectionData = { ...section, content: extractSectionContent(lines, section), label: `${hashes} ${section.heading}` };
|
|
29
|
+
}
|
|
30
|
+
else if (ext === '.yaml' || ext === '.yml') {
|
|
31
|
+
const sections = parseYamlSections(content);
|
|
32
|
+
const section = findYamlSection(sections, args.heading);
|
|
33
|
+
if (!section) {
|
|
34
|
+
return {
|
|
35
|
+
content: [{
|
|
36
|
+
type: 'text',
|
|
37
|
+
text: `Section "${args.heading}" not found in ${args.path}.\nAvailable sections: ${sections.map(s => s.heading).join(', ')}`,
|
|
38
|
+
}],
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
sectionData = { ...section, content: extractYamlSectionContent(lines, section), label: section.heading };
|
|
42
|
+
}
|
|
43
|
+
else if (ext === '.json') {
|
|
44
|
+
const sections = parseJsonSections(content);
|
|
45
|
+
const section = findJsonSection(sections, args.heading);
|
|
46
|
+
if (!section) {
|
|
47
|
+
return {
|
|
48
|
+
content: [{
|
|
49
|
+
type: 'text',
|
|
50
|
+
text: `Section "${args.heading}" not found in ${args.path}.\nAvailable sections: ${sections.map(s => s.heading).join(', ')}`,
|
|
51
|
+
}],
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
sectionData = { ...section, content: extractJsonSectionContent(lines, section), label: section.heading };
|
|
55
|
+
}
|
|
56
|
+
else if (ext === '.csv') {
|
|
57
|
+
const outline = parseCsvOutline(content);
|
|
58
|
+
const section = parseCsvSectionSpec(args.heading, outline.rowCount);
|
|
59
|
+
if (!section) {
|
|
60
|
+
return {
|
|
61
|
+
content: [{
|
|
62
|
+
type: 'text',
|
|
63
|
+
text: `Invalid section spec "${args.heading}" for CSV. Use format: rows:1-50 or row:5\nTotal rows: ${outline.rowCount}`,
|
|
64
|
+
}],
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
sectionData = { ...section, content: extractCsvSectionContent(lines, section), label: section.heading };
|
|
68
|
+
}
|
|
69
|
+
else {
|
|
70
|
+
return {
|
|
71
|
+
content: [{
|
|
72
|
+
type: 'text',
|
|
73
|
+
text: `read_section supports: .md, .yaml, .yml, .json, .csv. Got: ${ext}`,
|
|
74
|
+
}],
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
const outputLines = [
|
|
78
|
+
`FILE: ${args.path}`,
|
|
79
|
+
`SECTION: ${sectionData.label} [L${sectionData.startLine}-${sectionData.endLine}] (${sectionData.lineCount} lines)`,
|
|
80
|
+
'',
|
|
81
|
+
sectionData.content,
|
|
82
|
+
'',
|
|
83
|
+
`HINT: Use read_for_edit("${args.path}", section="${sectionData.heading}") for edit context.`,
|
|
84
|
+
'CONTEXT TRACKED.',
|
|
85
|
+
];
|
|
86
|
+
const output = outputLines.join('\n');
|
|
87
|
+
const tokens = estimateTokens(output);
|
|
88
|
+
contextRegistry.trackLoad(absPath, {
|
|
89
|
+
type: 'range',
|
|
90
|
+
startLine: sectionData.startLine,
|
|
91
|
+
endLine: sectionData.endLine,
|
|
92
|
+
tokens,
|
|
93
|
+
});
|
|
94
|
+
return { content: [{ type: 'text', text: output }] };
|
|
95
|
+
}
|
|
96
|
+
//# sourceMappingURL=read-section.js.map
|
|
@@ -8,6 +8,7 @@ export interface ReadSymbolArgs {
|
|
|
8
8
|
context_before?: number;
|
|
9
9
|
context_after?: number;
|
|
10
10
|
show?: 'full' | 'head' | 'tail' | 'outline';
|
|
11
|
+
include_edit_context?: boolean;
|
|
11
12
|
}
|
|
12
13
|
export declare function handleReadSymbol(args: ReadSymbolArgs, projectRoot: string, symbolResolver: SymbolResolver, fileCache: FileCache, contextRegistry: ContextRegistry, astIndex?: AstIndexClient, advisoryReminders?: boolean): Promise<{
|
|
13
14
|
content: Array<{
|
|
@@ -2,6 +2,7 @@ import { readFile } from 'node:fs/promises';
|
|
|
2
2
|
import { estimateTokens } from '../core/token-estimator.js';
|
|
3
3
|
import { resolveSafePath } from '../core/validation.js';
|
|
4
4
|
import { assessConfidence, formatConfidence } from '../core/confidence.js';
|
|
5
|
+
import { MAX_SYMBOL_LINES, MAX_FULL_LINES, SYMBOL_HEAD_LINES, SYMBOL_TAIL_LINES } from './symbol-display-constants.js';
|
|
5
6
|
export async function handleReadSymbol(args, projectRoot, symbolResolver, fileCache, contextRegistry, astIndex, advisoryReminders = true) {
|
|
6
7
|
const absPath = resolveSafePath(projectRoot, args.path);
|
|
7
8
|
// Get file content
|
|
@@ -48,10 +49,9 @@ export async function handleReadSymbol(args, projectRoot, symbolResolver, fileCa
|
|
|
48
49
|
const loc = `[L${resolved.startLine}-${resolved.endLine}]`;
|
|
49
50
|
const lineCount = resolved.endLine - resolved.startLine + 1;
|
|
50
51
|
// Show mode: control how large symbols are displayed
|
|
51
|
-
|
|
52
|
-
const
|
|
53
|
-
const
|
|
54
|
-
const TAIL = 30;
|
|
52
|
+
// Display constants from shared module (imported at top level)
|
|
53
|
+
const HEAD = SYMBOL_HEAD_LINES;
|
|
54
|
+
const TAIL = SYMBOL_TAIL_LINES;
|
|
55
55
|
let displaySource = source;
|
|
56
56
|
let truncated = false;
|
|
57
57
|
// Determine effective show mode
|
|
@@ -119,6 +119,22 @@ export async function handleReadSymbol(args, projectRoot, symbolResolver, fileCa
|
|
|
119
119
|
// Build full output including tracking message, THEN estimate tokens
|
|
120
120
|
outputLines.push('');
|
|
121
121
|
outputLines.push('CONTEXT TRACKED: This symbol is now in your context.');
|
|
122
|
+
// Optional: append raw edit context to save a separate read_for_edit call (small symbols only)
|
|
123
|
+
if (args.include_edit_context) {
|
|
124
|
+
const rawLines = lines.slice(resolved.startLine - 1, resolved.endLine);
|
|
125
|
+
const maxEditLines = 60;
|
|
126
|
+
if (rawLines.length <= maxEditLines) {
|
|
127
|
+
outputLines.push('');
|
|
128
|
+
outputLines.push('EDIT_CONTEXT (raw — copy directly as old_string):');
|
|
129
|
+
outputLines.push('```');
|
|
130
|
+
outputLines.push(rawLines.join('\n'));
|
|
131
|
+
outputLines.push('```');
|
|
132
|
+
}
|
|
133
|
+
else {
|
|
134
|
+
outputLines.push('');
|
|
135
|
+
outputLines.push(`EDIT_CONTEXT: Symbol too large (${rawLines.length} lines). Use read_for_edit("${args.path}", symbol="${args.symbol}") instead.`);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
122
138
|
const output = outputLines.join('\n');
|
|
123
139
|
const tokens = estimateTokens(output);
|
|
124
140
|
// Track
|
|
@@ -23,6 +23,15 @@ export async function handleSmartReadMany(args, projectRoot, astIndex, fileCache
|
|
|
23
23
|
for (let i = 0; i < uniquePaths.length; i += BATCH_CONCURRENCY) {
|
|
24
24
|
const batch = uniquePaths.slice(i, i + BATCH_CONCURRENCY);
|
|
25
25
|
const settled = await Promise.allSettled(batch.map(async (path) => {
|
|
26
|
+
// Per-file dedup: if file is in context and unchanged, return compact reminder
|
|
27
|
+
const absPath = resolveSafePath(projectRoot, path);
|
|
28
|
+
const cachedFile = fileCache.get(absPath);
|
|
29
|
+
if (cachedFile && contextRegistry.hasAnyLoaded(absPath) && !contextRegistry.isStale(absPath, cachedFile.hash)) {
|
|
30
|
+
const reminder = contextRegistry.compactReminder(absPath, cachedFile.structure?.symbols ?? []);
|
|
31
|
+
const reminderText = reminder || `FILE: ${path} (already in context, unchanged)`;
|
|
32
|
+
const fullTokens = await estimateFullFileTokens(projectRoot, path);
|
|
33
|
+
return { path, text: reminderText + `\nFor full re-read: smart_read("${path}")`, fullTokens };
|
|
34
|
+
}
|
|
26
35
|
const result = await handleSmartRead({ path }, projectRoot, astIndex, fileCache, contextRegistry, config);
|
|
27
36
|
const text = result.content[0]?.text ?? '';
|
|
28
37
|
const fullTokens = await estimateFullFileTokens(projectRoot, path);
|
|
@@ -8,6 +8,7 @@ export interface SmartReadArgs {
|
|
|
8
8
|
show_docs?: boolean;
|
|
9
9
|
show_references?: boolean;
|
|
10
10
|
depth?: number;
|
|
11
|
+
scope?: 'full' | 'nav' | 'exports';
|
|
11
12
|
}
|
|
12
13
|
export declare function handleSmartRead(args: SmartReadArgs, projectRoot: string, astIndex: AstIndexClient, fileCache: FileCache, contextRegistry: ContextRegistry, config: TokenPilotConfig): Promise<{
|
|
13
14
|
content: Array<{
|
|
@@ -6,6 +6,7 @@ import { resolveSafePath } from '../core/validation.js';
|
|
|
6
6
|
import { isNonCodeStructured, handleNonCodeRead } from './non-code.js';
|
|
7
7
|
import { parseTypeScriptRegex } from '../ast-index/regex-parser.js';
|
|
8
8
|
import { buildFileStructure } from '../ast-index/enricher.js';
|
|
9
|
+
import { formatDuration } from '../core/format-duration.js';
|
|
9
10
|
const TS_JS_EXTENSIONS = new Set(['ts', 'tsx', 'js', 'jsx', 'mjs', 'cjs']);
|
|
10
11
|
import { assessConfidence, formatConfidence } from '../core/confidence.js';
|
|
11
12
|
export async function handleSmartRead(args, projectRoot, astIndex, fileCache, contextRegistry, config) {
|
|
@@ -111,12 +112,64 @@ export async function handleSmartRead(args, projectRoot, astIndex, fileCache, co
|
|
|
111
112
|
return { content: [{ type: 'text', text: reminder }] };
|
|
112
113
|
}
|
|
113
114
|
}
|
|
115
|
+
// 5b. Auto-delta: file changed since last load, recently loaded
|
|
116
|
+
if (config.smartRead.autoDelta?.enabled &&
|
|
117
|
+
previouslyLoaded &&
|
|
118
|
+
contextRegistry.isStale(absPath, cached.hash)) {
|
|
119
|
+
const loadedAt = contextRegistry.getLoadedAt(absPath);
|
|
120
|
+
if (loadedAt !== undefined && (Date.now() - loadedAt) < (config.smartRead.autoDelta.maxAgeSec ?? 120) * 1000) {
|
|
121
|
+
const prevNames = contextRegistry.getSymbolNames(absPath) ?? [];
|
|
122
|
+
const prevSet = new Set(prevNames);
|
|
123
|
+
const currentNames = cached.structure.symbols.map(s => s.name);
|
|
124
|
+
const currentSet = new Set(currentNames);
|
|
125
|
+
const added = currentNames.filter(n => !prevSet.has(n));
|
|
126
|
+
const removed = prevNames.filter(n => !currentSet.has(n));
|
|
127
|
+
const unchanged = currentNames.filter(n => prevSet.has(n));
|
|
128
|
+
const elapsed = formatDuration(Date.now() - loadedAt);
|
|
129
|
+
const deltaLines = [
|
|
130
|
+
`FILE: ${args.path} (DELTA — changed since last read ${elapsed} ago)`,
|
|
131
|
+
'',
|
|
132
|
+
];
|
|
133
|
+
if (added.length > 0) {
|
|
134
|
+
deltaLines.push('ADDED:');
|
|
135
|
+
for (const name of added) {
|
|
136
|
+
const sym = cached.structure.symbols.find(s => s.name === name);
|
|
137
|
+
if (sym)
|
|
138
|
+
deltaLines.push(` ${sym.kind} ${sym.signature} [L${sym.location.startLine}-${sym.location.endLine}]`);
|
|
139
|
+
}
|
|
140
|
+
deltaLines.push('');
|
|
141
|
+
}
|
|
142
|
+
if (removed.length > 0) {
|
|
143
|
+
deltaLines.push(`REMOVED: ${removed.join(', ')}`);
|
|
144
|
+
deltaLines.push('');
|
|
145
|
+
}
|
|
146
|
+
if (unchanged.length > 0) {
|
|
147
|
+
deltaLines.push(`UNCHANGED (${unchanged.length} symbols):`);
|
|
148
|
+
for (const name of unchanged.slice(0, 15)) {
|
|
149
|
+
const sym = cached.structure.symbols.find(s => s.name === name);
|
|
150
|
+
if (sym)
|
|
151
|
+
deltaLines.push(` ${sym.name} [L${sym.location.startLine}-${sym.location.endLine}]`);
|
|
152
|
+
}
|
|
153
|
+
if (unchanged.length > 15)
|
|
154
|
+
deltaLines.push(` ... and ${unchanged.length - 15} more`);
|
|
155
|
+
deltaLines.push('');
|
|
156
|
+
}
|
|
157
|
+
deltaLines.push(`HINT: For full re-read: smart_read("${args.path}", scope="full")`);
|
|
158
|
+
const deltaText = deltaLines.join('\n');
|
|
159
|
+
const deltaTokens = estimateTokens(deltaText);
|
|
160
|
+
contextRegistry.trackLoad(absPath, { type: 'structure', startLine: 1, endLine: cached.structure.meta.lines, tokens: deltaTokens });
|
|
161
|
+
contextRegistry.setContentHash(absPath, cached.hash);
|
|
162
|
+
contextRegistry.trackStructureSymbols(absPath, currentNames);
|
|
163
|
+
return { content: [{ type: 'text', text: deltaText }] };
|
|
164
|
+
}
|
|
165
|
+
}
|
|
114
166
|
// 6. Format output
|
|
115
167
|
const output = formatOutline(cached.structure, {
|
|
116
168
|
showImports: args.show_imports ?? config.display.showImports,
|
|
117
169
|
showDocs: args.show_docs ?? config.display.showDocs,
|
|
118
170
|
showDependencyHints: config.smartRead.showDependencyHints,
|
|
119
171
|
maxDepth: args.depth ?? config.display.maxDepth,
|
|
172
|
+
scope: args.scope ?? 'full',
|
|
120
173
|
});
|
|
121
174
|
// 6b. Adaptive fallback: if outline is not significantly smaller than raw, return raw
|
|
122
175
|
const structureTokens = estimateTokens(output);
|
|
@@ -129,6 +182,9 @@ export async function handleSmartRead(args, projectRoot, astIndex, fileCache, co
|
|
|
129
182
|
tokens: fullTokens,
|
|
130
183
|
});
|
|
131
184
|
contextRegistry.setContentHash(absPath, cached.hash);
|
|
185
|
+
if (cached.structure.symbols.length > 0) {
|
|
186
|
+
contextRegistry.trackStructureSymbols(absPath, cached.structure.symbols.map(s => s.name));
|
|
187
|
+
}
|
|
132
188
|
return {
|
|
133
189
|
content: [{
|
|
134
190
|
type: 'text',
|
|
@@ -148,6 +204,7 @@ export async function handleSmartRead(args, projectRoot, astIndex, fileCache, co
|
|
|
148
204
|
tokens: structureTokens,
|
|
149
205
|
});
|
|
150
206
|
contextRegistry.setContentHash(absPath, cached.hash);
|
|
207
|
+
contextRegistry.trackStructureSymbols(absPath, cached.structure.symbols.map(s => s.name));
|
|
151
208
|
// 9. Confidence metadata
|
|
152
209
|
const confidenceMeta = assessConfidence({
|
|
153
210
|
symbolResolved: (cached.structure.symbols?.length ?? 0) > 0,
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/** Shared display constants for symbol rendering (read_symbol, read_symbols). */
|
|
2
|
+
/** Symbols larger than this get auto-truncated to outline mode. */
|
|
3
|
+
export declare const MAX_SYMBOL_LINES = 300;
|
|
4
|
+
/** Symbols larger than this are never shown in full, even with show="full". */
|
|
5
|
+
export declare const MAX_FULL_LINES = 500;
|
|
6
|
+
/** Lines shown from the start in head/outline mode. */
|
|
7
|
+
export declare const SYMBOL_HEAD_LINES = 50;
|
|
8
|
+
/** Lines shown from the end in tail/outline mode. */
|
|
9
|
+
export declare const SYMBOL_TAIL_LINES = 30;
|
|
10
|
+
//# sourceMappingURL=symbol-display-constants.d.ts.map
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/** Shared display constants for symbol rendering (read_symbol, read_symbols). */
|
|
2
|
+
/** Symbols larger than this get auto-truncated to outline mode. */
|
|
3
|
+
export const MAX_SYMBOL_LINES = 300;
|
|
4
|
+
/** Symbols larger than this are never shown in full, even with show="full". */
|
|
5
|
+
export const MAX_FULL_LINES = 500;
|
|
6
|
+
/** Lines shown from the start in head/outline mode. */
|
|
7
|
+
export const SYMBOL_HEAD_LINES = 50;
|
|
8
|
+
/** Lines shown from the end in tail/outline mode. */
|
|
9
|
+
export const SYMBOL_TAIL_LINES = 30;
|
|
10
|
+
//# sourceMappingURL=symbol-display-constants.js.map
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* YAML section parser — parses top-level keys with line ranges.
|
|
3
|
+
*/
|
|
4
|
+
export interface YamlSection {
|
|
5
|
+
heading: string;
|
|
6
|
+
startLine: number;
|
|
7
|
+
endLine: number;
|
|
8
|
+
lineCount: number;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Parse YAML into sections based on top-level keys.
|
|
12
|
+
* A top-level key is a key at indent level 0.
|
|
13
|
+
*/
|
|
14
|
+
export declare function parseYamlSections(content: string): YamlSection[];
|
|
15
|
+
export declare function findYamlSection(sections: YamlSection[], heading: string): YamlSection | undefined;
|
|
16
|
+
export declare function extractYamlSectionContent(lines: string[], section: YamlSection): string;
|
|
17
|
+
//# sourceMappingURL=yaml-sections.d.ts.map
|