@oh-my-pi/pi-coding-agent 1.337.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 +1228 -0
- package/README.md +1041 -0
- package/docs/compaction.md +403 -0
- package/docs/custom-tools.md +541 -0
- package/docs/extension-loading.md +1004 -0
- package/docs/hooks.md +867 -0
- package/docs/rpc.md +1040 -0
- package/docs/sdk.md +994 -0
- package/docs/session-tree-plan.md +441 -0
- package/docs/session.md +240 -0
- package/docs/skills.md +290 -0
- package/docs/theme.md +637 -0
- package/docs/tree.md +197 -0
- package/docs/tui.md +341 -0
- package/examples/README.md +21 -0
- package/examples/custom-tools/README.md +124 -0
- package/examples/custom-tools/hello/index.ts +20 -0
- package/examples/custom-tools/question/index.ts +84 -0
- package/examples/custom-tools/subagent/README.md +172 -0
- package/examples/custom-tools/subagent/agents/planner.md +37 -0
- package/examples/custom-tools/subagent/agents/reviewer.md +35 -0
- package/examples/custom-tools/subagent/agents/scout.md +50 -0
- package/examples/custom-tools/subagent/agents/worker.md +24 -0
- package/examples/custom-tools/subagent/agents.ts +156 -0
- package/examples/custom-tools/subagent/commands/implement-and-review.md +10 -0
- package/examples/custom-tools/subagent/commands/implement.md +10 -0
- package/examples/custom-tools/subagent/commands/scout-and-plan.md +9 -0
- package/examples/custom-tools/subagent/index.ts +1002 -0
- package/examples/custom-tools/todo/index.ts +212 -0
- package/examples/hooks/README.md +56 -0
- package/examples/hooks/auto-commit-on-exit.ts +49 -0
- package/examples/hooks/confirm-destructive.ts +59 -0
- package/examples/hooks/custom-compaction.ts +116 -0
- package/examples/hooks/dirty-repo-guard.ts +52 -0
- package/examples/hooks/file-trigger.ts +41 -0
- package/examples/hooks/git-checkpoint.ts +53 -0
- package/examples/hooks/handoff.ts +150 -0
- package/examples/hooks/permission-gate.ts +34 -0
- package/examples/hooks/protected-paths.ts +30 -0
- package/examples/hooks/qna.ts +119 -0
- package/examples/hooks/snake.ts +343 -0
- package/examples/hooks/status-line.ts +40 -0
- package/examples/sdk/01-minimal.ts +22 -0
- package/examples/sdk/02-custom-model.ts +49 -0
- package/examples/sdk/03-custom-prompt.ts +44 -0
- package/examples/sdk/04-skills.ts +44 -0
- package/examples/sdk/05-tools.ts +90 -0
- package/examples/sdk/06-hooks.ts +61 -0
- package/examples/sdk/07-context-files.ts +36 -0
- package/examples/sdk/08-slash-commands.ts +42 -0
- package/examples/sdk/09-api-keys-and-oauth.ts +55 -0
- package/examples/sdk/10-settings.ts +38 -0
- package/examples/sdk/11-sessions.ts +48 -0
- package/examples/sdk/12-full-control.ts +95 -0
- package/examples/sdk/README.md +154 -0
- package/package.json +81 -0
- package/src/cli/args.ts +246 -0
- package/src/cli/file-processor.ts +72 -0
- package/src/cli/list-models.ts +104 -0
- package/src/cli/plugin-cli.ts +650 -0
- package/src/cli/session-picker.ts +41 -0
- package/src/cli.ts +10 -0
- package/src/commands/init.md +20 -0
- package/src/config.ts +159 -0
- package/src/core/agent-session.ts +1900 -0
- package/src/core/auth-storage.ts +236 -0
- package/src/core/bash-executor.ts +196 -0
- package/src/core/compaction/branch-summarization.ts +343 -0
- package/src/core/compaction/compaction.ts +742 -0
- package/src/core/compaction/index.ts +7 -0
- package/src/core/compaction/utils.ts +154 -0
- package/src/core/custom-tools/index.ts +21 -0
- package/src/core/custom-tools/loader.ts +248 -0
- package/src/core/custom-tools/types.ts +169 -0
- package/src/core/custom-tools/wrapper.ts +28 -0
- package/src/core/exec.ts +129 -0
- package/src/core/export-html/index.ts +211 -0
- package/src/core/export-html/template.css +781 -0
- package/src/core/export-html/template.html +54 -0
- package/src/core/export-html/template.js +1185 -0
- package/src/core/export-html/vendor/highlight.min.js +1213 -0
- package/src/core/export-html/vendor/marked.min.js +6 -0
- package/src/core/hooks/index.ts +16 -0
- package/src/core/hooks/loader.ts +312 -0
- package/src/core/hooks/runner.ts +434 -0
- package/src/core/hooks/tool-wrapper.ts +99 -0
- package/src/core/hooks/types.ts +773 -0
- package/src/core/index.ts +52 -0
- package/src/core/mcp/client.ts +158 -0
- package/src/core/mcp/config.ts +154 -0
- package/src/core/mcp/index.ts +45 -0
- package/src/core/mcp/loader.ts +68 -0
- package/src/core/mcp/manager.ts +181 -0
- package/src/core/mcp/tool-bridge.ts +148 -0
- package/src/core/mcp/transports/http.ts +316 -0
- package/src/core/mcp/transports/index.ts +6 -0
- package/src/core/mcp/transports/stdio.ts +252 -0
- package/src/core/mcp/types.ts +220 -0
- package/src/core/messages.ts +189 -0
- package/src/core/model-registry.ts +317 -0
- package/src/core/model-resolver.ts +393 -0
- package/src/core/plugins/doctor.ts +59 -0
- package/src/core/plugins/index.ts +38 -0
- package/src/core/plugins/installer.ts +189 -0
- package/src/core/plugins/loader.ts +338 -0
- package/src/core/plugins/manager.ts +672 -0
- package/src/core/plugins/parser.ts +105 -0
- package/src/core/plugins/paths.ts +32 -0
- package/src/core/plugins/types.ts +190 -0
- package/src/core/sdk.ts +760 -0
- package/src/core/session-manager.ts +1128 -0
- package/src/core/settings-manager.ts +443 -0
- package/src/core/skills.ts +437 -0
- package/src/core/slash-commands.ts +248 -0
- package/src/core/system-prompt.ts +439 -0
- package/src/core/timings.ts +25 -0
- package/src/core/tools/ask.ts +211 -0
- package/src/core/tools/bash-interceptor.ts +120 -0
- package/src/core/tools/bash.ts +250 -0
- package/src/core/tools/context.ts +32 -0
- package/src/core/tools/edit-diff.ts +475 -0
- package/src/core/tools/edit.ts +208 -0
- package/src/core/tools/exa/company.ts +59 -0
- package/src/core/tools/exa/index.ts +64 -0
- package/src/core/tools/exa/linkedin.ts +59 -0
- package/src/core/tools/exa/logger.ts +56 -0
- package/src/core/tools/exa/mcp-client.ts +368 -0
- package/src/core/tools/exa/render.ts +196 -0
- package/src/core/tools/exa/researcher.ts +90 -0
- package/src/core/tools/exa/search.ts +337 -0
- package/src/core/tools/exa/types.ts +168 -0
- package/src/core/tools/exa/websets.ts +248 -0
- package/src/core/tools/find.ts +261 -0
- package/src/core/tools/grep.ts +555 -0
- package/src/core/tools/index.ts +202 -0
- package/src/core/tools/ls.ts +140 -0
- package/src/core/tools/lsp/client.ts +605 -0
- package/src/core/tools/lsp/config.ts +147 -0
- package/src/core/tools/lsp/edits.ts +101 -0
- package/src/core/tools/lsp/index.ts +804 -0
- package/src/core/tools/lsp/render.ts +447 -0
- package/src/core/tools/lsp/rust-analyzer.ts +145 -0
- package/src/core/tools/lsp/types.ts +463 -0
- package/src/core/tools/lsp/utils.ts +486 -0
- package/src/core/tools/notebook.ts +229 -0
- package/src/core/tools/path-utils.ts +61 -0
- package/src/core/tools/read.ts +240 -0
- package/src/core/tools/renderers.ts +540 -0
- package/src/core/tools/task/agents.ts +153 -0
- package/src/core/tools/task/artifacts.ts +114 -0
- package/src/core/tools/task/bundled-agents/browser.md +71 -0
- package/src/core/tools/task/bundled-agents/explore.md +82 -0
- package/src/core/tools/task/bundled-agents/plan.md +54 -0
- package/src/core/tools/task/bundled-agents/reviewer.md +59 -0
- package/src/core/tools/task/bundled-agents/task.md +53 -0
- package/src/core/tools/task/bundled-commands/architect-plan.md +10 -0
- package/src/core/tools/task/bundled-commands/implement-with-critic.md +11 -0
- package/src/core/tools/task/bundled-commands/implement.md +11 -0
- package/src/core/tools/task/commands.ts +213 -0
- package/src/core/tools/task/discovery.ts +208 -0
- package/src/core/tools/task/executor.ts +367 -0
- package/src/core/tools/task/index.ts +388 -0
- package/src/core/tools/task/model-resolver.ts +115 -0
- package/src/core/tools/task/parallel.ts +38 -0
- package/src/core/tools/task/render.ts +232 -0
- package/src/core/tools/task/types.ts +99 -0
- package/src/core/tools/truncate.ts +265 -0
- package/src/core/tools/web-fetch.ts +2370 -0
- package/src/core/tools/web-search/auth.ts +193 -0
- package/src/core/tools/web-search/index.ts +537 -0
- package/src/core/tools/web-search/providers/anthropic.ts +198 -0
- package/src/core/tools/web-search/providers/exa.ts +302 -0
- package/src/core/tools/web-search/providers/perplexity.ts +195 -0
- package/src/core/tools/web-search/render.ts +182 -0
- package/src/core/tools/web-search/types.ts +180 -0
- package/src/core/tools/write.ts +99 -0
- package/src/index.ts +176 -0
- package/src/main.ts +464 -0
- package/src/migrations.ts +135 -0
- package/src/modes/index.ts +43 -0
- package/src/modes/interactive/components/armin.ts +382 -0
- package/src/modes/interactive/components/assistant-message.ts +86 -0
- package/src/modes/interactive/components/bash-execution.ts +196 -0
- package/src/modes/interactive/components/bordered-loader.ts +41 -0
- package/src/modes/interactive/components/branch-summary-message.ts +42 -0
- package/src/modes/interactive/components/compaction-summary-message.ts +45 -0
- package/src/modes/interactive/components/custom-editor.ts +122 -0
- package/src/modes/interactive/components/diff.ts +147 -0
- package/src/modes/interactive/components/dynamic-border.ts +25 -0
- package/src/modes/interactive/components/footer.ts +381 -0
- package/src/modes/interactive/components/hook-editor.ts +117 -0
- package/src/modes/interactive/components/hook-input.ts +64 -0
- package/src/modes/interactive/components/hook-message.ts +96 -0
- package/src/modes/interactive/components/hook-selector.ts +91 -0
- package/src/modes/interactive/components/model-selector.ts +247 -0
- package/src/modes/interactive/components/oauth-selector.ts +120 -0
- package/src/modes/interactive/components/plugin-settings.ts +479 -0
- package/src/modes/interactive/components/queue-mode-selector.ts +56 -0
- package/src/modes/interactive/components/session-selector.ts +204 -0
- package/src/modes/interactive/components/settings-selector.ts +453 -0
- package/src/modes/interactive/components/show-images-selector.ts +45 -0
- package/src/modes/interactive/components/theme-selector.ts +62 -0
- package/src/modes/interactive/components/thinking-selector.ts +64 -0
- package/src/modes/interactive/components/tool-execution.ts +675 -0
- package/src/modes/interactive/components/tree-selector.ts +866 -0
- package/src/modes/interactive/components/user-message-selector.ts +159 -0
- package/src/modes/interactive/components/user-message.ts +18 -0
- package/src/modes/interactive/components/visual-truncate.ts +50 -0
- package/src/modes/interactive/components/welcome.ts +183 -0
- package/src/modes/interactive/interactive-mode.ts +2516 -0
- package/src/modes/interactive/theme/dark.json +101 -0
- package/src/modes/interactive/theme/light.json +98 -0
- package/src/modes/interactive/theme/theme-schema.json +308 -0
- package/src/modes/interactive/theme/theme.ts +998 -0
- package/src/modes/print-mode.ts +128 -0
- package/src/modes/rpc/rpc-client.ts +527 -0
- package/src/modes/rpc/rpc-mode.ts +483 -0
- package/src/modes/rpc/rpc-types.ts +203 -0
- package/src/utils/changelog.ts +99 -0
- package/src/utils/clipboard.ts +265 -0
- package/src/utils/fuzzy.ts +108 -0
- package/src/utils/mime.ts +30 -0
- package/src/utils/shell.ts +276 -0
- package/src/utils/tools-manager.ts +274 -0
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from "fs";
|
|
2
|
+
|
|
3
|
+
export interface ChangelogEntry {
|
|
4
|
+
major: number;
|
|
5
|
+
minor: number;
|
|
6
|
+
patch: number;
|
|
7
|
+
content: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Parse changelog entries from CHANGELOG.md
|
|
12
|
+
* Scans for ## lines and collects content until next ## or EOF
|
|
13
|
+
*/
|
|
14
|
+
export function parseChangelog(changelogPath: string): ChangelogEntry[] {
|
|
15
|
+
if (!existsSync(changelogPath)) {
|
|
16
|
+
return [];
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
try {
|
|
20
|
+
const content = readFileSync(changelogPath, "utf-8");
|
|
21
|
+
const lines = content.split("\n");
|
|
22
|
+
const entries: ChangelogEntry[] = [];
|
|
23
|
+
|
|
24
|
+
let currentLines: string[] = [];
|
|
25
|
+
let currentVersion: { major: number; minor: number; patch: number } | null = null;
|
|
26
|
+
|
|
27
|
+
for (const line of lines) {
|
|
28
|
+
// Check if this is a version header (## [x.y.z] ...)
|
|
29
|
+
if (line.startsWith("## ")) {
|
|
30
|
+
// Save previous entry if exists
|
|
31
|
+
if (currentVersion && currentLines.length > 0) {
|
|
32
|
+
entries.push({
|
|
33
|
+
...currentVersion,
|
|
34
|
+
content: currentLines.join("\n").trim(),
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Try to parse version from this line
|
|
39
|
+
const versionMatch = line.match(/##\s+\[?(\d+)\.(\d+)\.(\d+)\]?/);
|
|
40
|
+
if (versionMatch) {
|
|
41
|
+
currentVersion = {
|
|
42
|
+
major: Number.parseInt(versionMatch[1], 10),
|
|
43
|
+
minor: Number.parseInt(versionMatch[2], 10),
|
|
44
|
+
patch: Number.parseInt(versionMatch[3], 10),
|
|
45
|
+
};
|
|
46
|
+
currentLines = [line];
|
|
47
|
+
} else {
|
|
48
|
+
// Reset if we can't parse version
|
|
49
|
+
currentVersion = null;
|
|
50
|
+
currentLines = [];
|
|
51
|
+
}
|
|
52
|
+
} else if (currentVersion) {
|
|
53
|
+
// Collect lines for current version
|
|
54
|
+
currentLines.push(line);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Save last entry
|
|
59
|
+
if (currentVersion && currentLines.length > 0) {
|
|
60
|
+
entries.push({
|
|
61
|
+
...currentVersion,
|
|
62
|
+
content: currentLines.join("\n").trim(),
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return entries;
|
|
67
|
+
} catch (error) {
|
|
68
|
+
console.error(`Warning: Could not parse changelog: ${error}`);
|
|
69
|
+
return [];
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Compare versions. Returns: -1 if v1 < v2, 0 if v1 === v2, 1 if v1 > v2
|
|
75
|
+
*/
|
|
76
|
+
export function compareVersions(v1: ChangelogEntry, v2: ChangelogEntry): number {
|
|
77
|
+
if (v1.major !== v2.major) return v1.major - v2.major;
|
|
78
|
+
if (v1.minor !== v2.minor) return v1.minor - v2.minor;
|
|
79
|
+
return v1.patch - v2.patch;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Get entries newer than lastVersion
|
|
84
|
+
*/
|
|
85
|
+
export function getNewEntries(entries: ChangelogEntry[], lastVersion: string): ChangelogEntry[] {
|
|
86
|
+
// Parse lastVersion
|
|
87
|
+
const parts = lastVersion.split(".").map(Number);
|
|
88
|
+
const last: ChangelogEntry = {
|
|
89
|
+
major: parts[0] || 0,
|
|
90
|
+
minor: parts[1] || 0,
|
|
91
|
+
patch: parts[2] || 0,
|
|
92
|
+
content: "",
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
return entries.filter((entry) => compareVersions(entry, last) > 0);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Re-export getChangelogPath from paths.ts for convenience
|
|
99
|
+
export { getChangelogPath } from "../config.js";
|
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
import { platform } from "os";
|
|
2
|
+
|
|
3
|
+
async function spawnWithTimeout(cmd: string[], input: string, timeoutMs: number): Promise<void> {
|
|
4
|
+
const proc = Bun.spawn(cmd, { stdin: "pipe" });
|
|
5
|
+
|
|
6
|
+
const timeoutPromise = new Promise<never>((_, reject) => {
|
|
7
|
+
setTimeout(() => reject(new Error("Clipboard operation timed out")), timeoutMs);
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
try {
|
|
11
|
+
proc.stdin.write(input);
|
|
12
|
+
proc.stdin.end();
|
|
13
|
+
await Promise.race([proc.exited, timeoutPromise]);
|
|
14
|
+
|
|
15
|
+
if (proc.exitCode !== 0) {
|
|
16
|
+
throw new Error(`Command failed with exit code ${proc.exitCode}`);
|
|
17
|
+
}
|
|
18
|
+
} finally {
|
|
19
|
+
proc.kill();
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
async function spawnAndRead(cmd: string[], timeoutMs: number): Promise<Buffer | null> {
|
|
24
|
+
const proc = Bun.spawn(cmd, { stdout: "pipe", stderr: "pipe" });
|
|
25
|
+
|
|
26
|
+
const timeoutPromise = new Promise<never>((_, reject) => {
|
|
27
|
+
setTimeout(() => reject(new Error("Clipboard operation timed out")), timeoutMs);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
try {
|
|
31
|
+
const [exitCode, stdout] = await Promise.race([
|
|
32
|
+
Promise.all([proc.exited, new Response(proc.stdout).arrayBuffer()]),
|
|
33
|
+
timeoutPromise,
|
|
34
|
+
]);
|
|
35
|
+
|
|
36
|
+
if (exitCode !== 0) {
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return Buffer.from(stdout);
|
|
41
|
+
} catch {
|
|
42
|
+
return null;
|
|
43
|
+
} finally {
|
|
44
|
+
proc.kill();
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export async function copyToClipboard(text: string): Promise<void> {
|
|
49
|
+
const p = platform();
|
|
50
|
+
const timeout = 5000;
|
|
51
|
+
|
|
52
|
+
try {
|
|
53
|
+
if (p === "darwin") {
|
|
54
|
+
await spawnWithTimeout(["pbcopy"], text, timeout);
|
|
55
|
+
} else if (p === "win32") {
|
|
56
|
+
await spawnWithTimeout(["clip"], text, timeout);
|
|
57
|
+
} else {
|
|
58
|
+
// Linux - try xclip first, fall back to xsel
|
|
59
|
+
try {
|
|
60
|
+
await spawnWithTimeout(["xclip", "-selection", "clipboard"], text, timeout);
|
|
61
|
+
} catch {
|
|
62
|
+
await spawnWithTimeout(["xsel", "--clipboard", "--input"], text, timeout);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
} catch (error) {
|
|
66
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
67
|
+
if (p === "linux") {
|
|
68
|
+
throw new Error(`Failed to copy to clipboard. Install xclip or xsel: ${msg}`);
|
|
69
|
+
}
|
|
70
|
+
throw new Error(`Failed to copy to clipboard: ${msg}`);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export interface ClipboardImage {
|
|
75
|
+
data: string; // base64 encoded
|
|
76
|
+
mimeType: string;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Read image from system clipboard if available.
|
|
81
|
+
* Returns null if no image is in clipboard or clipboard access fails.
|
|
82
|
+
*
|
|
83
|
+
* Supported platforms:
|
|
84
|
+
* - Linux: requires xclip
|
|
85
|
+
* - macOS: uses osascript + pbpaste
|
|
86
|
+
* - Windows: uses PowerShell
|
|
87
|
+
*/
|
|
88
|
+
export async function readImageFromClipboard(): Promise<ClipboardImage | null> {
|
|
89
|
+
const p = platform();
|
|
90
|
+
const timeout = 3000;
|
|
91
|
+
|
|
92
|
+
try {
|
|
93
|
+
if (p === "linux") {
|
|
94
|
+
return await readImageLinux(timeout);
|
|
95
|
+
} else if (p === "darwin") {
|
|
96
|
+
return await readImageMacOS(timeout);
|
|
97
|
+
} else if (p === "win32") {
|
|
98
|
+
return await readImageWindows(timeout);
|
|
99
|
+
}
|
|
100
|
+
} catch {
|
|
101
|
+
// Clipboard access failed silently
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return null;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
async function readImageLinux(timeout: number): Promise<ClipboardImage | null> {
|
|
108
|
+
// Try Wayland first (wl-paste), then X11 (xclip)
|
|
109
|
+
const wayland = await readImageWayland(timeout);
|
|
110
|
+
if (wayland) return wayland;
|
|
111
|
+
|
|
112
|
+
return await readImageX11(timeout);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
async function readImageWayland(timeout: number): Promise<ClipboardImage | null> {
|
|
116
|
+
// wl-paste --list-types shows available MIME types
|
|
117
|
+
const types = await spawnAndRead(["wl-paste", "--list-types"], timeout);
|
|
118
|
+
if (!types) return null;
|
|
119
|
+
|
|
120
|
+
const typeList = types.toString("utf-8");
|
|
121
|
+
|
|
122
|
+
// Try PNG first, then JPEG
|
|
123
|
+
const imageTypes = [
|
|
124
|
+
{ type: "image/png", mimeType: "image/png" },
|
|
125
|
+
{ type: "image/jpeg", mimeType: "image/jpeg" },
|
|
126
|
+
];
|
|
127
|
+
|
|
128
|
+
for (const { type, mimeType } of imageTypes) {
|
|
129
|
+
if (typeList.includes(type)) {
|
|
130
|
+
const imageData = await spawnAndRead(["wl-paste", "--type", type], timeout);
|
|
131
|
+
if (imageData && imageData.length > 0) {
|
|
132
|
+
return {
|
|
133
|
+
data: imageData.toString("base64"),
|
|
134
|
+
mimeType,
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return null;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
async function readImageX11(timeout: number): Promise<ClipboardImage | null> {
|
|
144
|
+
// Check available targets in clipboard
|
|
145
|
+
const targets = await spawnAndRead(["xclip", "-selection", "clipboard", "-t", "TARGETS", "-o"], timeout);
|
|
146
|
+
if (!targets) return null;
|
|
147
|
+
|
|
148
|
+
const targetList = targets.toString("utf-8");
|
|
149
|
+
|
|
150
|
+
// Try PNG first (preferred), then JPEG
|
|
151
|
+
const imageTypes = [
|
|
152
|
+
{ target: "image/png", mimeType: "image/png" },
|
|
153
|
+
{ target: "image/jpeg", mimeType: "image/jpeg" },
|
|
154
|
+
{ target: "image/jpg", mimeType: "image/jpeg" },
|
|
155
|
+
];
|
|
156
|
+
|
|
157
|
+
for (const { target, mimeType } of imageTypes) {
|
|
158
|
+
if (targetList.includes(target)) {
|
|
159
|
+
const imageData = await spawnAndRead(["xclip", "-selection", "clipboard", "-t", target, "-o"], timeout);
|
|
160
|
+
if (imageData && imageData.length > 0) {
|
|
161
|
+
return {
|
|
162
|
+
data: imageData.toString("base64"),
|
|
163
|
+
mimeType,
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
return null;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
async function readImageMacOS(timeout: number): Promise<ClipboardImage | null> {
|
|
173
|
+
// Use osascript to check clipboard class and read PNG data
|
|
174
|
+
// First check if clipboard has image data
|
|
175
|
+
const checkScript = `
|
|
176
|
+
try
|
|
177
|
+
clipboard info for «class PNGf»
|
|
178
|
+
return "png"
|
|
179
|
+
on error
|
|
180
|
+
try
|
|
181
|
+
clipboard info for «class JPEG»
|
|
182
|
+
return "jpeg"
|
|
183
|
+
on error
|
|
184
|
+
return "none"
|
|
185
|
+
end try
|
|
186
|
+
end try
|
|
187
|
+
`;
|
|
188
|
+
|
|
189
|
+
const checkProc = Bun.spawn(["osascript", "-e", checkScript], { stdout: "pipe", stderr: "pipe" });
|
|
190
|
+
const checkResult = await Promise.race([
|
|
191
|
+
new Response(checkProc.stdout).text(),
|
|
192
|
+
new Promise<string>((_, reject) => setTimeout(() => reject(new Error("timeout")), timeout)),
|
|
193
|
+
]).catch(() => "none");
|
|
194
|
+
|
|
195
|
+
await checkProc.exited;
|
|
196
|
+
const imageType = checkResult.trim();
|
|
197
|
+
|
|
198
|
+
if (imageType === "none") return null;
|
|
199
|
+
|
|
200
|
+
// Read the actual image data using a temp file approach
|
|
201
|
+
// osascript can't output binary directly, so we write to a temp file
|
|
202
|
+
const tempFile = `/tmp/pi-clipboard-${Date.now()}.${imageType === "png" ? "png" : "jpg"}`;
|
|
203
|
+
const clipboardClass = imageType === "png" ? "«class PNGf»" : "«class JPEG»";
|
|
204
|
+
|
|
205
|
+
const readScript = `
|
|
206
|
+
set imageData to the clipboard as ${clipboardClass}
|
|
207
|
+
set filePath to POSIX file "${tempFile}"
|
|
208
|
+
set fileRef to open for access filePath with write permission
|
|
209
|
+
write imageData to fileRef
|
|
210
|
+
close access fileRef
|
|
211
|
+
`;
|
|
212
|
+
|
|
213
|
+
const writeProc = Bun.spawn(["osascript", "-e", readScript], { stdout: "pipe", stderr: "pipe" });
|
|
214
|
+
await Promise.race([
|
|
215
|
+
writeProc.exited,
|
|
216
|
+
new Promise((_, reject) => setTimeout(() => reject(new Error("timeout")), timeout)),
|
|
217
|
+
]).catch(() => null);
|
|
218
|
+
|
|
219
|
+
try {
|
|
220
|
+
const file = Bun.file(tempFile);
|
|
221
|
+
if (await file.exists()) {
|
|
222
|
+
const buffer = await file.arrayBuffer();
|
|
223
|
+
await Bun.write(tempFile, ""); // Clear file
|
|
224
|
+
const { unlink } = await import("fs/promises");
|
|
225
|
+
await unlink(tempFile).catch(() => {});
|
|
226
|
+
|
|
227
|
+
if (buffer.byteLength > 0) {
|
|
228
|
+
return {
|
|
229
|
+
data: Buffer.from(buffer).toString("base64"),
|
|
230
|
+
mimeType: imageType === "png" ? "image/png" : "image/jpeg",
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
} catch {
|
|
235
|
+
// File read failed
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
return null;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
async function readImageWindows(timeout: number): Promise<ClipboardImage | null> {
|
|
242
|
+
// PowerShell script to read image from clipboard as base64
|
|
243
|
+
const script = `
|
|
244
|
+
Add-Type -AssemblyName System.Windows.Forms
|
|
245
|
+
$clipboard = [System.Windows.Forms.Clipboard]::GetImage()
|
|
246
|
+
if ($clipboard -ne $null) {
|
|
247
|
+
$ms = New-Object System.IO.MemoryStream
|
|
248
|
+
$clipboard.Save($ms, [System.Drawing.Imaging.ImageFormat]::Png)
|
|
249
|
+
[Convert]::ToBase64String($ms.ToArray())
|
|
250
|
+
}
|
|
251
|
+
`;
|
|
252
|
+
|
|
253
|
+
const result = await spawnAndRead(["powershell", "-NoProfile", "-Command", script], timeout);
|
|
254
|
+
if (result && result.length > 0) {
|
|
255
|
+
const base64 = result.toString("utf-8").trim();
|
|
256
|
+
if (base64.length > 0) {
|
|
257
|
+
return {
|
|
258
|
+
data: base64,
|
|
259
|
+
mimeType: "image/png",
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
return null;
|
|
265
|
+
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
// Fuzzy search. Matches if all query characters appear in order (not necessarily consecutive).
|
|
2
|
+
// Lower score = better match.
|
|
3
|
+
|
|
4
|
+
export interface FuzzyMatch {
|
|
5
|
+
matches: boolean;
|
|
6
|
+
score: number;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function fuzzyMatch(query: string, text: string): FuzzyMatch {
|
|
10
|
+
const queryLower = query.toLowerCase();
|
|
11
|
+
const textLower = text.toLowerCase();
|
|
12
|
+
|
|
13
|
+
if (queryLower.length === 0) {
|
|
14
|
+
return { matches: true, score: 0 };
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
if (queryLower.length > textLower.length) {
|
|
18
|
+
return { matches: false, score: 0 };
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
let queryIndex = 0;
|
|
22
|
+
let score = 0;
|
|
23
|
+
let lastMatchIndex = -1;
|
|
24
|
+
let consecutiveMatches = 0;
|
|
25
|
+
|
|
26
|
+
for (let i = 0; i < textLower.length && queryIndex < queryLower.length; i++) {
|
|
27
|
+
if (textLower[i] === queryLower[queryIndex]) {
|
|
28
|
+
const isWordBoundary = i === 0 || /[\s\-_./]/.test(textLower[i - 1]!);
|
|
29
|
+
|
|
30
|
+
// Reward consecutive character matches (e.g., typing "foo" matches "foobar" better than "f_o_o")
|
|
31
|
+
if (lastMatchIndex === i - 1) {
|
|
32
|
+
consecutiveMatches++;
|
|
33
|
+
score -= consecutiveMatches * 5;
|
|
34
|
+
} else {
|
|
35
|
+
consecutiveMatches = 0;
|
|
36
|
+
// Penalize gaps between matched characters
|
|
37
|
+
if (lastMatchIndex >= 0) {
|
|
38
|
+
score += (i - lastMatchIndex - 1) * 2;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Reward matches at word boundaries (start of words are more likely intentional targets)
|
|
43
|
+
if (isWordBoundary) {
|
|
44
|
+
score -= 10;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Slight penalty for matches later in the string (prefer earlier matches)
|
|
48
|
+
score += i * 0.1;
|
|
49
|
+
|
|
50
|
+
lastMatchIndex = i;
|
|
51
|
+
queryIndex++;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Not all query characters were found in order
|
|
56
|
+
if (queryIndex < queryLower.length) {
|
|
57
|
+
return { matches: false, score: 0 };
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return { matches: true, score };
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Filter and sort items by fuzzy match quality (best matches first)
|
|
64
|
+
// Supports space-separated tokens: all tokens must match, sorted by match count then score
|
|
65
|
+
export function fuzzyFilter<T>(items: T[], query: string, getText: (item: T) => string): T[] {
|
|
66
|
+
if (!query.trim()) {
|
|
67
|
+
return items;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Split query into tokens
|
|
71
|
+
const tokens = query
|
|
72
|
+
.trim()
|
|
73
|
+
.split(/\s+/)
|
|
74
|
+
.filter((t) => t.length > 0);
|
|
75
|
+
|
|
76
|
+
if (tokens.length === 0) {
|
|
77
|
+
return items;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const results: { item: T; totalScore: number }[] = [];
|
|
81
|
+
|
|
82
|
+
for (const item of items) {
|
|
83
|
+
const text = getText(item);
|
|
84
|
+
let totalScore = 0;
|
|
85
|
+
let allMatch = true;
|
|
86
|
+
|
|
87
|
+
// Check each token against the text - ALL must match
|
|
88
|
+
for (const token of tokens) {
|
|
89
|
+
const match = fuzzyMatch(token, text);
|
|
90
|
+
if (match.matches) {
|
|
91
|
+
totalScore += match.score;
|
|
92
|
+
} else {
|
|
93
|
+
allMatch = false;
|
|
94
|
+
break;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Only include if all tokens match
|
|
99
|
+
if (allMatch) {
|
|
100
|
+
results.push({ item, totalScore });
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Sort by score (asc, lower is better)
|
|
105
|
+
results.sort((a, b) => a.totalScore - b.totalScore);
|
|
106
|
+
|
|
107
|
+
return results.map((r) => r.item);
|
|
108
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { open } from "node:fs/promises";
|
|
2
|
+
import { fileTypeFromBuffer } from "file-type";
|
|
3
|
+
|
|
4
|
+
const IMAGE_MIME_TYPES = new Set(["image/jpeg", "image/png", "image/gif", "image/webp"]);
|
|
5
|
+
|
|
6
|
+
const FILE_TYPE_SNIFF_BYTES = 4100;
|
|
7
|
+
|
|
8
|
+
export async function detectSupportedImageMimeTypeFromFile(filePath: string): Promise<string | null> {
|
|
9
|
+
const fileHandle = await open(filePath, "r");
|
|
10
|
+
try {
|
|
11
|
+
const buffer = Buffer.alloc(FILE_TYPE_SNIFF_BYTES);
|
|
12
|
+
const { bytesRead } = await fileHandle.read(buffer, 0, FILE_TYPE_SNIFF_BYTES, 0);
|
|
13
|
+
if (bytesRead === 0) {
|
|
14
|
+
return null;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const fileType = await fileTypeFromBuffer(buffer.subarray(0, bytesRead));
|
|
18
|
+
if (!fileType) {
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
if (!IMAGE_MIME_TYPES.has(fileType.mime)) {
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
return fileType.mime;
|
|
27
|
+
} finally {
|
|
28
|
+
await fileHandle.close();
|
|
29
|
+
}
|
|
30
|
+
}
|