pi-rtk-optimizer 0.6.0 → 0.7.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/CHANGELOG.md +24 -0
- package/README.md +19 -12
- package/config/config.example.json +6 -3
- package/package.json +67 -64
- package/src/additional-coverage-test.ts +151 -50
- package/src/command-rewriter-test.ts +187 -118
- package/src/command-rewriter.ts +5 -2
- package/src/config-modal-test.ts +95 -29
- package/src/config-modal.ts +29 -3
- package/src/config-store.ts +32 -5
- package/src/index-test.ts +227 -3
- package/src/index.ts +50 -5
- package/src/output-compactor-test.ts +45 -2
- package/src/output-compactor.ts +26 -15
- package/src/rewrite-pipeline-safety.ts +203 -157
- package/src/rtk-command-environment.ts +13 -4
- package/src/rtk-executable-resolver.ts +97 -0
- package/src/rtk-rewrite-provider.ts +39 -3
- package/src/shell-env-prefix.ts +5 -1
- package/src/test-helpers.ts +23 -10
- package/src/tool-execution-sanitizer.ts +80 -69
- package/src/types.ts +13 -3
- package/src/windows-command-helpers.ts +92 -16
- package/src/zellij-modal.ts +1 -1
package/src/config-store.ts
CHANGED
|
@@ -30,10 +30,14 @@ function toMode(value: unknown): RtkIntegrationConfig["mode"] {
|
|
|
30
30
|
: DEFAULT_RTK_INTEGRATION_CONFIG.mode;
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
-
function toSourceFilterLevel(value: unknown): RtkSourceFilterLevel {
|
|
33
|
+
function toSourceFilterLevel(value: unknown, fallback: RtkSourceFilterLevel): RtkSourceFilterLevel {
|
|
34
34
|
return RTK_SOURCE_FILTER_LEVELS.includes(value as RtkSourceFilterLevel)
|
|
35
35
|
? (value as RtkSourceFilterLevel)
|
|
36
|
-
:
|
|
36
|
+
: fallback;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function hasOwnProperty(source: Record<string, unknown>, key: string): boolean {
|
|
40
|
+
return Object.prototype.hasOwnProperty.call(source, key);
|
|
37
41
|
}
|
|
38
42
|
|
|
39
43
|
function toObject(value: unknown): Record<string, unknown> {
|
|
@@ -46,8 +50,20 @@ function toObject(value: unknown): Record<string, unknown> {
|
|
|
46
50
|
export function normalizeRtkIntegrationConfig(raw: unknown): RtkIntegrationConfig {
|
|
47
51
|
const source = toObject(raw);
|
|
48
52
|
const outputCompactionSource = toObject(source.outputCompaction);
|
|
53
|
+
const readCompactionSource = toObject(outputCompactionSource.readCompaction);
|
|
49
54
|
const truncateSource = toObject(outputCompactionSource.truncate);
|
|
50
55
|
const smartTruncateSource = toObject(outputCompactionSource.smartTruncate);
|
|
56
|
+
const hasReadCompaction = hasOwnProperty(outputCompactionSource, "readCompaction");
|
|
57
|
+
const legacyReadCompactionFallback = !hasReadCompaction;
|
|
58
|
+
const sourceFilteringFallback = legacyReadCompactionFallback
|
|
59
|
+
? true
|
|
60
|
+
: DEFAULT_RTK_INTEGRATION_CONFIG.outputCompaction.sourceCodeFilteringEnabled;
|
|
61
|
+
const sourceFilterLevelFallback = legacyReadCompactionFallback
|
|
62
|
+
? "minimal"
|
|
63
|
+
: DEFAULT_RTK_INTEGRATION_CONFIG.outputCompaction.sourceCodeFiltering;
|
|
64
|
+
const smartTruncateEnabledFallback = legacyReadCompactionFallback
|
|
65
|
+
? true
|
|
66
|
+
: DEFAULT_RTK_INTEGRATION_CONFIG.outputCompaction.smartTruncate.enabled;
|
|
51
67
|
|
|
52
68
|
return {
|
|
53
69
|
enabled: toBoolean(source.enabled, DEFAULT_RTK_INTEGRATION_CONFIG.enabled),
|
|
@@ -69,9 +85,17 @@ export function normalizeRtkIntegrationConfig(raw: unknown): RtkIntegrationConfi
|
|
|
69
85
|
outputCompactionSource.stripAnsi,
|
|
70
86
|
DEFAULT_RTK_INTEGRATION_CONFIG.outputCompaction.stripAnsi,
|
|
71
87
|
),
|
|
88
|
+
readCompaction: {
|
|
89
|
+
enabled: hasReadCompaction
|
|
90
|
+
? toBoolean(
|
|
91
|
+
readCompactionSource.enabled,
|
|
92
|
+
DEFAULT_RTK_INTEGRATION_CONFIG.outputCompaction.readCompaction.enabled,
|
|
93
|
+
)
|
|
94
|
+
: true,
|
|
95
|
+
},
|
|
72
96
|
sourceCodeFilteringEnabled: toBoolean(
|
|
73
97
|
outputCompactionSource.sourceCodeFilteringEnabled,
|
|
74
|
-
|
|
98
|
+
sourceFilteringFallback,
|
|
75
99
|
),
|
|
76
100
|
preserveExactSkillReads: toBoolean(
|
|
77
101
|
outputCompactionSource.preserveExactSkillReads,
|
|
@@ -89,11 +113,14 @@ export function normalizeRtkIntegrationConfig(raw: unknown): RtkIntegrationConfi
|
|
|
89
113
|
200_000,
|
|
90
114
|
),
|
|
91
115
|
},
|
|
92
|
-
sourceCodeFiltering: toSourceFilterLevel(
|
|
116
|
+
sourceCodeFiltering: toSourceFilterLevel(
|
|
117
|
+
outputCompactionSource.sourceCodeFiltering,
|
|
118
|
+
sourceFilterLevelFallback,
|
|
119
|
+
),
|
|
93
120
|
smartTruncate: {
|
|
94
121
|
enabled: toBoolean(
|
|
95
122
|
smartTruncateSource.enabled,
|
|
96
|
-
|
|
123
|
+
smartTruncateEnabledFallback,
|
|
97
124
|
),
|
|
98
125
|
maxLines: toInteger(
|
|
99
126
|
smartTruncateSource.maxLines,
|
package/src/index-test.ts
CHANGED
|
@@ -28,12 +28,37 @@ mock.module("@mariozechner/pi-tui", () => ({
|
|
|
28
28
|
visibleWidth: (text: string) => text.length,
|
|
29
29
|
}));
|
|
30
30
|
|
|
31
|
-
const
|
|
31
|
+
const indexModule = await import("./index.ts");
|
|
32
|
+
const { createBoundedNoticeTracker, shouldInjectSourceFilterTroubleshootingNote } = indexModule;
|
|
33
|
+
const rtkIntegrationExtension = indexModule.default;
|
|
32
34
|
const { DEFAULT_RTK_INTEGRATION_CONFIG } = await import("./types.ts");
|
|
33
35
|
|
|
36
|
+
type Notification = { message: string; level: "info" | "warning" | "error" };
|
|
37
|
+
type ExtensionHandler = (event: Record<string, unknown>, ctx: Record<string, unknown>) => Promise<Record<string, unknown> | void>;
|
|
38
|
+
|
|
39
|
+
function createNotificationContext(notifications: Notification[]): Record<string, unknown> {
|
|
40
|
+
return {
|
|
41
|
+
hasUI: true,
|
|
42
|
+
ui: {
|
|
43
|
+
notify(message: string, level: "info" | "warning" | "error") {
|
|
44
|
+
notifications.push({ message, level });
|
|
45
|
+
},
|
|
46
|
+
},
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function firstText(content: unknown): string {
|
|
51
|
+
if (!Array.isArray(content) || content.length === 0) {
|
|
52
|
+
return "";
|
|
53
|
+
}
|
|
54
|
+
const block = content[0] as { type?: string; text?: string };
|
|
55
|
+
return block.type === "text" && typeof block.text === "string" ? block.text : "";
|
|
56
|
+
}
|
|
57
|
+
|
|
34
58
|
function configWith(overrides: {
|
|
35
59
|
enabled?: boolean;
|
|
36
60
|
compactionEnabled?: boolean;
|
|
61
|
+
readCompactionEnabled?: boolean;
|
|
37
62
|
sourceFilteringEnabled?: boolean;
|
|
38
63
|
sourceFilteringLevel?: "none" | "minimal" | "aggressive";
|
|
39
64
|
smartTruncateEnabled?: boolean;
|
|
@@ -46,6 +71,10 @@ function configWith(overrides: {
|
|
|
46
71
|
outputCompaction: {
|
|
47
72
|
...base.outputCompaction,
|
|
48
73
|
enabled: overrides.compactionEnabled ?? base.outputCompaction.enabled,
|
|
74
|
+
readCompaction: {
|
|
75
|
+
...base.outputCompaction.readCompaction,
|
|
76
|
+
enabled: overrides.readCompactionEnabled ?? base.outputCompaction.readCompaction.enabled,
|
|
77
|
+
},
|
|
49
78
|
sourceCodeFilteringEnabled:
|
|
50
79
|
overrides.sourceFilteringEnabled ?? base.outputCompaction.sourceCodeFilteringEnabled,
|
|
51
80
|
sourceCodeFiltering: overrides.sourceFilteringLevel ?? base.outputCompaction.sourceCodeFiltering,
|
|
@@ -86,13 +115,23 @@ runTest("bounded notice tracker coerces invalid limits to a safe minimum", () =>
|
|
|
86
115
|
runTest("source-filter note injected when source filtering is active", () => {
|
|
87
116
|
assert.equal(
|
|
88
117
|
shouldInjectSourceFilterTroubleshootingNote(
|
|
89
|
-
configWith({
|
|
118
|
+
configWith({
|
|
119
|
+
readCompactionEnabled: true,
|
|
120
|
+
sourceFilteringEnabled: true,
|
|
121
|
+
sourceFilteringLevel: "minimal",
|
|
122
|
+
smartTruncateEnabled: true,
|
|
123
|
+
}),
|
|
90
124
|
),
|
|
91
125
|
true,
|
|
92
126
|
);
|
|
93
127
|
assert.equal(
|
|
94
128
|
shouldInjectSourceFilterTroubleshootingNote(
|
|
95
|
-
configWith({
|
|
129
|
+
configWith({
|
|
130
|
+
readCompactionEnabled: true,
|
|
131
|
+
sourceFilteringEnabled: true,
|
|
132
|
+
sourceFilteringLevel: "aggressive",
|
|
133
|
+
smartTruncateEnabled: true,
|
|
134
|
+
}),
|
|
96
135
|
),
|
|
97
136
|
true,
|
|
98
137
|
);
|
|
@@ -106,6 +145,20 @@ runTest("source-filter note skipped when compaction is disabled", () => {
|
|
|
106
145
|
assert.equal(shouldInjectSourceFilterTroubleshootingNote(configWith({ compactionEnabled: false })), false);
|
|
107
146
|
});
|
|
108
147
|
|
|
148
|
+
runTest("source-filter note skipped when read compaction is disabled", () => {
|
|
149
|
+
assert.equal(
|
|
150
|
+
shouldInjectSourceFilterTroubleshootingNote(
|
|
151
|
+
configWith({
|
|
152
|
+
readCompactionEnabled: false,
|
|
153
|
+
sourceFilteringEnabled: true,
|
|
154
|
+
sourceFilteringLevel: "minimal",
|
|
155
|
+
smartTruncateEnabled: true,
|
|
156
|
+
}),
|
|
157
|
+
),
|
|
158
|
+
false,
|
|
159
|
+
);
|
|
160
|
+
});
|
|
161
|
+
|
|
109
162
|
runTest("source-filter note skipped when source filtering flag is off", () => {
|
|
110
163
|
assert.equal(
|
|
111
164
|
shouldInjectSourceFilterTroubleshootingNote(configWith({ sourceFilteringEnabled: false })),
|
|
@@ -131,4 +184,175 @@ runTest("source-filter note skipped when all read filtering safeguards are disab
|
|
|
131
184
|
);
|
|
132
185
|
});
|
|
133
186
|
|
|
187
|
+
await runTest("session_start refreshes RTK provenance and runtime guard skips missing rewrites", async () => {
|
|
188
|
+
const handlers: Record<string, ExtensionHandler> = {};
|
|
189
|
+
const notifications: Notification[] = [];
|
|
190
|
+
const execCommands: string[] = [];
|
|
191
|
+
let rtkAvailable = false;
|
|
192
|
+
let rewriteCalls = 0;
|
|
193
|
+
|
|
194
|
+
rtkIntegrationExtension({
|
|
195
|
+
exec: async (command: string, args: string[]) => {
|
|
196
|
+
execCommands.push(command);
|
|
197
|
+
if (command === "which" || command === "where") {
|
|
198
|
+
return { code: 0, stdout: "/opt/rtk/bin/rtk\n", stderr: "" };
|
|
199
|
+
}
|
|
200
|
+
if (args[0] === "--version") {
|
|
201
|
+
return rtkAvailable
|
|
202
|
+
? { code: 0, stdout: "rtk 1.0.0", stderr: "" }
|
|
203
|
+
: { code: 1, stdout: "", stderr: "missing rtk" };
|
|
204
|
+
}
|
|
205
|
+
if (args[0] === "rewrite") {
|
|
206
|
+
rewriteCalls += 1;
|
|
207
|
+
return { code: 3, stdout: "rtk git status", stderr: "" };
|
|
208
|
+
}
|
|
209
|
+
return { code: 1, stdout: "", stderr: "unexpected" };
|
|
210
|
+
},
|
|
211
|
+
on(eventName: string, handler: ExtensionHandler) {
|
|
212
|
+
handlers[eventName] = handler;
|
|
213
|
+
},
|
|
214
|
+
registerCommand() {},
|
|
215
|
+
} as never);
|
|
216
|
+
|
|
217
|
+
const sessionStartHandler = handlers.session_start;
|
|
218
|
+
const toolCallHandler = handlers.tool_call;
|
|
219
|
+
assert.ok(sessionStartHandler);
|
|
220
|
+
assert.ok(toolCallHandler);
|
|
221
|
+
|
|
222
|
+
await sessionStartHandler({}, createNotificationContext(notifications));
|
|
223
|
+
const skippedEvent = { toolName: "bash", input: { command: "git status" } };
|
|
224
|
+
await toolCallHandler(skippedEvent, createNotificationContext(notifications));
|
|
225
|
+
|
|
226
|
+
assert.equal((skippedEvent.input as { command: string }).command, "git status");
|
|
227
|
+
assert.equal(rewriteCalls, 0);
|
|
228
|
+
assert.ok(notifications.some((notice) => notice.message.includes("rtk binary unavailable")));
|
|
229
|
+
|
|
230
|
+
rtkAvailable = true;
|
|
231
|
+
await sessionStartHandler({}, createNotificationContext(notifications));
|
|
232
|
+
const rewrittenEvent = { toolName: "bash", input: { command: "git status" } };
|
|
233
|
+
await toolCallHandler(rewrittenEvent, createNotificationContext(notifications));
|
|
234
|
+
|
|
235
|
+
assert.equal(rewriteCalls, 1);
|
|
236
|
+
assert.ok((rewrittenEvent.input as { command: string }).command.includes("rtk git status"));
|
|
237
|
+
assert.ok(execCommands.includes("/opt/rtk/bin/rtk"));
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
await runTest("tool execution lifecycle sanitizes streamed bash output", async () => {
|
|
241
|
+
const handlers: Record<string, ExtensionHandler> = {};
|
|
242
|
+
|
|
243
|
+
rtkIntegrationExtension({
|
|
244
|
+
exec: async () => ({ code: 0, stdout: "rtk 1.0.0", stderr: "" }),
|
|
245
|
+
on(eventName: string, handler: ExtensionHandler) {
|
|
246
|
+
handlers[eventName] = handler;
|
|
247
|
+
},
|
|
248
|
+
registerCommand() {},
|
|
249
|
+
} as never);
|
|
250
|
+
|
|
251
|
+
const startHandler = handlers.tool_execution_start;
|
|
252
|
+
const updateHandler = handlers.tool_execution_update;
|
|
253
|
+
const endHandler = handlers.tool_execution_end;
|
|
254
|
+
assert.ok(startHandler);
|
|
255
|
+
assert.ok(updateHandler);
|
|
256
|
+
assert.ok(endHandler);
|
|
257
|
+
|
|
258
|
+
await startHandler(
|
|
259
|
+
{ toolName: "bash", toolCallId: "bash-1", args: { command: "rtk git status" } },
|
|
260
|
+
{},
|
|
261
|
+
);
|
|
262
|
+
const updateEvent = {
|
|
263
|
+
toolName: "bash",
|
|
264
|
+
toolCallId: "bash-1",
|
|
265
|
+
args: { command: "rtk git status" },
|
|
266
|
+
partialResult: {
|
|
267
|
+
content: [
|
|
268
|
+
{
|
|
269
|
+
type: "text",
|
|
270
|
+
text: "[rtk] /!\\ No hook installed — run `rtk init -g` for automatic token savings\n\nworking tree clean\n",
|
|
271
|
+
},
|
|
272
|
+
],
|
|
273
|
+
},
|
|
274
|
+
};
|
|
275
|
+
await updateHandler(updateEvent, {});
|
|
276
|
+
assert.equal(firstText(updateEvent.partialResult.content), "working tree clean\n");
|
|
277
|
+
|
|
278
|
+
const endEvent = {
|
|
279
|
+
toolName: "bash",
|
|
280
|
+
toolCallId: "bash-1",
|
|
281
|
+
result: { content: [{ type: "text", text: "📄 src/file.ts\n✅ Files are identical\n" }] },
|
|
282
|
+
};
|
|
283
|
+
await endHandler(endEvent, {});
|
|
284
|
+
assert.equal(firstText(endEvent.result.content), "> src/file.ts\n[OK] Files are identical\n");
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
await runTest("tool_result lifecycle merges compaction metadata with existing details", async () => {
|
|
288
|
+
const handlers: Record<string, ExtensionHandler> = {};
|
|
289
|
+
const notifications: Notification[] = [];
|
|
290
|
+
|
|
291
|
+
rtkIntegrationExtension({
|
|
292
|
+
exec: async () => ({ code: 0, stdout: "rtk 1.0.0", stderr: "" }),
|
|
293
|
+
on(eventName: string, handler: ExtensionHandler) {
|
|
294
|
+
handlers[eventName] = handler;
|
|
295
|
+
},
|
|
296
|
+
registerCommand() {},
|
|
297
|
+
} as never);
|
|
298
|
+
|
|
299
|
+
const toolResultHandler = handlers.tool_result;
|
|
300
|
+
assert.ok(toolResultHandler);
|
|
301
|
+
const result = await toolResultHandler(
|
|
302
|
+
{
|
|
303
|
+
toolName: "grep",
|
|
304
|
+
input: { pattern: "TODO" },
|
|
305
|
+
content: [{ type: "text", text: "src/a.ts:1:TODO\nsrc/b.ts:2:TODO\n" }],
|
|
306
|
+
details: { metadata: { requestId: "abc" }, traceId: "trace-1" },
|
|
307
|
+
},
|
|
308
|
+
createNotificationContext(notifications),
|
|
309
|
+
);
|
|
310
|
+
|
|
311
|
+
assert.ok(result);
|
|
312
|
+
assert.ok(firstText(result.content).startsWith("2 matches in 2 files:"));
|
|
313
|
+
assert.equal((result.details as { traceId?: string }).traceId, "trace-1");
|
|
314
|
+
const details = result.details as { rtkCompaction?: { applied: boolean }; metadata?: Record<string, unknown> };
|
|
315
|
+
assert.equal(details.rtkCompaction?.applied, true);
|
|
316
|
+
assert.deepEqual(details.metadata?.requestId, "abc");
|
|
317
|
+
assert.equal((details.metadata?.rtkCompaction as { applied?: boolean } | undefined)?.applied, true);
|
|
318
|
+
assert.equal(notifications.length, 0);
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
await runTest("tool_call surfaces RTK rewrite errors through existing UI warning path", async () => {
|
|
322
|
+
const handlers: Record<string, (event: Record<string, unknown>, ctx: Record<string, unknown>) => Promise<Record<string, unknown> | void>> = {};
|
|
323
|
+
const notifications: Notification[] = [];
|
|
324
|
+
|
|
325
|
+
rtkIntegrationExtension({
|
|
326
|
+
exec: async (_command: string, args: string[]) => {
|
|
327
|
+
if (args[0] === "--version") {
|
|
328
|
+
return { code: 0, stdout: "rtk 1.0.0", stderr: "" };
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
return { code: 2, stdout: "", stderr: "denied unsafe rewrite" };
|
|
332
|
+
},
|
|
333
|
+
on(eventName: string, handler: (event: Record<string, unknown>, ctx: Record<string, unknown>) => Promise<Record<string, unknown> | void>) {
|
|
334
|
+
handlers[eventName] = handler;
|
|
335
|
+
},
|
|
336
|
+
registerCommand() {},
|
|
337
|
+
} as never);
|
|
338
|
+
|
|
339
|
+
const toolCallHandler = handlers.tool_call;
|
|
340
|
+
assert.ok(toolCallHandler);
|
|
341
|
+
const event = { toolName: "bash", input: { command: "git status" } };
|
|
342
|
+
await toolCallHandler(event, {
|
|
343
|
+
hasUI: true,
|
|
344
|
+
ui: {
|
|
345
|
+
notify(message: string, level: "info" | "warning" | "error") {
|
|
346
|
+
notifications.push({ message, level });
|
|
347
|
+
},
|
|
348
|
+
},
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
assert.equal((event.input as { command: string }).command, "git status");
|
|
352
|
+
assert.equal(notifications.length, 1);
|
|
353
|
+
assert.equal(notifications[0]?.level, "warning");
|
|
354
|
+
assert.ok(notifications[0]?.message.includes("rtk rewrite skipped"));
|
|
355
|
+
assert.ok(notifications[0]?.message.includes("denied unsafe rewrite"));
|
|
356
|
+
});
|
|
357
|
+
|
|
134
358
|
console.log("All index tests passed.");
|
package/src/index.ts
CHANGED
|
@@ -13,6 +13,7 @@ import { clearOutputMetrics, getOutputMetricsSummary } from "./output-metrics.js
|
|
|
13
13
|
import { compactToolResult, type ToolResultCompactionMetadata } from "./output-compactor.js";
|
|
14
14
|
import { toRecord } from "./record-utils.js";
|
|
15
15
|
import { applyRtkCommandEnvironment } from "./rtk-command-environment.js";
|
|
16
|
+
import { resolveRtkExecutable, type RtkExecutableResolution } from "./rtk-executable-resolver.js";
|
|
16
17
|
import { applyRewrittenCommandShellSafetyFixups } from "./rewrite-pipeline-safety.js";
|
|
17
18
|
import { shouldRequireRtkAvailabilityForCommandHandling, shouldSkipCommandHandlingWhenRtkMissing } from "./runtime-guard.js";
|
|
18
19
|
import { sanitizeStreamingBashExecutionResult } from "./tool-execution-sanitizer.js";
|
|
@@ -28,13 +29,14 @@ function trimMessage(raw: string, maxLength = 220): string {
|
|
|
28
29
|
}
|
|
29
30
|
|
|
30
31
|
const SOURCE_FILTER_TROUBLESHOOTING_NOTE =
|
|
31
|
-
"RTK note: If file edits repeatedly fail because old text does not match, ask the user to manually run '/rtk' in the Pi TUI, disable 'Read
|
|
32
|
+
"RTK note: If file edits repeatedly fail because old text does not match, ask the user to manually run '/rtk' in the Pi TUI, disable 'Read compaction enabled', re-read the file, apply the edit, then ask the user to manually re-enable it in the Pi TUI.";
|
|
32
33
|
|
|
33
34
|
export function shouldInjectSourceFilterTroubleshootingNote(config: RtkIntegrationConfig): boolean {
|
|
34
35
|
const compaction = config.outputCompaction;
|
|
35
36
|
return (
|
|
36
37
|
config.enabled &&
|
|
37
38
|
compaction.enabled &&
|
|
39
|
+
compaction.readCompaction.enabled &&
|
|
38
40
|
compaction.sourceCodeFilteringEnabled &&
|
|
39
41
|
compaction.sourceCodeFiltering !== "none" &&
|
|
40
42
|
(compaction.smartTruncate.enabled || compaction.truncate.enabled)
|
|
@@ -114,6 +116,12 @@ export default function rtkIntegrationExtension(pi: ExtensionAPI): void {
|
|
|
114
116
|
return `RTK rewrite: ${original} -> ${rewritten}`;
|
|
115
117
|
};
|
|
116
118
|
|
|
119
|
+
const formatRewriteWarning = (command: string, warning: string): string => {
|
|
120
|
+
const target = trimMessage(command, 100);
|
|
121
|
+
const detail = trimMessage(warning, 120);
|
|
122
|
+
return `${EXTENSION_NAME}: rtk rewrite skipped for '${target}' (${detail}).`;
|
|
123
|
+
};
|
|
124
|
+
|
|
117
125
|
const warnOnce = (
|
|
118
126
|
ctx: ExtensionContext | ExtensionCommandContext,
|
|
119
127
|
message: string,
|
|
@@ -189,12 +197,18 @@ export default function rtkIntegrationExtension(pi: ExtensionAPI): void {
|
|
|
189
197
|
};
|
|
190
198
|
|
|
191
199
|
const refreshRuntimeStatus = async (): Promise<RuntimeStatus> => {
|
|
200
|
+
let executableResolution: RtkExecutableResolution | undefined;
|
|
192
201
|
try {
|
|
193
|
-
|
|
202
|
+
executableResolution = await resolveRtkExecutable(pi);
|
|
203
|
+
const result = await pi.exec(executableResolution.command, ["--version"], { timeout: 5000 });
|
|
194
204
|
if (result.code === 0) {
|
|
195
205
|
runtimeStatus = {
|
|
196
206
|
rtkAvailable: true,
|
|
197
207
|
lastCheckedAt: Date.now(),
|
|
208
|
+
rtkExecutablePath: executableResolution.resolvedPath,
|
|
209
|
+
rtkExecutableCommand: executableResolution.command,
|
|
210
|
+
rtkExecutableResolver: executableResolution.resolver,
|
|
211
|
+
rtkExecutableResolutionWarning: executableResolution.warning,
|
|
198
212
|
};
|
|
199
213
|
missingRtkWarningShown = false;
|
|
200
214
|
return runtimeStatus;
|
|
@@ -207,6 +221,10 @@ export default function rtkIntegrationExtension(pi: ExtensionAPI): void {
|
|
|
207
221
|
rtkAvailable: false,
|
|
208
222
|
lastCheckedAt: Date.now(),
|
|
209
223
|
lastError: detail || `exit ${result.code}`,
|
|
224
|
+
rtkExecutablePath: executableResolution.resolvedPath,
|
|
225
|
+
rtkExecutableCommand: executableResolution.command,
|
|
226
|
+
rtkExecutableResolver: executableResolution.resolver,
|
|
227
|
+
rtkExecutableResolutionWarning: executableResolution.warning,
|
|
210
228
|
};
|
|
211
229
|
return runtimeStatus;
|
|
212
230
|
} catch (error) {
|
|
@@ -215,6 +233,10 @@ export default function rtkIntegrationExtension(pi: ExtensionAPI): void {
|
|
|
215
233
|
rtkAvailable: false,
|
|
216
234
|
lastCheckedAt: Date.now(),
|
|
217
235
|
lastError: trimMessage(message),
|
|
236
|
+
rtkExecutablePath: executableResolution?.resolvedPath,
|
|
237
|
+
rtkExecutableCommand: executableResolution?.command,
|
|
238
|
+
rtkExecutableResolver: executableResolution?.resolver,
|
|
239
|
+
rtkExecutableResolutionWarning: executableResolution?.warning,
|
|
218
240
|
};
|
|
219
241
|
return runtimeStatus;
|
|
220
242
|
}
|
|
@@ -302,10 +324,13 @@ export default function rtkIntegrationExtension(pi: ExtensionAPI): void {
|
|
|
302
324
|
}
|
|
303
325
|
|
|
304
326
|
trackBashCommand(eventRecord.toolCallId, eventRecord.args);
|
|
305
|
-
sanitizeStreamingBashExecutionResult(
|
|
327
|
+
const sanitization = sanitizeStreamingBashExecutionResult(
|
|
306
328
|
eventRecord.partialResult,
|
|
307
329
|
getTrackedBashCommand(eventRecord.toolCallId),
|
|
308
330
|
);
|
|
331
|
+
if (sanitization.changed) {
|
|
332
|
+
eventRecord.partialResult = sanitization.result;
|
|
333
|
+
}
|
|
309
334
|
});
|
|
310
335
|
|
|
311
336
|
pi.on("tool_execution_end", async (event) => {
|
|
@@ -316,7 +341,13 @@ export default function rtkIntegrationExtension(pi: ExtensionAPI): void {
|
|
|
316
341
|
|
|
317
342
|
try {
|
|
318
343
|
if (config.enabled && config.outputCompaction.enabled) {
|
|
319
|
-
sanitizeStreamingBashExecutionResult(
|
|
344
|
+
const sanitization = sanitizeStreamingBashExecutionResult(
|
|
345
|
+
eventRecord.result,
|
|
346
|
+
getTrackedBashCommand(eventRecord.toolCallId),
|
|
347
|
+
);
|
|
348
|
+
if (sanitization.changed) {
|
|
349
|
+
eventRecord.result = sanitization.result;
|
|
350
|
+
}
|
|
320
351
|
}
|
|
321
352
|
} finally {
|
|
322
353
|
forgetTrackedBashCommand(eventRecord.toolCallId);
|
|
@@ -361,8 +392,22 @@ export default function rtkIntegrationExtension(pi: ExtensionAPI): void {
|
|
|
361
392
|
return {};
|
|
362
393
|
}
|
|
363
394
|
|
|
364
|
-
|
|
395
|
+
let executableResolution: RtkExecutableResolution | undefined;
|
|
396
|
+
if (runtimeStatus.rtkExecutableCommand) {
|
|
397
|
+
const resolver: RtkExecutableResolution["resolver"] =
|
|
398
|
+
runtimeStatus.rtkExecutableResolver === "where" ? "where" : "which";
|
|
399
|
+
executableResolution = {
|
|
400
|
+
command: runtimeStatus.rtkExecutableCommand,
|
|
401
|
+
resolvedPath: runtimeStatus.rtkExecutablePath,
|
|
402
|
+
resolver,
|
|
403
|
+
warning: runtimeStatus.rtkExecutableResolutionWarning,
|
|
404
|
+
};
|
|
405
|
+
}
|
|
406
|
+
const decision = await computeRewriteDecision(event.input.command, config, pi, { executableResolution });
|
|
365
407
|
if (!decision.changed) {
|
|
408
|
+
if (decision.warning) {
|
|
409
|
+
warnOnce(ctx, formatRewriteWarning(decision.originalCommand, decision.warning));
|
|
410
|
+
}
|
|
366
411
|
return {};
|
|
367
412
|
}
|
|
368
413
|
|
|
@@ -24,6 +24,10 @@ function buildReadContent(lineCount: number): string {
|
|
|
24
24
|
return `${lines.join("\n")}\n`;
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
+
function setReadCompaction(config: ReturnType<typeof cloneDefaultConfig>, enabled: boolean): void {
|
|
28
|
+
config.outputCompaction.readCompaction = { enabled };
|
|
29
|
+
}
|
|
30
|
+
|
|
27
31
|
function firstTextBlock(content: unknown[] | undefined): string {
|
|
28
32
|
if (!Array.isArray(content) || content.length === 0) {
|
|
29
33
|
return "";
|
|
@@ -73,6 +77,7 @@ function assertNoOutputEmoji(text: string): void {
|
|
|
73
77
|
|
|
74
78
|
runTest("precision read with offset keeps exact output (no source/smart/hard truncation)", () => {
|
|
75
79
|
const config = cloneDefaultConfig();
|
|
80
|
+
setReadCompaction(config, true);
|
|
76
81
|
config.outputCompaction.truncate.enabled = true;
|
|
77
82
|
config.outputCompaction.truncate.maxChars = 500;
|
|
78
83
|
config.outputCompaction.smartTruncate.enabled = true;
|
|
@@ -94,6 +99,7 @@ runTest("precision read with offset keeps exact output (no source/smart/hard tru
|
|
|
94
99
|
|
|
95
100
|
runTest("precision read with limit keeps exact output", () => {
|
|
96
101
|
const config = cloneDefaultConfig();
|
|
102
|
+
setReadCompaction(config, true);
|
|
97
103
|
config.outputCompaction.truncate.enabled = true;
|
|
98
104
|
config.outputCompaction.truncate.maxChars = 500;
|
|
99
105
|
config.outputCompaction.smartTruncate.enabled = true;
|
|
@@ -113,8 +119,34 @@ runTest("precision read with limit keeps exact output", () => {
|
|
|
113
119
|
assert.deepEqual(result.techniques, []);
|
|
114
120
|
});
|
|
115
121
|
|
|
116
|
-
runTest("
|
|
122
|
+
runTest("default read output stays exact when read compaction is disabled by default", () => {
|
|
123
|
+
const config = cloneDefaultConfig();
|
|
124
|
+
config.outputCompaction.sourceCodeFilteringEnabled = true;
|
|
125
|
+
config.outputCompaction.sourceCodeFiltering = "aggressive";
|
|
126
|
+
config.outputCompaction.smartTruncate.enabled = true;
|
|
127
|
+
config.outputCompaction.smartTruncate.maxLines = 40;
|
|
128
|
+
config.outputCompaction.truncate.enabled = true;
|
|
129
|
+
config.outputCompaction.truncate.maxChars = 500;
|
|
130
|
+
|
|
131
|
+
const content = buildReadContent(220);
|
|
132
|
+
const result = compactToolResult(
|
|
133
|
+
{
|
|
134
|
+
toolName: "read",
|
|
135
|
+
input: { path: "sample.ts" },
|
|
136
|
+
content: [{ type: "text", text: content }],
|
|
137
|
+
},
|
|
138
|
+
config,
|
|
139
|
+
);
|
|
140
|
+
|
|
141
|
+
assert.equal(result.changed, false);
|
|
142
|
+
assert.deepEqual(result.techniques, []);
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
runTest("normal read compacts and adds banner when read compaction is enabled", () => {
|
|
117
146
|
const config = cloneDefaultConfig();
|
|
147
|
+
setReadCompaction(config, true);
|
|
148
|
+
config.outputCompaction.sourceCodeFilteringEnabled = true;
|
|
149
|
+
config.outputCompaction.sourceCodeFiltering = "minimal";
|
|
118
150
|
config.outputCompaction.smartTruncate.enabled = true;
|
|
119
151
|
config.outputCompaction.smartTruncate.maxLines = 40;
|
|
120
152
|
|
|
@@ -138,6 +170,7 @@ runTest("normal read compacts and adds banner", () => {
|
|
|
138
170
|
|
|
139
171
|
runTest("short read output stays exact below threshold", () => {
|
|
140
172
|
const config = cloneDefaultConfig();
|
|
173
|
+
setReadCompaction(config, true);
|
|
141
174
|
const content = buildReadContent(40);
|
|
142
175
|
|
|
143
176
|
const result = compactToolResult(
|
|
@@ -155,6 +188,7 @@ runTest("short read output stays exact below threshold", () => {
|
|
|
155
188
|
|
|
156
189
|
runTest("read output stays exact at the 80-line boundary with trailing newline", () => {
|
|
157
190
|
const config = cloneDefaultConfig();
|
|
191
|
+
setReadCompaction(config, true);
|
|
158
192
|
config.outputCompaction.smartTruncate.enabled = true;
|
|
159
193
|
config.outputCompaction.smartTruncate.maxLines = 40;
|
|
160
194
|
|
|
@@ -172,8 +206,11 @@ runTest("read output stays exact at the 80-line boundary with trailing newline",
|
|
|
172
206
|
assert.deepEqual(result.techniques, []);
|
|
173
207
|
});
|
|
174
208
|
|
|
175
|
-
runTest("read output compacts once the content exceeds the 80-line exactness threshold", () => {
|
|
209
|
+
runTest("read output compacts once the content exceeds the 80-line exactness threshold when read compaction is enabled", () => {
|
|
176
210
|
const config = cloneDefaultConfig();
|
|
211
|
+
setReadCompaction(config, true);
|
|
212
|
+
config.outputCompaction.sourceCodeFilteringEnabled = true;
|
|
213
|
+
config.outputCompaction.sourceCodeFiltering = "minimal";
|
|
177
214
|
config.outputCompaction.smartTruncate.enabled = true;
|
|
178
215
|
config.outputCompaction.smartTruncate.maxLines = 40;
|
|
179
216
|
|
|
@@ -193,6 +230,9 @@ runTest("read output compacts once the content exceeds the 80-line exactness thr
|
|
|
193
230
|
|
|
194
231
|
runTest("source file reads skip lossy source filtering when truncation safeguards are not needed", () => {
|
|
195
232
|
const config = cloneDefaultConfig();
|
|
233
|
+
setReadCompaction(config, true);
|
|
234
|
+
config.outputCompaction.sourceCodeFilteringEnabled = true;
|
|
235
|
+
config.outputCompaction.sourceCodeFiltering = "minimal";
|
|
196
236
|
config.outputCompaction.smartTruncate.enabled = false;
|
|
197
237
|
config.outputCompaction.truncate.enabled = false;
|
|
198
238
|
|
|
@@ -213,6 +253,7 @@ runTest("source file reads skip lossy source filtering when truncation safeguard
|
|
|
213
253
|
|
|
214
254
|
runTest("skill reads stay exact when preserveExactSkillReads is enabled for user skills", () => {
|
|
215
255
|
const config = cloneDefaultConfig();
|
|
256
|
+
setReadCompaction(config, true);
|
|
216
257
|
config.outputCompaction.preserveExactSkillReads = true;
|
|
217
258
|
config.outputCompaction.truncate.enabled = true;
|
|
218
259
|
config.outputCompaction.truncate.maxChars = 500;
|
|
@@ -235,6 +276,7 @@ runTest("skill reads stay exact when preserveExactSkillReads is enabled for user
|
|
|
235
276
|
|
|
236
277
|
runTest("project .pi skill reads stay exact when preserveExactSkillReads is enabled", () => {
|
|
237
278
|
const config = cloneDefaultConfig();
|
|
279
|
+
setReadCompaction(config, true);
|
|
238
280
|
config.outputCompaction.preserveExactSkillReads = true;
|
|
239
281
|
config.outputCompaction.truncate.enabled = true;
|
|
240
282
|
config.outputCompaction.truncate.maxChars = 500;
|
|
@@ -257,6 +299,7 @@ runTest("project .pi skill reads stay exact when preserveExactSkillReads is enab
|
|
|
257
299
|
|
|
258
300
|
runTest("ancestor .agents skill reads stay exact when preserveExactSkillReads is enabled", () => {
|
|
259
301
|
const config = cloneDefaultConfig();
|
|
302
|
+
setReadCompaction(config, true);
|
|
260
303
|
config.outputCompaction.preserveExactSkillReads = true;
|
|
261
304
|
config.outputCompaction.truncate.enabled = true;
|
|
262
305
|
config.outputCompaction.truncate.maxChars = 500;
|