pi-rtk-optimizer 0.4.0 → 0.5.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 +19 -0
- package/package.json +60 -60
- package/src/additional-coverage-test.ts +81 -0
- package/src/index.ts +91 -1
- package/src/output-compactor-test.ts +166 -0
- package/src/output-compactor.ts +14 -0
- package/src/rtk-command-environment.ts +64 -0
- package/src/techniques/emoji.ts +91 -0
- package/src/techniques/git.ts +4 -2
- package/src/techniques/index.ts +2 -0
- package/src/techniques/rtk.ts +136 -0
- package/src/tool-execution-sanitizer.ts +69 -0
- package/src/types-shims.d.ts +3 -0
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,25 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.5.0] - 2026-03-23
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
- RTK_DB_PATH environment variable support for rewritten commands — enables RTK history database isolation per session
|
|
14
|
+
- Tool execution sanitizer to strip RTK self-diagnostics from streamed bash results before TUI rendering
|
|
15
|
+
- Tracking of active bash commands by tool call ID for output sanitization
|
|
16
|
+
- `rtk-command-environment.ts` module for platform-specific temp directory resolution and shell-safe quoting
|
|
17
|
+
|
|
18
|
+
### Changed
|
|
19
|
+
- Updated `@mariozechner/pi-coding-agent` and `@mariozechner/pi-tui` peer dependencies to ^0.62.0
|
|
20
|
+
- Simplified RTK hook warning detection — removed unused command-specific patterns and consolidated detection logic
|
|
21
|
+
- Focus on canonical hook warning messages that RTK emits
|
|
22
|
+
- Updated tests to verify simplified behavior and ensure non-hook RTK output is preserved verbatim
|
|
23
|
+
|
|
24
|
+
### Tests
|
|
25
|
+
- Added additional coverage tests for edge cases
|
|
26
|
+
- Added tests for output compactor behavior with RTK diagnostics
|
|
27
|
+
- Added tests for emoji stripping in RTK output
|
|
28
|
+
|
|
10
29
|
## [0.4.0] - 2026-03-12
|
|
11
30
|
|
|
12
31
|
### Added
|
package/package.json
CHANGED
|
@@ -1,60 +1,60 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "pi-rtk-optimizer",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Pi extension that optimizes RTK command rewriting and tool output compaction for the coding agent.",
|
|
5
|
-
"type": "module",
|
|
6
|
-
"main": "./index.ts",
|
|
7
|
-
"exports": {
|
|
8
|
-
".": "./index.ts"
|
|
9
|
-
},
|
|
10
|
-
"files": [
|
|
11
|
-
"index.ts",
|
|
12
|
-
"src",
|
|
13
|
-
"config/config.example.json",
|
|
14
|
-
"README.md",
|
|
15
|
-
"CHANGELOG.md",
|
|
16
|
-
"LICENSE"
|
|
17
|
-
],
|
|
18
|
-
"scripts": {
|
|
19
|
-
"build": "npx --yes -p typescript@5.7.3 tsc -p tsconfig.json --noCheck",
|
|
20
|
-
"lint": "npm run build",
|
|
21
|
-
"typecheck": "npx --yes -p typescript@5.7.3 tsc -p tsconfig.json",
|
|
22
|
-
"test": "bun ./src/output-compactor-test.ts && bun ./src/command-rewriter-test.ts && bun ./src/runtime-guard-test.ts && bun ./src/additional-coverage-test.ts && bun ./src/config-modal-test.ts && bun ./src/index-test.ts",
|
|
23
|
-
"check": "npm run lint && npm run typecheck && npm run test",
|
|
24
|
-
"build:check": "bunx esbuild ./index.ts --bundle --platform=node --format=esm --outfile=./.pi-rtk-optimizer-check.mjs --external:@mariozechner/pi-coding-agent --external:@mariozechner/pi-tui && bun -e \"import { unlinkSync } from 'node:fs'; unlinkSync('./.pi-rtk-optimizer-check.mjs');\""
|
|
25
|
-
},
|
|
26
|
-
"keywords": [
|
|
27
|
-
"pi-package",
|
|
28
|
-
"pi",
|
|
29
|
-
"pi-extension",
|
|
30
|
-
"rtk",
|
|
31
|
-
"token-optimization",
|
|
32
|
-
"tool-compaction",
|
|
33
|
-
"coding-agent"
|
|
34
|
-
],
|
|
35
|
-
"author": "MasuRii",
|
|
36
|
-
"license": "MIT",
|
|
37
|
-
"repository": {
|
|
38
|
-
"type": "git",
|
|
39
|
-
"url": "git+https://github.com/MasuRii/pi-rtk-optimizer.git"
|
|
40
|
-
},
|
|
41
|
-
"bugs": {
|
|
42
|
-
"url": "https://github.com/MasuRii/pi-rtk-optimizer/issues"
|
|
43
|
-
},
|
|
44
|
-
"homepage": "https://github.com/MasuRii/pi-rtk-optimizer#readme",
|
|
45
|
-
"engines": {
|
|
46
|
-
"node": ">=20"
|
|
47
|
-
},
|
|
48
|
-
"publishConfig": {
|
|
49
|
-
"access": "public"
|
|
50
|
-
},
|
|
51
|
-
"pi": {
|
|
52
|
-
"extensions": [
|
|
53
|
-
"./index.ts"
|
|
54
|
-
]
|
|
55
|
-
},
|
|
56
|
-
"peerDependencies": {
|
|
57
|
-
"@mariozechner/pi-coding-agent": "
|
|
58
|
-
"@mariozechner/pi-tui": "
|
|
59
|
-
}
|
|
60
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "pi-rtk-optimizer",
|
|
3
|
+
"version": "0.5.0",
|
|
4
|
+
"description": "Pi extension that optimizes RTK command rewriting and tool output compaction for the coding agent.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./index.ts",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": "./index.ts"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"index.ts",
|
|
12
|
+
"src",
|
|
13
|
+
"config/config.example.json",
|
|
14
|
+
"README.md",
|
|
15
|
+
"CHANGELOG.md",
|
|
16
|
+
"LICENSE"
|
|
17
|
+
],
|
|
18
|
+
"scripts": {
|
|
19
|
+
"build": "npx --yes -p typescript@5.7.3 tsc -p tsconfig.json --noCheck",
|
|
20
|
+
"lint": "npm run build",
|
|
21
|
+
"typecheck": "npx --yes -p typescript@5.7.3 tsc -p tsconfig.json",
|
|
22
|
+
"test": "bun ./src/output-compactor-test.ts && bun ./src/command-rewriter-test.ts && bun ./src/runtime-guard-test.ts && bun ./src/additional-coverage-test.ts && bun ./src/config-modal-test.ts && bun ./src/index-test.ts",
|
|
23
|
+
"check": "npm run lint && npm run typecheck && npm run test",
|
|
24
|
+
"build:check": "bunx esbuild ./index.ts --bundle --platform=node --format=esm --outfile=./.pi-rtk-optimizer-check.mjs --external:@mariozechner/pi-coding-agent --external:@mariozechner/pi-tui && bun -e \"import { unlinkSync } from 'node:fs'; unlinkSync('./.pi-rtk-optimizer-check.mjs');\""
|
|
25
|
+
},
|
|
26
|
+
"keywords": [
|
|
27
|
+
"pi-package",
|
|
28
|
+
"pi",
|
|
29
|
+
"pi-extension",
|
|
30
|
+
"rtk",
|
|
31
|
+
"token-optimization",
|
|
32
|
+
"tool-compaction",
|
|
33
|
+
"coding-agent"
|
|
34
|
+
],
|
|
35
|
+
"author": "MasuRii",
|
|
36
|
+
"license": "MIT",
|
|
37
|
+
"repository": {
|
|
38
|
+
"type": "git",
|
|
39
|
+
"url": "git+https://github.com/MasuRii/pi-rtk-optimizer.git"
|
|
40
|
+
},
|
|
41
|
+
"bugs": {
|
|
42
|
+
"url": "https://github.com/MasuRii/pi-rtk-optimizer/issues"
|
|
43
|
+
},
|
|
44
|
+
"homepage": "https://github.com/MasuRii/pi-rtk-optimizer#readme",
|
|
45
|
+
"engines": {
|
|
46
|
+
"node": ">=20"
|
|
47
|
+
},
|
|
48
|
+
"publishConfig": {
|
|
49
|
+
"access": "public"
|
|
50
|
+
},
|
|
51
|
+
"pi": {
|
|
52
|
+
"extensions": [
|
|
53
|
+
"./index.ts"
|
|
54
|
+
]
|
|
55
|
+
},
|
|
56
|
+
"peerDependencies": {
|
|
57
|
+
"@mariozechner/pi-coding-agent": "^0.62.0",
|
|
58
|
+
"@mariozechner/pi-tui": "^0.62.0"
|
|
59
|
+
}
|
|
60
|
+
}
|
|
@@ -14,6 +14,9 @@ import { matchesCommandPatterns, normalizeCommandForDetection } from "./techniqu
|
|
|
14
14
|
import { compactPath } from "./techniques/path-utils.ts";
|
|
15
15
|
import { applyWindowsBashCompatibilityFixes } from "./windows-command-helpers.ts";
|
|
16
16
|
import { applyRewrittenCommandShellSafetyFixups } from "./rewrite-pipeline-safety.ts";
|
|
17
|
+
import { sanitizeStreamingBashExecutionResult } from "./tool-execution-sanitizer.ts";
|
|
18
|
+
import { sanitizeRtkEmojiOutput } from "./techniques/emoji.ts";
|
|
19
|
+
import { stripRtkHookWarnings } from "./techniques/rtk.ts";
|
|
17
20
|
|
|
18
21
|
function makeTempConfigPath(): string {
|
|
19
22
|
return `${getRtkIntegrationConfigPath()}.test-${Date.now()}-${Math.random().toString(16).slice(2)}.json`;
|
|
@@ -194,4 +197,82 @@ runTest("rewrite pipeline safety buffers rewritten Windows producer commands", (
|
|
|
194
197
|
assert.equal(applyRewrittenCommandShellSafetyFixups("git diff | grep TODO"), "git diff | grep TODO");
|
|
195
198
|
});
|
|
196
199
|
|
|
200
|
+
runTest("stripRtkHookWarnings handles bare, prefixed, and already-sanitized hook notices", () => {
|
|
201
|
+
assert.equal(
|
|
202
|
+
stripRtkHookWarnings("No hook installed — run `rtk init -g` for automatic token savings\n\nready\n", null),
|
|
203
|
+
"ready\n",
|
|
204
|
+
);
|
|
205
|
+
assert.equal(
|
|
206
|
+
stripRtkHookWarnings("[WARN] Hook outdated — run `rtk init -g` to update\n\nready\n", null),
|
|
207
|
+
"ready\n",
|
|
208
|
+
);
|
|
209
|
+
assert.equal(
|
|
210
|
+
stripRtkHookWarnings(
|
|
211
|
+
"?? bun.lock[rtk] /!\\ No hook installed — run `rtk init -g` for automatic token savings\n",
|
|
212
|
+
null,
|
|
213
|
+
),
|
|
214
|
+
"?? bun.lock\n",
|
|
215
|
+
);
|
|
216
|
+
assert.equal(
|
|
217
|
+
stripRtkHookWarnings("[rtk] /!\\ No hook installed — run `rtk init -g` for automatic token savings\n\n", "rtk git status"),
|
|
218
|
+
"",
|
|
219
|
+
);
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
runTest("stripRtkHookWarnings leaves quoted hook text untouched", () => {
|
|
223
|
+
const quoted = 'const warning = "No hook installed — run `rtk init -g` for automatic token savings";\n';
|
|
224
|
+
assert.equal(stripRtkHookWarnings(quoted, null), null);
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
runTest("sanitizeRtkEmojiOutput normalizes RTK-shaped warning output without removing content", () => {
|
|
228
|
+
const sanitized = sanitizeRtkEmojiOutput(
|
|
229
|
+
"⚠️ Warning: --hook-only only makes sense with --global\n For local projects, use default mode or --claude-md\n",
|
|
230
|
+
"rtk init --hook-only",
|
|
231
|
+
);
|
|
232
|
+
assert.equal(
|
|
233
|
+
sanitized,
|
|
234
|
+
"[WARN] Warning: --hook-only only makes sense with --global\n For local projects, use default mode or --claude-md\n",
|
|
235
|
+
);
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
runTest("streaming sanitizer strips hook notices, sanitizes emoji output, and preserves non-text blocks", () => {
|
|
239
|
+
const hookNoticeResult = {
|
|
240
|
+
content: [
|
|
241
|
+
{
|
|
242
|
+
type: "text",
|
|
243
|
+
text: "[rtk] /!\\ No hook installed — run `rtk init -g` for automatic token savings\n\nworking tree clean\n",
|
|
244
|
+
},
|
|
245
|
+
],
|
|
246
|
+
};
|
|
247
|
+
assert.equal(sanitizeStreamingBashExecutionResult(hookNoticeResult, "rtk git status"), true);
|
|
248
|
+
assert.equal(
|
|
249
|
+
(hookNoticeResult.content[0] as { text: string }).text,
|
|
250
|
+
"working tree clean\n",
|
|
251
|
+
);
|
|
252
|
+
|
|
253
|
+
const emojiResult = {
|
|
254
|
+
content: [
|
|
255
|
+
{ type: "text", text: "📄 src/file.ts\n✅ Files are identical\n" },
|
|
256
|
+
{ type: "image", url: "ignored" },
|
|
257
|
+
],
|
|
258
|
+
};
|
|
259
|
+
assert.equal(sanitizeStreamingBashExecutionResult(emojiResult, "rtk git diff -- src/file.ts"), true);
|
|
260
|
+
assert.equal((emojiResult.content[0] as { text: string }).text, "> src/file.ts\n[OK] Files are identical\n");
|
|
261
|
+
assert.deepEqual(emojiResult.content[1], { type: "image", url: "ignored" });
|
|
262
|
+
|
|
263
|
+
const parseWarningResult = {
|
|
264
|
+
content: [
|
|
265
|
+
{
|
|
266
|
+
type: "text",
|
|
267
|
+
text: "[rtk] warning: builtin filters: parse failure\n\nworking tree clean\n",
|
|
268
|
+
},
|
|
269
|
+
],
|
|
270
|
+
};
|
|
271
|
+
assert.equal(sanitizeStreamingBashExecutionResult(parseWarningResult, "rtk git status"), false);
|
|
272
|
+
assert.equal(
|
|
273
|
+
(parseWarningResult.content[0] as { text: string }).text,
|
|
274
|
+
"[rtk] warning: builtin filters: parse failure\n\nworking tree clean\n",
|
|
275
|
+
);
|
|
276
|
+
});
|
|
277
|
+
|
|
197
278
|
console.log("All additional coverage tests passed.");
|
package/src/index.ts
CHANGED
|
@@ -12,8 +12,10 @@ import { EXTENSION_NAME } from "./constants.js";
|
|
|
12
12
|
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
|
+
import { applyRtkCommandEnvironment } from "./rtk-command-environment.js";
|
|
15
16
|
import { applyRewrittenCommandShellSafetyFixups } from "./rewrite-pipeline-safety.js";
|
|
16
17
|
import { shouldRequireRtkAvailabilityForCommandHandling, shouldSkipCommandHandlingWhenRtkMissing } from "./runtime-guard.js";
|
|
18
|
+
import { sanitizeStreamingBashExecutionResult } from "./tool-execution-sanitizer.js";
|
|
17
19
|
import type { RtkIntegrationConfig, RuntimeStatus } from "./types.js";
|
|
18
20
|
import { applyWindowsBashCompatibilityFixes } from "./windows-command-helpers.js";
|
|
19
21
|
|
|
@@ -92,6 +94,7 @@ export default function rtkIntegrationExtension(pi: ExtensionAPI): void {
|
|
|
92
94
|
let runtimeStatus: RuntimeStatus = { rtkAvailable: false };
|
|
93
95
|
const warnedMessages = createBoundedNoticeTracker(100);
|
|
94
96
|
const suggestionNotices = createBoundedNoticeTracker(200);
|
|
97
|
+
const activeBashCommands = new Map<string, string>();
|
|
95
98
|
let missingRtkWarningShown = false;
|
|
96
99
|
|
|
97
100
|
const formatRewriteNotice = (originalCommand: string, rewrittenCommand: string): string => {
|
|
@@ -115,6 +118,41 @@ export default function rtkIntegrationExtension(pi: ExtensionAPI): void {
|
|
|
115
118
|
}
|
|
116
119
|
};
|
|
117
120
|
|
|
121
|
+
const clearTrackedBashCommands = (): void => {
|
|
122
|
+
activeBashCommands.clear();
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
const trackBashCommand = (toolCallId: unknown, args: unknown): void => {
|
|
126
|
+
if (typeof toolCallId !== "string") {
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const argsRecord = toRecord(args);
|
|
131
|
+
const command = typeof argsRecord.command === "string" ? argsRecord.command.trim() : "";
|
|
132
|
+
if (!command) {
|
|
133
|
+
activeBashCommands.delete(toolCallId);
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
activeBashCommands.set(toolCallId, command);
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
const getTrackedBashCommand = (toolCallId: unknown): string | undefined => {
|
|
141
|
+
if (typeof toolCallId !== "string") {
|
|
142
|
+
return undefined;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return activeBashCommands.get(toolCallId);
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
const forgetTrackedBashCommand = (toolCallId: unknown): void => {
|
|
149
|
+
if (typeof toolCallId !== "string") {
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
activeBashCommands.delete(toolCallId);
|
|
154
|
+
};
|
|
155
|
+
|
|
118
156
|
const refreshConfig = async (ctx?: ExtensionContext | ExtensionCommandContext): Promise<void> => {
|
|
119
157
|
const ensured = ensureConfigExists();
|
|
120
158
|
if (ensured.error && ctx) {
|
|
@@ -218,6 +256,7 @@ export default function rtkIntegrationExtension(pi: ExtensionAPI): void {
|
|
|
218
256
|
pi.on("session_start", async (_event, ctx) => {
|
|
219
257
|
warnedMessages.reset();
|
|
220
258
|
suggestionNotices.reset();
|
|
259
|
+
clearTrackedBashCommands();
|
|
221
260
|
missingRtkWarningShown = false;
|
|
222
261
|
await refreshConfig(ctx);
|
|
223
262
|
maybeWarnRtkMissing(ctx);
|
|
@@ -226,11 +265,61 @@ export default function rtkIntegrationExtension(pi: ExtensionAPI): void {
|
|
|
226
265
|
pi.on("session_switch", async (_event, ctx) => {
|
|
227
266
|
warnedMessages.reset();
|
|
228
267
|
suggestionNotices.reset();
|
|
268
|
+
clearTrackedBashCommands();
|
|
229
269
|
missingRtkWarningShown = false;
|
|
230
270
|
await refreshConfig(ctx);
|
|
231
271
|
maybeWarnRtkMissing(ctx);
|
|
232
272
|
});
|
|
233
273
|
|
|
274
|
+
pi.on("agent_end", async () => {
|
|
275
|
+
clearTrackedBashCommands();
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
pi.on("tool_execution_start", async (event) => {
|
|
279
|
+
if (!config.enabled || !config.outputCompaction.enabled) {
|
|
280
|
+
return;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
const eventRecord = toRecord(event);
|
|
284
|
+
if (eventRecord.toolName !== "bash") {
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
trackBashCommand(eventRecord.toolCallId, eventRecord.args);
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
pi.on("tool_execution_update", async (event) => {
|
|
292
|
+
if (!config.enabled || !config.outputCompaction.enabled) {
|
|
293
|
+
return;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
const eventRecord = toRecord(event);
|
|
297
|
+
if (eventRecord.toolName !== "bash") {
|
|
298
|
+
return;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
trackBashCommand(eventRecord.toolCallId, eventRecord.args);
|
|
302
|
+
sanitizeStreamingBashExecutionResult(
|
|
303
|
+
eventRecord.partialResult,
|
|
304
|
+
getTrackedBashCommand(eventRecord.toolCallId),
|
|
305
|
+
);
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
pi.on("tool_execution_end", async (event) => {
|
|
309
|
+
const eventRecord = toRecord(event);
|
|
310
|
+
if (eventRecord.toolName !== "bash") {
|
|
311
|
+
return;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
try {
|
|
315
|
+
if (config.enabled && config.outputCompaction.enabled) {
|
|
316
|
+
sanitizeStreamingBashExecutionResult(eventRecord.result, getTrackedBashCommand(eventRecord.toolCallId));
|
|
317
|
+
}
|
|
318
|
+
} finally {
|
|
319
|
+
forgetTrackedBashCommand(eventRecord.toolCallId);
|
|
320
|
+
}
|
|
321
|
+
});
|
|
322
|
+
|
|
234
323
|
pi.on("before_agent_start", async (event, ctx) => {
|
|
235
324
|
await ensureRuntimeStatusFresh();
|
|
236
325
|
maybeWarnRtkMissing(ctx);
|
|
@@ -274,7 +363,8 @@ export default function rtkIntegrationExtension(pi: ExtensionAPI): void {
|
|
|
274
363
|
if (config.showRewriteNotifications && ctx.hasUI) {
|
|
275
364
|
ctx.ui.notify(formatRewriteNotice(decision.originalCommand, decision.rewrittenCommand), "info");
|
|
276
365
|
}
|
|
277
|
-
|
|
366
|
+
const safeRewrittenCommand = applyRewrittenCommandShellSafetyFixups(decision.rewrittenCommand);
|
|
367
|
+
event.input.command = applyRtkCommandEnvironment(safeRewrittenCommand);
|
|
278
368
|
return {};
|
|
279
369
|
}
|
|
280
370
|
|
|
@@ -331,4 +331,170 @@ runTest("search output uses plain-text summary and file markers", () => {
|
|
|
331
331
|
assertNoOutputEmoji(compacted);
|
|
332
332
|
});
|
|
333
333
|
|
|
334
|
+
runTest("rtk env output sanitizes emoji section headers", () => {
|
|
335
|
+
const compacted = compactBashOutput(
|
|
336
|
+
"rtk env",
|
|
337
|
+
"📂 PATH Variables:\n🔧 Language/Runtime:\n☁️ Cloud/Services:\n🛠️ Tools:\n📋 Other:\n📊 Total: 91 vars\n",
|
|
338
|
+
);
|
|
339
|
+
|
|
340
|
+
assert.ok(compacted.includes("PATH Variables:"));
|
|
341
|
+
assert.ok(compacted.includes("Language/Runtime:"));
|
|
342
|
+
assert.ok(compacted.includes("Cloud/Services:"));
|
|
343
|
+
assert.ok(compacted.includes("Tools:"));
|
|
344
|
+
assert.ok(compacted.includes("Other:"));
|
|
345
|
+
assert.ok(compacted.includes("Total: 91 vars"));
|
|
346
|
+
assertNoOutputEmoji(compacted);
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
runTest("rtk-shaped env output sanitizes even when command name is not rtk", () => {
|
|
350
|
+
const compacted = compactBashOutput(
|
|
351
|
+
"echo probe",
|
|
352
|
+
"📂 PATH Variables:\n🔧 Language/Runtime:\n📊 Total: 91 vars\n",
|
|
353
|
+
);
|
|
354
|
+
|
|
355
|
+
assert.ok(compacted.includes("PATH Variables:"));
|
|
356
|
+
assert.ok(compacted.includes("Language/Runtime:"));
|
|
357
|
+
assert.ok(compacted.includes("Total: 91 vars"));
|
|
358
|
+
assertNoOutputEmoji(compacted);
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
runTest("rtk git-style output sanitizes emoji status markers", () => {
|
|
362
|
+
const compacted = compactBashOutput(
|
|
363
|
+
"rtk git status",
|
|
364
|
+
"📌 main\n✅ Staged: 1 files\n📝 Modified: 2 files\n❓ Untracked: 1 files\n⚠️ Conflicts: 1 files\n",
|
|
365
|
+
);
|
|
366
|
+
|
|
367
|
+
assert.ok(compacted.includes("Branch: main"));
|
|
368
|
+
assert.ok(compacted.includes("[OK] Staged: 1 files"));
|
|
369
|
+
assert.ok(compacted.includes("Modified: 2 files"));
|
|
370
|
+
assert.ok(compacted.includes("[INFO] Untracked: 1 files"));
|
|
371
|
+
assert.ok(compacted.includes("[WARN] Conflicts: 1 files"));
|
|
372
|
+
assertNoOutputEmoji(compacted);
|
|
373
|
+
});
|
|
374
|
+
|
|
375
|
+
runTest("rtk grep-style output sanitizes emoji file markers", () => {
|
|
376
|
+
const compacted = compactBashOutput(
|
|
377
|
+
"rtk grep EXTENSION_NAME agent/extensions/pi-rtk-optimizer/src/constants.ts",
|
|
378
|
+
"🔍 2 in 1F:\n\n📄 agent/extensions/pi-rtk-optimizer/src/constants.ts (2):\n 4: export const EXTENSION_NAME = \"pi-rtk-optimizer\";\n",
|
|
379
|
+
);
|
|
380
|
+
|
|
381
|
+
assert.ok(compacted.startsWith("2 in 1F:\n\n> agent/extensions/pi-rtk-optimizer/src/constants.ts (2):"));
|
|
382
|
+
assertNoOutputEmoji(compacted);
|
|
383
|
+
});
|
|
384
|
+
|
|
385
|
+
runTest("rtk git diff verbose summary sanitizes file markers", () => {
|
|
386
|
+
const compacted = compactBashOutput(
|
|
387
|
+
"rtk git diff -- agent/extensions/pi-mcp-adapter/package.json",
|
|
388
|
+
"agent/extensions/pi-mcp-adapter/package.json | 2 +-\n\n--- Changes ---\n\n📄 agent/extensions/pi-mcp-adapter/package.json\n @@ -38,7 +38,7 @@\n - \"@mariozechner/pi-coding-agent\": \"^0.58.1\",\n",
|
|
389
|
+
);
|
|
390
|
+
|
|
391
|
+
assert.ok(compacted.includes("--- Changes ---"));
|
|
392
|
+
assert.ok(compacted.includes("> agent/extensions/pi-mcp-adapter/package.json"));
|
|
393
|
+
assertNoOutputEmoji(compacted);
|
|
394
|
+
});
|
|
395
|
+
|
|
396
|
+
runTest("git diff compaction skips already-compacted RTK-shaped output", () => {
|
|
397
|
+
const compacted = compactBashOutput(
|
|
398
|
+
"git diff -- agent/extensions/pi-mcp-adapter/package.json",
|
|
399
|
+
"agent/extensions/pi-mcp-adapter/package.json | 2 +-\n\n--- Changes ---\n\n📄 agent/extensions/pi-mcp-adapter/package.json\n @@ -38,7 +38,7 @@\n - \"@mariozechner/pi-coding-agent\": \"^0.58.1\",\n",
|
|
400
|
+
);
|
|
401
|
+
|
|
402
|
+
assert.ok(compacted.includes("--- Changes ---"));
|
|
403
|
+
assert.ok(compacted.includes("> agent/extensions/pi-mcp-adapter/package.json"));
|
|
404
|
+
assertNoOutputEmoji(compacted);
|
|
405
|
+
});
|
|
406
|
+
|
|
407
|
+
runTest("rtk-shaped diff output sanitizes even when command name is not rtk", () => {
|
|
408
|
+
const compacted = compactBashOutput(
|
|
409
|
+
"echo probe",
|
|
410
|
+
"📊 file-a.txt → file-b.txt\n +1 added, -1 removed, ~0 modified\n\n- 2 beta\n+ 2 gamma\n",
|
|
411
|
+
);
|
|
412
|
+
|
|
413
|
+
assert.ok(compacted.startsWith("file-a.txt -> file-b.txt"));
|
|
414
|
+
assertNoOutputEmoji(compacted);
|
|
415
|
+
});
|
|
416
|
+
|
|
417
|
+
runTest("rtk-shaped identical diff output sanitizes even when command name is not rtk", () => {
|
|
418
|
+
const compacted = compactBashOutput("echo probe", "✅ Files are identical");
|
|
419
|
+
|
|
420
|
+
assert.equal(compacted, "[OK] Files are identical");
|
|
421
|
+
assertNoOutputEmoji(compacted);
|
|
422
|
+
});
|
|
423
|
+
|
|
424
|
+
runTest("hook warning is stripped even when the command label is not rtk", () => {
|
|
425
|
+
const compacted = compactBashOutput(
|
|
426
|
+
"echo probe",
|
|
427
|
+
"[rtk] /!\\ No hook installed — run `rtk init -g` for automatic token savings\n\n4 files changed\n",
|
|
428
|
+
);
|
|
429
|
+
|
|
430
|
+
assert.equal(compacted, "4 files changed\n");
|
|
431
|
+
});
|
|
432
|
+
|
|
433
|
+
runTest("hook-only output compacts to an empty text result", () => {
|
|
434
|
+
const result = compactToolResult(
|
|
435
|
+
{
|
|
436
|
+
toolName: "bash",
|
|
437
|
+
input: { command: "rtk git status" },
|
|
438
|
+
content: [{ type: "text", text: "[rtk] /!\\ No hook installed — run `rtk init -g` for automatic token savings\n" }],
|
|
439
|
+
},
|
|
440
|
+
cloneDefaultConfig(),
|
|
441
|
+
);
|
|
442
|
+
|
|
443
|
+
assert.equal(result.changed, true);
|
|
444
|
+
assert.ok(result.techniques.includes("rtk-hook-warning"));
|
|
445
|
+
assert.equal(firstTextBlock(result.content), "");
|
|
446
|
+
});
|
|
447
|
+
|
|
448
|
+
runTest("non-hook RTK warnings are preserved verbatim", () => {
|
|
449
|
+
const result = compactToolResult(
|
|
450
|
+
{
|
|
451
|
+
toolName: "bash",
|
|
452
|
+
input: { command: "FOO=1 rtk git status" },
|
|
453
|
+
content: [{ type: "text", text: "[rtk] warning: builtin filters: parse failure\n\nworking tree clean\n" }],
|
|
454
|
+
},
|
|
455
|
+
cloneDefaultConfig(),
|
|
456
|
+
);
|
|
457
|
+
|
|
458
|
+
assert.equal(result.changed, false);
|
|
459
|
+
assert.equal(result.content, undefined);
|
|
460
|
+
assert.deepEqual(result.techniques, []);
|
|
461
|
+
});
|
|
462
|
+
|
|
463
|
+
runTest("emoji RTK warnings stay visible and are sanitized to plain text", () => {
|
|
464
|
+
const compacted = compactBashOutput(
|
|
465
|
+
"rtk init --hook-only",
|
|
466
|
+
"⚠️ Warning: --hook-only only makes sense with --global\n For local projects, use default mode or --claude-md\n\nready\n",
|
|
467
|
+
);
|
|
468
|
+
|
|
469
|
+
assert.ok(compacted.includes("[WARN] Warning: --hook-only only makes sense with --global"));
|
|
470
|
+
assert.ok(compacted.includes("For local projects, use default mode or --claude-md"));
|
|
471
|
+
assert.ok(compacted.includes("ready\n"));
|
|
472
|
+
assertNoOutputEmoji(compacted);
|
|
473
|
+
});
|
|
474
|
+
|
|
475
|
+
runTest("outdated hook warning is stripped while preserving the RTK payload", () => {
|
|
476
|
+
const compacted = compactBashOutput(
|
|
477
|
+
"rtk gain",
|
|
478
|
+
"⚠️ Hook outdated — run `rtk init -g` to update\n\nSaved 42 tokens\n",
|
|
479
|
+
);
|
|
480
|
+
|
|
481
|
+
assert.equal(compacted, "Saved 42 tokens\n");
|
|
482
|
+
});
|
|
483
|
+
|
|
484
|
+
runTest("quoted hook warning text is preserved as payload", () => {
|
|
485
|
+
const quotedHookText = 'const warning = "No hook installed — run `rtk init -g` for automatic token savings";\n';
|
|
486
|
+
const result = compactToolResult(
|
|
487
|
+
{
|
|
488
|
+
toolName: "bash",
|
|
489
|
+
input: { command: "echo probe" },
|
|
490
|
+
content: [{ type: "text", text: quotedHookText }],
|
|
491
|
+
},
|
|
492
|
+
cloneDefaultConfig(),
|
|
493
|
+
);
|
|
494
|
+
|
|
495
|
+
assert.equal(result.changed, false);
|
|
496
|
+
assert.equal(result.content, undefined);
|
|
497
|
+
assert.deepEqual(result.techniques, []);
|
|
498
|
+
});
|
|
499
|
+
|
|
334
500
|
console.log("All output-compactor tests passed.");
|
package/src/output-compactor.ts
CHANGED
|
@@ -8,8 +8,10 @@ import {
|
|
|
8
8
|
filterBuildOutput,
|
|
9
9
|
filterSourceCode,
|
|
10
10
|
groupSearchResults,
|
|
11
|
+
sanitizeRtkEmojiOutput,
|
|
11
12
|
smartTruncate,
|
|
12
13
|
stripAnsiFast,
|
|
14
|
+
stripRtkHookWarnings,
|
|
13
15
|
truncate,
|
|
14
16
|
} from "./techniques/index.js";
|
|
15
17
|
import { trackOutputSavings } from "./output-metrics.js";
|
|
@@ -200,6 +202,18 @@ function compactBashText(
|
|
|
200
202
|
}
|
|
201
203
|
}
|
|
202
204
|
|
|
205
|
+
const withoutRtkHookWarnings = stripRtkHookWarnings(nextText, command);
|
|
206
|
+
if (withoutRtkHookWarnings !== null && withoutRtkHookWarnings !== nextText) {
|
|
207
|
+
nextText = withoutRtkHookWarnings;
|
|
208
|
+
techniques.push("rtk-hook-warning");
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const withoutRtkEmoji = sanitizeRtkEmojiOutput(nextText, command);
|
|
212
|
+
if (withoutRtkEmoji !== null && withoutRtkEmoji !== nextText) {
|
|
213
|
+
nextText = withoutRtkEmoji;
|
|
214
|
+
techniques.push("rtk-emoji");
|
|
215
|
+
}
|
|
216
|
+
|
|
203
217
|
if (compaction.filterBuildOutput) {
|
|
204
218
|
const compacted = filterBuildOutput(nextText, command);
|
|
205
219
|
if (compacted !== null && compacted !== nextText) {
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { join } from "node:path";
|
|
2
|
+
|
|
3
|
+
const RTK_DB_PATH_ENV_NAME = "RTK_DB_PATH";
|
|
4
|
+
const LEADING_ENV_ASSIGNMENT_PATTERN = /^((?:[A-Za-z_][A-Za-z0-9_]*=(?:"[^"]*"|'[^']*'|[^\s]+)\s+)*)/;
|
|
5
|
+
const RTK_DB_PATH_ASSIGNMENT_PATTERN = /(?:^|\s)RTK_DB_PATH=(?:"[^"]*"|'[^']*'|[^\s]+)(?=\s|$)/;
|
|
6
|
+
|
|
7
|
+
function resolveTemporaryDirectory(): string {
|
|
8
|
+
if (process.platform === "win32") {
|
|
9
|
+
const windowsTempDir = process.env.TEMP ?? process.env.TMP;
|
|
10
|
+
if (windowsTempDir && windowsTempDir.trim()) {
|
|
11
|
+
return windowsTempDir;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const localAppData = process.env.LOCALAPPDATA;
|
|
15
|
+
if (localAppData && localAppData.trim()) {
|
|
16
|
+
return join(localAppData, "Temp");
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const userProfile = process.env.USERPROFILE;
|
|
20
|
+
if (userProfile && userProfile.trim()) {
|
|
21
|
+
return join(userProfile, "AppData", "Local", "Temp");
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const systemRoot = process.env.SystemRoot ?? process.env.WINDIR;
|
|
25
|
+
if (systemRoot && systemRoot.trim()) {
|
|
26
|
+
return join(systemRoot, "Temp");
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return "C:/Windows/Temp";
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const posixTempDir = process.env.TMPDIR ?? process.env.TMP;
|
|
33
|
+
if (posixTempDir && posixTempDir.trim()) {
|
|
34
|
+
return posixTempDir;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return "/tmp";
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function getTemporaryRtkHistoryDbPath(): string {
|
|
41
|
+
return join(resolveTemporaryDirectory(), "pi-rtk-optimizer", "history.db");
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function quoteForShellEnv(value: string): string {
|
|
45
|
+
const normalizedValue = process.platform === "win32" ? value.replace(/\\/g, "/") : value;
|
|
46
|
+
return `"${normalizedValue.replace(/"/g, '\\"')}"`;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function hasLeadingRtkDbPathAssignment(command: string): boolean {
|
|
50
|
+
const leadingAssignments = command.match(LEADING_ENV_ASSIGNMENT_PATTERN)?.[1] ?? "";
|
|
51
|
+
return RTK_DB_PATH_ASSIGNMENT_PATTERN.test(leadingAssignments);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function applyRtkCommandEnvironment(command: string): string {
|
|
55
|
+
if (!command.trim()) {
|
|
56
|
+
return command;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (hasLeadingRtkDbPathAssignment(command)) {
|
|
60
|
+
return command;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return `${RTK_DB_PATH_ENV_NAME}=${quoteForShellEnv(getTemporaryRtkHistoryDbPath())} ${command}`;
|
|
64
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
const RTK_COMMAND_PATTERN = /^\s*rtk(?:\.exe)?(?:\s|$)/;
|
|
2
|
+
const RTK_OUTPUT_SIGNATURE_PATTERNS = [
|
|
3
|
+
/^📂 PATH Variables:/m,
|
|
4
|
+
/^🔧 Language\/Runtime:/m,
|
|
5
|
+
/^☁️?\s+Cloud\/Services:/m,
|
|
6
|
+
/^🛠️?\s+Tools:/m,
|
|
7
|
+
/^📋 Other:/m,
|
|
8
|
+
/^📊 Total:/m,
|
|
9
|
+
/^📊\s+.+\s+→\s+.+/m,
|
|
10
|
+
/^📌\s+/m,
|
|
11
|
+
/^✅ Files are identical$/m,
|
|
12
|
+
/^✅ Staged:/m,
|
|
13
|
+
/^📝 Modified:/m,
|
|
14
|
+
/^❓ Untracked:/m,
|
|
15
|
+
/^⚠️?\s+Conflicts:/m,
|
|
16
|
+
/^🔍 CI Checks Summary:/m,
|
|
17
|
+
/^🔍\s+\d+\s+in\s+\d+F:/m,
|
|
18
|
+
/^--- Changes ---$/m,
|
|
19
|
+
/^📄\s+.+$/m,
|
|
20
|
+
/^📁\s+\d+F\s+\d+D:/m,
|
|
21
|
+
/^☸️?\s+\d+\s+pods:/m,
|
|
22
|
+
/^📦\s+/m,
|
|
23
|
+
] as const;
|
|
24
|
+
|
|
25
|
+
const LINE_PREFIX_REPLACEMENTS: Array<{ pattern: RegExp; replacement: string }> = [
|
|
26
|
+
{ pattern: /^🔍\s+/gm, replacement: "" },
|
|
27
|
+
{ pattern: /^📄\s+/gm, replacement: "> " },
|
|
28
|
+
{ pattern: /^📂\s+/gm, replacement: "" },
|
|
29
|
+
{ pattern: /^🔧\s+/gm, replacement: "" },
|
|
30
|
+
{ pattern: /^☁️?\s+/gm, replacement: "" },
|
|
31
|
+
{ pattern: /^🛠️?\s+/gm, replacement: "" },
|
|
32
|
+
{ pattern: /^📋\s+/gm, replacement: "" },
|
|
33
|
+
{ pattern: /^📊\s+/gm, replacement: "" },
|
|
34
|
+
{ pattern: /^📌\s+/gm, replacement: "Branch: " },
|
|
35
|
+
{ pattern: /^📝\s+/gm, replacement: "" },
|
|
36
|
+
{ pattern: /^📦\s+/gm, replacement: "" },
|
|
37
|
+
{ pattern: /^📁\s+/gm, replacement: "" },
|
|
38
|
+
{ pattern: /^☸️?\s+/gm, replacement: "" },
|
|
39
|
+
] as const;
|
|
40
|
+
|
|
41
|
+
const INLINE_REPLACEMENTS: Array<{ pattern: RegExp; replacement: string }> = [
|
|
42
|
+
{ pattern: /✅|✓|✔/g, replacement: "[OK]" },
|
|
43
|
+
{ pattern: /❌|✗|✕/g, replacement: "[ERROR]" },
|
|
44
|
+
{ pattern: /⚠️|⚠/g, replacement: "[WARN]" },
|
|
45
|
+
{ pattern: /❓/g, replacement: "[INFO]" },
|
|
46
|
+
{ pattern: /⏭️|⏭/g, replacement: "[SKIP]" },
|
|
47
|
+
{ pattern: /⏳/g, replacement: "Pending" },
|
|
48
|
+
{ pattern: /⬆️|⬆/g, replacement: "up" },
|
|
49
|
+
{ pattern: /→/g, replacement: "->" },
|
|
50
|
+
{ pattern: /•/g, replacement: "-" },
|
|
51
|
+
] as const;
|
|
52
|
+
|
|
53
|
+
const REMAINING_EMOJI_PATTERN = /\p{Extended_Pictographic}/gu;
|
|
54
|
+
const EMOJI_VARIATION_SELECTOR_PATTERN = /\uFE0F/g;
|
|
55
|
+
const INLINE_LABEL_SPACING_PATTERN = /(\[[A-Z]+\])(\S)/g;
|
|
56
|
+
|
|
57
|
+
function isRtkCommand(command: string | undefined | null): boolean {
|
|
58
|
+
return typeof command === "string" && RTK_COMMAND_PATTERN.test(command);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function looksLikeRtkStyledOutput(output: string): boolean {
|
|
62
|
+
return RTK_OUTPUT_SIGNATURE_PATTERNS.some((pattern) => pattern.test(output));
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* RTK emits emoji-heavy presentation in several command outputs. Pi should
|
|
67
|
+
* present tool results as plain text, so normalize RTK output markers before
|
|
68
|
+
* the agent consumes them. We apply this to explicit `rtk ...` commands and to
|
|
69
|
+
* recognizable RTK-shaped output that may have been prefixed by another layer.
|
|
70
|
+
*/
|
|
71
|
+
export function sanitizeRtkEmojiOutput(output: string, command: string | undefined | null): string | null {
|
|
72
|
+
if (!isRtkCommand(command) && !looksLikeRtkStyledOutput(output)) {
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
let nextText = output;
|
|
77
|
+
|
|
78
|
+
for (const { pattern, replacement } of LINE_PREFIX_REPLACEMENTS) {
|
|
79
|
+
nextText = nextText.replace(pattern, replacement);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
for (const { pattern, replacement } of INLINE_REPLACEMENTS) {
|
|
83
|
+
nextText = nextText.replace(pattern, replacement);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
nextText = nextText.replace(REMAINING_EMOJI_PATTERN, "");
|
|
87
|
+
nextText = nextText.replace(EMOJI_VARIATION_SELECTOR_PATTERN, "");
|
|
88
|
+
nextText = nextText.replace(INLINE_LABEL_SPACING_PATTERN, "$1 $2");
|
|
89
|
+
|
|
90
|
+
return nextText === output ? null : nextText;
|
|
91
|
+
}
|
package/src/techniques/git.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { matchesCommandPatterns, normalizeCommandForDetection } from "./command-detection.js";
|
|
2
2
|
|
|
3
3
|
const GIT_COMMAND_PATTERNS = [/^git\s+(diff|status|log|show|stash)\b/] as const;
|
|
4
|
+
const RAW_GIT_DIFF_PATTERN = /^diff --git /m;
|
|
5
|
+
const RAW_GIT_STATUS_PATTERN = /^(?:## |(?:M|A|D|R|C|U|\?| )\S)/m;
|
|
4
6
|
|
|
5
7
|
export function isGitCommand(command: string | undefined | null): boolean {
|
|
6
8
|
return matchesCommandPatterns(command, GIT_COMMAND_PATTERNS);
|
|
@@ -216,10 +218,10 @@ export function compactGitOutput(output: string, command: string | undefined | n
|
|
|
216
218
|
}
|
|
217
219
|
|
|
218
220
|
if (normalized.startsWith("git diff")) {
|
|
219
|
-
return compactDiff(output);
|
|
221
|
+
return RAW_GIT_DIFF_PATTERN.test(output) ? compactDiff(output) : null;
|
|
220
222
|
}
|
|
221
223
|
if (normalized.startsWith("git status")) {
|
|
222
|
-
return compactStatus(output);
|
|
224
|
+
return RAW_GIT_STATUS_PATTERN.test(output) ? compactStatus(output) : null;
|
|
223
225
|
}
|
|
224
226
|
if (normalized.startsWith("git log")) {
|
|
225
227
|
return compactLog(output);
|
package/src/techniques/index.ts
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
export { stripAnsiFast } from "./ansi.js";
|
|
2
2
|
export { truncate } from "./truncate.js";
|
|
3
|
+
export { sanitizeRtkEmojiOutput } from "./emoji.js";
|
|
3
4
|
export { filterBuildOutput } from "./build.js";
|
|
4
5
|
export { aggregateTestOutput } from "./test-output.js";
|
|
5
6
|
export { aggregateLinterOutput } from "./linter.js";
|
|
6
7
|
export { detectLanguage, smartTruncate, filterSourceCode } from "./source.js";
|
|
7
8
|
export { compactGitOutput } from "./git.js";
|
|
8
9
|
export { groupSearchResults } from "./search.js";
|
|
10
|
+
export { stripRtkHookWarnings } from "./rtk.js";
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
const RTK_HOOK_WARNING_MESSAGES = [
|
|
2
|
+
"No hook installed — run `rtk init -g` for automatic token savings",
|
|
3
|
+
"Hook outdated — run `rtk init -g` to update",
|
|
4
|
+
] as const;
|
|
5
|
+
|
|
6
|
+
const RTK_HOOK_WARNING_PREFIX_MARKERS = ["[rtk] /!\\", "⚠", "[WARN]"] as const;
|
|
7
|
+
|
|
8
|
+
type HookWarningLineStripResult =
|
|
9
|
+
| {
|
|
10
|
+
removed: false;
|
|
11
|
+
removedLine: false;
|
|
12
|
+
line: string;
|
|
13
|
+
}
|
|
14
|
+
| {
|
|
15
|
+
removed: true;
|
|
16
|
+
removedLine: boolean;
|
|
17
|
+
line: string;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
function outputContainsKnownHookWarning(output: string): boolean {
|
|
21
|
+
return RTK_HOOK_WARNING_MESSAGES.some((message) => output.includes(message));
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function isQuotedPrefixBoundary(line: string, prefixIndex: number): boolean {
|
|
25
|
+
if (prefixIndex <= 0) {
|
|
26
|
+
return false;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const charBefore = line[prefixIndex - 1];
|
|
30
|
+
return charBefore === "\"" || charBefore === "'";
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function findClosestWarningPrefixIndex(line: string, beforeIndex: number): number {
|
|
34
|
+
let closestIndex = -1;
|
|
35
|
+
for (const marker of RTK_HOOK_WARNING_PREFIX_MARKERS) {
|
|
36
|
+
const index = line.lastIndexOf(marker, beforeIndex);
|
|
37
|
+
if (index > closestIndex) {
|
|
38
|
+
closestIndex = index;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return closestIndex;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function stripHookWarningFromLine(line: string): HookWarningLineStripResult {
|
|
46
|
+
const trimmed = line.trim();
|
|
47
|
+
if (!trimmed) {
|
|
48
|
+
return { removed: false, removedLine: false, line };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (RTK_HOOK_WARNING_MESSAGES.some((message) => trimmed === message)) {
|
|
52
|
+
return { removed: true, removedLine: true, line: "" };
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
for (const message of RTK_HOOK_WARNING_MESSAGES) {
|
|
56
|
+
const messageIndex = line.indexOf(message);
|
|
57
|
+
if (messageIndex === -1) {
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const prefixIndex = findClosestWarningPrefixIndex(line, messageIndex);
|
|
62
|
+
if (prefixIndex === -1) {
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (isQuotedPrefixBoundary(line, prefixIndex)) {
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
let removalStart = prefixIndex;
|
|
71
|
+
while (removalStart > 0 && /\s/.test(line[removalStart - 1] ?? "")) {
|
|
72
|
+
removalStart -= 1;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const removalEnd = messageIndex + message.length;
|
|
76
|
+
const before = line.slice(0, removalStart);
|
|
77
|
+
const after = line.slice(removalEnd);
|
|
78
|
+
|
|
79
|
+
let nextLine = `${before}${after}`;
|
|
80
|
+
if (before.trim() !== "" && after.trim() !== "") {
|
|
81
|
+
nextLine = `${before.trimEnd()}\n${after}`;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (!nextLine.trim()) {
|
|
85
|
+
return { removed: true, removedLine: true, line: "" };
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return { removed: true, removedLine: false, line: nextLine };
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return { removed: false, removedLine: false, line };
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Removes only RTK hook status notices that are not actionable inside Pi.
|
|
96
|
+
* Other RTK warnings should remain visible so the agent can inspect them.
|
|
97
|
+
*/
|
|
98
|
+
export function stripRtkHookWarnings(output: string, _command: string | undefined | null): string | null {
|
|
99
|
+
if (!outputContainsKnownHookWarning(output)) {
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const filteredLines: string[] = [];
|
|
104
|
+
let removedWarning = false;
|
|
105
|
+
let skipImmediateBlankLine = false;
|
|
106
|
+
|
|
107
|
+
for (const line of output.split("\n")) {
|
|
108
|
+
if (skipImmediateBlankLine && line.trim() === "") {
|
|
109
|
+
skipImmediateBlankLine = false;
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const stripped = stripHookWarningFromLine(line);
|
|
114
|
+
if (stripped.removed) {
|
|
115
|
+
removedWarning = true;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (stripped.removedLine) {
|
|
119
|
+
skipImmediateBlankLine = true;
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
skipImmediateBlankLine = false;
|
|
124
|
+
filteredLines.push(stripped.line);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (!removedWarning) {
|
|
128
|
+
return null;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
while (filteredLines.length > 0 && filteredLines[0]?.trim() === "") {
|
|
132
|
+
filteredLines.shift();
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return filteredLines.join("\n");
|
|
136
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { toRecord } from "./record-utils.js";
|
|
2
|
+
import { sanitizeRtkEmojiOutput, stripAnsiFast, stripRtkHookWarnings } from "./techniques/index.js";
|
|
3
|
+
|
|
4
|
+
interface ToolResultTextBlock {
|
|
5
|
+
type: string;
|
|
6
|
+
text?: string;
|
|
7
|
+
[key: string]: unknown;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function sanitizeStreamingBashText(text: string, command: string | undefined | null): string {
|
|
11
|
+
let nextText = stripAnsiFast(text);
|
|
12
|
+
|
|
13
|
+
const withoutRtkHookWarnings = stripRtkHookWarnings(nextText, command);
|
|
14
|
+
if (withoutRtkHookWarnings !== null) {
|
|
15
|
+
nextText = withoutRtkHookWarnings;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const withoutRtkEmoji = sanitizeRtkEmojiOutput(nextText, command);
|
|
19
|
+
if (withoutRtkEmoji !== null) {
|
|
20
|
+
nextText = withoutRtkEmoji;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
return nextText;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Sanitizes streamed bash result blocks before the TUI renders them so RTK
|
|
28
|
+
* self-diagnostics never flash in partial or final tool output.
|
|
29
|
+
*/
|
|
30
|
+
export function sanitizeStreamingBashExecutionResult(
|
|
31
|
+
result: unknown,
|
|
32
|
+
command: string | undefined | null,
|
|
33
|
+
): boolean {
|
|
34
|
+
const resultRecord = toRecord(result);
|
|
35
|
+
const sourceContent = Array.isArray(resultRecord.content) ? resultRecord.content : null;
|
|
36
|
+
if (!sourceContent || sourceContent.length === 0) {
|
|
37
|
+
return false;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
let changed = false;
|
|
41
|
+
const nextContent = sourceContent.map((block) => {
|
|
42
|
+
if (!block || typeof block !== "object" || Array.isArray(block)) {
|
|
43
|
+
return block;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const contentBlock = block as ToolResultTextBlock;
|
|
47
|
+
if (contentBlock.type !== "text" || typeof contentBlock.text !== "string") {
|
|
48
|
+
return block;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const sanitizedText = sanitizeStreamingBashText(contentBlock.text, command);
|
|
52
|
+
if (sanitizedText === contentBlock.text) {
|
|
53
|
+
return block;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
changed = true;
|
|
57
|
+
return {
|
|
58
|
+
...contentBlock,
|
|
59
|
+
text: sanitizedText,
|
|
60
|
+
};
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
if (!changed) {
|
|
64
|
+
return false;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
resultRecord.content = nextContent;
|
|
68
|
+
return true;
|
|
69
|
+
}
|
package/src/types-shims.d.ts
CHANGED
|
@@ -168,6 +168,7 @@ declare module "bun:test" {
|
|
|
168
168
|
declare const process: {
|
|
169
169
|
platform: string;
|
|
170
170
|
env: Record<string, string | undefined>;
|
|
171
|
+
cwd(): string;
|
|
171
172
|
};
|
|
172
173
|
|
|
173
174
|
declare module "node:os" {
|
|
@@ -177,6 +178,8 @@ declare module "node:os" {
|
|
|
177
178
|
declare module "node:path" {
|
|
178
179
|
export function join(...segments: string[]): string;
|
|
179
180
|
export function dirname(path: string): string;
|
|
181
|
+
export function resolve(...segments: string[]): string;
|
|
182
|
+
export const sep: string;
|
|
180
183
|
}
|
|
181
184
|
|
|
182
185
|
declare module "node:fs" {
|