@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.
Files changed (224) hide show
  1. package/CHANGELOG.md +1228 -0
  2. package/README.md +1041 -0
  3. package/docs/compaction.md +403 -0
  4. package/docs/custom-tools.md +541 -0
  5. package/docs/extension-loading.md +1004 -0
  6. package/docs/hooks.md +867 -0
  7. package/docs/rpc.md +1040 -0
  8. package/docs/sdk.md +994 -0
  9. package/docs/session-tree-plan.md +441 -0
  10. package/docs/session.md +240 -0
  11. package/docs/skills.md +290 -0
  12. package/docs/theme.md +637 -0
  13. package/docs/tree.md +197 -0
  14. package/docs/tui.md +341 -0
  15. package/examples/README.md +21 -0
  16. package/examples/custom-tools/README.md +124 -0
  17. package/examples/custom-tools/hello/index.ts +20 -0
  18. package/examples/custom-tools/question/index.ts +84 -0
  19. package/examples/custom-tools/subagent/README.md +172 -0
  20. package/examples/custom-tools/subagent/agents/planner.md +37 -0
  21. package/examples/custom-tools/subagent/agents/reviewer.md +35 -0
  22. package/examples/custom-tools/subagent/agents/scout.md +50 -0
  23. package/examples/custom-tools/subagent/agents/worker.md +24 -0
  24. package/examples/custom-tools/subagent/agents.ts +156 -0
  25. package/examples/custom-tools/subagent/commands/implement-and-review.md +10 -0
  26. package/examples/custom-tools/subagent/commands/implement.md +10 -0
  27. package/examples/custom-tools/subagent/commands/scout-and-plan.md +9 -0
  28. package/examples/custom-tools/subagent/index.ts +1002 -0
  29. package/examples/custom-tools/todo/index.ts +212 -0
  30. package/examples/hooks/README.md +56 -0
  31. package/examples/hooks/auto-commit-on-exit.ts +49 -0
  32. package/examples/hooks/confirm-destructive.ts +59 -0
  33. package/examples/hooks/custom-compaction.ts +116 -0
  34. package/examples/hooks/dirty-repo-guard.ts +52 -0
  35. package/examples/hooks/file-trigger.ts +41 -0
  36. package/examples/hooks/git-checkpoint.ts +53 -0
  37. package/examples/hooks/handoff.ts +150 -0
  38. package/examples/hooks/permission-gate.ts +34 -0
  39. package/examples/hooks/protected-paths.ts +30 -0
  40. package/examples/hooks/qna.ts +119 -0
  41. package/examples/hooks/snake.ts +343 -0
  42. package/examples/hooks/status-line.ts +40 -0
  43. package/examples/sdk/01-minimal.ts +22 -0
  44. package/examples/sdk/02-custom-model.ts +49 -0
  45. package/examples/sdk/03-custom-prompt.ts +44 -0
  46. package/examples/sdk/04-skills.ts +44 -0
  47. package/examples/sdk/05-tools.ts +90 -0
  48. package/examples/sdk/06-hooks.ts +61 -0
  49. package/examples/sdk/07-context-files.ts +36 -0
  50. package/examples/sdk/08-slash-commands.ts +42 -0
  51. package/examples/sdk/09-api-keys-and-oauth.ts +55 -0
  52. package/examples/sdk/10-settings.ts +38 -0
  53. package/examples/sdk/11-sessions.ts +48 -0
  54. package/examples/sdk/12-full-control.ts +95 -0
  55. package/examples/sdk/README.md +154 -0
  56. package/package.json +81 -0
  57. package/src/cli/args.ts +246 -0
  58. package/src/cli/file-processor.ts +72 -0
  59. package/src/cli/list-models.ts +104 -0
  60. package/src/cli/plugin-cli.ts +650 -0
  61. package/src/cli/session-picker.ts +41 -0
  62. package/src/cli.ts +10 -0
  63. package/src/commands/init.md +20 -0
  64. package/src/config.ts +159 -0
  65. package/src/core/agent-session.ts +1900 -0
  66. package/src/core/auth-storage.ts +236 -0
  67. package/src/core/bash-executor.ts +196 -0
  68. package/src/core/compaction/branch-summarization.ts +343 -0
  69. package/src/core/compaction/compaction.ts +742 -0
  70. package/src/core/compaction/index.ts +7 -0
  71. package/src/core/compaction/utils.ts +154 -0
  72. package/src/core/custom-tools/index.ts +21 -0
  73. package/src/core/custom-tools/loader.ts +248 -0
  74. package/src/core/custom-tools/types.ts +169 -0
  75. package/src/core/custom-tools/wrapper.ts +28 -0
  76. package/src/core/exec.ts +129 -0
  77. package/src/core/export-html/index.ts +211 -0
  78. package/src/core/export-html/template.css +781 -0
  79. package/src/core/export-html/template.html +54 -0
  80. package/src/core/export-html/template.js +1185 -0
  81. package/src/core/export-html/vendor/highlight.min.js +1213 -0
  82. package/src/core/export-html/vendor/marked.min.js +6 -0
  83. package/src/core/hooks/index.ts +16 -0
  84. package/src/core/hooks/loader.ts +312 -0
  85. package/src/core/hooks/runner.ts +434 -0
  86. package/src/core/hooks/tool-wrapper.ts +99 -0
  87. package/src/core/hooks/types.ts +773 -0
  88. package/src/core/index.ts +52 -0
  89. package/src/core/mcp/client.ts +158 -0
  90. package/src/core/mcp/config.ts +154 -0
  91. package/src/core/mcp/index.ts +45 -0
  92. package/src/core/mcp/loader.ts +68 -0
  93. package/src/core/mcp/manager.ts +181 -0
  94. package/src/core/mcp/tool-bridge.ts +148 -0
  95. package/src/core/mcp/transports/http.ts +316 -0
  96. package/src/core/mcp/transports/index.ts +6 -0
  97. package/src/core/mcp/transports/stdio.ts +252 -0
  98. package/src/core/mcp/types.ts +220 -0
  99. package/src/core/messages.ts +189 -0
  100. package/src/core/model-registry.ts +317 -0
  101. package/src/core/model-resolver.ts +393 -0
  102. package/src/core/plugins/doctor.ts +59 -0
  103. package/src/core/plugins/index.ts +38 -0
  104. package/src/core/plugins/installer.ts +189 -0
  105. package/src/core/plugins/loader.ts +338 -0
  106. package/src/core/plugins/manager.ts +672 -0
  107. package/src/core/plugins/parser.ts +105 -0
  108. package/src/core/plugins/paths.ts +32 -0
  109. package/src/core/plugins/types.ts +190 -0
  110. package/src/core/sdk.ts +760 -0
  111. package/src/core/session-manager.ts +1128 -0
  112. package/src/core/settings-manager.ts +443 -0
  113. package/src/core/skills.ts +437 -0
  114. package/src/core/slash-commands.ts +248 -0
  115. package/src/core/system-prompt.ts +439 -0
  116. package/src/core/timings.ts +25 -0
  117. package/src/core/tools/ask.ts +211 -0
  118. package/src/core/tools/bash-interceptor.ts +120 -0
  119. package/src/core/tools/bash.ts +250 -0
  120. package/src/core/tools/context.ts +32 -0
  121. package/src/core/tools/edit-diff.ts +475 -0
  122. package/src/core/tools/edit.ts +208 -0
  123. package/src/core/tools/exa/company.ts +59 -0
  124. package/src/core/tools/exa/index.ts +64 -0
  125. package/src/core/tools/exa/linkedin.ts +59 -0
  126. package/src/core/tools/exa/logger.ts +56 -0
  127. package/src/core/tools/exa/mcp-client.ts +368 -0
  128. package/src/core/tools/exa/render.ts +196 -0
  129. package/src/core/tools/exa/researcher.ts +90 -0
  130. package/src/core/tools/exa/search.ts +337 -0
  131. package/src/core/tools/exa/types.ts +168 -0
  132. package/src/core/tools/exa/websets.ts +248 -0
  133. package/src/core/tools/find.ts +261 -0
  134. package/src/core/tools/grep.ts +555 -0
  135. package/src/core/tools/index.ts +202 -0
  136. package/src/core/tools/ls.ts +140 -0
  137. package/src/core/tools/lsp/client.ts +605 -0
  138. package/src/core/tools/lsp/config.ts +147 -0
  139. package/src/core/tools/lsp/edits.ts +101 -0
  140. package/src/core/tools/lsp/index.ts +804 -0
  141. package/src/core/tools/lsp/render.ts +447 -0
  142. package/src/core/tools/lsp/rust-analyzer.ts +145 -0
  143. package/src/core/tools/lsp/types.ts +463 -0
  144. package/src/core/tools/lsp/utils.ts +486 -0
  145. package/src/core/tools/notebook.ts +229 -0
  146. package/src/core/tools/path-utils.ts +61 -0
  147. package/src/core/tools/read.ts +240 -0
  148. package/src/core/tools/renderers.ts +540 -0
  149. package/src/core/tools/task/agents.ts +153 -0
  150. package/src/core/tools/task/artifacts.ts +114 -0
  151. package/src/core/tools/task/bundled-agents/browser.md +71 -0
  152. package/src/core/tools/task/bundled-agents/explore.md +82 -0
  153. package/src/core/tools/task/bundled-agents/plan.md +54 -0
  154. package/src/core/tools/task/bundled-agents/reviewer.md +59 -0
  155. package/src/core/tools/task/bundled-agents/task.md +53 -0
  156. package/src/core/tools/task/bundled-commands/architect-plan.md +10 -0
  157. package/src/core/tools/task/bundled-commands/implement-with-critic.md +11 -0
  158. package/src/core/tools/task/bundled-commands/implement.md +11 -0
  159. package/src/core/tools/task/commands.ts +213 -0
  160. package/src/core/tools/task/discovery.ts +208 -0
  161. package/src/core/tools/task/executor.ts +367 -0
  162. package/src/core/tools/task/index.ts +388 -0
  163. package/src/core/tools/task/model-resolver.ts +115 -0
  164. package/src/core/tools/task/parallel.ts +38 -0
  165. package/src/core/tools/task/render.ts +232 -0
  166. package/src/core/tools/task/types.ts +99 -0
  167. package/src/core/tools/truncate.ts +265 -0
  168. package/src/core/tools/web-fetch.ts +2370 -0
  169. package/src/core/tools/web-search/auth.ts +193 -0
  170. package/src/core/tools/web-search/index.ts +537 -0
  171. package/src/core/tools/web-search/providers/anthropic.ts +198 -0
  172. package/src/core/tools/web-search/providers/exa.ts +302 -0
  173. package/src/core/tools/web-search/providers/perplexity.ts +195 -0
  174. package/src/core/tools/web-search/render.ts +182 -0
  175. package/src/core/tools/web-search/types.ts +180 -0
  176. package/src/core/tools/write.ts +99 -0
  177. package/src/index.ts +176 -0
  178. package/src/main.ts +464 -0
  179. package/src/migrations.ts +135 -0
  180. package/src/modes/index.ts +43 -0
  181. package/src/modes/interactive/components/armin.ts +382 -0
  182. package/src/modes/interactive/components/assistant-message.ts +86 -0
  183. package/src/modes/interactive/components/bash-execution.ts +196 -0
  184. package/src/modes/interactive/components/bordered-loader.ts +41 -0
  185. package/src/modes/interactive/components/branch-summary-message.ts +42 -0
  186. package/src/modes/interactive/components/compaction-summary-message.ts +45 -0
  187. package/src/modes/interactive/components/custom-editor.ts +122 -0
  188. package/src/modes/interactive/components/diff.ts +147 -0
  189. package/src/modes/interactive/components/dynamic-border.ts +25 -0
  190. package/src/modes/interactive/components/footer.ts +381 -0
  191. package/src/modes/interactive/components/hook-editor.ts +117 -0
  192. package/src/modes/interactive/components/hook-input.ts +64 -0
  193. package/src/modes/interactive/components/hook-message.ts +96 -0
  194. package/src/modes/interactive/components/hook-selector.ts +91 -0
  195. package/src/modes/interactive/components/model-selector.ts +247 -0
  196. package/src/modes/interactive/components/oauth-selector.ts +120 -0
  197. package/src/modes/interactive/components/plugin-settings.ts +479 -0
  198. package/src/modes/interactive/components/queue-mode-selector.ts +56 -0
  199. package/src/modes/interactive/components/session-selector.ts +204 -0
  200. package/src/modes/interactive/components/settings-selector.ts +453 -0
  201. package/src/modes/interactive/components/show-images-selector.ts +45 -0
  202. package/src/modes/interactive/components/theme-selector.ts +62 -0
  203. package/src/modes/interactive/components/thinking-selector.ts +64 -0
  204. package/src/modes/interactive/components/tool-execution.ts +675 -0
  205. package/src/modes/interactive/components/tree-selector.ts +866 -0
  206. package/src/modes/interactive/components/user-message-selector.ts +159 -0
  207. package/src/modes/interactive/components/user-message.ts +18 -0
  208. package/src/modes/interactive/components/visual-truncate.ts +50 -0
  209. package/src/modes/interactive/components/welcome.ts +183 -0
  210. package/src/modes/interactive/interactive-mode.ts +2516 -0
  211. package/src/modes/interactive/theme/dark.json +101 -0
  212. package/src/modes/interactive/theme/light.json +98 -0
  213. package/src/modes/interactive/theme/theme-schema.json +308 -0
  214. package/src/modes/interactive/theme/theme.ts +998 -0
  215. package/src/modes/print-mode.ts +128 -0
  216. package/src/modes/rpc/rpc-client.ts +527 -0
  217. package/src/modes/rpc/rpc-mode.ts +483 -0
  218. package/src/modes/rpc/rpc-types.ts +203 -0
  219. package/src/utils/changelog.ts +99 -0
  220. package/src/utils/clipboard.ts +265 -0
  221. package/src/utils/fuzzy.ts +108 -0
  222. package/src/utils/mime.ts +30 -0
  223. package/src/utils/shell.ts +276 -0
  224. 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
+ }