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.
Files changed (34) hide show
  1. package/CHANGELOG.md +33 -0
  2. package/dist/config/defaults.js +5 -0
  3. package/dist/core/context-registry.d.ts +4 -0
  4. package/dist/core/context-registry.js +13 -0
  5. package/dist/core/validation.d.ts +6 -0
  6. package/dist/core/validation.js +25 -2
  7. package/dist/formatters/structure.d.ts +1 -0
  8. package/dist/formatters/structure.js +48 -2
  9. package/dist/handlers/csv-sections.d.ts +35 -0
  10. package/dist/handlers/csv-sections.js +129 -0
  11. package/dist/handlers/find-usages.js +65 -13
  12. package/dist/handlers/json-sections.d.ts +17 -0
  13. package/dist/handlers/json-sections.js +66 -0
  14. package/dist/handlers/markdown-sections.d.ts +15 -0
  15. package/dist/handlers/markdown-sections.js +49 -0
  16. package/dist/handlers/non-code.js +48 -11
  17. package/dist/handlers/read-for-edit.d.ts +4 -1
  18. package/dist/handlers/read-for-edit.js +113 -2
  19. package/dist/handlers/read-section.d.ts +12 -0
  20. package/dist/handlers/read-section.js +96 -0
  21. package/dist/handlers/read-symbol.d.ts +1 -0
  22. package/dist/handlers/read-symbol.js +20 -4
  23. package/dist/handlers/smart-read-many.js +9 -0
  24. package/dist/handlers/smart-read.d.ts +1 -0
  25. package/dist/handlers/smart-read.js +57 -0
  26. package/dist/handlers/symbol-display-constants.d.ts +10 -0
  27. package/dist/handlers/symbol-display-constants.js +10 -0
  28. package/dist/handlers/yaml-sections.d.ts +17 -0
  29. package/dist/handlers/yaml-sections.js +46 -0
  30. package/dist/server/tool-definitions.d.ts +161 -16
  31. package/dist/server/tool-definitions.js +53 -16
  32. package/dist/server.js +19 -2
  33. package/dist/types.d.ts +6 -0
  34. 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
- // Extract headings
200
- for (const line of content.split('\n')) {
201
- const match = line.match(/^(#{1,6})\s+(.+)/);
202
- if (match) {
203
- const level = match[1].length;
204
- const indent = ' '.repeat(level - 1);
205
- lines.push(`${indent}${match[2]}`);
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): Promise<{
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
- const MAX_SYMBOL_LINES = 300;
52
- const MAX_FULL_LINES = 500;
53
- const HEAD = 50;
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