pi-rtk-optimizer 0.6.0 → 0.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,4 +1,5 @@
1
1
  import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
2
+ import { resolveRtkExecutable, type RtkExecutableResolution } from "./rtk-executable-resolver.js";
2
3
 
3
4
  export interface RtkRewriteProviderResult {
4
5
  changed: boolean;
@@ -6,6 +7,14 @@ export interface RtkRewriteProviderResult {
6
7
  rewrittenCommand: string;
7
8
  exitCode: number;
8
9
  error?: string;
10
+ executableResolution?: RtkExecutableResolution;
11
+ }
12
+
13
+ export interface RtkRewriteProviderOptions {
14
+ timeoutMs?: number;
15
+ resolverTimeoutMs?: number;
16
+ platform?: typeof process.platform;
17
+ executableResolution?: RtkExecutableResolution;
9
18
  }
10
19
 
11
20
  function isAlreadyRtk(command: string): boolean {
@@ -13,11 +22,21 @@ function isAlreadyRtk(command: string): boolean {
13
22
  return trimmed === "rtk" || trimmed.startsWith("rtk ");
14
23
  }
15
24
 
25
+ function normalizeOptions(optionsOrTimeout: number | RtkRewriteProviderOptions): RtkRewriteProviderOptions {
26
+ if (typeof optionsOrTimeout === "number") {
27
+ return { timeoutMs: optionsOrTimeout };
28
+ }
29
+ return optionsOrTimeout;
30
+ }
31
+
16
32
  export async function resolveRtkRewrite(
17
33
  pi: ExtensionAPI,
18
34
  command: string,
19
- timeoutMs = 3000,
35
+ optionsOrTimeout: number | RtkRewriteProviderOptions = {},
20
36
  ): Promise<RtkRewriteProviderResult> {
37
+ const options = normalizeOptions(optionsOrTimeout);
38
+ const timeoutMs = options.timeoutMs ?? 3000;
39
+
21
40
  if (!command || !command.trim()) {
22
41
  return { changed: false, originalCommand: command, rewrittenCommand: command, exitCode: 1 };
23
42
  }
@@ -27,10 +46,22 @@ export async function resolveRtkRewrite(
27
46
  }
28
47
 
29
48
  try {
30
- const result = await pi.exec("rtk", ["rewrite", command], { timeout: timeoutMs });
49
+ const executableResolution =
50
+ options.executableResolution ??
51
+ (await resolveRtkExecutable(pi, {
52
+ platform: options.platform,
53
+ timeoutMs: options.resolverTimeoutMs,
54
+ }));
55
+ const result = await pi.exec(executableResolution.command, ["rewrite", command], { timeout: timeoutMs });
31
56
 
32
57
  if (result.code === 1) {
33
- return { changed: false, originalCommand: command, rewrittenCommand: command, exitCode: 1 };
58
+ return {
59
+ changed: false,
60
+ originalCommand: command,
61
+ rewrittenCommand: command,
62
+ exitCode: 1,
63
+ executableResolution,
64
+ };
34
65
  }
35
66
 
36
67
  if (result.code === 2) {
@@ -40,6 +71,7 @@ export async function resolveRtkRewrite(
40
71
  rewrittenCommand: command,
41
72
  exitCode: 2,
42
73
  error: result.stderr?.trim() || "rtk denied rewrite",
74
+ executableResolution,
43
75
  };
44
76
  }
45
77
 
@@ -52,6 +84,7 @@ export async function resolveRtkRewrite(
52
84
  rewrittenCommand: command,
53
85
  exitCode: result.code,
54
86
  error: "rtk returned empty output",
87
+ executableResolution,
55
88
  };
56
89
  }
57
90
  if (rewritten === command) {
@@ -60,6 +93,7 @@ export async function resolveRtkRewrite(
60
93
  originalCommand: command,
61
94
  rewrittenCommand: command,
62
95
  exitCode: result.code,
96
+ executableResolution,
63
97
  };
64
98
  }
65
99
  return {
@@ -67,6 +101,7 @@ export async function resolveRtkRewrite(
67
101
  originalCommand: command,
68
102
  rewrittenCommand: rewritten,
69
103
  exitCode: result.code,
104
+ executableResolution,
70
105
  };
71
106
  }
72
107
 
@@ -76,6 +111,7 @@ export async function resolveRtkRewrite(
76
111
  rewrittenCommand: command,
77
112
  exitCode: result.code,
78
113
  error: `unexpected exit code ${result.code}`,
114
+ executableResolution,
79
115
  };
80
116
  } catch (error) {
81
117
  const message = error instanceof Error ? error.message : String(error);
@@ -1,4 +1,8 @@
1
- const LEADING_ENV_ASSIGNMENT_PATTERN = /^((?:[A-Za-z_][A-Za-z0-9_]*=(?:"[^"]*"|'[^']*'|[^\s]+)\s+)*)/;
1
+ const SINGLE_QUOTED_SHELL_VALUE_PATTERN = "'(?:'\\\\''|[^'])*'";
2
+ const ENV_ASSIGNMENT_VALUE_PATTERN = `(?:"[^"]*"|${SINGLE_QUOTED_SHELL_VALUE_PATTERN}|[^\\s]+)`;
3
+ const LEADING_ENV_ASSIGNMENT_PATTERN = new RegExp(
4
+ `^((?:[A-Za-z_][A-Za-z0-9_]*=${ENV_ASSIGNMENT_VALUE_PATTERN}\\s+)*)`,
5
+ );
2
6
 
3
7
  export interface LeadingEnvAssignmentSplit {
4
8
  envPrefix: string;
@@ -1,10 +1,23 @@
1
- import { DEFAULT_RTK_INTEGRATION_CONFIG, type RtkIntegrationConfig } from "./types.ts";
2
-
3
- export function runTest(name: string, testFn: () => void): void {
4
- testFn();
5
- console.log(`[PASS] ${name}`);
6
- }
7
-
8
- export function cloneDefaultConfig(): RtkIntegrationConfig {
9
- return structuredClone(DEFAULT_RTK_INTEGRATION_CONFIG);
10
- }
1
+ import { DEFAULT_RTK_INTEGRATION_CONFIG, type RtkIntegrationConfig } from "./types.ts";
2
+
3
+ type TestResult = void | Promise<void>;
4
+
5
+ function isPromiseLike(value: TestResult): value is Promise<void> {
6
+ return Boolean(value && typeof (value as Promise<void>).then === "function");
7
+ }
8
+
9
+ export function runTest(name: string, testFn: () => TestResult): TestResult {
10
+ const result = testFn();
11
+ if (!isPromiseLike(result)) {
12
+ console.log(`[PASS] ${name}`);
13
+ return;
14
+ }
15
+
16
+ return result.then(() => {
17
+ console.log(`[PASS] ${name}`);
18
+ });
19
+ }
20
+
21
+ export function cloneDefaultConfig(): RtkIntegrationConfig {
22
+ return structuredClone(DEFAULT_RTK_INTEGRATION_CONFIG);
23
+ }
@@ -1,69 +1,80 @@
1
- import { toRecord } from "./record-utils.js";
2
- import { sanitizeRtkEmojiOutput, stripAnsiFast, stripRtkHookWarnings } from "./techniques/index.js";
3
-
4
- interface ToolResultTextBlock {
5
- type: string;
6
- text?: string;
7
- [key: string]: unknown;
8
- }
9
-
10
- function sanitizeStreamingBashText(text: string, command: string | undefined | null): string {
11
- let nextText = stripAnsiFast(text);
12
-
13
- const withoutRtkHookWarnings = stripRtkHookWarnings(nextText, command);
14
- if (withoutRtkHookWarnings !== null) {
15
- nextText = withoutRtkHookWarnings;
16
- }
17
-
18
- const withoutRtkEmoji = sanitizeRtkEmojiOutput(nextText, command);
19
- if (withoutRtkEmoji !== null) {
20
- nextText = withoutRtkEmoji;
21
- }
22
-
23
- return nextText;
24
- }
25
-
26
- /**
27
- * Sanitizes streamed bash result blocks before the TUI renders them so RTK
28
- * self-diagnostics never flash in partial or final tool output.
29
- */
30
- export function sanitizeStreamingBashExecutionResult(
31
- result: unknown,
32
- command: string | undefined | null,
33
- ): boolean {
34
- const resultRecord = toRecord(result);
35
- const sourceContent = Array.isArray(resultRecord.content) ? resultRecord.content : null;
36
- if (!sourceContent || sourceContent.length === 0) {
37
- return false;
38
- }
39
-
40
- let changed = false;
41
- const nextContent = sourceContent.map((block) => {
42
- if (!block || typeof block !== "object" || Array.isArray(block)) {
43
- return block;
44
- }
45
-
46
- const contentBlock = block as ToolResultTextBlock;
47
- if (contentBlock.type !== "text" || typeof contentBlock.text !== "string") {
48
- return block;
49
- }
50
-
51
- const sanitizedText = sanitizeStreamingBashText(contentBlock.text, command);
52
- if (sanitizedText === contentBlock.text) {
53
- return block;
54
- }
55
-
56
- changed = true;
57
- return {
58
- ...contentBlock,
59
- text: sanitizedText,
60
- };
61
- });
62
-
63
- if (!changed) {
64
- return false;
65
- }
66
-
67
- resultRecord.content = nextContent;
68
- return true;
69
- }
1
+ import { toRecord } from "./record-utils.js";
2
+ import { sanitizeRtkEmojiOutput, stripAnsiFast, stripRtkHookWarnings } from "./techniques/index.js";
3
+
4
+ interface ToolResultTextBlock {
5
+ type: string;
6
+ text?: string;
7
+ [key: string]: unknown;
8
+ }
9
+
10
+ export interface StreamingBashExecutionSanitizationResult {
11
+ changed: boolean;
12
+ result: unknown;
13
+ }
14
+
15
+ function sanitizeStreamingBashText(text: string, command: string | undefined | null): string {
16
+ let nextText = stripAnsiFast(text);
17
+
18
+ const withoutRtkHookWarnings = stripRtkHookWarnings(nextText, command);
19
+ if (withoutRtkHookWarnings !== null) {
20
+ nextText = withoutRtkHookWarnings;
21
+ }
22
+
23
+ const withoutRtkEmoji = sanitizeRtkEmojiOutput(nextText, command);
24
+ if (withoutRtkEmoji !== null) {
25
+ nextText = withoutRtkEmoji;
26
+ }
27
+
28
+ return nextText;
29
+ }
30
+
31
+ /**
32
+ * Returns a sanitized shallow copy of streamed bash result blocks before the
33
+ * TUI renders them so RTK self-diagnostics never flash in partial or final
34
+ * tool output. The input object is not mutated.
35
+ */
36
+ export function sanitizeStreamingBashExecutionResult(
37
+ result: unknown,
38
+ command: string | undefined | null,
39
+ ): StreamingBashExecutionSanitizationResult {
40
+ const resultRecord = toRecord(result);
41
+ const sourceContent = Array.isArray(resultRecord.content) ? resultRecord.content : null;
42
+ if (!sourceContent || sourceContent.length === 0) {
43
+ return { changed: false, result };
44
+ }
45
+
46
+ let changed = false;
47
+ const nextContent = sourceContent.map((block) => {
48
+ if (!block || typeof block !== "object" || Array.isArray(block)) {
49
+ return block;
50
+ }
51
+
52
+ const contentBlock = block as ToolResultTextBlock;
53
+ if (contentBlock.type !== "text" || typeof contentBlock.text !== "string") {
54
+ return block;
55
+ }
56
+
57
+ const sanitizedText = sanitizeStreamingBashText(contentBlock.text, command);
58
+ if (sanitizedText === contentBlock.text) {
59
+ return block;
60
+ }
61
+
62
+ changed = true;
63
+ return {
64
+ ...contentBlock,
65
+ text: sanitizedText,
66
+ };
67
+ });
68
+
69
+ if (!changed) {
70
+ return { changed: false, result };
71
+ }
72
+
73
+ return {
74
+ changed: true,
75
+ result: {
76
+ ...resultRecord,
77
+ content: nextContent,
78
+ },
79
+ };
80
+ }
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,
@@ -81,5 +87,9 @@ export interface RuntimeStatus {
81
87
  rtkAvailable: boolean;
82
88
  lastCheckedAt?: number;
83
89
  lastError?: string;
90
+ rtkExecutablePath?: string;
91
+ rtkExecutableCommand?: string;
92
+ rtkExecutableResolver?: string;
93
+ rtkExecutableResolutionWarning?: string;
84
94
  }
85
95
 
@@ -3,6 +3,12 @@ interface WindowsBashCompatibilityResult {
3
3
  applied: string[];
4
4
  }
5
5
 
6
+ interface LeadingCdSlashDParse {
7
+ rawPath: string;
8
+ operator: string;
9
+ tail: string;
10
+ }
11
+
6
12
  const PYTHON_UTF8_ENV_PREFIX = "PYTHONIOENCODING=utf-8";
7
13
 
8
14
  function normalizeWindowsPathForBash(rawPath: string): string {
@@ -20,29 +26,96 @@ function quoteForBash(value: string): string {
20
26
  return `"${escaped}"`;
21
27
  }
22
28
 
29
+ function parseLeadingCdSlashD(command: string): LeadingCdSlashDParse | null {
30
+ const prefixMatch = command.match(/^\s*cd\s+\/d\s+/i);
31
+ if (!prefixMatch) {
32
+ return null;
33
+ }
34
+
35
+ const pathStart = prefixMatch[0].length;
36
+ let quote: '"' | "'" | null = null;
37
+ let escaped = false;
38
+
39
+ for (let index = pathStart; index < command.length; index += 1) {
40
+ const character = command[index] ?? "";
41
+ const nextCharacter = command[index + 1] ?? "";
42
+
43
+ if (escaped) {
44
+ escaped = false;
45
+ continue;
46
+ }
47
+
48
+ if (quote !== null) {
49
+ if (character === "\\" && quote !== "'") {
50
+ escaped = true;
51
+ continue;
52
+ }
53
+ if (character === quote) {
54
+ quote = null;
55
+ }
56
+ continue;
57
+ }
58
+
59
+ if (character === "\\") {
60
+ escaped = true;
61
+ continue;
62
+ }
63
+
64
+ if (character === '"' || character === "'") {
65
+ quote = character;
66
+ continue;
67
+ }
68
+
69
+ if (character === "&" && nextCharacter === "&") {
70
+ return {
71
+ rawPath: command.slice(pathStart, index),
72
+ operator: "&&",
73
+ tail: command.slice(index + 2),
74
+ };
75
+ }
76
+
77
+ if (character === "|" && nextCharacter === "|") {
78
+ return {
79
+ rawPath: command.slice(pathStart, index),
80
+ operator: "||",
81
+ tail: command.slice(index + 2),
82
+ };
83
+ }
84
+
85
+ if (character === "|" || character === ";") {
86
+ return {
87
+ rawPath: command.slice(pathStart, index),
88
+ operator: character,
89
+ tail: command.slice(index + 1),
90
+ };
91
+ }
92
+ }
93
+
94
+ return {
95
+ rawPath: command.slice(pathStart),
96
+ operator: "",
97
+ tail: "",
98
+ };
99
+ }
100
+
23
101
  function rewriteLeadingCdSlashD(command: string): { command: string; changed: boolean } {
24
- const withTailMatch = command.match(/^\s*cd\s+\/d\s+(.+?)\s*&&\s*([\s\S]+)$/i);
25
- if (withTailMatch) {
26
- const rawPath = withTailMatch[1] ?? "";
27
- const tail = withTailMatch[2] ?? "";
28
- const normalizedPath = quoteForBash(normalizeWindowsPathForBash(rawPath));
29
- return {
30
- command: `cd ${normalizedPath} && ${tail}`,
31
- changed: true,
32
- };
102
+ const parsed = parseLeadingCdSlashD(command);
103
+ if (!parsed) {
104
+ return { command, changed: false };
33
105
  }
34
106
 
35
- const onlyCdMatch = command.match(/^\s*cd\s+\/d\s+(.+)$/i);
36
- if (onlyCdMatch) {
37
- const rawPath = onlyCdMatch[1] ?? "";
38
- const normalizedPath = quoteForBash(normalizeWindowsPathForBash(rawPath));
107
+ const normalizedPath = quoteForBash(normalizeWindowsPathForBash(parsed.rawPath));
108
+ if (!parsed.operator) {
39
109
  return {
40
110
  command: `cd ${normalizedPath}`,
41
111
  changed: true,
42
112
  };
43
113
  }
44
114
 
45
- return { command, changed: false };
115
+ return {
116
+ command: `cd ${normalizedPath} ${parsed.operator} ${parsed.tail.trimStart()}`,
117
+ changed: true,
118
+ };
46
119
  }
47
120
 
48
121
  function ensurePythonUtf8(command: string): { command: string; changed: boolean } {
@@ -60,8 +133,11 @@ function ensurePythonUtf8(command: string): { command: string; changed: boolean
60
133
  };
61
134
  }
62
135
 
63
- export function applyWindowsBashCompatibilityFixes(command: string): WindowsBashCompatibilityResult {
64
- if (process.platform !== "win32") {
136
+ export function applyWindowsBashCompatibilityFixes(
137
+ command: string,
138
+ platform: string = process.platform,
139
+ ): WindowsBashCompatibilityResult {
140
+ if (platform !== "win32") {
65
141
  return { command, applied: [] };
66
142
  }
67
143
 
@@ -831,7 +831,7 @@ export class ZellijSettingsModal implements ZellijModalContentRenderer {
831
831
  options.settings,
832
832
  Math.min(Math.max(options.settings.length + 2, 6), 18),
833
833
  getSettingsListTheme(),
834
- (id, value) => {
834
+ (id: string, value: string) => {
835
835
  this.options.onChange(id, value);
836
836
  },
837
837
  () => {