pi-rtk-optimizer 0.4.0 → 0.5.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 CHANGED
@@ -7,6 +7,43 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.5.1] - 2026-03-24
11
+
12
+ ### Fixed
13
+ - RTK_DB_PATH environment variable now correctly scoped to rewritten producer commands only — Windows commands now use subshell scoping `{ RTK_DB_PATH=...; ... }` instead of leaking the prefix into the rewritten command
14
+ - Command rewrite pipeline now applies environment scoping BEFORE shell safety fixups to prevent env prefix stripping
15
+
16
+ ### Added
17
+ - `shell-env-prefix.ts` module for splitting leading environment variable assignments from commands
18
+ - `splitLeadingEnvAssignments()` function to properly extract `ENV=value` prefixes before command analysis
19
+
20
+ ### Changed
21
+ - Refactored `rtk-command-environment.ts` to use the new `splitLeadingEnvAssignments` utility
22
+ - Refactored `rewrite-pipeline-safety.ts` to preserve env prefixes when analyzing and rewriting rtk commands
23
+
24
+ ### Tests
25
+ - Added test coverage for RTK_DB_PATH scoping on Windows vs Unix platforms
26
+ - Verified env prefix is preserved through the rewrite pipeline
27
+
28
+ ## [0.5.0] - 2026-03-23
29
+
30
+ ### Added
31
+ - RTK_DB_PATH environment variable support for rewritten commands — enables RTK history database isolation per session
32
+ - Tool execution sanitizer to strip RTK self-diagnostics from streamed bash results before TUI rendering
33
+ - Tracking of active bash commands by tool call ID for output sanitization
34
+ - `rtk-command-environment.ts` module for platform-specific temp directory resolution and shell-safe quoting
35
+
36
+ ### Changed
37
+ - Updated `@mariozechner/pi-coding-agent` and `@mariozechner/pi-tui` peer dependencies to ^0.62.0
38
+ - Simplified RTK hook warning detection — removed unused command-specific patterns and consolidated detection logic
39
+ - Focus on canonical hook warning messages that RTK emits
40
+ - Updated tests to verify simplified behavior and ensure non-hook RTK output is preserved verbatim
41
+
42
+ ### Tests
43
+ - Added additional coverage tests for edge cases
44
+ - Added tests for output compactor behavior with RTK diagnostics
45
+ - Added tests for emoji stripping in RTK output
46
+
10
47
  ## [0.4.0] - 2026-03-12
11
48
 
12
49
  ### Added
package/package.json CHANGED
@@ -1,60 +1,60 @@
1
- {
2
- "name": "pi-rtk-optimizer",
3
- "version": "0.4.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.1",
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,10 @@ 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 { applyRtkCommandEnvironment } from "./rtk-command-environment.ts";
18
+ import { sanitizeStreamingBashExecutionResult } from "./tool-execution-sanitizer.ts";
19
+ import { sanitizeRtkEmojiOutput } from "./techniques/emoji.ts";
20
+ import { stripRtkHookWarnings } from "./techniques/rtk.ts";
17
21
 
18
22
  function makeTempConfigPath(): string {
19
23
  return `${getRtkIntegrationConfigPath()}.test-${Date.now()}-${Math.random().toString(16).slice(2)}.json`;
@@ -194,4 +198,104 @@ runTest("rewrite pipeline safety buffers rewritten Windows producer commands", (
194
198
  assert.equal(applyRewrittenCommandShellSafetyFixups("git diff | grep TODO"), "git diff | grep TODO");
195
199
  });
196
200
 
201
+ runTest("rewrite pipeline safety keeps RTK_DB_PATH scoped to rewritten producer commands", () => {
202
+ const rewritten = applyRewrittenCommandShellSafetyFixups(
203
+ applyRtkCommandEnvironment("rtk git diff agent/extensions/pi-multi-auth/account-manager.ts | head -200"),
204
+ );
205
+
206
+ if (process.platform === "win32") {
207
+ assert.ok(rewritten.startsWith("{"));
208
+ assert.equal(rewritten.startsWith("RTK_DB_PATH="), false);
209
+ assert.ok(rewritten.includes("RTK_DB_PATH="));
210
+ assert.ok(
211
+ rewritten.includes('rtk git diff agent/extensions/pi-multi-auth/account-manager.ts > "$__pi_rtk_pipe_tmp"'),
212
+ );
213
+ assert.ok(rewritten.includes('(head -200) < "$__pi_rtk_pipe_tmp"'));
214
+ } else {
215
+ assert.ok(rewritten.startsWith("RTK_DB_PATH="));
216
+ assert.equal(
217
+ rewritten,
218
+ applyRtkCommandEnvironment("rtk git diff agent/extensions/pi-multi-auth/account-manager.ts | head -200"),
219
+ );
220
+ }
221
+ });
222
+
223
+ runTest("stripRtkHookWarnings handles bare, prefixed, and already-sanitized hook notices", () => {
224
+ assert.equal(
225
+ stripRtkHookWarnings("No hook installed — run `rtk init -g` for automatic token savings\n\nready\n", null),
226
+ "ready\n",
227
+ );
228
+ assert.equal(
229
+ stripRtkHookWarnings("[WARN] Hook outdated — run `rtk init -g` to update\n\nready\n", null),
230
+ "ready\n",
231
+ );
232
+ assert.equal(
233
+ stripRtkHookWarnings(
234
+ "?? bun.lock[rtk] /!\\ No hook installed — run `rtk init -g` for automatic token savings\n",
235
+ null,
236
+ ),
237
+ "?? bun.lock\n",
238
+ );
239
+ assert.equal(
240
+ stripRtkHookWarnings("[rtk] /!\\ No hook installed — run `rtk init -g` for automatic token savings\n\n", "rtk git status"),
241
+ "",
242
+ );
243
+ });
244
+
245
+ runTest("stripRtkHookWarnings leaves quoted hook text untouched", () => {
246
+ const quoted = 'const warning = "No hook installed — run `rtk init -g` for automatic token savings";\n';
247
+ assert.equal(stripRtkHookWarnings(quoted, null), null);
248
+ });
249
+
250
+ runTest("sanitizeRtkEmojiOutput normalizes RTK-shaped warning output without removing content", () => {
251
+ const sanitized = sanitizeRtkEmojiOutput(
252
+ "⚠️ Warning: --hook-only only makes sense with --global\n For local projects, use default mode or --claude-md\n",
253
+ "rtk init --hook-only",
254
+ );
255
+ assert.equal(
256
+ sanitized,
257
+ "[WARN] Warning: --hook-only only makes sense with --global\n For local projects, use default mode or --claude-md\n",
258
+ );
259
+ });
260
+
261
+ runTest("streaming sanitizer strips hook notices, sanitizes emoji output, and preserves non-text blocks", () => {
262
+ const hookNoticeResult = {
263
+ content: [
264
+ {
265
+ type: "text",
266
+ text: "[rtk] /!\\ No hook installed — run `rtk init -g` for automatic token savings\n\nworking tree clean\n",
267
+ },
268
+ ],
269
+ };
270
+ assert.equal(sanitizeStreamingBashExecutionResult(hookNoticeResult, "rtk git status"), true);
271
+ assert.equal(
272
+ (hookNoticeResult.content[0] as { text: string }).text,
273
+ "working tree clean\n",
274
+ );
275
+
276
+ const emojiResult = {
277
+ content: [
278
+ { type: "text", text: "📄 src/file.ts\n✅ Files are identical\n" },
279
+ { type: "image", url: "ignored" },
280
+ ],
281
+ };
282
+ assert.equal(sanitizeStreamingBashExecutionResult(emojiResult, "rtk git diff -- src/file.ts"), true);
283
+ assert.equal((emojiResult.content[0] as { text: string }).text, "> src/file.ts\n[OK] Files are identical\n");
284
+ assert.deepEqual(emojiResult.content[1], { type: "image", url: "ignored" });
285
+
286
+ const parseWarningResult = {
287
+ content: [
288
+ {
289
+ type: "text",
290
+ text: "[rtk] warning: builtin filters: parse failure\n\nworking tree clean\n",
291
+ },
292
+ ],
293
+ };
294
+ assert.equal(sanitizeStreamingBashExecutionResult(parseWarningResult, "rtk git status"), false);
295
+ assert.equal(
296
+ (parseWarningResult.content[0] as { text: string }).text,
297
+ "[rtk] warning: builtin filters: parse failure\n\nworking tree clean\n",
298
+ );
299
+ });
300
+
197
301
  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
- event.input.command = applyRewrittenCommandShellSafetyFixups(decision.rewrittenCommand);
366
+ const envScopedRewrittenCommand = applyRtkCommandEnvironment(decision.rewrittenCommand);
367
+ event.input.command = applyRewrittenCommandShellSafetyFixups(envScopedRewrittenCommand);
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.");
@@ -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) {
@@ -1,3 +1,5 @@
1
+ import { splitLeadingEnvAssignments } from "./shell-env-prefix.js";
2
+
1
3
  interface ParsedPipeline {
2
4
  segments: string[];
3
5
  separators: string[];
@@ -88,18 +90,19 @@ function parseSimpleTopLevelPipeline(command: string): ParsedPipeline | null {
88
90
 
89
91
  function extractProducerRewritePlan(segment: string, firstSeparator: string): ProducerRewritePlan | null {
90
92
  const trimmed = segment.trim();
91
- if (!/^rtk\s+/i.test(trimmed)) {
93
+ const { envPrefix, command: commandWithOptionalRedirect } = splitLeadingEnvAssignments(trimmed);
94
+ if (!/^rtk\s+/i.test(commandWithOptionalRedirect)) {
92
95
  return null;
93
96
  }
94
97
 
95
- const stderrMergeMatch = trimmed.match(/^(.*?)(?:\s+)?2>\s*&1\s*$/u);
98
+ const stderrMergeMatch = commandWithOptionalRedirect.match(/^(.*?)(?:\s+)?2>\s*&1\s*$/u);
96
99
  if (stderrMergeMatch) {
97
100
  const command = stderrMergeMatch[1]?.trimEnd() ?? "";
98
- return command ? { command, captureStderr: true } : null;
101
+ return command ? { command: `${envPrefix}${command}`.trim(), captureStderr: true } : null;
99
102
  }
100
103
 
101
104
  return {
102
- command: trimmed,
105
+ command: `${envPrefix}${commandWithOptionalRedirect}`.trim(),
103
106
  captureStderr: firstSeparator === "|&",
104
107
  };
105
108
  }
@@ -0,0 +1,64 @@
1
+ import { join } from "node:path";
2
+
3
+ import { splitLeadingEnvAssignments } from "./shell-env-prefix.js";
4
+
5
+ const RTK_DB_PATH_ENV_NAME = "RTK_DB_PATH";
6
+ const RTK_DB_PATH_ASSIGNMENT_PATTERN = /(?:^|\s)RTK_DB_PATH=(?:"[^"]*"|'[^']*'|[^\s]+)(?=\s|$)/;
7
+
8
+ function resolveTemporaryDirectory(): string {
9
+ if (process.platform === "win32") {
10
+ const windowsTempDir = process.env.TEMP ?? process.env.TMP;
11
+ if (windowsTempDir && windowsTempDir.trim()) {
12
+ return windowsTempDir;
13
+ }
14
+
15
+ const localAppData = process.env.LOCALAPPDATA;
16
+ if (localAppData && localAppData.trim()) {
17
+ return join(localAppData, "Temp");
18
+ }
19
+
20
+ const userProfile = process.env.USERPROFILE;
21
+ if (userProfile && userProfile.trim()) {
22
+ return join(userProfile, "AppData", "Local", "Temp");
23
+ }
24
+
25
+ const systemRoot = process.env.SystemRoot ?? process.env.WINDIR;
26
+ if (systemRoot && systemRoot.trim()) {
27
+ return join(systemRoot, "Temp");
28
+ }
29
+
30
+ return "C:/Windows/Temp";
31
+ }
32
+
33
+ const posixTempDir = process.env.TMPDIR ?? process.env.TMP;
34
+ if (posixTempDir && posixTempDir.trim()) {
35
+ return posixTempDir;
36
+ }
37
+
38
+ return "/tmp";
39
+ }
40
+
41
+ function getTemporaryRtkHistoryDbPath(): string {
42
+ return join(resolveTemporaryDirectory(), "pi-rtk-optimizer", "history.db");
43
+ }
44
+
45
+ function quoteForShellEnv(value: string): string {
46
+ const normalizedValue = process.platform === "win32" ? value.replace(/\\/g, "/") : value;
47
+ return `"${normalizedValue.replace(/"/g, '\\"')}"`;
48
+ }
49
+
50
+ function hasLeadingRtkDbPathAssignment(command: string): boolean {
51
+ return RTK_DB_PATH_ASSIGNMENT_PATTERN.test(splitLeadingEnvAssignments(command).envPrefix);
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,14 @@
1
+ const LEADING_ENV_ASSIGNMENT_PATTERN = /^((?:[A-Za-z_][A-Za-z0-9_]*=(?:"[^"]*"|'[^']*'|[^\s]+)\s+)*)/;
2
+
3
+ export interface LeadingEnvAssignmentSplit {
4
+ envPrefix: string;
5
+ command: string;
6
+ }
7
+
8
+ export function splitLeadingEnvAssignments(input: string): LeadingEnvAssignmentSplit {
9
+ const envPrefix = input.match(LEADING_ENV_ASSIGNMENT_PATTERN)?.[1] ?? "";
10
+ return {
11
+ envPrefix,
12
+ command: input.slice(envPrefix.length),
13
+ };
14
+ }
@@ -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
+ }
@@ -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);
@@ -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
+ }
@@ -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" {