pi-rtk-optimizer 0.3.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 +38 -0
- package/LICENSE +21 -0
- package/README.md +114 -0
- package/config/config.example.json +35 -0
- package/index.ts +3 -0
- package/package.json +59 -0
- package/src/command-completions.ts +49 -0
- package/src/command-rewriter.ts +349 -0
- package/src/compat-commands.ts +207 -0
- package/src/config-modal.ts +600 -0
- package/src/config-store.ts +217 -0
- package/src/constants.ts +6 -0
- package/src/index.ts +291 -0
- package/src/output-compactor-test.ts +120 -0
- package/src/output-compactor.ts +343 -0
- package/src/output-metrics.ts +69 -0
- package/src/rewrite-rules.ts +248 -0
- package/src/techniques/ansi.ts +13 -0
- package/src/techniques/build.ts +155 -0
- package/src/techniques/command-detection.ts +53 -0
- package/src/techniques/git.ts +229 -0
- package/src/techniques/index.ts +16 -0
- package/src/techniques/linter.ts +161 -0
- package/src/techniques/search.ts +76 -0
- package/src/techniques/source.ts +230 -0
- package/src/techniques/test-output.ts +172 -0
- package/src/techniques/truncate.ts +11 -0
- package/src/types-shims.d.ts +131 -0
- package/src/types.ts +114 -0
- package/src/windows-command-helpers.ts +84 -0
- package/src/zellij-modal.ts +1001 -0
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, renameSync, unlinkSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { dirname } from "node:path";
|
|
3
|
+
import { CONFIG_PATH } from "./constants.js";
|
|
4
|
+
import {
|
|
5
|
+
DEFAULT_RTK_INTEGRATION_CONFIG,
|
|
6
|
+
RTK_MODES,
|
|
7
|
+
RTK_SOURCE_FILTER_LEVELS,
|
|
8
|
+
type ConfigLoadResult,
|
|
9
|
+
type ConfigSaveResult,
|
|
10
|
+
type EnsureConfigResult,
|
|
11
|
+
type RtkIntegrationConfig,
|
|
12
|
+
type RtkSourceFilterLevel,
|
|
13
|
+
} from "./types.js";
|
|
14
|
+
|
|
15
|
+
function toBoolean(value: unknown, fallback: boolean): boolean {
|
|
16
|
+
return typeof value === "boolean" ? value : fallback;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function toInteger(value: unknown, fallback: number, min: number, max: number): number {
|
|
20
|
+
if (typeof value !== "number" || !Number.isFinite(value)) {
|
|
21
|
+
return fallback;
|
|
22
|
+
}
|
|
23
|
+
const rounded = Math.round(value);
|
|
24
|
+
return Math.max(min, Math.min(max, rounded));
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function toMode(value: unknown): RtkIntegrationConfig["mode"] {
|
|
28
|
+
return RTK_MODES.includes(value as RtkIntegrationConfig["mode"])
|
|
29
|
+
? (value as RtkIntegrationConfig["mode"])
|
|
30
|
+
: DEFAULT_RTK_INTEGRATION_CONFIG.mode;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function toSourceFilterLevel(value: unknown): RtkSourceFilterLevel {
|
|
34
|
+
return RTK_SOURCE_FILTER_LEVELS.includes(value as RtkSourceFilterLevel)
|
|
35
|
+
? (value as RtkSourceFilterLevel)
|
|
36
|
+
: DEFAULT_RTK_INTEGRATION_CONFIG.outputCompaction.sourceCodeFiltering;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function toObject(value: unknown): Record<string, unknown> {
|
|
40
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
41
|
+
return {};
|
|
42
|
+
}
|
|
43
|
+
return value as Record<string, unknown>;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function normalizeRtkIntegrationConfig(raw: unknown): RtkIntegrationConfig {
|
|
47
|
+
const source = toObject(raw);
|
|
48
|
+
const outputCompactionSource = toObject(source.outputCompaction);
|
|
49
|
+
const truncateSource = toObject(outputCompactionSource.truncate);
|
|
50
|
+
const smartTruncateSource = toObject(outputCompactionSource.smartTruncate);
|
|
51
|
+
|
|
52
|
+
return {
|
|
53
|
+
enabled: toBoolean(source.enabled, DEFAULT_RTK_INTEGRATION_CONFIG.enabled),
|
|
54
|
+
mode: toMode(source.mode),
|
|
55
|
+
guardWhenRtkMissing: toBoolean(
|
|
56
|
+
source.guardWhenRtkMissing,
|
|
57
|
+
DEFAULT_RTK_INTEGRATION_CONFIG.guardWhenRtkMissing,
|
|
58
|
+
),
|
|
59
|
+
showRewriteNotifications: toBoolean(
|
|
60
|
+
source.showRewriteNotifications,
|
|
61
|
+
DEFAULT_RTK_INTEGRATION_CONFIG.showRewriteNotifications,
|
|
62
|
+
),
|
|
63
|
+
rewriteGitGithub: toBoolean(
|
|
64
|
+
source.rewriteGitGithub,
|
|
65
|
+
DEFAULT_RTK_INTEGRATION_CONFIG.rewriteGitGithub,
|
|
66
|
+
),
|
|
67
|
+
rewriteFilesystem: toBoolean(
|
|
68
|
+
source.rewriteFilesystem,
|
|
69
|
+
DEFAULT_RTK_INTEGRATION_CONFIG.rewriteFilesystem,
|
|
70
|
+
),
|
|
71
|
+
rewriteRust: toBoolean(source.rewriteRust, DEFAULT_RTK_INTEGRATION_CONFIG.rewriteRust),
|
|
72
|
+
rewriteJavaScript: toBoolean(
|
|
73
|
+
source.rewriteJavaScript,
|
|
74
|
+
DEFAULT_RTK_INTEGRATION_CONFIG.rewriteJavaScript,
|
|
75
|
+
),
|
|
76
|
+
rewritePython: toBoolean(source.rewritePython, DEFAULT_RTK_INTEGRATION_CONFIG.rewritePython),
|
|
77
|
+
rewriteGo: toBoolean(source.rewriteGo, DEFAULT_RTK_INTEGRATION_CONFIG.rewriteGo),
|
|
78
|
+
rewriteContainers: toBoolean(
|
|
79
|
+
source.rewriteContainers,
|
|
80
|
+
DEFAULT_RTK_INTEGRATION_CONFIG.rewriteContainers,
|
|
81
|
+
),
|
|
82
|
+
rewriteNetwork: toBoolean(source.rewriteNetwork, DEFAULT_RTK_INTEGRATION_CONFIG.rewriteNetwork),
|
|
83
|
+
rewritePackageManagers: toBoolean(
|
|
84
|
+
source.rewritePackageManagers,
|
|
85
|
+
DEFAULT_RTK_INTEGRATION_CONFIG.rewritePackageManagers,
|
|
86
|
+
),
|
|
87
|
+
outputCompaction: {
|
|
88
|
+
enabled: toBoolean(
|
|
89
|
+
outputCompactionSource.enabled,
|
|
90
|
+
DEFAULT_RTK_INTEGRATION_CONFIG.outputCompaction.enabled,
|
|
91
|
+
),
|
|
92
|
+
stripAnsi: toBoolean(
|
|
93
|
+
outputCompactionSource.stripAnsi,
|
|
94
|
+
DEFAULT_RTK_INTEGRATION_CONFIG.outputCompaction.stripAnsi,
|
|
95
|
+
),
|
|
96
|
+
sourceCodeFilteringEnabled: toBoolean(
|
|
97
|
+
outputCompactionSource.sourceCodeFilteringEnabled,
|
|
98
|
+
DEFAULT_RTK_INTEGRATION_CONFIG.outputCompaction.sourceCodeFilteringEnabled,
|
|
99
|
+
),
|
|
100
|
+
truncate: {
|
|
101
|
+
enabled: toBoolean(
|
|
102
|
+
truncateSource.enabled,
|
|
103
|
+
DEFAULT_RTK_INTEGRATION_CONFIG.outputCompaction.truncate.enabled,
|
|
104
|
+
),
|
|
105
|
+
maxChars: toInteger(
|
|
106
|
+
truncateSource.maxChars,
|
|
107
|
+
DEFAULT_RTK_INTEGRATION_CONFIG.outputCompaction.truncate.maxChars,
|
|
108
|
+
1_000,
|
|
109
|
+
200_000,
|
|
110
|
+
),
|
|
111
|
+
},
|
|
112
|
+
sourceCodeFiltering: toSourceFilterLevel(outputCompactionSource.sourceCodeFiltering),
|
|
113
|
+
smartTruncate: {
|
|
114
|
+
enabled: toBoolean(
|
|
115
|
+
smartTruncateSource.enabled,
|
|
116
|
+
DEFAULT_RTK_INTEGRATION_CONFIG.outputCompaction.smartTruncate.enabled,
|
|
117
|
+
),
|
|
118
|
+
maxLines: toInteger(
|
|
119
|
+
smartTruncateSource.maxLines,
|
|
120
|
+
DEFAULT_RTK_INTEGRATION_CONFIG.outputCompaction.smartTruncate.maxLines,
|
|
121
|
+
40,
|
|
122
|
+
4_000,
|
|
123
|
+
),
|
|
124
|
+
},
|
|
125
|
+
aggregateTestOutput: toBoolean(
|
|
126
|
+
outputCompactionSource.aggregateTestOutput,
|
|
127
|
+
DEFAULT_RTK_INTEGRATION_CONFIG.outputCompaction.aggregateTestOutput,
|
|
128
|
+
),
|
|
129
|
+
filterBuildOutput: toBoolean(
|
|
130
|
+
outputCompactionSource.filterBuildOutput,
|
|
131
|
+
DEFAULT_RTK_INTEGRATION_CONFIG.outputCompaction.filterBuildOutput,
|
|
132
|
+
),
|
|
133
|
+
compactGitOutput: toBoolean(
|
|
134
|
+
outputCompactionSource.compactGitOutput,
|
|
135
|
+
DEFAULT_RTK_INTEGRATION_CONFIG.outputCompaction.compactGitOutput,
|
|
136
|
+
),
|
|
137
|
+
aggregateLinterOutput: toBoolean(
|
|
138
|
+
outputCompactionSource.aggregateLinterOutput,
|
|
139
|
+
DEFAULT_RTK_INTEGRATION_CONFIG.outputCompaction.aggregateLinterOutput,
|
|
140
|
+
),
|
|
141
|
+
groupSearchOutput: toBoolean(
|
|
142
|
+
outputCompactionSource.groupSearchOutput,
|
|
143
|
+
DEFAULT_RTK_INTEGRATION_CONFIG.outputCompaction.groupSearchOutput,
|
|
144
|
+
),
|
|
145
|
+
trackSavings: toBoolean(
|
|
146
|
+
outputCompactionSource.trackSavings,
|
|
147
|
+
DEFAULT_RTK_INTEGRATION_CONFIG.outputCompaction.trackSavings,
|
|
148
|
+
),
|
|
149
|
+
},
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
export function ensureConfigExists(): EnsureConfigResult {
|
|
154
|
+
if (existsSync(CONFIG_PATH)) {
|
|
155
|
+
return { created: false };
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
try {
|
|
159
|
+
mkdirSync(dirname(CONFIG_PATH), { recursive: true });
|
|
160
|
+
writeFileSync(CONFIG_PATH, `${JSON.stringify(DEFAULT_RTK_INTEGRATION_CONFIG, null, 2)}\n`, "utf-8");
|
|
161
|
+
return { created: true };
|
|
162
|
+
} catch (error) {
|
|
163
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
164
|
+
return {
|
|
165
|
+
created: false,
|
|
166
|
+
error: `Failed to create ${CONFIG_PATH}: ${message}`,
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
export function loadRtkIntegrationConfig(): ConfigLoadResult {
|
|
172
|
+
if (!existsSync(CONFIG_PATH)) {
|
|
173
|
+
return { config: { ...DEFAULT_RTK_INTEGRATION_CONFIG } };
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
try {
|
|
177
|
+
const rawText = readFileSync(CONFIG_PATH, "utf-8");
|
|
178
|
+
const parsed = JSON.parse(rawText) as unknown;
|
|
179
|
+
return { config: normalizeRtkIntegrationConfig(parsed) };
|
|
180
|
+
} catch (error) {
|
|
181
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
182
|
+
return {
|
|
183
|
+
config: { ...DEFAULT_RTK_INTEGRATION_CONFIG },
|
|
184
|
+
warning: `Failed to parse ${CONFIG_PATH}: ${message}`,
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
export function saveRtkIntegrationConfig(config: RtkIntegrationConfig): ConfigSaveResult {
|
|
190
|
+
const normalized = normalizeRtkIntegrationConfig(config);
|
|
191
|
+
const tmpPath = `${CONFIG_PATH}.tmp`;
|
|
192
|
+
|
|
193
|
+
try {
|
|
194
|
+
mkdirSync(dirname(CONFIG_PATH), { recursive: true });
|
|
195
|
+
writeFileSync(tmpPath, `${JSON.stringify(normalized, null, 2)}\n`, "utf-8");
|
|
196
|
+
renameSync(tmpPath, CONFIG_PATH);
|
|
197
|
+
return { success: true };
|
|
198
|
+
} catch (error) {
|
|
199
|
+
try {
|
|
200
|
+
if (existsSync(tmpPath)) {
|
|
201
|
+
unlinkSync(tmpPath);
|
|
202
|
+
}
|
|
203
|
+
} catch {
|
|
204
|
+
// Ignore cleanup failures.
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
208
|
+
return {
|
|
209
|
+
success: false,
|
|
210
|
+
error: `Failed to save ${CONFIG_PATH}: ${message}`,
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
export function getRtkIntegrationConfigPath(): string {
|
|
216
|
+
return CONFIG_PATH;
|
|
217
|
+
}
|
package/src/constants.ts
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { homedir } from "node:os";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
|
|
4
|
+
export const EXTENSION_NAME = "pi-rtk-optimizer";
|
|
5
|
+
export const CONFIG_DIR = join(homedir(), ".pi", "agent", "extensions", EXTENSION_NAME);
|
|
6
|
+
export const CONFIG_PATH = join(CONFIG_DIR, "config.json");
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
import { isToolCallEventType, type ExtensionAPI, type ExtensionCommandContext, type ExtensionContext } from "@mariozechner/pi-coding-agent";
|
|
2
|
+
import {
|
|
3
|
+
ensureConfigExists,
|
|
4
|
+
getRtkIntegrationConfigPath,
|
|
5
|
+
loadRtkIntegrationConfig,
|
|
6
|
+
normalizeRtkIntegrationConfig,
|
|
7
|
+
saveRtkIntegrationConfig,
|
|
8
|
+
} from "./config-store.js";
|
|
9
|
+
import { computeRewriteDecision } from "./command-rewriter.js";
|
|
10
|
+
import { registerRtkIntegrationCommand } from "./config-modal.js";
|
|
11
|
+
import { EXTENSION_NAME } from "./constants.js";
|
|
12
|
+
import { clearOutputMetrics, getOutputMetricsSummary } from "./output-metrics.js";
|
|
13
|
+
import { compactToolResult, type ToolResultCompactionMetadata } from "./output-compactor.js";
|
|
14
|
+
import type { RtkIntegrationConfig, RuntimeStatus } from "./types.js";
|
|
15
|
+
import { applyWindowsBashCompatibilityFixes } from "./windows-command-helpers.js";
|
|
16
|
+
|
|
17
|
+
function trimMessage(raw: string, maxLength = 220): string {
|
|
18
|
+
const clean = raw.replace(/\s+/g, " ").trim();
|
|
19
|
+
if (clean.length <= maxLength) {
|
|
20
|
+
return clean;
|
|
21
|
+
}
|
|
22
|
+
return `${clean.slice(0, maxLength - 1)}…`;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const SOURCE_FILTER_TROUBLESHOOTING_NOTE =
|
|
26
|
+
"RTK note: If file edits repeatedly fail because old text does not match, run '/rtk', turn off 'Read source filtering enabled', re-read the file, apply the edit, then turn it back on.";
|
|
27
|
+
|
|
28
|
+
function toRecord(value: unknown): Record<string, unknown> {
|
|
29
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
30
|
+
return {};
|
|
31
|
+
}
|
|
32
|
+
return value as Record<string, unknown>;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function mergeCompactionDetails(
|
|
36
|
+
existingDetails: unknown,
|
|
37
|
+
compaction: ToolResultCompactionMetadata,
|
|
38
|
+
): Record<string, unknown> {
|
|
39
|
+
const baseDetails = toRecord(existingDetails);
|
|
40
|
+
const baseMetadata = toRecord(baseDetails.metadata);
|
|
41
|
+
|
|
42
|
+
const nextDetails: Record<string, unknown> = {
|
|
43
|
+
...baseDetails,
|
|
44
|
+
rtkCompaction: compaction,
|
|
45
|
+
metadata: {
|
|
46
|
+
...baseMetadata,
|
|
47
|
+
rtkCompaction: compaction,
|
|
48
|
+
},
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
if (Object.keys(baseDetails).length === 0 && existingDetails !== undefined) {
|
|
52
|
+
nextDetails.rawDetails = existingDetails;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return nextDetails;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export default function rtkIntegrationExtension(pi: ExtensionAPI): void {
|
|
59
|
+
const initialLoad = loadRtkIntegrationConfig();
|
|
60
|
+
let config: RtkIntegrationConfig = initialLoad.config;
|
|
61
|
+
let pendingLoadWarning = initialLoad.warning;
|
|
62
|
+
let runtimeStatus: RuntimeStatus = { rtkAvailable: false };
|
|
63
|
+
const warnedMessages = new Set<string>();
|
|
64
|
+
const suggestionNotices = new Set<string>();
|
|
65
|
+
let missingRtkWarningShown = false;
|
|
66
|
+
|
|
67
|
+
const formatRewriteNotice = (originalCommand: string, rewrittenCommand: string): string => {
|
|
68
|
+
const original = trimMessage(originalCommand, 100);
|
|
69
|
+
const rewritten = trimMessage(rewrittenCommand, 120);
|
|
70
|
+
return `RTK rewrite: ${original} -> ${rewritten}`;
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
const warnOnce = (
|
|
74
|
+
ctx: ExtensionContext | ExtensionCommandContext,
|
|
75
|
+
message: string,
|
|
76
|
+
level: "warning" | "error" = "warning",
|
|
77
|
+
): void => {
|
|
78
|
+
if (warnedMessages.has(message)) {
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
warnedMessages.add(message);
|
|
83
|
+
console.warn(`[${EXTENSION_NAME}] ${message}`);
|
|
84
|
+
if (ctx.hasUI) {
|
|
85
|
+
ctx.ui.notify(message, level);
|
|
86
|
+
}
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
const refreshConfig = (ctx?: ExtensionContext | ExtensionCommandContext): void => {
|
|
90
|
+
const ensured = ensureConfigExists();
|
|
91
|
+
if (ensured.error && ctx) {
|
|
92
|
+
warnOnce(ctx, ensured.error);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const loaded = loadRtkIntegrationConfig();
|
|
96
|
+
config = loaded.config;
|
|
97
|
+
pendingLoadWarning = loaded.warning;
|
|
98
|
+
|
|
99
|
+
if (pendingLoadWarning && ctx) {
|
|
100
|
+
warnOnce(ctx, pendingLoadWarning);
|
|
101
|
+
pendingLoadWarning = undefined;
|
|
102
|
+
}
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
const setConfig = (next: RtkIntegrationConfig, ctx: ExtensionCommandContext): void => {
|
|
106
|
+
config = normalizeRtkIntegrationConfig(next);
|
|
107
|
+
const saved = saveRtkIntegrationConfig(config);
|
|
108
|
+
if (!saved.success && saved.error) {
|
|
109
|
+
ctx.ui.notify(saved.error, "error");
|
|
110
|
+
}
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
const refreshRuntimeStatus = async (): Promise<RuntimeStatus> => {
|
|
114
|
+
try {
|
|
115
|
+
const result = await pi.exec("rtk", ["--version"], { timeout: 5000 });
|
|
116
|
+
if (result.code === 0) {
|
|
117
|
+
runtimeStatus = {
|
|
118
|
+
rtkAvailable: true,
|
|
119
|
+
lastCheckedAt: Date.now(),
|
|
120
|
+
};
|
|
121
|
+
missingRtkWarningShown = false;
|
|
122
|
+
return runtimeStatus;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const detail = trimMessage(
|
|
126
|
+
`${result.stderr || ""} ${result.stdout || ""} ${result.code ? `(exit ${result.code})` : ""}`,
|
|
127
|
+
);
|
|
128
|
+
runtimeStatus = {
|
|
129
|
+
rtkAvailable: false,
|
|
130
|
+
lastCheckedAt: Date.now(),
|
|
131
|
+
lastError: detail || `exit ${result.code}`,
|
|
132
|
+
};
|
|
133
|
+
return runtimeStatus;
|
|
134
|
+
} catch (error) {
|
|
135
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
136
|
+
runtimeStatus = {
|
|
137
|
+
rtkAvailable: false,
|
|
138
|
+
lastCheckedAt: Date.now(),
|
|
139
|
+
lastError: trimMessage(message),
|
|
140
|
+
};
|
|
141
|
+
return runtimeStatus;
|
|
142
|
+
}
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
const maybeWarnRtkMissing = (ctx: ExtensionContext): void => {
|
|
146
|
+
if (!config.enabled || config.mode !== "rewrite" || !config.guardWhenRtkMissing) {
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
if (runtimeStatus.rtkAvailable) {
|
|
151
|
+
missingRtkWarningShown = false;
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
if (missingRtkWarningShown) {
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
missingRtkWarningShown = true;
|
|
160
|
+
const reason = runtimeStatus.lastError ? ` (${runtimeStatus.lastError})` : "";
|
|
161
|
+
warnOnce(ctx, `${EXTENSION_NAME}: rtk binary unavailable, command rewrite bypassed${reason}.`);
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
const ensureRuntimeStatusFresh = async (): Promise<void> => {
|
|
165
|
+
if (!config.guardWhenRtkMissing) {
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const now = Date.now();
|
|
170
|
+
const isStale = !runtimeStatus.lastCheckedAt || now - runtimeStatus.lastCheckedAt > 30_000;
|
|
171
|
+
if (isStale) {
|
|
172
|
+
await refreshRuntimeStatus();
|
|
173
|
+
}
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
const controller = {
|
|
177
|
+
getConfig: () => config,
|
|
178
|
+
setConfig,
|
|
179
|
+
getConfigPath: getRtkIntegrationConfigPath,
|
|
180
|
+
getRuntimeStatus: () => runtimeStatus,
|
|
181
|
+
refreshRuntimeStatus,
|
|
182
|
+
getMetricsSummary: getOutputMetricsSummary,
|
|
183
|
+
clearMetrics: clearOutputMetrics,
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
registerRtkIntegrationCommand(pi, controller);
|
|
187
|
+
|
|
188
|
+
pi.on("session_start", async (_event, ctx) => {
|
|
189
|
+
refreshConfig(ctx);
|
|
190
|
+
await refreshRuntimeStatus();
|
|
191
|
+
maybeWarnRtkMissing(ctx);
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
pi.on("session_switch", async (_event, ctx) => {
|
|
195
|
+
refreshConfig(ctx);
|
|
196
|
+
await refreshRuntimeStatus();
|
|
197
|
+
maybeWarnRtkMissing(ctx);
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
pi.on("before_agent_start", async (event, ctx) => {
|
|
201
|
+
await ensureRuntimeStatusFresh();
|
|
202
|
+
maybeWarnRtkMissing(ctx);
|
|
203
|
+
|
|
204
|
+
if (event.systemPrompt.includes(SOURCE_FILTER_TROUBLESHOOTING_NOTE)) {
|
|
205
|
+
return {};
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
return {
|
|
209
|
+
systemPrompt: `${event.systemPrompt}\n\n${SOURCE_FILTER_TROUBLESHOOTING_NOTE}`,
|
|
210
|
+
};
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
pi.on("tool_call", async (event, ctx) => {
|
|
214
|
+
if (!config.enabled) {
|
|
215
|
+
return {};
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
if (!isToolCallEventType("bash", event)) {
|
|
219
|
+
return {};
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
if (config.mode === "rewrite") {
|
|
223
|
+
const compatibility = applyWindowsBashCompatibilityFixes(event.input.command);
|
|
224
|
+
if (compatibility.command !== event.input.command) {
|
|
225
|
+
event.input.command = compatibility.command;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
await ensureRuntimeStatusFresh();
|
|
230
|
+
if (config.guardWhenRtkMissing && !runtimeStatus.rtkAvailable) {
|
|
231
|
+
return {};
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
const decision = computeRewriteDecision(event.input.command, config);
|
|
235
|
+
if (!decision.changed || !decision.rule) {
|
|
236
|
+
return {};
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
if (config.mode === "rewrite") {
|
|
240
|
+
if (config.showRewriteNotifications && ctx.hasUI) {
|
|
241
|
+
ctx.ui.notify(formatRewriteNotice(decision.originalCommand, decision.rewrittenCommand), "info");
|
|
242
|
+
}
|
|
243
|
+
event.input.command = decision.rewrittenCommand;
|
|
244
|
+
return {};
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
if (config.mode === "suggest") {
|
|
248
|
+
const suggestionKey = `${decision.rule.id}:${decision.rewrittenCommand}`;
|
|
249
|
+
if (!suggestionNotices.has(suggestionKey)) {
|
|
250
|
+
suggestionNotices.add(suggestionKey);
|
|
251
|
+
if (ctx.hasUI) {
|
|
252
|
+
ctx.ui.notify(`RTK suggestion: ${decision.rewrittenCommand}`, "info");
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
return {};
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
pi.on("tool_result", async (event, ctx) => {
|
|
261
|
+
if (!config.enabled || !config.outputCompaction.enabled) {
|
|
262
|
+
return {};
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
try {
|
|
266
|
+
const outcome = compactToolResult(
|
|
267
|
+
{
|
|
268
|
+
toolName: event.toolName,
|
|
269
|
+
input: event.input,
|
|
270
|
+
content: event.content,
|
|
271
|
+
},
|
|
272
|
+
config,
|
|
273
|
+
);
|
|
274
|
+
|
|
275
|
+
if (!outcome.changed || !outcome.content) {
|
|
276
|
+
return {};
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
return {
|
|
280
|
+
content: outcome.content,
|
|
281
|
+
details: outcome.metadata
|
|
282
|
+
? mergeCompactionDetails((event as Record<string, unknown>).details, outcome.metadata)
|
|
283
|
+
: undefined,
|
|
284
|
+
};
|
|
285
|
+
} catch (error) {
|
|
286
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
287
|
+
warnOnce(ctx, `${EXTENSION_NAME}: output compaction failed, using raw output (${trimMessage(message)}).`);
|
|
288
|
+
return {};
|
|
289
|
+
}
|
|
290
|
+
});
|
|
291
|
+
}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
|
|
3
|
+
import { compactToolResult } from "./output-compactor.ts";
|
|
4
|
+
import { DEFAULT_RTK_INTEGRATION_CONFIG, type RtkIntegrationConfig } from "./types.ts";
|
|
5
|
+
|
|
6
|
+
function runTest(name: string, testFn: () => void): void {
|
|
7
|
+
testFn();
|
|
8
|
+
console.log(`[PASS] ${name}`);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function cloneConfig(): RtkIntegrationConfig {
|
|
12
|
+
return structuredClone(DEFAULT_RTK_INTEGRATION_CONFIG);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function buildReadContent(lineCount: number): string {
|
|
16
|
+
const lines: string[] = [];
|
|
17
|
+
for (let index = 0; index < lineCount; index += 1) {
|
|
18
|
+
if (index % 2 === 0) {
|
|
19
|
+
lines.push(`// comment ${index}`);
|
|
20
|
+
} else {
|
|
21
|
+
lines.push(`const value${index} = ${index};`);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
return `${lines.join("\n")}\n`;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function firstTextBlock(content: unknown[] | undefined): string {
|
|
28
|
+
if (!Array.isArray(content) || content.length === 0) {
|
|
29
|
+
return "";
|
|
30
|
+
}
|
|
31
|
+
const first = content[0] as { type?: string; text?: string };
|
|
32
|
+
if (first?.type !== "text" || typeof first.text !== "string") {
|
|
33
|
+
return "";
|
|
34
|
+
}
|
|
35
|
+
return first.text;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
runTest("precision read with offset keeps exact output (no source/smart/hard truncation)", () => {
|
|
39
|
+
const config = cloneConfig();
|
|
40
|
+
config.outputCompaction.truncate.enabled = true;
|
|
41
|
+
config.outputCompaction.truncate.maxChars = 500;
|
|
42
|
+
config.outputCompaction.smartTruncate.enabled = true;
|
|
43
|
+
config.outputCompaction.smartTruncate.maxLines = 40;
|
|
44
|
+
|
|
45
|
+
const content = buildReadContent(220);
|
|
46
|
+
const result = compactToolResult(
|
|
47
|
+
{
|
|
48
|
+
toolName: "read",
|
|
49
|
+
input: { path: "sample.ts", offset: 1 },
|
|
50
|
+
content: [{ type: "text", text: content }],
|
|
51
|
+
},
|
|
52
|
+
config,
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
assert.equal(result.changed, false);
|
|
56
|
+
assert.deepEqual(result.techniques, []);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
runTest("precision read with limit keeps exact output", () => {
|
|
60
|
+
const config = cloneConfig();
|
|
61
|
+
config.outputCompaction.truncate.enabled = true;
|
|
62
|
+
config.outputCompaction.truncate.maxChars = 500;
|
|
63
|
+
config.outputCompaction.smartTruncate.enabled = true;
|
|
64
|
+
config.outputCompaction.smartTruncate.maxLines = 40;
|
|
65
|
+
|
|
66
|
+
const content = buildReadContent(220);
|
|
67
|
+
const result = compactToolResult(
|
|
68
|
+
{
|
|
69
|
+
toolName: "read",
|
|
70
|
+
input: { path: "sample.ts", limit: 200 },
|
|
71
|
+
content: [{ type: "text", text: content }],
|
|
72
|
+
},
|
|
73
|
+
config,
|
|
74
|
+
);
|
|
75
|
+
|
|
76
|
+
assert.equal(result.changed, false);
|
|
77
|
+
assert.deepEqual(result.techniques, []);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
runTest("normal read compacts and adds banner", () => {
|
|
81
|
+
const config = cloneConfig();
|
|
82
|
+
config.outputCompaction.smartTruncate.enabled = true;
|
|
83
|
+
config.outputCompaction.smartTruncate.maxLines = 40;
|
|
84
|
+
|
|
85
|
+
const content = buildReadContent(220);
|
|
86
|
+
const result = compactToolResult(
|
|
87
|
+
{
|
|
88
|
+
toolName: "read",
|
|
89
|
+
input: { path: "sample.ts" },
|
|
90
|
+
content: [{ type: "text", text: content }],
|
|
91
|
+
},
|
|
92
|
+
config,
|
|
93
|
+
);
|
|
94
|
+
|
|
95
|
+
assert.equal(result.changed, true);
|
|
96
|
+
assert.ok(result.techniques.includes("source:minimal"));
|
|
97
|
+
|
|
98
|
+
const compacted = firstTextBlock(result.content);
|
|
99
|
+
assert.ok(compacted.startsWith("[RTK compacted output:"));
|
|
100
|
+
assert.ok(compacted.includes("source:minimal"));
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
runTest("short read output stays exact below threshold", () => {
|
|
104
|
+
const config = cloneConfig();
|
|
105
|
+
const content = buildReadContent(40);
|
|
106
|
+
|
|
107
|
+
const result = compactToolResult(
|
|
108
|
+
{
|
|
109
|
+
toolName: "read",
|
|
110
|
+
input: { path: "sample.ts" },
|
|
111
|
+
content: [{ type: "text", text: content }],
|
|
112
|
+
},
|
|
113
|
+
config,
|
|
114
|
+
);
|
|
115
|
+
|
|
116
|
+
assert.equal(result.changed, false);
|
|
117
|
+
assert.deepEqual(result.techniques, []);
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
console.log("All output-compactor tests passed.");
|