pi-rtk-optimizer 0.6.0 → 0.7.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 CHANGED
@@ -7,6 +7,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.7.0] - 2026-04-30
11
+
12
+ ### Added
13
+ - Added opt-in `readCompaction` controls for `read` output so lossy source filtering and smart truncation stay disabled unless explicitly enabled.
14
+
15
+ ### Changed
16
+ - Updated README and example configuration defaults for safer read-compaction behavior and troubleshooting guidance.
17
+ - Updated `@mariozechner/pi-coding-agent` and `@mariozechner/pi-tui` peer dependencies to ^0.70.6.
18
+
10
19
  ## [0.6.0] - 2026-04-27
11
20
 
12
21
  ### Changed
package/README.md CHANGED
@@ -130,9 +130,10 @@ Bash command support is intentionally resolved by the installed `rtk` binary thr
130
130
  |--------|------|---------|-------------|
131
131
  | `outputCompaction.enabled` | boolean | `true` | Enable output compaction pipeline |
132
132
  | `outputCompaction.stripAnsi` | boolean | `true` | Remove ANSI escape codes |
133
- | `outputCompaction.sourceCodeFilteringEnabled` | boolean | `true` | Enable source code filtering for `read` output |
133
+ | `outputCompaction.readCompaction.enabled` | boolean | `false` | Enable lossy compaction for `read` output; defaults off so code reads stay exact |
134
+ | `outputCompaction.sourceCodeFilteringEnabled` | boolean | `false` | Enable source code filtering for `read` output when read compaction is enabled |
134
135
  | `outputCompaction.preserveExactSkillReads` | boolean | `false` | Keep reads under configured Pi/global/project skill directories exact, bypassing read compaction |
135
- | `outputCompaction.sourceCodeFiltering` | string | `"minimal"` | Filter level: `"none"`, `"minimal"`, `"aggressive"` |
136
+ | `outputCompaction.sourceCodeFiltering` | string | `"none"` | Filter level: `"none"`, `"minimal"`, `"aggressive"` |
136
137
  | `outputCompaction.aggregateTestOutput` | boolean | `true` | Summarize test runner output |
137
138
  | `outputCompaction.filterBuildOutput` | boolean | `true` | Filter build/compile output |
138
139
  | `outputCompaction.compactGitOutput` | boolean | `true` | Compact git command output |
@@ -146,7 +147,7 @@ Skill-read preservation covers the global Pi skills directory (`~/.pi/agent/skil
146
147
 
147
148
  | Option | Type | Default | Range | Description |
148
149
  |--------|------|---------|-------|-------------|
149
- | `outputCompaction.smartTruncate.enabled` | boolean | `true` | — | Enable smart line-based truncation |
150
+ | `outputCompaction.smartTruncate.enabled` | boolean | `false` | — | Enable smart line-based truncation for read output when read compaction is enabled |
150
151
  | `outputCompaction.smartTruncate.maxLines` | number | `220` | 40–4000 | Maximum lines after smart truncation |
151
152
  | `outputCompaction.truncate.enabled` | boolean | `true` | — | Enable hard character truncation |
152
153
  | `outputCompaction.truncate.maxChars` | number | `12000` | 1000–200000 | Maximum characters in final output |
@@ -159,7 +160,7 @@ Skill-read preservation covers the global Pi skills directory (`~/.pi/agent/skil
159
160
  | `minimal` | Removes non-doc comments, collapses blank lines |
160
161
  | `aggressive` | Also removes imports, keeps only signatures and key logic |
161
162
 
162
- > **Note:** When source filtering and read truncation safeguards are active, Pi injects a troubleshooting note for repeated file-edit mismatches. If edits fail because "old text does not match," disable source filtering via `/rtk`, re-read the file, apply the edit, then re-enable filtering.
163
+ > **Note:** When read compaction, source filtering, and read truncation safeguards are active, Pi injects a troubleshooting note for repeated file-edit mismatches. If edits fail because "old text does not match," disable read compaction via `/rtk`, re-read the file, apply the edit, then re-enable compaction.
163
164
 
164
165
  ### Example Configuration
165
166
 
@@ -172,9 +173,12 @@ Skill-read preservation covers the global Pi skills directory (`~/.pi/agent/skil
172
173
  "outputCompaction": {
173
174
  "enabled": true,
174
175
  "stripAnsi": true,
175
- "sourceCodeFilteringEnabled": true,
176
+ "readCompaction": {
177
+ "enabled": false
178
+ },
179
+ "sourceCodeFilteringEnabled": false,
176
180
  "preserveExactSkillReads": false,
177
- "sourceCodeFiltering": "minimal",
181
+ "sourceCodeFiltering": "none",
178
182
  "aggregateTestOutput": true,
179
183
  "filterBuildOutput": true,
180
184
  "compactGitOutput": true,
@@ -182,7 +186,7 @@ Skill-read preservation covers the global Pi skills directory (`~/.pi/agent/skil
182
186
  "groupSearchOutput": true,
183
187
  "trackSavings": true,
184
188
  "smartTruncate": {
185
- "enabled": true,
189
+ "enabled": false,
186
190
  "maxLines": 220
187
191
  },
188
192
  "truncate": {
@@ -6,15 +6,18 @@
6
6
  "outputCompaction": {
7
7
  "enabled": true,
8
8
  "stripAnsi": true,
9
- "sourceCodeFilteringEnabled": true,
9
+ "readCompaction": {
10
+ "enabled": false
11
+ },
12
+ "sourceCodeFilteringEnabled": false,
10
13
  "preserveExactSkillReads": false,
11
14
  "truncate": {
12
15
  "enabled": true,
13
16
  "maxChars": 12000
14
17
  },
15
- "sourceCodeFiltering": "minimal",
18
+ "sourceCodeFiltering": "none",
16
19
  "smartTruncate": {
17
- "enabled": true,
20
+ "enabled": false,
18
21
  "maxLines": 220
19
22
  },
20
23
  "aggregateTestOutput": true,
package/package.json CHANGED
@@ -1,64 +1,64 @@
1
- {
2
- "name": "pi-rtk-optimizer",
3
- "version": "0.6.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
- "pi-coding-agent",
31
- "coding-agent",
32
- "rtk",
33
- "token-optimization",
34
- "tool-compaction",
35
- "output-compaction",
36
- "command-rewrite",
37
- "optimization"
38
- ],
39
- "author": "MasuRii",
40
- "license": "MIT",
41
- "repository": {
42
- "type": "git",
43
- "url": "git+https://github.com/MasuRii/pi-rtk-optimizer.git"
44
- },
45
- "bugs": {
46
- "url": "https://github.com/MasuRii/pi-rtk-optimizer/issues"
47
- },
48
- "homepage": "https://github.com/MasuRii/pi-rtk-optimizer#readme",
49
- "engines": {
50
- "node": ">=20"
51
- },
52
- "publishConfig": {
53
- "access": "public"
54
- },
55
- "pi": {
56
- "extensions": [
57
- "./index.ts"
58
- ]
59
- },
60
- "peerDependencies": {
61
- "@mariozechner/pi-coding-agent": "^0.70.2",
62
- "@mariozechner/pi-tui": "^0.70.2"
63
- }
64
- }
1
+ {
2
+ "name": "pi-rtk-optimizer",
3
+ "version": "0.7.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
+ "pi-coding-agent",
31
+ "coding-agent",
32
+ "rtk",
33
+ "token-optimization",
34
+ "tool-compaction",
35
+ "output-compaction",
36
+ "command-rewrite",
37
+ "optimization"
38
+ ],
39
+ "author": "MasuRii",
40
+ "license": "MIT",
41
+ "repository": {
42
+ "type": "git",
43
+ "url": "git+https://github.com/MasuRii/pi-rtk-optimizer.git"
44
+ },
45
+ "bugs": {
46
+ "url": "https://github.com/MasuRii/pi-rtk-optimizer/issues"
47
+ },
48
+ "homepage": "https://github.com/MasuRii/pi-rtk-optimizer#readme",
49
+ "engines": {
50
+ "node": ">=20"
51
+ },
52
+ "publishConfig": {
53
+ "access": "public"
54
+ },
55
+ "pi": {
56
+ "extensions": [
57
+ "./index.ts"
58
+ ]
59
+ },
60
+ "peerDependencies": {
61
+ "@mariozechner/pi-coding-agent": "^0.70.6",
62
+ "@mariozechner/pi-tui": "^0.70.6"
63
+ }
64
+ }
@@ -66,6 +66,7 @@ runTest("config-store normalizes invalid values and clamps numeric ranges", () =
66
66
  assert.equal(normalized.mode, "rewrite");
67
67
  assert.equal(Object.hasOwn(normalized, "rewriteGitGithub"), false);
68
68
  assert.equal(normalized.outputCompaction.stripAnsi, false);
69
+ assert.equal(normalized.outputCompaction.readCompaction.enabled, true);
69
70
  assert.equal(normalized.outputCompaction.sourceCodeFilteringEnabled, true);
70
71
  assert.equal(normalized.outputCompaction.sourceCodeFiltering, "minimal");
71
72
  assert.equal(normalized.outputCompaction.truncate.maxChars, 1_000);
@@ -73,18 +74,32 @@ runTest("config-store normalizes invalid values and clamps numeric ranges", () =
73
74
  assert.equal(normalized.outputCompaction.trackSavings, false);
74
75
  });
75
76
 
77
+ runTest("config-store uses safer read defaults when readCompaction is explicit", () => {
78
+ const normalized = normalizeRtkIntegrationConfig({
79
+ outputCompaction: {
80
+ readCompaction: { enabled: false },
81
+ },
82
+ });
83
+
84
+ assert.equal(normalized.outputCompaction.readCompaction.enabled, false);
85
+ assert.equal(normalized.outputCompaction.sourceCodeFilteringEnabled, false);
86
+ assert.equal(normalized.outputCompaction.sourceCodeFiltering, "none");
87
+ assert.equal(normalized.outputCompaction.smartTruncate.enabled, false);
88
+ });
89
+
76
90
  runTest("config-store can ensure, save, and reload isolated config files", () => {
77
91
  const tempPath = makeTempConfigPath();
78
92
  cleanupFile(tempPath);
79
93
 
80
94
  try {
81
95
  const ensured = ensureConfigExists(tempPath);
82
- assert.equal(ensured.created, true);
96
+ assert.equal(ensured.error, undefined);
83
97
  assert.equal(existsSync(tempPath), true);
84
98
 
85
99
  const defaultLoad = loadRtkIntegrationConfig(tempPath);
86
100
  assert.equal(defaultLoad.warning, undefined);
87
101
  assert.equal(defaultLoad.config.mode, "rewrite");
102
+ assert.equal(defaultLoad.config.outputCompaction.readCompaction.enabled, false);
88
103
 
89
104
  const saved = saveRtkIntegrationConfig(
90
105
  {
@@ -153,6 +168,14 @@ runTest("command detection ignores env prefixes, blank lines, and chained suffix
153
168
  assert.equal(matchesCommandPatterns("echo hello", [/^bun test/]), false);
154
169
  });
155
170
 
171
+ runTest("RTK command environment preserves explicit leading RTK_DB_PATH overrides", () => {
172
+ const command = 'RTK_DB_PATH="/custom/history.db" rtk git diff';
173
+ assert.equal(applyRtkCommandEnvironment(command), command);
174
+
175
+ const exportedCommand = 'export RTK_DB_PATH="/custom/history.db"; rtk git diff';
176
+ assert.equal(applyRtkCommandEnvironment(exportedCommand), exportedCommand);
177
+ });
178
+
156
179
  runTest("path compaction preserves the tail and handles Windows separators", () => {
157
180
  const unixPath = "/Users/example/projects/pi-rtk-optimizer/src/techniques/path-utils.ts";
158
181
  const compactUnixPath = compactPath(unixPath, 28);
@@ -204,21 +227,21 @@ runTest("rewrite pipeline safety buffers rewritten Windows producer commands", (
204
227
  assert.equal(applyRewrittenCommandShellSafetyFixups("git diff | grep TODO"), "git diff | grep TODO");
205
228
  });
206
229
 
207
- runTest("rewrite pipeline safety keeps RTK_DB_PATH scoped to rewritten producer commands", () => {
230
+ runTest("rewrite pipeline safety keeps exported RTK_DB_PATH on rewritten producer commands", () => {
208
231
  const rewritten = applyRewrittenCommandShellSafetyFixups(
209
232
  applyRtkCommandEnvironment("rtk git diff agent/extensions/pi-multi-auth/account-manager.ts | head -200"),
210
233
  );
211
234
 
212
235
  if (process.platform === "win32") {
213
- assert.ok(rewritten.startsWith("{"));
236
+ assert.ok(rewritten.startsWith("export RTK_DB_PATH="));
214
237
  assert.equal(rewritten.startsWith("RTK_DB_PATH="), false);
215
- assert.ok(rewritten.includes("RTK_DB_PATH="));
238
+ assert.ok(rewritten.includes("; {"));
216
239
  assert.ok(
217
240
  rewritten.includes('rtk git diff agent/extensions/pi-multi-auth/account-manager.ts > "$__pi_rtk_pipe_tmp"'),
218
241
  );
219
242
  assert.ok(rewritten.includes('(head -200) < "$__pi_rtk_pipe_tmp"'));
220
243
  } else {
221
- assert.ok(rewritten.startsWith("RTK_DB_PATH="));
244
+ assert.ok(rewritten.startsWith("export RTK_DB_PATH="));
222
245
  assert.equal(
223
246
  rewritten,
224
247
  applyRtkCommandEnvironment("rtk git diff agent/extensions/pi-multi-auth/account-manager.ts | head -200"),
@@ -226,6 +249,25 @@ runTest("rewrite pipeline safety keeps RTK_DB_PATH scoped to rewritten producer
226
249
  }
227
250
  });
228
251
 
252
+ runTest("rewrite pipeline safety buffers explicit RTK_DB_PATH export preludes", () => {
253
+ const command = 'export RTK_DB_PATH="/custom/history.db"; rtk git diff | head -200';
254
+ const rewritten = applyRewrittenCommandShellSafetyFixups(command);
255
+
256
+ if (process.platform === "win32") {
257
+ assert.ok(rewritten.startsWith('export RTK_DB_PATH="/custom/history.db"; {'));
258
+ assert.ok(rewritten.includes('rtk git diff > "$__pi_rtk_pipe_tmp"'));
259
+ assert.ok(rewritten.includes('(head -200) < "$__pi_rtk_pipe_tmp"'));
260
+ } else {
261
+ assert.equal(rewritten, command);
262
+ }
263
+ });
264
+
265
+ runTest("RTK command environment uses export prelude for shell compound commands", () => {
266
+ const rewritten = applyRtkCommandEnvironment('for d in a b; do echo "$d"; done');
267
+ assert.ok(/^export RTK_DB_PATH=/.test(rewritten));
268
+ assert.ok(/; for d in a b; do echo "\$d"; done$/.test(rewritten));
269
+ });
270
+
229
271
  runTest("stripRtkHookWarnings handles bare, prefixed, and already-sanitized hook notices", () => {
230
272
  assert.equal(
231
273
  stripRtkHookWarnings("No hook installed — run `rtk init -g` for automatic token savings\n\nready\n", null),
@@ -58,7 +58,7 @@ function summarizeConfig(config: RtkIntegrationConfig, runtimeStatus: RuntimeSta
58
58
  ? "rtk=available"
59
59
  : `rtk=missing${runtimeStatus.lastError ? ` (${runtimeStatus.lastError})` : ""}`;
60
60
 
61
- return `enabled=${config.enabled}, mode=${config.mode}, rewriteSource=rtk, rewriteNotice=${config.showRewriteNotifications}, compaction=${config.outputCompaction.enabled}, sourceFilterEnabled=${config.outputCompaction.sourceCodeFilteringEnabled}, preserveSkillReads=${config.outputCompaction.preserveExactSkillReads}, sourceFilter=${config.outputCompaction.sourceCodeFiltering}, ${runtime}`;
61
+ return `enabled=${config.enabled}, mode=${config.mode}, rewriteSource=rtk, rewriteNotice=${config.showRewriteNotifications}, compaction=${config.outputCompaction.enabled}, readCompaction=${config.outputCompaction.readCompaction.enabled}, sourceFilterEnabled=${config.outputCompaction.sourceCodeFilteringEnabled}, preserveSkillReads=${config.outputCompaction.preserveExactSkillReads}, sourceFilter=${config.outputCompaction.sourceCodeFiltering}, ${runtime}`;
62
62
  }
63
63
 
64
64
  function buildSettingItems(config: RtkIntegrationConfig): SettingItem[] {
@@ -105,6 +105,13 @@ function buildSettingItems(config: RtkIntegrationConfig): SettingItem[] {
105
105
  currentValue: toOnOff(config.outputCompaction.stripAnsi),
106
106
  values: ON_OFF,
107
107
  },
108
+ {
109
+ id: "outputReadCompactionEnabled",
110
+ label: "Read compaction enabled",
111
+ description: "If off, read tool output stays exact; build/test/git/grep compaction can still run",
112
+ currentValue: toOnOff(config.outputCompaction.readCompaction.enabled),
113
+ values: ON_OFF,
114
+ },
108
115
  {
109
116
  id: "outputTruncateEnabled",
110
117
  label: "Hard truncation enabled",
@@ -219,6 +226,14 @@ function applySetting(config: RtkIntegrationConfig, id: string, value: string):
219
226
  ...config,
220
227
  outputCompaction: { ...config.outputCompaction, stripAnsi: value === "on" },
221
228
  };
229
+ case "outputReadCompactionEnabled":
230
+ return {
231
+ ...config,
232
+ outputCompaction: {
233
+ ...config.outputCompaction,
234
+ readCompaction: { enabled: value === "on" },
235
+ },
236
+ };
222
237
  case "outputTruncateEnabled":
223
238
  return {
224
239
  ...config,
@@ -353,6 +368,7 @@ function syncSettingValues(settingsList: SettingValueSyncTarget, config: RtkInte
353
368
  settingsList.updateValue("guardWhenRtkMissing", toOnOff(config.guardWhenRtkMissing));
354
369
  settingsList.updateValue("outputCompactionEnabled", toOnOff(config.outputCompaction.enabled));
355
370
  settingsList.updateValue("outputStripAnsi", toOnOff(config.outputCompaction.stripAnsi));
371
+ settingsList.updateValue("outputReadCompactionEnabled", toOnOff(config.outputCompaction.readCompaction.enabled));
356
372
  settingsList.updateValue("outputTruncateEnabled", toOnOff(config.outputCompaction.truncate.enabled));
357
373
  settingsList.updateValue("outputTruncateMaxChars", String(config.outputCompaction.truncate.maxChars));
358
374
  settingsList.updateValue("outputSourceFilteringEnabled", toOnOff(config.outputCompaction.sourceCodeFilteringEnabled));
@@ -30,10 +30,14 @@ function toMode(value: unknown): RtkIntegrationConfig["mode"] {
30
30
  : DEFAULT_RTK_INTEGRATION_CONFIG.mode;
31
31
  }
32
32
 
33
- function toSourceFilterLevel(value: unknown): RtkSourceFilterLevel {
33
+ function toSourceFilterLevel(value: unknown, fallback: RtkSourceFilterLevel): RtkSourceFilterLevel {
34
34
  return RTK_SOURCE_FILTER_LEVELS.includes(value as RtkSourceFilterLevel)
35
35
  ? (value as RtkSourceFilterLevel)
36
- : DEFAULT_RTK_INTEGRATION_CONFIG.outputCompaction.sourceCodeFiltering;
36
+ : fallback;
37
+ }
38
+
39
+ function hasOwnProperty(source: Record<string, unknown>, key: string): boolean {
40
+ return Object.prototype.hasOwnProperty.call(source, key);
37
41
  }
38
42
 
39
43
  function toObject(value: unknown): Record<string, unknown> {
@@ -46,8 +50,20 @@ function toObject(value: unknown): Record<string, unknown> {
46
50
  export function normalizeRtkIntegrationConfig(raw: unknown): RtkIntegrationConfig {
47
51
  const source = toObject(raw);
48
52
  const outputCompactionSource = toObject(source.outputCompaction);
53
+ const readCompactionSource = toObject(outputCompactionSource.readCompaction);
49
54
  const truncateSource = toObject(outputCompactionSource.truncate);
50
55
  const smartTruncateSource = toObject(outputCompactionSource.smartTruncate);
56
+ const hasReadCompaction = hasOwnProperty(outputCompactionSource, "readCompaction");
57
+ const legacyReadCompactionFallback = !hasReadCompaction;
58
+ const sourceFilteringFallback = legacyReadCompactionFallback
59
+ ? true
60
+ : DEFAULT_RTK_INTEGRATION_CONFIG.outputCompaction.sourceCodeFilteringEnabled;
61
+ const sourceFilterLevelFallback = legacyReadCompactionFallback
62
+ ? "minimal"
63
+ : DEFAULT_RTK_INTEGRATION_CONFIG.outputCompaction.sourceCodeFiltering;
64
+ const smartTruncateEnabledFallback = legacyReadCompactionFallback
65
+ ? true
66
+ : DEFAULT_RTK_INTEGRATION_CONFIG.outputCompaction.smartTruncate.enabled;
51
67
 
52
68
  return {
53
69
  enabled: toBoolean(source.enabled, DEFAULT_RTK_INTEGRATION_CONFIG.enabled),
@@ -69,9 +85,17 @@ export function normalizeRtkIntegrationConfig(raw: unknown): RtkIntegrationConfi
69
85
  outputCompactionSource.stripAnsi,
70
86
  DEFAULT_RTK_INTEGRATION_CONFIG.outputCompaction.stripAnsi,
71
87
  ),
88
+ readCompaction: {
89
+ enabled: hasReadCompaction
90
+ ? toBoolean(
91
+ readCompactionSource.enabled,
92
+ DEFAULT_RTK_INTEGRATION_CONFIG.outputCompaction.readCompaction.enabled,
93
+ )
94
+ : true,
95
+ },
72
96
  sourceCodeFilteringEnabled: toBoolean(
73
97
  outputCompactionSource.sourceCodeFilteringEnabled,
74
- DEFAULT_RTK_INTEGRATION_CONFIG.outputCompaction.sourceCodeFilteringEnabled,
98
+ sourceFilteringFallback,
75
99
  ),
76
100
  preserveExactSkillReads: toBoolean(
77
101
  outputCompactionSource.preserveExactSkillReads,
@@ -89,11 +113,14 @@ export function normalizeRtkIntegrationConfig(raw: unknown): RtkIntegrationConfi
89
113
  200_000,
90
114
  ),
91
115
  },
92
- sourceCodeFiltering: toSourceFilterLevel(outputCompactionSource.sourceCodeFiltering),
116
+ sourceCodeFiltering: toSourceFilterLevel(
117
+ outputCompactionSource.sourceCodeFiltering,
118
+ sourceFilterLevelFallback,
119
+ ),
93
120
  smartTruncate: {
94
121
  enabled: toBoolean(
95
122
  smartTruncateSource.enabled,
96
- DEFAULT_RTK_INTEGRATION_CONFIG.outputCompaction.smartTruncate.enabled,
123
+ smartTruncateEnabledFallback,
97
124
  ),
98
125
  maxLines: toInteger(
99
126
  smartTruncateSource.maxLines,
package/src/index-test.ts CHANGED
@@ -34,6 +34,7 @@ const { DEFAULT_RTK_INTEGRATION_CONFIG } = await import("./types.ts");
34
34
  function configWith(overrides: {
35
35
  enabled?: boolean;
36
36
  compactionEnabled?: boolean;
37
+ readCompactionEnabled?: boolean;
37
38
  sourceFilteringEnabled?: boolean;
38
39
  sourceFilteringLevel?: "none" | "minimal" | "aggressive";
39
40
  smartTruncateEnabled?: boolean;
@@ -46,6 +47,10 @@ function configWith(overrides: {
46
47
  outputCompaction: {
47
48
  ...base.outputCompaction,
48
49
  enabled: overrides.compactionEnabled ?? base.outputCompaction.enabled,
50
+ readCompaction: {
51
+ ...base.outputCompaction.readCompaction,
52
+ enabled: overrides.readCompactionEnabled ?? base.outputCompaction.readCompaction.enabled,
53
+ },
49
54
  sourceCodeFilteringEnabled:
50
55
  overrides.sourceFilteringEnabled ?? base.outputCompaction.sourceCodeFilteringEnabled,
51
56
  sourceCodeFiltering: overrides.sourceFilteringLevel ?? base.outputCompaction.sourceCodeFiltering,
@@ -86,13 +91,23 @@ runTest("bounded notice tracker coerces invalid limits to a safe minimum", () =>
86
91
  runTest("source-filter note injected when source filtering is active", () => {
87
92
  assert.equal(
88
93
  shouldInjectSourceFilterTroubleshootingNote(
89
- configWith({ sourceFilteringEnabled: true, sourceFilteringLevel: "minimal" }),
94
+ configWith({
95
+ readCompactionEnabled: true,
96
+ sourceFilteringEnabled: true,
97
+ sourceFilteringLevel: "minimal",
98
+ smartTruncateEnabled: true,
99
+ }),
90
100
  ),
91
101
  true,
92
102
  );
93
103
  assert.equal(
94
104
  shouldInjectSourceFilterTroubleshootingNote(
95
- configWith({ sourceFilteringEnabled: true, sourceFilteringLevel: "aggressive" }),
105
+ configWith({
106
+ readCompactionEnabled: true,
107
+ sourceFilteringEnabled: true,
108
+ sourceFilteringLevel: "aggressive",
109
+ smartTruncateEnabled: true,
110
+ }),
96
111
  ),
97
112
  true,
98
113
  );
@@ -106,6 +121,20 @@ runTest("source-filter note skipped when compaction is disabled", () => {
106
121
  assert.equal(shouldInjectSourceFilterTroubleshootingNote(configWith({ compactionEnabled: false })), false);
107
122
  });
108
123
 
124
+ runTest("source-filter note skipped when read compaction is disabled", () => {
125
+ assert.equal(
126
+ shouldInjectSourceFilterTroubleshootingNote(
127
+ configWith({
128
+ readCompactionEnabled: false,
129
+ sourceFilteringEnabled: true,
130
+ sourceFilteringLevel: "minimal",
131
+ smartTruncateEnabled: true,
132
+ }),
133
+ ),
134
+ false,
135
+ );
136
+ });
137
+
109
138
  runTest("source-filter note skipped when source filtering flag is off", () => {
110
139
  assert.equal(
111
140
  shouldInjectSourceFilterTroubleshootingNote(configWith({ sourceFilteringEnabled: false })),
package/src/index.ts CHANGED
@@ -28,13 +28,14 @@ function trimMessage(raw: string, maxLength = 220): string {
28
28
  }
29
29
 
30
30
  const SOURCE_FILTER_TROUBLESHOOTING_NOTE =
31
- "RTK note: If file edits repeatedly fail because old text does not match, ask the user to manually run '/rtk' in the Pi TUI, disable 'Read source filtering enabled', re-read the file, apply the edit, then ask the user to manually re-enable it in the Pi TUI.";
31
+ "RTK note: If file edits repeatedly fail because old text does not match, ask the user to manually run '/rtk' in the Pi TUI, disable 'Read compaction enabled', re-read the file, apply the edit, then ask the user to manually re-enable it in the Pi TUI.";
32
32
 
33
33
  export function shouldInjectSourceFilterTroubleshootingNote(config: RtkIntegrationConfig): boolean {
34
34
  const compaction = config.outputCompaction;
35
35
  return (
36
36
  config.enabled &&
37
37
  compaction.enabled &&
38
+ compaction.readCompaction.enabled &&
38
39
  compaction.sourceCodeFilteringEnabled &&
39
40
  compaction.sourceCodeFiltering !== "none" &&
40
41
  (compaction.smartTruncate.enabled || compaction.truncate.enabled)
@@ -24,6 +24,10 @@ function buildReadContent(lineCount: number): string {
24
24
  return `${lines.join("\n")}\n`;
25
25
  }
26
26
 
27
+ function setReadCompaction(config: ReturnType<typeof cloneDefaultConfig>, enabled: boolean): void {
28
+ config.outputCompaction.readCompaction = { enabled };
29
+ }
30
+
27
31
  function firstTextBlock(content: unknown[] | undefined): string {
28
32
  if (!Array.isArray(content) || content.length === 0) {
29
33
  return "";
@@ -73,6 +77,7 @@ function assertNoOutputEmoji(text: string): void {
73
77
 
74
78
  runTest("precision read with offset keeps exact output (no source/smart/hard truncation)", () => {
75
79
  const config = cloneDefaultConfig();
80
+ setReadCompaction(config, true);
76
81
  config.outputCompaction.truncate.enabled = true;
77
82
  config.outputCompaction.truncate.maxChars = 500;
78
83
  config.outputCompaction.smartTruncate.enabled = true;
@@ -94,6 +99,7 @@ runTest("precision read with offset keeps exact output (no source/smart/hard tru
94
99
 
95
100
  runTest("precision read with limit keeps exact output", () => {
96
101
  const config = cloneDefaultConfig();
102
+ setReadCompaction(config, true);
97
103
  config.outputCompaction.truncate.enabled = true;
98
104
  config.outputCompaction.truncate.maxChars = 500;
99
105
  config.outputCompaction.smartTruncate.enabled = true;
@@ -113,8 +119,34 @@ runTest("precision read with limit keeps exact output", () => {
113
119
  assert.deepEqual(result.techniques, []);
114
120
  });
115
121
 
116
- runTest("normal read compacts and adds banner", () => {
122
+ runTest("default read output stays exact when read compaction is disabled by default", () => {
123
+ const config = cloneDefaultConfig();
124
+ config.outputCompaction.sourceCodeFilteringEnabled = true;
125
+ config.outputCompaction.sourceCodeFiltering = "aggressive";
126
+ config.outputCompaction.smartTruncate.enabled = true;
127
+ config.outputCompaction.smartTruncate.maxLines = 40;
128
+ config.outputCompaction.truncate.enabled = true;
129
+ config.outputCompaction.truncate.maxChars = 500;
130
+
131
+ const content = buildReadContent(220);
132
+ const result = compactToolResult(
133
+ {
134
+ toolName: "read",
135
+ input: { path: "sample.ts" },
136
+ content: [{ type: "text", text: content }],
137
+ },
138
+ config,
139
+ );
140
+
141
+ assert.equal(result.changed, false);
142
+ assert.deepEqual(result.techniques, []);
143
+ });
144
+
145
+ runTest("normal read compacts and adds banner when read compaction is enabled", () => {
117
146
  const config = cloneDefaultConfig();
147
+ setReadCompaction(config, true);
148
+ config.outputCompaction.sourceCodeFilteringEnabled = true;
149
+ config.outputCompaction.sourceCodeFiltering = "minimal";
118
150
  config.outputCompaction.smartTruncate.enabled = true;
119
151
  config.outputCompaction.smartTruncate.maxLines = 40;
120
152
 
@@ -138,6 +170,7 @@ runTest("normal read compacts and adds banner", () => {
138
170
 
139
171
  runTest("short read output stays exact below threshold", () => {
140
172
  const config = cloneDefaultConfig();
173
+ setReadCompaction(config, true);
141
174
  const content = buildReadContent(40);
142
175
 
143
176
  const result = compactToolResult(
@@ -155,6 +188,7 @@ runTest("short read output stays exact below threshold", () => {
155
188
 
156
189
  runTest("read output stays exact at the 80-line boundary with trailing newline", () => {
157
190
  const config = cloneDefaultConfig();
191
+ setReadCompaction(config, true);
158
192
  config.outputCompaction.smartTruncate.enabled = true;
159
193
  config.outputCompaction.smartTruncate.maxLines = 40;
160
194
 
@@ -172,8 +206,11 @@ runTest("read output stays exact at the 80-line boundary with trailing newline",
172
206
  assert.deepEqual(result.techniques, []);
173
207
  });
174
208
 
175
- runTest("read output compacts once the content exceeds the 80-line exactness threshold", () => {
209
+ runTest("read output compacts once the content exceeds the 80-line exactness threshold when read compaction is enabled", () => {
176
210
  const config = cloneDefaultConfig();
211
+ setReadCompaction(config, true);
212
+ config.outputCompaction.sourceCodeFilteringEnabled = true;
213
+ config.outputCompaction.sourceCodeFiltering = "minimal";
177
214
  config.outputCompaction.smartTruncate.enabled = true;
178
215
  config.outputCompaction.smartTruncate.maxLines = 40;
179
216
 
@@ -193,6 +230,9 @@ runTest("read output compacts once the content exceeds the 80-line exactness thr
193
230
 
194
231
  runTest("source file reads skip lossy source filtering when truncation safeguards are not needed", () => {
195
232
  const config = cloneDefaultConfig();
233
+ setReadCompaction(config, true);
234
+ config.outputCompaction.sourceCodeFilteringEnabled = true;
235
+ config.outputCompaction.sourceCodeFiltering = "minimal";
196
236
  config.outputCompaction.smartTruncate.enabled = false;
197
237
  config.outputCompaction.truncate.enabled = false;
198
238
 
@@ -213,6 +253,7 @@ runTest("source file reads skip lossy source filtering when truncation safeguard
213
253
 
214
254
  runTest("skill reads stay exact when preserveExactSkillReads is enabled for user skills", () => {
215
255
  const config = cloneDefaultConfig();
256
+ setReadCompaction(config, true);
216
257
  config.outputCompaction.preserveExactSkillReads = true;
217
258
  config.outputCompaction.truncate.enabled = true;
218
259
  config.outputCompaction.truncate.maxChars = 500;
@@ -235,6 +276,7 @@ runTest("skill reads stay exact when preserveExactSkillReads is enabled for user
235
276
 
236
277
  runTest("project .pi skill reads stay exact when preserveExactSkillReads is enabled", () => {
237
278
  const config = cloneDefaultConfig();
279
+ setReadCompaction(config, true);
238
280
  config.outputCompaction.preserveExactSkillReads = true;
239
281
  config.outputCompaction.truncate.enabled = true;
240
282
  config.outputCompaction.truncate.maxChars = 500;
@@ -257,6 +299,7 @@ runTest("project .pi skill reads stay exact when preserveExactSkillReads is enab
257
299
 
258
300
  runTest("ancestor .agents skill reads stay exact when preserveExactSkillReads is enabled", () => {
259
301
  const config = cloneDefaultConfig();
302
+ setReadCompaction(config, true);
260
303
  config.outputCompaction.preserveExactSkillReads = true;
261
304
  config.outputCompaction.truncate.enabled = true;
262
305
  config.outputCompaction.truncate.maxChars = 500;
@@ -140,6 +140,10 @@ function shouldPreserveExactReadOutput(
140
140
  input: Record<string, unknown>,
141
141
  config: RtkIntegrationConfig,
142
142
  ): boolean {
143
+ if (!config.outputCompaction.readCompaction.enabled) {
144
+ return true;
145
+ }
146
+
143
147
  if (hasExplicitReadRange(input)) {
144
148
  return true;
145
149
  }
@@ -10,6 +10,26 @@ interface ProducerRewritePlan {
10
10
  captureStderr: boolean;
11
11
  }
12
12
 
13
+ interface ShellSafetyTarget {
14
+ environmentPrelude: string;
15
+ command: string;
16
+ }
17
+
18
+ const LEADING_RTK_DB_PATH_EXPORT_PRELUDE_PATTERN =
19
+ /^(\s*export\s+RTK_DB_PATH=(?:"(?:\\.|[^"])*"|'[^']*'|[^\s;]+)\s*;\s*)([\s\S]*)$/u;
20
+
21
+ function splitLeadingRtkDbPathExportPrelude(command: string): ShellSafetyTarget {
22
+ const match = command.match(LEADING_RTK_DB_PATH_EXPORT_PRELUDE_PATTERN);
23
+ if (!match) {
24
+ return { environmentPrelude: "", command };
25
+ }
26
+
27
+ return {
28
+ environmentPrelude: match[1] ?? "",
29
+ command: match[2] ?? "",
30
+ };
31
+ }
32
+
13
33
  function isTopLevelQuoteCharacter(character: string): character is '"' | "'" | "`" {
14
34
  return character === '"' || character === "'" || character === "`";
15
35
  }
@@ -134,7 +154,8 @@ export function applyRewrittenCommandShellSafetyFixups(command: string): string
134
154
  return command;
135
155
  }
136
156
 
137
- const parsedPipeline = parseSimpleTopLevelPipeline(command);
157
+ const target = splitLeadingRtkDbPathExportPrelude(command);
158
+ const parsedPipeline = parseSimpleTopLevelPipeline(target.command);
138
159
  if (!parsedPipeline) {
139
160
  return command;
140
161
  }
@@ -153,5 +174,5 @@ export function applyRewrittenCommandShellSafetyFixups(command: string): string
153
174
  return command;
154
175
  }
155
176
 
156
- return buildBufferedPipelineCommand(producer, remainder);
177
+ return `${target.environmentPrelude}${buildBufferedPipelineCommand(producer, remainder)}`;
157
178
  }
@@ -1,64 +1,69 @@
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
- }
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
+ const RTK_DB_PATH_EXPORT_PATTERN = /^export\s+RTK_DB_PATH=(?:"[^"]*"|'[^']*'|[^\s;]+)(?=\s*(?:;|$))/;
8
+
9
+ function resolveTemporaryDirectory(): string {
10
+ if (process.platform === "win32") {
11
+ const windowsTempDir = process.env.TEMP ?? process.env.TMP;
12
+ if (windowsTempDir && windowsTempDir.trim()) {
13
+ return windowsTempDir;
14
+ }
15
+
16
+ const localAppData = process.env.LOCALAPPDATA;
17
+ if (localAppData && localAppData.trim()) {
18
+ return join(localAppData, "Temp");
19
+ }
20
+
21
+ const userProfile = process.env.USERPROFILE;
22
+ if (userProfile && userProfile.trim()) {
23
+ return join(userProfile, "AppData", "Local", "Temp");
24
+ }
25
+
26
+ const systemRoot = process.env.SystemRoot ?? process.env.WINDIR;
27
+ if (systemRoot && systemRoot.trim()) {
28
+ return join(systemRoot, "Temp");
29
+ }
30
+
31
+ return "C:/Windows/Temp";
32
+ }
33
+
34
+ const posixTempDir = process.env.TMPDIR ?? process.env.TMP;
35
+ if (posixTempDir && posixTempDir.trim()) {
36
+ return posixTempDir;
37
+ }
38
+
39
+ return "/tmp";
40
+ }
41
+
42
+ function getTemporaryRtkHistoryDbPath(): string {
43
+ return join(resolveTemporaryDirectory(), "pi-rtk-optimizer", "history.db");
44
+ }
45
+
46
+ function quoteForShellEnv(value: string): string {
47
+ const normalizedValue = process.platform === "win32" ? value.replace(/\\/g, "/") : value;
48
+ return `"${normalizedValue.replace(/"/g, '\\"')}"`;
49
+ }
50
+
51
+ function hasLeadingRtkDbPathAssignment(command: string): boolean {
52
+ const trimmed = command.trimStart();
53
+ return (
54
+ RTK_DB_PATH_ASSIGNMENT_PATTERN.test(splitLeadingEnvAssignments(trimmed).envPrefix) ||
55
+ RTK_DB_PATH_EXPORT_PATTERN.test(trimmed)
56
+ );
57
+ }
58
+
59
+ export function applyRtkCommandEnvironment(command: string): string {
60
+ if (!command.trim()) {
61
+ return command;
62
+ }
63
+
64
+ if (hasLeadingRtkDbPathAssignment(command)) {
65
+ return command;
66
+ }
67
+
68
+ return `export ${RTK_DB_PATH_ENV_NAME}=${quoteForShellEnv(getTemporaryRtkHistoryDbPath())}; ${command}`;
69
+ }
package/src/types.ts CHANGED
@@ -7,6 +7,9 @@ export type RtkSourceFilterLevel = (typeof RTK_SOURCE_FILTER_LEVELS)[number];
7
7
  export interface RtkOutputCompactionConfig {
8
8
  enabled: boolean;
9
9
  stripAnsi: boolean;
10
+ readCompaction: {
11
+ enabled: boolean;
12
+ };
10
13
  truncate: {
11
14
  enabled: boolean;
12
15
  maxChars: number;
@@ -42,15 +45,18 @@ export const DEFAULT_RTK_INTEGRATION_CONFIG: RtkIntegrationConfig = {
42
45
  outputCompaction: {
43
46
  enabled: true,
44
47
  stripAnsi: true,
48
+ readCompaction: {
49
+ enabled: false,
50
+ },
45
51
  truncate: {
46
52
  enabled: true,
47
53
  maxChars: 12_000,
48
54
  },
49
- sourceCodeFilteringEnabled: true,
55
+ sourceCodeFilteringEnabled: false,
50
56
  preserveExactSkillReads: false,
51
- sourceCodeFiltering: "minimal",
57
+ sourceCodeFiltering: "none",
52
58
  smartTruncate: {
53
- enabled: true,
59
+ enabled: false,
54
60
  maxLines: 220,
55
61
  },
56
62
  aggregateTestOutput: true,