pi-optimizer 0.7.0-alpha.4
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/LICENSE +21 -0
- package/README.md +48 -0
- package/index.js +1984 -0
- package/index.js.map +7 -0
- package/package.json +31 -0
package/index.js
ADDED
|
@@ -0,0 +1,1984 @@
|
|
|
1
|
+
// src/optimizer/deduplicator/index.ts
|
|
2
|
+
import "@earendil-works/pi-coding-agent";
|
|
3
|
+
|
|
4
|
+
// src/optimizer/entries.ts
|
|
5
|
+
import { existsSync, readFileSync, renameSync, unlinkSync, writeFileSync } from "node:fs";
|
|
6
|
+
|
|
7
|
+
// src/utils.ts
|
|
8
|
+
import { spawn } from "node:child_process";
|
|
9
|
+
import { readFile, writeFile } from "node:fs/promises";
|
|
10
|
+
import { homedir } from "node:os";
|
|
11
|
+
import { join } from "node:path";
|
|
12
|
+
import {
|
|
13
|
+
keyHint,
|
|
14
|
+
SettingsManager,
|
|
15
|
+
truncateToVisualLines
|
|
16
|
+
} from "@earendil-works/pi-coding-agent";
|
|
17
|
+
import { Spacer, Text } from "@earendil-works/pi-tui";
|
|
18
|
+
import { Container } from "@earendil-works/pi-tui";
|
|
19
|
+
var PI_PATHS = {
|
|
20
|
+
web: "web-results",
|
|
21
|
+
agents: "agents",
|
|
22
|
+
settings: "settings.json",
|
|
23
|
+
mcp: "mcp.json",
|
|
24
|
+
permissions: "permissions.json",
|
|
25
|
+
checkpoints: "checkpoints",
|
|
26
|
+
subsessions: "subsessions.json",
|
|
27
|
+
subsessionsDir: "subsessions",
|
|
28
|
+
plans: "plans",
|
|
29
|
+
grammars: "grammars",
|
|
30
|
+
system: "SYSTEM.md"
|
|
31
|
+
};
|
|
32
|
+
function getPiPath(key, ...full) {
|
|
33
|
+
const path = PI_PATHS[key];
|
|
34
|
+
const remaining = path.includes(".") ? [] : full.slice(1);
|
|
35
|
+
const isGlobal = !full[0] || full[0] === "global";
|
|
36
|
+
const baseDir = isGlobal ? homedir() : full[0];
|
|
37
|
+
const piPath = isGlobal ? [".pi", "agent"] : [".pi"];
|
|
38
|
+
return join(baseDir, ...piPath, path, ...remaining);
|
|
39
|
+
}
|
|
40
|
+
async function readJson(filePath, fallback) {
|
|
41
|
+
try {
|
|
42
|
+
return JSON.parse(await readFile(filePath, "utf8"));
|
|
43
|
+
} catch {
|
|
44
|
+
return fallback;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
async function writeJson(filePath, data) {
|
|
48
|
+
await writeFile(filePath, JSON.stringify(data, null, 2) + "\n", "utf8");
|
|
49
|
+
}
|
|
50
|
+
async function runCommand(cwd, command, argumentsList, options) {
|
|
51
|
+
const abortMessage = options?.abortMessage ?? "command aborted";
|
|
52
|
+
if (options?.signal?.aborted) {
|
|
53
|
+
throw new Error(abortMessage);
|
|
54
|
+
}
|
|
55
|
+
return await new Promise(
|
|
56
|
+
(resolveCommand, rejectCommand) => {
|
|
57
|
+
const executable = process.platform === "win32" ? `${command}.cmd` : command;
|
|
58
|
+
const childProcess = spawn(executable, argumentsList, {
|
|
59
|
+
cwd,
|
|
60
|
+
env: process.env,
|
|
61
|
+
signal: options?.signal,
|
|
62
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
63
|
+
});
|
|
64
|
+
let stdout = "";
|
|
65
|
+
let stderr = "";
|
|
66
|
+
childProcess.stdout.on("data", (chunk) => {
|
|
67
|
+
stdout += chunk.toString();
|
|
68
|
+
});
|
|
69
|
+
childProcess.stderr.on("data", (chunk) => {
|
|
70
|
+
stderr += chunk.toString();
|
|
71
|
+
});
|
|
72
|
+
childProcess.on("error", (error) => {
|
|
73
|
+
rejectCommand(error);
|
|
74
|
+
});
|
|
75
|
+
childProcess.on("close", (exitCode) => {
|
|
76
|
+
if (options?.signal?.aborted) {
|
|
77
|
+
rejectCommand(new Error(abortMessage));
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
const successExitCodes = options?.successExitCodes ?? [0];
|
|
81
|
+
if (!successExitCodes.includes(exitCode ?? -1)) {
|
|
82
|
+
const commandText = `${command} ${argumentsList.join(" ")}`;
|
|
83
|
+
const stderrText = stderr.trim();
|
|
84
|
+
rejectCommand(
|
|
85
|
+
new Error(
|
|
86
|
+
`${commandText} failed with exit code ${exitCode ?? "unknown"}${stderrText ? `: ${stderrText}` : ""}`
|
|
87
|
+
)
|
|
88
|
+
);
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
resolveCommand({ stdout, stderr, exitCode });
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
function unique(values) {
|
|
97
|
+
return [...new Set(values)];
|
|
98
|
+
}
|
|
99
|
+
function isRecord(value) {
|
|
100
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
101
|
+
}
|
|
102
|
+
function isMissingFileError(error) {
|
|
103
|
+
return Boolean(error) && typeof error === "object" && "code" in error && error.code === "ENOENT";
|
|
104
|
+
}
|
|
105
|
+
function renderCallText(text, isPartial) {
|
|
106
|
+
const container = new Container();
|
|
107
|
+
container.addChild(new Text(text, 0, 0));
|
|
108
|
+
if (!isPartial) {
|
|
109
|
+
container.addChild(new Spacer(1));
|
|
110
|
+
}
|
|
111
|
+
return container;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// src/optimizer/entries.ts
|
|
115
|
+
function readSessionEntries(sessionFile) {
|
|
116
|
+
if (!sessionFile || !existsSync(sessionFile)) return;
|
|
117
|
+
try {
|
|
118
|
+
const sessionText = readFileSync(sessionFile, "utf8");
|
|
119
|
+
const entries = [];
|
|
120
|
+
for (const line of sessionText.split("\n")) {
|
|
121
|
+
if (line.trim().length === 0) continue;
|
|
122
|
+
const entry = JSON.parse(line);
|
|
123
|
+
if (!isRecord(entry)) return;
|
|
124
|
+
entries.push(entry);
|
|
125
|
+
}
|
|
126
|
+
return entries;
|
|
127
|
+
} catch {
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
function writeSessionEntries(sessionFile, entries) {
|
|
132
|
+
const temporaryFile = `${sessionFile}.${process.pid}.${Date.now()}.tmp`;
|
|
133
|
+
try {
|
|
134
|
+
writeFileSync(
|
|
135
|
+
temporaryFile,
|
|
136
|
+
`${entries.map((entry) => JSON.stringify(entry)).join("\n")}
|
|
137
|
+
`,
|
|
138
|
+
"utf8"
|
|
139
|
+
);
|
|
140
|
+
renameSync(temporaryFile, sessionFile);
|
|
141
|
+
} finally {
|
|
142
|
+
if (existsSync(temporaryFile)) unlinkSync(temporaryFile);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
function getEntryId(entry) {
|
|
146
|
+
const entryId = entry.id;
|
|
147
|
+
if (typeof entryId !== "string" || entryId.length === 0) return;
|
|
148
|
+
return entryId;
|
|
149
|
+
}
|
|
150
|
+
function getMessage(entry) {
|
|
151
|
+
if (entry.type !== "message") return;
|
|
152
|
+
const message = entry.message;
|
|
153
|
+
if (!isRecord(message)) return;
|
|
154
|
+
return message;
|
|
155
|
+
}
|
|
156
|
+
function getToolResultMessage(entry) {
|
|
157
|
+
const message = getMessage(entry);
|
|
158
|
+
if (!message || message.role !== "toolResult") return;
|
|
159
|
+
if (typeof message.toolCallId !== "string" || message.toolCallId.length === 0) return;
|
|
160
|
+
if (typeof message.toolName !== "string" || message.toolName.length === 0) return;
|
|
161
|
+
return message;
|
|
162
|
+
}
|
|
163
|
+
function getParentId(entry) {
|
|
164
|
+
if (typeof entry.parentId === "string") return entry.parentId;
|
|
165
|
+
return null;
|
|
166
|
+
}
|
|
167
|
+
function getBranchEntries(entries, leafId) {
|
|
168
|
+
if (!leafId) return [];
|
|
169
|
+
const entriesById = /* @__PURE__ */ new Map();
|
|
170
|
+
for (const entry of entries) {
|
|
171
|
+
const entryId = getEntryId(entry);
|
|
172
|
+
if (entryId) entriesById.set(entryId, entry);
|
|
173
|
+
}
|
|
174
|
+
if (!entriesById.has(leafId)) return [];
|
|
175
|
+
const branch = [];
|
|
176
|
+
const visitedIds = /* @__PURE__ */ new Set();
|
|
177
|
+
let currentId = leafId;
|
|
178
|
+
while (currentId && !visitedIds.has(currentId)) {
|
|
179
|
+
const entry = entriesById.get(currentId);
|
|
180
|
+
if (!entry) break;
|
|
181
|
+
visitedIds.add(currentId);
|
|
182
|
+
branch.push(entry);
|
|
183
|
+
currentId = getParentId(entry);
|
|
184
|
+
}
|
|
185
|
+
return branch.reverse();
|
|
186
|
+
}
|
|
187
|
+
function getLastEntryId(entries) {
|
|
188
|
+
for (let index = entries.length - 1; index >= 0; index -= 1) {
|
|
189
|
+
const entryId = getEntryId(entries[index]);
|
|
190
|
+
if (entryId) return entryId;
|
|
191
|
+
}
|
|
192
|
+
return null;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// src/optimizer/deduplicator/helpers.ts
|
|
196
|
+
function filterDeduplicatedMessages(messages, state) {
|
|
197
|
+
const prunedIds = /* @__PURE__ */ new Set();
|
|
198
|
+
for (const [toolCallId, repeatIds] of state.replacements) {
|
|
199
|
+
if (repeatIds.every((repeatId) => state.resultEntryIds.has(repeatId))) {
|
|
200
|
+
prunedIds.add(toolCallId);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
if (prunedIds.size === 0) {
|
|
204
|
+
return { changed: false, messages };
|
|
205
|
+
}
|
|
206
|
+
const retained = [];
|
|
207
|
+
let changed = false;
|
|
208
|
+
for (const message of messages) {
|
|
209
|
+
if (message.role === "toolResult" && prunedIds.has(message.toolCallId)) {
|
|
210
|
+
changed = true;
|
|
211
|
+
continue;
|
|
212
|
+
}
|
|
213
|
+
if (message.role !== "assistant") {
|
|
214
|
+
retained.push(message);
|
|
215
|
+
continue;
|
|
216
|
+
}
|
|
217
|
+
const retainedContent = message.content.filter(
|
|
218
|
+
(block) => block.type !== "toolCall" || !prunedIds.has(block.id)
|
|
219
|
+
);
|
|
220
|
+
if (retainedContent.length === message.content.length) {
|
|
221
|
+
retained.push(message);
|
|
222
|
+
continue;
|
|
223
|
+
}
|
|
224
|
+
changed = true;
|
|
225
|
+
if (retainedContent.some((block) => block.type !== "thinking")) {
|
|
226
|
+
retained.push({ ...message, content: retainedContent });
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
return { changed, messages: retained };
|
|
230
|
+
}
|
|
231
|
+
function mergeRanges(ranges) {
|
|
232
|
+
const sorted = ranges.toSorted(([firstStart], [secondStart]) => firstStart - secondStart);
|
|
233
|
+
const merged = [];
|
|
234
|
+
for (const range of sorted) {
|
|
235
|
+
const previousRange = merged.at(-1);
|
|
236
|
+
if (!previousRange || previousRange[1] < range[0] - 1) {
|
|
237
|
+
merged.push([range[0], range[1]]);
|
|
238
|
+
continue;
|
|
239
|
+
}
|
|
240
|
+
previousRange[1] = Math.max(previousRange[1], range[1]);
|
|
241
|
+
}
|
|
242
|
+
return merged;
|
|
243
|
+
}
|
|
244
|
+
function hasFullCoverage(range, candidates) {
|
|
245
|
+
const intersections = [];
|
|
246
|
+
for (const candidate of candidates) {
|
|
247
|
+
const start = Math.max(range[0], candidate[0]);
|
|
248
|
+
const end = Math.min(range[1], candidate[1]);
|
|
249
|
+
if (start <= end) intersections.push([start, end]);
|
|
250
|
+
}
|
|
251
|
+
if (intersections.length === 0) return false;
|
|
252
|
+
const merged = mergeRanges(intersections);
|
|
253
|
+
return merged.reduce((lines, [start, end]) => lines + end - start + 1, 0) === range[1] - range[0] + 1;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// src/optimizer/deduplicator/resources.ts
|
|
257
|
+
import { realpathSync } from "node:fs";
|
|
258
|
+
import { normalize, resolve } from "node:path";
|
|
259
|
+
|
|
260
|
+
// src/optimizer/inspector/helpers.ts
|
|
261
|
+
function parseInspectToolDetails(inspected) {
|
|
262
|
+
if (!inspected || typeof inspected !== "object") return;
|
|
263
|
+
const path = inspected.path;
|
|
264
|
+
const symbol = inspected.symbol;
|
|
265
|
+
const range = inspected.range;
|
|
266
|
+
if (typeof path !== "string" || path.length === 0) return;
|
|
267
|
+
if (typeof symbol !== "string" || symbol.length === 0) return;
|
|
268
|
+
if (!Array.isArray(range) || range.length !== 2) return;
|
|
269
|
+
const start = range[0];
|
|
270
|
+
const end = range[1];
|
|
271
|
+
if (typeof start !== "number" || typeof end !== "number" || !Number.isInteger(start) || !Number.isInteger(end) || start < 1 || end < start) {
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
274
|
+
return { path, symbol, range: [start, end] };
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// src/optimizer/deduplicator/resources.ts
|
|
278
|
+
function getResultText(message) {
|
|
279
|
+
if (!Array.isArray(message.content) || message.content.length !== 1) return;
|
|
280
|
+
const content = message.content[0];
|
|
281
|
+
if (!isRecord(content) || content.type !== "text" || typeof content.text !== "string") return;
|
|
282
|
+
return content.text;
|
|
283
|
+
}
|
|
284
|
+
function normalizeResourcePath(sourcePath, cwd) {
|
|
285
|
+
const absolutePath = resolve(cwd, sourcePath);
|
|
286
|
+
try {
|
|
287
|
+
return normalize(realpathSync(absolutePath));
|
|
288
|
+
} catch {
|
|
289
|
+
return normalize(absolutePath);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
function getResourceCoverage(toolName, input, result, cwd) {
|
|
293
|
+
if (!isRecord(result)) return;
|
|
294
|
+
if (toolName === "inspect") {
|
|
295
|
+
const inspected = parseInspectToolDetails(result.details);
|
|
296
|
+
if (!inspected) return;
|
|
297
|
+
return {
|
|
298
|
+
resource: normalizeResourcePath(inspected.path, cwd),
|
|
299
|
+
range: inspected.range
|
|
300
|
+
};
|
|
301
|
+
}
|
|
302
|
+
if (toolName !== "read") return;
|
|
303
|
+
const sourcePath = input.path;
|
|
304
|
+
const offset = input.offset;
|
|
305
|
+
if (typeof sourcePath !== "string" || sourcePath.length === 0) return;
|
|
306
|
+
if (offset !== void 0 && (typeof offset !== "number" || !Number.isInteger(offset) || offset < 1)) {
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
309
|
+
const details = isRecord(result.details) ? result.details : void 0;
|
|
310
|
+
const truncation = details?.truncation;
|
|
311
|
+
if (truncation !== void 0 && !isRecord(truncation)) return;
|
|
312
|
+
if (isRecord(truncation) && truncation.firstLineExceedsLimit === true) return;
|
|
313
|
+
const resultText = getResultText(result);
|
|
314
|
+
if (resultText === void 0) return;
|
|
315
|
+
const continuation = resultText.match(/\n\n\[[^\n]*Use offset=\d+ to continue\.\]$/)?.[0];
|
|
316
|
+
const visibleText = continuation ? resultText.slice(0, -continuation.length) : resultText;
|
|
317
|
+
const outputLines = isRecord(truncation) && truncation.truncated === true ? truncation.outputLines : visibleText.split("\n").length;
|
|
318
|
+
if (typeof outputLines !== "number" || !Number.isInteger(outputLines) || outputLines <= 0) return;
|
|
319
|
+
const start = typeof offset === "number" ? offset : 1;
|
|
320
|
+
return {
|
|
321
|
+
resource: normalizeResourcePath(sourcePath, cwd),
|
|
322
|
+
range: [start, start + outputLines - 1]
|
|
323
|
+
};
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
// src/optimizer/deduplicator/state.ts
|
|
327
|
+
function collectToolCallInputs(activeEntries) {
|
|
328
|
+
const inputsByCallId = /* @__PURE__ */ new Map();
|
|
329
|
+
for (const entry of activeEntries) {
|
|
330
|
+
const message = getMessage(entry);
|
|
331
|
+
if (message?.role !== "assistant" || !Array.isArray(message.content)) continue;
|
|
332
|
+
for (const block of message.content) {
|
|
333
|
+
if (!isRecord(block) || block.type !== "toolCall") continue;
|
|
334
|
+
if (typeof block.id !== "string" || !isRecord(block.arguments)) continue;
|
|
335
|
+
inputsByCallId.set(block.id, block.arguments);
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
return inputsByCallId;
|
|
339
|
+
}
|
|
340
|
+
function collectResourceResults(activeEntries, inputsByCallId, cwd) {
|
|
341
|
+
const results = [];
|
|
342
|
+
for (const entry of activeEntries) {
|
|
343
|
+
const entryId = getEntryId(entry);
|
|
344
|
+
const message = getToolResultMessage(entry);
|
|
345
|
+
if (!entryId || !message || message.isError === true || typeof message.toolCallId !== "string") {
|
|
346
|
+
continue;
|
|
347
|
+
}
|
|
348
|
+
const input = inputsByCallId.get(message.toolCallId);
|
|
349
|
+
if (!input || message.toolName !== "read" && message.toolName !== "inspect") continue;
|
|
350
|
+
const coverage = getResourceCoverage(message.toolName, input, message, cwd);
|
|
351
|
+
if (!coverage) continue;
|
|
352
|
+
results.push({
|
|
353
|
+
entryId,
|
|
354
|
+
range: coverage.range,
|
|
355
|
+
resource: coverage.resource,
|
|
356
|
+
toolCallId: message.toolCallId,
|
|
357
|
+
prunable: true
|
|
358
|
+
});
|
|
359
|
+
}
|
|
360
|
+
return results;
|
|
361
|
+
}
|
|
362
|
+
function collectReplacementsById(results) {
|
|
363
|
+
const retainedByResource = /* @__PURE__ */ new Map();
|
|
364
|
+
const replacementsById = /* @__PURE__ */ new Map();
|
|
365
|
+
for (let index = results.length - 1; index >= 0; index -= 1) {
|
|
366
|
+
const result = results[index];
|
|
367
|
+
const retained = retainedByResource.get(result.resource) ?? [];
|
|
368
|
+
const covering = retained.filter(
|
|
369
|
+
(candidate) => candidate.range[0] <= result.range[1] && candidate.range[1] >= result.range[0]
|
|
370
|
+
);
|
|
371
|
+
if (hasFullCoverage(
|
|
372
|
+
result.range,
|
|
373
|
+
covering.map(({ range }) => range)
|
|
374
|
+
)) {
|
|
375
|
+
if (result.prunable) {
|
|
376
|
+
replacementsById.set(result.entryId, [
|
|
377
|
+
...new Set(covering.map((candidate) => candidate.entryId))
|
|
378
|
+
]);
|
|
379
|
+
}
|
|
380
|
+
continue;
|
|
381
|
+
}
|
|
382
|
+
retained.push(result);
|
|
383
|
+
retainedByResource.set(result.resource, retained);
|
|
384
|
+
}
|
|
385
|
+
return replacementsById;
|
|
386
|
+
}
|
|
387
|
+
function buildDeduplicatorState(entries, leafId, cwd) {
|
|
388
|
+
let activeEntries = getBranchEntries(entries, leafId);
|
|
389
|
+
if (activeEntries.length === 0 && leafId !== null) {
|
|
390
|
+
activeEntries = getBranchEntries(entries, getLastEntryId(entries));
|
|
391
|
+
}
|
|
392
|
+
const results = collectResourceResults(activeEntries, collectToolCallInputs(activeEntries), cwd);
|
|
393
|
+
const replacementsById = collectReplacementsById(results);
|
|
394
|
+
const replacements = /* @__PURE__ */ new Map();
|
|
395
|
+
for (const result of results) {
|
|
396
|
+
if (!result.prunable) continue;
|
|
397
|
+
const replacement = replacementsById.get(result.entryId);
|
|
398
|
+
if (!replacement) continue;
|
|
399
|
+
replacements.set(result.toolCallId, replacement);
|
|
400
|
+
}
|
|
401
|
+
return { replacements, resultEntryIds: new Set(results.map((result) => result.entryId)) };
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
// src/optimizer/deduplicator/index.ts
|
|
405
|
+
function deduplicator_default(pi) {
|
|
406
|
+
let state = buildDeduplicatorState([], null, "");
|
|
407
|
+
pi.on("session_start", (_event, ctx) => {
|
|
408
|
+
const entries = readSessionEntries(ctx.sessionManager.getSessionFile()) ?? [];
|
|
409
|
+
state = buildDeduplicatorState(entries, ctx.sessionManager.getLeafId(), ctx.cwd);
|
|
410
|
+
});
|
|
411
|
+
pi.on("session_tree", (event, ctx) => {
|
|
412
|
+
const entries = readSessionEntries(ctx.sessionManager.getSessionFile()) ?? [];
|
|
413
|
+
state = buildDeduplicatorState(entries, event.newLeafId, ctx.cwd);
|
|
414
|
+
});
|
|
415
|
+
pi.on("context", (event) => {
|
|
416
|
+
const deduplicated = filterDeduplicatedMessages(event.messages, state);
|
|
417
|
+
if (deduplicated.changed) return { messages: deduplicated.messages };
|
|
418
|
+
});
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
// src/optimizer/compactor/index.ts
|
|
422
|
+
import { statSync } from "node:fs";
|
|
423
|
+
import {
|
|
424
|
+
createBashToolDefinition,
|
|
425
|
+
createGrepToolDefinition,
|
|
426
|
+
isGrepToolResult
|
|
427
|
+
} from "@earendil-works/pi-coding-agent";
|
|
428
|
+
|
|
429
|
+
// src/optimizer/compactor/bash.ts
|
|
430
|
+
import { stripVTControlCharacters } from "node:util";
|
|
431
|
+
import { createLocalBashOperations } from "@earendil-works/pi-coding-agent";
|
|
432
|
+
var localBash = createLocalBashOperations();
|
|
433
|
+
var MIN_SIMILAR_LENGTH = 10;
|
|
434
|
+
var SIMILARITY_THRESHOLD = 0.8;
|
|
435
|
+
function isSimilarLine(previousLine, nextLine) {
|
|
436
|
+
if (Math.min(previousLine.length, nextLine.length) < MIN_SIMILAR_LENGTH) return false;
|
|
437
|
+
const longestLength = Math.max(previousLine.length, nextLine.length);
|
|
438
|
+
if (1 - Math.abs(previousLine.length - nextLine.length) / longestLength < SIMILARITY_THRESHOLD) {
|
|
439
|
+
return false;
|
|
440
|
+
}
|
|
441
|
+
const distances = new Uint32Array(nextLine.length + 1);
|
|
442
|
+
for (let nextIndex = 0; nextIndex <= nextLine.length; nextIndex++) {
|
|
443
|
+
distances[nextIndex] = nextIndex;
|
|
444
|
+
}
|
|
445
|
+
for (let previousIndex = 1; previousIndex <= previousLine.length; previousIndex++) {
|
|
446
|
+
let diagonalDistance = distances[0];
|
|
447
|
+
distances[0] = previousIndex;
|
|
448
|
+
for (let nextIndex = 1; nextIndex <= nextLine.length; nextIndex++) {
|
|
449
|
+
const upperDistance = distances[nextIndex];
|
|
450
|
+
const editCost = previousLine[previousIndex - 1] === nextLine[nextIndex - 1] ? 0 : 1;
|
|
451
|
+
distances[nextIndex] = Math.min(
|
|
452
|
+
upperDistance + 1,
|
|
453
|
+
distances[nextIndex - 1] + 1,
|
|
454
|
+
diagonalDistance + editCost
|
|
455
|
+
);
|
|
456
|
+
diagonalDistance = upperDistance;
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
return 1 - distances[nextLine.length] / longestLength >= SIMILARITY_THRESHOLD;
|
|
460
|
+
}
|
|
461
|
+
var BashResultCompactor = class {
|
|
462
|
+
decoder = new TextDecoder();
|
|
463
|
+
onData;
|
|
464
|
+
// Exact regex and carriage-return handling require holding one logical line.
|
|
465
|
+
currentLine = "";
|
|
466
|
+
carriageLine;
|
|
467
|
+
hasPendingLine = false;
|
|
468
|
+
pendingLine;
|
|
469
|
+
pendingTerminated = false;
|
|
470
|
+
omittedLines = 0;
|
|
471
|
+
hasSimilarLines = false;
|
|
472
|
+
constructor(onData) {
|
|
473
|
+
this.onData = onData;
|
|
474
|
+
}
|
|
475
|
+
append(data) {
|
|
476
|
+
this.process(this.decoder.decode(data, { stream: true }));
|
|
477
|
+
}
|
|
478
|
+
finish() {
|
|
479
|
+
this.process(this.decoder.decode());
|
|
480
|
+
if (this.hasPendingLine) {
|
|
481
|
+
this.emit(this.getNormalizedLine(), false);
|
|
482
|
+
this.resetLine();
|
|
483
|
+
}
|
|
484
|
+
this.flushPending();
|
|
485
|
+
}
|
|
486
|
+
process(text) {
|
|
487
|
+
for (const character of text) {
|
|
488
|
+
if (character === "\r") {
|
|
489
|
+
if (this.carriageLine === void 0 || stripVTControlCharacters(this.currentLine).length > 0) {
|
|
490
|
+
this.carriageLine = this.currentLine;
|
|
491
|
+
}
|
|
492
|
+
this.currentLine = "";
|
|
493
|
+
this.hasPendingLine = true;
|
|
494
|
+
continue;
|
|
495
|
+
}
|
|
496
|
+
if (character === "\n") {
|
|
497
|
+
this.emit(this.getNormalizedLine(), true);
|
|
498
|
+
this.resetLine();
|
|
499
|
+
continue;
|
|
500
|
+
}
|
|
501
|
+
this.currentLine += character;
|
|
502
|
+
this.hasPendingLine = true;
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
getNormalizedLine() {
|
|
506
|
+
const normalizedLine = stripVTControlCharacters(this.currentLine);
|
|
507
|
+
if (this.carriageLine !== void 0 && normalizedLine.length === 0) {
|
|
508
|
+
return stripVTControlCharacters(this.carriageLine);
|
|
509
|
+
}
|
|
510
|
+
return normalizedLine;
|
|
511
|
+
}
|
|
512
|
+
emit(normalizedLine, terminated) {
|
|
513
|
+
if (this.pendingLine === void 0) {
|
|
514
|
+
this.pendingLine = normalizedLine;
|
|
515
|
+
this.pendingTerminated = terminated;
|
|
516
|
+
return;
|
|
517
|
+
}
|
|
518
|
+
if (normalizedLine === this.pendingLine) {
|
|
519
|
+
this.omittedLines++;
|
|
520
|
+
return;
|
|
521
|
+
}
|
|
522
|
+
if (isSimilarLine(this.pendingLine, normalizedLine)) {
|
|
523
|
+
this.pendingLine = normalizedLine;
|
|
524
|
+
this.pendingTerminated = terminated;
|
|
525
|
+
this.omittedLines++;
|
|
526
|
+
this.hasSimilarLines = true;
|
|
527
|
+
return;
|
|
528
|
+
}
|
|
529
|
+
this.flushPending();
|
|
530
|
+
this.pendingLine = normalizedLine;
|
|
531
|
+
this.pendingTerminated = terminated;
|
|
532
|
+
}
|
|
533
|
+
flushPending() {
|
|
534
|
+
if (this.pendingLine === void 0) return;
|
|
535
|
+
const suffix = this.hasSimilarLines ? ` (similar line x${this.omittedLines})` : "";
|
|
536
|
+
this.onData(Buffer.from(`${this.pendingLine}${suffix}${this.pendingTerminated ? "\n" : ""}`));
|
|
537
|
+
this.pendingLine = void 0;
|
|
538
|
+
this.pendingTerminated = false;
|
|
539
|
+
this.omittedLines = 0;
|
|
540
|
+
this.hasSimilarLines = false;
|
|
541
|
+
}
|
|
542
|
+
resetLine() {
|
|
543
|
+
this.currentLine = "";
|
|
544
|
+
this.carriageLine = void 0;
|
|
545
|
+
this.hasPendingLine = false;
|
|
546
|
+
}
|
|
547
|
+
};
|
|
548
|
+
function createCompactingBashOperations(filter) {
|
|
549
|
+
return {
|
|
550
|
+
async exec(command, cwd, options) {
|
|
551
|
+
const compactor = new BashResultCompactor(options.onData);
|
|
552
|
+
try {
|
|
553
|
+
return await localBash.exec(command, cwd, {
|
|
554
|
+
...options,
|
|
555
|
+
onData: (data) => compactor.append(data)
|
|
556
|
+
});
|
|
557
|
+
} finally {
|
|
558
|
+
compactor.finish();
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
};
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
// src/optimizer/compactor/grep.ts
|
|
565
|
+
import {
|
|
566
|
+
appendFileSync,
|
|
567
|
+
existsSync as existsSync2,
|
|
568
|
+
readFileSync as readFileSync2,
|
|
569
|
+
renameSync as renameSync2,
|
|
570
|
+
unlinkSync as unlinkSync2,
|
|
571
|
+
writeFileSync as writeFileSync2
|
|
572
|
+
} from "node:fs";
|
|
573
|
+
function extractGrepSummary(contentText) {
|
|
574
|
+
if (contentText === "No matches found") return null;
|
|
575
|
+
const notice = contentText.match(/\n\n(\[[^\n]+\])$/)?.[1];
|
|
576
|
+
const fileLines = /* @__PURE__ */ new Map();
|
|
577
|
+
const contentLines = contentText.split("\n");
|
|
578
|
+
let filePath;
|
|
579
|
+
let matched = false;
|
|
580
|
+
for (let lineIndex = 0; lineIndex < contentLines.length; lineIndex++) {
|
|
581
|
+
const contentLine = contentLines[lineIndex] ?? "";
|
|
582
|
+
const lineMatch = contentLine.match(/^(\d+): /);
|
|
583
|
+
if (!lineMatch) {
|
|
584
|
+
const nextLine = contentLines[lineIndex + 1] ?? "";
|
|
585
|
+
if (contentLine && !/^\d+- /.test(contentLine) && /^\d+[:-] /.test(nextLine)) {
|
|
586
|
+
filePath = contentLine;
|
|
587
|
+
}
|
|
588
|
+
continue;
|
|
589
|
+
}
|
|
590
|
+
const lineNumStr = lineMatch[1];
|
|
591
|
+
if (!filePath || !lineNumStr) continue;
|
|
592
|
+
matched = true;
|
|
593
|
+
const lineNum = parseInt(lineNumStr, 10);
|
|
594
|
+
const existing = fileLines.get(filePath);
|
|
595
|
+
if (existing) {
|
|
596
|
+
existing.add(lineNum);
|
|
597
|
+
} else {
|
|
598
|
+
fileLines.set(filePath, /* @__PURE__ */ new Set([lineNum]));
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
if (!matched) return null;
|
|
602
|
+
return Array.from(fileLines.entries()).map(([filePath2, lines]) => `${filePath2}: lines_matched=[${Array.from(lines).join(", ")}]`).join("\n") + (notice ? `
|
|
603
|
+
|
|
604
|
+
${notice}` : "");
|
|
605
|
+
}
|
|
606
|
+
function rewriteTailWithSummaries(sessionFile, offset, summaries) {
|
|
607
|
+
if (summaries.size === 0) return;
|
|
608
|
+
const sessionBuffer = readFileSync2(sessionFile);
|
|
609
|
+
if (offset > sessionBuffer.length) return;
|
|
610
|
+
const prefixBuffer = sessionBuffer.subarray(0, offset);
|
|
611
|
+
const tailText = sessionBuffer.subarray(offset).toString("utf-8");
|
|
612
|
+
if (tailText.length === 0) return;
|
|
613
|
+
let changed = false;
|
|
614
|
+
const rewrittenTail = tailText.split("\n").filter((line) => line.trim().length > 0).map((line) => {
|
|
615
|
+
const entry = JSON.parse(line);
|
|
616
|
+
const message = entry.message;
|
|
617
|
+
if (entry.type === "message" && message?.role === "toolResult" && typeof message.toolCallId === "string" && summaries.has(message.toolCallId)) {
|
|
618
|
+
changed = true;
|
|
619
|
+
return JSON.stringify({
|
|
620
|
+
...entry,
|
|
621
|
+
message: {
|
|
622
|
+
...message,
|
|
623
|
+
content: [{ type: "text", text: summaries.get(message.toolCallId) }]
|
|
624
|
+
}
|
|
625
|
+
});
|
|
626
|
+
}
|
|
627
|
+
return line;
|
|
628
|
+
}).join("\n");
|
|
629
|
+
if (!changed) return;
|
|
630
|
+
const tempFile = `${sessionFile}.${process.pid}.${Date.now()}.tmp`;
|
|
631
|
+
try {
|
|
632
|
+
writeFileSync2(tempFile, prefixBuffer);
|
|
633
|
+
appendFileSync(tempFile, `${rewrittenTail}
|
|
634
|
+
`, "utf8");
|
|
635
|
+
renameSync2(tempFile, sessionFile);
|
|
636
|
+
} finally {
|
|
637
|
+
if (existsSync2(tempFile)) {
|
|
638
|
+
unlinkSync2(tempFile);
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
function formatGrepResult(content) {
|
|
643
|
+
const formattedLines = [];
|
|
644
|
+
const formattedIndexes = /* @__PURE__ */ new Map();
|
|
645
|
+
let currentFilePath;
|
|
646
|
+
let changed = false;
|
|
647
|
+
for (const line of content.split("\n")) {
|
|
648
|
+
const matchLine = line.match(/^(.+?):(\d+): (.*)$/);
|
|
649
|
+
const contextLine = matchLine ? null : line.match(/^(.+?)-(\d+)- (.*)$/);
|
|
650
|
+
const filePath = matchLine?.[1] ?? contextLine?.[1];
|
|
651
|
+
const lineNumber = matchLine?.[2] ?? contextLine?.[2];
|
|
652
|
+
const lineText = matchLine?.[3] ?? contextLine?.[3];
|
|
653
|
+
if (!filePath || !lineNumber || lineText === void 0) {
|
|
654
|
+
formattedLines.push(line);
|
|
655
|
+
continue;
|
|
656
|
+
}
|
|
657
|
+
const lineKey = `${filePath}\0${lineNumber}`;
|
|
658
|
+
const formattedIndex = formattedIndexes.get(lineKey);
|
|
659
|
+
if (formattedIndex !== void 0) {
|
|
660
|
+
if (matchLine) formattedLines[formattedIndex] = `${lineNumber}: ${lineText}`;
|
|
661
|
+
continue;
|
|
662
|
+
}
|
|
663
|
+
if (filePath !== currentFilePath) {
|
|
664
|
+
formattedLines.push(filePath);
|
|
665
|
+
currentFilePath = filePath;
|
|
666
|
+
}
|
|
667
|
+
formattedIndexes.set(lineKey, formattedLines.length);
|
|
668
|
+
formattedLines.push(`${lineNumber}${matchLine ? ":" : "-"} ${lineText}`);
|
|
669
|
+
changed = true;
|
|
670
|
+
}
|
|
671
|
+
return changed ? formattedLines.join("\n") : content;
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
// src/optimizer/compactor/index.ts
|
|
675
|
+
import Type from "typebox";
|
|
676
|
+
function compactor_default(pi) {
|
|
677
|
+
let writeStartOffset = 0;
|
|
678
|
+
const store = /* @__PURE__ */ new Map();
|
|
679
|
+
const grepTool = createGrepToolDefinition(process.cwd());
|
|
680
|
+
const bashTool = createBashToolDefinition(process.cwd(), {
|
|
681
|
+
operations: createCompactingBashOperations()
|
|
682
|
+
});
|
|
683
|
+
pi.registerTool({
|
|
684
|
+
...grepTool,
|
|
685
|
+
parameters: Type.Object({
|
|
686
|
+
...grepTool.parameters.properties,
|
|
687
|
+
context: Type.Optional(
|
|
688
|
+
Type.Number({
|
|
689
|
+
description: "Number of lines to show before and after each match (default: 0)",
|
|
690
|
+
maximum: 3
|
|
691
|
+
})
|
|
692
|
+
)
|
|
693
|
+
})
|
|
694
|
+
});
|
|
695
|
+
pi.registerTool({
|
|
696
|
+
...bashTool,
|
|
697
|
+
description: "Execute a bash command in the current working directory. Strips ANSI escapes, collapses carriage-return updates, removes consecutive duplicate lines, then optionally filters lines with a JavaScript regex before truncating to the last 2000 lines or 50KB. Saved full output is compacted.",
|
|
698
|
+
parameters: Type.Object({
|
|
699
|
+
...bashTool.parameters.properties,
|
|
700
|
+
purpose: Type.String({
|
|
701
|
+
description: "Briefly explain what this command will do and why before running it",
|
|
702
|
+
minLength: 1,
|
|
703
|
+
maxLength: 256,
|
|
704
|
+
pattern: "\\S"
|
|
705
|
+
})
|
|
706
|
+
}),
|
|
707
|
+
prepareArguments: void 0,
|
|
708
|
+
execute(toolCallId, params, signal, onUpdate, ctx) {
|
|
709
|
+
return bashTool.execute(
|
|
710
|
+
toolCallId,
|
|
711
|
+
{ command: params.command, timeout: params.timeout },
|
|
712
|
+
signal,
|
|
713
|
+
onUpdate,
|
|
714
|
+
ctx
|
|
715
|
+
);
|
|
716
|
+
}
|
|
717
|
+
});
|
|
718
|
+
pi.on("agent_start", async (_event, ctx) => {
|
|
719
|
+
if (store.size > 0) return;
|
|
720
|
+
const sessionFile = ctx.sessionManager.getSessionFile();
|
|
721
|
+
if (!sessionFile) {
|
|
722
|
+
writeStartOffset = 0;
|
|
723
|
+
return;
|
|
724
|
+
}
|
|
725
|
+
try {
|
|
726
|
+
writeStartOffset = statSync(sessionFile).size;
|
|
727
|
+
} catch {
|
|
728
|
+
writeStartOffset = 0;
|
|
729
|
+
}
|
|
730
|
+
});
|
|
731
|
+
pi.on("agent_end", async (event) => {
|
|
732
|
+
for (const message of event.messages) {
|
|
733
|
+
if (message.role !== "toolResult" || message.toolName !== "grep") continue;
|
|
734
|
+
const text = message.content.find((content) => content.type === "text")?.text;
|
|
735
|
+
if (text === void 0) continue;
|
|
736
|
+
const summary = extractGrepSummary(text);
|
|
737
|
+
if (!summary) continue;
|
|
738
|
+
store.set(message.toolCallId, summary);
|
|
739
|
+
}
|
|
740
|
+
});
|
|
741
|
+
pi.on("session_shutdown", (event, ctx) => {
|
|
742
|
+
if (store.size === 0) return;
|
|
743
|
+
const sessionFile = ctx.sessionManager.getSessionFile();
|
|
744
|
+
if (sessionFile) {
|
|
745
|
+
rewriteTailWithSummaries(sessionFile, writeStartOffset, store);
|
|
746
|
+
}
|
|
747
|
+
if (event.targetSessionFile && event.targetSessionFile !== sessionFile) {
|
|
748
|
+
rewriteTailWithSummaries(event.targetSessionFile, writeStartOffset, store);
|
|
749
|
+
}
|
|
750
|
+
});
|
|
751
|
+
pi.on("context", async (event) => {
|
|
752
|
+
if (store.size === 0) return;
|
|
753
|
+
let changed = false;
|
|
754
|
+
for (const message of event.messages) {
|
|
755
|
+
if (message.role !== "toolResult" || message.toolName !== "grep") continue;
|
|
756
|
+
const summary = store.get(message.toolCallId);
|
|
757
|
+
if (summary === void 0) continue;
|
|
758
|
+
message.content = [{ type: "text", text: summary }];
|
|
759
|
+
changed = true;
|
|
760
|
+
}
|
|
761
|
+
if (!changed) return;
|
|
762
|
+
return { messages: event.messages };
|
|
763
|
+
});
|
|
764
|
+
pi.on("tool_result", async (event) => {
|
|
765
|
+
if (!isGrepToolResult(event) || event.isError) return;
|
|
766
|
+
let changed = false;
|
|
767
|
+
const content = event.content.map((item) => {
|
|
768
|
+
if (item.type !== "text") return item;
|
|
769
|
+
const text = formatGrepResult(item.text);
|
|
770
|
+
if (text === item.text) return item;
|
|
771
|
+
changed = true;
|
|
772
|
+
return { ...item, text };
|
|
773
|
+
});
|
|
774
|
+
if (changed) return { content };
|
|
775
|
+
});
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
// src/optimizer/mapper/index.ts
|
|
779
|
+
import { defineTool, truncateHead } from "@earendil-works/pi-coding-agent";
|
|
780
|
+
import { Type as Type2 } from "typebox";
|
|
781
|
+
|
|
782
|
+
// src/optimizer/mapper/files.ts
|
|
783
|
+
import picomatch from "picomatch";
|
|
784
|
+
var SKIPPED_DIRECTORIES = /* @__PURE__ */ new Set([".git", ".pi", "build", "coverage", "dist", "node_modules"]);
|
|
785
|
+
function normalizePath(pathValue) {
|
|
786
|
+
const normalizedPath = pathValue.replaceAll("\\", "/");
|
|
787
|
+
return normalizedPath.startsWith("./") ? normalizedPath.slice(2) : normalizedPath;
|
|
788
|
+
}
|
|
789
|
+
async function rgFiles(projectPath, globTargets, signal) {
|
|
790
|
+
const args = ["--files", "--hidden"];
|
|
791
|
+
for (const skipped of SKIPPED_DIRECTORIES) {
|
|
792
|
+
args.push("--glob", `!**/${skipped}/**`);
|
|
793
|
+
}
|
|
794
|
+
for (const globTarget of globTargets) {
|
|
795
|
+
args.push("--glob", globTarget);
|
|
796
|
+
}
|
|
797
|
+
args.push(".");
|
|
798
|
+
const commandResult = await runCommand(projectPath, "rg", args, {
|
|
799
|
+
signal,
|
|
800
|
+
successExitCodes: [0, 1],
|
|
801
|
+
abortMessage: "mapper aborted"
|
|
802
|
+
});
|
|
803
|
+
return commandResult.stdout.split("\n").map((line) => normalizePath(line.trim())).filter((line) => line.length > 0);
|
|
804
|
+
}
|
|
805
|
+
async function grepFiles(projectPath, globTargets, signal) {
|
|
806
|
+
const args = ["-r", "-I", "-l"];
|
|
807
|
+
for (const skipped of SKIPPED_DIRECTORIES) {
|
|
808
|
+
args.push("--exclude-dir", skipped);
|
|
809
|
+
}
|
|
810
|
+
args.push("-e", "", ".");
|
|
811
|
+
const commandResult = await runCommand(projectPath, "grep", args, {
|
|
812
|
+
signal,
|
|
813
|
+
successExitCodes: [0, 1],
|
|
814
|
+
abortMessage: "mapper aborted"
|
|
815
|
+
});
|
|
816
|
+
const paths = commandResult.stdout.split("\n").map((line) => normalizePath(line.trim())).filter((line) => line.length > 0);
|
|
817
|
+
const matchers = globTargets.map((globTarget) => picomatch(globTarget, { dot: true }));
|
|
818
|
+
return paths.filter((pathValue) => matchers.some((matcher) => matcher(pathValue)));
|
|
819
|
+
}
|
|
820
|
+
async function resolveTargetPaths(projectPath, targets, signal) {
|
|
821
|
+
const globTargets = targets.flatMap((target) => {
|
|
822
|
+
const normalized = normalizePath(target);
|
|
823
|
+
if (normalized.length === 0) return [];
|
|
824
|
+
if (normalized === ".") return ["**"];
|
|
825
|
+
if (/[*?[\]{}]/.test(normalized)) return [normalized];
|
|
826
|
+
return [normalized, `${normalized}/**`];
|
|
827
|
+
});
|
|
828
|
+
if (globTargets.length === 0) return [];
|
|
829
|
+
try {
|
|
830
|
+
const paths = await rgFiles(projectPath, globTargets, signal);
|
|
831
|
+
return unique(paths).sort();
|
|
832
|
+
} catch (rgError) {
|
|
833
|
+
const rgMessage = rgError instanceof Error ? rgError.message : String(rgError);
|
|
834
|
+
if (rgMessage === "mapper aborted") {
|
|
835
|
+
throw rgError;
|
|
836
|
+
}
|
|
837
|
+
try {
|
|
838
|
+
const paths = await grepFiles(projectPath, globTargets, signal);
|
|
839
|
+
return unique(paths).sort();
|
|
840
|
+
} catch (grepError) {
|
|
841
|
+
const grepMessage = grepError instanceof Error ? grepError.message : String(grepError);
|
|
842
|
+
throw new Error(`mapper file scan failed: rg=${rgMessage}; grep=${grepMessage}`);
|
|
843
|
+
}
|
|
844
|
+
}
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
// src/optimizer/languages/grammar.ts
|
|
848
|
+
import { existsSync as existsSync3 } from "node:fs";
|
|
849
|
+
import { readFile as readFile2 } from "node:fs/promises";
|
|
850
|
+
import { resolve as resolve2 } from "node:path";
|
|
851
|
+
import { createRequire } from "node:module";
|
|
852
|
+
import { pathToFileURL } from "node:url";
|
|
853
|
+
var GRAMMAR_COVERAGE_TARGET = 0.9;
|
|
854
|
+
var GRAMMAR_INSTALL_KEY = "grammarInstall";
|
|
855
|
+
async function ensureGrammarCachePackage() {
|
|
856
|
+
const grammarCacheDir = getPiPath("grammars");
|
|
857
|
+
const jsonPath = resolve2(grammarCacheDir, "package.json");
|
|
858
|
+
try {
|
|
859
|
+
await readFile2(jsonPath, "utf8");
|
|
860
|
+
} catch (error) {
|
|
861
|
+
if (!isMissingFileError(error)) {
|
|
862
|
+
throw error;
|
|
863
|
+
}
|
|
864
|
+
await writeJson(jsonPath, {
|
|
865
|
+
name: "tree-sitter-grammars",
|
|
866
|
+
private: true,
|
|
867
|
+
dependencies: {}
|
|
868
|
+
});
|
|
869
|
+
}
|
|
870
|
+
}
|
|
871
|
+
async function getRepositoryRoot(cwd) {
|
|
872
|
+
try {
|
|
873
|
+
const { stdout } = await runCommand(cwd, "git", ["rev-parse", "--show-toplevel"]);
|
|
874
|
+
const repoRoot = stdout.trim();
|
|
875
|
+
return repoRoot.length > 0 ? repoRoot : void 0;
|
|
876
|
+
} catch {
|
|
877
|
+
}
|
|
878
|
+
}
|
|
879
|
+
async function selectSupportedBuckets(cwd) {
|
|
880
|
+
const bucketCounts = /* @__PURE__ */ new Map();
|
|
881
|
+
let totalFileCount = 0;
|
|
882
|
+
const { stdout } = await runCommand(cwd, "bash", [
|
|
883
|
+
"-lc",
|
|
884
|
+
"git ls-files | sed 's/.*\\.//' | sort | uniq -c | sort -rn"
|
|
885
|
+
]);
|
|
886
|
+
for (const line of stdout.split(/\r?\n/)) {
|
|
887
|
+
const lineMatch = line.trim().match(/^(\d+)\s+(.+)$/);
|
|
888
|
+
if (!lineMatch) continue;
|
|
889
|
+
const rawCount = lineMatch[1];
|
|
890
|
+
const rawExtension = lineMatch[2];
|
|
891
|
+
if (!rawCount || !rawExtension) continue;
|
|
892
|
+
const count = Number.parseInt(rawCount, 10);
|
|
893
|
+
const extension = `.${rawExtension.toLowerCase()}`;
|
|
894
|
+
if (!Number.isFinite(count) || count <= 0) continue;
|
|
895
|
+
totalFileCount += count;
|
|
896
|
+
for (const languageEntry of LANGUAGE_REGISTRY) {
|
|
897
|
+
if (!languageEntry.profile.extensions.has(extension)) continue;
|
|
898
|
+
const nextCount = bucketCounts.get(languageEntry.bucketName) ?? 0;
|
|
899
|
+
bucketCounts.set(languageEntry.bucketName, nextCount + count);
|
|
900
|
+
break;
|
|
901
|
+
}
|
|
902
|
+
}
|
|
903
|
+
if (totalFileCount === 0) return [];
|
|
904
|
+
const rankedBuckets = LANGUAGE_REGISTRY.map((languageEntry) => ({
|
|
905
|
+
count: bucketCounts.get(languageEntry.bucketName) ?? 0,
|
|
906
|
+
packageName: languageEntry.packageName,
|
|
907
|
+
version: languageEntry.version
|
|
908
|
+
})).filter((bucket) => bucket.count > 0).sort((leftBucket, rightBucket) => rightBucket.count - leftBucket.count);
|
|
909
|
+
let coveredCount = 0;
|
|
910
|
+
const pickedBuckets = [];
|
|
911
|
+
for (const bucket of rankedBuckets) {
|
|
912
|
+
if (coveredCount / totalFileCount >= GRAMMAR_COVERAGE_TARGET) break;
|
|
913
|
+
coveredCount += bucket.count;
|
|
914
|
+
pickedBuckets.push({
|
|
915
|
+
packageName: bucket.packageName,
|
|
916
|
+
version: bucket.version
|
|
917
|
+
});
|
|
918
|
+
}
|
|
919
|
+
return pickedBuckets;
|
|
920
|
+
}
|
|
921
|
+
function getMissingGrammarPackages(pickedBuckets) {
|
|
922
|
+
const grammarCacheDir = getPiPath("grammars");
|
|
923
|
+
return pickedBuckets.filter((bucket) => {
|
|
924
|
+
const packagePath = resolve2(
|
|
925
|
+
grammarCacheDir,
|
|
926
|
+
"node_modules",
|
|
927
|
+
bucket.packageName,
|
|
928
|
+
"package.json"
|
|
929
|
+
);
|
|
930
|
+
return !existsSync3(packagePath);
|
|
931
|
+
}).map((bucket) => `${bucket.packageName}@${bucket.version}`);
|
|
932
|
+
}
|
|
933
|
+
function readGrammarInstallSettings(settings) {
|
|
934
|
+
const rawSettings = settings[GRAMMAR_INSTALL_KEY];
|
|
935
|
+
if (!rawSettings || typeof rawSettings !== "object") return {};
|
|
936
|
+
const settingsMap = rawSettings;
|
|
937
|
+
const allowed = Array.isArray(settingsMap.allowedRepos) ? settingsMap.allowedRepos.filter((repo) => typeof repo === "string") : void 0;
|
|
938
|
+
const denied = Array.isArray(settingsMap.deniedRepos) ? settingsMap.deniedRepos.filter((repo) => typeof repo === "string") : void 0;
|
|
939
|
+
return { allowedRepos: allowed, deniedRepos: denied };
|
|
940
|
+
}
|
|
941
|
+
async function updateGrammarInstallSettings(settings, installSettings) {
|
|
942
|
+
settings[GRAMMAR_INSTALL_KEY] = installSettings;
|
|
943
|
+
await writeJson(getPiPath("settings"), settings);
|
|
944
|
+
}
|
|
945
|
+
async function ensureGrammarCache(ctx) {
|
|
946
|
+
await ensureGrammarCachePackage();
|
|
947
|
+
const repoRoot = await getRepositoryRoot(ctx.cwd);
|
|
948
|
+
if (!repoRoot) return;
|
|
949
|
+
const settings = await readJson(getPiPath("settings"), {});
|
|
950
|
+
const installSettings = readGrammarInstallSettings(settings);
|
|
951
|
+
const allowed = new Set(installSettings.allowedRepos ?? []);
|
|
952
|
+
const denied = new Set(installSettings.deniedRepos ?? []);
|
|
953
|
+
if (denied.has(repoRoot)) return;
|
|
954
|
+
const pickedBuckets = await selectSupportedBuckets(ctx.cwd);
|
|
955
|
+
if (pickedBuckets.length === 0) return;
|
|
956
|
+
const missingPkgs = getMissingGrammarPackages(pickedBuckets);
|
|
957
|
+
if (missingPkgs.length === 0) return;
|
|
958
|
+
let canInstall = allowed.has(repoRoot);
|
|
959
|
+
if (!canInstall) {
|
|
960
|
+
if (!ctx.hasUI) return;
|
|
961
|
+
canInstall = await ctx.ui.confirm(
|
|
962
|
+
"Install tree-sitter grammars for this repo?",
|
|
963
|
+
"surgent installs missing parser grammars for optimized code read, input tokens and context optimizations."
|
|
964
|
+
);
|
|
965
|
+
if (!canInstall) {
|
|
966
|
+
denied.add(repoRoot);
|
|
967
|
+
await updateGrammarInstallSettings(settings, {
|
|
968
|
+
allowedRepos: [...allowed],
|
|
969
|
+
deniedRepos: [...denied]
|
|
970
|
+
});
|
|
971
|
+
return;
|
|
972
|
+
}
|
|
973
|
+
allowed.add(repoRoot);
|
|
974
|
+
await updateGrammarInstallSettings(settings, {
|
|
975
|
+
allowedRepos: [...allowed],
|
|
976
|
+
deniedRepos: [...denied]
|
|
977
|
+
});
|
|
978
|
+
}
|
|
979
|
+
await runCommand(getPiPath("grammars"), "npm", ["install", "--save-exact", ...missingPkgs]);
|
|
980
|
+
}
|
|
981
|
+
async function loadGrammarModule(pkgName) {
|
|
982
|
+
const grammarCacheDir = getPiPath("grammars");
|
|
983
|
+
const cacheRequire = createRequire(resolve2(grammarCacheDir, "package.json"));
|
|
984
|
+
let entryPath = "";
|
|
985
|
+
try {
|
|
986
|
+
entryPath = cacheRequire.resolve(pkgName);
|
|
987
|
+
} catch (error) {
|
|
988
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
989
|
+
throw new Error(
|
|
990
|
+
`grammar package not installed in cache (${grammarCacheDir}): ${pkgName}. ${message}`
|
|
991
|
+
);
|
|
992
|
+
}
|
|
993
|
+
try {
|
|
994
|
+
return await import(pathToFileURL(entryPath).href);
|
|
995
|
+
} catch (error) {
|
|
996
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
997
|
+
throw new Error(
|
|
998
|
+
`failed loading grammar module from cache (${grammarCacheDir}): ${pkgName}. ${message}`
|
|
999
|
+
);
|
|
1000
|
+
}
|
|
1001
|
+
}
|
|
1002
|
+
|
|
1003
|
+
// src/optimizer/languages/types.ts
|
|
1004
|
+
var RuleBasedLanguageProfile = class {
|
|
1005
|
+
constructor(extensions, nameFieldByType, topLevelRoots, topLevelParents, typeRule) {
|
|
1006
|
+
this.extensions = extensions;
|
|
1007
|
+
this.nameFieldByType = nameFieldByType;
|
|
1008
|
+
this.topLevelRoots = topLevelRoots;
|
|
1009
|
+
this.topLevelParents = topLevelParents;
|
|
1010
|
+
this.typeRule = typeRule;
|
|
1011
|
+
}
|
|
1012
|
+
extensions;
|
|
1013
|
+
nameFieldByType;
|
|
1014
|
+
topLevelRoots;
|
|
1015
|
+
topLevelParents;
|
|
1016
|
+
typeRule;
|
|
1017
|
+
findContainerNode(node) {
|
|
1018
|
+
let current = node.parent;
|
|
1019
|
+
while (current) {
|
|
1020
|
+
if (this.typeRule.has(current.type)) {
|
|
1021
|
+
return current;
|
|
1022
|
+
}
|
|
1023
|
+
current = current.parent;
|
|
1024
|
+
}
|
|
1025
|
+
}
|
|
1026
|
+
matchesTopLevelRule(node) {
|
|
1027
|
+
let current = node.parent;
|
|
1028
|
+
while (current) {
|
|
1029
|
+
if (this.topLevelRoots.has(current.type)) {
|
|
1030
|
+
return true;
|
|
1031
|
+
}
|
|
1032
|
+
if (this.topLevelParents.size > 0 && !this.topLevelParents.has(current.type)) {
|
|
1033
|
+
return false;
|
|
1034
|
+
}
|
|
1035
|
+
current = current.parent;
|
|
1036
|
+
}
|
|
1037
|
+
return false;
|
|
1038
|
+
}
|
|
1039
|
+
readFieldText(node, fieldName) {
|
|
1040
|
+
return this.readNodeText(node.childForFieldName(fieldName));
|
|
1041
|
+
}
|
|
1042
|
+
readNameField(node) {
|
|
1043
|
+
const fieldName = this.nameFieldByType[node.type] ?? this.nameFieldByType.__default__;
|
|
1044
|
+
return this.readFieldText(node, fieldName);
|
|
1045
|
+
}
|
|
1046
|
+
readNodeText(node) {
|
|
1047
|
+
if (!node) return;
|
|
1048
|
+
const nodeText = node.text.trim().replaceAll("\n", " ");
|
|
1049
|
+
return nodeText.length > 0 ? nodeText : void 0;
|
|
1050
|
+
}
|
|
1051
|
+
readContainerName(node) {
|
|
1052
|
+
const containerNode = this.findContainerNode(node);
|
|
1053
|
+
return containerNode ? this.readNodeName(containerNode) : void 0;
|
|
1054
|
+
}
|
|
1055
|
+
isPublicSymbol(node) {
|
|
1056
|
+
return this.findContainerNode(node)?.type === "export_statement";
|
|
1057
|
+
}
|
|
1058
|
+
resolveSymbolKind(node) {
|
|
1059
|
+
const symbolKindRules = this.typeRule.get(node.type);
|
|
1060
|
+
if (!symbolKindRules) return;
|
|
1061
|
+
for (const symbolKindRule of symbolKindRules) {
|
|
1062
|
+
if (symbolKindRule.parent && node.parent?.type !== symbolKindRule.parent) {
|
|
1063
|
+
continue;
|
|
1064
|
+
}
|
|
1065
|
+
if (symbolKindRule.container) {
|
|
1066
|
+
const containerNode = this.findContainerNode(node);
|
|
1067
|
+
if (!containerNode || containerNode.type !== symbolKindRule.container) {
|
|
1068
|
+
continue;
|
|
1069
|
+
}
|
|
1070
|
+
}
|
|
1071
|
+
if (symbolKindRule.topLevelOnly && !this.matchesTopLevelRule(node)) {
|
|
1072
|
+
continue;
|
|
1073
|
+
}
|
|
1074
|
+
return symbolKindRule.kind;
|
|
1075
|
+
}
|
|
1076
|
+
}
|
|
1077
|
+
shouldSkipSymbol(_node) {
|
|
1078
|
+
return false;
|
|
1079
|
+
}
|
|
1080
|
+
};
|
|
1081
|
+
|
|
1082
|
+
// src/optimizer/languages/typescript.ts
|
|
1083
|
+
var TypeScriptLanguageProfile = class extends RuleBasedLanguageProfile {
|
|
1084
|
+
constructor() {
|
|
1085
|
+
super(
|
|
1086
|
+
/* @__PURE__ */ new Set([".ts", ".tsx", ".js", ".mjs", ".cjs", ".jsx"]),
|
|
1087
|
+
{ __default__: "name" },
|
|
1088
|
+
/* @__PURE__ */ new Set(["program"]),
|
|
1089
|
+
/* @__PURE__ */ new Set([
|
|
1090
|
+
"lexical_declaration",
|
|
1091
|
+
"variable_declaration",
|
|
1092
|
+
"variable_declarator",
|
|
1093
|
+
"export_statement"
|
|
1094
|
+
]),
|
|
1095
|
+
/* @__PURE__ */ new Map([
|
|
1096
|
+
["function_declaration", [{ kind: "func", topLevelOnly: true }]],
|
|
1097
|
+
["arrow_function", [{ kind: "func", topLevelOnly: true }]],
|
|
1098
|
+
["function_expression", [{ kind: "func", topLevelOnly: true }]],
|
|
1099
|
+
["abstract_class_declaration", [{ kind: "class" }]],
|
|
1100
|
+
["class_declaration", [{ kind: "class" }]],
|
|
1101
|
+
["abstract_method_signature", [{ kind: "method", parent: "class_body" }]],
|
|
1102
|
+
[
|
|
1103
|
+
"method_definition",
|
|
1104
|
+
[
|
|
1105
|
+
{ kind: "method", parent: "class_body" },
|
|
1106
|
+
{ kind: "method", parent: "object" }
|
|
1107
|
+
]
|
|
1108
|
+
],
|
|
1109
|
+
["variable_declarator", [{ kind: "decl", topLevelOnly: true }]],
|
|
1110
|
+
["import_specifier", [{ kind: "deps" }]],
|
|
1111
|
+
["namespace_import", [{ kind: "deps" }]],
|
|
1112
|
+
["identifier", [{ kind: "deps", parent: "import_clause" }]],
|
|
1113
|
+
["export_statement", [{ kind: "public", topLevelOnly: true }]]
|
|
1114
|
+
])
|
|
1115
|
+
);
|
|
1116
|
+
}
|
|
1117
|
+
async loadLanguage(extension) {
|
|
1118
|
+
const languagePack = await loadGrammarModule("tree-sitter-typescript");
|
|
1119
|
+
const languageExports = typeof languagePack.default === "object" && languagePack.default !== null ? languagePack.default : languagePack;
|
|
1120
|
+
const typeScriptLanguage = languageExports.typescript;
|
|
1121
|
+
if (!typeScriptLanguage) {
|
|
1122
|
+
throw new Error("tree-sitter-typescript missing typescript export");
|
|
1123
|
+
}
|
|
1124
|
+
if (extension === ".tsx" || extension === ".jsx") {
|
|
1125
|
+
return languageExports.tsx ?? typeScriptLanguage;
|
|
1126
|
+
}
|
|
1127
|
+
return typeScriptLanguage;
|
|
1128
|
+
}
|
|
1129
|
+
readNodeName(node) {
|
|
1130
|
+
if (node.type === "export_statement") {
|
|
1131
|
+
return "exports";
|
|
1132
|
+
}
|
|
1133
|
+
if (node.type === "import_specifier") {
|
|
1134
|
+
const aliasText = this.readFieldText(node, "alias");
|
|
1135
|
+
if (aliasText) {
|
|
1136
|
+
return aliasText;
|
|
1137
|
+
}
|
|
1138
|
+
}
|
|
1139
|
+
const nodeName = this.readNameField(node);
|
|
1140
|
+
if (nodeName) {
|
|
1141
|
+
return nodeName;
|
|
1142
|
+
}
|
|
1143
|
+
if (node.type === "namespace_import") {
|
|
1144
|
+
return this.readNodeText(node.namedChild(0));
|
|
1145
|
+
}
|
|
1146
|
+
if (node.type === "identifier") {
|
|
1147
|
+
return this.readNodeText(node);
|
|
1148
|
+
}
|
|
1149
|
+
if (node.type === "arrow_function" || node.type === "function_expression" || node.type === "function_declaration") {
|
|
1150
|
+
const declaratorNode = node.parent?.type === "variable_declarator" ? node.parent : void 0;
|
|
1151
|
+
if (declaratorNode) {
|
|
1152
|
+
const declaratorName = this.readFieldText(declaratorNode, "name");
|
|
1153
|
+
if (declaratorName) {
|
|
1154
|
+
return declaratorName;
|
|
1155
|
+
}
|
|
1156
|
+
}
|
|
1157
|
+
return "anonymous";
|
|
1158
|
+
}
|
|
1159
|
+
}
|
|
1160
|
+
shouldSkipSymbol(node) {
|
|
1161
|
+
if (node.type === "export_statement") {
|
|
1162
|
+
const inlineNode = node.namedChild(0);
|
|
1163
|
+
return node.childForFieldName("declaration") !== null || inlineNode?.type === "arrow_function" || inlineNode?.type === "function_expression" || inlineNode?.type === "function_declaration" || inlineNode?.type === "class_declaration" && inlineNode.childForFieldName("name") !== null;
|
|
1164
|
+
}
|
|
1165
|
+
if (node.type === "variable_declarator") {
|
|
1166
|
+
return node.childForFieldName("value")?.type === "arrow_function";
|
|
1167
|
+
}
|
|
1168
|
+
return node.type === "function_expression" && node.childForFieldName("name") !== null;
|
|
1169
|
+
}
|
|
1170
|
+
};
|
|
1171
|
+
|
|
1172
|
+
// src/optimizer/languages/python.ts
|
|
1173
|
+
var PythonLanguageProfile = class extends RuleBasedLanguageProfile {
|
|
1174
|
+
constructor() {
|
|
1175
|
+
super(
|
|
1176
|
+
/* @__PURE__ */ new Set([".py"]),
|
|
1177
|
+
{
|
|
1178
|
+
__default__: "name",
|
|
1179
|
+
assignment: "left",
|
|
1180
|
+
future_import_statement: "name",
|
|
1181
|
+
import_from_statement: "name",
|
|
1182
|
+
import_statement: "name"
|
|
1183
|
+
},
|
|
1184
|
+
/* @__PURE__ */ new Set(["module"]),
|
|
1185
|
+
/* @__PURE__ */ new Set(["decorated_definition", "expression_statement"]),
|
|
1186
|
+
/* @__PURE__ */ new Map([
|
|
1187
|
+
["class_definition", [{ kind: "class" }]],
|
|
1188
|
+
[
|
|
1189
|
+
"function_definition",
|
|
1190
|
+
[
|
|
1191
|
+
{ kind: "method", container: "class_definition" },
|
|
1192
|
+
{ kind: "func", topLevelOnly: true }
|
|
1193
|
+
]
|
|
1194
|
+
],
|
|
1195
|
+
["assignment", [{ kind: "decl", topLevelOnly: true }]],
|
|
1196
|
+
["future_import_statement", [{ kind: "deps", topLevelOnly: true }]],
|
|
1197
|
+
["import_from_statement", [{ kind: "deps", topLevelOnly: true }]],
|
|
1198
|
+
["import_statement", [{ kind: "deps", topLevelOnly: true }]]
|
|
1199
|
+
])
|
|
1200
|
+
);
|
|
1201
|
+
}
|
|
1202
|
+
async loadLanguage(_extension) {
|
|
1203
|
+
const languagePack = await loadGrammarModule("tree-sitter-python");
|
|
1204
|
+
const languageExport = languagePack.default ?? languagePack;
|
|
1205
|
+
return languageExport;
|
|
1206
|
+
}
|
|
1207
|
+
resolveSymbolKind(node) {
|
|
1208
|
+
if (node.type === "assignment" && this.matchesTopLevelRule(node) && this.readFieldText(node, "left") === "__all__") {
|
|
1209
|
+
return "public";
|
|
1210
|
+
}
|
|
1211
|
+
return super.resolveSymbolKind(node);
|
|
1212
|
+
}
|
|
1213
|
+
readNodeName(node) {
|
|
1214
|
+
const nodeName = this.readNameField(node);
|
|
1215
|
+
if (nodeName) {
|
|
1216
|
+
return nodeName;
|
|
1217
|
+
}
|
|
1218
|
+
if (node.type === "dotted_name" || node.type === "identifier") {
|
|
1219
|
+
return this.readNodeText(node);
|
|
1220
|
+
}
|
|
1221
|
+
}
|
|
1222
|
+
};
|
|
1223
|
+
|
|
1224
|
+
// src/optimizer/languages/go.ts
|
|
1225
|
+
var GoLanguageProfile = class extends RuleBasedLanguageProfile {
|
|
1226
|
+
constructor() {
|
|
1227
|
+
super(
|
|
1228
|
+
/* @__PURE__ */ new Set([".go"]),
|
|
1229
|
+
{ __default__: "name" },
|
|
1230
|
+
/* @__PURE__ */ new Set(["source_file"]),
|
|
1231
|
+
/* @__PURE__ */ new Set([
|
|
1232
|
+
"const_declaration",
|
|
1233
|
+
"import_declaration",
|
|
1234
|
+
"import_spec_list",
|
|
1235
|
+
"type_declaration",
|
|
1236
|
+
"var_declaration",
|
|
1237
|
+
"var_spec_list"
|
|
1238
|
+
]),
|
|
1239
|
+
/* @__PURE__ */ new Map([
|
|
1240
|
+
["function_declaration", [{ kind: "func", topLevelOnly: true }]],
|
|
1241
|
+
["method_declaration", [{ kind: "method" }]],
|
|
1242
|
+
["method_elem", [{ kind: "method", parent: "interface_type", container: "type_spec" }]],
|
|
1243
|
+
["const_spec", [{ kind: "decl", topLevelOnly: true }]],
|
|
1244
|
+
["import_spec", [{ kind: "deps", topLevelOnly: true }]],
|
|
1245
|
+
["type_alias", [{ kind: "class", topLevelOnly: true }]],
|
|
1246
|
+
["type_spec", [{ kind: "class", topLevelOnly: true }]],
|
|
1247
|
+
["var_spec", [{ kind: "decl", topLevelOnly: true }]]
|
|
1248
|
+
])
|
|
1249
|
+
);
|
|
1250
|
+
}
|
|
1251
|
+
async loadLanguage(_extension) {
|
|
1252
|
+
const languagePack = await loadGrammarModule("tree-sitter-go");
|
|
1253
|
+
const languageExport = languagePack.default ?? languagePack;
|
|
1254
|
+
return languageExport;
|
|
1255
|
+
}
|
|
1256
|
+
readNodeName(node) {
|
|
1257
|
+
if (node.type === "import_spec") {
|
|
1258
|
+
const importName = this.readFieldText(node, "name");
|
|
1259
|
+
if (importName) {
|
|
1260
|
+
return importName;
|
|
1261
|
+
}
|
|
1262
|
+
const importPath = this.readFieldText(node, "path");
|
|
1263
|
+
if (importPath) {
|
|
1264
|
+
return importPath.replace(/^("|')(.*)\1$/, "$2");
|
|
1265
|
+
}
|
|
1266
|
+
}
|
|
1267
|
+
const nodeName = this.readNameField(node);
|
|
1268
|
+
if (nodeName) {
|
|
1269
|
+
return nodeName;
|
|
1270
|
+
}
|
|
1271
|
+
if (node.type === "field_identifier" || node.type === "identifier" || node.type === "package_identifier" || node.type === "type_identifier") {
|
|
1272
|
+
return this.readNodeText(node);
|
|
1273
|
+
}
|
|
1274
|
+
}
|
|
1275
|
+
readContainerName(node) {
|
|
1276
|
+
if (node.type === "method_declaration") {
|
|
1277
|
+
const receiverNode = node.childForFieldName("receiver");
|
|
1278
|
+
const receiverParameterNode = receiverNode?.namedChild(0);
|
|
1279
|
+
const receiverTypeNode = receiverParameterNode?.childForFieldName("type");
|
|
1280
|
+
const receiverType = this.readNodeText(receiverTypeNode);
|
|
1281
|
+
if (receiverType) {
|
|
1282
|
+
return receiverType.replace(/^\*+/, "");
|
|
1283
|
+
}
|
|
1284
|
+
}
|
|
1285
|
+
return super.readContainerName(node);
|
|
1286
|
+
}
|
|
1287
|
+
};
|
|
1288
|
+
|
|
1289
|
+
// src/optimizer/languages/java.ts
|
|
1290
|
+
var JavaLanguageProfile = class extends RuleBasedLanguageProfile {
|
|
1291
|
+
constructor() {
|
|
1292
|
+
super(
|
|
1293
|
+
/* @__PURE__ */ new Set([".java"]),
|
|
1294
|
+
{ __default__: "name", exports_module_directive: "package" },
|
|
1295
|
+
/* @__PURE__ */ new Set(["program"]),
|
|
1296
|
+
/* @__PURE__ */ new Set(),
|
|
1297
|
+
/* @__PURE__ */ new Map([
|
|
1298
|
+
["annotation_type_declaration", [{ kind: "class" }]],
|
|
1299
|
+
["class_declaration", [{ kind: "class" }]],
|
|
1300
|
+
["enum_declaration", [{ kind: "class" }]],
|
|
1301
|
+
["interface_declaration", [{ kind: "class" }]],
|
|
1302
|
+
["record_declaration", [{ kind: "class" }]],
|
|
1303
|
+
[
|
|
1304
|
+
"constructor_declaration",
|
|
1305
|
+
[
|
|
1306
|
+
{ kind: "method", parent: "class_body" },
|
|
1307
|
+
{ kind: "method", parent: "enum_body_declarations" }
|
|
1308
|
+
]
|
|
1309
|
+
],
|
|
1310
|
+
[
|
|
1311
|
+
"method_declaration",
|
|
1312
|
+
[
|
|
1313
|
+
{ kind: "method", parent: "class_body" },
|
|
1314
|
+
{ kind: "method", parent: "enum_body_declarations" },
|
|
1315
|
+
{ kind: "method", parent: "interface_body" }
|
|
1316
|
+
]
|
|
1317
|
+
],
|
|
1318
|
+
["import_declaration", [{ kind: "deps" }]],
|
|
1319
|
+
["exports_module_directive", [{ kind: "public", parent: "module_body" }]]
|
|
1320
|
+
])
|
|
1321
|
+
);
|
|
1322
|
+
}
|
|
1323
|
+
async loadLanguage(_extension) {
|
|
1324
|
+
const languagePack = await loadGrammarModule("tree-sitter-java");
|
|
1325
|
+
const languageExport = languagePack.default ?? languagePack;
|
|
1326
|
+
return languageExport;
|
|
1327
|
+
}
|
|
1328
|
+
readNodeName(node) {
|
|
1329
|
+
const nodeName = this.readNameField(node);
|
|
1330
|
+
if (nodeName) {
|
|
1331
|
+
return nodeName;
|
|
1332
|
+
}
|
|
1333
|
+
if (node.type === "import_declaration") {
|
|
1334
|
+
const importText = node.text.replace(/^import\s+/, "").replace(/\s*;\s*$/, "").trim().replaceAll("\n", " ");
|
|
1335
|
+
return importText.length > 0 ? importText : void 0;
|
|
1336
|
+
}
|
|
1337
|
+
if (node.type === "identifier" || node.type === "scoped_identifier") {
|
|
1338
|
+
return this.readNodeText(node);
|
|
1339
|
+
}
|
|
1340
|
+
}
|
|
1341
|
+
};
|
|
1342
|
+
|
|
1343
|
+
// src/optimizer/languages/rust.ts
|
|
1344
|
+
var RustLanguageProfile = class extends RuleBasedLanguageProfile {
|
|
1345
|
+
constructor() {
|
|
1346
|
+
super(
|
|
1347
|
+
/* @__PURE__ */ new Set([".rs"]),
|
|
1348
|
+
{ __default__: "name", use_declaration: "argument" },
|
|
1349
|
+
/* @__PURE__ */ new Set(["source_file"]),
|
|
1350
|
+
/* @__PURE__ */ new Set(["declaration_list", "foreign_mod_item", "mod_item"]),
|
|
1351
|
+
/* @__PURE__ */ new Map([
|
|
1352
|
+
[
|
|
1353
|
+
"function_item",
|
|
1354
|
+
[
|
|
1355
|
+
{ kind: "method", container: "impl_item" },
|
|
1356
|
+
{ kind: "method", container: "trait_item" },
|
|
1357
|
+
{ kind: "func", topLevelOnly: true }
|
|
1358
|
+
]
|
|
1359
|
+
],
|
|
1360
|
+
[
|
|
1361
|
+
"function_signature_item",
|
|
1362
|
+
[
|
|
1363
|
+
{ kind: "method", container: "trait_item" },
|
|
1364
|
+
{ kind: "func", topLevelOnly: true }
|
|
1365
|
+
]
|
|
1366
|
+
],
|
|
1367
|
+
["struct_item", [{ kind: "class", topLevelOnly: true }]],
|
|
1368
|
+
["enum_item", [{ kind: "class", topLevelOnly: true }]],
|
|
1369
|
+
["union_item", [{ kind: "class", topLevelOnly: true }]],
|
|
1370
|
+
["trait_item", [{ kind: "class", topLevelOnly: true }]],
|
|
1371
|
+
["type_item", [{ kind: "class", topLevelOnly: true }]],
|
|
1372
|
+
["mod_item", [{ kind: "class", topLevelOnly: true }]],
|
|
1373
|
+
["impl_item", [{ kind: "class" }]],
|
|
1374
|
+
["macro_definition", [{ kind: "func", topLevelOnly: true }]],
|
|
1375
|
+
["const_item", [{ kind: "decl", topLevelOnly: true }]],
|
|
1376
|
+
["static_item", [{ kind: "decl", topLevelOnly: true }]],
|
|
1377
|
+
["use_declaration", [{ kind: "deps", topLevelOnly: true }]]
|
|
1378
|
+
])
|
|
1379
|
+
);
|
|
1380
|
+
}
|
|
1381
|
+
async loadLanguage(_extension) {
|
|
1382
|
+
const languagePack = await loadGrammarModule("tree-sitter-rust");
|
|
1383
|
+
const languageExport = languagePack.default ?? languagePack;
|
|
1384
|
+
return languageExport;
|
|
1385
|
+
}
|
|
1386
|
+
readNodeName(node) {
|
|
1387
|
+
if (node.type === "impl_item") {
|
|
1388
|
+
return this.readFieldText(node, "type");
|
|
1389
|
+
}
|
|
1390
|
+
const nodeName = this.readNameField(node);
|
|
1391
|
+
if (nodeName) {
|
|
1392
|
+
return nodeName;
|
|
1393
|
+
}
|
|
1394
|
+
if (node.type === "identifier" || node.type === "type_identifier") {
|
|
1395
|
+
return this.readNodeText(node);
|
|
1396
|
+
}
|
|
1397
|
+
}
|
|
1398
|
+
isPublicSymbol(node) {
|
|
1399
|
+
return node.namedChildren.some((childNode) => childNode.type === "visibility_modifier");
|
|
1400
|
+
}
|
|
1401
|
+
shouldSkipSymbol(node) {
|
|
1402
|
+
return node.type === "impl_item";
|
|
1403
|
+
}
|
|
1404
|
+
};
|
|
1405
|
+
|
|
1406
|
+
// src/optimizer/languages/symbols.ts
|
|
1407
|
+
import { readFile as readFile3 } from "node:fs/promises";
|
|
1408
|
+
import { extname, resolve as resolve3 } from "node:path";
|
|
1409
|
+
import Parser from "tree-sitter";
|
|
1410
|
+
async function createCodeParser(extension) {
|
|
1411
|
+
const languageProfile = getLanguageProfile(extension);
|
|
1412
|
+
if (!languageProfile) {
|
|
1413
|
+
throw new Error(`missing grammar for extension: ${extension}`);
|
|
1414
|
+
}
|
|
1415
|
+
const parser = new Parser();
|
|
1416
|
+
parser.setLanguage(await languageProfile.loadLanguage(extension));
|
|
1417
|
+
return parser;
|
|
1418
|
+
}
|
|
1419
|
+
async function getParserForPath(path, parsers) {
|
|
1420
|
+
const extension = extname(path).toLowerCase();
|
|
1421
|
+
const parser = parsers.get(extension);
|
|
1422
|
+
if (parser) return parser;
|
|
1423
|
+
const createdParser = await createCodeParser(extension);
|
|
1424
|
+
parsers.set(extension, createdParser);
|
|
1425
|
+
return createdParser;
|
|
1426
|
+
}
|
|
1427
|
+
async function getRootNode(cwd, path) {
|
|
1428
|
+
const parsers = /* @__PURE__ */ new Map();
|
|
1429
|
+
const absolutePath = resolve3(cwd, path);
|
|
1430
|
+
let code = "";
|
|
1431
|
+
try {
|
|
1432
|
+
code = await readFile3(absolutePath, "utf8");
|
|
1433
|
+
} catch {
|
|
1434
|
+
return;
|
|
1435
|
+
}
|
|
1436
|
+
const parser = await getParserForPath(path, parsers);
|
|
1437
|
+
const tree = parser.parse(code);
|
|
1438
|
+
return tree.rootNode;
|
|
1439
|
+
}
|
|
1440
|
+
async function collectSymbols(cwd, path, kinds) {
|
|
1441
|
+
const extension = extname(path).toLowerCase();
|
|
1442
|
+
const profile = getLanguageProfile(extension);
|
|
1443
|
+
if (!profile) return [];
|
|
1444
|
+
const root = await getRootNode(cwd, path);
|
|
1445
|
+
if (!root) return [];
|
|
1446
|
+
const symbols = [];
|
|
1447
|
+
const pendingNodes = [root];
|
|
1448
|
+
const symbolIdCounts = /* @__PURE__ */ new Map();
|
|
1449
|
+
while (pendingNodes.length > 0) {
|
|
1450
|
+
const currentNode = pendingNodes.pop();
|
|
1451
|
+
if (!currentNode) continue;
|
|
1452
|
+
for (let childIndex = currentNode.namedChildCount - 1; childIndex >= 0; childIndex -= 1) {
|
|
1453
|
+
const namedChild = currentNode.namedChild(childIndex);
|
|
1454
|
+
if (namedChild) {
|
|
1455
|
+
pendingNodes.push(namedChild);
|
|
1456
|
+
}
|
|
1457
|
+
}
|
|
1458
|
+
const symbolKind = profile.resolveSymbolKind(currentNode);
|
|
1459
|
+
if (!symbolKind || !kinds.has(symbolKind) || profile.shouldSkipSymbol(currentNode)) {
|
|
1460
|
+
continue;
|
|
1461
|
+
}
|
|
1462
|
+
const baseName = profile.readNodeName(currentNode);
|
|
1463
|
+
if (!baseName) continue;
|
|
1464
|
+
const containerName = profile.readContainerName(currentNode);
|
|
1465
|
+
const symbolName = symbolKind === "method" && containerName ? `${containerName}.${baseName}` : baseName;
|
|
1466
|
+
const symbolIdCount = (symbolIdCounts.get(symbolName) ?? 0) + 1;
|
|
1467
|
+
symbolIdCounts.set(symbolName, symbolIdCount);
|
|
1468
|
+
const isAnonymous = symbolName === "anonymous" && !currentNode.childForFieldName("name");
|
|
1469
|
+
symbols.push({
|
|
1470
|
+
name: isAnonymous || symbolIdCount > 1 ? `${symbolName}~${symbolIdCount}` : symbolName,
|
|
1471
|
+
path,
|
|
1472
|
+
kind: symbolKind,
|
|
1473
|
+
node: currentNode,
|
|
1474
|
+
range: [currentNode.startPosition.row + 1, currentNode.endPosition.row + 1],
|
|
1475
|
+
public: profile.isPublicSymbol(currentNode)
|
|
1476
|
+
});
|
|
1477
|
+
}
|
|
1478
|
+
return symbols;
|
|
1479
|
+
}
|
|
1480
|
+
|
|
1481
|
+
// src/optimizer/languages/index.ts
|
|
1482
|
+
var LANGUAGE_REGISTRY = [
|
|
1483
|
+
{
|
|
1484
|
+
bucketName: "typescript",
|
|
1485
|
+
packageName: "tree-sitter-typescript",
|
|
1486
|
+
version: "0.23.2",
|
|
1487
|
+
profile: new TypeScriptLanguageProfile()
|
|
1488
|
+
},
|
|
1489
|
+
{
|
|
1490
|
+
bucketName: "python",
|
|
1491
|
+
packageName: "tree-sitter-python",
|
|
1492
|
+
version: "0.25.0",
|
|
1493
|
+
profile: new PythonLanguageProfile()
|
|
1494
|
+
},
|
|
1495
|
+
{
|
|
1496
|
+
bucketName: "go",
|
|
1497
|
+
packageName: "tree-sitter-go",
|
|
1498
|
+
version: "0.25.0",
|
|
1499
|
+
profile: new GoLanguageProfile()
|
|
1500
|
+
},
|
|
1501
|
+
{
|
|
1502
|
+
bucketName: "java",
|
|
1503
|
+
packageName: "tree-sitter-java",
|
|
1504
|
+
version: "0.23.5",
|
|
1505
|
+
profile: new JavaLanguageProfile()
|
|
1506
|
+
},
|
|
1507
|
+
{
|
|
1508
|
+
bucketName: "rust",
|
|
1509
|
+
packageName: "tree-sitter-rust",
|
|
1510
|
+
version: "0.24.0",
|
|
1511
|
+
profile: new RustLanguageProfile()
|
|
1512
|
+
}
|
|
1513
|
+
];
|
|
1514
|
+
function getLanguageProfile(extension) {
|
|
1515
|
+
return LANGUAGE_REGISTRY.find(({ profile }) => profile.extensions.has(extension))?.profile;
|
|
1516
|
+
}
|
|
1517
|
+
function languages_default(pi) {
|
|
1518
|
+
pi.on("session_start", async (_event, ctx) => {
|
|
1519
|
+
try {
|
|
1520
|
+
await ensureGrammarCache(ctx);
|
|
1521
|
+
} catch (error) {
|
|
1522
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1523
|
+
if (ctx.hasUI) {
|
|
1524
|
+
ctx.ui.notify(`tree-sitter grammar install failed: ${message}`, "error");
|
|
1525
|
+
}
|
|
1526
|
+
}
|
|
1527
|
+
});
|
|
1528
|
+
}
|
|
1529
|
+
var SYMBOL_KINDS = ["func", "class", "method", "decl", "deps", "public"];
|
|
1530
|
+
|
|
1531
|
+
// src/optimizer/mapper/index.ts
|
|
1532
|
+
function collapseGroupedSymbols(symbols) {
|
|
1533
|
+
let groupedSymbolKind;
|
|
1534
|
+
let importsGroupIndex = 0;
|
|
1535
|
+
let exportsGroupIndex = 0;
|
|
1536
|
+
return symbols.flatMap((symbol) => {
|
|
1537
|
+
if (symbol.kind !== "deps" && symbol.kind !== "public") {
|
|
1538
|
+
groupedSymbolKind = void 0;
|
|
1539
|
+
return [symbol];
|
|
1540
|
+
}
|
|
1541
|
+
if (symbol.kind === groupedSymbolKind) return [];
|
|
1542
|
+
groupedSymbolKind = symbol.kind;
|
|
1543
|
+
if (symbol.kind === "deps") {
|
|
1544
|
+
importsGroupIndex += 1;
|
|
1545
|
+
return [{ ...symbol, name: `imports~${importsGroupIndex}`, range: void 0 }];
|
|
1546
|
+
}
|
|
1547
|
+
exportsGroupIndex += 1;
|
|
1548
|
+
return [{ ...symbol, name: `exports~${exportsGroupIndex}`, range: void 0 }];
|
|
1549
|
+
});
|
|
1550
|
+
}
|
|
1551
|
+
var codeMap = defineTool({
|
|
1552
|
+
name: "code_map",
|
|
1553
|
+
label: "Code map",
|
|
1554
|
+
description: "Fast symbol and code blocks offset/limit indexing. Best for: narrowing targets to inspect/read or code discovery.",
|
|
1555
|
+
parameters: Type2.Object({
|
|
1556
|
+
targets: Type2.Array(Type2.String(), {
|
|
1557
|
+
description: "Paths or globs to scan (relative to cwd). Keep scope narrow."
|
|
1558
|
+
}),
|
|
1559
|
+
kinds: Type2.Optional(
|
|
1560
|
+
Type2.Array(Type2.Union(SYMBOL_KINDS.map((kind) => Type2.Literal(kind))), {
|
|
1561
|
+
description: "Abstraction kinds to include. Omit to include all supported."
|
|
1562
|
+
})
|
|
1563
|
+
)
|
|
1564
|
+
}),
|
|
1565
|
+
async execute(_toolCallId, params, signal, _onUpdate, ctx) {
|
|
1566
|
+
const kinds = new Set(params.kinds ?? SYMBOL_KINDS);
|
|
1567
|
+
const result = { symbols: [], failed: [] };
|
|
1568
|
+
let paths = [];
|
|
1569
|
+
try {
|
|
1570
|
+
paths = await resolveTargetPaths(ctx.cwd, params.targets, signal);
|
|
1571
|
+
} catch (error) {
|
|
1572
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1573
|
+
return {
|
|
1574
|
+
isError: true,
|
|
1575
|
+
details: `code_map target scan failed: ${message}`,
|
|
1576
|
+
content: [{ type: "text", text: `code_map target scan failed: ${message}` }]
|
|
1577
|
+
};
|
|
1578
|
+
}
|
|
1579
|
+
if (paths.length === 0) {
|
|
1580
|
+
return {
|
|
1581
|
+
isError: false,
|
|
1582
|
+
details: "(no symbols found) targets matched no supported files or symbols",
|
|
1583
|
+
content: [
|
|
1584
|
+
{
|
|
1585
|
+
type: "text",
|
|
1586
|
+
text: "(no symbols found) targets matched no supported files or symbols"
|
|
1587
|
+
}
|
|
1588
|
+
]
|
|
1589
|
+
};
|
|
1590
|
+
}
|
|
1591
|
+
for (const path of paths) {
|
|
1592
|
+
if (signal?.aborted) {
|
|
1593
|
+
return {
|
|
1594
|
+
isError: true,
|
|
1595
|
+
details: "code_map aborted",
|
|
1596
|
+
content: [{ type: "text", text: "code_map aborted" }]
|
|
1597
|
+
};
|
|
1598
|
+
}
|
|
1599
|
+
try {
|
|
1600
|
+
const symbols = collapseGroupedSymbols(await collectSymbols(ctx.cwd, path, kinds));
|
|
1601
|
+
result.symbols.push(...symbols);
|
|
1602
|
+
} catch (error) {
|
|
1603
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1604
|
+
result.failed.push(`${path}: ${message}`);
|
|
1605
|
+
}
|
|
1606
|
+
}
|
|
1607
|
+
const outputLines = [];
|
|
1608
|
+
let outputPath = "";
|
|
1609
|
+
for (const symbol of result.symbols) {
|
|
1610
|
+
if (symbol.path !== outputPath) {
|
|
1611
|
+
outputPath = symbol.path;
|
|
1612
|
+
outputLines.push(symbol.path);
|
|
1613
|
+
}
|
|
1614
|
+
let line = ` [${symbol.public ? "public " : ""}${symbol.kind}] ${symbol.name}`;
|
|
1615
|
+
if (symbol.range) {
|
|
1616
|
+
line += ` L${symbol.range[0]}-L${symbol.range[1]}`;
|
|
1617
|
+
}
|
|
1618
|
+
outputLines.push(line);
|
|
1619
|
+
}
|
|
1620
|
+
if (result.failed.length > 0) {
|
|
1621
|
+
outputLines.push(...result.failed.map((failure) => `failed ${failure}`));
|
|
1622
|
+
}
|
|
1623
|
+
if (outputLines.length === 0) {
|
|
1624
|
+
outputLines.push("(no symbols found) targets matched no supported files or symbols");
|
|
1625
|
+
}
|
|
1626
|
+
const truncation = truncateHead(outputLines.join("\n"));
|
|
1627
|
+
const output = truncation.truncated ? `${truncation.content}
|
|
1628
|
+
|
|
1629
|
+
[Output truncated at 2000 lines or 50KB. Narrow targets or kinds.]` : truncation.content;
|
|
1630
|
+
return { isError: false, details: output, content: [{ type: "text", text: output }] };
|
|
1631
|
+
},
|
|
1632
|
+
renderCall(args, theme, { isPartial }) {
|
|
1633
|
+
const targets = Array.isArray(args.targets) ? args.targets.join(", ") : "";
|
|
1634
|
+
const kinds = Array.isArray(args.kinds) ? args.kinds.join(", ") : "default";
|
|
1635
|
+
return renderCallText(
|
|
1636
|
+
`${theme.fg("toolTitle", "code_map")} ${theme.underline(theme.fg("accent", targets))} ${theme.fg("dim", `[${kinds}]`)}`,
|
|
1637
|
+
isPartial
|
|
1638
|
+
);
|
|
1639
|
+
}
|
|
1640
|
+
});
|
|
1641
|
+
var mapper_default = codeMap;
|
|
1642
|
+
|
|
1643
|
+
// src/optimizer/inspector/index.ts
|
|
1644
|
+
import { defineTool as defineTool2 } from "@earendil-works/pi-coding-agent";
|
|
1645
|
+
import { Text as Text2 } from "@earendil-works/pi-tui";
|
|
1646
|
+
import { Type as Type3 } from "typebox";
|
|
1647
|
+
|
|
1648
|
+
// src/optimizer/inspector/inspect.ts
|
|
1649
|
+
function inspectGroupedSymbols(path, symbolName, symbols) {
|
|
1650
|
+
const groupedSymbolsMatch = /^(imports|exports)~([1-9]\d*)$/.exec(symbolName);
|
|
1651
|
+
if (!groupedSymbolsMatch) return;
|
|
1652
|
+
const groupedSymbolKind = groupedSymbolsMatch[1] === "imports" ? "deps" : "public";
|
|
1653
|
+
const requestedGroup = Number(groupedSymbolsMatch[2]);
|
|
1654
|
+
const groupedNodes = [];
|
|
1655
|
+
const groupedRanges = /* @__PURE__ */ new Set();
|
|
1656
|
+
let symbolsCollapsed = false;
|
|
1657
|
+
let symbolsGroupIndex = 0;
|
|
1658
|
+
for (const symbol of symbols) {
|
|
1659
|
+
if (symbol.kind !== groupedSymbolKind) {
|
|
1660
|
+
symbolsCollapsed = false;
|
|
1661
|
+
continue;
|
|
1662
|
+
}
|
|
1663
|
+
if (!symbolsCollapsed) {
|
|
1664
|
+
symbolsCollapsed = true;
|
|
1665
|
+
symbolsGroupIndex += 1;
|
|
1666
|
+
}
|
|
1667
|
+
if (symbolsGroupIndex !== requestedGroup) continue;
|
|
1668
|
+
let groupedNode = symbol.node;
|
|
1669
|
+
if (groupedSymbolKind === "deps") {
|
|
1670
|
+
while (groupedNode.parent?.parent) {
|
|
1671
|
+
groupedNode = groupedNode.parent;
|
|
1672
|
+
}
|
|
1673
|
+
}
|
|
1674
|
+
const groupedRange = `${groupedNode.startIndex}-${groupedNode.endIndex}`;
|
|
1675
|
+
if (groupedRanges.has(groupedRange)) continue;
|
|
1676
|
+
groupedRanges.add(groupedRange);
|
|
1677
|
+
groupedNodes.push(groupedNode);
|
|
1678
|
+
}
|
|
1679
|
+
const firstNode = groupedNodes[0];
|
|
1680
|
+
const lastNode = groupedNodes[groupedNodes.length - 1];
|
|
1681
|
+
if (!firstNode || !lastNode) return;
|
|
1682
|
+
return {
|
|
1683
|
+
path,
|
|
1684
|
+
symbol: symbolName,
|
|
1685
|
+
text: groupedNodes.map((groupedNode) => groupedNode.text).join("\n"),
|
|
1686
|
+
range: [
|
|
1687
|
+
firstNode.startPosition.row + 1,
|
|
1688
|
+
lastNode.endPosition.row + (lastNode.endPosition.column > 0 ? 1 : 0)
|
|
1689
|
+
]
|
|
1690
|
+
};
|
|
1691
|
+
}
|
|
1692
|
+
async function inspectSymbol(cwd, path, symbolName, signal) {
|
|
1693
|
+
if (signal?.aborted) {
|
|
1694
|
+
throw new Error("inspector aborted");
|
|
1695
|
+
}
|
|
1696
|
+
const kinds = new Set(SYMBOL_KINDS);
|
|
1697
|
+
const symbols = await collectSymbols(cwd, path, kinds);
|
|
1698
|
+
const inspectedGroup = inspectGroupedSymbols(path, symbolName, symbols);
|
|
1699
|
+
if (inspectedGroup) return inspectedGroup;
|
|
1700
|
+
for (const symbol of symbols) {
|
|
1701
|
+
if (symbol.name !== symbolName) continue;
|
|
1702
|
+
return {
|
|
1703
|
+
path: symbol.path,
|
|
1704
|
+
symbol: symbol.name,
|
|
1705
|
+
range: [
|
|
1706
|
+
symbol.node.startPosition.row + 1,
|
|
1707
|
+
symbol.node.endPosition.row + (symbol.node.endPosition.column > 0 ? 1 : 0)
|
|
1708
|
+
],
|
|
1709
|
+
text: symbol.node.text
|
|
1710
|
+
};
|
|
1711
|
+
}
|
|
1712
|
+
}
|
|
1713
|
+
|
|
1714
|
+
// src/optimizer/inspector/index.ts
|
|
1715
|
+
var inspect = defineTool2({
|
|
1716
|
+
name: "inspect",
|
|
1717
|
+
label: "Inspect",
|
|
1718
|
+
description: "Fetch one symbol's full body from one file. Output is safe for targeted edits.",
|
|
1719
|
+
parameters: Type3.Object({
|
|
1720
|
+
path: Type3.String({
|
|
1721
|
+
description: "Exact file path containing target symbol (relative to cwd or absolute)"
|
|
1722
|
+
}),
|
|
1723
|
+
symbol: Type3.String({
|
|
1724
|
+
description: "Exact symbol string for one declaration in file: function name, class name, method name (MyClass.method), synthetic anonymous name (anonymous~1), or duplicate form name~2"
|
|
1725
|
+
})
|
|
1726
|
+
}),
|
|
1727
|
+
async execute(_toolCallId, params, signal, _onUpdate, ctx) {
|
|
1728
|
+
const path = params.path.trim().replaceAll("\\", "/");
|
|
1729
|
+
const symbol = params.symbol.trim();
|
|
1730
|
+
if (path.length === 0 || symbol.length === 0 || symbol.includes("#")) {
|
|
1731
|
+
return {
|
|
1732
|
+
isError: false,
|
|
1733
|
+
details: null,
|
|
1734
|
+
content: [
|
|
1735
|
+
{
|
|
1736
|
+
type: "text",
|
|
1737
|
+
text: "inspect symbol format invalid. path/symbol must be non-empty; symbol cannot include '#'. use symbol name, e.g. name or name~2"
|
|
1738
|
+
}
|
|
1739
|
+
]
|
|
1740
|
+
};
|
|
1741
|
+
}
|
|
1742
|
+
try {
|
|
1743
|
+
const inspected = await inspectSymbol(ctx.cwd, path, symbol, signal);
|
|
1744
|
+
if (!inspected) {
|
|
1745
|
+
return {
|
|
1746
|
+
isError: false,
|
|
1747
|
+
details: null,
|
|
1748
|
+
content: [
|
|
1749
|
+
{
|
|
1750
|
+
type: "text",
|
|
1751
|
+
text: "inspect symbol not found. verify path/symbol; for duplicates use ~n suffix (name~2). then try container symbol. if still missing, run code_map with broader kinds"
|
|
1752
|
+
}
|
|
1753
|
+
]
|
|
1754
|
+
};
|
|
1755
|
+
}
|
|
1756
|
+
const details = {
|
|
1757
|
+
path: inspected.path,
|
|
1758
|
+
symbol: inspected.symbol,
|
|
1759
|
+
range: inspected.range
|
|
1760
|
+
};
|
|
1761
|
+
return { isError: false, details, content: [{ type: "text", text: inspected.text }] };
|
|
1762
|
+
} catch (error) {
|
|
1763
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1764
|
+
return {
|
|
1765
|
+
isError: true,
|
|
1766
|
+
details: null,
|
|
1767
|
+
content: [{ type: "text", text: `inspect failed: ${message}` }]
|
|
1768
|
+
};
|
|
1769
|
+
}
|
|
1770
|
+
},
|
|
1771
|
+
renderCall(args, theme) {
|
|
1772
|
+
return new Text2(
|
|
1773
|
+
`${theme.fg("toolTitle", "inspect")} ${theme.underline(theme.fg("accent", args.path))} ${theme.fg("warning", args.symbol)}`,
|
|
1774
|
+
0,
|
|
1775
|
+
0
|
|
1776
|
+
);
|
|
1777
|
+
},
|
|
1778
|
+
renderResult(result, { expanded }, theme, context) {
|
|
1779
|
+
if (!expanded && !context.isError) {
|
|
1780
|
+
return new Text2("", 0, 0);
|
|
1781
|
+
}
|
|
1782
|
+
const output = result.content[0];
|
|
1783
|
+
return new Text2(
|
|
1784
|
+
`
|
|
1785
|
+
${output?.type === "text" ? theme.fg("toolOutput", output.text) : ""}`,
|
|
1786
|
+
0,
|
|
1787
|
+
0
|
|
1788
|
+
);
|
|
1789
|
+
}
|
|
1790
|
+
});
|
|
1791
|
+
var inspector_default = inspect;
|
|
1792
|
+
|
|
1793
|
+
// src/optimizer/pruner/cleanup.ts
|
|
1794
|
+
function resolveRemovedId(entryId, replacementParents) {
|
|
1795
|
+
const visitedIds = /* @__PURE__ */ new Set();
|
|
1796
|
+
let resolvedId = entryId;
|
|
1797
|
+
while (resolvedId && replacementParents.has(resolvedId) && !visitedIds.has(resolvedId)) {
|
|
1798
|
+
visitedIds.add(resolvedId);
|
|
1799
|
+
resolvedId = replacementParents.get(resolvedId) ?? null;
|
|
1800
|
+
}
|
|
1801
|
+
return resolvedId;
|
|
1802
|
+
}
|
|
1803
|
+
function hasEmptyResult(message) {
|
|
1804
|
+
if (message.toolName !== "ls" && message.toolName !== "find") {
|
|
1805
|
+
return false;
|
|
1806
|
+
}
|
|
1807
|
+
if (!Array.isArray(message.content) || message.content.length !== 1) return false;
|
|
1808
|
+
const content = message.content[0];
|
|
1809
|
+
if (!isRecord(content) || content.type !== "text" || typeof content.text !== "string") return false;
|
|
1810
|
+
return content.text === "(empty directory)" || content.text === "No files found matching pattern" || content.text === "No changes found." || content.text === "No changes in selected files.";
|
|
1811
|
+
}
|
|
1812
|
+
function getRemovedToolCallId(message) {
|
|
1813
|
+
if (typeof message.toolCallId !== "string") return;
|
|
1814
|
+
if (message.isError === true) return message.toolName === "bash" ? void 0 : message.toolCallId;
|
|
1815
|
+
return hasEmptyResult(message) ? message.toolCallId : void 0;
|
|
1816
|
+
}
|
|
1817
|
+
function removeEntries(entries) {
|
|
1818
|
+
const removedToolCallIds = /* @__PURE__ */ new Set();
|
|
1819
|
+
const replacementParents = /* @__PURE__ */ new Map();
|
|
1820
|
+
for (const entry of entries) {
|
|
1821
|
+
const message = getToolResultMessage(entry);
|
|
1822
|
+
const entryId = getEntryId(entry);
|
|
1823
|
+
const toolCallId = message ? getRemovedToolCallId(message) : void 0;
|
|
1824
|
+
if (!entryId || !toolCallId) continue;
|
|
1825
|
+
removedToolCallIds.add(toolCallId);
|
|
1826
|
+
replacementParents.set(entryId, getParentId(entry));
|
|
1827
|
+
}
|
|
1828
|
+
if (removedToolCallIds.size === 0) {
|
|
1829
|
+
return { changed: false, entries, replacementParents };
|
|
1830
|
+
}
|
|
1831
|
+
const retainedEntries = [];
|
|
1832
|
+
for (const entry of entries) {
|
|
1833
|
+
const entryId = getEntryId(entry);
|
|
1834
|
+
if (entryId && replacementParents.has(entryId)) continue;
|
|
1835
|
+
const message = getMessage(entry);
|
|
1836
|
+
if (message?.role !== "assistant" || !Array.isArray(message.content)) {
|
|
1837
|
+
retainedEntries.push(entry);
|
|
1838
|
+
continue;
|
|
1839
|
+
}
|
|
1840
|
+
const retainedContent = message.content.filter(
|
|
1841
|
+
(block) => !isRecord(block) || block.type !== "toolCall" || typeof block.id !== "string" || !removedToolCallIds.has(block.id)
|
|
1842
|
+
);
|
|
1843
|
+
if (retainedContent.length === message.content.length) {
|
|
1844
|
+
retainedEntries.push(entry);
|
|
1845
|
+
continue;
|
|
1846
|
+
}
|
|
1847
|
+
if (entryId && retainedContent.every((block) => isRecord(block) && block.type === "thinking")) {
|
|
1848
|
+
replacementParents.set(entryId, getParentId(entry));
|
|
1849
|
+
continue;
|
|
1850
|
+
}
|
|
1851
|
+
retainedEntries.push({ ...entry, message: { ...message, content: retainedContent } });
|
|
1852
|
+
}
|
|
1853
|
+
const repairedEntries = [];
|
|
1854
|
+
for (const entry of retainedEntries) {
|
|
1855
|
+
const updatedEntry = { ...entry };
|
|
1856
|
+
let changed = false;
|
|
1857
|
+
const parentId = getParentId(entry);
|
|
1858
|
+
const repairedParentId = resolveRemovedId(parentId, replacementParents);
|
|
1859
|
+
if (parentId !== repairedParentId) {
|
|
1860
|
+
updatedEntry.parentId = repairedParentId;
|
|
1861
|
+
changed = true;
|
|
1862
|
+
}
|
|
1863
|
+
for (const referenceField of ["fromId", "targetId", "firstKeptEntryId"]) {
|
|
1864
|
+
const referenceId = entry[referenceField];
|
|
1865
|
+
if (typeof referenceId !== "string") continue;
|
|
1866
|
+
const repairedReferenceId = resolveRemovedId(referenceId, replacementParents);
|
|
1867
|
+
if (repairedReferenceId && repairedReferenceId !== referenceId) {
|
|
1868
|
+
updatedEntry[referenceField] = repairedReferenceId;
|
|
1869
|
+
changed = true;
|
|
1870
|
+
}
|
|
1871
|
+
}
|
|
1872
|
+
repairedEntries.push(changed ? updatedEntry : entry);
|
|
1873
|
+
}
|
|
1874
|
+
return { changed: true, entries: repairedEntries, replacementParents };
|
|
1875
|
+
}
|
|
1876
|
+
|
|
1877
|
+
// src/optimizer/pruner/context.ts
|
|
1878
|
+
function buildPrunerState(entries) {
|
|
1879
|
+
const removedToolCallIds = /* @__PURE__ */ new Set();
|
|
1880
|
+
for (const entry of entries) {
|
|
1881
|
+
const message = getToolResultMessage(entry);
|
|
1882
|
+
const toolCallId = message ? getRemovedToolCallId(message) : void 0;
|
|
1883
|
+
if (toolCallId) removedToolCallIds.add(toolCallId);
|
|
1884
|
+
}
|
|
1885
|
+
return removedToolCallIds;
|
|
1886
|
+
}
|
|
1887
|
+
function filterContextMessages(messages, state) {
|
|
1888
|
+
if (state.size === 0) return { changed: false, messages };
|
|
1889
|
+
const retainedMessages = [];
|
|
1890
|
+
let changed = false;
|
|
1891
|
+
for (const message of messages) {
|
|
1892
|
+
if (message.role === "toolResult" && state.has(message.toolCallId)) {
|
|
1893
|
+
changed = true;
|
|
1894
|
+
continue;
|
|
1895
|
+
}
|
|
1896
|
+
if (message.role !== "assistant") {
|
|
1897
|
+
retainedMessages.push(message);
|
|
1898
|
+
continue;
|
|
1899
|
+
}
|
|
1900
|
+
const retainedContent = message.content.filter(
|
|
1901
|
+
(block) => block.type !== "toolCall" || !state.has(block.id)
|
|
1902
|
+
);
|
|
1903
|
+
if (retainedContent.length === message.content.length) {
|
|
1904
|
+
retainedMessages.push(message);
|
|
1905
|
+
continue;
|
|
1906
|
+
}
|
|
1907
|
+
changed = true;
|
|
1908
|
+
if (retainedContent.some((block) => block.type !== "thinking")) {
|
|
1909
|
+
retainedMessages.push({ ...message, content: retainedContent });
|
|
1910
|
+
}
|
|
1911
|
+
}
|
|
1912
|
+
return { changed, messages: retainedMessages };
|
|
1913
|
+
}
|
|
1914
|
+
|
|
1915
|
+
// src/optimizer/pruner/session.ts
|
|
1916
|
+
function pruneEntries(entries, leafId) {
|
|
1917
|
+
const entryRemoval = removeEntries(entries);
|
|
1918
|
+
let activeLeafId = leafId;
|
|
1919
|
+
while (activeLeafId && entryRemoval.replacementParents.has(activeLeafId)) {
|
|
1920
|
+
activeLeafId = entryRemoval.replacementParents.get(activeLeafId) ?? null;
|
|
1921
|
+
}
|
|
1922
|
+
return {
|
|
1923
|
+
activeLeafId,
|
|
1924
|
+
changed: entryRemoval.changed,
|
|
1925
|
+
entries: entryRemoval.entries
|
|
1926
|
+
};
|
|
1927
|
+
}
|
|
1928
|
+
function rewritePrunedSessionFile(sessionFile, leafId, fallbackToLastEntry) {
|
|
1929
|
+
const entries = readSessionEntries(sessionFile);
|
|
1930
|
+
if (!entries) return;
|
|
1931
|
+
const effectiveLeafId = leafId ?? (fallbackToLastEntry ? getLastEntryId(entries) : null);
|
|
1932
|
+
const pruned = pruneEntries(entries, effectiveLeafId);
|
|
1933
|
+
if (!pruned.changed) return;
|
|
1934
|
+
writeSessionEntries(sessionFile, pruned.entries);
|
|
1935
|
+
}
|
|
1936
|
+
|
|
1937
|
+
// src/optimizer/pruner/index.ts
|
|
1938
|
+
function loadPrunerState(sessionFile) {
|
|
1939
|
+
const entries = readSessionEntries(sessionFile);
|
|
1940
|
+
return entries ? buildPrunerState(entries) : /* @__PURE__ */ new Set();
|
|
1941
|
+
}
|
|
1942
|
+
function pruner_default(pi) {
|
|
1943
|
+
let state = /* @__PURE__ */ new Set();
|
|
1944
|
+
pi.on("session_start", (_event, ctx) => {
|
|
1945
|
+
state = loadPrunerState(ctx.sessionManager.getSessionFile());
|
|
1946
|
+
});
|
|
1947
|
+
pi.on("session_tree", (_event, ctx) => {
|
|
1948
|
+
state = loadPrunerState(ctx.sessionManager.getSessionFile());
|
|
1949
|
+
});
|
|
1950
|
+
pi.on("agent_end", (event) => {
|
|
1951
|
+
for (const message of event.messages) {
|
|
1952
|
+
if (message.role !== "toolResult") continue;
|
|
1953
|
+
const toolCallId = getRemovedToolCallId(message);
|
|
1954
|
+
if (toolCallId) state.add(toolCallId);
|
|
1955
|
+
}
|
|
1956
|
+
});
|
|
1957
|
+
pi.on("session_shutdown", (event, ctx) => {
|
|
1958
|
+
const sessionFile = ctx.sessionManager.getSessionFile();
|
|
1959
|
+
if (sessionFile) {
|
|
1960
|
+
rewritePrunedSessionFile(sessionFile, ctx.sessionManager.getLeafId(), false);
|
|
1961
|
+
}
|
|
1962
|
+
if (event.targetSessionFile && event.targetSessionFile !== sessionFile) {
|
|
1963
|
+
rewritePrunedSessionFile(event.targetSessionFile, null, true);
|
|
1964
|
+
}
|
|
1965
|
+
});
|
|
1966
|
+
pi.on("context", (event) => {
|
|
1967
|
+
const pruned = filterContextMessages(event.messages, state);
|
|
1968
|
+
if (pruned.changed) return { messages: pruned.messages };
|
|
1969
|
+
});
|
|
1970
|
+
}
|
|
1971
|
+
|
|
1972
|
+
// src/optimizer/index.ts
|
|
1973
|
+
function index_default(pi) {
|
|
1974
|
+
compactor_default(pi);
|
|
1975
|
+
languages_default(pi);
|
|
1976
|
+
deduplicator_default(pi);
|
|
1977
|
+
pruner_default(pi);
|
|
1978
|
+
pi.registerTool(mapper_default);
|
|
1979
|
+
pi.registerTool(inspector_default);
|
|
1980
|
+
}
|
|
1981
|
+
export {
|
|
1982
|
+
index_default as default
|
|
1983
|
+
};
|
|
1984
|
+
//# sourceMappingURL=index.js.map
|