token-pilot 0.30.0 → 0.30.1
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/.claude-plugin/marketplace.json +2 -2
- package/.claude-plugin/plugin.json +2 -4
- package/README.md +24 -0
- package/agents/tp-api-surface-tracker.md +1 -1
- package/agents/tp-audit-scanner.md +1 -1
- package/agents/tp-commit-writer.md +1 -1
- package/agents/tp-context-engineer.md +1 -1
- package/agents/tp-dead-code-finder.md +1 -1
- package/agents/tp-debugger.md +1 -1
- package/agents/tp-dep-health.md +1 -1
- package/agents/tp-doc-writer.md +1 -1
- package/agents/tp-history-explorer.md +1 -1
- package/agents/tp-impact-analyzer.md +1 -1
- package/agents/tp-incident-timeline.md +1 -1
- package/agents/tp-incremental-builder.md +1 -1
- package/agents/tp-migration-scout.md +1 -1
- package/agents/tp-onboard.md +1 -1
- package/agents/tp-performance-profiler.md +1 -1
- package/agents/tp-pr-reviewer.md +1 -1
- package/agents/tp-refactor-planner.md +1 -1
- package/agents/tp-review-impact.md +1 -1
- package/agents/tp-run.md +1 -1
- package/agents/tp-session-restorer.md +1 -1
- package/agents/tp-ship-coordinator.md +1 -1
- package/agents/tp-spec-writer.md +1 -1
- package/agents/tp-test-coverage-gapper.md +1 -1
- package/agents/tp-test-triage.md +1 -1
- package/agents/tp-test-writer.md +1 -1
- package/dist/ast-index/client.d.ts +17 -2
- package/dist/ast-index/client.js +233 -107
- package/dist/core/edit-prep-state.d.ts +42 -0
- package/dist/core/edit-prep-state.js +108 -0
- package/dist/handlers/explore-area.js +6 -1
- package/dist/handlers/read-for-edit.d.ts +5 -5
- package/dist/handlers/read-for-edit.js +188 -110
- package/dist/hooks/installer.js +18 -0
- package/dist/hooks/pre-bash.d.ts +9 -0
- package/dist/hooks/pre-bash.js +48 -0
- package/dist/hooks/pre-edit.d.ts +69 -0
- package/dist/hooks/pre-edit.js +104 -0
- package/dist/hooks/pre-grep.d.ts +10 -0
- package/dist/hooks/pre-grep.js +38 -2
- package/dist/index.d.ts +30 -0
- package/dist/index.js +83 -20
- package/dist/server/tool-definitions.js +18 -6
- package/dist/server.js +21 -5
- package/docs/installation.md +27 -1
- package/hooks/hooks.json +18 -0
- package/package.json +1 -1
- package/start.sh +19 -9
|
@@ -1,121 +1,184 @@
|
|
|
1
|
-
import { readFile, stat, access } from
|
|
2
|
-
import { execFile } from
|
|
3
|
-
import { promisify } from
|
|
4
|
-
import { createHash } from
|
|
5
|
-
import { relative, join, extname } from
|
|
6
|
-
import { parseMarkdownSections, findSection, extractSectionContent } from
|
|
7
|
-
import { parseYamlSections, findYamlSection, extractYamlSectionContent } from
|
|
8
|
-
import { parseJsonSections, findJsonSection, extractJsonSectionContent } from
|
|
9
|
-
import { parseCsvOutline, parseCsvSectionSpec, extractCsvSectionContent } from
|
|
10
|
-
import { estimateTokens } from
|
|
11
|
-
import { resolveSafePath } from
|
|
12
|
-
import {
|
|
1
|
+
import { readFile, stat, access } from "node:fs/promises";
|
|
2
|
+
import { execFile } from "node:child_process";
|
|
3
|
+
import { promisify } from "node:util";
|
|
4
|
+
import { createHash } from "node:crypto";
|
|
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";
|
|
10
|
+
import { estimateTokens } from "../core/token-estimator.js";
|
|
11
|
+
import { resolveSafePath } from "../core/validation.js";
|
|
12
|
+
import { markEditPrepared } from "../core/edit-prep-state.js";
|
|
13
|
+
import { assessConfidence, formatConfidence } from "../core/confidence.js";
|
|
13
14
|
const execFileAsync = promisify(execFile);
|
|
14
15
|
const DEFAULT_CONTEXT = 5;
|
|
15
16
|
export async function handleReadForEdit(args, projectRoot, symbolResolver, fileCache, contextRegistry, astIndex, options) {
|
|
16
17
|
const absPath = resolveSafePath(projectRoot, args.path);
|
|
18
|
+
// Record intent BEFORE any downstream failure: if the agent explicitly
|
|
19
|
+
// called read_for_edit on this path, they have declared they want to
|
|
20
|
+
// edit it next. The PreToolUse:Edit hook reads this to decide whether
|
|
21
|
+
// to allow or deny the follow-up Edit. Best-effort — never throws.
|
|
22
|
+
markEditPrepared(projectRoot, absPath);
|
|
17
23
|
const ctx = args.context ?? DEFAULT_CONTEXT;
|
|
18
24
|
// Section mode: markdown/YAML section extraction for edit
|
|
19
25
|
if (args.section) {
|
|
20
26
|
const ext = extname(absPath).toLowerCase();
|
|
21
|
-
const supportedExts = new Set([
|
|
27
|
+
const supportedExts = new Set([
|
|
28
|
+
".md",
|
|
29
|
+
".markdown",
|
|
30
|
+
".yaml",
|
|
31
|
+
".yml",
|
|
32
|
+
".json",
|
|
33
|
+
".csv",
|
|
34
|
+
]);
|
|
22
35
|
if (!supportedExts.has(ext)) {
|
|
23
36
|
return {
|
|
24
|
-
content: [
|
|
25
|
-
|
|
37
|
+
content: [
|
|
38
|
+
{
|
|
39
|
+
type: "text",
|
|
26
40
|
text: `"section" parameter only works with Markdown, YAML, or JSON files. Got: ${ext}. Use "symbol" for code files.`,
|
|
27
|
-
}
|
|
41
|
+
},
|
|
42
|
+
],
|
|
28
43
|
};
|
|
29
44
|
}
|
|
30
|
-
const fileContent = await readFile(absPath,
|
|
31
|
-
const fileLines = fileContent.split(
|
|
45
|
+
const fileContent = await readFile(absPath, "utf-8");
|
|
46
|
+
const fileLines = fileContent.split("\n");
|
|
32
47
|
// Cache file in fileCache for read_diff baseline
|
|
33
48
|
if (!fileCache.get(absPath)) {
|
|
34
49
|
const fileStat = await stat(absPath);
|
|
35
|
-
const hash = createHash(
|
|
36
|
-
const language = ext ===
|
|
50
|
+
const hash = createHash("sha256").update(fileContent).digest("hex");
|
|
51
|
+
const language = ext === ".csv"
|
|
52
|
+
? "csv"
|
|
53
|
+
: ext === ".json"
|
|
54
|
+
? "json"
|
|
55
|
+
: ext === ".md" || ext === ".markdown"
|
|
56
|
+
? "markdown"
|
|
57
|
+
: "yaml";
|
|
37
58
|
fileCache.set(absPath, {
|
|
38
|
-
structure: {
|
|
39
|
-
|
|
59
|
+
structure: {
|
|
60
|
+
path: absPath,
|
|
61
|
+
language,
|
|
62
|
+
meta: {
|
|
63
|
+
lines: fileLines.length,
|
|
64
|
+
bytes: fileContent.length,
|
|
65
|
+
lastModified: fileStat.mtimeMs,
|
|
66
|
+
contentHash: hash,
|
|
67
|
+
},
|
|
68
|
+
imports: [],
|
|
69
|
+
exports: [],
|
|
70
|
+
symbols: [],
|
|
71
|
+
},
|
|
72
|
+
content: fileContent,
|
|
73
|
+
lines: fileLines,
|
|
74
|
+
mtime: fileStat.mtimeMs,
|
|
75
|
+
hash,
|
|
76
|
+
lastAccess: Date.now(),
|
|
40
77
|
});
|
|
41
78
|
}
|
|
42
79
|
let sectionResult = null;
|
|
43
|
-
if (ext ===
|
|
80
|
+
if (ext === ".md" || ext === ".markdown") {
|
|
44
81
|
const sections = parseMarkdownSections(fileContent);
|
|
45
82
|
const section = findSection(sections, args.section);
|
|
46
83
|
if (!section) {
|
|
47
|
-
const available = sections.map(s => s.heading).join(
|
|
84
|
+
const available = sections.map((s) => s.heading).join(", ");
|
|
48
85
|
return {
|
|
49
|
-
content: [
|
|
50
|
-
|
|
86
|
+
content: [
|
|
87
|
+
{
|
|
88
|
+
type: "text",
|
|
51
89
|
text: `Section "${args.section}" not found in ${args.path}.\nAvailable: ${available}`,
|
|
52
|
-
}
|
|
90
|
+
},
|
|
91
|
+
],
|
|
53
92
|
};
|
|
54
93
|
}
|
|
55
|
-
const hashes =
|
|
56
|
-
sectionResult = {
|
|
94
|
+
const hashes = "#".repeat(section.level);
|
|
95
|
+
sectionResult = {
|
|
96
|
+
...section,
|
|
97
|
+
rawContent: extractSectionContent(fileLines, section),
|
|
98
|
+
label: `${hashes} ${section.heading}`,
|
|
99
|
+
};
|
|
57
100
|
}
|
|
58
|
-
else if (ext ===
|
|
101
|
+
else if (ext === ".yaml" || ext === ".yml") {
|
|
59
102
|
const sections = parseYamlSections(fileContent);
|
|
60
103
|
const section = findYamlSection(sections, args.section);
|
|
61
104
|
if (!section) {
|
|
62
|
-
const available = sections.map(s => s.heading).join(
|
|
105
|
+
const available = sections.map((s) => s.heading).join(", ");
|
|
63
106
|
return {
|
|
64
|
-
content: [
|
|
65
|
-
|
|
107
|
+
content: [
|
|
108
|
+
{
|
|
109
|
+
type: "text",
|
|
66
110
|
text: `Section "${args.section}" not found in ${args.path}.\nAvailable: ${available}`,
|
|
67
|
-
}
|
|
111
|
+
},
|
|
112
|
+
],
|
|
68
113
|
};
|
|
69
114
|
}
|
|
70
|
-
sectionResult = {
|
|
115
|
+
sectionResult = {
|
|
116
|
+
...section,
|
|
117
|
+
rawContent: extractYamlSectionContent(fileLines, section),
|
|
118
|
+
label: section.heading,
|
|
119
|
+
};
|
|
71
120
|
}
|
|
72
|
-
else if (ext ===
|
|
121
|
+
else if (ext === ".json") {
|
|
73
122
|
const sections = parseJsonSections(fileContent);
|
|
74
123
|
const section = findJsonSection(sections, args.section);
|
|
75
124
|
if (!section) {
|
|
76
|
-
const available = sections.map(s => s.heading).join(
|
|
125
|
+
const available = sections.map((s) => s.heading).join(", ");
|
|
77
126
|
return {
|
|
78
|
-
content: [
|
|
79
|
-
|
|
127
|
+
content: [
|
|
128
|
+
{
|
|
129
|
+
type: "text",
|
|
80
130
|
text: `Section "${args.section}" not found in ${args.path}.\nAvailable: ${available}`,
|
|
81
|
-
}
|
|
131
|
+
},
|
|
132
|
+
],
|
|
82
133
|
};
|
|
83
134
|
}
|
|
84
|
-
sectionResult = {
|
|
135
|
+
sectionResult = {
|
|
136
|
+
...section,
|
|
137
|
+
rawContent: extractJsonSectionContent(fileLines, section),
|
|
138
|
+
label: section.heading,
|
|
139
|
+
};
|
|
85
140
|
}
|
|
86
|
-
else if (ext ===
|
|
141
|
+
else if (ext === ".csv") {
|
|
87
142
|
const outline = parseCsvOutline(fileContent);
|
|
88
143
|
const section = parseCsvSectionSpec(args.section, outline.rowCount);
|
|
89
144
|
if (!section) {
|
|
90
145
|
return {
|
|
91
|
-
content: [
|
|
92
|
-
|
|
146
|
+
content: [
|
|
147
|
+
{
|
|
148
|
+
type: "text",
|
|
93
149
|
text: `Invalid section "${args.section}" for CSV. Use: rows:1-50 or row:5\nTotal rows: ${outline.rowCount}`,
|
|
94
|
-
}
|
|
150
|
+
},
|
|
151
|
+
],
|
|
95
152
|
};
|
|
96
153
|
}
|
|
97
|
-
sectionResult = {
|
|
154
|
+
sectionResult = {
|
|
155
|
+
...section,
|
|
156
|
+
rawContent: extractCsvSectionContent(fileLines, section),
|
|
157
|
+
label: section.heading,
|
|
158
|
+
};
|
|
98
159
|
}
|
|
99
160
|
if (!sectionResult) {
|
|
100
|
-
return {
|
|
161
|
+
return {
|
|
162
|
+
content: [{ type: "text", text: `Unsupported file type: ${ext}` }],
|
|
163
|
+
};
|
|
101
164
|
}
|
|
102
165
|
const outputLines = [
|
|
103
166
|
`FILE: ${args.path}`,
|
|
104
167
|
`EDIT SECTION: ${sectionResult.label} [L${sectionResult.startLine}-${sectionResult.endLine}] (${sectionResult.lineCount} lines)`,
|
|
105
|
-
|
|
168
|
+
"",
|
|
106
169
|
sectionResult.rawContent,
|
|
107
|
-
|
|
170
|
+
"",
|
|
108
171
|
`AFTER EDIT: Use read_diff("${args.path}") to verify changes (90% cheaper than re-reading).`,
|
|
109
172
|
];
|
|
110
|
-
const output = outputLines.join(
|
|
173
|
+
const output = outputLines.join("\n");
|
|
111
174
|
const tokens = estimateTokens(output);
|
|
112
175
|
contextRegistry.trackLoad(absPath, {
|
|
113
|
-
type:
|
|
176
|
+
type: "range",
|
|
114
177
|
startLine: sectionResult.startLine,
|
|
115
178
|
endLine: sectionResult.endLine,
|
|
116
179
|
tokens,
|
|
117
180
|
});
|
|
118
|
-
return { content: [{ type:
|
|
181
|
+
return { content: [{ type: "text", text: output }] };
|
|
119
182
|
}
|
|
120
183
|
// Get file content — also cache for read_diff baseline
|
|
121
184
|
const cached = fileCache.get(absPath);
|
|
@@ -124,16 +187,21 @@ export async function handleReadForEdit(args, projectRoot, symbolResolver, fileC
|
|
|
124
187
|
lines = cached.lines;
|
|
125
188
|
}
|
|
126
189
|
else {
|
|
127
|
-
const content = await readFile(absPath,
|
|
128
|
-
lines = content.split(
|
|
190
|
+
const content = await readFile(absPath, "utf-8");
|
|
191
|
+
lines = content.split("\n");
|
|
129
192
|
// Cache the full file so read_diff can use it as baseline after edits
|
|
130
193
|
const fileStat = await stat(absPath);
|
|
131
|
-
const hash = createHash(
|
|
194
|
+
const hash = createHash("sha256").update(content).digest("hex");
|
|
132
195
|
fileCache.set(absPath, {
|
|
133
196
|
structure: {
|
|
134
197
|
path: absPath,
|
|
135
|
-
language:
|
|
136
|
-
meta: {
|
|
198
|
+
language: "unknown",
|
|
199
|
+
meta: {
|
|
200
|
+
lines: lines.length,
|
|
201
|
+
bytes: content.length,
|
|
202
|
+
lastModified: fileStat.mtimeMs,
|
|
203
|
+
contentHash: hash,
|
|
204
|
+
},
|
|
137
205
|
imports: [],
|
|
138
206
|
exports: [],
|
|
139
207
|
symbols: [],
|
|
@@ -149,19 +217,19 @@ export async function handleReadForEdit(args, projectRoot, symbolResolver, fileC
|
|
|
149
217
|
if (args.symbols && args.symbols.length > 0) {
|
|
150
218
|
let structure = cached?.structure;
|
|
151
219
|
if (!structure) {
|
|
152
|
-
structure = await astIndex.outline(absPath) ?? undefined;
|
|
220
|
+
structure = (await astIndex.outline(absPath)) ?? undefined;
|
|
153
221
|
}
|
|
154
222
|
const sections = [];
|
|
155
223
|
sections.push(`--- EDIT CONTEXT (BATCH: ${args.symbols.length} symbols) ---`);
|
|
156
224
|
sections.push(`FILE: ${args.path}`);
|
|
157
|
-
sections.push(
|
|
225
|
+
sections.push("");
|
|
158
226
|
let resolved_count = 0;
|
|
159
227
|
for (let i = 0; i < args.symbols.length; i++) {
|
|
160
228
|
const symName = args.symbols[i];
|
|
161
229
|
const resolved = await symbolResolver.resolve(symName, structure);
|
|
162
230
|
if (!resolved) {
|
|
163
231
|
sections.push(`=== SYMBOL ${i + 1}/${args.symbols.length}: ${symName} — NOT FOUND ===`);
|
|
164
|
-
sections.push(
|
|
232
|
+
sections.push("");
|
|
165
233
|
continue;
|
|
166
234
|
}
|
|
167
235
|
resolved_count++;
|
|
@@ -180,22 +248,22 @@ export async function handleReadForEdit(args, projectRoot, symbolResolver, fileC
|
|
|
180
248
|
}
|
|
181
249
|
const rangeStart = Math.max(1, effStart - ctx);
|
|
182
250
|
const rangeEnd = Math.min(lines.length, effEnd + ctx);
|
|
183
|
-
const rawCode = lines.slice(rangeStart - 1, rangeEnd).join(
|
|
251
|
+
const rawCode = lines.slice(rangeStart - 1, rangeEnd).join("\n");
|
|
184
252
|
sections.push(`=== SYMBOL ${i + 1}/${args.symbols.length}: ${label} ===`);
|
|
185
|
-
sections.push(
|
|
253
|
+
sections.push("");
|
|
186
254
|
sections.push(rawCode);
|
|
187
|
-
sections.push(
|
|
255
|
+
sections.push("");
|
|
188
256
|
// Track each symbol
|
|
189
257
|
contextRegistry.trackLoad(absPath, {
|
|
190
|
-
type:
|
|
258
|
+
type: "symbol",
|
|
191
259
|
symbolName: symName,
|
|
192
260
|
startLine: rangeStart,
|
|
193
261
|
endLine: rangeEnd,
|
|
194
262
|
tokens: estimateTokens(rawCode),
|
|
195
263
|
});
|
|
196
264
|
}
|
|
197
|
-
sections.push(
|
|
198
|
-
sections.push(
|
|
265
|
+
sections.push("--- END EDIT CONTEXT ---");
|
|
266
|
+
sections.push("");
|
|
199
267
|
sections.push(`To edit: use exact text from each section as old_string in Edit tool.`);
|
|
200
268
|
if (resolved_count < args.symbols.length) {
|
|
201
269
|
sections.push(`WARNING: ${args.symbols.length - resolved_count} symbol(s) not found. Use smart_read to see available symbols.`);
|
|
@@ -207,8 +275,8 @@ export async function handleReadForEdit(args, projectRoot, symbolResolver, fileC
|
|
|
207
275
|
astAvailable: true,
|
|
208
276
|
});
|
|
209
277
|
sections.push(formatConfidence(confidenceMeta));
|
|
210
|
-
const output = sections.join(
|
|
211
|
-
return { content: [{ type:
|
|
278
|
+
const output = sections.join("\n");
|
|
279
|
+
return { content: [{ type: "text", text: output }] };
|
|
212
280
|
}
|
|
213
281
|
let startLine;
|
|
214
282
|
let endLine;
|
|
@@ -217,15 +285,17 @@ export async function handleReadForEdit(args, projectRoot, symbolResolver, fileC
|
|
|
217
285
|
// Resolve symbol via AST
|
|
218
286
|
let structure = cached?.structure;
|
|
219
287
|
if (!structure) {
|
|
220
|
-
structure = await astIndex.outline(absPath) ?? undefined;
|
|
288
|
+
structure = (await astIndex.outline(absPath)) ?? undefined;
|
|
221
289
|
}
|
|
222
290
|
const resolved = await symbolResolver.resolve(args.symbol, structure);
|
|
223
291
|
if (!resolved) {
|
|
224
292
|
return {
|
|
225
|
-
content: [
|
|
226
|
-
|
|
293
|
+
content: [
|
|
294
|
+
{
|
|
295
|
+
type: "text",
|
|
227
296
|
text: `Symbol "${args.symbol}" not found in ${args.path}.\nHINT: Use smart_read("${args.path}") to see available symbols.`,
|
|
228
|
-
}
|
|
297
|
+
},
|
|
298
|
+
],
|
|
229
299
|
};
|
|
230
300
|
}
|
|
231
301
|
const symbolLines = resolved.endLine - resolved.startLine + 1;
|
|
@@ -243,10 +313,12 @@ export async function handleReadForEdit(args, projectRoot, symbolResolver, fileC
|
|
|
243
313
|
else if (args.line) {
|
|
244
314
|
if (args.line < 1 || args.line > lines.length) {
|
|
245
315
|
return {
|
|
246
|
-
content: [
|
|
247
|
-
|
|
316
|
+
content: [
|
|
317
|
+
{
|
|
318
|
+
type: "text",
|
|
248
319
|
text: `Line ${args.line} out of range (file has ${lines.length} lines).`,
|
|
249
|
-
}
|
|
320
|
+
},
|
|
321
|
+
],
|
|
250
322
|
};
|
|
251
323
|
}
|
|
252
324
|
startLine = args.line;
|
|
@@ -255,10 +327,12 @@ export async function handleReadForEdit(args, projectRoot, symbolResolver, fileC
|
|
|
255
327
|
}
|
|
256
328
|
else {
|
|
257
329
|
return {
|
|
258
|
-
content: [
|
|
259
|
-
|
|
330
|
+
content: [
|
|
331
|
+
{
|
|
332
|
+
type: "text",
|
|
260
333
|
text: 'Either "symbol" or "line" must be provided.',
|
|
261
|
-
}
|
|
334
|
+
},
|
|
335
|
+
],
|
|
262
336
|
};
|
|
263
337
|
}
|
|
264
338
|
// Apply context padding
|
|
@@ -266,17 +340,17 @@ export async function handleReadForEdit(args, projectRoot, symbolResolver, fileC
|
|
|
266
340
|
const rangeEnd = Math.min(lines.length, endLine + ctx);
|
|
267
341
|
const rangeCount = rangeEnd - rangeStart + 1;
|
|
268
342
|
// Extract RAW code (no line number prefixes — ready for Edit old_string)
|
|
269
|
-
const rawCode = lines.slice(rangeStart - 1, rangeEnd).join(
|
|
343
|
+
const rawCode = lines.slice(rangeStart - 1, rangeEnd).join("\n");
|
|
270
344
|
const outputLines = [
|
|
271
345
|
`--- EDIT CONTEXT ---`,
|
|
272
346
|
`FILE: ${args.path}`,
|
|
273
347
|
`TARGET: ${targetLabel}`,
|
|
274
348
|
`SHOWING: L${rangeStart}-${rangeEnd} (${rangeCount} lines)`,
|
|
275
|
-
|
|
349
|
+
"",
|
|
276
350
|
rawCode,
|
|
277
|
-
|
|
351
|
+
"",
|
|
278
352
|
`--- END EDIT CONTEXT ---`,
|
|
279
|
-
|
|
353
|
+
"",
|
|
280
354
|
`To edit: use exact text above as old_string in Edit tool.`,
|
|
281
355
|
`For Read requirement: Read("${args.path}", offset=${rangeStart}, limit=${rangeCount})`,
|
|
282
356
|
];
|
|
@@ -287,17 +361,17 @@ export async function handleReadForEdit(args, projectRoot, symbolResolver, fileC
|
|
|
287
361
|
const refs = await astIndex.refs(args.symbol, 10);
|
|
288
362
|
const callers = refs.usages.slice(0, 5);
|
|
289
363
|
if (callers.length > 0) {
|
|
290
|
-
outputLines.push(
|
|
364
|
+
outputLines.push("");
|
|
291
365
|
outputLines.push(`CALLERS (${callers.length}):`);
|
|
292
366
|
for (const c of callers) {
|
|
293
367
|
const relPath = relative(projectRoot, c.path);
|
|
294
|
-
const ctx = c.context ? ` — ${c.context.trim().slice(0, 80)}` :
|
|
368
|
+
const ctx = c.context ? ` — ${c.context.trim().slice(0, 80)}` : "";
|
|
295
369
|
outputLines.push(` ${relPath}:${c.line}${ctx}`);
|
|
296
370
|
}
|
|
297
371
|
}
|
|
298
372
|
else {
|
|
299
|
-
outputLines.push(
|
|
300
|
-
outputLines.push(
|
|
373
|
+
outputLines.push("");
|
|
374
|
+
outputLines.push("CALLERS: none found");
|
|
301
375
|
}
|
|
302
376
|
}
|
|
303
377
|
catch {
|
|
@@ -307,13 +381,13 @@ export async function handleReadForEdit(args, projectRoot, symbolResolver, fileC
|
|
|
307
381
|
// include_tests: find related test file and list test names
|
|
308
382
|
if (args.include_tests) {
|
|
309
383
|
const testSection = await findTestSection(absPath, args.path, projectRoot, astIndex);
|
|
310
|
-
outputLines.push(
|
|
384
|
+
outputLines.push("");
|
|
311
385
|
outputLines.push(...testSection);
|
|
312
386
|
}
|
|
313
387
|
// include_changes: git diff filtered to target region
|
|
314
388
|
if (args.include_changes) {
|
|
315
389
|
const diffSection = await findChangesSection(absPath, projectRoot, rangeStart, rangeEnd);
|
|
316
|
-
outputLines.push(
|
|
390
|
+
outputLines.push("");
|
|
317
391
|
outputLines.push(...diffSection);
|
|
318
392
|
}
|
|
319
393
|
// Confidence metadata
|
|
@@ -328,37 +402,37 @@ export async function handleReadForEdit(args, projectRoot, symbolResolver, fileC
|
|
|
328
402
|
outputLines.push(formatConfidence(confidenceMeta));
|
|
329
403
|
// Add post-edit hint (config-gated)
|
|
330
404
|
if (options?.actionableHints !== false) {
|
|
331
|
-
outputLines.push(
|
|
405
|
+
outputLines.push("");
|
|
332
406
|
outputLines.push(`AFTER EDIT: Use read_diff("${args.path}") to verify changes (90% cheaper than re-reading the file).`);
|
|
333
407
|
}
|
|
334
|
-
const output = outputLines.join(
|
|
408
|
+
const output = outputLines.join("\n");
|
|
335
409
|
const tokens = estimateTokens(output);
|
|
336
410
|
// Track in context
|
|
337
411
|
contextRegistry.trackLoad(absPath, {
|
|
338
|
-
type:
|
|
412
|
+
type: "symbol",
|
|
339
413
|
symbolName: args.symbol ?? `line:${args.line}`,
|
|
340
414
|
startLine: rangeStart,
|
|
341
415
|
endLine: rangeEnd,
|
|
342
416
|
tokens,
|
|
343
417
|
});
|
|
344
|
-
return { content: [{ type:
|
|
418
|
+
return { content: [{ type: "text", text: output }] };
|
|
345
419
|
}
|
|
346
420
|
// --- Helper: find related test file and extract test names ---
|
|
347
421
|
async function findTestSection(absPath, relPath, projectRoot, astIndex) {
|
|
348
422
|
// Derive test file path from source path using common conventions
|
|
349
423
|
// src/handlers/foo.ts → tests/handlers/foo.test.ts
|
|
350
424
|
// src/core/bar.ts → tests/core/bar.test.ts
|
|
351
|
-
const srcPrefix =
|
|
425
|
+
const srcPrefix = "src/";
|
|
352
426
|
let testRelPath;
|
|
353
427
|
if (relPath.startsWith(srcPrefix)) {
|
|
354
428
|
const rest = relPath.slice(srcPrefix.length);
|
|
355
|
-
const ext = rest.match(/\.[^.]+$/)?.[0] ??
|
|
356
|
-
const base = rest.replace(/\.[^.]+$/,
|
|
429
|
+
const ext = rest.match(/\.[^.]+$/)?.[0] ?? ".ts";
|
|
430
|
+
const base = rest.replace(/\.[^.]+$/, "");
|
|
357
431
|
testRelPath = `tests/${base}.test${ext}`;
|
|
358
432
|
}
|
|
359
433
|
else {
|
|
360
|
-
const ext = relPath.match(/\.[^.]+$/)?.[0] ??
|
|
361
|
-
const base = relPath.replace(/\.[^.]+$/,
|
|
434
|
+
const ext = relPath.match(/\.[^.]+$/)?.[0] ?? ".ts";
|
|
435
|
+
const base = relPath.replace(/\.[^.]+$/, "");
|
|
362
436
|
testRelPath = `${base}.test${ext}`;
|
|
363
437
|
}
|
|
364
438
|
const testAbsPath = join(projectRoot, testRelPath);
|
|
@@ -395,10 +469,10 @@ async function findChangesSection(absPath, projectRoot, rangeStart, rangeEnd) {
|
|
|
395
469
|
const MAX_DIFF_LINES = 30;
|
|
396
470
|
try {
|
|
397
471
|
// Try unstaged changes first
|
|
398
|
-
let diffOutput =
|
|
399
|
-
let diffLabel =
|
|
472
|
+
let diffOutput = "";
|
|
473
|
+
let diffLabel = "unstaged";
|
|
400
474
|
try {
|
|
401
|
-
const { stdout } = await execFileAsync(
|
|
475
|
+
const { stdout } = await execFileAsync("git", ["diff", "HEAD", "--", absPath], {
|
|
402
476
|
cwd: projectRoot,
|
|
403
477
|
timeout: 5000,
|
|
404
478
|
});
|
|
@@ -406,29 +480,29 @@ async function findChangesSection(absPath, projectRoot, rangeStart, rangeEnd) {
|
|
|
406
480
|
}
|
|
407
481
|
catch {
|
|
408
482
|
// git not available or not a repo
|
|
409
|
-
return [
|
|
483
|
+
return ["RECENT CHANGES: unavailable (not a git repo)"];
|
|
410
484
|
}
|
|
411
485
|
// If no unstaged changes, try last commit
|
|
412
486
|
if (!diffOutput.trim()) {
|
|
413
487
|
try {
|
|
414
|
-
const { stdout } = await execFileAsync(
|
|
488
|
+
const { stdout } = await execFileAsync("git", ["diff", "HEAD~1", "--", absPath], {
|
|
415
489
|
cwd: projectRoot,
|
|
416
490
|
timeout: 5000,
|
|
417
491
|
});
|
|
418
492
|
diffOutput = stdout;
|
|
419
|
-
diffLabel =
|
|
493
|
+
diffLabel = "last commit";
|
|
420
494
|
}
|
|
421
495
|
catch {
|
|
422
496
|
// no previous commit
|
|
423
497
|
}
|
|
424
498
|
}
|
|
425
499
|
if (!diffOutput.trim()) {
|
|
426
|
-
return [
|
|
500
|
+
return ["RECENT CHANGES: none (file unchanged)"];
|
|
427
501
|
}
|
|
428
502
|
// Filter hunks to those overlapping with target range
|
|
429
503
|
const relevantLines = filterDiffHunks(diffOutput, rangeStart, rangeEnd);
|
|
430
504
|
if (relevantLines.length === 0) {
|
|
431
|
-
return [
|
|
505
|
+
return ["RECENT CHANGES: none in target region"];
|
|
432
506
|
}
|
|
433
507
|
const lines = [`RECENT CHANGES (${diffLabel}):`];
|
|
434
508
|
const trimmed = relevantLines.slice(0, MAX_DIFF_LINES);
|
|
@@ -441,12 +515,12 @@ async function findChangesSection(absPath, projectRoot, rangeStart, rangeEnd) {
|
|
|
441
515
|
return lines;
|
|
442
516
|
}
|
|
443
517
|
catch {
|
|
444
|
-
return [
|
|
518
|
+
return ["RECENT CHANGES: unavailable"];
|
|
445
519
|
}
|
|
446
520
|
}
|
|
447
521
|
/** Filter diff output to only hunks overlapping [rangeStart, rangeEnd]. */
|
|
448
522
|
function filterDiffHunks(diff, rangeStart, rangeEnd) {
|
|
449
|
-
const allLines = diff.split(
|
|
523
|
+
const allLines = diff.split("\n");
|
|
450
524
|
const result = [];
|
|
451
525
|
let inRelevantHunk = false;
|
|
452
526
|
for (const line of allLines) {
|
|
@@ -454,7 +528,7 @@ function filterDiffHunks(diff, rangeStart, rangeEnd) {
|
|
|
454
528
|
const hunkMatch = line.match(/^@@ -\d+(?:,\d+)? \+(\d+)(?:,(\d+))? @@/);
|
|
455
529
|
if (hunkMatch) {
|
|
456
530
|
const hunkStart = parseInt(hunkMatch[1], 10);
|
|
457
|
-
const hunkLen = parseInt(hunkMatch[2] ??
|
|
531
|
+
const hunkLen = parseInt(hunkMatch[2] ?? "1", 10);
|
|
458
532
|
const hunkEnd = hunkStart + hunkLen - 1;
|
|
459
533
|
// Check overlap with target range
|
|
460
534
|
inRelevantHunk = hunkStart <= rangeEnd && hunkEnd >= rangeStart;
|
|
@@ -464,10 +538,14 @@ function filterDiffHunks(diff, rangeStart, rangeEnd) {
|
|
|
464
538
|
continue;
|
|
465
539
|
}
|
|
466
540
|
// Skip diff metadata lines (diff --git, index, ---, +++)
|
|
467
|
-
if (line.startsWith(
|
|
541
|
+
if (line.startsWith("diff ") ||
|
|
542
|
+
line.startsWith("index ") ||
|
|
543
|
+
line.startsWith("--- ") ||
|
|
544
|
+
line.startsWith("+++ ")) {
|
|
468
545
|
continue;
|
|
469
546
|
}
|
|
470
|
-
if (inRelevantHunk &&
|
|
547
|
+
if (inRelevantHunk &&
|
|
548
|
+
(line.startsWith("+") || line.startsWith("-") || line.startsWith(" "))) {
|
|
471
549
|
result.push(line);
|
|
472
550
|
}
|
|
473
551
|
}
|
package/dist/hooks/installer.js
CHANGED
|
@@ -34,6 +34,24 @@ function createHookConfig(options) {
|
|
|
34
34
|
},
|
|
35
35
|
],
|
|
36
36
|
},
|
|
37
|
+
{
|
|
38
|
+
matcher: "MultiEdit",
|
|
39
|
+
hooks: [
|
|
40
|
+
{
|
|
41
|
+
type: "command",
|
|
42
|
+
command: buildHookCommand("hook-edit", options),
|
|
43
|
+
},
|
|
44
|
+
],
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
matcher: "Write",
|
|
48
|
+
hooks: [
|
|
49
|
+
{
|
|
50
|
+
type: "command",
|
|
51
|
+
command: buildHookCommand("hook-edit", options),
|
|
52
|
+
},
|
|
53
|
+
],
|
|
54
|
+
},
|
|
37
55
|
{
|
|
38
56
|
matcher: "Bash",
|
|
39
57
|
hooks: [
|
package/dist/hooks/pre-bash.d.ts
CHANGED
|
@@ -34,6 +34,9 @@ export interface PreBashInput {
|
|
|
34
34
|
}
|
|
35
35
|
export type PreBashDecision = {
|
|
36
36
|
kind: "allow";
|
|
37
|
+
} | {
|
|
38
|
+
kind: "advise";
|
|
39
|
+
reason: string;
|
|
37
40
|
} | {
|
|
38
41
|
kind: "deny";
|
|
39
42
|
reason: string;
|
|
@@ -50,6 +53,12 @@ export type PreBashDecision = {
|
|
|
50
53
|
*/
|
|
51
54
|
export declare function extractWrappedCommands(command: string): string[];
|
|
52
55
|
export declare function detectHeavyPattern(command: string): PreBashDecision;
|
|
56
|
+
/**
|
|
57
|
+
* Detect common test-runner invocations. Returns true for anything we'd
|
|
58
|
+
* route through `test_summary`. Kept as a pure string test so it's unit-
|
|
59
|
+
* testable without spinning up child processes.
|
|
60
|
+
*/
|
|
61
|
+
export declare function isTestRunnerCommand(cmd: string): boolean;
|
|
53
62
|
export declare function decidePreBash(input: PreBashInput, mode?: EnforcementMode): PreBashDecision;
|
|
54
63
|
export declare function renderPreBashOutput(decision: PreBashDecision): string | null;
|
|
55
64
|
//# sourceMappingURL=pre-bash.d.ts.map
|