Created
December 18, 2025 12:28
-
-
Save vojtaholik/48482d57fc5fdf3489189c56bb9d20e9 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // Name: Save to MyMind | |
| // Description: Save bookmarks, notes, or images to MyMind | |
| // Shortcut: cmd+shift+s | |
| import "@johnlindquist/kit"; | |
| import { readFile } from "node:fs/promises"; | |
| import { extname, basename } from "node:path"; | |
| const MYMIND_URL = "http://localhost:1234"; | |
| type Action = | |
| | "bookmark-page" | |
| | "bookmark-clipboard" | |
| | "note" | |
| | "image-clipboard" | |
| | "image-file"; | |
| const IMAGE_EXTENSIONS = [".jpg", ".jpeg", ".png", ".gif", ".webp"]; | |
| // Get media type from file extension | |
| function getMediaType(filePath: string): string | null { | |
| const ext = extname(filePath).toLowerCase(); | |
| const map: Record<string, string> = { | |
| ".jpg": "image/jpeg", | |
| ".jpeg": "image/jpeg", | |
| ".png": "image/png", | |
| ".gif": "image/gif", | |
| ".webp": "image/webp", | |
| }; | |
| return map[ext] || null; | |
| } | |
| // Check if a file is an image based on extension | |
| function isImageFile(filePath: string): boolean { | |
| const ext = extname(filePath).toLowerCase(); | |
| return IMAGE_EXTENSIONS.includes(ext); | |
| } | |
| // Check if Finder is the frontmost app | |
| async function isFinderFrontmost(): Promise<boolean> { | |
| try { | |
| const frontApp = await applescript(` | |
| tell application "System Events" | |
| set frontApp to name of first application process whose frontmost is true | |
| end tell | |
| return frontApp | |
| `); | |
| return frontApp === "Finder"; | |
| } catch { | |
| return false; | |
| } | |
| } | |
| // Get selected image files from Finder | |
| async function getSelectedImageFiles(): Promise<string[]> { | |
| try { | |
| // getSelectedFile works best when Finder is frontmost | |
| const selected = await getSelectedFile(); | |
| if (!selected) return []; | |
| const files = selected.split("\n").filter((f) => f.trim()); | |
| return files.filter(isImageFile); | |
| } catch { | |
| return []; | |
| } | |
| } | |
| // Check if Chrome/Arc/Safari is the frontmost app | |
| async function getBrowserUrl(): Promise<string | null> { | |
| const browsers = [ | |
| "Google Chrome", | |
| "Arc", | |
| "Safari", | |
| "Brave Browser", | |
| "Microsoft Edge", | |
| ]; | |
| try { | |
| const frontApp = await applescript(` | |
| tell application "System Events" | |
| set frontApp to name of first application process whose frontmost is true | |
| end tell | |
| return frontApp | |
| `); | |
| if (!browsers.some((b) => frontApp.includes(b))) { | |
| return null; | |
| } | |
| // Get URL from the active tab | |
| const appName = | |
| browsers.find((b) => frontApp.includes(b)) || "Google Chrome"; | |
| if (appName === "Safari") { | |
| return await applescript(` | |
| tell application "Safari" | |
| return URL of current tab of front window | |
| end tell | |
| `); | |
| } else { | |
| // Chrome-based browsers | |
| return await applescript(` | |
| tell application "${appName}" | |
| return URL of active tab of front window | |
| end tell | |
| `); | |
| } | |
| } catch { | |
| return null; | |
| } | |
| } | |
| // Check clipboard for image | |
| async function getClipboardImage(): Promise<{ | |
| data: Buffer; | |
| mediaType: string; | |
| } | null> { | |
| try { | |
| const imageBuffer = await clipboard.readImage(); | |
| if (imageBuffer && imageBuffer.byteLength > 0) { | |
| // Script Kit returns PNG format from clipboard | |
| return { data: imageBuffer, mediaType: "image/png" }; | |
| } | |
| } catch { | |
| // No image in clipboard | |
| } | |
| return null; | |
| } | |
| // Save a bookmark | |
| async function saveBookmark(url: string, notes?: string): Promise<void> { | |
| const res = await fetch(`${MYMIND_URL}/api/items`, { | |
| method: "POST", | |
| headers: { "Content-Type": "application/json" }, | |
| body: JSON.stringify({ url, notes }), | |
| }); | |
| const data = await res.json(); | |
| if (!data.success) { | |
| throw new Error(data.error || "Failed to save bookmark"); | |
| } | |
| await notify({ | |
| title: data.isNew ? "Bookmark Saved" : "Bookmark Already Exists", | |
| message: data.item?.title || url, | |
| }); | |
| } | |
| // Save a note | |
| async function saveNote(content: string, title?: string): Promise<void> { | |
| const res = await fetch(`${MYMIND_URL}/api/notes`, { | |
| method: "POST", | |
| headers: { "Content-Type": "application/json" }, | |
| body: JSON.stringify({ content, title }), | |
| }); | |
| const data = await res.json(); | |
| if (!data.success) { | |
| throw new Error(data.error || "Failed to save note"); | |
| } | |
| await notify({ | |
| title: "Note Saved", | |
| message: data.item?.title || "New note saved", | |
| }); | |
| } | |
| // Save an image via the chat API (for AI description) | |
| async function saveImage(imageData: Buffer, mediaType: string): Promise<void> { | |
| const base64 = imageData.toString("base64"); | |
| // Use the chat API to let Claude analyze and save the image | |
| const res = await fetch(`${MYMIND_URL}/api/chat`, { | |
| method: "POST", | |
| headers: { "Content-Type": "application/json" }, | |
| body: JSON.stringify({ | |
| messages: [ | |
| { | |
| role: "user", | |
| content: [ | |
| { | |
| type: "image", | |
| source: { | |
| type: "base64", | |
| media_type: mediaType, | |
| data: base64, | |
| }, | |
| }, | |
| { | |
| type: "text", | |
| text: "Save this image to my collection.", | |
| }, | |
| ], | |
| }, | |
| ], | |
| }), | |
| }); | |
| if (!res.ok) { | |
| throw new Error("Failed to save image"); | |
| } | |
| // The response is SSE, we need to consume it | |
| const reader = res.body?.getReader(); | |
| if (!reader) throw new Error("No response body"); | |
| const decoder = new TextDecoder(); | |
| let saved = false; | |
| while (true) { | |
| const { done, value } = await reader.read(); | |
| if (done) break; | |
| const chunk = decoder.decode(value); | |
| const lines = chunk.split("\n"); | |
| for (const line of lines) { | |
| if (line.startsWith("data: ")) { | |
| try { | |
| const event = JSON.parse(line.slice(6)); | |
| // result exists means success, error exists means failure | |
| if ( | |
| event.type === "tool_call_result" && | |
| event.result && | |
| !event.error | |
| ) { | |
| saved = true; | |
| } | |
| } catch { | |
| // ignore parse errors | |
| } | |
| } | |
| } | |
| } | |
| if (saved) { | |
| await notify({ | |
| title: "Image Saved", | |
| message: "Image analyzed and saved to MyMind", | |
| }); | |
| } else { | |
| throw new Error("Image save not confirmed"); | |
| } | |
| } | |
| // Main flow | |
| async function main() { | |
| const finderIsFront = await isFinderFrontmost(); | |
| // Only check for selected files if Finder is frontmost | |
| const selectedImageFiles = finderIsFront ? await getSelectedImageFiles() : []; | |
| // Only check for browser URL if Finder is NOT frontmost | |
| const browserUrl = finderIsFront ? null : await getBrowserUrl(); | |
| const clipboardImage = await getClipboardImage(); | |
| const clipboardText = await clipboard.readText(); | |
| const choices: { name: string; value: Action; description: string }[] = []; | |
| // Priority: Selected image file in Finder (when Finder is frontmost) | |
| if (selectedImageFiles.length > 0) { | |
| const fileNames = selectedImageFiles.map((f) => basename(f)).join(", "); | |
| choices.push({ | |
| name: `π Save image${ | |
| selectedImageFiles.length > 1 ? "s" : "" | |
| } from Finder`, | |
| value: "image-file", | |
| description: | |
| fileNames.substring(0, 60) + (fileNames.length > 60 ? "..." : ""), | |
| }); | |
| } | |
| // Image in clipboard | |
| if (clipboardImage) { | |
| choices.push({ | |
| name: "πΈ Save image from clipboard", | |
| value: "image-clipboard", | |
| description: `${Math.round( | |
| clipboardImage.data.byteLength / 1024 | |
| )}KB image`, | |
| }); | |
| } | |
| // If browser is open (and Finder is not frontmost), offer to bookmark current page | |
| if (browserUrl) { | |
| choices.push({ | |
| name: "π Bookmark current page", | |
| value: "bookmark-page", | |
| description: | |
| browserUrl.substring(0, 60) + (browserUrl.length > 60 ? "..." : ""), | |
| }); | |
| } | |
| // Check if clipboard has text (and it's not the same as the URL) | |
| if (clipboardText && clipboardText.trim() && clipboardText !== browserUrl) { | |
| const isUrl = /^https?:\/\//.test(clipboardText.trim()); | |
| if (isUrl) { | |
| choices.push({ | |
| name: "π Bookmark URL from clipboard", | |
| value: "bookmark-clipboard", | |
| description: | |
| clipboardText.substring(0, 60) + | |
| (clipboardText.length > 60 ? "..." : ""), | |
| }); | |
| } else { | |
| choices.push({ | |
| name: "π Save text as note", | |
| value: "note", | |
| description: | |
| clipboardText.substring(0, 60) + | |
| (clipboardText.length > 60 ? "..." : ""), | |
| }); | |
| } | |
| } | |
| if (choices.length === 0) { | |
| await notify({ | |
| title: "Nothing to save", | |
| message: "No browser URL, image file, or clipboard content", | |
| }); | |
| return; | |
| } | |
| // If only one choice, just do it. Otherwise, ask. | |
| let action: Action; | |
| if (choices.length === 1) { | |
| action = choices[0].value; | |
| } else { | |
| action = await arg("What do you want to save?", choices); | |
| } | |
| try { | |
| if (action === "image-file" && selectedImageFiles.length > 0) { | |
| await hide(); | |
| // Save each selected image | |
| for (const filePath of selectedImageFiles) { | |
| const mediaType = getMediaType(filePath); | |
| if (!mediaType) continue; | |
| await notify({ title: "Saving image...", message: basename(filePath) }); | |
| const imageData = await readFile(filePath); | |
| await saveImage(imageData, mediaType); | |
| } | |
| } else if (action === "image-clipboard" && clipboardImage) { | |
| await hide(); | |
| await notify({ title: "Saving image...", message: "Analyzing with AI" }); | |
| await saveImage(clipboardImage.data, clipboardImage.mediaType); | |
| } else if (action === "bookmark-page" || action === "bookmark-clipboard") { | |
| const url = | |
| action === "bookmark-clipboard" ? clipboardText?.trim() : browserUrl; | |
| if (!url) throw new Error("No URL available"); | |
| // Optional note - just press Enter to skip | |
| const notes = await arg({ | |
| placeholder: "Add a note (optional), Enter to save", | |
| hint: url, | |
| }); | |
| // Hide the prompt and let it work in background | |
| await hide(); | |
| await notify({ title: "Saving bookmark...", message: url }); | |
| await saveBookmark(url, notes?.trim() || undefined); | |
| } else if (action === "note") { | |
| const content = clipboardText?.trim(); | |
| if (!content) throw new Error("No text in clipboard"); | |
| // Preview and optionally edit | |
| const finalContent = await editor({ | |
| value: content, | |
| hint: "Edit your note if needed, then press Cmd+S to save", | |
| }); | |
| if (!finalContent?.trim()) { | |
| await notify({ title: "Cancelled", message: "Note was empty" }); | |
| return; | |
| } | |
| await hide(); | |
| await notify({ title: "Saving note...", message: "Processing" }); | |
| await saveNote(finalContent); | |
| } | |
| } catch (error) { | |
| const message = error instanceof Error ? error.message : String(error); | |
| await notify({ | |
| title: "Error", | |
| message, | |
| }); | |
| } | |
| } | |
| await main(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment