/* THIS IS A GENERATED/BUNDLED FILE BY ESBUILD if you want to view the source, please visit the github repository of this plugin */ var __defProp = Object.defineProperty; var __getOwnPropDesc = Object.getOwnPropertyDescriptor; var __getOwnPropNames = Object.getOwnPropertyNames; var __hasOwnProp = Object.prototype.hasOwnProperty; var __export = (target, all) => { for (var name in all) __defProp(target, name, { get: all[name], enumerable: true }); }; var __copyProps = (to, from, except, desc) => { if (from && typeof from === "object" || typeof from === "function") { for (let key of __getOwnPropNames(from)) if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); } return to; }; var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); // src/main.ts var main_exports = {}; __export(main_exports, { default: () => HoarderPlugin }); module.exports = __toCommonJS(main_exports); var import_obsidian3 = require("obsidian"); // src/asset-handler.ts function getAssetUrl(assetId, client, settings) { if (client) { return client.getAssetUrl(assetId); } const baseUrl = settings.apiEndpoint.replace(/\/v1\/?$/, ""); return `${baseUrl}/assets/${assetId}`; } function sanitizeAssetFileName(title) { let sanitizedTitle = title.replace(/[\\/:*?"<>|]/g, "-").replace(/\s+/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, ""); const maxTitleLength = 30; if (sanitizedTitle.length > maxTitleLength) { const truncated = sanitizedTitle.substring(0, maxTitleLength); const lastDash = truncated.lastIndexOf("-"); if (lastDash > maxTitleLength / 2) { sanitizedTitle = truncated.substring(0, lastDash); } else { sanitizedTitle = truncated; } } return sanitizedTitle; } async function downloadImage(app, url, assetId, title, client, settings) { var _a; try { if (!await app.vault.adapter.exists(settings.attachmentsFolder)) { await app.vault.createFolder(settings.attachmentsFolder); } const extension = ((_a = url.split(".").pop()) == null ? void 0 : _a.toLowerCase()) || "jpg"; const safeExtension = ["jpg", "jpeg", "png", "gif", "webp"].includes(extension) ? extension : "jpg"; const safeTitle = sanitizeAssetFileName(title); const fileName = `${assetId}${safeTitle ? "-" + safeTitle : ""}.${safeExtension}`; const filePath = `${settings.attachmentsFolder}/${fileName}`; const files = await app.vault.adapter.list(settings.attachmentsFolder); const existingFile = files.files.find( (filePathItem) => filePathItem.startsWith(`${settings.attachmentsFolder}/${assetId}`) ); if (existingFile) { return existingFile; } let buffer; const apiDomain = new URL(settings.apiEndpoint).origin; if (url.startsWith(apiDomain) && client) { buffer = await client.downloadAsset(assetId); } else { const headers = {}; if (url.startsWith(apiDomain)) { headers["Authorization"] = `Bearer ${settings.apiKey}`; } const response = await fetch(url, { headers }); if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`); buffer = await response.arrayBuffer(); } await app.vault.adapter.writeBinary(filePath, buffer); return filePath; } catch (error) { console.error("Error downloading image:", url, error); return null; } } function escapeMarkdownPath(path) { if (path.includes(" ") || /[<>\[\](){}]/.test(path)) { return `<${path}>`; } return path; } var toWikilink = (path) => `"[[${path}]]"`; async function processBookmarkAssets(app, bookmark, title, client, settings) { let content = ""; const fm = {}; if (bookmark.content.type === "asset" && bookmark.content.assetType === "image") { if (bookmark.content.assetId) { const assetUrl = getAssetUrl(bookmark.content.assetId, client, settings); let imagePath = null; if (settings.downloadAssets) { imagePath = await downloadImage( app, assetUrl, bookmark.content.assetId, title, client, settings ); } if (imagePath) { content += ` ![${title}](${escapeMarkdownPath(imagePath)}) `; fm.image = toWikilink(imagePath); } else { content += ` ![${title}](${escapeMarkdownPath(assetUrl)}) `; } } else if (bookmark.content.sourceUrl) { content += ` ![${title}](${escapeMarkdownPath(bookmark.content.sourceUrl)}) `; } } else if (bookmark.content.type === "link") { const assetIds = []; const assetLabels = []; if (bookmark.content.imageAssetId) { assetIds.push(bookmark.content.imageAssetId); assetLabels.push("Banner Image"); } if (bookmark.content.screenshotAssetId) { assetIds.push(bookmark.content.screenshotAssetId); assetLabels.push("Screenshot"); } if (bookmark.content.fullPageArchiveAssetId) { assetIds.push(bookmark.content.fullPageArchiveAssetId); assetLabels.push("Full Page Archive"); } if (bookmark.content.videoAssetId) { assetIds.push(bookmark.content.videoAssetId); assetLabels.push("Video"); } for (let i = 0; i < assetIds.length; i++) { const assetId = assetIds[i]; const label = assetLabels[i]; const assetUrl = getAssetUrl(assetId, client, settings); if (label === "Video") { content += ` [${title} - ${label}](${escapeMarkdownPath(assetUrl)}) `; } else { let imagePath = null; if (settings.downloadAssets) { imagePath = await downloadImage( app, assetUrl, assetId, `${title}-${label}`, client, settings ); } if (imagePath) { content += ` ![${title} - ${label}](${escapeMarkdownPath(imagePath)}) `; if (label === "Banner Image") { fm.banner = toWikilink(imagePath); } else if (label === "Screenshot") { fm.screenshot = toWikilink(imagePath); } else if (label === "Full Page Archive") { fm.full_page_archive = toWikilink(imagePath); } } else { content += ` ![${title} - ${label}](${escapeMarkdownPath(assetUrl)}) `; } } } if (assetIds.length === 0 && bookmark.content.imageUrl) { content += ` ![${title}](${escapeMarkdownPath(bookmark.content.imageUrl)}) `; } } if (bookmark.assets && bookmark.assets.length > 0) { const processedAssetIds = /* @__PURE__ */ new Set(); if (bookmark.content.type === "asset" && bookmark.content.assetId) { processedAssetIds.add(bookmark.content.assetId); } if (bookmark.content.type === "link") { if (bookmark.content.imageAssetId) processedAssetIds.add(bookmark.content.imageAssetId); if (bookmark.content.screenshotAssetId) processedAssetIds.add(bookmark.content.screenshotAssetId); if (bookmark.content.fullPageArchiveAssetId) processedAssetIds.add(bookmark.content.fullPageArchiveAssetId); if (bookmark.content.videoAssetId) processedAssetIds.add(bookmark.content.videoAssetId); } for (const asset of bookmark.assets) { if (!processedAssetIds.has(asset.id)) { const assetUrl = getAssetUrl(asset.id, client, settings); const label = asset.assetType === "image" ? "Additional Image" : asset.assetType; if (asset.assetType === "video") { content += ` [${title} - ${label}](${escapeMarkdownPath(assetUrl)}) `; } else { let imagePath = null; if (settings.downloadAssets) { imagePath = await downloadImage( app, assetUrl, asset.id, `${title}-${label}`, client, settings ); } if (imagePath) { content += ` ![${title} - ${label}](${escapeMarkdownPath(imagePath)}) `; fm.additional = fm.additional || []; fm.additional.push(toWikilink(imagePath)); } else { content += ` ![${title} - ${label}](${escapeMarkdownPath(assetUrl)}) `; } } } } } return { content, frontmatter: Object.keys(fm).length > 0 ? fm : null }; } // src/bookmark-utils.ts function getBookmarkTitle(bookmark) { if (bookmark.title) { return bookmark.title; } if (bookmark.content.type === "link") { if (bookmark.content.title) { return bookmark.content.title; } if (bookmark.content.url) { return extractTitleFromUrl(bookmark.content.url); } } else if (bookmark.content.type === "text") { if (bookmark.content.text) { return extractTitleFromText(bookmark.content.text); } } else if (bookmark.content.type === "asset") { if (bookmark.content.fileName) { return bookmark.content.fileName.replace(/\.[^/.]+$/, ""); } if (bookmark.content.sourceUrl) { return extractTitleFromUrl(bookmark.content.sourceUrl); } } return `Bookmark-${bookmark.id}-${new Date(bookmark.createdAt).toISOString().split("T")[0]}`; } function extractTitleFromUrl(url) { var _a, _b; try { const parsedUrl = new URL(url); const pathTitle = (_b = (_a = parsedUrl.pathname.split("/").pop()) == null ? void 0 : _a.replace(/\.[^/.]+$/, "")) == null ? void 0 : _b.replace(/-|_/g, " "); if (pathTitle) { return pathTitle; } return parsedUrl.hostname.replace(/^www\./, ""); } catch (e) { return url; } } function extractTitleFromText(text) { const firstLine = text.split("\n")[0]; if (firstLine.length <= 100) { return firstLine; } return firstLine.substring(0, 97) + "..."; } // src/deletion-handler.ts function determineDeletionActions(localBookmarkIds, activeBookmarkIds, archivedBookmarkIds, settings) { const instructions = []; if (!settings.syncDeletions && !settings.handleArchivedBookmarks) { return instructions; } for (const bookmarkId of localBookmarkIds) { const isActive = activeBookmarkIds.has(bookmarkId); const isArchived = archivedBookmarkIds.has(bookmarkId); if (!isActive && !isArchived) { if (settings.syncDeletions && settings.deletionAction !== "ignore") { instructions.push({ bookmarkId, action: settings.deletionAction, reason: "deleted" }); } } else if (!isActive && isArchived) { if (settings.handleArchivedBookmarks && settings.archivedBookmarkAction !== "ignore") { instructions.push({ bookmarkId, action: settings.archivedBookmarkAction, reason: "archived" }); } } } return instructions; } function countDeletionResults(instructions) { const results = { deleted: 0, archived: 0, tagged: 0, archivedHandled: 0 }; for (const instruction of instructions) { if (instruction.reason === "deleted") { switch (instruction.action) { case "delete": results.deleted++; break; case "archive": results.archived++; break; case "tag": results.tagged++; break; } } else if (instruction.reason === "archived") { results.archivedHandled++; } } return results; } // src/filename-utils.ts function sanitizeFileName(title, createdAt) { const date = typeof createdAt === "string" ? new Date(createdAt) : createdAt; const dateStr = date.toISOString().split("T")[0]; let sanitizedTitle = title.replace(/[\\/:*?"<>|]/g, "-").replace(/\s+/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, ""); const maxTitleLength = 36; if (sanitizedTitle.length > maxTitleLength) { const truncated = sanitizedTitle.substring(0, maxTitleLength); const lastDash = truncated.lastIndexOf("-"); if (lastDash > maxTitleLength / 2) { sanitizedTitle = truncated.substring(0, lastDash); } else { sanitizedTitle = truncated; } } return `${dateStr}-${sanitizedTitle}`; } // src/filter-utils.ts function shouldIncludeBookmark(bookmarkTags, includedTags, excludedTags, isFavorited) { if (includedTags.length > 0) { const hasIncludedTag = includedTags.some((includedTag) => bookmarkTags.includes(includedTag)); if (!hasIncludedTag) { return { include: false, reason: "missing_included_tag" }; } } if (!isFavorited && excludedTags.length > 0) { const hasExcludedTag = excludedTags.some((excludedTag) => bookmarkTags.includes(excludedTag)); if (hasExcludedTag) { return { include: false, reason: "excluded_tag" }; } } return { include: true }; } // src/formatting-utils.ts function escapeYaml(str) { if (!str) return ""; if (str.includes("\n") || /[:#{}\[\],&*?|<>=!%@`]/.test(str)) { return `| ${str.replace(/\n/g, "\n ")}`; } if (str.includes('"')) { return `'${str}'`; } if (str.includes("'") || /^[ \t]|[ \t]$/.test(str)) { return `"${str.replace(/\"/g, '\\"')}"`; } return str; } function escapeMarkdownPath2(path) { if (path.includes(" ") || /[<>[\](){}]/.test(path)) { return `<${path}>`; } return path; } // src/hoarder-client.ts var import_obsidian = require("obsidian"); var HoarderApiClient = class { constructor(baseUrl, apiKey, useObsidianRequest = false) { this.baseUrl = baseUrl.replace(/\/$/, ""); this.apiKey = apiKey; this.useObsidianRequest = useObsidianRequest; } async makeRequest(endpoint, method = "GET", body, params) { const url = new URL(`${this.baseUrl}${endpoint}`); if (params) { Object.entries(params).forEach(([key, value]) => { if (value !== void 0 && value !== null) { url.searchParams.append(key, String(value)); } }); } const headers = { "Content-Type": "application/json", Authorization: `Bearer ${this.apiKey}` }; try { if (this.useObsidianRequest) { const response = await (0, import_obsidian.requestUrl)({ url: url.toString(), method, headers, body: body ? JSON.stringify(body) : void 0 }); if (response.status >= 400) { throw new Error(`HTTP ${response.status}: ${response.text || "Unknown error"}`); } return response.json; } else { const response = await fetch(url.toString(), { method, headers, body: body ? JSON.stringify(body) : void 0 }); if (!response.ok) { const errorText = await response.text(); throw new Error(`HTTP ${response.status}: ${errorText || "Unknown error"}`); } return await response.json(); } } catch (error) { console.error("API request failed:", url.toString(), error); throw error; } } async getBookmarks(params) { return this.makeRequest("/bookmarks", "GET", void 0, params); } async updateBookmark(bookmarkId, data) { return this.makeRequest(`/bookmarks/${bookmarkId}`, "PATCH", data); } async getBookmarkHighlights(bookmarkId) { return this.makeRequest( `/bookmarks/${bookmarkId}/highlights`, "GET" ); } async getHighlights(params) { return this.makeRequest("/highlights", "GET", void 0, params); } async getAllHighlights() { const allHighlights = []; let cursor; do { const data = await this.getHighlights({ limit: 100, cursor: cursor || void 0 }); allHighlights.push(...data.highlights || []); cursor = data.nextCursor || void 0; } while (cursor); return allHighlights; } async downloadAsset(assetId) { const url = `${this.baseUrl}/assets/${assetId}`; const headers = { Authorization: `Bearer ${this.apiKey}` }; try { if (this.useObsidianRequest) { const response = await (0, import_obsidian.requestUrl)({ url, method: "GET", headers }); if (response.status >= 400) { throw new Error(`HTTP ${response.status}: ${response.text || "Unknown error"}`); } return response.arrayBuffer; } else { const response = await fetch(url, { method: "GET", headers }); if (!response.ok) { const errorText = await response.text(); throw new Error(`HTTP ${response.status}: ${errorText || "Unknown error"}`); } return await response.arrayBuffer(); } } catch (error) { console.error("Asset download failed:", url, error); throw error; } } getAssetUrl(assetId) { const baseUrl = this.baseUrl.replace(/\/api\/v1\/?$/, ""); return `${baseUrl}/assets/${assetId}`; } }; // src/markdown-utils.ts function extractNotesSection(content) { const notesMatch = content.match(/## Notes\n\n([\s\S]*?)(?=\n##|\n\[|$)/); return notesMatch ? notesMatch[1].trim() : null; } // src/message-utils.ts function buildSyncMessage(stats) { let message = `Successfully synced ${stats.totalBookmarks} bookmark${stats.totalBookmarks === 1 ? "" : "s"}`; if (stats.skippedFiles > 0) { message += ` (skipped ${stats.skippedFiles} existing file${stats.skippedFiles === 1 ? "" : "s"})`; } if (stats.updatedInHoarder > 0) { message += ` and updated ${stats.updatedInHoarder} note${stats.updatedInHoarder === 1 ? "" : "s"} in Karakeep`; } if (stats.excludedByTags > 0) { message += `, excluded ${stats.excludedByTags} bookmark${stats.excludedByTags === 1 ? "" : "s"} by tags`; } if (stats.includedByTags > 0 && stats.includedTagsEnabled) { message += `, included ${stats.includedByTags} bookmark${stats.includedByTags === 1 ? "" : "s"} by tags`; } if (stats.skippedNoHighlights > 0) { message += `, skipped ${stats.skippedNoHighlights} bookmark${stats.skippedNoHighlights === 1 ? "" : "s"} without highlights`; } const totalDeleted = stats.deletionResults.deleted + stats.deletionResults.archived + stats.deletionResults.tagged; const totalArchived = stats.deletionResults.archivedHandled; if (totalDeleted > 0 || totalArchived > 0) { if (totalDeleted > 0) { message += `, processed ${totalDeleted} deleted bookmark${totalDeleted === 1 ? "" : "s"}`; if (stats.deletionResults.deleted > 0) { message += ` (${stats.deletionResults.deleted} deleted)`; } if (stats.deletionResults.archived > 0) { message += ` (${stats.deletionResults.archived} archived)`; } if (stats.deletionResults.tagged > 0) { message += ` (${stats.deletionResults.tagged} tagged)`; } } if (totalArchived > 0) { message += `, handled ${totalArchived} archived bookmark${totalArchived === 1 ? "" : "s"}`; } } return message; } // src/settings.ts var import_obsidian2 = require("obsidian"); var DEFAULT_SETTINGS = { apiKey: "", apiEndpoint: "https://api.hoarder.app/api/v1", syncFolder: "Hoarder", attachmentsFolder: "Hoarder/attachments", syncIntervalMinutes: 60, lastSyncTimestamp: 0, updateExistingFiles: false, excludeArchived: true, onlyFavorites: false, syncNotesToHoarder: true, syncHighlights: true, onlyBookmarksWithHighlights: false, excludedTags: [], includedTags: [], downloadAssets: true, syncDeletions: false, deletionAction: "delete", deletionTag: "deleted", archiveFolder: "Hoarder/deleted", handleArchivedBookmarks: false, archivedBookmarkAction: "delete", archivedBookmarkTag: "archived", archivedBookmarkFolder: "Hoarder/archived", useObsidianRequest: false }; var FolderSuggest = class extends import_obsidian2.AbstractInputSuggest { constructor(app, inputEl) { super(app, inputEl); this.folders = this.getFolders(); this.inputEl = inputEl; } getSuggestions(inputStr) { const lowerCaseInputStr = inputStr.toLowerCase(); return this.folders.filter((folder) => folder.path.toLowerCase().contains(lowerCaseInputStr)); } renderSuggestion(folder, el) { el.setText(folder.path); } selectSuggestion(folder) { const value = folder.path; this.inputEl.value = value; this.inputEl.trigger("input"); this.close(); } getFolders() { const folders = []; this.app.vault.getAllLoadedFiles().forEach((file) => { if (file instanceof import_obsidian2.TFolder) { folders.push(file); } }); return folders.sort((a, b) => a.path.localeCompare(b.path)); } }; var HoarderSettingTab = class extends import_obsidian2.PluginSettingTab { constructor(app, plugin) { super(app, plugin); this.updateSyncButton = (isSyncing) => { if (this.syncButton) { this.syncButton.setButtonText(isSyncing ? "Syncing..." : "Sync Now"); this.syncButton.setDisabled(isSyncing); } }; this.plugin = plugin; } onunload() { this.plugin.events.off("sync-state-change", this.updateSyncButton); } display() { const { containerEl } = this; containerEl.empty(); containerEl.createEl("h3", { text: "API Configuration" }); containerEl.createEl("div", { text: "Connection settings for your Karakeep instance", cls: "setting-item-description" }); new import_obsidian2.Setting(containerEl).setName("Api key").setDesc("Your Hoarder API key").addText( (text) => text.setPlaceholder("Enter your API key").setValue(this.plugin.settings.apiKey).onChange(async (value) => { this.plugin.settings.apiKey = value; await this.plugin.saveSettings(); }).inputEl.addClass("hoarder-wide-input") ); new import_obsidian2.Setting(containerEl).setName("Api endpoint").setDesc("Hoarder API endpoint URL (default: https://api.karakeep.app/api/v1)").addText( (text) => text.setPlaceholder("Enter API endpoint").setValue(this.plugin.settings.apiEndpoint).onChange(async (value) => { this.plugin.settings.apiEndpoint = value; await this.plugin.saveSettings(); }).inputEl.addClass("hoarder-wide-input") ); new import_obsidian2.Setting(containerEl).setName("Bypass CORS").setDesc( "Use Obsidian's internal request method to avoid CORS issues. Enable this if you're experiencing connection problems." ).addToggle( (toggle) => toggle.setValue(this.plugin.settings.useObsidianRequest).onChange(async (value) => { this.plugin.settings.useObsidianRequest = value; await this.plugin.saveSettings(); }) ); containerEl.createEl("h3", { text: "File Organization" }); containerEl.createEl("div", { text: "Configure where your bookmarks and assets are stored", cls: "setting-item-description" }); new import_obsidian2.Setting(containerEl).setName("Sync folder").setDesc("Folder where bookmarks will be saved").addText((text) => { text.setPlaceholder("Example: folder1/folder2").setValue(this.plugin.settings.syncFolder).onChange(async (value) => { this.plugin.settings.syncFolder = value; await this.plugin.saveSettings(); }); text.inputEl.addClass("hoarder-medium-input"); new FolderSuggest(this.app, text.inputEl); return text; }); new import_obsidian2.Setting(containerEl).setName("Attachments folder").setDesc("Folder where bookmark images will be saved").addText((text) => { text.setPlaceholder("Example: folder1/attachments").setValue(this.plugin.settings.attachmentsFolder).onChange(async (value) => { this.plugin.settings.attachmentsFolder = value; await this.plugin.saveSettings(); }); text.inputEl.addClass("hoarder-medium-input"); new FolderSuggest(this.app, text.inputEl); return text; }); containerEl.createEl("h3", { text: "Sync Behavior" }); containerEl.createEl("div", { text: "Control how synchronization works", cls: "setting-item-description" }); new import_obsidian2.Setting(containerEl).setName("Sync interval").setDesc("How often to sync (in minutes)").addText( (text) => text.setPlaceholder("60").setValue(String(this.plugin.settings.syncIntervalMinutes)).onChange(async (value) => { const numValue = parseInt(value); if (!isNaN(numValue) && numValue > 0) { this.plugin.settings.syncIntervalMinutes = numValue; await this.plugin.saveSettings(); this.plugin.startPeriodicSync(); } }).inputEl.addClass("hoarder-small-input") ); new import_obsidian2.Setting(containerEl).setName("Update existing files").setDesc( "Whether to update existing bookmark files when remote data changes. When disabled, only new bookmarks will be created." ).addToggle( (toggle) => toggle.setValue(this.plugin.settings.updateExistingFiles).onChange(async (value) => { this.plugin.settings.updateExistingFiles = value; await this.plugin.saveSettings(); }) ); new import_obsidian2.Setting(containerEl).setName("Sync notes to Karakeep").setDesc("Whether to sync notes to Karakeep").addToggle( (toggle) => toggle.setValue(this.plugin.settings.syncNotesToHoarder).onChange(async (value) => { this.plugin.settings.syncNotesToHoarder = value; await this.plugin.saveSettings(); }) ); new import_obsidian2.Setting(containerEl).setName("Sync highlights").setDesc("Whether to sync highlights from Karakeep into bookmark files").addToggle( (toggle) => toggle.setValue(this.plugin.settings.syncHighlights).onChange(async (value) => { this.plugin.settings.syncHighlights = value; await this.plugin.saveSettings(); }) ); new import_obsidian2.Setting(containerEl).setName("Download assets").setDesc( "Download images and other assets locally (if disabled, assets will be embedded using their source URLs)" ).addToggle( (toggle) => toggle.setValue(this.plugin.settings.downloadAssets).onChange(async (value) => { this.plugin.settings.downloadAssets = value; await this.plugin.saveSettings(); }) ); containerEl.createEl("h3", { text: "Sync Filtering" }); containerEl.createEl("div", { text: "Control which bookmarks are synchronized", cls: "setting-item-description" }); new import_obsidian2.Setting(containerEl).setName("Exclude archived").setDesc("Exclude archived bookmarks from sync").addToggle( (toggle) => toggle.setValue(this.plugin.settings.excludeArchived).onChange(async (value) => { this.plugin.settings.excludeArchived = value; await this.plugin.saveSettings(); }) ); new import_obsidian2.Setting(containerEl).setName("Only favorites").setDesc("Only sync favorited bookmarks").addToggle( (toggle) => toggle.setValue(this.plugin.settings.onlyFavorites).onChange(async (value) => { this.plugin.settings.onlyFavorites = value; await this.plugin.saveSettings(); }) ); new import_obsidian2.Setting(containerEl).setName("Only bookmarks with highlights").setDesc( "Only sync bookmarks that have highlights (requires 'Sync highlights' to be enabled)" ).addToggle( (toggle) => toggle.setValue(this.plugin.settings.onlyBookmarksWithHighlights).onChange(async (value) => { this.plugin.settings.onlyBookmarksWithHighlights = value; await this.plugin.saveSettings(); }) ); new import_obsidian2.Setting(containerEl).setName("Excluded tags").setDesc("Bookmarks with these tags will not be synced (comma-separated), unless favorited").addText( (text) => text.setPlaceholder("private, secret, draft").setValue(this.plugin.settings.excludedTags.join(", ")).onChange(async (value) => { this.plugin.settings.excludedTags = value.split(",").map((tag) => tag.trim()).filter((tag) => tag.length > 0); await this.plugin.saveSettings(); }).inputEl.addClass("hoarder-wide-input") ); new import_obsidian2.Setting(containerEl).setName("Included tags").setDesc("Bookmarks with these tags will be synced (comma-separated)").addText( (text) => text.setPlaceholder("public, shared").setValue(this.plugin.settings.includedTags.join(", ")).onChange(async (value) => { this.plugin.settings.includedTags = value.split(",").map((tag) => tag.trim()).filter((tag) => tag.length > 0); await this.plugin.saveSettings(); }).inputEl.addClass("hoarder-wide-input") ); containerEl.createEl("h3", { text: "Deletion Handling" }); containerEl.createEl("div", { text: "Configure what happens when bookmarks are deleted in Karakeep", cls: "setting-item-description" }); const syncDeletionsToggle = new import_obsidian2.Setting(containerEl).setName("Sync deletions").setDesc("Automatically handle bookmarks that are deleted in Karakeep").addToggle( (toggle) => toggle.setValue(this.plugin.settings.syncDeletions).onChange(async (value) => { this.plugin.settings.syncDeletions = value; await this.plugin.saveSettings(); this.display(); }) ); if (this.plugin.settings.syncDeletions) { const deletionActionSetting = new import_obsidian2.Setting(containerEl).setName("Deletion action").setDesc("What to do with local files when bookmarks are deleted in Karakeep").addDropdown( (dropdown) => dropdown.addOption("delete", "Delete file").addOption("archive", "Move to archive folder").addOption("tag", "Add deletion tag").setValue(this.plugin.settings.deletionAction).onChange(async (value) => { this.plugin.settings.deletionAction = value; await this.plugin.saveSettings(); this.display(); }) ); if (this.plugin.settings.deletionAction === "archive") { new import_obsidian2.Setting(containerEl).setName("Archive folder").setDesc("Folder to move deleted bookmarks to").addText((text) => { text.setPlaceholder("Example: Hoarder/deleted").setValue(this.plugin.settings.archiveFolder).onChange(async (value) => { this.plugin.settings.archiveFolder = value; await this.plugin.saveSettings(); }); text.inputEl.addClass("hoarder-medium-input"); new FolderSuggest(this.app, text.inputEl); return text; }); } if (this.plugin.settings.deletionAction === "tag") { new import_obsidian2.Setting(containerEl).setName("Deletion tag").setDesc("Tag to add to files when bookmarks are deleted").addText( (text) => text.setPlaceholder("deleted").setValue(this.plugin.settings.deletionTag).onChange(async (value) => { this.plugin.settings.deletionTag = value; await this.plugin.saveSettings(); }).inputEl.addClass("hoarder-medium-input") ); } } containerEl.createEl("h3", { text: "Archive Handling" }); containerEl.createEl("div", { text: "Configure what happens when bookmarks are archived in Karakeep", cls: "setting-item-description" }); const handleArchivedToggle = new import_obsidian2.Setting(containerEl).setName("Handle archived bookmarks").setDesc("Separately handle bookmarks that are archived (not deleted) in Karakeep").addToggle( (toggle) => toggle.setValue(this.plugin.settings.handleArchivedBookmarks).onChange(async (value) => { this.plugin.settings.handleArchivedBookmarks = value; await this.plugin.saveSettings(); this.display(); }) ); if (this.plugin.settings.handleArchivedBookmarks) { const archivedActionSetting = new import_obsidian2.Setting(containerEl).setName("Archived bookmark action").setDesc("What to do with local files when bookmarks are archived in Karakeep").addDropdown( (dropdown) => dropdown.addOption("ignore", "Do nothing").addOption("delete", "Delete file").addOption("archive", "Move to archive folder").addOption("tag", "Add archived tag").setValue(this.plugin.settings.archivedBookmarkAction).onChange(async (value) => { this.plugin.settings.archivedBookmarkAction = value; await this.plugin.saveSettings(); this.display(); }) ); if (this.plugin.settings.archivedBookmarkAction === "archive") { new import_obsidian2.Setting(containerEl).setName("Archived bookmark folder").setDesc("Folder to move archived bookmarks to").addText((text) => { text.setPlaceholder("Example: Hoarder/archived").setValue(this.plugin.settings.archivedBookmarkFolder).onChange(async (value) => { this.plugin.settings.archivedBookmarkFolder = value; await this.plugin.saveSettings(); }); text.inputEl.addClass("hoarder-medium-input"); new FolderSuggest(this.app, text.inputEl); return text; }); } if (this.plugin.settings.archivedBookmarkAction === "tag") { new import_obsidian2.Setting(containerEl).setName("Archived bookmark tag").setDesc("Tag to add to files when bookmarks are archived").addText( (text) => text.setPlaceholder("archived").setValue(this.plugin.settings.archivedBookmarkTag).onChange(async (value) => { this.plugin.settings.archivedBookmarkTag = value; await this.plugin.saveSettings(); }).inputEl.addClass("hoarder-medium-input") ); } } containerEl.createEl("h3", { text: "Manual Actions & Status" }); containerEl.createEl("div", { text: "Manual sync controls and synchronization status", cls: "setting-item-description" }); new import_obsidian2.Setting(containerEl).setName("Manual sync").setDesc("Sync bookmarks now").addButton((button) => { this.syncButton = button.setButtonText(this.plugin.isSyncing ? "Syncing..." : "Sync Now").setDisabled(this.plugin.isSyncing).onClick(async () => { const result = await this.plugin.syncBookmarks(); new import_obsidian2.Notice(result.message); }); this.plugin.events.on("sync-state-change", this.updateSyncButton); return button; }); if (this.plugin.settings.lastSyncTimestamp > 0) { containerEl.createEl("div", { text: `Last synced: ${new Date(this.plugin.settings.lastSyncTimestamp).toLocaleString()}`, cls: "setting-item-description" }); } } }; // src/tag-utils.ts function sanitizeTag(tag) { let sanitized = tag.trim(); if (!sanitized) return null; sanitized = sanitized.replace(/\s+/g, "-"); sanitized = sanitized.replace(/[^a-zA-Z0-9_\-/]/g, ""); if (!sanitized) return null; if (/^\d+$/.test(sanitized)) { sanitized = "tag-" + sanitized; } if (/^[\d\/\-_]+$/.test(sanitized)) { sanitized = "tag-" + sanitized; } return sanitized; } function sanitizeTags(tags) { return tags.map(sanitizeTag).filter((tag) => tag !== null); } // src/main.ts var HoarderPlugin = class extends import_obsidian3.Plugin { constructor() { super(...arguments); this.isSyncing = false; this.skippedFiles = 0; this.events = new import_obsidian3.Events(); this.modificationTimeout = null; this.lastSyncedNotes = null; this.client = null; } async onload() { await this.loadSettings(); this.initializeClient(); this.addSettingTab(new HoarderSettingTab(this.app, this)); this.addCommand({ id: "trigger-hoarder-sync", name: "Sync Bookmarks", callback: async () => { const result = await this.syncBookmarks(); new import_obsidian3.Notice(result.message); } }); this.registerEvent( this.app.vault.on("modify", async (file) => { if (this.settings.syncNotesToHoarder && file.path.startsWith(this.settings.syncFolder) && file.path.endsWith(".md") && file instanceof import_obsidian3.TFile) { if (this.modificationTimeout) { window.clearTimeout(this.modificationTimeout); } this.modificationTimeout = window.setTimeout(async () => { await this.handleFileModification(file); }, 2e3); } }) ); this.startPeriodicSync(); } onunload() { if (this.syncIntervalId) { window.clearInterval(this.syncIntervalId); } if (this.modificationTimeout) { window.clearTimeout(this.modificationTimeout); } } async loadSettings() { this.settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData()); } async saveSettings() { await this.saveData(this.settings); this.initializeClient(); } startPeriodicSync() { if (this.syncIntervalId) { window.clearInterval(this.syncIntervalId); } const interval = this.settings.syncIntervalMinutes * 60 * 1e3; this.syncBookmarks(); this.syncIntervalId = window.setInterval(() => { this.syncBookmarks(); }, interval); } async fetchBookmarks(cursor, limit = 100) { if (!this.client) { throw new Error("Client not initialized"); } return await this.client.getBookmarks({ limit, cursor: cursor || void 0, archived: this.settings.excludeArchived ? false : void 0, favourited: this.settings.onlyFavorites ? true : void 0 }); } async fetchAllBookmarks(includeArchived = false) { if (!this.client) { throw new Error("Client not initialized"); } const allBookmarks = []; let cursor; do { const data = await this.client.getBookmarks({ limit: 100, cursor: cursor || void 0, archived: includeArchived ? void 0 : false, favourited: this.settings.onlyFavorites ? true : void 0 }); allBookmarks.push(...data.bookmarks || []); cursor = data.nextCursor || void 0; } while (cursor); return allBookmarks; } async extractNotesFromFile(filePath) { var _a, _b; try { const file = this.app.vault.getAbstractFileByPath(filePath); if (!(file instanceof import_obsidian3.TFile)) { return { currentNotes: null, originalNotes: null }; } const content = await this.app.vault.adapter.read(filePath); const currentNotes = extractNotesSection(content); const metadata = (_a = this.app.metadataCache.getFileCache(file)) == null ? void 0 : _a.frontmatter; const originalNotes = (_b = metadata == null ? void 0 : metadata.original_note) != null ? _b : null; return { currentNotes, originalNotes }; } catch (error) { console.error("Error reading file:", error); return { currentNotes: null, originalNotes: null }; } } async updateBookmarkInHoarder(bookmarkId, note) { try { if (!this.client) { throw new Error("Client not initialized"); } await this.client.updateBookmark(bookmarkId, { note }); return true; } catch (error) { console.error("Error updating bookmark in Hoarder:", error); return false; } } setSyncing(value) { this.isSyncing = value; this.events.trigger("sync-state-change", value); } async getLocalBookmarkFiles() { var _a; const bookmarkFiles = /* @__PURE__ */ new Map(); const folderPath = this.settings.syncFolder; if (!await this.app.vault.adapter.exists(folderPath)) { return bookmarkFiles; } const files = this.app.vault.getMarkdownFiles(); for (const file of files) { if (file.path.startsWith(folderPath) && file.path.endsWith(".md")) { const metadata = (_a = this.app.metadataCache.getFileCache(file)) == null ? void 0 : _a.frontmatter; const bookmarkId = metadata == null ? void 0 : metadata.bookmark_id; if (bookmarkId) { bookmarkFiles.set(bookmarkId, file.path); } } } return bookmarkFiles; } async handleDeletedAndArchivedBookmarks(localBookmarkFiles, activeBookmarkIds, archivedBookmarkIds) { const deletionSettings = { syncDeletions: this.settings.syncDeletions, deletionAction: this.settings.deletionAction, handleArchivedBookmarks: this.settings.handleArchivedBookmarks, archivedBookmarkAction: this.settings.archivedBookmarkAction }; const localBookmarkIds = Array.from(localBookmarkFiles.keys()); const instructions = determineDeletionActions( localBookmarkIds, activeBookmarkIds, archivedBookmarkIds, deletionSettings ); for (const instruction of instructions) { const filePath = localBookmarkFiles.get(instruction.bookmarkId); if (!filePath) continue; const file = this.app.vault.getAbstractFileByPath(filePath); if (!(file instanceof import_obsidian3.TFile)) continue; try { switch (instruction.action) { case "delete": await this.app.vault.delete(file); break; case "archive": const archiveFolder = instruction.reason === "deleted" ? this.settings.archiveFolder : this.settings.archivedBookmarkFolder; await this.moveToArchiveFolder(file, archiveFolder); break; case "tag": const tag = instruction.reason === "deleted" ? this.settings.deletionTag : this.settings.archivedBookmarkTag; await this.addDeletionTag(file, tag); break; } } catch (error) { console.error(`Error handling bookmark ${instruction.bookmarkId}:`, error); } } return countDeletionResults(instructions); } async moveToArchiveFolder(file, archiveFolder) { if (!archiveFolder) { throw new Error("Archive folder not configured"); } if (!await this.app.vault.adapter.exists(archiveFolder)) { await this.app.vault.createFolder(archiveFolder); } const fileName = file.name; const newPath = `${archiveFolder}/${fileName}`; let finalPath = newPath; let counter = 1; while (await this.app.vault.adapter.exists(finalPath)) { const nameWithoutExt = fileName.replace(/\.md$/, ""); finalPath = `${archiveFolder}/${nameWithoutExt}-${counter}.md`; counter++; } await this.app.fileManager.renameFile(file, finalPath); } async addDeletionTag(file, tag) { if (!tag) { throw new Error("Tag not configured"); } await this.app.fileManager.processFrontMatter(file, (frontmatter) => { if (!frontmatter.tags) { frontmatter.tags = []; } if (typeof frontmatter.tags === "string") { frontmatter.tags = [frontmatter.tags]; } if (!frontmatter.tags.includes(tag)) { frontmatter.tags.push(tag); } }); } async syncBookmarks() { var _a; if (this.isSyncing) { console.error("[Hoarder] Sync already in progress"); return { success: false, message: "Sync already in progress" }; } if (!this.settings.apiKey) { console.log("[Hoarder] API key not configured"); return { success: false, message: "Hoarder API key not configured" }; } console.log("[Hoarder] Starting sync..."); console.log( `[Hoarder] Settings: syncNotesToHoarder=${this.settings.syncNotesToHoarder}, updateExistingFiles=${this.settings.updateExistingFiles}` ); this.setSyncing(true); let totalBookmarks = 0; this.skippedFiles = 0; let updatedInHoarder = 0; let excludedByTags = 0; let includedByTags = 0; let totalBookmarksProcessed = 0; let skippedNoHighlights = 0; try { const folderPath = this.settings.syncFolder; if (!await this.app.vault.adapter.exists(folderPath)) { await this.app.vault.createFolder(folderPath); } const localBookmarkFiles = await this.getLocalBookmarkFiles(); const activeBookmarks = await this.fetchAllBookmarks(false); const allBookmarks = await this.fetchAllBookmarks(true); const activeBookmarkIds = new Set(activeBookmarks.map((b) => b.id)); const allBookmarkIds = new Set(allBookmarks.map((b) => b.id)); const archivedBookmarkIds = new Set( allBookmarks.filter((b) => b.archived && !activeBookmarkIds.has(b.id)).map((b) => b.id) ); let highlightsByBookmarkId = /* @__PURE__ */ new Map(); let bookmarkIdsWithHighlights = /* @__PURE__ */ new Set(); if ((this.settings.syncHighlights || this.settings.onlyBookmarksWithHighlights) && this.client) { try { const allHighlights = await this.client.getAllHighlights(); for (const highlight of allHighlights) { if (!highlightsByBookmarkId.has(highlight.bookmarkId)) { highlightsByBookmarkId.set(highlight.bookmarkId, []); } highlightsByBookmarkId.get(highlight.bookmarkId).push(highlight); bookmarkIdsWithHighlights.add(highlight.bookmarkId); } } catch (error) { console.error("Error fetching highlights in bulk:", error); } } let cursor; do { const result = await this.fetchBookmarks(cursor); const bookmarks = result.bookmarks || []; cursor = result.nextCursor || void 0; totalBookmarksProcessed += bookmarks.length; for (const bookmark of bookmarks) { if (this.settings.onlyBookmarksWithHighlights && !bookmarkIdsWithHighlights.has(bookmark.id)) { skippedNoHighlights++; continue; } const bookmarkTags = bookmark.tags.map((tag) => tag.name.toLowerCase()); const filterResult = shouldIncludeBookmark( bookmarkTags, this.settings.includedTags.map((t) => t.toLowerCase()), this.settings.excludedTags.map((t) => t.toLowerCase()), bookmark.favourited ); if (!filterResult.include) { excludedByTags++; continue; } if (this.settings.includedTags.length > 0) { includedByTags++; } const title = getBookmarkTitle(bookmark); const fileName = `${folderPath}/${sanitizeFileName(title, bookmark.createdAt)}.md`; const highlights = highlightsByBookmarkId.get(bookmark.id) || []; const fileExists = await this.app.vault.adapter.exists(fileName); if (fileExists) { const file = this.app.vault.getAbstractFileByPath(fileName); if (file instanceof import_obsidian3.TFile) { const metadata = (_a = this.app.metadataCache.getFileCache(file)) == null ? void 0 : _a.frontmatter; const storedModifiedTime = (metadata == null ? void 0 : metadata.modified) ? new Date(metadata.modified).getTime() : 0; const bookmarkModifiedTime = bookmark.modifiedAt ? new Date(bookmark.modifiedAt).getTime() : new Date(bookmark.createdAt).getTime(); let hasNewHighlights = false; if (this.settings.syncHighlights && highlights.length > 0) { const newestHighlightTime = Math.max( ...highlights.map((h) => new Date(h.createdAt).getTime()) ); hasNewHighlights = !storedModifiedTime || newestHighlightTime > storedModifiedTime; } if (!this.settings.updateExistingFiles) { this.skippedFiles++; continue; } if (this.settings.syncNotesToHoarder) { const { currentNotes, originalNotes } = await this.extractNotesFromFile(fileName); const remoteNotes = bookmark.note || ""; if (originalNotes === null && currentNotes !== null) { console.debug(`[Hoarder] original_note missing for ${fileName}`); if (currentNotes !== remoteNotes) { console.log(`[Hoarder] Local notes differ from remote, syncing to Hoarder`); const updated = await this.updateBookmarkInHoarder(bookmark.id, currentNotes); if (updated) { updatedInHoarder++; bookmark.note = currentNotes; this.lastSyncedNotes = currentNotes; console.debug( `[Hoarder] Initializing original_note to synced value for ${fileName}` ); await this.app.fileManager.processFrontMatter(file, (frontmatter) => { frontmatter["original_note"] = currentNotes; }); } } else { console.debug( `[Hoarder] Notes match remote, skipping original_note initialization to preserve mtime` ); } } else if (currentNotes !== null && originalNotes !== null && currentNotes !== originalNotes && currentNotes !== remoteNotes) { console.log(`[Hoarder] Local notes changed for ${fileName}, syncing to Hoarder`); const updated = await this.updateBookmarkInHoarder(bookmark.id, currentNotes); if (updated) { updatedInHoarder++; bookmark.note = currentNotes; this.lastSyncedNotes = currentNotes; } } } const newContent = await this.formatBookmarkAsMarkdown(bookmark, title, highlights); const existingContent = await this.app.vault.adapter.read(fileName); if (existingContent !== newContent) { await this.app.vault.adapter.write(fileName, newContent); totalBookmarks++; } else { this.skippedFiles++; } } } else { const content = await this.formatBookmarkAsMarkdown(bookmark, title, highlights); await this.app.vault.create(fileName, content); totalBookmarks++; } } } while (cursor); const deletionResults = await this.handleDeletedAndArchivedBookmarks( localBookmarkFiles, activeBookmarkIds, archivedBookmarkIds ); this.settings.lastSyncTimestamp = Date.now(); await this.saveSettings(); const stats = { totalBookmarks, skippedFiles: this.skippedFiles, updatedInHoarder, excludedByTags, includedByTags, includedTagsEnabled: this.settings.includedTags.length > 0, skippedNoHighlights, deletionResults }; const message = buildSyncMessage(stats); return { success: true, message }; } catch (error) { console.error("Error syncing bookmarks:", error); return { success: false, message: `Error syncing: ${error.message}` }; } finally { this.setSyncing(false); this.skippedFiles = 0; } } async formatBookmarkAsMarkdown(bookmark, title, highlights) { const url = bookmark.content.type === "link" ? bookmark.content.url : bookmark.content.sourceUrl; const description = bookmark.content.type === "link" ? bookmark.content.description : bookmark.content.text; const rawTags = bookmark.tags.map((tag) => tag.name); const tags = sanitizeTags(rawTags); const { content: assetContent, frontmatter: assetsFm } = await processBookmarkAssets( this.app, bookmark, title, this.client, this.settings ); let assetsYaml = ""; if (assetsFm) { const lines = []; if (assetsFm.image) lines.push(`image: ${assetsFm.image}`); if (assetsFm.banner) lines.push(`banner: ${assetsFm.banner}`); if (assetsFm.screenshot) lines.push(`screenshot: ${assetsFm.screenshot}`); if (assetsFm.full_page_archive) lines.push(`full_page_archive: ${assetsFm.full_page_archive}`); if (assetsFm.video) lines.push(`video: ${assetsFm.video}`); if (assetsFm.additional && assetsFm.additional.length > 0) { lines.push("additional:"); for (const link of assetsFm.additional) { lines.push(` - ${link}`); } } assetsYaml = lines.join("\n") + "\n"; } const tagsYaml = tags.length > 0 ? `tags: - ${tags.join("\n - ")} ` : ""; let content = `--- bookmark_id: "${bookmark.id}" url: ${escapeYaml(url)} title: ${escapeYaml(title)} date: ${new Date(bookmark.createdAt).toISOString()} ${bookmark.modifiedAt ? `modified: ${new Date(bookmark.modifiedAt).toISOString()} ` : ""}${tagsYaml}note: ${escapeYaml(bookmark.note)} original_note: ${escapeYaml(bookmark.note)} summary: ${escapeYaml(bookmark.summary)} ${assetsYaml} --- # ${title} `; content += assetContent; if (bookmark.summary) { content += ` ## Summary ${bookmark.summary} `; } if (description) { content += ` ## Description ${description} `; } if (highlights && highlights.length > 0 && this.settings.syncHighlights) { content += ` ## Highlights `; const sortedHighlights = highlights.sort((a, b) => a.startOffset - b.startOffset); for (const highlight of sortedHighlights) { const date = new Date(highlight.createdAt).toLocaleDateString("en-US", { year: "numeric", month: "long", day: "numeric" }); content += `> [!karakeep-${highlight.color}] ${date} `; const highlightLines = highlight.text.split("\n"); for (const line of highlightLines) { content += `> ${line} `; } if (highlight.note && highlight.note.trim()) { content += `> `; const noteLines = highlight.note.split("\n"); for (let i = 0; i < noteLines.length; i++) { if (i === 0) { content += `> *Note: ${noteLines[i]}* `; } else { content += `> *${noteLines[i]}* `; } } } content += ` `; } } content += ` ## Notes ${bookmark.note || ""} `; if (url && bookmark.content.type !== "asset") { content += ` [Visit Link](${escapeMarkdownPath2(url)}) `; } const hoarderUrl = `${this.settings.apiEndpoint.replace("/api/v1", "/dashboard/preview")}/${bookmark.id}`; content += ` [View in Hoarder](${escapeMarkdownPath2(hoarderUrl)})`; return content; } async handleFileModification(file) { var _a; try { console.debug(`[Hoarder] File modified: ${file.path}`); const { currentNotes, originalNotes } = await this.extractNotesFromFile(file.path); const currentNotesStr = currentNotes || ""; const originalNotesStr = originalNotes || ""; console.log( `[Hoarder] Current notes length: ${currentNotesStr.length}, Original notes length: ${originalNotesStr.length}` ); if (currentNotesStr === this.lastSyncedNotes) { console.log("[Hoarder] Skipping - notes match last synced version"); return; } const metadata = (_a = this.app.metadataCache.getFileCache(file)) == null ? void 0 : _a.frontmatter; const bookmarkId = metadata == null ? void 0 : metadata.bookmark_id; if (!bookmarkId) { console.log("[Hoarder] No bookmark_id found in frontmatter"); return; } console.log(`[Hoarder] Bookmark ID: ${bookmarkId}`); if (originalNotes === null) { const frontmatterNote = (metadata == null ? void 0 : metadata.note) || ""; console.log(`[Hoarder] original_note is null for ${file.path}`); if (currentNotesStr !== frontmatterNote) { console.log("[Hoarder] Notes have changed from frontmatter note"); const success = await this.updateBookmarkInHoarder(bookmarkId, currentNotesStr); if (success) { this.lastSyncedNotes = currentNotesStr; setTimeout(async () => { try { const { currentNotes: latestNotes, originalNotes: currentOriginalNotes } = await this.extractNotesFromFile(file.path); if (latestNotes === currentNotesStr && currentOriginalNotes !== currentNotesStr) { await this.app.fileManager.processFrontMatter(file, (frontmatter) => { frontmatter["original_note"] = currentNotesStr; }); console.debug("[Hoarder] Initialized and updated original_note in frontmatter"); } else if (currentOriginalNotes === currentNotesStr) { console.debug( "[Hoarder] original_note already initialized, skipping frontmatter update" ); } } catch (error) { console.error("[Hoarder] Error updating frontmatter:", error); } }, 5e3); new import_obsidian3.Notice("Notes synced to Hoarder"); } else { console.error("[Hoarder] Failed to update bookmark in Hoarder"); } } else { console.debug( "[Hoarder] Notes match frontmatter, skipping original_note initialization to preserve mtime" ); } return; } if (currentNotesStr !== originalNotesStr) { console.log("[Hoarder] Notes have changed, syncing to Hoarder"); const updated = await this.updateBookmarkInHoarder(bookmarkId, currentNotesStr); if (updated) { this.lastSyncedNotes = currentNotesStr; console.log("[Hoarder] Successfully synced notes to Hoarder"); setTimeout(async () => { try { const { currentNotes: latestNotes, originalNotes: currentOriginalNotes } = await this.extractNotesFromFile(file.path); if (latestNotes === currentNotesStr && currentOriginalNotes !== currentNotesStr) { await this.app.fileManager.processFrontMatter(file, (frontmatter) => { frontmatter["original_note"] = currentNotesStr; }); console.debug("[Hoarder] Updated original_note in frontmatter"); } else if (latestNotes !== currentNotesStr) { console.debug("[Hoarder] Notes changed again, skipping frontmatter update"); } else { console.debug( "[Hoarder] original_note already up to date, skipping frontmatter update" ); } } catch (error) { console.error("[Hoarder] Error updating frontmatter:", error); } }, 5e3); new import_obsidian3.Notice("Notes synced to Hoarder"); } else { console.error("[Hoarder] Failed to update bookmark in Hoarder"); new import_obsidian3.Notice("Failed to sync notes to Hoarder"); } } else { console.log("[Hoarder] Notes unchanged, no sync needed"); } } catch (error) { console.error("[Hoarder] Error handling file modification:", error); new import_obsidian3.Notice("Failed to sync notes to Hoarder"); } } initializeClient() { if (!this.settings.apiKey || !this.settings.apiEndpoint) { this.client = null; } else { this.client = new HoarderApiClient( this.settings.apiEndpoint, this.settings.apiKey, this.settings.useObsidianRequest ); } } }; /* nosourcemap */