@oh-my-pi/pi-coding-agent 3.37.1 → 4.0.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 (70) hide show
  1. package/CHANGELOG.md +87 -0
  2. package/README.md +44 -3
  3. package/docs/extensions.md +29 -4
  4. package/docs/sdk.md +3 -3
  5. package/package.json +5 -5
  6. package/src/cli/args.ts +8 -0
  7. package/src/config.ts +5 -15
  8. package/src/core/agent-session.ts +193 -47
  9. package/src/core/auth-storage.ts +16 -3
  10. package/src/core/bash-executor.ts +79 -14
  11. package/src/core/custom-commands/types.ts +1 -1
  12. package/src/core/custom-tools/types.ts +1 -1
  13. package/src/core/export-html/index.ts +33 -1
  14. package/src/core/export-html/template.css +99 -0
  15. package/src/core/export-html/template.generated.ts +1 -1
  16. package/src/core/export-html/template.js +133 -8
  17. package/src/core/extensions/index.ts +22 -4
  18. package/src/core/extensions/loader.ts +152 -214
  19. package/src/core/extensions/runner.ts +139 -79
  20. package/src/core/extensions/types.ts +143 -19
  21. package/src/core/extensions/wrapper.ts +5 -8
  22. package/src/core/hooks/types.ts +1 -1
  23. package/src/core/index.ts +2 -1
  24. package/src/core/keybindings.ts +4 -1
  25. package/src/core/model-registry.ts +1 -1
  26. package/src/core/model-resolver.ts +35 -26
  27. package/src/core/sdk.ts +96 -76
  28. package/src/core/settings-manager.ts +45 -14
  29. package/src/core/system-prompt.ts +5 -15
  30. package/src/core/tools/bash.ts +115 -54
  31. package/src/core/tools/find.ts +86 -7
  32. package/src/core/tools/grep.ts +27 -6
  33. package/src/core/tools/index.ts +15 -6
  34. package/src/core/tools/ls.ts +49 -18
  35. package/src/core/tools/render-utils.ts +2 -1
  36. package/src/core/tools/task/worker.ts +35 -12
  37. package/src/core/tools/web-search/auth.ts +37 -32
  38. package/src/core/tools/web-search/providers/anthropic.ts +35 -22
  39. package/src/index.ts +101 -9
  40. package/src/main.ts +60 -20
  41. package/src/migrations.ts +47 -2
  42. package/src/modes/index.ts +2 -2
  43. package/src/modes/interactive/components/assistant-message.ts +25 -7
  44. package/src/modes/interactive/components/bash-execution.ts +5 -0
  45. package/src/modes/interactive/components/branch-summary-message.ts +5 -0
  46. package/src/modes/interactive/components/compaction-summary-message.ts +5 -0
  47. package/src/modes/interactive/components/countdown-timer.ts +38 -0
  48. package/src/modes/interactive/components/custom-editor.ts +8 -0
  49. package/src/modes/interactive/components/custom-message.ts +5 -0
  50. package/src/modes/interactive/components/footer.ts +2 -5
  51. package/src/modes/interactive/components/hook-input.ts +29 -20
  52. package/src/modes/interactive/components/hook-selector.ts +52 -38
  53. package/src/modes/interactive/components/index.ts +39 -0
  54. package/src/modes/interactive/components/login-dialog.ts +160 -0
  55. package/src/modes/interactive/components/model-selector.ts +10 -2
  56. package/src/modes/interactive/components/session-selector.ts +5 -1
  57. package/src/modes/interactive/components/settings-defs.ts +9 -0
  58. package/src/modes/interactive/components/status-line/segments.ts +3 -3
  59. package/src/modes/interactive/components/tool-execution.ts +9 -16
  60. package/src/modes/interactive/components/tree-selector.ts +1 -6
  61. package/src/modes/interactive/interactive-mode.ts +466 -215
  62. package/src/modes/interactive/theme/theme.ts +50 -2
  63. package/src/modes/print-mode.ts +78 -31
  64. package/src/modes/rpc/rpc-mode.ts +186 -78
  65. package/src/modes/rpc/rpc-types.ts +10 -3
  66. package/src/prompts/system-prompt.md +36 -28
  67. package/src/utils/clipboard.ts +90 -50
  68. package/src/utils/image-convert.ts +1 -1
  69. package/src/utils/image-resize.ts +1 -1
  70. package/src/utils/tools-manager.ts +2 -2
@@ -1,6 +1,35 @@
1
+ import { unlink } from "node:fs/promises";
1
2
  import { platform } from "node:os";
2
3
  import { nanoid } from "nanoid";
3
4
 
5
+ const PREFERRED_IMAGE_MIME_TYPES = ["image/png", "image/jpeg", "image/webp", "image/gif"] as const;
6
+
7
+ function isWaylandSession(env: NodeJS.ProcessEnv = process.env): boolean {
8
+ return Boolean(env.WAYLAND_DISPLAY) || env.XDG_SESSION_TYPE === "wayland";
9
+ }
10
+
11
+ function baseMimeType(mimeType: string): string {
12
+ const base = mimeType.split(";")[0]?.trim().toLowerCase() ?? mimeType.toLowerCase();
13
+ return base === "image/jpg" ? "image/jpeg" : base;
14
+ }
15
+
16
+ function selectPreferredImageMimeType(mimeTypes: string[]): string | null {
17
+ const normalized = mimeTypes
18
+ .map((t) => t.trim())
19
+ .filter(Boolean)
20
+ .map((t) => ({ raw: t, base: baseMimeType(t) }));
21
+
22
+ for (const preferred of PREFERRED_IMAGE_MIME_TYPES) {
23
+ const match = normalized.find((t) => t.base === preferred);
24
+ if (match) {
25
+ return match.raw;
26
+ }
27
+ }
28
+
29
+ const anyImage = normalized.find((t) => t.base.startsWith("image/"));
30
+ return anyImage?.raw ?? null;
31
+ }
32
+
4
33
  async function spawnWithTimeout(cmd: string[], input: string, timeoutMs: number): Promise<void> {
5
34
  const proc = Bun.spawn(cmd, { stdin: "pipe" });
6
35
 
@@ -22,15 +51,18 @@ async function spawnWithTimeout(cmd: string[], input: string, timeoutMs: number)
22
51
  }
23
52
 
24
53
  async function spawnAndRead(cmd: string[], timeoutMs: number): Promise<Buffer | null> {
25
- const proc = Bun.spawn(cmd, { stdout: "pipe", stderr: "pipe" });
26
-
27
- const timeoutPromise = new Promise<never>((_, reject) => {
28
- setTimeout(() => reject(new Error("Clipboard operation timed out")), timeoutMs);
29
- });
54
+ let proc: ReturnType<typeof Bun.spawn> | null = null;
30
55
 
31
56
  try {
57
+ proc = Bun.spawn(cmd, { stdout: "pipe", stderr: "pipe" });
58
+
59
+ const timeoutPromise = new Promise<never>((_, reject) => {
60
+ setTimeout(() => reject(new Error("Clipboard operation timed out")), timeoutMs);
61
+ });
62
+
63
+ const stdoutStream = proc.stdout as ReadableStream<Uint8Array>;
32
64
  const [exitCode, stdout] = await Promise.race([
33
- Promise.all([proc.exited, new Response(proc.stdout).arrayBuffer()]),
65
+ Promise.all([proc.exited, new Response(stdoutStream).arrayBuffer()]),
34
66
  timeoutPromise,
35
67
  ]);
36
68
 
@@ -42,7 +74,7 @@ async function spawnAndRead(cmd: string[], timeoutMs: number): Promise<Buffer |
42
74
  } catch {
43
75
  return null;
44
76
  } finally {
45
- proc.kill();
77
+ proc?.kill();
46
78
  }
47
79
  }
48
80
 
@@ -56,6 +88,19 @@ export async function copyToClipboard(text: string): Promise<void> {
56
88
  } else if (p === "win32") {
57
89
  await spawnWithTimeout(["clip"], text, timeout);
58
90
  } else {
91
+ const wayland = isWaylandSession();
92
+ if (wayland) {
93
+ const wlCopyPath = Bun.which("wl-copy");
94
+ if (wlCopyPath) {
95
+ // Fire-and-forget: wl-copy may not exit promptly, so we unref to avoid blocking
96
+ const proc = Bun.spawn([wlCopyPath], { stdin: "pipe" });
97
+ proc.stdin.write(text);
98
+ proc.stdin.end();
99
+ proc.unref();
100
+ return;
101
+ }
102
+ }
103
+
59
104
  // Linux - try xclip first, fall back to xsel
60
105
  try {
61
106
  await spawnWithTimeout(["xclip", "-selection", "clipboard"], text, timeout);
@@ -66,7 +111,8 @@ export async function copyToClipboard(text: string): Promise<void> {
66
111
  } catch (error) {
67
112
  const msg = error instanceof Error ? error.message : String(error);
68
113
  if (p === "linux") {
69
- throw new Error(`Failed to copy to clipboard. Install xclip or xsel: ${msg}`);
114
+ const tools = isWaylandSession() ? "wl-copy, xclip, or xsel" : "xclip or xsel";
115
+ throw new Error(`Failed to copy to clipboard. Install ${tools}: ${msg}`);
70
116
  }
71
117
  throw new Error(`Failed to copy to clipboard: ${msg}`);
72
118
  }
@@ -82,7 +128,7 @@ export interface ClipboardImage {
82
128
  * Returns null if no image is in clipboard or clipboard access fails.
83
129
  *
84
130
  * Supported platforms:
85
- * - Linux: requires xclip
131
+ * - Linux: requires wl-paste (Wayland) or xclip (X11)
86
132
  * - macOS: uses osascript + pbpaste
87
133
  * - Windows: uses PowerShell
88
134
  */
@@ -106,64 +152,59 @@ export async function readImageFromClipboard(): Promise<ClipboardImage | null> {
106
152
  }
107
153
 
108
154
  async function readImageLinux(timeout: number): Promise<ClipboardImage | null> {
109
- // Try Wayland first (wl-paste), then X11 (xclip)
110
- const wayland = await readImageWayland(timeout);
111
- if (wayland) return wayland;
155
+ const wayland = isWaylandSession();
156
+ if (wayland) {
157
+ const image = await readImageWayland(timeout);
158
+ if (image) return image;
159
+ }
112
160
 
113
161
  return await readImageX11(timeout);
114
162
  }
115
163
 
116
164
  async function readImageWayland(timeout: number): Promise<ClipboardImage | null> {
117
- // wl-paste --list-types shows available MIME types
118
165
  const types = await spawnAndRead(["wl-paste", "--list-types"], timeout);
119
166
  if (!types) return null;
120
167
 
121
- const typeList = types.toString("utf-8");
168
+ const typeList = types
169
+ .toString("utf-8")
170
+ .split(/\r?\n/)
171
+ .map((t) => t.trim())
172
+ .filter(Boolean);
122
173
 
123
- // Try PNG first, then JPEG
124
- const imageTypes = [
125
- { type: "image/png", mimeType: "image/png" },
126
- { type: "image/jpeg", mimeType: "image/jpeg" },
127
- ];
174
+ const selectedType = selectPreferredImageMimeType(typeList);
175
+ if (!selectedType) return null;
128
176
 
129
- for (const { type, mimeType } of imageTypes) {
130
- if (typeList.includes(type)) {
131
- const imageData = await spawnAndRead(["wl-paste", "--type", type], timeout);
132
- if (imageData && imageData.length > 0) {
133
- return {
134
- data: imageData.toString("base64"),
135
- mimeType,
136
- };
137
- }
138
- }
139
- }
177
+ const imageData = await spawnAndRead(["wl-paste", "--type", selectedType, "--no-newline"], timeout);
178
+ if (!imageData || imageData.length === 0) return null;
140
179
 
141
- return null;
180
+ return {
181
+ data: imageData.toString("base64"),
182
+ mimeType: baseMimeType(selectedType),
183
+ };
142
184
  }
143
185
 
144
186
  async function readImageX11(timeout: number): Promise<ClipboardImage | null> {
145
- // Check available targets in clipboard
146
187
  const targets = await spawnAndRead(["xclip", "-selection", "clipboard", "-t", "TARGETS", "-o"], timeout);
147
- if (!targets) return null;
148
188
 
149
- const targetList = targets.toString("utf-8");
189
+ let candidateTypes: string[] = [];
190
+ if (targets) {
191
+ candidateTypes = targets
192
+ .toString("utf-8")
193
+ .split(/\r?\n/)
194
+ .map((t) => t.trim())
195
+ .filter(Boolean);
196
+ }
150
197
 
151
- // Try PNG first (preferred), then JPEG
152
- const imageTypes = [
153
- { target: "image/png", mimeType: "image/png" },
154
- { target: "image/jpeg", mimeType: "image/jpeg" },
155
- { target: "image/jpg", mimeType: "image/jpeg" },
156
- ];
198
+ const preferred = candidateTypes.length > 0 ? selectPreferredImageMimeType(candidateTypes) : null;
199
+ const tryTypes = preferred ? [preferred, ...PREFERRED_IMAGE_MIME_TYPES] : [...PREFERRED_IMAGE_MIME_TYPES];
157
200
 
158
- for (const { target, mimeType } of imageTypes) {
159
- if (targetList.includes(target)) {
160
- const imageData = await spawnAndRead(["xclip", "-selection", "clipboard", "-t", target, "-o"], timeout);
161
- if (imageData && imageData.length > 0) {
162
- return {
163
- data: imageData.toString("base64"),
164
- mimeType,
165
- };
166
- }
201
+ for (const mimeType of tryTypes) {
202
+ const imageData = await spawnAndRead(["xclip", "-selection", "clipboard", "-t", mimeType, "-o"], timeout);
203
+ if (imageData && imageData.length > 0) {
204
+ return {
205
+ data: imageData.toString("base64"),
206
+ mimeType: baseMimeType(mimeType),
207
+ };
167
208
  }
168
209
  }
169
210
 
@@ -222,7 +263,6 @@ async function readImageMacOS(timeout: number): Promise<ClipboardImage | null> {
222
263
  if (await file.exists()) {
223
264
  const buffer = await file.arrayBuffer();
224
265
  await Bun.write(tempFile, ""); // Clear file
225
- const { unlink } = await import("fs/promises");
226
266
  await unlink(tempFile).catch(() => {});
227
267
 
228
268
  if (buffer.byteLength > 0) {
@@ -1,4 +1,4 @@
1
- import { convertToPngWithImageMagick } from "./image-magick.js";
1
+ import { convertToPngWithImageMagick } from "./image-magick";
2
2
 
3
3
  /**
4
4
  * Convert image to PNG format for terminal display.
@@ -1,5 +1,5 @@
1
1
  import type { ImageContent } from "@oh-my-pi/pi-ai";
2
- import { getImageDimensionsWithImageMagick, resizeWithImageMagick } from "./image-magick.js";
2
+ import { getImageDimensionsWithImageMagick, resizeWithImageMagick } from "./image-magick";
3
3
 
4
4
  export interface ImageResizeOptions {
5
5
  maxWidth?: number; // Default: 2000
@@ -2,9 +2,9 @@ import { chmodSync, createWriteStream, existsSync, mkdirSync, renameSync, rmSync
2
2
  import { arch, platform } from "node:os";
3
3
  import { join } from "node:path";
4
4
  import chalk from "chalk";
5
- import { APP_NAME, getToolsDir } from "../config";
5
+ import { APP_NAME, getBinDir } from "../config";
6
6
 
7
- const TOOLS_DIR = getToolsDir();
7
+ const TOOLS_DIR = getBinDir();
8
8
 
9
9
  interface ToolConfig {
10
10
  name: string;