pi-rtk-optimizer 0.7.0 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +25 -1
- package/README.md +14 -8
- package/package.json +67 -64
- package/src/additional-coverage-test.ts +117 -58
- package/src/command-completions.ts +49 -49
- package/src/command-rewriter-test.ts +187 -118
- package/src/command-rewriter.ts +46 -43
- package/src/config-modal-test.ts +97 -31
- package/src/config-modal.ts +91 -12
- package/src/constants.ts +1 -1
- package/src/index-test.ts +198 -3
- package/src/index.ts +49 -5
- package/src/output-compactor-test.ts +208 -3
- package/src/output-compactor.ts +316 -16
- package/src/rewrite-pipeline-safety.ts +203 -178
- package/src/rtk-command-environment.ts +73 -69
- package/src/rtk-executable-resolver.ts +97 -0
- package/src/rtk-rewrite-provider.ts +126 -90
- 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-shims.d.ts +4 -2
- package/src/types.ts +4 -0
- package/src/windows-command-helpers.ts +92 -16
- package/src/zellij-modal.ts +137 -30
package/src/config-modal.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import type { ExtensionAPI, ExtensionCommandContext } from "@
|
|
2
|
-
import type { SettingItem } from "@
|
|
1
|
+
import type { ExtensionAPI, ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import type { SettingItem } from "@earendil-works/pi-tui";
|
|
3
3
|
import { toOnOff } from "./boolean-format.js";
|
|
4
4
|
import { ZellijModal, ZellijSettingsModal } from "./zellij-modal.js";
|
|
5
5
|
import { getRtkArgumentCompletions } from "./command-completions.js";
|
|
@@ -31,6 +31,70 @@ const TRUNCATE_MAX_CHAR_VALUES = ["4000", "8000", "12000", "20000", "50000", "10
|
|
|
31
31
|
const SMART_TRUNCATE_LINE_VALUES = ["40", "80", "120", "160", "220", "320", "500", "1000", "2000", "4000"];
|
|
32
32
|
const RTK_USAGE_TEXT =
|
|
33
33
|
"Usage: /rtk [show|path|verify|stats|clear-stats|reset|help] (or run /rtk with no args to open settings modal)";
|
|
34
|
+
const SETTINGS_TAB_DEFINITIONS = [
|
|
35
|
+
{
|
|
36
|
+
label: "General",
|
|
37
|
+
settingIds: ["enabled", "mode", "showRewriteNotifications", "guardWhenRtkMissing"],
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
label: "Compaction",
|
|
41
|
+
settingIds: [
|
|
42
|
+
"outputCompactionEnabled",
|
|
43
|
+
"outputStripAnsi",
|
|
44
|
+
"outputAggregateTestOutput",
|
|
45
|
+
"outputFilterBuildOutput",
|
|
46
|
+
"outputCompactGitOutput",
|
|
47
|
+
"outputAggregateLinterOutput",
|
|
48
|
+
"outputGroupSearchOutput",
|
|
49
|
+
"outputTrackSavings",
|
|
50
|
+
],
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
label: "Read & Source",
|
|
54
|
+
settingIds: [
|
|
55
|
+
"outputReadCompactionEnabled",
|
|
56
|
+
"outputSourceFilteringEnabled",
|
|
57
|
+
"outputSourceFiltering",
|
|
58
|
+
"outputPreserveExactSkillReads",
|
|
59
|
+
],
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
label: "Truncation",
|
|
63
|
+
settingIds: [
|
|
64
|
+
"outputTruncateEnabled",
|
|
65
|
+
"outputTruncateMaxChars",
|
|
66
|
+
"outputSmartTruncate",
|
|
67
|
+
"outputSmartTruncateMaxLines",
|
|
68
|
+
],
|
|
69
|
+
},
|
|
70
|
+
] as const;
|
|
71
|
+
|
|
72
|
+
function buildTabbedSettingGroups(settings: SettingItem[]): Array<{ label: string; settings: SettingItem[] }> {
|
|
73
|
+
const byId = new Map(settings.map((setting) => [setting.id, setting]));
|
|
74
|
+
const assignedIds = new Set<string>();
|
|
75
|
+
|
|
76
|
+
const tabs = SETTINGS_TAB_DEFINITIONS.map(({ label, settingIds }) => ({
|
|
77
|
+
label,
|
|
78
|
+
settings: settingIds.map((id) => {
|
|
79
|
+
const setting = byId.get(id);
|
|
80
|
+
if (!setting) {
|
|
81
|
+
throw new Error(`Missing setting item for tab '${label}': ${id}`);
|
|
82
|
+
}
|
|
83
|
+
if (assignedIds.has(id)) {
|
|
84
|
+
throw new Error(`Setting item assigned to multiple tabs: ${id}`);
|
|
85
|
+
}
|
|
86
|
+
assignedIds.add(id);
|
|
87
|
+
return setting;
|
|
88
|
+
}),
|
|
89
|
+
}));
|
|
90
|
+
|
|
91
|
+
const unassignedIds = settings.map((setting) => setting.id).filter((id) => !assignedIds.has(id));
|
|
92
|
+
if (unassignedIds.length > 0) {
|
|
93
|
+
throw new Error(`Unassigned setting items: ${unassignedIds.join(", ")}`);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return tabs;
|
|
97
|
+
}
|
|
34
98
|
|
|
35
99
|
function parseSourceFilterLevel(
|
|
36
100
|
value: string,
|
|
@@ -53,12 +117,21 @@ function parseIntegerInRange(value: string, min: number, max: number): number |
|
|
|
53
117
|
return parsed;
|
|
54
118
|
}
|
|
55
119
|
|
|
56
|
-
function
|
|
120
|
+
function summarizeRuntimeStatus(runtimeStatus: RuntimeStatus): string {
|
|
57
121
|
const runtime = runtimeStatus.rtkAvailable
|
|
58
122
|
? "rtk=available"
|
|
59
123
|
: `rtk=missing${runtimeStatus.lastError ? ` (${runtimeStatus.lastError})` : ""}`;
|
|
124
|
+
const executable = runtimeStatus.rtkExecutablePath
|
|
125
|
+
? `, rtkPath=${runtimeStatus.rtkExecutablePath}`
|
|
126
|
+
: runtimeStatus.rtkExecutableResolutionWarning
|
|
127
|
+
? `, rtkPath=unresolved (${runtimeStatus.rtkExecutableResolutionWarning})`
|
|
128
|
+
: "";
|
|
60
129
|
|
|
61
|
-
return
|
|
130
|
+
return `${runtime}${executable}`;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function summarizeConfig(config: RtkIntegrationConfig, runtimeStatus: RuntimeStatus): string {
|
|
134
|
+
return `enabled=${config.enabled}, mode=${config.mode}, rewriteSource=rtk, rewriteNotice=${config.showRewriteNotifications}, compaction=${config.outputCompaction.enabled}, readCompaction=${config.outputCompaction.readCompaction.enabled}, sourceFilterEnabled=${config.outputCompaction.sourceCodeFilteringEnabled}, preserveSkillReads=${config.outputCompaction.preserveExactSkillReads}, sourceFilter=${config.outputCompaction.sourceCodeFiltering}, ${summarizeRuntimeStatus(runtimeStatus)}`;
|
|
62
135
|
}
|
|
63
136
|
|
|
64
137
|
function buildSettingItems(config: RtkIntegrationConfig): SettingItem[] {
|
|
@@ -391,12 +464,14 @@ async function openSettingsModal(ctx: ExtensionCommandContext, controller: RtkIn
|
|
|
391
464
|
(tui, theme, _keybindings, done) => {
|
|
392
465
|
let current = controller.getConfig();
|
|
393
466
|
let settingsModal: ZellijSettingsModal | null = null;
|
|
467
|
+
const allSettings = buildSettingItems(current);
|
|
468
|
+
const tabs = buildTabbedSettingGroups(allSettings);
|
|
394
469
|
|
|
395
470
|
settingsModal = new ZellijSettingsModal(
|
|
396
471
|
{
|
|
397
|
-
title: "RTK
|
|
398
|
-
|
|
399
|
-
|
|
472
|
+
title: "Pi RTK Optimizer",
|
|
473
|
+
tabs,
|
|
474
|
+
activeTabIndex: 0,
|
|
400
475
|
onChange: (id, newValue) => {
|
|
401
476
|
current = applySetting(current, id, newValue);
|
|
402
477
|
controller.setConfig(current, ctx);
|
|
@@ -406,7 +481,7 @@ async function openSettingsModal(ctx: ExtensionCommandContext, controller: RtkIn
|
|
|
406
481
|
}
|
|
407
482
|
},
|
|
408
483
|
onClose: () => done(),
|
|
409
|
-
helpText:
|
|
484
|
+
helpText: `Config: ${controller.getConfigPath()}`,
|
|
410
485
|
enableSearch: true,
|
|
411
486
|
},
|
|
412
487
|
theme,
|
|
@@ -417,11 +492,14 @@ async function openSettingsModal(ctx: ExtensionCommandContext, controller: RtkIn
|
|
|
417
492
|
{
|
|
418
493
|
borderStyle: "rounded",
|
|
419
494
|
titleBar: {
|
|
420
|
-
left: "RTK
|
|
421
|
-
right: "pi-rtk-optimizer",
|
|
495
|
+
left: "Pi RTK Optimizer",
|
|
422
496
|
},
|
|
423
497
|
helpUndertitle: {
|
|
424
|
-
|
|
498
|
+
variants: [
|
|
499
|
+
"←/→ tabs • Type to search • Enter/Space change • Esc close",
|
|
500
|
+
"←/→ tabs • Type to search • Esc close",
|
|
501
|
+
"←/→ tabs • Esc close",
|
|
502
|
+
],
|
|
425
503
|
color: "dim",
|
|
426
504
|
},
|
|
427
505
|
overlay: overlayOptions,
|
|
@@ -474,7 +552,8 @@ async function handleArgs(
|
|
|
474
552
|
if (normalized === "verify") {
|
|
475
553
|
const runtimeStatus = await controller.refreshRuntimeStatus();
|
|
476
554
|
if (runtimeStatus.rtkAvailable) {
|
|
477
|
-
|
|
555
|
+
const pathDetail = runtimeStatus.rtkExecutablePath ? ` at ${runtimeStatus.rtkExecutablePath}` : "";
|
|
556
|
+
ctx.ui.notify(`RTK binary is available${pathDetail}.`, "info");
|
|
478
557
|
} else {
|
|
479
558
|
ctx.ui.notify(
|
|
480
559
|
`RTK binary is not available${runtimeStatus.lastError ? `: ${runtimeStatus.lastError}` : ""}.`,
|
package/src/constants.ts
CHANGED
package/src/index-test.ts
CHANGED
|
@@ -3,13 +3,13 @@ import { mock } from "bun:test";
|
|
|
3
3
|
|
|
4
4
|
import { runTest } from "./test-helpers.ts";
|
|
5
5
|
|
|
6
|
-
mock.module("@
|
|
6
|
+
mock.module("@earendil-works/pi-coding-agent", () => ({
|
|
7
7
|
getAgentDir: () => "/tmp/.pi/agent",
|
|
8
8
|
getSettingsListTheme: () => ({}),
|
|
9
9
|
isToolCallEventType: (toolName: string, event: Record<string, unknown>) => event.toolName === toolName,
|
|
10
10
|
}));
|
|
11
11
|
|
|
12
|
-
mock.module("@
|
|
12
|
+
mock.module("@earendil-works/pi-tui", () => ({
|
|
13
13
|
Box: class {},
|
|
14
14
|
Container: class {
|
|
15
15
|
addChild(): void {}
|
|
@@ -28,9 +28,33 @@ 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;
|
|
@@ -160,4 +184,175 @@ runTest("source-filter note skipped when all read filtering safeguards are disab
|
|
|
160
184
|
);
|
|
161
185
|
});
|
|
162
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
|
+
|
|
163
358
|
console.log("All index tests passed.");
|
package/src/index.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { isToolCallEventType, type ExtensionAPI, type ExtensionCommandContext, type ExtensionContext } from "@
|
|
1
|
+
import { isToolCallEventType, type ExtensionAPI, type ExtensionCommandContext, type ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import {
|
|
3
3
|
ensureConfigExists,
|
|
4
4
|
getRtkIntegrationConfigPath,
|
|
@@ -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";
|
|
@@ -115,6 +116,12 @@ export default function rtkIntegrationExtension(pi: ExtensionAPI): void {
|
|
|
115
116
|
return `RTK rewrite: ${original} -> ${rewritten}`;
|
|
116
117
|
};
|
|
117
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
|
+
|
|
118
125
|
const warnOnce = (
|
|
119
126
|
ctx: ExtensionContext | ExtensionCommandContext,
|
|
120
127
|
message: string,
|
|
@@ -190,12 +197,18 @@ export default function rtkIntegrationExtension(pi: ExtensionAPI): void {
|
|
|
190
197
|
};
|
|
191
198
|
|
|
192
199
|
const refreshRuntimeStatus = async (): Promise<RuntimeStatus> => {
|
|
200
|
+
let executableResolution: RtkExecutableResolution | undefined;
|
|
193
201
|
try {
|
|
194
|
-
|
|
202
|
+
executableResolution = await resolveRtkExecutable(pi);
|
|
203
|
+
const result = await pi.exec(executableResolution.command, ["--version"], { timeout: 5000 });
|
|
195
204
|
if (result.code === 0) {
|
|
196
205
|
runtimeStatus = {
|
|
197
206
|
rtkAvailable: true,
|
|
198
207
|
lastCheckedAt: Date.now(),
|
|
208
|
+
rtkExecutablePath: executableResolution.resolvedPath,
|
|
209
|
+
rtkExecutableCommand: executableResolution.command,
|
|
210
|
+
rtkExecutableResolver: executableResolution.resolver,
|
|
211
|
+
rtkExecutableResolutionWarning: executableResolution.warning,
|
|
199
212
|
};
|
|
200
213
|
missingRtkWarningShown = false;
|
|
201
214
|
return runtimeStatus;
|
|
@@ -208,6 +221,10 @@ export default function rtkIntegrationExtension(pi: ExtensionAPI): void {
|
|
|
208
221
|
rtkAvailable: false,
|
|
209
222
|
lastCheckedAt: Date.now(),
|
|
210
223
|
lastError: detail || `exit ${result.code}`,
|
|
224
|
+
rtkExecutablePath: executableResolution.resolvedPath,
|
|
225
|
+
rtkExecutableCommand: executableResolution.command,
|
|
226
|
+
rtkExecutableResolver: executableResolution.resolver,
|
|
227
|
+
rtkExecutableResolutionWarning: executableResolution.warning,
|
|
211
228
|
};
|
|
212
229
|
return runtimeStatus;
|
|
213
230
|
} catch (error) {
|
|
@@ -216,6 +233,10 @@ export default function rtkIntegrationExtension(pi: ExtensionAPI): void {
|
|
|
216
233
|
rtkAvailable: false,
|
|
217
234
|
lastCheckedAt: Date.now(),
|
|
218
235
|
lastError: trimMessage(message),
|
|
236
|
+
rtkExecutablePath: executableResolution?.resolvedPath,
|
|
237
|
+
rtkExecutableCommand: executableResolution?.command,
|
|
238
|
+
rtkExecutableResolver: executableResolution?.resolver,
|
|
239
|
+
rtkExecutableResolutionWarning: executableResolution?.warning,
|
|
219
240
|
};
|
|
220
241
|
return runtimeStatus;
|
|
221
242
|
}
|
|
@@ -303,10 +324,13 @@ export default function rtkIntegrationExtension(pi: ExtensionAPI): void {
|
|
|
303
324
|
}
|
|
304
325
|
|
|
305
326
|
trackBashCommand(eventRecord.toolCallId, eventRecord.args);
|
|
306
|
-
sanitizeStreamingBashExecutionResult(
|
|
327
|
+
const sanitization = sanitizeStreamingBashExecutionResult(
|
|
307
328
|
eventRecord.partialResult,
|
|
308
329
|
getTrackedBashCommand(eventRecord.toolCallId),
|
|
309
330
|
);
|
|
331
|
+
if (sanitization.changed) {
|
|
332
|
+
eventRecord.partialResult = sanitization.result;
|
|
333
|
+
}
|
|
310
334
|
});
|
|
311
335
|
|
|
312
336
|
pi.on("tool_execution_end", async (event) => {
|
|
@@ -317,7 +341,13 @@ export default function rtkIntegrationExtension(pi: ExtensionAPI): void {
|
|
|
317
341
|
|
|
318
342
|
try {
|
|
319
343
|
if (config.enabled && config.outputCompaction.enabled) {
|
|
320
|
-
sanitizeStreamingBashExecutionResult(
|
|
344
|
+
const sanitization = sanitizeStreamingBashExecutionResult(
|
|
345
|
+
eventRecord.result,
|
|
346
|
+
getTrackedBashCommand(eventRecord.toolCallId),
|
|
347
|
+
);
|
|
348
|
+
if (sanitization.changed) {
|
|
349
|
+
eventRecord.result = sanitization.result;
|
|
350
|
+
}
|
|
321
351
|
}
|
|
322
352
|
} finally {
|
|
323
353
|
forgetTrackedBashCommand(eventRecord.toolCallId);
|
|
@@ -362,8 +392,22 @@ export default function rtkIntegrationExtension(pi: ExtensionAPI): void {
|
|
|
362
392
|
return {};
|
|
363
393
|
}
|
|
364
394
|
|
|
365
|
-
|
|
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 });
|
|
366
407
|
if (!decision.changed) {
|
|
408
|
+
if (decision.warning) {
|
|
409
|
+
warnOnce(ctx, formatRewriteWarning(decision.originalCommand, decision.warning));
|
|
410
|
+
}
|
|
367
411
|
return {};
|
|
368
412
|
}
|
|
369
413
|
|