@oh-my-pi/pi-coding-agent 9.7.0 → 9.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.
- package/CHANGELOG.md +29 -0
- package/package.json +8 -8
- package/src/exec/bash-executor.ts +95 -96
- package/src/ipy/executor.ts +2 -1
- package/src/modes/controllers/command-controller.ts +1 -1
- package/src/modes/controllers/input-controller.ts +5 -4
- package/src/session/agent-session.ts +1 -0
- package/src/system-prompt.ts +24 -244
- package/src/utils/image-convert.ts +3 -3
- package/src/utils/image-resize.ts +10 -7
- package/src/exec/shell-session.ts +0 -604
- package/src/utils/clipboard.ts +0 -248
package/src/utils/clipboard.ts
DELETED
|
@@ -1,248 +0,0 @@
|
|
|
1
|
-
import * as fs from "node:fs/promises";
|
|
2
|
-
import * as os from "node:os";
|
|
3
|
-
import { $ } from "bun";
|
|
4
|
-
import { nanoid } from "nanoid";
|
|
5
|
-
|
|
6
|
-
const PREFERRED_IMAGE_MIME_TYPES = ["image/png", "image/jpeg", "image/webp", "image/gif"] as const;
|
|
7
|
-
|
|
8
|
-
function isWaylandSession(env: NodeJS.ProcessEnv = process.env): boolean {
|
|
9
|
-
return Boolean(env.WAYLAND_DISPLAY) || env.XDG_SESSION_TYPE === "wayland";
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
function baseMimeType(mimeType: string): string {
|
|
13
|
-
const base = mimeType.split(";")[0]?.trim().toLowerCase() ?? mimeType.toLowerCase();
|
|
14
|
-
return base === "image/jpg" ? "image/jpeg" : base;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
function selectPreferredImageMimeType(mimeTypes: string[]): string | null {
|
|
18
|
-
const normalized = mimeTypes
|
|
19
|
-
.map(t => t.trim())
|
|
20
|
-
.filter(Boolean)
|
|
21
|
-
.map(t => ({ raw: t, base: baseMimeType(t) }));
|
|
22
|
-
|
|
23
|
-
for (const preferred of PREFERRED_IMAGE_MIME_TYPES) {
|
|
24
|
-
const match = normalized.find(t => t.base === preferred);
|
|
25
|
-
if (match) {
|
|
26
|
-
return match.raw;
|
|
27
|
-
}
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
const anyImage = normalized.find(t => t.base.startsWith("image/"));
|
|
31
|
-
return anyImage?.raw ?? null;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
export async function copyToClipboard(text: string): Promise<void> {
|
|
35
|
-
const p = os.platform();
|
|
36
|
-
const timeout = 5000;
|
|
37
|
-
|
|
38
|
-
try {
|
|
39
|
-
if (p === "darwin") {
|
|
40
|
-
await Bun.spawn(["pbcopy"], { stdin: Buffer.from(text), timeout, windowsHide: true }).exited;
|
|
41
|
-
} else if (p === "win32") {
|
|
42
|
-
await Bun.spawn(["clip"], { stdin: Buffer.from(text), timeout, windowsHide: true }).exited;
|
|
43
|
-
} else {
|
|
44
|
-
const wayland = isWaylandSession();
|
|
45
|
-
if (wayland) {
|
|
46
|
-
const wlCopyPath = Bun.which("wl-copy");
|
|
47
|
-
if (wlCopyPath) {
|
|
48
|
-
// Fire-and-forget: wl-copy may not exit promptly, so we unref to avoid blocking
|
|
49
|
-
void Bun.spawn([wlCopyPath], { stdin: Buffer.from(text), timeout, windowsHide: true }).unref();
|
|
50
|
-
return;
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
// Linux - try xclip first, fall back to xsel
|
|
55
|
-
try {
|
|
56
|
-
await Bun.spawn(["xclip", "-selection", "clipboard"], {
|
|
57
|
-
stdin: Buffer.from(text),
|
|
58
|
-
timeout,
|
|
59
|
-
windowsHide: true,
|
|
60
|
-
}).exited;
|
|
61
|
-
} catch {
|
|
62
|
-
await Bun.spawn(["xsel", "--clipboard", "--input"], {
|
|
63
|
-
stdin: Buffer.from(text),
|
|
64
|
-
timeout,
|
|
65
|
-
windowsHide: true,
|
|
66
|
-
}).exited;
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
} catch (error) {
|
|
70
|
-
const msg = error instanceof Error ? error.message : String(error);
|
|
71
|
-
if (p === "linux") {
|
|
72
|
-
const tools = isWaylandSession() ? "wl-copy, xclip, or xsel" : "xclip or xsel";
|
|
73
|
-
throw new Error(`Failed to copy to clipboard. Install ${tools}: ${msg}`);
|
|
74
|
-
}
|
|
75
|
-
throw new Error(`Failed to copy to clipboard: ${msg}`);
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
export interface ClipboardImage {
|
|
80
|
-
data: string; // base64 encoded
|
|
81
|
-
mimeType: string;
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
/**
|
|
85
|
-
* Read image from system clipboard if available.
|
|
86
|
-
* Returns null if no image is in clipboard or clipboard access fails.
|
|
87
|
-
*
|
|
88
|
-
* Supported platforms:
|
|
89
|
-
* - Linux: requires wl-paste (Wayland) or xclip (X11)
|
|
90
|
-
* - macOS: uses osascript + pbpaste
|
|
91
|
-
* - Windows: uses PowerShell
|
|
92
|
-
*/
|
|
93
|
-
export async function readImageFromClipboard(): Promise<ClipboardImage | null> {
|
|
94
|
-
const p = os.platform();
|
|
95
|
-
const timeout = 3000;
|
|
96
|
-
let promise: Promise<ClipboardImage | null>;
|
|
97
|
-
switch (p) {
|
|
98
|
-
case "linux":
|
|
99
|
-
promise = readImageLinux();
|
|
100
|
-
break;
|
|
101
|
-
case "darwin":
|
|
102
|
-
promise = readImageMacOS();
|
|
103
|
-
break;
|
|
104
|
-
case "win32":
|
|
105
|
-
promise = readImageWindows();
|
|
106
|
-
break;
|
|
107
|
-
default:
|
|
108
|
-
return null;
|
|
109
|
-
}
|
|
110
|
-
return Promise.race([promise, Bun.sleep(timeout).then(() => null)]);
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
type ClipboardReadResult =
|
|
114
|
-
| { status: "found"; image: ClipboardImage }
|
|
115
|
-
| { status: "empty" } // Tools ran successfully, no image in clipboard
|
|
116
|
-
| { status: "unavailable" }; // Tools not found or failed to run
|
|
117
|
-
|
|
118
|
-
async function readImageLinux(): Promise<ClipboardImage | null> {
|
|
119
|
-
const wayland = isWaylandSession();
|
|
120
|
-
if (wayland) {
|
|
121
|
-
const result = await readImageWayland();
|
|
122
|
-
if (result.status === "found") return result.image;
|
|
123
|
-
if (result.status === "empty") return null; // Don't fall back to X11 if Wayland worked
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
const result = await readImageX11();
|
|
127
|
-
return result.status === "found" ? result.image : null;
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
async function readImageWayland(): Promise<ClipboardReadResult> {
|
|
131
|
-
const types = await $`wl-paste --list-types`.quiet().text();
|
|
132
|
-
if (!types) return { status: "unavailable" }; // Command failed
|
|
133
|
-
|
|
134
|
-
const typeList = types
|
|
135
|
-
.split(/\r?\n/)
|
|
136
|
-
.map(t => t.trim())
|
|
137
|
-
.filter(Boolean);
|
|
138
|
-
|
|
139
|
-
const selectedType = selectPreferredImageMimeType(typeList);
|
|
140
|
-
if (!selectedType) return { status: "empty" }; // No image types available
|
|
141
|
-
|
|
142
|
-
const imageData = await $`wl-paste --type ${selectedType} --no-newline`.quiet().arrayBuffer();
|
|
143
|
-
if (!imageData || imageData.byteLength === 0) return { status: "empty" };
|
|
144
|
-
|
|
145
|
-
return {
|
|
146
|
-
status: "found",
|
|
147
|
-
image: {
|
|
148
|
-
data: Buffer.from(imageData).toString("base64"),
|
|
149
|
-
mimeType: baseMimeType(selectedType),
|
|
150
|
-
},
|
|
151
|
-
};
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
async function readImageX11(): Promise<ClipboardReadResult> {
|
|
155
|
-
const targets = await $`xclip -selection clipboard -t TARGETS -o`.quiet().text();
|
|
156
|
-
if (!targets) return { status: "unavailable" }; // xclip failed (no X server?)
|
|
157
|
-
|
|
158
|
-
const candidateTypes = targets
|
|
159
|
-
.split(/\r?\n/)
|
|
160
|
-
.map(t => t.trim())
|
|
161
|
-
.filter(Boolean);
|
|
162
|
-
|
|
163
|
-
const selectedType = selectPreferredImageMimeType(candidateTypes);
|
|
164
|
-
if (!selectedType) return { status: "empty" }; // Clipboard has no image types
|
|
165
|
-
|
|
166
|
-
const imageData = await $`xclip -selection clipboard -t ${selectedType} -o`.quiet().arrayBuffer();
|
|
167
|
-
if (!imageData || imageData.byteLength === 0) return { status: "empty" };
|
|
168
|
-
|
|
169
|
-
return {
|
|
170
|
-
status: "found",
|
|
171
|
-
image: {
|
|
172
|
-
data: Buffer.from(imageData).toString("base64"),
|
|
173
|
-
mimeType: baseMimeType(selectedType),
|
|
174
|
-
},
|
|
175
|
-
};
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
async function readImageMacOS(): Promise<ClipboardImage | null> {
|
|
179
|
-
// Use osascript to check clipboard class and read PNG data
|
|
180
|
-
// First check if clipboard has image data
|
|
181
|
-
const checkScript = `
|
|
182
|
-
try
|
|
183
|
-
clipboard info for «class PNGf»
|
|
184
|
-
return "png"
|
|
185
|
-
on error
|
|
186
|
-
try
|
|
187
|
-
clipboard info for «class JPEG»
|
|
188
|
-
return "jpeg"
|
|
189
|
-
on error
|
|
190
|
-
return "none"
|
|
191
|
-
end try
|
|
192
|
-
end try
|
|
193
|
-
`;
|
|
194
|
-
|
|
195
|
-
const checkResult = await $`osascript -e ${checkScript}`.quiet().text();
|
|
196
|
-
const imageType = checkResult.trim();
|
|
197
|
-
if (imageType === "none") return null;
|
|
198
|
-
|
|
199
|
-
// Read the actual image data using a temp file approach
|
|
200
|
-
// osascript can't output binary directly, so we write to a temp file
|
|
201
|
-
const tempFile = `/tmp/omp-clipboard-${nanoid()}.${imageType === "png" ? "png" : "jpg"}`;
|
|
202
|
-
const clipboardClass = imageType === "png" ? "«class PNGf»" : "«class JPEG»";
|
|
203
|
-
|
|
204
|
-
const readScript = `
|
|
205
|
-
set imageData to the clipboard as ${clipboardClass}
|
|
206
|
-
set filePath to POSIX file "${tempFile}"
|
|
207
|
-
set fileRef to open for access filePath with write permission
|
|
208
|
-
write imageData to fileRef
|
|
209
|
-
close access fileRef
|
|
210
|
-
`;
|
|
211
|
-
|
|
212
|
-
await $`osascript -e ${readScript}`.quiet().text();
|
|
213
|
-
|
|
214
|
-
try {
|
|
215
|
-
const file = Bun.file(tempFile);
|
|
216
|
-
if (await file.exists()) {
|
|
217
|
-
const buffer = await file.bytes();
|
|
218
|
-
await fs.unlink(tempFile).catch(() => {});
|
|
219
|
-
|
|
220
|
-
if (buffer.length > 0) {
|
|
221
|
-
return {
|
|
222
|
-
data: Buffer.from(buffer).toString("base64"),
|
|
223
|
-
mimeType: imageType === "png" ? "image/png" : "image/jpeg",
|
|
224
|
-
};
|
|
225
|
-
}
|
|
226
|
-
}
|
|
227
|
-
} catch {
|
|
228
|
-
// File read failed
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
return null;
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
async function readImageWindows(): Promise<ClipboardImage | null> {
|
|
235
|
-
// PowerShell script to read image from clipboard as base64
|
|
236
|
-
const script = `
|
|
237
|
-
Add-Type -AssemblyName System.Windows.Forms
|
|
238
|
-
$clipboard = [System.Windows.Forms.Clipboard]::GetImage()
|
|
239
|
-
if ($clipboard -ne $null) {
|
|
240
|
-
$ms = New-Object System.IO.MemoryStream
|
|
241
|
-
$clipboard.Save($ms, [System.Drawing.Imaging.ImageFormat]::Png)
|
|
242
|
-
[Convert]::ToBase64String($ms.ToArray())
|
|
243
|
-
}
|
|
244
|
-
`;
|
|
245
|
-
|
|
246
|
-
const result = await $`powershell -NoProfile -Command ${script}`.quiet().text();
|
|
247
|
-
return result ? { data: result, mimeType: "image/png" } : null;
|
|
248
|
-
}
|