Action
Bookmark to Karakeep
Posted by Jihad,
Last update
3 days ago
Save the current draft to a self-hosted Karakeep instance.
The action detects the first URL in the draft and saves it as a Karakeep link bookmark. Any remaining text is saved as the bookmark note, and inline hashtags are attached as Karakeep tags. If no URL is found, the draft is saved as a text bookmark instead.
Optional settings let you include Drafts tags, remove the captured URL or hashtags from the saved note, archive the draft after saving, and add a Drafts permalink back to the original note.
Steps
-
configurationKey
name Karakeep Base URL
key karakeepBaseUrl
-
configurationKey
name API Key
key karakeepApiKey
-
configurationKey
name Include Drafts Tags
key includeDraftsTags
-
configurationKey
name Remove Link from Note
key removePrimaryUrlFromNote
-
configurationKey
name Remove Hashtags from Note
key removeHashtagsFromNote
-
configurationKey
name Archive Draft After Saving
key archiveDraftAfterSuccess
-
configurationKey
name Add Drafts Link to Note
key addDraftsPermalinkToNote
-
script
/* Drafts action: Save current draft to Karakeep Recommended setup: 1. Create these "Configured Value" steps before the Script step: - Key: karakeepBaseUrl, Type: String, Required: On Example: https://karakeep.example.com - Key: karakeepApiKey, Type: String, Required: On Value: API key from Karakeep Settings > API Keys - Key: includeDraftsTags, Type: Boolean, Default: true - Key: removePrimaryUrlFromNote, Type: Boolean, Default: true - Key: removeHashtagsFromNote, Type: Boolean, Default: true - Key: archiveDraftAfterSuccess, Type: Boolean, Default: false - Key: addDraftsPermalinkToNote, Type: Boolean, Default: false 2. Add one "Script" step after those values. 3. Paste this file into the script step. If karakeepBaseUrl or karakeepApiKey are not defined as Configured Values, the script falls back to Drafts Credentials. Credentials are a better fit for secrets, because Configured Values only store strings, numbers, and booleans. Behavior: - First http(s) URL in the draft becomes the Karakeep link bookmark. - Remaining draft text becomes the bookmark note. - If there is no URL, a Karakeep text bookmark is created instead. - Inline hashtags are attached as Karakeep tags. */ const configuredValues = context.configuredValues || {}; function hasConfiguredValue(key) { return Object.prototype.hasOwnProperty.call(configuredValues, key); } function configuredString(key, fallback) { if (hasConfiguredValue(key)) { return String(configuredValues[key] || "").trim(); } return fallback; } function configuredBoolean(key, fallback) { if (!hasConfiguredValue(key)) { return fallback; } const value = configuredValues[key]; if (typeof value === "boolean") { return value; } if (typeof value === "string") { return /^(true|yes|1)$/i.test(value.trim()); } return Boolean(value); } const CONFIG = { includeDraftsTags: configuredBoolean("includeDraftsTags", true), removePrimaryUrlFromNote: configuredBoolean("removePrimaryUrlFromNote", true), removeHashtagsFromNote: configuredBoolean("removeHashtagsFromNote", true), archiveDraftAfterSuccess: configuredBoolean("archiveDraftAfterSuccess", false), addDraftsPermalinkToNote: configuredBoolean("addDraftsPermalinkToNote", false), source: configuredString("source", "api"), crawlPriority: configuredString("crawlPriority", "normal"), }; function stop(message) { app.displayErrorMessage(message); context.fail(message); throw new Error(message); } function unique(values) { const seen = {}; const out = []; for (const value of values) { const clean = String(value || "").trim(); const key = clean.toLocaleLowerCase(); if (clean && !seen[key]) { seen[key] = true; out.push(clean); } } return out; } function normalizeBaseUrl(value) { let baseUrl = String(value || "").trim(); baseUrl = baseUrl.replace(/\/+$/, ""); baseUrl = baseUrl.replace(/\/api\/v1$/i, ""); if (!/^https?:\/\//i.test(baseUrl)) { stop("Karakeep URL must start with http:// or https://"); } return baseUrl; } function apiPath(baseUrl, path) { return baseUrl + "/api/v1" + path; } function escapeRegex(value) { return String(value).replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); } function extractHttpUrls(text) { const fromDrafts = draft.urls || []; const urls = fromDrafts.filter((url) => /^https?:\/\//i.test(url)); if (urls.length > 0) { return unique(urls); } const matches = String(text).match(/https?:\/\/[^\s<>"')\]]+/gi) || []; return unique(matches.map((url) => url.replace(/[.,;:!?]+$/, ""))); } function extractHashtags(text) { const withoutUrls = String(text).replace(/https?:\/\/[^\s<>"')\]]+/gi, " "); const tags = []; const tagRegex = /(^|[\s([{])#([^\s#.,;:!?()[\]{}<>]+)/g; let match; while ((match = tagRegex.exec(withoutUrls)) !== null) { const tag = match[2].replace(/^[#]+|[#.,;:!?()[\]{}<>]+$/g, ""); if (tag && !/^\d+$/.test(tag)) { tags.push(tag); } } return unique(tags); } function removeHashtags(text) { return String(text).replace(/(^|[\s([{])#[^\s#.,;:!?()[\]{}<>]+/g, "$1"); } function removePrimaryUrl(text, url) { if (!url) { return text; } const escapedUrl = escapeRegex(url); let cleaned = String(text); // Turn Markdown links that point to the primary URL into their label text. cleaned = cleaned.replace(new RegExp("\\[([^\\]]+)\\]\\(" + escapedUrl + "\\)", "g"), "$1"); cleaned = cleaned.replace(new RegExp(escapedUrl, "g"), ""); return cleaned; } function cleanNote(text, primaryUrl) { let note = String(text || "").replace(/\r\n/g, "\n"); if (CONFIG.removePrimaryUrlFromNote) { note = removePrimaryUrl(note, primaryUrl); } if (CONFIG.removeHashtagsFromNote) { note = removeHashtags(note); } note = note .replace(/[ \t]+\n/g, "\n") .replace(/\n{3,}/g, "\n\n") .trim(); if (CONFIG.addDraftsPermalinkToNote) { note = note ? note + "\n\nDrafts: " + draft.permalink : "Drafts: " + draft.permalink; } return note; } function firstLine(text) { return String(text || "") .split("\n") .map((line) => line.replace(/^#+\s*/, "").trim()) .filter(Boolean)[0] || ""; } const credential = Credential.create( "Karakeep", "Enter your self-hosted Karakeep URL and API key." ); credential.addURLField("baseUrl", "Karakeep URL"); credential.addPasswordField("apiKey", "API key"); let rawBaseUrl = configuredString("karakeepBaseUrl", ""); let apiKey = configuredString("karakeepApiKey", ""); if (!rawBaseUrl || !apiKey) { if (!credential.authorize()) { stop("Karakeep credentials were not saved."); } rawBaseUrl = rawBaseUrl || credential.getValue("baseUrl"); apiKey = apiKey || String(credential.getValue("apiKey") || "").trim(); } const baseUrl = normalizeBaseUrl(rawBaseUrl); if (!apiKey) { stop("Karakeep API key is empty."); } const content = String(draft.content || "").trim(); if (!content) { stop("Draft is empty."); } const urls = extractHttpUrls(content); const primaryUrl = urls[0] || ""; const note = cleanNote(content, primaryUrl); let tags = extractHashtags(content); if (CONFIG.includeDraftsTags && draft.tags) { tags = unique(tags.concat(draft.tags)); } const http = HTTP.create(); function karakeepRequest(method, path, body) { const response = http.request({ url: apiPath(baseUrl, path), method: method, encoding: "json", data: body, headers: { Authorization: "Bearer " + apiKey, Accept: "application/json", "Content-Type": "application/json", }, }); if (!response.success || response.statusCode < 200 || response.statusCode > 299) { const detail = response.responseText || response.error || "HTTP " + response.statusCode; stop("Karakeep request failed: " + detail); } if (response.responseData && typeof response.responseData === "object") { return response.responseData; } if (!response.responseText) { return {}; } try { return JSON.parse(response.responseText); } catch (e) { return {}; } } let payload; if (primaryUrl) { payload = { type: "link", url: primaryUrl, source: CONFIG.source, crawlPriority: CONFIG.crawlPriority, }; if (note) { payload.note = note; } } else { payload = { type: "text", text: note || content, source: CONFIG.source, }; const title = firstLine(note || content); if (title) { payload.title = title; } } const bookmark = karakeepRequest("POST", "/bookmarks", payload); if (!bookmark.id) { stop("Karakeep did not return a bookmark id."); } if (tags.length > 0) { karakeepRequest("POST", "/bookmarks/" + encodeURIComponent(bookmark.id) + "/tags", { tags: tags.map((tagName) => ({ tagName: tagName, attachedBy: "human", })), }); } if (CONFIG.archiveDraftAfterSuccess) { draft.isArchived = true; draft.update(); } app.displaySuccessMessage( "Saved to Karakeep" + (tags.length ? " with " + tags.length + " tag(s)" : "") );
Options
-
After Success Archive , Tags: processed, bookmark Notification Info Log Level Info
Items available in the Drafts Directory are uploaded by community members. Use appropriate caution reviewing downloaded items before use.