@oh-my-pi/pi-coding-agent 6.7.67 → 6.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.
Files changed (114) hide show
  1. package/CHANGELOG.md +28 -0
  2. package/package.json +6 -7
  3. package/src/cli/session-picker.ts +27 -28
  4. package/src/cli/setup-cli.ts +7 -16
  5. package/src/cli/update-cli.ts +1 -1
  6. package/src/config.ts +1 -1
  7. package/src/core/agent-session.ts +202 -37
  8. package/src/core/agent-storage.ts +1 -1
  9. package/src/core/auth-storage.ts +15 -25
  10. package/src/core/bash-executor.ts +63 -105
  11. package/src/core/custom-commands/loader.ts +1 -1
  12. package/src/core/custom-tools/loader.ts +1 -1
  13. package/src/core/custom-tools/types.ts +1 -2
  14. package/src/core/exec.ts +16 -100
  15. package/src/core/extensions/index.ts +1 -7
  16. package/src/core/extensions/loader.ts +1 -1
  17. package/src/core/extensions/runner.ts +1 -1
  18. package/src/core/extensions/types.ts +2 -2
  19. package/src/core/extensions/wrapper.ts +15 -20
  20. package/src/core/frontmatter.ts +1 -1
  21. package/src/core/history-storage.ts +3 -6
  22. package/src/core/hooks/index.ts +2 -2
  23. package/src/core/hooks/loader.ts +1 -1
  24. package/src/core/hooks/tool-wrapper.ts +14 -26
  25. package/src/core/hooks/types.ts +1 -2
  26. package/src/core/keybindings.ts +1 -1
  27. package/src/core/mcp/client.ts +13 -13
  28. package/src/core/mcp/json-rpc.ts +1 -1
  29. package/src/core/mcp/loader.ts +1 -1
  30. package/src/core/mcp/manager.ts +2 -2
  31. package/src/core/mcp/tool-cache.ts +1 -1
  32. package/src/core/mcp/transports/http.ts +32 -70
  33. package/src/core/model-registry.ts +1 -1
  34. package/src/core/plugins/installer.ts +13 -11
  35. package/src/core/prompt-templates.ts +4 -9
  36. package/src/core/python-executor.ts +23 -18
  37. package/src/core/python-gateway-coordinator.ts +29 -28
  38. package/src/core/python-kernel.ts +230 -211
  39. package/src/core/sdk.ts +10 -13
  40. package/src/core/session-manager.ts +1 -1
  41. package/src/core/settings-manager.ts +22 -9
  42. package/src/core/skills.ts +1 -1
  43. package/src/core/ssh/connection-manager.ts +19 -33
  44. package/src/core/ssh/ssh-executor.ts +39 -35
  45. package/src/core/ssh/sshfs-mount.ts +14 -33
  46. package/src/core/storage-migration.ts +1 -1
  47. package/src/core/streaming-output.ts +183 -127
  48. package/src/core/system-prompt.ts +119 -79
  49. package/src/core/title-generator.ts +1 -1
  50. package/src/core/tools/ask.ts +2 -2
  51. package/src/core/tools/bash.ts +3 -3
  52. package/src/core/tools/calculator.ts +1 -1
  53. package/src/core/tools/exa/mcp-client.ts +1 -1
  54. package/src/core/tools/exa/render.ts +1 -1
  55. package/src/core/tools/find.ts +39 -71
  56. package/src/core/tools/gemini-image.ts +1 -1
  57. package/src/core/tools/grep.ts +88 -100
  58. package/src/core/tools/index.ts +1 -1
  59. package/src/core/tools/ls.ts +1 -1
  60. package/src/core/tools/lsp/client.ts +50 -50
  61. package/src/core/tools/lsp/clients/lsp-linter-client.ts +1 -1
  62. package/src/core/tools/lsp/config.ts +1 -1
  63. package/src/core/tools/lsp/index.ts +2 -4
  64. package/src/core/tools/lsp/lspmux.ts +1 -1
  65. package/src/core/tools/lsp/rust-analyzer.ts +2 -2
  66. package/src/core/tools/lsp/utils.ts +0 -14
  67. package/src/core/tools/notebook.ts +1 -1
  68. package/src/core/tools/patch/shared.ts +3 -4
  69. package/src/core/tools/python.ts +3 -3
  70. package/src/core/tools/read.ts +29 -68
  71. package/src/core/tools/render-utils.ts +0 -5
  72. package/src/core/tools/ssh.ts +3 -3
  73. package/src/core/tools/task/model-resolver.ts +7 -9
  74. package/src/core/tools/task/worker.ts +144 -139
  75. package/src/core/tools/todo-write.ts +1 -1
  76. package/src/core/tools/truncate.ts +2 -2
  77. package/src/core/tools/web-fetch.ts +13 -15
  78. package/src/core/tools/web-scrapers/types.ts +1 -3
  79. package/src/core/tools/web-scrapers/utils.ts +14 -13
  80. package/src/core/tools/web-scrapers/youtube.ts +39 -12
  81. package/src/core/tools/web-search/auth.ts +9 -45
  82. package/src/core/tools/write.ts +1 -1
  83. package/src/core/ttsr.ts +1 -1
  84. package/src/core/utils.ts +1 -187
  85. package/src/core/voice-controller.ts +1 -1
  86. package/src/core/voice-supervisor.ts +11 -38
  87. package/src/core/voice.ts +1 -8
  88. package/src/discovery/codex.ts +1 -1
  89. package/src/index.ts +4 -4
  90. package/src/main.ts +5 -10
  91. package/src/migrations.ts +1 -1
  92. package/src/modes/index.ts +7 -40
  93. package/src/modes/interactive/components/extensions/state-manager.ts +1 -1
  94. package/src/modes/interactive/components/hook-editor.ts +12 -9
  95. package/src/modes/interactive/components/login-dialog.ts +24 -11
  96. package/src/modes/interactive/components/settings-defs.ts +9 -0
  97. package/src/modes/interactive/components/status-line.ts +36 -35
  98. package/src/modes/interactive/components/todo-display.ts +1 -1
  99. package/src/modes/interactive/components/tool-execution.ts +1 -1
  100. package/src/modes/interactive/controllers/command-controller.ts +50 -84
  101. package/src/modes/interactive/controllers/extension-ui-controller.ts +76 -76
  102. package/src/modes/interactive/controllers/input-controller.ts +12 -11
  103. package/src/modes/interactive/interactive-mode.ts +10 -11
  104. package/src/modes/interactive/theme/theme.ts +1 -1
  105. package/src/modes/interactive/types.ts +1 -1
  106. package/src/modes/rpc/rpc-client.ts +91 -121
  107. package/src/modes/rpc/rpc-mode.ts +71 -79
  108. package/src/prompts/system/ttsr-interrupt.md +7 -0
  109. package/src/utils/clipboard.ts +57 -141
  110. package/src/utils/shell-snapshot.ts +12 -60
  111. package/src/utils/shell.ts +35 -56
  112. package/src/utils/tools-manager.ts +42 -71
  113. package/src/core/logger.ts +0 -111
  114. package/src/modes/cleanup.ts +0 -23
@@ -1,154 +1,210 @@
1
1
  import { tmpdir } from "node:os";
2
2
  import { join } from "node:path";
3
+ import { sanitizeText } from "@oh-my-pi/pi-utils";
3
4
  import { nanoid } from "nanoid";
4
- import stripAnsi from "strip-ansi";
5
- import { truncateTail } from "./tools/truncate";
5
+ import { DEFAULT_MAX_BYTES, DEFAULT_MAX_COLUMN } from "./tools/truncate";
6
6
 
7
- /**
8
- * Sanitize binary output for display/storage.
9
- * Removes characters that crash string-width or cause display issues:
10
- * - Control characters (except tab, newline, carriage return)
11
- * - Lone surrogates
12
- * - Unicode Format characters (crash string-width due to a bug)
13
- * - Characters with undefined code points
14
- */
15
- export function sanitizeBinaryOutput(str: string): string {
16
- // Use Array.from to properly iterate over code points (not code units)
17
- // This handles surrogate pairs correctly and catches edge cases where
18
- // codePointAt() might return undefined
19
- return Array.from(str)
20
- .filter((char) => {
21
- // Filter out characters that cause string-width to crash
22
- // This includes:
23
- // - Unicode format characters
24
- // - Lone surrogates (already filtered by Array.from)
25
- // - Control chars except \t \n \r
26
- // - Characters with undefined code points
27
-
28
- const code = char.codePointAt(0);
29
-
30
- // Skip if code point is undefined (edge case with invalid strings)
31
- if (code === undefined) return false;
32
-
33
- // Allow tab, newline, carriage return
34
- if (code === 0x09 || code === 0x0a || code === 0x0d) return true;
35
-
36
- // Filter out control characters (0x00-0x1F, except 0x09, 0x0a, 0x0x0d)
37
- if (code <= 0x1f) return false;
38
-
39
- // Filter out Unicode format characters
40
- if (code >= 0xfff9 && code <= 0xfffb) return false;
41
-
42
- return true;
43
- })
44
- .join("");
7
+ export interface OutputResult {
8
+ output: string;
9
+ truncated: boolean;
10
+ fullOutputPath?: string;
45
11
  }
46
12
 
47
- /**
48
- * Sanitize text output: strip ANSI codes, remove binary garbage, normalize line endings.
49
- */
50
- export function sanitizeText(text: string): string {
51
- return sanitizeBinaryOutput(stripAnsi(text)).replace(/\r/g, "");
13
+ export interface OutputSinkOptions {
14
+ allocateFilePath?: () => string;
15
+ spillThreshold?: number;
16
+ maxColumn?: number;
17
+ onLine?: (line: string) => void;
18
+ onChunk?: (chunk: string) => void;
52
19
  }
53
20
 
54
- interface OutputFileSink {
55
- write(data: string): number | Promise<number>;
56
- end(): void;
21
+ function defaultFilePathAllocator(): string {
22
+ return join(tmpdir(), `omp-${nanoid()}.log`);
57
23
  }
58
24
 
59
- export function createSanitizer(): TransformStream<Uint8Array, string> {
60
- const decoder = new TextDecoder();
61
- return new TransformStream({
62
- transform(chunk, controller) {
63
- const text = sanitizeText(decoder.decode(chunk, { stream: true }));
64
- if (text) {
65
- controller.enqueue(text);
66
- }
67
- },
68
- flush(controller) {
69
- const text = sanitizeText(decoder.decode());
70
- if (text) {
71
- controller.enqueue(text);
25
+ /**
26
+ * Line-buffered output sink with file spill support.
27
+ *
28
+ * Uses a single string buffer with line position tracking.
29
+ * When memory limit exceeded, spills ~half to file in one batch operation.
30
+ */
31
+ export class OutputSink {
32
+ private buffer = "";
33
+ private lineEnds: number[] = []; // String index after each \n
34
+
35
+ private fileSink?: Bun.FileSink;
36
+ private filePath?: string;
37
+
38
+ private readonly allocateFilePath: () => string;
39
+ private readonly spillThreshold: number;
40
+ private readonly maxColumn: number;
41
+ private readonly onLine?: (line: string) => void;
42
+ private readonly onChunk?: (chunk: string) => void;
43
+
44
+ constructor(options?: OutputSinkOptions) {
45
+ const {
46
+ allocateFilePath = defaultFilePathAllocator,
47
+ spillThreshold = DEFAULT_MAX_BYTES,
48
+ maxColumn = DEFAULT_MAX_COLUMN,
49
+ onLine,
50
+ onChunk,
51
+ } = options ?? {};
52
+
53
+ this.allocateFilePath = allocateFilePath;
54
+ this.spillThreshold = spillThreshold;
55
+ this.maxColumn = maxColumn;
56
+ this.onLine = onLine;
57
+ this.onChunk = onChunk;
58
+ }
59
+
60
+ private pushLine(line: string, term?: string): void {
61
+ while (line.length > this.maxColumn) {
62
+ this.pushLine(line.slice(0, this.maxColumn), "--\n");
63
+ line = line.slice(this.maxColumn);
64
+ }
65
+
66
+ this.buffer += line;
67
+ if (term) {
68
+ this.buffer += term;
69
+ }
70
+
71
+ this.lineEnds.push(this.buffer.length);
72
+ this.onLine?.(line);
73
+
74
+ if (this.buffer.length > this.spillThreshold) {
75
+ this.spillHalf();
76
+ }
77
+ }
78
+
79
+ private pushChunk(line: string): void {
80
+ this.onChunk?.(line);
81
+ this.pushLine(line);
82
+ }
83
+
84
+ private getFileSink(): Bun.FileSink {
85
+ if (!this.fileSink) {
86
+ const filePath = this.allocateFilePath();
87
+ this.filePath = filePath;
88
+ this.fileSink = Bun.file(filePath).writer();
89
+ }
90
+ return this.fileSink;
91
+ }
92
+
93
+ private spillHalf(): void {
94
+ const target = this.buffer.length >>> 1;
95
+
96
+ // Binary search: first line ending >= target
97
+ let lo = 0;
98
+ let hi = this.lineEnds.length;
99
+ while (lo < hi) {
100
+ const mid = (lo + hi) >>> 1;
101
+ if (this.lineEnds[mid] < target) {
102
+ lo = mid + 1;
103
+ } else {
104
+ hi = mid;
72
105
  }
73
- },
74
- });
75
- }
106
+ }
107
+
108
+ // Clamp: evict at least 1 line, keep at least 1 line
109
+ const splitIdx = Math.max(1, Math.min(lo, this.lineEnds.length - 1));
110
+ const splitPos = this.lineEnds[splitIdx - 1];
76
111
 
77
- export async function pumpStream(readable: ReadableStream<Uint8Array>, writer: WritableStreamDefaultWriter<string>) {
78
- const reader = readable.pipeThrough(createSanitizer()).getReader();
79
- try {
80
- while (true) {
81
- const { done, value } = await reader.read();
82
- if (done) break;
83
- await writer.write(value);
112
+ // Write evicted portion to file
113
+ this.getFileSink().write(this.buffer.slice(0, splitPos));
114
+
115
+ // Truncate buffer, shift line positions
116
+ this.buffer = this.buffer.slice(splitPos);
117
+ const remaining = this.lineEnds.length - splitIdx;
118
+ for (let i = 0; i < remaining; i++) {
119
+ this.lineEnds[i] = this.lineEnds[i + splitIdx] - splitPos;
84
120
  }
85
- } finally {
86
- reader.releaseLock();
121
+ this.lineEnds.length = remaining;
87
122
  }
88
- }
89
123
 
90
- export interface OutputSinkDump {
91
- output: string;
92
- truncated: boolean;
93
- fullOutputPath?: string;
94
- }
124
+ createWritable(): WritableStream<Uint8Array> {
125
+ const decoder = new TextDecoder("utf-8", { ignoreBOM: true });
126
+ let buf = "";
127
+
128
+ const flushLines = () => {
129
+ let start = 0;
130
+ while (true) {
131
+ const nl = buf.indexOf("\n", start);
132
+ if (nl === -1) break;
133
+ this.pushChunk(buf.slice(start, nl + 1));
134
+ start = nl + 1;
135
+ }
136
+ buf = buf.slice(start);
137
+ };
138
+
139
+ const finalize = () => {
140
+ buf += sanitizeText(decoder.decode());
141
+ flushLines();
142
+ buf = buf.trimEnd();
143
+ if (buf) {
144
+ this.pushChunk(`${buf}\n`);
145
+ }
146
+ };
95
147
 
96
- export class OutputSink {
97
- private readonly stream: WritableStream<string>;
98
- private readonly chunks: Array<{ text: string; bytes: number }> = [];
99
- private chunkBytes = 0;
100
- private totalBytes = 0;
101
- private fullOutputPath: string | undefined;
102
- private fullOutputStream: OutputFileSink | undefined;
103
-
104
- constructor(
105
- private readonly spillThreshold: number,
106
- private readonly maxBuffer: number,
107
- private readonly onChunk?: (text: string) => void,
108
- ) {
109
- this.stream = new WritableStream<string>({
110
- write: (text) => {
111
- const bytes = Buffer.byteLength(text, "utf-8");
112
- this.totalBytes += bytes;
113
-
114
- if (this.totalBytes > this.spillThreshold && !this.fullOutputPath) {
115
- this.fullOutputPath = join(tmpdir(), `omp-${nanoid()}.buffer`);
116
- const stream = Bun.file(this.fullOutputPath).writer();
117
- for (const chunk of this.chunks) {
118
- stream.write(chunk.text);
119
- }
120
- this.fullOutputStream = stream;
121
- }
122
- this.fullOutputStream?.write(text);
123
-
124
- this.chunks.push({ text, bytes });
125
- this.chunkBytes += bytes;
126
- while (this.chunkBytes > this.maxBuffer && this.chunks.length > 1) {
127
- const removed = this.chunks.shift();
128
- if (removed) {
129
- this.chunkBytes -= removed.bytes;
130
- }
131
- }
132
-
133
- this.onChunk?.(text);
148
+ return new WritableStream<Uint8Array>({
149
+ write: (chunk) => {
150
+ buf += sanitizeText(decoder.decode(chunk, { stream: true }));
151
+ flushLines();
134
152
  },
135
- close: () => {
136
- this.fullOutputStream?.end();
153
+ close: finalize,
154
+ abort: finalize,
155
+ });
156
+ }
157
+
158
+ createStringWritable(): WritableStream<string> {
159
+ let buf = "";
160
+
161
+ const flushLines = () => {
162
+ let start = 0;
163
+ while (true) {
164
+ const nl = buf.indexOf("\n", start);
165
+ if (nl === -1) break;
166
+ this.pushChunk(buf.slice(start, nl + 1));
167
+ start = nl + 1;
168
+ }
169
+ buf = buf.slice(start);
170
+ };
171
+
172
+ const finalize = () => {
173
+ flushLines();
174
+ buf = buf.trimEnd();
175
+ if (buf) {
176
+ this.pushChunk(`${buf}\n`);
177
+ }
178
+ };
179
+
180
+ return new WritableStream<string>({
181
+ write: (chunk) => {
182
+ buf += sanitizeText(chunk);
183
+ flushLines();
137
184
  },
185
+ close: finalize,
186
+ abort: finalize,
138
187
  });
139
188
  }
140
189
 
141
- getWriter(): WritableStreamDefaultWriter<string> {
142
- return this.stream.getWriter();
190
+ async close(): Promise<void> {
191
+ await this.fileSink?.end();
143
192
  }
144
193
 
145
- dump(annotation?: string): OutputSinkDump {
194
+ dump(annotation?: string): OutputResult {
195
+ let output = this.buffer;
146
196
  if (annotation) {
147
- const text = `\n\n${annotation}`;
148
- this.chunks.push({ text, bytes: Buffer.byteLength(text, "utf-8") });
197
+ output += `\n${annotation}\n`;
198
+ }
199
+ if (!this.filePath) {
200
+ return { output, truncated: false };
149
201
  }
150
- const full = this.chunks.map((chunk) => chunk.text).join("");
151
- const { content, truncated } = truncateTail(full);
152
- return { output: truncated ? content : full, truncated, fullOutputPath: this.fullOutputPath };
202
+ this.fileSink!.write(this.buffer);
203
+ this.fileSink!.flush();
204
+ return {
205
+ output,
206
+ truncated: true,
207
+ fullOutputPath: this.filePath,
208
+ };
153
209
  }
154
210
  }