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.
- package/CHANGELOG.md +30 -0
- package/README.md +19 -5
- package/dist/config/defaults.js +2 -0
- package/dist/core/policy-engine.d.ts +6 -0
- package/dist/core/policy-engine.js +24 -0
- package/dist/core/validation.d.ts +7 -0
- package/dist/core/validation.js +18 -3
- package/dist/handlers/csv-sections.d.ts +35 -0
- package/dist/handlers/csv-sections.js +129 -0
- package/dist/handlers/json-sections.d.ts +17 -0
- package/dist/handlers/json-sections.js +66 -0
- package/dist/handlers/markdown-sections.d.ts +15 -0
- package/dist/handlers/markdown-sections.js +49 -0
- package/dist/handlers/non-code.js +48 -11
- package/dist/handlers/read-for-edit.d.ts +1 -0
- package/dist/handlers/read-for-edit.js +107 -1
- package/dist/handlers/read-section.d.ts +12 -0
- package/dist/handlers/read-section.js +96 -0
- package/dist/handlers/session-snapshot.d.ts +14 -0
- package/dist/handlers/session-snapshot.js +22 -0
- package/dist/handlers/smart-read-many.d.ts +1 -0
- package/dist/handlers/smart-read-many.js +1 -1
- package/dist/handlers/smart-read.d.ts +1 -0
- package/dist/handlers/smart-read.js +50 -27
- package/dist/handlers/yaml-sections.d.ts +17 -0
- package/dist/handlers/yaml-sections.js +46 -0
- package/dist/server/tool-definitions.d.ts +298 -0
- package/dist/server/tool-definitions.js +40 -0
- package/dist/server.js +34 -1
- package/dist/types.d.ts +2 -0
- package/package.json +1 -1
|
@@ -2,12 +2,16 @@ import { readFile } from 'node:fs/promises';
|
|
|
2
2
|
import { extname } from 'node:path';
|
|
3
3
|
import { estimateTokens } from '../core/token-estimator.js';
|
|
4
4
|
import { resolveSafePath } from '../core/validation.js';
|
|
5
|
+
import { parseMarkdownSections } from './markdown-sections.js';
|
|
6
|
+
import { parseYamlSections } from './yaml-sections.js';
|
|
7
|
+
import { parseJsonSections } from './json-sections.js';
|
|
8
|
+
import { parseCsvOutline, formatCsvOutline } from './csv-sections.js';
|
|
5
9
|
/**
|
|
6
10
|
* Detect if a file is a non-code structured file (JSON, YAML, Markdown, etc.)
|
|
7
11
|
*/
|
|
8
12
|
export function isNonCodeStructured(filePath) {
|
|
9
13
|
const ext = extname(filePath).toLowerCase();
|
|
10
|
-
return ['.json', '.yaml', '.yml', '.md', '.markdown', '.toml'].includes(ext);
|
|
14
|
+
return ['.json', '.yaml', '.yml', '.md', '.markdown', '.toml', '.csv'].includes(ext);
|
|
11
15
|
}
|
|
12
16
|
/**
|
|
13
17
|
* Generate a structural summary for non-code files.
|
|
@@ -37,6 +41,9 @@ export async function handleNonCodeRead(filePath, projectRoot, contextRegistry,
|
|
|
37
41
|
case '.toml':
|
|
38
42
|
summary = summarizeToml(filePath, content, lines.length);
|
|
39
43
|
break;
|
|
44
|
+
case '.csv':
|
|
45
|
+
summary = summarizeCsv(filePath, content, lines.length);
|
|
46
|
+
break;
|
|
40
47
|
default:
|
|
41
48
|
return null;
|
|
42
49
|
}
|
|
@@ -87,14 +94,34 @@ function summarizeJson(filePath, content, lineCount) {
|
|
|
87
94
|
catch {
|
|
88
95
|
lines.push('(Invalid JSON — parse error)');
|
|
89
96
|
}
|
|
97
|
+
// Add section line ranges for navigation
|
|
98
|
+
const jsonSections = parseJsonSections(content);
|
|
99
|
+
if (jsonSections.length > 0) {
|
|
100
|
+
lines.push('');
|
|
101
|
+
lines.push('SECTION RANGES:');
|
|
102
|
+
for (const sec of jsonSections) {
|
|
103
|
+
lines.push(` "${sec.heading}" [L${sec.startLine}-${sec.endLine}] (${sec.lineCount} lines)`);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
lines.push('');
|
|
107
|
+
lines.push(`HINT: Use read_section("${filePath}", heading="<key>") to load a specific section.`);
|
|
90
108
|
return lines.join('\n');
|
|
91
109
|
}
|
|
92
110
|
function summarizeYaml(filePath, content, lineCount) {
|
|
93
111
|
const lines = [
|
|
94
112
|
`FILE: ${filePath} (${lineCount} lines, YAML)`,
|
|
95
113
|
'',
|
|
96
|
-
'STRUCTURE:',
|
|
97
114
|
];
|
|
115
|
+
// Add top-level section overview with line ranges
|
|
116
|
+
const yamlSections = parseYamlSections(content);
|
|
117
|
+
if (yamlSections.length > 0) {
|
|
118
|
+
lines.push('SECTIONS:');
|
|
119
|
+
for (const sec of yamlSections) {
|
|
120
|
+
lines.push(` ${sec.heading}: [L${sec.startLine}-${sec.endLine}] (${sec.lineCount} lines)`);
|
|
121
|
+
}
|
|
122
|
+
lines.push('');
|
|
123
|
+
}
|
|
124
|
+
lines.push('STRUCTURE:');
|
|
98
125
|
const rawLines = content.split('\n');
|
|
99
126
|
const roots = [];
|
|
100
127
|
// Stack tracks parent nodes at each indent level
|
|
@@ -159,6 +186,8 @@ function summarizeYaml(filePath, content, lineCount) {
|
|
|
159
186
|
formatYamlNode(root, lines, 1, 3); // max 3 levels deep
|
|
160
187
|
}
|
|
161
188
|
}
|
|
189
|
+
lines.push('');
|
|
190
|
+
lines.push(`HINT: Use read_section("${filePath}", heading="<key>") to load a specific section.`);
|
|
162
191
|
return lines.join('\n');
|
|
163
192
|
}
|
|
164
193
|
function formatYamlNode(node, lines, depth, maxDepth) {
|
|
@@ -194,16 +223,17 @@ function summarizeMarkdown(filePath, content, lineCount) {
|
|
|
194
223
|
const lines = [
|
|
195
224
|
`FILE: ${filePath} (${lineCount} lines, Markdown)`,
|
|
196
225
|
'',
|
|
197
|
-
'TABLE OF CONTENTS:',
|
|
198
226
|
];
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
227
|
+
const sections = parseMarkdownSections(content);
|
|
228
|
+
if (sections.length === 0) {
|
|
229
|
+
lines.push('(No headings found)');
|
|
230
|
+
return lines.join('\n');
|
|
231
|
+
}
|
|
232
|
+
lines.push('SECTIONS:');
|
|
233
|
+
for (const sec of sections) {
|
|
234
|
+
const indent = ' '.repeat(sec.level);
|
|
235
|
+
const hashes = '#'.repeat(sec.level);
|
|
236
|
+
lines.push(`${indent}${hashes} ${sec.heading} [L${sec.startLine}-${sec.endLine}] (${sec.lineCount} lines)`);
|
|
207
237
|
}
|
|
208
238
|
// Count code blocks
|
|
209
239
|
const codeBlocks = (content.match(/```/g) || []).length / 2;
|
|
@@ -211,6 +241,9 @@ function summarizeMarkdown(filePath, content, lineCount) {
|
|
|
211
241
|
lines.push('');
|
|
212
242
|
lines.push(`Code blocks: ${Math.floor(codeBlocks)}`);
|
|
213
243
|
}
|
|
244
|
+
lines.push('');
|
|
245
|
+
lines.push(`HINT: Use read_section("${filePath}", heading="<name>") to load a specific section.`);
|
|
246
|
+
lines.push(` Use read_for_edit("${filePath}", section="<name>") for edit context.`);
|
|
214
247
|
return lines.join('\n');
|
|
215
248
|
}
|
|
216
249
|
function summarizeToml(filePath, content, lineCount) {
|
|
@@ -228,4 +261,8 @@ function summarizeToml(filePath, content, lineCount) {
|
|
|
228
261
|
}
|
|
229
262
|
return lines.join('\n');
|
|
230
263
|
}
|
|
264
|
+
function summarizeCsv(filePath, content, lineCount) {
|
|
265
|
+
const outline = parseCsvOutline(content);
|
|
266
|
+
return formatCsvOutline(filePath, outline, lineCount);
|
|
267
|
+
}
|
|
231
268
|
//# sourceMappingURL=non-code.js.map
|
|
@@ -11,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
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|