pi-rtk-optimizer 0.8.1 → 0.8.3
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 +14 -0
- package/README.md +2 -2
- package/package.json +67 -67
- package/src/additional-coverage-test.ts +25 -5
- package/src/command-register.ts +31 -0
- package/src/command-rewriter-test.ts +39 -0
- package/src/command-rewriter.ts +3 -1
- package/src/config-modal-test.ts +32 -29
- package/src/config-modal.ts +19 -13
- package/src/config-store.ts +2 -2
- package/src/index-test.ts +29 -26
- package/src/index.ts +10 -2
- package/src/output-compactor-test.ts +6 -5
- package/src/rtk-rewrite-provider.ts +111 -2
- package/src/test-helpers.ts +35 -0
- package/src/tool-execution-sanitizer.ts +82 -80
- package/src/types-shims.d.ts +6 -0
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,20 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.8.3] - 2026-06-16
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
- Added a runtime-agnostic `mock.module` shim so tests using `node:test` module mocking also pass under Bun's `bun:test` compatibility layer.
|
|
14
|
+
- Deep-cloned fallback default config objects in `config-store.ts` to prevent caller mutations from leaking into subsequent config loads.
|
|
15
|
+
|
|
16
|
+
## [0.8.2] - 2026-06-01
|
|
17
|
+
|
|
18
|
+
### Changed
|
|
19
|
+
- Deferred output compactor and configuration modal loading during extension bootstrap.
|
|
20
|
+
- Replaced technique barrel imports with direct module imports.
|
|
21
|
+
- Migrated inline test entrypoints from Bun to Node/tsx.
|
|
22
|
+
- Widened Pi peer dependency ranges to include `^0.77.0 || ^0.78.0`.
|
|
23
|
+
|
|
10
24
|
## [0.8.1] - 2026-05-26
|
|
11
25
|
|
|
12
26
|
### Changed
|
package/README.md
CHANGED
|
@@ -257,7 +257,7 @@ Automatic fixes applied on Windows:
|
|
|
257
257
|
|
|
258
258
|
- **Peer dependencies:** `@earendil-works/pi-coding-agent`, `@earendil-works/pi-tui`
|
|
259
259
|
- **Runtime:** Node.js ≥20, optional `rtk` binary for command rewriting
|
|
260
|
-
- **Development verification:** Node.js ≥
|
|
260
|
+
- **Development verification:** Node.js ≥24 and npm for Node/tsx test scripts using Node's experimental test module mocks
|
|
261
261
|
|
|
262
262
|
## Development
|
|
263
263
|
|
|
@@ -268,7 +268,7 @@ npm run build
|
|
|
268
268
|
# Full typecheck
|
|
269
269
|
npm run typecheck
|
|
270
270
|
|
|
271
|
-
# Run
|
|
271
|
+
# Run Node/tsx tests
|
|
272
272
|
npm run test
|
|
273
273
|
|
|
274
274
|
# Full verification
|
package/package.json
CHANGED
|
@@ -1,67 +1,67 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "pi-rtk-optimizer",
|
|
3
|
-
"version": "0.8.
|
|
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": "tsc -p tsconfig.json --noCheck",
|
|
20
|
-
"typecheck": "tsc -p tsconfig.json",
|
|
21
|
-
"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",
|
|
22
|
-
"check": "npm run typecheck && npm run test && npm run build:check",
|
|
23
|
-
"build:check": "esbuild ./index.ts --bundle --platform=node --format=esm --outfile=./.pi-rtk-optimizer-check.mjs --external:@mariozechner/pi-coding-agent --external:@mariozechner/pi-tui && node -e \"import { unlinkSync } from 'node:fs'; unlinkSync('./.pi-rtk-optimizer-check.mjs');\""
|
|
24
|
-
},
|
|
25
|
-
"keywords": [
|
|
26
|
-
"pi-package",
|
|
27
|
-
"pi",
|
|
28
|
-
"pi-extension",
|
|
29
|
-
"pi-coding-agent",
|
|
30
|
-
"coding-agent",
|
|
31
|
-
"rtk",
|
|
32
|
-
"token-optimization",
|
|
33
|
-
"tool-compaction",
|
|
34
|
-
"output-compaction",
|
|
35
|
-
"command-rewrite",
|
|
36
|
-
"optimization"
|
|
37
|
-
],
|
|
38
|
-
"author": "MasuRii",
|
|
39
|
-
"license": "MIT",
|
|
40
|
-
"repository": {
|
|
41
|
-
"type": "git",
|
|
42
|
-
"url": "git+https://github.com/MasuRii/pi-rtk-optimizer.git"
|
|
43
|
-
},
|
|
44
|
-
"bugs": {
|
|
45
|
-
"url": "https://github.com/MasuRii/pi-rtk-optimizer/issues"
|
|
46
|
-
},
|
|
47
|
-
"homepage": "https://github.com/MasuRii/pi-rtk-optimizer#readme",
|
|
48
|
-
"engines": {
|
|
49
|
-
"node": ">=20"
|
|
50
|
-
},
|
|
51
|
-
"publishConfig": {
|
|
52
|
-
"access": "public"
|
|
53
|
-
},
|
|
54
|
-
"devDependencies": {
|
|
55
|
-
"esbuild": "0.28.
|
|
56
|
-
"typescript": "6.0.3"
|
|
57
|
-
},
|
|
58
|
-
"pi": {
|
|
59
|
-
"extensions": [
|
|
60
|
-
"./index.ts"
|
|
61
|
-
]
|
|
62
|
-
},
|
|
63
|
-
"peerDependencies": {
|
|
64
|
-
"@earendil-works/pi-coding-agent": "^0.74.0 || ^0.75.0",
|
|
65
|
-
"@earendil-works/pi-tui": "^0.74.0 || ^0.75.0"
|
|
66
|
-
}
|
|
67
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "pi-rtk-optimizer",
|
|
3
|
+
"version": "0.8.3",
|
|
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": "tsc -p tsconfig.json --noCheck",
|
|
20
|
+
"typecheck": "tsc -p tsconfig.json",
|
|
21
|
+
"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",
|
|
22
|
+
"check": "npm run typecheck && npm run test && npm run build:check",
|
|
23
|
+
"build:check": "esbuild ./index.ts --bundle --platform=node --format=esm --outfile=./.pi-rtk-optimizer-check.mjs --external:@mariozechner/pi-coding-agent --external:@mariozechner/pi-tui && node -e \"import { unlinkSync } from 'node:fs'; unlinkSync('./.pi-rtk-optimizer-check.mjs');\""
|
|
24
|
+
},
|
|
25
|
+
"keywords": [
|
|
26
|
+
"pi-package",
|
|
27
|
+
"pi",
|
|
28
|
+
"pi-extension",
|
|
29
|
+
"pi-coding-agent",
|
|
30
|
+
"coding-agent",
|
|
31
|
+
"rtk",
|
|
32
|
+
"token-optimization",
|
|
33
|
+
"tool-compaction",
|
|
34
|
+
"output-compaction",
|
|
35
|
+
"command-rewrite",
|
|
36
|
+
"optimization"
|
|
37
|
+
],
|
|
38
|
+
"author": "MasuRii",
|
|
39
|
+
"license": "MIT",
|
|
40
|
+
"repository": {
|
|
41
|
+
"type": "git",
|
|
42
|
+
"url": "git+https://github.com/MasuRii/pi-rtk-optimizer.git"
|
|
43
|
+
},
|
|
44
|
+
"bugs": {
|
|
45
|
+
"url": "https://github.com/MasuRii/pi-rtk-optimizer/issues"
|
|
46
|
+
},
|
|
47
|
+
"homepage": "https://github.com/MasuRii/pi-rtk-optimizer#readme",
|
|
48
|
+
"engines": {
|
|
49
|
+
"node": ">=20"
|
|
50
|
+
},
|
|
51
|
+
"publishConfig": {
|
|
52
|
+
"access": "public"
|
|
53
|
+
},
|
|
54
|
+
"devDependencies": {
|
|
55
|
+
"esbuild": "0.28.1",
|
|
56
|
+
"typescript": "6.0.3"
|
|
57
|
+
},
|
|
58
|
+
"pi": {
|
|
59
|
+
"extensions": [
|
|
60
|
+
"./index.ts"
|
|
61
|
+
]
|
|
62
|
+
},
|
|
63
|
+
"peerDependencies": {
|
|
64
|
+
"@earendil-works/pi-coding-agent": "^0.74.0 || ^0.75.0 || ^0.78.0 || ^0.79.0",
|
|
65
|
+
"@earendil-works/pi-tui": "^0.74.0 || ^0.75.0 || ^0.78.0 || ^0.79.0"
|
|
66
|
+
}
|
|
67
|
+
}
|
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
import assert from "node:assert/strict";
|
|
2
2
|
import { existsSync, readFileSync, unlinkSync, writeFileSync } from "node:fs";
|
|
3
|
-
import { mock } from "bun:test";
|
|
4
3
|
|
|
5
4
|
import { clearOutputMetrics, getOutputMetricsSummary, trackOutputSavings } from "./output-metrics.ts";
|
|
6
|
-
import { runTest } from "./test-helpers.ts";
|
|
5
|
+
import { mock, runTest } from "./test-helpers.ts";
|
|
7
6
|
import { matchesCommandPatterns, normalizeCommandForDetection } from "./techniques/command-detection.ts";
|
|
8
7
|
import { compactPath } from "./techniques/path-utils.ts";
|
|
9
8
|
import { applyWindowsBashCompatibilityFixes } from "./windows-command-helpers.ts";
|
|
@@ -13,9 +12,11 @@ import { sanitizeStreamingBashExecutionResult } from "./tool-execution-sanitizer
|
|
|
13
12
|
import { sanitizeRtkEmojiOutput } from "./techniques/emoji.ts";
|
|
14
13
|
import { stripRtkHookWarnings } from "./techniques/rtk.ts";
|
|
15
14
|
|
|
16
|
-
mock.module("@earendil-works/pi-coding-agent",
|
|
17
|
-
|
|
18
|
-
|
|
15
|
+
mock.module("@earendil-works/pi-coding-agent", {
|
|
16
|
+
namedExports: {
|
|
17
|
+
getAgentDir: () => "/tmp/.pi/agent",
|
|
18
|
+
},
|
|
19
|
+
});
|
|
19
20
|
|
|
20
21
|
const {
|
|
21
22
|
ensureConfigExists,
|
|
@@ -141,6 +142,25 @@ runTest("config-store falls back to defaults when JSON is invalid", () => {
|
|
|
141
142
|
}
|
|
142
143
|
});
|
|
143
144
|
|
|
145
|
+
runTest("config-store malformed-file defaults are isolated from caller mutation", () => {
|
|
146
|
+
const tempPath = makeTempConfigPath();
|
|
147
|
+
cleanupFile(tempPath);
|
|
148
|
+
|
|
149
|
+
try {
|
|
150
|
+
writeFileSync(tempPath, "{not valid json", "utf-8");
|
|
151
|
+
const firstLoad = loadRtkIntegrationConfig(tempPath);
|
|
152
|
+
firstLoad.config.outputCompaction.truncate.maxChars = 42_424;
|
|
153
|
+
firstLoad.config.outputCompaction.readCompaction.enabled = true;
|
|
154
|
+
|
|
155
|
+
const secondLoad = loadRtkIntegrationConfig(tempPath);
|
|
156
|
+
|
|
157
|
+
assert.equal(secondLoad.config.outputCompaction.truncate.maxChars, 12_000);
|
|
158
|
+
assert.equal(secondLoad.config.outputCompaction.readCompaction.enabled, false);
|
|
159
|
+
} finally {
|
|
160
|
+
cleanupFile(tempPath);
|
|
161
|
+
}
|
|
162
|
+
});
|
|
163
|
+
|
|
144
164
|
runTest("output metrics summarize tracked savings and clear state", () => {
|
|
145
165
|
clearOutputMetrics();
|
|
146
166
|
assert.equal(getOutputMetricsSummary(), "RTK output compaction metrics: no data yet.");
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { ExtensionAPI, ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { getRtkArgumentCompletions } from "./command-completions.js";
|
|
3
|
+
import type { RtkIntegrationConfig, RuntimeStatus } from "./types.js";
|
|
4
|
+
|
|
5
|
+
export interface RtkIntegrationController {
|
|
6
|
+
getConfig(): RtkIntegrationConfig;
|
|
7
|
+
setConfig(next: RtkIntegrationConfig, ctx: ExtensionCommandContext): void;
|
|
8
|
+
getConfigPath(): string;
|
|
9
|
+
getRuntimeStatus(): RuntimeStatus;
|
|
10
|
+
refreshRuntimeStatus(): Promise<RuntimeStatus>;
|
|
11
|
+
getMetricsSummary(): string;
|
|
12
|
+
clearMetrics(): void;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
let commandModalModulePromise: Promise<typeof import("./config-modal.js")> | undefined;
|
|
16
|
+
|
|
17
|
+
function loadCommandModalModule(): Promise<typeof import("./config-modal.js")> {
|
|
18
|
+
commandModalModulePromise ??= import("./config-modal.js");
|
|
19
|
+
return commandModalModulePromise;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function registerRtkIntegrationCommand(pi: ExtensionAPI, controller: RtkIntegrationController): void {
|
|
23
|
+
pi.registerCommand("rtk", {
|
|
24
|
+
description: "Configure RTK rewrite and output compaction integration",
|
|
25
|
+
getArgumentCompletions: getRtkArgumentCompletions,
|
|
26
|
+
handler: async (args, ctx) => {
|
|
27
|
+
const { handleRtkIntegrationCommand } = await loadCommandModalModule();
|
|
28
|
+
await handleRtkIntegrationCommand(args, ctx, controller);
|
|
29
|
+
},
|
|
30
|
+
});
|
|
31
|
+
}
|
|
@@ -89,6 +89,26 @@ await runTest("already rtk unchanged", async () => {
|
|
|
89
89
|
assert.equal(decision.reason, "already_rtk");
|
|
90
90
|
});
|
|
91
91
|
|
|
92
|
+
await runTest("env-prefixed rtk command is treated as already RTK and never re-rewritten", async () => {
|
|
93
|
+
let execCallCount = 0;
|
|
94
|
+
const pi = {
|
|
95
|
+
exec: async () => {
|
|
96
|
+
execCallCount += 1;
|
|
97
|
+
return { code: 0, stdout: "rtk rtk status", stderr: "" };
|
|
98
|
+
},
|
|
99
|
+
} as unknown as ExtensionAPI;
|
|
100
|
+
|
|
101
|
+
const command = "CI=1 RTK_DB_PATH=/tmp/history.db rtk status";
|
|
102
|
+
const decision = await computeRewriteDecision(command, cloneDefaultConfig(), pi, {
|
|
103
|
+
executableResolution: { command: "rtk", resolver: "which" },
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
assert.equal(decision.changed, false);
|
|
107
|
+
assert.equal(decision.rewrittenCommand, command);
|
|
108
|
+
assert.equal(decision.reason, "already_rtk");
|
|
109
|
+
assert.equal(execCallCount, 0);
|
|
110
|
+
});
|
|
111
|
+
|
|
92
112
|
await runTest("rtk unsupported heredoc result leaves command unchanged", async () => {
|
|
93
113
|
const config = cloneDefaultConfig();
|
|
94
114
|
const decision = await computeRewriteDecision("cat <<EOF", config, createMockPi({ code: 1 }));
|
|
@@ -109,6 +129,25 @@ await runTest("quoted heredoc marker is delegated to RTK rewrite", async () => {
|
|
|
109
129
|
assert.equal(decision.reason, "ok");
|
|
110
130
|
});
|
|
111
131
|
|
|
132
|
+
await runTest("rg rewrite keeps the RTK ripgrep proxy instead of the grep proxy", async () => {
|
|
133
|
+
const config = cloneDefaultConfig();
|
|
134
|
+
const command = "cd /workspace && rg -n --glob '!node_modules/**' --glob '!dist/**' \"needle\" src";
|
|
135
|
+
const decision = await computeRewriteDecision(
|
|
136
|
+
command,
|
|
137
|
+
config,
|
|
138
|
+
createMockPi({
|
|
139
|
+
code: 3,
|
|
140
|
+
stdout: "cd /workspace && rtk grep -n --glob '!node_modules/**' --glob '!dist/**' \"needle\" src",
|
|
141
|
+
}),
|
|
142
|
+
);
|
|
143
|
+
assert.equal(decision.changed, true);
|
|
144
|
+
assert.equal(
|
|
145
|
+
decision.rewrittenCommand,
|
|
146
|
+
"cd /workspace && rtk rg -n --glob '!node_modules/**' --glob '!dist/**' \"needle\" src",
|
|
147
|
+
);
|
|
148
|
+
assert.equal(decision.reason, "ok");
|
|
149
|
+
});
|
|
150
|
+
|
|
112
151
|
await runTest("legacy category toggles do not pre-filter RTK rewrite source of truth", async () => {
|
|
113
152
|
const config = { ...cloneDefaultConfig(), rewriteGitGithub: false };
|
|
114
153
|
const decision = await computeRewriteDecision("git status", config, createMockPi({ code: 3, stdout: "rtk git status" }));
|
package/src/command-rewriter.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { resolveRtkRewrite, type RtkRewriteProviderOptions } from "./rtk-rewrite-provider.js";
|
|
2
|
+
import { splitLeadingEnvAssignments } from "./shell-env-prefix.js";
|
|
2
3
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
3
4
|
import type { RtkIntegrationConfig } from "./types.js";
|
|
4
5
|
|
|
@@ -21,7 +22,8 @@ export async function computeRewriteDecision(
|
|
|
21
22
|
}
|
|
22
23
|
|
|
23
24
|
const trimmedStart = command.trimStart();
|
|
24
|
-
|
|
25
|
+
const effectiveCommand = splitLeadingEnvAssignments(trimmedStart).command.trimStart();
|
|
26
|
+
if (effectiveCommand === "rtk" || effectiveCommand.startsWith("rtk ")) {
|
|
25
27
|
return { changed: false, originalCommand: command, rewrittenCommand: command, reason: "already_rtk" };
|
|
26
28
|
}
|
|
27
29
|
|
package/src/config-modal-test.ts
CHANGED
|
@@ -1,40 +1,43 @@
|
|
|
1
1
|
import assert from "node:assert/strict";
|
|
2
|
-
import { mock } from "bun:test";
|
|
3
2
|
|
|
4
|
-
import { cloneDefaultConfig, runTest } from "./test-helpers.ts";
|
|
3
|
+
import { cloneDefaultConfig, mock, runTest } from "./test-helpers.ts";
|
|
5
4
|
|
|
6
|
-
mock.module("@earendil-works/pi-coding-agent",
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
})
|
|
5
|
+
mock.module("@earendil-works/pi-coding-agent", {
|
|
6
|
+
namedExports: {
|
|
7
|
+
getAgentDir: () => "/tmp/.pi/agent",
|
|
8
|
+
getSettingsListTheme: () => ({}),
|
|
9
|
+
},
|
|
10
|
+
});
|
|
10
11
|
|
|
11
12
|
const settingsListInputs: string[] = [];
|
|
12
13
|
const settingsListUpdates: Array<{ id: string; value: string }> = [];
|
|
13
14
|
|
|
14
|
-
mock.module("@earendil-works/pi-tui",
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
15
|
+
mock.module("@earendil-works/pi-tui", {
|
|
16
|
+
namedExports: {
|
|
17
|
+
Box: class {
|
|
18
|
+
addChild(): void {}
|
|
19
|
+
},
|
|
20
|
+
Container: class {
|
|
21
|
+
addChild(): void {}
|
|
22
|
+
render(): string[] {
|
|
23
|
+
return ["settings-content"];
|
|
24
|
+
}
|
|
25
|
+
invalidate(): void {}
|
|
26
|
+
},
|
|
27
|
+
SettingsList: class {
|
|
28
|
+
handleInput(data: string): void {
|
|
29
|
+
settingsListInputs.push(data);
|
|
30
|
+
}
|
|
31
|
+
updateValue(id: string, value: string): void {
|
|
32
|
+
settingsListUpdates.push({ id, value });
|
|
33
|
+
}
|
|
34
|
+
},
|
|
35
|
+
Spacer: class {},
|
|
36
|
+
Text: class {},
|
|
37
|
+
truncateToWidth: (text: string, width: number) => text.slice(0, width),
|
|
38
|
+
visibleWidth: (text: string) => text.length,
|
|
32
39
|
},
|
|
33
|
-
|
|
34
|
-
Text: class {},
|
|
35
|
-
truncateToWidth: (text: string, width: number) => text.slice(0, width),
|
|
36
|
-
visibleWidth: (text: string) => text.length,
|
|
37
|
-
}));
|
|
40
|
+
});
|
|
38
41
|
|
|
39
42
|
function stripAnsi(text: string): string {
|
|
40
43
|
return text.replace(/\x1b\[[0-9;]*m/g, "");
|
package/src/config-modal.ts
CHANGED
|
@@ -10,7 +10,7 @@ import {
|
|
|
10
10
|
type RuntimeStatus,
|
|
11
11
|
} from "./types.js";
|
|
12
12
|
|
|
13
|
-
interface RtkIntegrationController {
|
|
13
|
+
export interface RtkIntegrationController {
|
|
14
14
|
getConfig(): RtkIntegrationConfig;
|
|
15
15
|
setConfig(next: RtkIntegrationConfig, ctx: ExtensionCommandContext): void;
|
|
16
16
|
getConfigPath(): string;
|
|
@@ -584,21 +584,27 @@ async function handleArgs(
|
|
|
584
584
|
return true;
|
|
585
585
|
}
|
|
586
586
|
|
|
587
|
+
export async function handleRtkIntegrationCommand(
|
|
588
|
+
args: string,
|
|
589
|
+
ctx: ExtensionCommandContext,
|
|
590
|
+
controller: RtkIntegrationController,
|
|
591
|
+
): Promise<void> {
|
|
592
|
+
if (await handleArgs(args, ctx, controller)) {
|
|
593
|
+
return;
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
if (!ctx.hasUI) {
|
|
597
|
+
ctx.ui.notify("/rtk requires interactive TUI mode.", "warning");
|
|
598
|
+
return;
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
await openSettingsModal(ctx, controller);
|
|
602
|
+
}
|
|
603
|
+
|
|
587
604
|
export function registerRtkIntegrationCommand(pi: ExtensionAPI, controller: RtkIntegrationController): void {
|
|
588
605
|
pi.registerCommand("rtk", {
|
|
589
606
|
description: "Configure RTK rewrite and output compaction integration",
|
|
590
607
|
getArgumentCompletions: getRtkArgumentCompletions,
|
|
591
|
-
handler:
|
|
592
|
-
if (await handleArgs(args, ctx, controller)) {
|
|
593
|
-
return;
|
|
594
|
-
}
|
|
595
|
-
|
|
596
|
-
if (!ctx.hasUI) {
|
|
597
|
-
ctx.ui.notify("/rtk requires interactive TUI mode.", "warning");
|
|
598
|
-
return;
|
|
599
|
-
}
|
|
600
|
-
|
|
601
|
-
await openSettingsModal(ctx, controller);
|
|
602
|
-
},
|
|
608
|
+
handler: (args, ctx) => handleRtkIntegrationCommand(args, ctx, controller),
|
|
603
609
|
});
|
|
604
610
|
}
|
package/src/config-store.ts
CHANGED
|
@@ -177,7 +177,7 @@ export function ensureConfigExists(configPath = CONFIG_PATH): EnsureConfigResult
|
|
|
177
177
|
|
|
178
178
|
export function loadRtkIntegrationConfig(configPath = CONFIG_PATH): ConfigLoadResult {
|
|
179
179
|
if (!existsSync(configPath)) {
|
|
180
|
-
return { config:
|
|
180
|
+
return { config: structuredClone(DEFAULT_RTK_INTEGRATION_CONFIG) };
|
|
181
181
|
}
|
|
182
182
|
|
|
183
183
|
try {
|
|
@@ -187,7 +187,7 @@ export function loadRtkIntegrationConfig(configPath = CONFIG_PATH): ConfigLoadRe
|
|
|
187
187
|
} catch (error) {
|
|
188
188
|
const message = error instanceof Error ? error.message : String(error);
|
|
189
189
|
return {
|
|
190
|
-
config:
|
|
190
|
+
config: structuredClone(DEFAULT_RTK_INTEGRATION_CONFIG),
|
|
191
191
|
warning: `Failed to parse ${configPath}: ${message}`,
|
|
192
192
|
};
|
|
193
193
|
}
|
package/src/index-test.ts
CHANGED
|
@@ -1,32 +1,35 @@
|
|
|
1
1
|
import assert from "node:assert/strict";
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
}));
|
|
11
|
-
|
|
12
|
-
mock.module("@earendil-works/pi-tui", () => ({
|
|
13
|
-
Box: class {},
|
|
14
|
-
Container: class {
|
|
15
|
-
addChild(): void {}
|
|
16
|
-
render(): string[] {
|
|
17
|
-
return [];
|
|
18
|
-
}
|
|
19
|
-
invalidate(): void {}
|
|
2
|
+
|
|
3
|
+
import { mock, runTest } from "./test-helpers.ts";
|
|
4
|
+
|
|
5
|
+
mock.module("@earendil-works/pi-coding-agent", {
|
|
6
|
+
namedExports: {
|
|
7
|
+
getAgentDir: () => "/tmp/.pi/agent",
|
|
8
|
+
getSettingsListTheme: () => ({}),
|
|
9
|
+
isToolCallEventType: (toolName: string, event: Record<string, unknown>) => event.toolName === toolName,
|
|
20
10
|
},
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
mock.module("@earendil-works/pi-tui", {
|
|
14
|
+
namedExports: {
|
|
15
|
+
Box: class {},
|
|
16
|
+
Container: class {
|
|
17
|
+
addChild(): void {}
|
|
18
|
+
render(): string[] {
|
|
19
|
+
return [];
|
|
20
|
+
}
|
|
21
|
+
invalidate(): void {}
|
|
22
|
+
},
|
|
23
|
+
SettingsList: class {
|
|
24
|
+
handleInput(): void {}
|
|
25
|
+
updateValue(): void {}
|
|
26
|
+
},
|
|
27
|
+
Spacer: class {},
|
|
28
|
+
Text: class {},
|
|
29
|
+
truncateToWidth: (text: string) => text,
|
|
30
|
+
visibleWidth: (text: string) => text.length,
|
|
24
31
|
},
|
|
25
|
-
|
|
26
|
-
Text: class {},
|
|
27
|
-
truncateToWidth: (text: string) => text,
|
|
28
|
-
visibleWidth: (text: string) => text.length,
|
|
29
|
-
}));
|
|
32
|
+
});
|
|
30
33
|
|
|
31
34
|
const indexModule = await import("./index.ts");
|
|
32
35
|
const { createBoundedNoticeTracker, shouldInjectSourceFilterTroubleshootingNote } = indexModule;
|
package/src/index.ts
CHANGED
|
@@ -7,10 +7,10 @@ import {
|
|
|
7
7
|
saveRtkIntegrationConfig,
|
|
8
8
|
} from "./config-store.js";
|
|
9
9
|
import { computeRewriteDecision } from "./command-rewriter.js";
|
|
10
|
-
import { registerRtkIntegrationCommand } from "./
|
|
10
|
+
import { registerRtkIntegrationCommand } from "./command-register.js";
|
|
11
11
|
import { EXTENSION_NAME } from "./constants.js";
|
|
12
12
|
import { clearOutputMetrics, getOutputMetricsSummary } from "./output-metrics.js";
|
|
13
|
-
import {
|
|
13
|
+
import type { ToolResultCompactionMetadata } from "./output-compactor.js";
|
|
14
14
|
import { toRecord } from "./record-utils.js";
|
|
15
15
|
import { applyRtkCommandEnvironment } from "./rtk-command-environment.js";
|
|
16
16
|
import { resolveRtkExecutable, type RtkExecutableResolution } from "./rtk-executable-resolver.js";
|
|
@@ -31,6 +31,13 @@ function trimMessage(raw: string, maxLength = 220): string {
|
|
|
31
31
|
const SOURCE_FILTER_TROUBLESHOOTING_NOTE =
|
|
32
32
|
"RTK note: If file edits repeatedly fail because old text does not match, ask the user to manually run '/rtk' in the Pi TUI, disable 'Read compaction enabled', re-read the file, apply the edit, then ask the user to manually re-enable it in the Pi TUI.";
|
|
33
33
|
|
|
34
|
+
let outputCompactorModulePromise: Promise<typeof import("./output-compactor.js")> | undefined;
|
|
35
|
+
|
|
36
|
+
function loadOutputCompactorModule(): Promise<typeof import("./output-compactor.js")> {
|
|
37
|
+
outputCompactorModulePromise ??= import("./output-compactor.js");
|
|
38
|
+
return outputCompactorModulePromise;
|
|
39
|
+
}
|
|
40
|
+
|
|
34
41
|
export function shouldInjectSourceFilterTroubleshootingNote(config: RtkIntegrationConfig): boolean {
|
|
35
42
|
const compaction = config.outputCompaction;
|
|
36
43
|
return (
|
|
@@ -436,6 +443,7 @@ export default function rtkIntegrationExtension(pi: ExtensionAPI): void {
|
|
|
436
443
|
}
|
|
437
444
|
|
|
438
445
|
try {
|
|
446
|
+
const { compactToolResult } = await loadOutputCompactorModule();
|
|
439
447
|
const outcome = compactToolResult(
|
|
440
448
|
{
|
|
441
449
|
toolName: event.toolName,
|
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
import assert from "node:assert/strict";
|
|
2
2
|
import { join } from "node:path";
|
|
3
|
-
import { mock } from "bun:test";
|
|
4
3
|
|
|
5
|
-
import { cloneDefaultConfig, runTest } from "./test-helpers.ts";
|
|
4
|
+
import { cloneDefaultConfig, mock, runTest } from "./test-helpers.ts";
|
|
6
5
|
|
|
7
6
|
const TEST_AGENT_DIR = "/tmp/.pi/agent";
|
|
8
7
|
|
|
9
|
-
mock.module("@earendil-works/pi-coding-agent",
|
|
10
|
-
|
|
11
|
-
|
|
8
|
+
mock.module("@earendil-works/pi-coding-agent", {
|
|
9
|
+
namedExports: {
|
|
10
|
+
getAgentDir: () => TEST_AGENT_DIR,
|
|
11
|
+
},
|
|
12
|
+
});
|
|
12
13
|
|
|
13
14
|
const { compactToolResult } = await import("./output-compactor.ts");
|
|
14
15
|
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import { resolveRtkExecutable, type RtkExecutableResolution } from "./rtk-executable-resolver.js";
|
|
3
|
+
import { splitLeadingEnvAssignments } from "./shell-env-prefix.js";
|
|
3
4
|
|
|
4
5
|
export interface RtkRewriteProviderResult {
|
|
5
6
|
changed: boolean;
|
|
@@ -29,6 +30,113 @@ function normalizeOptions(optionsOrTimeout: number | RtkRewriteProviderOptions):
|
|
|
29
30
|
return optionsOrTimeout;
|
|
30
31
|
}
|
|
31
32
|
|
|
33
|
+
interface ShellSegmentSplit {
|
|
34
|
+
segments: string[];
|
|
35
|
+
separators: string[];
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function splitTopLevelShellSegments(command: string): ShellSegmentSplit {
|
|
39
|
+
const segments: string[] = [];
|
|
40
|
+
const separators: string[] = [];
|
|
41
|
+
let quote: '"' | "'" | "`" | null = null;
|
|
42
|
+
let escaped = false;
|
|
43
|
+
let segmentStart = 0;
|
|
44
|
+
|
|
45
|
+
for (let index = 0; index < command.length; index += 1) {
|
|
46
|
+
const character = command[index] ?? "";
|
|
47
|
+
const nextCharacter = command[index + 1] ?? "";
|
|
48
|
+
const previousCharacter = index > 0 ? (command[index - 1] ?? "") : "";
|
|
49
|
+
|
|
50
|
+
if (escaped) {
|
|
51
|
+
escaped = false;
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (quote !== null) {
|
|
56
|
+
if (character === "\\" && quote !== "'") {
|
|
57
|
+
escaped = true;
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
if (character === quote) {
|
|
61
|
+
quote = null;
|
|
62
|
+
}
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (character === "\\") {
|
|
67
|
+
escaped = true;
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (character === '"' || character === "'" || character === "`") {
|
|
72
|
+
quote = character;
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const twoCharacterOperator = `${character}${nextCharacter}`;
|
|
77
|
+
const separator =
|
|
78
|
+
twoCharacterOperator === "&&" || twoCharacterOperator === "||" || twoCharacterOperator === "|&"
|
|
79
|
+
? twoCharacterOperator
|
|
80
|
+
: character === ";" || (character === "|" && previousCharacter !== ">")
|
|
81
|
+
? character
|
|
82
|
+
: null;
|
|
83
|
+
|
|
84
|
+
if (separator === null) {
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
segments.push(command.slice(segmentStart, index));
|
|
89
|
+
separators.push(separator);
|
|
90
|
+
index += separator.length - 1;
|
|
91
|
+
segmentStart = index + 1;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
segments.push(command.slice(segmentStart));
|
|
95
|
+
return { segments, separators };
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function startsWithRipgrepCommand(segment: string): boolean {
|
|
99
|
+
const trimmed = segment.trimStart();
|
|
100
|
+
const effectiveCommand = splitLeadingEnvAssignments(trimmed).command.trimStart();
|
|
101
|
+
return /^rg(?=\s|$)/u.test(effectiveCommand);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function replaceRtkGrepProxyWithRtkRg(segment: string): string {
|
|
105
|
+
const leadingWhitespace = segment.match(/^\s*/u)?.[0] ?? "";
|
|
106
|
+
const withoutLeadingWhitespace = segment.slice(leadingWhitespace.length);
|
|
107
|
+
const { envPrefix, command } = splitLeadingEnvAssignments(withoutLeadingWhitespace);
|
|
108
|
+
const nextCommand = command.replace(/^(rtk)(\s+)grep(?=\s|$)/u, "$1$2rg");
|
|
109
|
+
return nextCommand === command ? segment : `${leadingWhitespace}${envPrefix}${nextCommand}`;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function normalizeRipgrepRewrite(originalCommand: string, rewrittenCommand: string): string {
|
|
113
|
+
const original = splitTopLevelShellSegments(originalCommand);
|
|
114
|
+
const rewritten = splitTopLevelShellSegments(rewrittenCommand);
|
|
115
|
+
if (original.segments.length !== rewritten.segments.length) {
|
|
116
|
+
return rewrittenCommand;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
let changed = false;
|
|
120
|
+
const rewrittenSegments = rewritten.segments.map((segment, index) => {
|
|
121
|
+
if (!startsWithRipgrepCommand(original.segments[index] ?? "")) {
|
|
122
|
+
return segment;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const nextSegment = replaceRtkGrepProxyWithRtkRg(segment);
|
|
126
|
+
changed ||= nextSegment !== segment;
|
|
127
|
+
return nextSegment;
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
if (!changed) {
|
|
131
|
+
return rewrittenCommand;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
return rewrittenSegments.reduce((accumulator, segment, index) => {
|
|
135
|
+
const separator = rewritten.separators[index - 1];
|
|
136
|
+
return separator === undefined ? segment : `${accumulator}${separator}${segment}`;
|
|
137
|
+
}, "");
|
|
138
|
+
}
|
|
139
|
+
|
|
32
140
|
export async function resolveRtkRewrite(
|
|
33
141
|
pi: ExtensionAPI,
|
|
34
142
|
command: string,
|
|
@@ -76,8 +184,8 @@ export async function resolveRtkRewrite(
|
|
|
76
184
|
}
|
|
77
185
|
|
|
78
186
|
if (result.code === 0 || result.code === 3) {
|
|
79
|
-
const
|
|
80
|
-
if (!
|
|
187
|
+
const rewrittenOutput = result.stdout?.trim();
|
|
188
|
+
if (!rewrittenOutput) {
|
|
81
189
|
return {
|
|
82
190
|
changed: false,
|
|
83
191
|
originalCommand: command,
|
|
@@ -87,6 +195,7 @@ export async function resolveRtkRewrite(
|
|
|
87
195
|
executableResolution,
|
|
88
196
|
};
|
|
89
197
|
}
|
|
198
|
+
const rewritten = normalizeRipgrepRewrite(command, rewrittenOutput);
|
|
90
199
|
if (rewritten === command) {
|
|
91
200
|
return {
|
|
92
201
|
changed: false,
|
package/src/test-helpers.ts
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
|
+
import { mock as nodeTestMockRaw } from "node:test";
|
|
2
|
+
|
|
1
3
|
import { DEFAULT_RTK_INTEGRATION_CONFIG, type RtkIntegrationConfig } from "./types.ts";
|
|
2
4
|
|
|
3
5
|
type TestResult = void | Promise<void>;
|
|
6
|
+
type MockModuleOptions = { namedExports?: Record<string, unknown>; defaultExport?: unknown };
|
|
4
7
|
|
|
5
8
|
function isPromiseLike(value: TestResult): value is Promise<void> {
|
|
6
9
|
return Boolean(value && typeof (value as Promise<void>).then === "function");
|
|
@@ -21,3 +24,35 @@ export function runTest(name: string, testFn: () => TestResult): TestResult {
|
|
|
21
24
|
export function cloneDefaultConfig(): RtkIntegrationConfig {
|
|
22
25
|
return structuredClone(DEFAULT_RTK_INTEGRATION_CONFIG);
|
|
23
26
|
}
|
|
27
|
+
|
|
28
|
+
// Runtime-agnostic module-mocking helper. The tests use the node:test-shaped
|
|
29
|
+
// API (`mock.module(specifier, { namedExports, defaultExport })`). Bun's
|
|
30
|
+
// implementation of `node:test` does not expose `mock.module`, but `bun:test`
|
|
31
|
+
// provides an equivalent that accepts a factory function. We detect the
|
|
32
|
+
// capability and adapt the options form to the factory form when needed.
|
|
33
|
+
const nodeTestMock = nodeTestMockRaw as unknown as { module?: unknown };
|
|
34
|
+
|
|
35
|
+
let mockModuleImpl: (specifier: string, options: MockModuleOptions) => void;
|
|
36
|
+
|
|
37
|
+
if (typeof nodeTestMock.module === "function") {
|
|
38
|
+
mockModuleImpl = nodeTestMock.module as (specifier: string, options: MockModuleOptions) => void;
|
|
39
|
+
} else {
|
|
40
|
+
const bunTest = (await import("bun:test")) as unknown as {
|
|
41
|
+
mock: { module: (specifier: string, factory: () => Record<string, unknown>) => void };
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
mockModuleImpl = (specifier, options) => {
|
|
45
|
+
bunTest.mock.module(specifier, () => {
|
|
46
|
+
const moduleExports: Record<string, unknown> = {};
|
|
47
|
+
if (options.defaultExport !== undefined) {
|
|
48
|
+
moduleExports.default = options.defaultExport;
|
|
49
|
+
}
|
|
50
|
+
if (options.namedExports) {
|
|
51
|
+
Object.assign(moduleExports, options.namedExports);
|
|
52
|
+
}
|
|
53
|
+
return moduleExports;
|
|
54
|
+
});
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export const mock = { module: mockModuleImpl };
|
|
@@ -1,80 +1,82 @@
|
|
|
1
|
-
import { toRecord } from "./record-utils.js";
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
*
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
}
|
|
1
|
+
import { toRecord } from "./record-utils.js";
|
|
2
|
+
import { stripAnsiFast } from "./techniques/ansi.js";
|
|
3
|
+
import { sanitizeRtkEmojiOutput } from "./techniques/emoji.js";
|
|
4
|
+
import { stripRtkHookWarnings } from "./techniques/rtk.js";
|
|
5
|
+
|
|
6
|
+
interface ToolResultTextBlock {
|
|
7
|
+
type: string;
|
|
8
|
+
text?: string;
|
|
9
|
+
[key: string]: unknown;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface StreamingBashExecutionSanitizationResult {
|
|
13
|
+
changed: boolean;
|
|
14
|
+
result: unknown;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function sanitizeStreamingBashText(text: string, command: string | undefined | null): string {
|
|
18
|
+
let nextText = stripAnsiFast(text);
|
|
19
|
+
|
|
20
|
+
const withoutRtkHookWarnings = stripRtkHookWarnings(nextText, command);
|
|
21
|
+
if (withoutRtkHookWarnings !== null) {
|
|
22
|
+
nextText = withoutRtkHookWarnings;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const withoutRtkEmoji = sanitizeRtkEmojiOutput(nextText, command);
|
|
26
|
+
if (withoutRtkEmoji !== null) {
|
|
27
|
+
nextText = withoutRtkEmoji;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return nextText;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Returns a sanitized shallow copy of streamed bash result blocks before the
|
|
35
|
+
* TUI renders them so RTK self-diagnostics never flash in partial or final
|
|
36
|
+
* tool output. The input object is not mutated.
|
|
37
|
+
*/
|
|
38
|
+
export function sanitizeStreamingBashExecutionResult(
|
|
39
|
+
result: unknown,
|
|
40
|
+
command: string | undefined | null,
|
|
41
|
+
): StreamingBashExecutionSanitizationResult {
|
|
42
|
+
const resultRecord = toRecord(result);
|
|
43
|
+
const sourceContent = Array.isArray(resultRecord.content) ? resultRecord.content : null;
|
|
44
|
+
if (!sourceContent || sourceContent.length === 0) {
|
|
45
|
+
return { changed: false, result };
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
let changed = false;
|
|
49
|
+
const nextContent = sourceContent.map((block) => {
|
|
50
|
+
if (!block || typeof block !== "object" || Array.isArray(block)) {
|
|
51
|
+
return block;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const contentBlock = block as ToolResultTextBlock;
|
|
55
|
+
if (contentBlock.type !== "text" || typeof contentBlock.text !== "string") {
|
|
56
|
+
return block;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const sanitizedText = sanitizeStreamingBashText(contentBlock.text, command);
|
|
60
|
+
if (sanitizedText === contentBlock.text) {
|
|
61
|
+
return block;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
changed = true;
|
|
65
|
+
return {
|
|
66
|
+
...contentBlock,
|
|
67
|
+
text: sanitizedText,
|
|
68
|
+
};
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
if (!changed) {
|
|
72
|
+
return { changed: false, result };
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return {
|
|
76
|
+
changed: true,
|
|
77
|
+
result: {
|
|
78
|
+
...resultRecord,
|
|
79
|
+
content: nextContent,
|
|
80
|
+
},
|
|
81
|
+
};
|
|
82
|
+
}
|
package/src/types-shims.d.ts
CHANGED
|
@@ -162,6 +162,12 @@ declare module "node:assert/strict" {
|
|
|
162
162
|
export default assert;
|
|
163
163
|
}
|
|
164
164
|
|
|
165
|
+
declare module "node:test" {
|
|
166
|
+
export const mock: {
|
|
167
|
+
module(specifier: string, options: { namedExports?: Record<string, unknown>; defaultExport?: unknown }): void;
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
|
|
165
171
|
declare module "bun:test" {
|
|
166
172
|
export const mock: {
|
|
167
173
|
module(specifier: string, factory: () => Record<string, unknown>): void;
|