@oh-my-pi/pi-coding-agent 6.7.670 → 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.
- package/CHANGELOG.md +28 -0
- package/package.json +6 -7
- package/src/cli/session-picker.ts +27 -28
- package/src/cli/setup-cli.ts +7 -16
- package/src/cli/update-cli.ts +1 -1
- package/src/config.ts +1 -1
- package/src/core/agent-session.ts +202 -37
- package/src/core/agent-storage.ts +1 -1
- package/src/core/auth-storage.ts +15 -25
- package/src/core/bash-executor.ts +63 -105
- package/src/core/custom-commands/loader.ts +1 -1
- package/src/core/custom-tools/loader.ts +1 -1
- package/src/core/custom-tools/types.ts +1 -2
- package/src/core/exec.ts +16 -100
- package/src/core/extensions/index.ts +1 -7
- package/src/core/extensions/loader.ts +1 -1
- package/src/core/extensions/runner.ts +1 -1
- package/src/core/extensions/types.ts +2 -2
- package/src/core/extensions/wrapper.ts +15 -20
- package/src/core/frontmatter.ts +1 -1
- package/src/core/history-storage.ts +3 -6
- package/src/core/hooks/index.ts +2 -2
- package/src/core/hooks/loader.ts +1 -1
- package/src/core/hooks/tool-wrapper.ts +14 -26
- package/src/core/hooks/types.ts +1 -2
- package/src/core/keybindings.ts +1 -1
- package/src/core/mcp/client.ts +13 -13
- package/src/core/mcp/json-rpc.ts +1 -1
- package/src/core/mcp/loader.ts +1 -1
- package/src/core/mcp/manager.ts +2 -2
- package/src/core/mcp/tool-cache.ts +1 -1
- package/src/core/mcp/transports/http.ts +32 -70
- package/src/core/model-registry.ts +1 -1
- package/src/core/plugins/installer.ts +13 -11
- package/src/core/prompt-templates.ts +4 -9
- package/src/core/python-executor.ts +23 -18
- package/src/core/python-gateway-coordinator.ts +29 -28
- package/src/core/python-kernel.ts +230 -211
- package/src/core/sdk.ts +10 -13
- package/src/core/session-manager.ts +1 -1
- package/src/core/settings-manager.ts +22 -9
- package/src/core/skills.ts +1 -1
- package/src/core/ssh/connection-manager.ts +19 -33
- package/src/core/ssh/ssh-executor.ts +39 -35
- package/src/core/ssh/sshfs-mount.ts +14 -33
- package/src/core/storage-migration.ts +1 -1
- package/src/core/streaming-output.ts +183 -127
- package/src/core/system-prompt.ts +119 -79
- package/src/core/title-generator.ts +1 -1
- package/src/core/tools/ask.ts +2 -2
- package/src/core/tools/bash.ts +3 -3
- package/src/core/tools/calculator.ts +1 -1
- package/src/core/tools/exa/mcp-client.ts +1 -1
- package/src/core/tools/exa/render.ts +1 -1
- package/src/core/tools/find.ts +39 -71
- package/src/core/tools/gemini-image.ts +1 -1
- package/src/core/tools/grep.ts +88 -100
- package/src/core/tools/index.ts +1 -1
- package/src/core/tools/ls.ts +1 -1
- package/src/core/tools/lsp/client.ts +50 -50
- package/src/core/tools/lsp/clients/lsp-linter-client.ts +1 -1
- package/src/core/tools/lsp/config.ts +1 -1
- package/src/core/tools/lsp/index.ts +2 -4
- package/src/core/tools/lsp/lspmux.ts +1 -1
- package/src/core/tools/lsp/rust-analyzer.ts +2 -2
- package/src/core/tools/lsp/utils.ts +0 -14
- package/src/core/tools/notebook.ts +1 -1
- package/src/core/tools/patch/shared.ts +3 -4
- package/src/core/tools/python.ts +3 -3
- package/src/core/tools/read.ts +29 -68
- package/src/core/tools/render-utils.ts +0 -5
- package/src/core/tools/ssh.ts +3 -3
- package/src/core/tools/task/model-resolver.ts +7 -9
- package/src/core/tools/task/worker.ts +144 -139
- package/src/core/tools/todo-write.ts +1 -1
- package/src/core/tools/truncate.ts +2 -2
- package/src/core/tools/web-fetch.ts +13 -15
- package/src/core/tools/web-scrapers/types.ts +1 -3
- package/src/core/tools/web-scrapers/utils.ts +14 -13
- package/src/core/tools/web-scrapers/youtube.ts +39 -12
- package/src/core/tools/web-search/auth.ts +1 -1
- package/src/core/tools/write.ts +1 -1
- package/src/core/ttsr.ts +1 -1
- package/src/core/utils.ts +1 -187
- package/src/core/voice-controller.ts +1 -1
- package/src/core/voice-supervisor.ts +11 -38
- package/src/core/voice.ts +1 -8
- package/src/discovery/codex.ts +1 -1
- package/src/index.ts +4 -4
- package/src/main.ts +5 -10
- package/src/migrations.ts +1 -1
- package/src/modes/index.ts +7 -40
- package/src/modes/interactive/components/extensions/state-manager.ts +1 -1
- package/src/modes/interactive/components/hook-editor.ts +12 -9
- package/src/modes/interactive/components/login-dialog.ts +24 -11
- package/src/modes/interactive/components/settings-defs.ts +9 -0
- package/src/modes/interactive/components/status-line.ts +36 -35
- package/src/modes/interactive/components/todo-display.ts +1 -1
- package/src/modes/interactive/components/tool-execution.ts +1 -1
- package/src/modes/interactive/controllers/command-controller.ts +50 -84
- package/src/modes/interactive/controllers/extension-ui-controller.ts +76 -76
- package/src/modes/interactive/controllers/input-controller.ts +12 -11
- package/src/modes/interactive/interactive-mode.ts +10 -11
- package/src/modes/interactive/theme/theme.ts +1 -1
- package/src/modes/interactive/types.ts +1 -1
- package/src/modes/rpc/rpc-client.ts +91 -121
- package/src/modes/rpc/rpc-mode.ts +71 -79
- package/src/prompts/system/ttsr-interrupt.md +7 -0
- package/src/utils/clipboard.ts +57 -141
- package/src/utils/shell-snapshot.ts +12 -60
- package/src/utils/shell.ts +35 -56
- package/src/utils/tools-manager.ts +42 -71
- package/src/core/logger.ts +0 -111
- package/src/modes/cleanup.ts +0 -23
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
* - Extension UI: Extension UI requests are emitted, client responds with extension_ui_response
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
|
+
import { readLines } from "@oh-my-pi/pi-utils";
|
|
14
15
|
import { nanoid } from "nanoid";
|
|
15
16
|
import type { AgentSession } from "../../core/agent-session";
|
|
16
17
|
import type { ExtensionUIContext, ExtensionUIDialogOptions } from "../../core/extensions/index";
|
|
@@ -86,37 +87,37 @@ export async function runRpcMode(session: AgentSession): Promise<never> {
|
|
|
86
87
|
if (opts?.signal?.aborted) return Promise.resolve(defaultValue);
|
|
87
88
|
|
|
88
89
|
const id = nanoid();
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
90
|
+
const { promise, resolve, reject } = Promise.withResolvers<T>();
|
|
91
|
+
let timeoutId: ReturnType<typeof setTimeout> | undefined;
|
|
92
|
+
|
|
93
|
+
const cleanup = () => {
|
|
94
|
+
if (timeoutId) clearTimeout(timeoutId);
|
|
95
|
+
opts?.signal?.removeEventListener("abort", onAbort);
|
|
96
|
+
this.pendingRequests.delete(id);
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
const onAbort = () => {
|
|
100
|
+
cleanup();
|
|
101
|
+
resolve(defaultValue);
|
|
102
|
+
};
|
|
103
|
+
opts?.signal?.addEventListener("abort", onAbort, { once: true });
|
|
104
|
+
|
|
105
|
+
if (opts?.timeout !== undefined) {
|
|
106
|
+
timeoutId = setTimeout(() => {
|
|
99
107
|
cleanup();
|
|
100
108
|
resolve(defaultValue);
|
|
101
|
-
};
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
if (opts?.timeout !== undefined) {
|
|
105
|
-
timeoutId = setTimeout(() => {
|
|
106
|
-
cleanup();
|
|
107
|
-
resolve(defaultValue);
|
|
108
|
-
}, opts.timeout);
|
|
109
|
-
}
|
|
109
|
+
}, opts.timeout);
|
|
110
|
+
}
|
|
110
111
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
});
|
|
118
|
-
this.output({ type: "extension_ui_request", id, ...request } as RpcExtensionUIRequest);
|
|
112
|
+
this.pendingRequests.set(id, {
|
|
113
|
+
resolve: (response: RpcExtensionUIResponse) => {
|
|
114
|
+
cleanup();
|
|
115
|
+
resolve(parseResponse(response));
|
|
116
|
+
},
|
|
117
|
+
reject,
|
|
119
118
|
});
|
|
119
|
+
this.output({ type: "extension_ui_request", id, ...request } as RpcExtensionUIRequest);
|
|
120
|
+
return promise;
|
|
120
121
|
}
|
|
121
122
|
|
|
122
123
|
select(title: string, options: string[], dialogOptions?: ExtensionUIDialogOptions): Promise<string | undefined> {
|
|
@@ -242,28 +243,28 @@ export async function runRpcMode(session: AgentSession): Promise<never> {
|
|
|
242
243
|
|
|
243
244
|
async editor(title: string, prefill?: string): Promise<string | undefined> {
|
|
244
245
|
const id = nanoid();
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
});
|
|
259
|
-
this.output({
|
|
260
|
-
type: "extension_ui_request",
|
|
261
|
-
id,
|
|
262
|
-
method: "editor",
|
|
263
|
-
title,
|
|
264
|
-
prefill,
|
|
265
|
-
} as RpcExtensionUIRequest);
|
|
246
|
+
const { promise, resolve, reject } = Promise.withResolvers<string | undefined>();
|
|
247
|
+
this.pendingRequests.set(id, {
|
|
248
|
+
resolve: (response: RpcExtensionUIResponse) => {
|
|
249
|
+
this.pendingRequests.delete(id);
|
|
250
|
+
if ("cancelled" in response && response.cancelled) {
|
|
251
|
+
resolve(undefined);
|
|
252
|
+
} else if ("value" in response) {
|
|
253
|
+
resolve(response.value);
|
|
254
|
+
} else {
|
|
255
|
+
resolve(undefined);
|
|
256
|
+
}
|
|
257
|
+
},
|
|
258
|
+
reject,
|
|
266
259
|
});
|
|
260
|
+
this.output({
|
|
261
|
+
type: "extension_ui_request",
|
|
262
|
+
id,
|
|
263
|
+
method: "editor",
|
|
264
|
+
title,
|
|
265
|
+
prefill,
|
|
266
|
+
} as RpcExtensionUIRequest);
|
|
267
|
+
return promise;
|
|
267
268
|
}
|
|
268
269
|
|
|
269
270
|
get theme(): Theme {
|
|
@@ -620,40 +621,31 @@ export async function runRpcMode(session: AgentSession): Promise<never> {
|
|
|
620
621
|
}
|
|
621
622
|
|
|
622
623
|
// Listen for JSON input using Bun's stdin
|
|
623
|
-
const
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
const parsed = JSON.parse(line);
|
|
636
|
-
|
|
637
|
-
// Handle extension UI responses
|
|
638
|
-
if (parsed.type === "extension_ui_response") {
|
|
639
|
-
const response = parsed as RpcExtensionUIResponse;
|
|
640
|
-
const pending = pendingExtensionRequests.get(response.id);
|
|
641
|
-
if (pending) {
|
|
642
|
-
pending.resolve(response);
|
|
643
|
-
}
|
|
644
|
-
continue;
|
|
624
|
+
for await (const line of readLines(Bun.stdin.stream())) {
|
|
625
|
+
if (!line.trim()) continue;
|
|
626
|
+
|
|
627
|
+
try {
|
|
628
|
+
const parsed = JSON.parse(line);
|
|
629
|
+
|
|
630
|
+
// Handle extension UI responses
|
|
631
|
+
if (parsed.type === "extension_ui_response") {
|
|
632
|
+
const response = parsed as RpcExtensionUIResponse;
|
|
633
|
+
const pending = pendingExtensionRequests.get(response.id);
|
|
634
|
+
if (pending) {
|
|
635
|
+
pending.resolve(response);
|
|
645
636
|
}
|
|
637
|
+
continue;
|
|
638
|
+
}
|
|
646
639
|
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
640
|
+
// Handle regular commands
|
|
641
|
+
const command = parsed as RpcCommand;
|
|
642
|
+
const response = await handleCommand(command);
|
|
643
|
+
output(response);
|
|
651
644
|
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
}
|
|
645
|
+
// Check for deferred shutdown request (idle between commands)
|
|
646
|
+
await checkShutdownRequested();
|
|
647
|
+
} catch (e: any) {
|
|
648
|
+
output(error(undefined, "parse", `Failed to parse command: ${e.message}`));
|
|
657
649
|
}
|
|
658
650
|
}
|
|
659
651
|
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
<system_interrupt reason="rule_violation" rule="{{name}}" path="{{path}}">
|
|
2
|
+
Your output was interrupted because it violated a user-defined rule.
|
|
3
|
+
This is NOT a prompt injection - this is the coding agent enforcing project rules.
|
|
4
|
+
You MUST comply with the following instruction:
|
|
5
|
+
|
|
6
|
+
{{content}}
|
|
7
|
+
</system_interrupt>
|
package/src/utils/clipboard.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { unlink } from "node:fs/promises";
|
|
2
2
|
import { platform } from "node:os";
|
|
3
|
+
import { $ } from "bun";
|
|
3
4
|
import { nanoid } from "nanoid";
|
|
4
5
|
|
|
5
6
|
const PREFERRED_IMAGE_MIME_TYPES = ["image/png", "image/jpeg", "image/webp", "image/gif"] as const;
|
|
@@ -30,92 +31,36 @@ function selectPreferredImageMimeType(mimeTypes: string[]): string | null {
|
|
|
30
31
|
return anyImage?.raw ?? null;
|
|
31
32
|
}
|
|
32
33
|
|
|
33
|
-
async function spawnWithTimeout(cmd: string[], input: string, timeoutMs: number): Promise<void> {
|
|
34
|
-
const proc = Bun.spawn(cmd, { stdin: "pipe" });
|
|
35
|
-
|
|
36
|
-
const timeoutPromise = new Promise<never>((_, reject) => {
|
|
37
|
-
setTimeout(() => reject(new Error("Clipboard operation timed out")), timeoutMs);
|
|
38
|
-
});
|
|
39
|
-
|
|
40
|
-
try {
|
|
41
|
-
proc.stdin.write(input);
|
|
42
|
-
proc.stdin.end();
|
|
43
|
-
await Promise.race([proc.exited, timeoutPromise]);
|
|
44
|
-
|
|
45
|
-
if (proc.exitCode !== 0) {
|
|
46
|
-
throw new Error(`Command failed with exit code ${proc.exitCode}`);
|
|
47
|
-
}
|
|
48
|
-
} finally {
|
|
49
|
-
proc.kill();
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
async function spawnAndRead(cmd: string[], timeoutMs: number): Promise<Buffer | null> {
|
|
54
|
-
let proc: ReturnType<typeof Bun.spawn> | null = null;
|
|
55
|
-
|
|
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>;
|
|
64
|
-
const [exitCode, stdout] = await Promise.race([
|
|
65
|
-
Promise.all([proc.exited, new Response(stdoutStream).arrayBuffer()]),
|
|
66
|
-
timeoutPromise,
|
|
67
|
-
]);
|
|
68
|
-
|
|
69
|
-
if (exitCode !== 0) {
|
|
70
|
-
return null;
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
return Buffer.from(stdout);
|
|
74
|
-
} catch {
|
|
75
|
-
return null;
|
|
76
|
-
} finally {
|
|
77
|
-
proc?.kill();
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
|
|
81
34
|
export async function copyToClipboard(text: string): Promise<void> {
|
|
82
|
-
const
|
|
83
|
-
const timeout = 5000;
|
|
35
|
+
const timeout = Bun.sleep(3000).then(() => Promise.reject(new Error("Clipboard operation timed out")));
|
|
84
36
|
|
|
37
|
+
let promise: Promise<void>;
|
|
85
38
|
try {
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
if (
|
|
95
|
-
|
|
96
|
-
const proc = Bun.spawn([wlCopyPath], { stdin: "pipe" });
|
|
97
|
-
proc.stdin.write(text);
|
|
98
|
-
proc.stdin.end();
|
|
99
|
-
proc.unref();
|
|
39
|
+
switch (platform()) {
|
|
40
|
+
case "darwin":
|
|
41
|
+
promise = $`pbcopy ${text}`.quiet().then(() => void 0);
|
|
42
|
+
break;
|
|
43
|
+
case "win32":
|
|
44
|
+
promise = $`clip ${text}`.quiet().then(() => void 0);
|
|
45
|
+
break;
|
|
46
|
+
case "linux":
|
|
47
|
+
if (isWaylandSession()) {
|
|
48
|
+
$`wl-copy ${text}`.quiet(); // fire and forget
|
|
100
49
|
return;
|
|
50
|
+
} else {
|
|
51
|
+
promise = $`xclip -selection clipboard -t text/plain -i ${text}`.quiet().then(() => void 0);
|
|
101
52
|
}
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
try {
|
|
106
|
-
await spawnWithTimeout(["xclip", "-selection", "clipboard"], text, timeout);
|
|
107
|
-
} catch {
|
|
108
|
-
await spawnWithTimeout(["xsel", "--clipboard", "--input"], text, timeout);
|
|
109
|
-
}
|
|
53
|
+
break;
|
|
54
|
+
default:
|
|
55
|
+
throw new Error(`Unsupported platform: ${platform()}`);
|
|
110
56
|
}
|
|
111
57
|
} catch (error) {
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
const tools = isWaylandSession() ? "wl-copy, xclip, or xsel" : "xclip or xsel";
|
|
115
|
-
throw new Error(`Failed to copy to clipboard. Install ${tools}: ${msg}`);
|
|
58
|
+
if (error instanceof Error) {
|
|
59
|
+
throw new Error(`Failed to copy to clipboard: ${error.message}`);
|
|
116
60
|
}
|
|
117
|
-
throw new Error(`Failed to copy to clipboard: ${
|
|
61
|
+
throw new Error(`Failed to copy to clipboard: ${String(error)}`);
|
|
118
62
|
}
|
|
63
|
+
await Promise.race([promise, timeout]);
|
|
119
64
|
}
|
|
120
65
|
|
|
121
66
|
export interface ClipboardImage {
|
|
@@ -135,20 +80,21 @@ export interface ClipboardImage {
|
|
|
135
80
|
export async function readImageFromClipboard(): Promise<ClipboardImage | null> {
|
|
136
81
|
const p = platform();
|
|
137
82
|
const timeout = 3000;
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
83
|
+
let promise: Promise<ClipboardImage | null>;
|
|
84
|
+
switch (p) {
|
|
85
|
+
case "linux":
|
|
86
|
+
promise = readImageLinux();
|
|
87
|
+
break;
|
|
88
|
+
case "darwin":
|
|
89
|
+
promise = readImageMacOS();
|
|
90
|
+
break;
|
|
91
|
+
case "win32":
|
|
92
|
+
promise = readImageWindows();
|
|
93
|
+
break;
|
|
94
|
+
default:
|
|
95
|
+
return null;
|
|
149
96
|
}
|
|
150
|
-
|
|
151
|
-
return null;
|
|
97
|
+
return Promise.race([promise, Bun.sleep(timeout).then(() => null)]);
|
|
152
98
|
}
|
|
153
99
|
|
|
154
100
|
type ClipboardReadResult =
|
|
@@ -156,27 +102,23 @@ type ClipboardReadResult =
|
|
|
156
102
|
| { status: "empty" } // Tools ran successfully, no image in clipboard
|
|
157
103
|
| { status: "unavailable" }; // Tools not found or failed to run
|
|
158
104
|
|
|
159
|
-
async function readImageLinux(
|
|
105
|
+
async function readImageLinux(): Promise<ClipboardImage | null> {
|
|
160
106
|
const wayland = isWaylandSession();
|
|
161
107
|
if (wayland) {
|
|
162
|
-
const result = await readImageWayland(
|
|
108
|
+
const result = await readImageWayland();
|
|
163
109
|
if (result.status === "found") return result.image;
|
|
164
110
|
if (result.status === "empty") return null; // Don't fall back to X11 if Wayland worked
|
|
165
111
|
}
|
|
166
112
|
|
|
167
|
-
const result = await readImageX11(
|
|
113
|
+
const result = await readImageX11();
|
|
168
114
|
return result.status === "found" ? result.image : null;
|
|
169
115
|
}
|
|
170
116
|
|
|
171
|
-
async function readImageWayland(
|
|
172
|
-
const
|
|
173
|
-
if (!wlPastePath) return { status: "unavailable" };
|
|
174
|
-
|
|
175
|
-
const types = await spawnAndRead([wlPastePath, "--list-types"], timeout);
|
|
117
|
+
async function readImageWayland(): Promise<ClipboardReadResult> {
|
|
118
|
+
const types = await $`wl-paste --list-types`.quiet().text();
|
|
176
119
|
if (!types) return { status: "unavailable" }; // Command failed
|
|
177
120
|
|
|
178
121
|
const typeList = types
|
|
179
|
-
.toString("utf-8")
|
|
180
122
|
.split(/\r?\n/)
|
|
181
123
|
.map((t) => t.trim())
|
|
182
124
|
.filter(Boolean);
|
|
@@ -184,27 +126,23 @@ async function readImageWayland(timeout: number): Promise<ClipboardReadResult> {
|
|
|
184
126
|
const selectedType = selectPreferredImageMimeType(typeList);
|
|
185
127
|
if (!selectedType) return { status: "empty" }; // No image types available
|
|
186
128
|
|
|
187
|
-
const imageData = await
|
|
188
|
-
if (!imageData || imageData.
|
|
129
|
+
const imageData = await $`wl-paste --type ${selectedType} --no-newline`.quiet().arrayBuffer();
|
|
130
|
+
if (!imageData || imageData.byteLength === 0) return { status: "empty" };
|
|
189
131
|
|
|
190
132
|
return {
|
|
191
133
|
status: "found",
|
|
192
134
|
image: {
|
|
193
|
-
data: imageData.toString("base64"),
|
|
135
|
+
data: Buffer.from(imageData).toString("base64"),
|
|
194
136
|
mimeType: baseMimeType(selectedType),
|
|
195
137
|
},
|
|
196
138
|
};
|
|
197
139
|
}
|
|
198
140
|
|
|
199
|
-
async function readImageX11(
|
|
200
|
-
const
|
|
201
|
-
if (!xclipPath) return { status: "unavailable" };
|
|
202
|
-
|
|
203
|
-
const targets = await spawnAndRead([xclipPath, "-selection", "clipboard", "-t", "TARGETS", "-o"], timeout);
|
|
141
|
+
async function readImageX11(): Promise<ClipboardReadResult> {
|
|
142
|
+
const targets = await $`xclip -selection clipboard -t TARGETS -o`.quiet().text();
|
|
204
143
|
if (!targets) return { status: "unavailable" }; // xclip failed (no X server?)
|
|
205
144
|
|
|
206
145
|
const candidateTypes = targets
|
|
207
|
-
.toString("utf-8")
|
|
208
146
|
.split(/\r?\n/)
|
|
209
147
|
.map((t) => t.trim())
|
|
210
148
|
.filter(Boolean);
|
|
@@ -212,19 +150,19 @@ async function readImageX11(timeout: number): Promise<ClipboardReadResult> {
|
|
|
212
150
|
const selectedType = selectPreferredImageMimeType(candidateTypes);
|
|
213
151
|
if (!selectedType) return { status: "empty" }; // Clipboard has no image types
|
|
214
152
|
|
|
215
|
-
const imageData = await
|
|
216
|
-
if (!imageData || imageData.
|
|
153
|
+
const imageData = await $`xclip -selection clipboard -t ${selectedType} -o`.quiet().arrayBuffer();
|
|
154
|
+
if (!imageData || imageData.byteLength === 0) return { status: "empty" };
|
|
217
155
|
|
|
218
156
|
return {
|
|
219
157
|
status: "found",
|
|
220
158
|
image: {
|
|
221
|
-
data: imageData.toString("base64"),
|
|
159
|
+
data: Buffer.from(imageData).toString("base64"),
|
|
222
160
|
mimeType: baseMimeType(selectedType),
|
|
223
161
|
},
|
|
224
162
|
};
|
|
225
163
|
}
|
|
226
164
|
|
|
227
|
-
async function readImageMacOS(
|
|
165
|
+
async function readImageMacOS(): Promise<ClipboardImage | null> {
|
|
228
166
|
// Use osascript to check clipboard class and read PNG data
|
|
229
167
|
// First check if clipboard has image data
|
|
230
168
|
const checkScript = `
|
|
@@ -241,15 +179,8 @@ async function readImageMacOS(timeout: number): Promise<ClipboardImage | null> {
|
|
|
241
179
|
end try
|
|
242
180
|
`;
|
|
243
181
|
|
|
244
|
-
const
|
|
245
|
-
const checkResult = await Promise.race([
|
|
246
|
-
new Response(checkProc.stdout).text(),
|
|
247
|
-
new Promise<string>((_, reject) => setTimeout(() => reject(new Error("timeout")), timeout)),
|
|
248
|
-
]).catch(() => "none");
|
|
249
|
-
|
|
250
|
-
await checkProc.exited;
|
|
182
|
+
const checkResult = await $`osascript -e ${checkScript}`.quiet().text();
|
|
251
183
|
const imageType = checkResult.trim();
|
|
252
|
-
|
|
253
184
|
if (imageType === "none") return null;
|
|
254
185
|
|
|
255
186
|
// Read the actual image data using a temp file approach
|
|
@@ -265,20 +196,15 @@ async function readImageMacOS(timeout: number): Promise<ClipboardImage | null> {
|
|
|
265
196
|
close access fileRef
|
|
266
197
|
`;
|
|
267
198
|
|
|
268
|
-
|
|
269
|
-
await Promise.race([
|
|
270
|
-
writeProc.exited,
|
|
271
|
-
new Promise((_, reject) => setTimeout(() => reject(new Error("timeout")), timeout)),
|
|
272
|
-
]).catch(() => null);
|
|
199
|
+
await $`osascript -e ${readScript}`.quiet().text();
|
|
273
200
|
|
|
274
201
|
try {
|
|
275
202
|
const file = Bun.file(tempFile);
|
|
276
203
|
if (await file.exists()) {
|
|
277
|
-
const buffer = await file.
|
|
278
|
-
await Bun.write(tempFile, ""); // Clear file
|
|
204
|
+
const buffer = await file.bytes();
|
|
279
205
|
await unlink(tempFile).catch(() => {});
|
|
280
206
|
|
|
281
|
-
if (buffer.
|
|
207
|
+
if (buffer.length > 0) {
|
|
282
208
|
return {
|
|
283
209
|
data: Buffer.from(buffer).toString("base64"),
|
|
284
210
|
mimeType: imageType === "png" ? "image/png" : "image/jpeg",
|
|
@@ -292,7 +218,7 @@ async function readImageMacOS(timeout: number): Promise<ClipboardImage | null> {
|
|
|
292
218
|
return null;
|
|
293
219
|
}
|
|
294
220
|
|
|
295
|
-
async function readImageWindows(
|
|
221
|
+
async function readImageWindows(): Promise<ClipboardImage | null> {
|
|
296
222
|
// PowerShell script to read image from clipboard as base64
|
|
297
223
|
const script = `
|
|
298
224
|
Add-Type -AssemblyName System.Windows.Forms
|
|
@@ -304,16 +230,6 @@ async function readImageWindows(timeout: number): Promise<ClipboardImage | null>
|
|
|
304
230
|
}
|
|
305
231
|
`;
|
|
306
232
|
|
|
307
|
-
const result = await
|
|
308
|
-
|
|
309
|
-
const base64 = result.toString("utf-8").trim();
|
|
310
|
-
if (base64.length > 0) {
|
|
311
|
-
return {
|
|
312
|
-
data: base64,
|
|
313
|
-
mimeType: "image/png",
|
|
314
|
-
};
|
|
315
|
-
}
|
|
316
|
-
}
|
|
317
|
-
|
|
318
|
-
return null;
|
|
233
|
+
const result = await $`powershell -NoProfile -Command ${script}`.quiet().text();
|
|
234
|
+
return result ? { data: result, mimeType: "image/png" } : null;
|
|
319
235
|
}
|
|
@@ -6,11 +6,14 @@
|
|
|
6
6
|
* shell experience.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
+
import { unlinkSync } from "node:fs";
|
|
10
|
+
import { mkdir } from "node:fs/promises";
|
|
9
11
|
import { homedir, tmpdir } from "node:os";
|
|
10
12
|
import { join } from "node:path";
|
|
13
|
+
import { postmortem } from "@oh-my-pi/pi-utils";
|
|
14
|
+
import { $ } from "bun";
|
|
11
15
|
|
|
12
16
|
let cachedSnapshotPath: string | null = null;
|
|
13
|
-
let cleanupRegistered = false;
|
|
14
17
|
|
|
15
18
|
/**
|
|
16
19
|
* Get the user's shell config file path.
|
|
@@ -129,32 +132,19 @@ export async function getOrCreateSnapshot(
|
|
|
129
132
|
|
|
130
133
|
// Create snapshot directory
|
|
131
134
|
const snapshotDir = join(tmpdir(), "omp-shell-snapshots");
|
|
132
|
-
|
|
133
|
-
if (mkdirProc.exitCode !== 0) {
|
|
134
|
-
return null;
|
|
135
|
-
}
|
|
135
|
+
await mkdir(snapshotDir, { recursive: true });
|
|
136
136
|
|
|
137
137
|
// Generate unique snapshot path
|
|
138
|
-
const timestamp = Date.now();
|
|
139
|
-
const random = Math.random().toString(36).substring(2, 8);
|
|
140
138
|
const shellName = shell.includes("zsh") ? "zsh" : shell.includes("bash") ? "bash" : "sh";
|
|
141
|
-
const snapshotPath = join(snapshotDir, `snapshot-${shellName}-${
|
|
139
|
+
const snapshotPath = join(snapshotDir, `snapshot-${shellName}-${crypto.randomUUID()}.sh`);
|
|
142
140
|
|
|
143
141
|
// Generate and execute snapshot script
|
|
144
142
|
const script = await generateSnapshotScript(shell, snapshotPath, rcFile);
|
|
145
143
|
|
|
146
144
|
try {
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
stdout: "pipe",
|
|
150
|
-
stderr: "pipe",
|
|
151
|
-
env,
|
|
152
|
-
timeout: 10000, // 10 second timeout
|
|
153
|
-
});
|
|
154
|
-
|
|
155
|
-
if (result.exitCode === 0 && (await Bun.file(snapshotPath).exists())) {
|
|
145
|
+
await $`${shell} -l -c ${script}`.env(env).quiet().text();
|
|
146
|
+
if (await Bun.file(snapshotPath).exists()) {
|
|
156
147
|
cachedSnapshotPath = snapshotPath;
|
|
157
|
-
registerCleanup();
|
|
158
148
|
return snapshotPath;
|
|
159
149
|
}
|
|
160
150
|
} catch {
|
|
@@ -175,46 +165,8 @@ export function getSnapshotSourceCommand(snapshotPath: string | null): string {
|
|
|
175
165
|
return `source '${escaped}' 2>/dev/null && `;
|
|
176
166
|
}
|
|
177
167
|
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
function registerCleanup(): void {
|
|
182
|
-
if (cleanupRegistered) return;
|
|
183
|
-
cleanupRegistered = true;
|
|
184
|
-
|
|
185
|
-
const cleanup = async () => {
|
|
186
|
-
if (cachedSnapshotPath && (await Bun.file(cachedSnapshotPath).exists())) {
|
|
187
|
-
try {
|
|
188
|
-
Bun.spawnSync(["rm", cachedSnapshotPath]);
|
|
189
|
-
} catch {
|
|
190
|
-
// Ignore cleanup errors
|
|
191
|
-
}
|
|
192
|
-
}
|
|
193
|
-
};
|
|
194
|
-
|
|
195
|
-
process.on("exit", () => {
|
|
196
|
-
cleanup();
|
|
197
|
-
});
|
|
198
|
-
process.on("SIGINT", () => {
|
|
199
|
-
cleanup();
|
|
200
|
-
process.exit(130);
|
|
201
|
-
});
|
|
202
|
-
process.on("SIGTERM", () => {
|
|
203
|
-
cleanup();
|
|
204
|
-
process.exit(143);
|
|
205
|
-
});
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
/**
|
|
209
|
-
* Clear the cached snapshot (for testing or forced refresh).
|
|
210
|
-
*/
|
|
211
|
-
export async function clearSnapshotCache(): Promise<void> {
|
|
212
|
-
if (cachedSnapshotPath && (await Bun.file(cachedSnapshotPath).exists())) {
|
|
213
|
-
try {
|
|
214
|
-
Bun.spawnSync(["rm", cachedSnapshotPath]);
|
|
215
|
-
} catch {
|
|
216
|
-
// Ignore
|
|
217
|
-
}
|
|
168
|
+
postmortem.register("shell-snapshot", () => {
|
|
169
|
+
if (cachedSnapshotPath) {
|
|
170
|
+
unlinkSync(cachedSnapshotPath);
|
|
218
171
|
}
|
|
219
|
-
|
|
220
|
-
}
|
|
172
|
+
});
|