pi-rtk-optimizer 0.7.0 → 0.8.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.
@@ -1,90 +1,126 @@
1
- import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
2
-
3
- export interface RtkRewriteProviderResult {
4
- changed: boolean;
5
- originalCommand: string;
6
- rewrittenCommand: string;
7
- exitCode: number;
8
- error?: string;
9
- }
10
-
11
- function isAlreadyRtk(command: string): boolean {
12
- const trimmed = command.trimStart();
13
- return trimmed === "rtk" || trimmed.startsWith("rtk ");
14
- }
15
-
16
- export async function resolveRtkRewrite(
17
- pi: ExtensionAPI,
18
- command: string,
19
- timeoutMs = 3000,
20
- ): Promise<RtkRewriteProviderResult> {
21
- if (!command || !command.trim()) {
22
- return { changed: false, originalCommand: command, rewrittenCommand: command, exitCode: 1 };
23
- }
24
-
25
- if (isAlreadyRtk(command)) {
26
- return { changed: false, originalCommand: command, rewrittenCommand: command, exitCode: 1 };
27
- }
28
-
29
- try {
30
- const result = await pi.exec("rtk", ["rewrite", command], { timeout: timeoutMs });
31
-
32
- if (result.code === 1) {
33
- return { changed: false, originalCommand: command, rewrittenCommand: command, exitCode: 1 };
34
- }
35
-
36
- if (result.code === 2) {
37
- return {
38
- changed: false,
39
- originalCommand: command,
40
- rewrittenCommand: command,
41
- exitCode: 2,
42
- error: result.stderr?.trim() || "rtk denied rewrite",
43
- };
44
- }
45
-
46
- if (result.code === 0 || result.code === 3) {
47
- const rewritten = result.stdout?.trim();
48
- if (!rewritten) {
49
- return {
50
- changed: false,
51
- originalCommand: command,
52
- rewrittenCommand: command,
53
- exitCode: result.code,
54
- error: "rtk returned empty output",
55
- };
56
- }
57
- if (rewritten === command) {
58
- return {
59
- changed: false,
60
- originalCommand: command,
61
- rewrittenCommand: command,
62
- exitCode: result.code,
63
- };
64
- }
65
- return {
66
- changed: true,
67
- originalCommand: command,
68
- rewrittenCommand: rewritten,
69
- exitCode: result.code,
70
- };
71
- }
72
-
73
- return {
74
- changed: false,
75
- originalCommand: command,
76
- rewrittenCommand: command,
77
- exitCode: result.code,
78
- error: `unexpected exit code ${result.code}`,
79
- };
80
- } catch (error) {
81
- const message = error instanceof Error ? error.message : String(error);
82
- return {
83
- changed: false,
84
- originalCommand: command,
85
- rewrittenCommand: command,
86
- exitCode: -1,
87
- error: message,
88
- };
89
- }
90
- }
1
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
+ import { resolveRtkExecutable, type RtkExecutableResolution } from "./rtk-executable-resolver.js";
3
+
4
+ export interface RtkRewriteProviderResult {
5
+ changed: boolean;
6
+ originalCommand: string;
7
+ rewrittenCommand: string;
8
+ exitCode: number;
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;
18
+ }
19
+
20
+ function isAlreadyRtk(command: string): boolean {
21
+ const trimmed = command.trimStart();
22
+ return trimmed === "rtk" || trimmed.startsWith("rtk ");
23
+ }
24
+
25
+ function normalizeOptions(optionsOrTimeout: number | RtkRewriteProviderOptions): RtkRewriteProviderOptions {
26
+ if (typeof optionsOrTimeout === "number") {
27
+ return { timeoutMs: optionsOrTimeout };
28
+ }
29
+ return optionsOrTimeout;
30
+ }
31
+
32
+ export async function resolveRtkRewrite(
33
+ pi: ExtensionAPI,
34
+ command: string,
35
+ optionsOrTimeout: number | RtkRewriteProviderOptions = {},
36
+ ): Promise<RtkRewriteProviderResult> {
37
+ const options = normalizeOptions(optionsOrTimeout);
38
+ const timeoutMs = options.timeoutMs ?? 3000;
39
+
40
+ if (!command || !command.trim()) {
41
+ return { changed: false, originalCommand: command, rewrittenCommand: command, exitCode: 1 };
42
+ }
43
+
44
+ if (isAlreadyRtk(command)) {
45
+ return { changed: false, originalCommand: command, rewrittenCommand: command, exitCode: 1 };
46
+ }
47
+
48
+ try {
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 });
56
+
57
+ if (result.code === 1) {
58
+ return {
59
+ changed: false,
60
+ originalCommand: command,
61
+ rewrittenCommand: command,
62
+ exitCode: 1,
63
+ executableResolution,
64
+ };
65
+ }
66
+
67
+ if (result.code === 2) {
68
+ return {
69
+ changed: false,
70
+ originalCommand: command,
71
+ rewrittenCommand: command,
72
+ exitCode: 2,
73
+ error: result.stderr?.trim() || "rtk denied rewrite",
74
+ executableResolution,
75
+ };
76
+ }
77
+
78
+ if (result.code === 0 || result.code === 3) {
79
+ const rewritten = result.stdout?.trim();
80
+ if (!rewritten) {
81
+ return {
82
+ changed: false,
83
+ originalCommand: command,
84
+ rewrittenCommand: command,
85
+ exitCode: result.code,
86
+ error: "rtk returned empty output",
87
+ executableResolution,
88
+ };
89
+ }
90
+ if (rewritten === command) {
91
+ return {
92
+ changed: false,
93
+ originalCommand: command,
94
+ rewrittenCommand: command,
95
+ exitCode: result.code,
96
+ executableResolution,
97
+ };
98
+ }
99
+ return {
100
+ changed: true,
101
+ originalCommand: command,
102
+ rewrittenCommand: rewritten,
103
+ exitCode: result.code,
104
+ executableResolution,
105
+ };
106
+ }
107
+
108
+ return {
109
+ changed: false,
110
+ originalCommand: command,
111
+ rewrittenCommand: command,
112
+ exitCode: result.code,
113
+ error: `unexpected exit code ${result.code}`,
114
+ executableResolution,
115
+ };
116
+ } catch (error) {
117
+ const message = error instanceof Error ? error.message : String(error);
118
+ return {
119
+ changed: false,
120
+ originalCommand: command,
121
+ rewrittenCommand: command,
122
+ exitCode: -1,
123
+ error: message,
124
+ };
125
+ }
126
+ }
@@ -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
+ }
@@ -1,4 +1,4 @@
1
- declare module "@mariozechner/pi-tui" {
1
+ declare module "@earendil-works/pi-tui" {
2
2
  export interface SettingItem {
3
3
  id: string;
4
4
  label: string;
@@ -27,6 +27,8 @@ declare module "@mariozechner/pi-tui" {
27
27
 
28
28
  export class SettingsList {
29
29
  constructor(...args: unknown[]);
30
+ render(width: number): string[];
31
+ invalidate(): void;
30
32
  handleInput(data: string): void;
31
33
  updateValue(id: string, value: string): void;
32
34
  }
@@ -43,7 +45,7 @@ declare module "@mariozechner/pi-tui" {
43
45
  export function visibleWidth(text: string): number;
44
46
  }
45
47
 
46
- declare module "@mariozechner/pi-coding-agent" {
48
+ declare module "@earendil-works/pi-coding-agent" {
47
49
  interface UiLike {
48
50
  notify(message: string, level: "info" | "warning" | "error"): void;
49
51
  custom<T>(
package/src/types.ts CHANGED
@@ -87,5 +87,9 @@ export interface RuntimeStatus {
87
87
  rtkAvailable: boolean;
88
88
  lastCheckedAt?: number;
89
89
  lastError?: string;
90
+ rtkExecutablePath?: string;
91
+ rtkExecutableCommand?: string;
92
+ rtkExecutableResolver?: string;
93
+ rtkExecutableResolutionWarning?: string;
90
94
  }
91
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