token-pilot 0.17.0 → 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 CHANGED
@@ -5,6 +5,23 @@ 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
+
8
25
  ## [0.17.0] - 2026-04-02
9
26
 
10
27
  ### Added
@@ -80,6 +80,7 @@ export declare function validateReadForEditArgs(args: unknown): {
80
80
  include_callers?: boolean;
81
81
  include_tests?: boolean;
82
82
  include_changes?: boolean;
83
+ section?: string;
83
84
  };
84
85
  /**
85
86
  * Validate related_files arguments.
@@ -155,6 +156,10 @@ export interface TestSummaryArgs {
155
156
  timeout?: number;
156
157
  }
157
158
  export declare function validateTestSummaryArgs(args: unknown): TestSummaryArgs;
159
+ export declare function validateReadSectionArgs(args: unknown): {
160
+ path: string;
161
+ heading: string;
162
+ };
158
163
  /** Detect roots that would cause ast-index to scan the entire filesystem */
159
164
  export declare function isDangerousRoot(root: string): boolean;
160
165
  //# sourceMappingURL=validation.d.ts.map
@@ -228,8 +228,8 @@ export function validateReadForEditArgs(args) {
228
228
  if (typeof a.path !== 'string' || a.path.length === 0) {
229
229
  throw new Error('Required parameter "path" must be a non-empty string.');
230
230
  }
231
- if (!a.symbol && !a.line && (!Array.isArray(a.symbols) || a.symbols.length === 0)) {
232
- throw new Error('Either "symbol", "symbols", or "line" must be provided.');
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.');
233
233
  }
234
234
  // Validate symbols array (batch mode)
235
235
  let symbols;
@@ -256,6 +256,7 @@ export function validateReadForEditArgs(args) {
256
256
  include_callers: optionalBool(a.include_callers, 'include_callers'),
257
257
  include_tests: optionalBool(a.include_tests, 'include_tests'),
258
258
  include_changes: optionalBool(a.include_changes, 'include_changes'),
259
+ section: optionalString(a.section, 'section'),
259
260
  };
260
261
  }
261
262
  /**
@@ -453,6 +454,19 @@ export function validateTestSummaryArgs(args) {
453
454
  }
454
455
  return { command: a.command, runner, timeout };
455
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
+ }
456
470
  /** Detect roots that would cause ast-index to scan the entire filesystem */
457
471
  export function isDangerousRoot(root) {
458
472
  const normalized = root.replace(/\/+$/, '') || '/';
@@ -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
@@ -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
@@ -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,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,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
@@ -38,12 +38,14 @@ export declare const TOOL_DEFINITIONS: ({
38
38
  symbols?: undefined;
39
39
  start_line?: undefined;
40
40
  end_line?: undefined;
41
+ heading?: undefined;
41
42
  context_lines?: undefined;
42
43
  line?: undefined;
43
44
  context?: undefined;
44
45
  include_callers?: undefined;
45
46
  include_tests?: undefined;
46
47
  include_changes?: undefined;
48
+ section?: undefined;
47
49
  paths?: undefined;
48
50
  kind?: undefined;
49
51
  limit?: undefined;
@@ -104,12 +106,14 @@ export declare const TOOL_DEFINITIONS: ({
104
106
  symbols?: undefined;
105
107
  start_line?: undefined;
106
108
  end_line?: undefined;
109
+ heading?: undefined;
107
110
  context_lines?: undefined;
108
111
  line?: undefined;
109
112
  context?: undefined;
110
113
  include_callers?: undefined;
111
114
  include_tests?: undefined;
112
115
  include_changes?: undefined;
116
+ section?: undefined;
113
117
  paths?: undefined;
114
118
  kind?: undefined;
115
119
  limit?: undefined;
@@ -170,12 +174,14 @@ export declare const TOOL_DEFINITIONS: ({
170
174
  include_edit_context?: undefined;
171
175
  start_line?: undefined;
172
176
  end_line?: undefined;
177
+ heading?: undefined;
173
178
  context_lines?: undefined;
174
179
  line?: undefined;
175
180
  context?: undefined;
176
181
  include_callers?: undefined;
177
182
  include_tests?: undefined;
178
183
  include_changes?: undefined;
184
+ section?: undefined;
179
185
  paths?: undefined;
180
186
  kind?: undefined;
181
187
  limit?: undefined;
@@ -226,12 +232,69 @@ export declare const TOOL_DEFINITIONS: ({
226
232
  show?: undefined;
227
233
  include_edit_context?: undefined;
228
234
  symbols?: undefined;
235
+ heading?: undefined;
229
236
  context_lines?: undefined;
230
237
  line?: undefined;
231
238
  context?: undefined;
232
239
  include_callers?: undefined;
233
240
  include_tests?: undefined;
234
241
  include_changes?: undefined;
242
+ section?: undefined;
243
+ paths?: undefined;
244
+ kind?: undefined;
245
+ limit?: undefined;
246
+ lang?: undefined;
247
+ mode?: undefined;
248
+ include?: undefined;
249
+ recursive?: undefined;
250
+ max_depth?: undefined;
251
+ verbose?: undefined;
252
+ module?: undefined;
253
+ export_only?: undefined;
254
+ check?: undefined;
255
+ pattern?: undefined;
256
+ name?: undefined;
257
+ ref?: undefined;
258
+ count?: undefined;
259
+ command?: undefined;
260
+ runner?: undefined;
261
+ timeout?: undefined;
262
+ };
263
+ required: string[];
264
+ };
265
+ } | {
266
+ name: string;
267
+ description: string;
268
+ inputSchema: {
269
+ type: "object";
270
+ properties: {
271
+ path: {
272
+ type: string;
273
+ description: string;
274
+ };
275
+ heading: {
276
+ type: string;
277
+ description: string;
278
+ };
279
+ show_imports?: undefined;
280
+ show_docs?: undefined;
281
+ depth?: undefined;
282
+ scope?: undefined;
283
+ symbol?: undefined;
284
+ context_before?: undefined;
285
+ context_after?: undefined;
286
+ show?: undefined;
287
+ include_edit_context?: undefined;
288
+ symbols?: undefined;
289
+ start_line?: undefined;
290
+ end_line?: undefined;
291
+ context_lines?: undefined;
292
+ line?: undefined;
293
+ context?: undefined;
294
+ include_callers?: undefined;
295
+ include_tests?: undefined;
296
+ include_changes?: undefined;
297
+ section?: undefined;
235
298
  paths?: undefined;
236
299
  kind?: undefined;
237
300
  limit?: undefined;
@@ -280,11 +343,13 @@ export declare const TOOL_DEFINITIONS: ({
280
343
  symbols?: undefined;
281
344
  start_line?: undefined;
282
345
  end_line?: undefined;
346
+ heading?: undefined;
283
347
  line?: undefined;
284
348
  context?: undefined;
285
349
  include_callers?: undefined;
286
350
  include_tests?: undefined;
287
351
  include_changes?: undefined;
352
+ section?: undefined;
288
353
  paths?: undefined;
289
354
  kind?: undefined;
290
355
  limit?: undefined;
@@ -348,6 +413,10 @@ export declare const TOOL_DEFINITIONS: ({
348
413
  type: string;
349
414
  description: string;
350
415
  };
416
+ section: {
417
+ type: string;
418
+ description: string;
419
+ };
351
420
  show_imports?: undefined;
352
421
  show_docs?: undefined;
353
422
  depth?: undefined;
@@ -358,6 +427,7 @@ export declare const TOOL_DEFINITIONS: ({
358
427
  include_edit_context?: undefined;
359
428
  start_line?: undefined;
360
429
  end_line?: undefined;
430
+ heading?: undefined;
361
431
  context_lines?: undefined;
362
432
  paths?: undefined;
363
433
  kind?: undefined;
@@ -407,12 +477,14 @@ export declare const TOOL_DEFINITIONS: ({
407
477
  symbols?: undefined;
408
478
  start_line?: undefined;
409
479
  end_line?: undefined;
480
+ heading?: undefined;
410
481
  context_lines?: undefined;
411
482
  line?: undefined;
412
483
  context?: undefined;
413
484
  include_callers?: undefined;
414
485
  include_tests?: undefined;
415
486
  include_changes?: undefined;
487
+ section?: undefined;
416
488
  kind?: undefined;
417
489
  limit?: undefined;
418
490
  lang?: undefined;
@@ -482,11 +554,13 @@ export declare const TOOL_DEFINITIONS: ({
482
554
  symbols?: undefined;
483
555
  start_line?: undefined;
484
556
  end_line?: undefined;
557
+ heading?: undefined;
485
558
  line?: undefined;
486
559
  context?: undefined;
487
560
  include_callers?: undefined;
488
561
  include_tests?: undefined;
489
562
  include_changes?: undefined;
563
+ section?: undefined;
490
564
  paths?: undefined;
491
565
  include?: undefined;
492
566
  recursive?: undefined;
@@ -532,12 +606,14 @@ export declare const TOOL_DEFINITIONS: ({
532
606
  symbols?: undefined;
533
607
  start_line?: undefined;
534
608
  end_line?: undefined;
609
+ heading?: undefined;
535
610
  context_lines?: undefined;
536
611
  line?: undefined;
537
612
  context?: undefined;
538
613
  include_callers?: undefined;
539
614
  include_tests?: undefined;
540
615
  include_changes?: undefined;
616
+ section?: undefined;
541
617
  paths?: undefined;
542
618
  kind?: undefined;
543
619
  limit?: undefined;
@@ -581,12 +657,14 @@ export declare const TOOL_DEFINITIONS: ({
581
657
  symbols?: undefined;
582
658
  start_line?: undefined;
583
659
  end_line?: undefined;
660
+ heading?: undefined;
584
661
  context_lines?: undefined;
585
662
  line?: undefined;
586
663
  context?: undefined;
587
664
  include_callers?: undefined;
588
665
  include_tests?: undefined;
589
666
  include_changes?: undefined;
667
+ section?: undefined;
590
668
  paths?: undefined;
591
669
  kind?: undefined;
592
670
  limit?: undefined;
@@ -639,12 +717,14 @@ export declare const TOOL_DEFINITIONS: ({
639
717
  symbols?: undefined;
640
718
  start_line?: undefined;
641
719
  end_line?: undefined;
720
+ heading?: undefined;
642
721
  context_lines?: undefined;
643
722
  line?: undefined;
644
723
  context?: undefined;
645
724
  include_callers?: undefined;
646
725
  include_tests?: undefined;
647
726
  include_changes?: undefined;
727
+ section?: undefined;
648
728
  paths?: undefined;
649
729
  kind?: undefined;
650
730
  limit?: undefined;
@@ -688,12 +768,14 @@ export declare const TOOL_DEFINITIONS: ({
688
768
  symbols?: undefined;
689
769
  start_line?: undefined;
690
770
  end_line?: undefined;
771
+ heading?: undefined;
691
772
  context_lines?: undefined;
692
773
  line?: undefined;
693
774
  context?: undefined;
694
775
  include_callers?: undefined;
695
776
  include_tests?: undefined;
696
777
  include_changes?: undefined;
778
+ section?: undefined;
697
779
  paths?: undefined;
698
780
  kind?: undefined;
699
781
  limit?: undefined;
@@ -746,12 +828,14 @@ export declare const TOOL_DEFINITIONS: ({
746
828
  symbols?: undefined;
747
829
  start_line?: undefined;
748
830
  end_line?: undefined;
831
+ heading?: undefined;
749
832
  context_lines?: undefined;
750
833
  line?: undefined;
751
834
  context?: undefined;
752
835
  include_callers?: undefined;
753
836
  include_tests?: undefined;
754
837
  include_changes?: undefined;
838
+ section?: undefined;
755
839
  paths?: undefined;
756
840
  kind?: undefined;
757
841
  lang?: undefined;
@@ -811,12 +895,14 @@ export declare const TOOL_DEFINITIONS: ({
811
895
  symbols?: undefined;
812
896
  start_line?: undefined;
813
897
  end_line?: undefined;
898
+ heading?: undefined;
814
899
  context_lines?: undefined;
815
900
  line?: undefined;
816
901
  context?: undefined;
817
902
  include_callers?: undefined;
818
903
  include_tests?: undefined;
819
904
  include_changes?: undefined;
905
+ section?: undefined;
820
906
  paths?: undefined;
821
907
  kind?: undefined;
822
908
  mode?: undefined;
@@ -862,12 +948,14 @@ export declare const TOOL_DEFINITIONS: ({
862
948
  symbols?: undefined;
863
949
  start_line?: undefined;
864
950
  end_line?: undefined;
951
+ heading?: undefined;
865
952
  context_lines?: undefined;
866
953
  line?: undefined;
867
954
  context?: undefined;
868
955
  include_callers?: undefined;
869
956
  include_tests?: undefined;
870
957
  include_changes?: undefined;
958
+ section?: undefined;
871
959
  paths?: undefined;
872
960
  kind?: undefined;
873
961
  limit?: undefined;
@@ -918,12 +1006,14 @@ export declare const TOOL_DEFINITIONS: ({
918
1006
  symbols?: undefined;
919
1007
  start_line?: undefined;
920
1008
  end_line?: undefined;
1009
+ heading?: undefined;
921
1010
  context_lines?: undefined;
922
1011
  line?: undefined;
923
1012
  context?: undefined;
924
1013
  include_callers?: undefined;
925
1014
  include_tests?: undefined;
926
1015
  include_changes?: undefined;
1016
+ section?: undefined;
927
1017
  paths?: undefined;
928
1018
  kind?: undefined;
929
1019
  limit?: undefined;
@@ -975,12 +1065,14 @@ export declare const TOOL_DEFINITIONS: ({
975
1065
  symbols?: undefined;
976
1066
  start_line?: undefined;
977
1067
  end_line?: undefined;
1068
+ heading?: undefined;
978
1069
  context_lines?: undefined;
979
1070
  line?: undefined;
980
1071
  context?: undefined;
981
1072
  include_callers?: undefined;
982
1073
  include_tests?: undefined;
983
1074
  include_changes?: undefined;
1075
+ section?: undefined;
984
1076
  paths?: undefined;
985
1077
  kind?: undefined;
986
1078
  limit?: undefined;
@@ -1032,12 +1124,14 @@ export declare const TOOL_DEFINITIONS: ({
1032
1124
  symbols?: undefined;
1033
1125
  start_line?: undefined;
1034
1126
  end_line?: undefined;
1127
+ heading?: undefined;
1035
1128
  context_lines?: undefined;
1036
1129
  line?: undefined;
1037
1130
  context?: undefined;
1038
1131
  include_callers?: undefined;
1039
1132
  include_tests?: undefined;
1040
1133
  include_changes?: undefined;
1134
+ section?: undefined;
1041
1135
  paths?: undefined;
1042
1136
  kind?: undefined;
1043
1137
  limit?: undefined;
@@ -1090,12 +1184,14 @@ export declare const TOOL_DEFINITIONS: ({
1090
1184
  symbols?: undefined;
1091
1185
  start_line?: undefined;
1092
1186
  end_line?: undefined;
1187
+ heading?: undefined;
1093
1188
  context_lines?: undefined;
1094
1189
  line?: undefined;
1095
1190
  context?: undefined;
1096
1191
  include_callers?: undefined;
1097
1192
  include_tests?: undefined;
1098
1193
  include_changes?: undefined;
1194
+ section?: undefined;
1099
1195
  paths?: undefined;
1100
1196
  kind?: undefined;
1101
1197
  limit?: undefined;
@@ -27,12 +27,15 @@ export const MCP_INSTRUCTIONS = [
27
27
  '15. Code quality → code_audit (TODOs, deprecated, structural patterns)',
28
28
  '16. Dead code → find_unused (unreferenced symbols across project)',
29
29
  '17. Module architecture → module_info (deps, dependents, public API)',
30
+ '18. Read markdown/yaml/json/csv section → read_section (loads one heading/key/row-range, NOT the whole file)',
31
+ ' - For editing sections: read_for_edit(path, section="Section Name")',
30
32
  '',
31
33
  'USE DEFAULT TOOLS ONLY FOR: regex text search → Grep | exact raw content → Read | non-code configs → Read',
32
34
  '',
33
35
  'WORKFLOWS:',
34
36
  '• Explore: project_overview → explore_area → smart_read → read_symbol',
35
37
  '• Edit: smart_read → read_symbol(include_edit_context=true) → Edit → read_diff',
38
+ '• Docs: smart_read (outline) → read_section → read_for_edit(section=) → Edit → read_diff',
36
39
  '• Refactor: find_usages → read_symbols → read_for_edit → Edit → test_summary',
37
40
  '• Audit: code_audit + find_unused + Grep (for regex patterns)',
38
41
  ].join('\n');
@@ -108,6 +111,18 @@ export const TOOL_DEFINITIONS = [
108
111
  required: ['path', 'start_line', 'end_line'],
109
112
  },
110
113
  },
114
+ {
115
+ name: 'read_section',
116
+ description: '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.',
117
+ inputSchema: {
118
+ type: 'object',
119
+ properties: {
120
+ path: { type: 'string', description: 'Path to .md, .yaml, .yml, .json, or .csv file' },
121
+ heading: { type: 'string', description: 'Section heading (Markdown), top-level key (YAML/JSON), or row range "rows:1-50" (CSV). Case-insensitive.' },
122
+ },
123
+ required: ['path', 'heading'],
124
+ },
125
+ },
111
126
  {
112
127
  name: 'read_diff',
113
128
  description: 'Use INSTEAD OF re-reading whole file after edits. Shows only changed hunks. REQUIRES: call smart_read or read_for_edit BEFORE editing to create baseline snapshot.',
@@ -138,6 +153,10 @@ export const TOOL_DEFINITIONS = [
138
153
  include_callers: { type: 'boolean', description: 'Show top callers of this symbol (saves a separate find_usages call)' },
139
154
  include_tests: { type: 'boolean', description: 'Show related test file and test names' },
140
155
  include_changes: { type: 'boolean', description: 'Show recent git changes in the target region' },
156
+ section: {
157
+ type: 'string',
158
+ description: 'Section to edit: heading (Markdown), top-level key (YAML/JSON), or "rows:1-50" (CSV). Returns raw section content for Edit old_string.',
159
+ },
141
160
  },
142
161
  required: ['path'],
143
162
  },
package/dist/server.js CHANGED
@@ -36,12 +36,13 @@ import { handleSmartDiff } from './handlers/smart-diff.js';
36
36
  import { handleExploreArea } from './handlers/explore-area.js';
37
37
  import { handleSmartLog } from './handlers/smart-log.js';
38
38
  import { handleTestSummary } from './handlers/test-summary.js';
39
+ import { handleReadSection } from './handlers/read-section.js';
39
40
  import { detectContextMode } from './integration/context-mode-detector.js';
40
41
  import { estimateTokens } from './core/token-estimator.js';
41
42
  import { checkPolicy, isFullReadTool } from './core/policy-engine.js';
42
43
  import { MCP_INSTRUCTIONS, TOOL_DEFINITIONS } from './server/tool-definitions.js';
43
44
  import { createTokenEstimates } from './server/token-estimates.js';
44
- import { validateSmartReadArgs, validateReadSymbolArgs, validateReadSymbolsArgs, validateReadRangeArgs, validateReadDiffArgs, validateFindUsagesArgs, validateSmartReadManyArgs, validateReadForEditArgs, validateRelatedFilesArgs, validateOutlineArgs, validateFindUnusedArgs, validateCodeAuditArgs, validateProjectOverviewArgs, validateModuleInfoArgs, validateSmartDiffArgs, validateExploreAreaArgs, validateSmartLogArgs, validateTestSummaryArgs, } from './core/validation.js';
45
+ import { validateSmartReadArgs, validateReadSymbolArgs, validateReadSymbolsArgs, validateReadRangeArgs, validateReadDiffArgs, validateFindUsagesArgs, validateSmartReadManyArgs, validateReadForEditArgs, validateRelatedFilesArgs, validateOutlineArgs, validateFindUnusedArgs, validateCodeAuditArgs, validateProjectOverviewArgs, validateModuleInfoArgs, validateSmartDiffArgs, validateExploreAreaArgs, validateSmartLogArgs, validateTestSummaryArgs, validateReadSectionArgs, } from './core/validation.js';
45
46
  export async function createServer(projectRoot, options) {
46
47
  const config = await loadConfig(projectRoot);
47
48
  const astIndex = new AstIndexClient(projectRoot, config.astIndex.timeout, {
@@ -310,6 +311,20 @@ export async function createServer(projectRoot, options) {
310
311
  recordWithTrace({ tool: 'read_range', path: rangeArgs.path, tokensReturned: rangeTokens, tokensWouldBe: fullTokensRange || rangeTokens, timestamp: Date.now(), savingsCategory: detectSavingsCategory(rangeText), absPath: resolve(projectRoot, rangeArgs.path), args: rangeArgs });
311
312
  return rangeResult;
312
313
  }
314
+ case 'read_section': {
315
+ const secArgs = validateReadSectionArgs(args);
316
+ const secResult = await handleReadSection(secArgs, projectRoot, contextRegistry);
317
+ const secText = secResult.content[0]?.text ?? '';
318
+ const secTokens = estimateTokens(secText);
319
+ const fullTokensSec = await fullFileTokens(secArgs.path);
320
+ recordWithTrace({
321
+ tool: 'read_section', path: secArgs.path,
322
+ tokensReturned: secTokens, tokensWouldBe: fullTokensSec || secTokens,
323
+ timestamp: Date.now(), savingsCategory: 'compression',
324
+ absPath: resolve(projectRoot, secArgs.path), args: secArgs,
325
+ });
326
+ return secResult;
327
+ }
313
328
  case 'read_diff': {
314
329
  const diffArgs = validateReadDiffArgs(args);
315
330
  const diffResult = await handleReadDiff(diffArgs, projectRoot, fileCache, contextRegistry);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "token-pilot",
3
- "version": "0.17.0",
3
+ "version": "0.18.0",
4
4
  "description": "Save up to 80% tokens when AI reads code — MCP server for token-efficient code navigation, AST-aware structural reading instead of dumping full files into context window",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",