token-pilot 0.17.0 → 0.19.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.
@@ -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,6 +11,7 @@ export interface ReadForEditArgs {
11
11
  include_callers?: boolean;
12
12
  include_tests?: boolean;
13
13
  include_changes?: boolean;
14
+ section?: string;
14
15
  }
15
16
  export declare function handleReadForEdit(args: ReadForEditArgs, projectRoot: string, symbolResolver: SymbolResolver, fileCache: FileCache, contextRegistry: ContextRegistry, astIndex: AstIndexClient, options?: {
16
17
  actionableHints?: boolean;
@@ -2,7 +2,11 @@ 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';
@@ -11,6 +15,108 @@ const DEFAULT_CONTEXT = 5;
11
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;
@@ -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
@@ -0,0 +1,14 @@
1
+ export interface SessionSnapshotArgs {
2
+ goal: string;
3
+ confirmed?: string[];
4
+ files?: string[];
5
+ blocked?: string;
6
+ next?: string;
7
+ }
8
+ export declare function handleSessionSnapshot(args: SessionSnapshotArgs): {
9
+ content: {
10
+ type: 'text';
11
+ text: string;
12
+ }[];
13
+ };
14
+ //# sourceMappingURL=session-snapshot.d.ts.map
@@ -0,0 +1,22 @@
1
+ export function handleSessionSnapshot(args) {
2
+ const lines = ['## Session State'];
3
+ lines.push(`**Goal:** ${args.goal}`);
4
+ if (args.confirmed?.length) {
5
+ lines.push('**Confirmed:**');
6
+ for (const item of args.confirmed) {
7
+ lines.push(`- ${item}`);
8
+ }
9
+ }
10
+ if (args.files?.length) {
11
+ lines.push(`**Files:** ${args.files.join(', ')}`);
12
+ }
13
+ if (args.blocked) {
14
+ lines.push(`**Blocked:** ${args.blocked}`);
15
+ }
16
+ if (args.next) {
17
+ lines.push(`**Next:** ${args.next}`);
18
+ }
19
+ const text = lines.join('\n');
20
+ return { content: [{ type: 'text', text }] };
21
+ }
22
+ //# sourceMappingURL=session-snapshot.js.map
@@ -4,6 +4,7 @@ import type { ContextRegistry } from '../core/context-registry.js';
4
4
  import type { TokenPilotConfig } from '../types.js';
5
5
  export interface SmartReadManyArgs {
6
6
  paths: string[];
7
+ max_tokens?: number;
7
8
  }
8
9
  export declare function handleSmartReadMany(args: SmartReadManyArgs, projectRoot: string, astIndex: AstIndexClient, fileCache: FileCache, contextRegistry: ContextRegistry, config: TokenPilotConfig): Promise<{
9
10
  content: Array<{
@@ -32,7 +32,7 @@ export async function handleSmartReadMany(args, projectRoot, astIndex, fileCache
32
32
  const fullTokens = await estimateFullFileTokens(projectRoot, path);
33
33
  return { path, text: reminderText + `\nFor full re-read: smart_read("${path}")`, fullTokens };
34
34
  }
35
- const result = await handleSmartRead({ path }, projectRoot, astIndex, fileCache, contextRegistry, config);
35
+ const result = await handleSmartRead({ path, max_tokens: args.max_tokens }, projectRoot, astIndex, fileCache, contextRegistry, config);
36
36
  const text = result.content[0]?.text ?? '';
37
37
  const fullTokens = await estimateFullFileTokens(projectRoot, path);
38
38
  return { path, text, fullTokens };
@@ -9,6 +9,7 @@ export interface SmartReadArgs {
9
9
  show_references?: boolean;
10
10
  depth?: number;
11
11
  scope?: 'full' | 'nav' | 'exports';
12
+ max_tokens?: number;
12
13
  }
13
14
  export declare function handleSmartRead(args: SmartReadArgs, projectRoot: string, astIndex: AstIndexClient, fileCache: FileCache, contextRegistry: ContextRegistry, config: TokenPilotConfig): Promise<{
14
15
  content: Array<{
@@ -28,31 +28,35 @@ export async function handleSmartRead(args, projectRoot, astIndex, fileCache, co
28
28
  if (lines.length <= config.smartRead.smallFileThreshold) {
29
29
  const hash = createHash('sha256').update(content).digest('hex');
30
30
  const tokens = estimateTokens(content);
31
- contextRegistry.trackLoad(absPath, {
32
- type: 'full',
33
- startLine: 1,
34
- endLine: lines.length,
35
- tokens,
36
- });
37
- contextRegistry.setContentHash(absPath, hash);
38
- // Cache for read_diff baseline (so read_diff works after external edits)
39
- if (!fileCache.get(absPath)) {
40
- const fileStat = await stat(absPath);
41
- fileCache.set(absPath, {
42
- structure: {
43
- path: absPath, language: 'unknown',
44
- meta: { lines: lines.length, bytes: content.length, lastModified: fileStat.mtimeMs, contentHash: hash },
45
- imports: [], exports: [], symbols: [],
46
- },
47
- content, lines, mtime: fileStat.mtimeMs, hash, lastAccess: Date.now(),
31
+ // Budget check: if full content exceeds max_tokens, skip pass-through and use outline path
32
+ if (!args.max_tokens || tokens <= args.max_tokens) {
33
+ contextRegistry.trackLoad(absPath, {
34
+ type: 'full',
35
+ startLine: 1,
36
+ endLine: lines.length,
37
+ tokens,
48
38
  });
39
+ contextRegistry.setContentHash(absPath, hash);
40
+ // Cache for read_diff baseline (so read_diff works after external edits)
41
+ if (!fileCache.get(absPath)) {
42
+ const fileStat = await stat(absPath);
43
+ fileCache.set(absPath, {
44
+ structure: {
45
+ path: absPath, language: 'unknown',
46
+ meta: { lines: lines.length, bytes: content.length, lastModified: fileStat.mtimeMs, contentHash: hash },
47
+ imports: [], exports: [], symbols: [],
48
+ },
49
+ content, lines, mtime: fileStat.mtimeMs, hash, lastAccess: Date.now(),
50
+ });
51
+ }
52
+ return {
53
+ content: [{
54
+ type: 'text',
55
+ text: `FILE: ${args.path} (${lines.length} lines — returned in full, below threshold)\n\n${content}`,
56
+ }],
57
+ };
49
58
  }
50
- return {
51
- content: [{
52
- type: 'text',
53
- text: `FILE: ${args.path} (${lines.length} lines — returned in full, below threshold)\n\n${content}`,
54
- }],
55
- };
59
+ // else: fall through to outline path even for small files
56
60
  }
57
61
  // 3. Check cache
58
62
  let cached = fileCache.get(absPath);
@@ -174,7 +178,7 @@ export async function handleSmartRead(args, projectRoot, astIndex, fileCache, co
174
178
  // 6b. Adaptive fallback: if outline is not significantly smaller than raw, return raw
175
179
  const structureTokens = estimateTokens(output);
176
180
  const fullTokens = estimateTokens(content);
177
- if (structureTokens >= fullTokens * 0.7) {
181
+ if (structureTokens >= fullTokens * 0.7 && (!args.max_tokens || fullTokens <= args.max_tokens)) {
178
182
  contextRegistry.trackLoad(absPath, {
179
183
  type: 'full',
180
184
  startLine: 1,
@@ -192,11 +196,30 @@ export async function handleSmartRead(args, projectRoot, astIndex, fileCache, co
192
196
  }],
193
197
  };
194
198
  }
195
- // 7. Add token savings
199
+ // 7. Budget enforcement: if outline exceeds max_tokens, return compact version
200
+ if (args.max_tokens && structureTokens > args.max_tokens) {
201
+ const symbols = cached.structure.symbols;
202
+ const compactLines = [
203
+ `FILE: ${args.path} (${lines.length} lines — compact, budget: ${args.max_tokens} tokens)`,
204
+ `Imports: ${cached.structure.imports?.length ?? 0} | Exports: ${cached.structure.exports?.length ?? 0} | Symbols: ${symbols.length}`,
205
+ '',
206
+ ];
207
+ for (const sym of symbols) {
208
+ compactLines.push(` ${sym.kind} ${sym.name} [L${sym.location.startLine}-${sym.location.endLine}]`);
209
+ }
210
+ compactLines.push('', `Use read_symbol("${args.path}", "<name>") to drill into any symbol.`);
211
+ const compactText = compactLines.join('\n');
212
+ const compactTokens = estimateTokens(compactText);
213
+ contextRegistry.trackLoad(absPath, { type: 'structure', startLine: 1, endLine: cached.structure.meta.lines, tokens: compactTokens });
214
+ contextRegistry.setContentHash(absPath, cached.hash);
215
+ contextRegistry.trackStructureSymbols(absPath, symbols.map(s => s.name));
216
+ return { content: [{ type: 'text', text: compactText }] };
217
+ }
218
+ // 8. Add token savings
196
219
  const savings = config.display.showTokenSavings
197
220
  ? '\n' + formatSavings(structureTokens, fullTokens)
198
221
  : '';
199
- // 8. Track
222
+ // 9. Track
200
223
  contextRegistry.trackLoad(absPath, {
201
224
  type: 'structure',
202
225
  startLine: 1,
@@ -205,7 +228,7 @@ export async function handleSmartRead(args, projectRoot, astIndex, fileCache, co
205
228
  });
206
229
  contextRegistry.setContentHash(absPath, cached.hash);
207
230
  contextRegistry.trackStructureSymbols(absPath, cached.structure.symbols.map(s => s.name));
208
- // 9. Confidence metadata
231
+ // 10. Confidence metadata
209
232
  const confidenceMeta = assessConfidence({
210
233
  symbolResolved: (cached.structure.symbols?.length ?? 0) > 0,
211
234
  fullFile: false,
@@ -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
@@ -0,0 +1,46 @@
1
+ /**
2
+ * YAML section parser — parses top-level keys with line ranges.
3
+ */
4
+ /**
5
+ * Parse YAML into sections based on top-level keys.
6
+ * A top-level key is a key at indent level 0.
7
+ */
8
+ export function parseYamlSections(content) {
9
+ if (!content.trim())
10
+ return [];
11
+ const lines = content.split('\n');
12
+ const topKeys = [];
13
+ for (let i = 0; i < lines.length; i++) {
14
+ const line = lines[i];
15
+ // Skip comments and empty lines
16
+ if (!line.trim() || line.trim().startsWith('#'))
17
+ continue;
18
+ // Top-level key: starts at column 0, has format "key:" or "key: value"
19
+ const match = line.match(/^([a-zA-Z_][a-zA-Z0-9_.-]*):/);
20
+ if (match) {
21
+ topKeys.push({ key: match[1], line: i + 1 });
22
+ }
23
+ }
24
+ if (topKeys.length === 0)
25
+ return [];
26
+ const sections = [];
27
+ for (let i = 0; i < topKeys.length; i++) {
28
+ const start = topKeys[i].line;
29
+ const end = i + 1 < topKeys.length ? topKeys[i + 1].line - 1 : lines.length;
30
+ sections.push({
31
+ heading: topKeys[i].key,
32
+ startLine: start,
33
+ endLine: end,
34
+ lineCount: end - start + 1,
35
+ });
36
+ }
37
+ return sections;
38
+ }
39
+ export function findYamlSection(sections, heading) {
40
+ const normalized = heading.trim().toLowerCase().replace(/:$/, '');
41
+ return sections.find(s => s.heading.toLowerCase() === normalized);
42
+ }
43
+ export function extractYamlSectionContent(lines, section) {
44
+ return lines.slice(section.startLine - 1, section.endLine).join('\n');
45
+ }
46
+ //# sourceMappingURL=yaml-sections.js.map