diff --git a/main.js b/main.js new file mode 100644 index 0000000..bffdb35 --- /dev/null +++ b/main.js @@ -0,0 +1,512 @@ +/* +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); + +// main.ts +var main_exports = {}; +__export(main_exports, { + default: () => LLMTaggerPlugin +}); +module.exports = __toCommonJS(main_exports); +var import_obsidian = require("obsidian"); +var ICON_NAME = "llm-tagger-robot"; +var VIEW_TYPE = "llm-tagger-view"; +var DEFAULT_SETTINGS = { + selectedModel: null, + defaultTags: [], + autoAddTags: false, + taggedFiles: {} +}; +var LLMTaggerPlugin = class extends import_obsidian.Plugin { + constructor() { + super(...arguments); + this.autoTaggingEnabled = false; + } + async onload() { + console.log("Loading LLM Tagger plugin"); + await this.loadSettings(); + (0, import_obsidian.addIcon)(ICON_NAME, ` + + + + + + `); + this.registerView( + VIEW_TYPE, + (leaf) => this.view = new LLMTaggerView(leaf, this) + ); + this.addRibbonIcon(ICON_NAME, "LLM Tagger", () => { + this.activateView(); + }); + this.app.workspace.onLayoutReady(() => { + if (this.settings.autoAddTags) { + this.enableAutoTagging(); + } + }); + this.addCommand({ + id: "add-tags", + name: "Add Tags to Documents", + callback: () => this.addTagsToDocuments() + }); + this.addSettingTab(new LLMTaggerSettingTab(this.app, this)); + console.log("LLM Tagger plugin loaded"); + } + enableAutoTagging() { + if (this.autoTaggingEnabled) + return; + this.autoTaggingEnabled = true; + const debouncedAutoTag = (0, import_obsidian.debounce)(async (file) => { + await this.autoTagFile(file); + }, 2e3, true); + this.registerEvent( + this.app.vault.on("create", async (file) => { + if (this.autoTaggingEnabled && file instanceof import_obsidian.TFile && file.extension === "md") { + await debouncedAutoTag(file); + } + }) + ); + this.registerEvent( + this.app.vault.on("modify", async (file) => { + if (this.autoTaggingEnabled && file instanceof import_obsidian.TFile && file.extension === "md") { + await debouncedAutoTag(file); + } + }) + ); + } + disableAutoTagging() { + this.autoTaggingEnabled = false; + } + async autoTagFile(file) { + if (!this.settings.autoAddTags || !this.settings.selectedModel || !this.settings.defaultTags.length) { + return; + } + try { + const content = await this.app.vault.read(file); + if (!content.trim()) { + return; + } + const taggedContent = await this.processContentWithOllama( + content, + this.settings.defaultTags + ); + if (taggedContent !== content) { + await this.app.vault.modify(file, taggedContent); + new import_obsidian.Notice(`Auto-tagged: ${file.basename}`); + this.settings.taggedFiles[file.path] = Date.now(); + await this.saveSettings(); + } + } catch (error) { + console.error("Error auto-tagging file:", error); + new import_obsidian.Notice(`Failed to auto-tag ${file.basename}: ${error.message}`); + } + } + async activateView() { + const { workspace } = this.app; + let leaf = workspace.getLeavesOfType(VIEW_TYPE)[0]; + if (!leaf) { + const rightLeaf = workspace.getRightLeaf(false); + if (rightLeaf) { + await rightLeaf.setViewState({ type: VIEW_TYPE }); + leaf = rightLeaf; + } + } + if (leaf) { + workspace.revealLeaf(leaf); + } + } + async onunload() { + console.log("Unloading LLM Tagger plugin"); + this.disableAutoTagging(); + this.app.workspace.detachLeavesOfType(VIEW_TYPE); + } + async loadSettings() { + this.settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData()); + } + async saveSettings() { + await this.saveData(this.settings); + if (this.settings.autoAddTags) { + this.enableAutoTagging(); + } else { + this.disableAutoTagging(); + } + } + async getOllamaModels() { + var _a; + try { + const response = await fetch("http://localhost:11434/api/tags"); + const data = await response.json(); + return ((_a = data.models) == null ? void 0 : _a.map((model) => model.name)) || []; + } catch (error) { + console.error("Failed to fetch Ollama models:", error); + return []; + } + } + async getUserDefinedTags() { + return new Promise(async (resolve) => { + const modal = new import_obsidian.Modal(this.app); + modal.titleEl.setText("Configure Tags"); + const modelContainer = modal.contentEl.createDiv(); + modelContainer.style.marginBottom = "1em"; + const modelLabel = modelContainer.createEl("label"); + modelLabel.setText("Select Ollama Model:"); + const modelSelect = modelContainer.createEl("select"); + modelSelect.style.width = "100%"; + modelSelect.style.marginTop = "0.5em"; + const placeholderOption = modelSelect.createEl("option"); + placeholderOption.value = ""; + placeholderOption.text = "Select a model..."; + placeholderOption.disabled = true; + placeholderOption.selected = !this.settings.selectedModel; + try { + const models = await this.getOllamaModels(); + models.forEach((model) => { + const option = modelSelect.createEl("option"); + option.value = model; + option.text = model; + if (model === this.settings.selectedModel) { + option.selected = true; + } + }); + } catch (error) { + console.error("Failed to load models:", error); + const option = modelSelect.createEl("option"); + option.text = "Failed to load models"; + option.disabled = true; + } + const tagsContainer = modal.contentEl.createDiv(); + tagsContainer.style.marginTop = "1em"; + const tagsLabel = tagsContainer.createEl("label"); + tagsLabel.setText("Enter tags (comma-separated):"); + const input = tagsContainer.createEl("textarea"); + input.style.width = "100%"; + input.style.height = "100px"; + input.style.marginTop = "0.5em"; + input.placeholder = "Enter tags separated by commas..."; + if (this.settings.defaultTags.length > 0) { + input.value = this.settings.defaultTags.join(", "); + } + const buttonContainer = modal.contentEl.createDiv(); + buttonContainer.style.marginTop = "1em"; + buttonContainer.style.display = "flex"; + buttonContainer.style.justifyContent = "flex-end"; + buttonContainer.style.gap = "10px"; + const cancelButton = buttonContainer.createEl("button", { text: "Cancel" }); + const okButton = buttonContainer.createEl("button", { text: "OK", cls: "mod-cta" }); + cancelButton.addEventListener("click", () => { + resolve(null); + modal.close(); + }); + okButton.addEventListener("click", () => { + if (!modelSelect.value) { + new import_obsidian.Notice("Please select a model first"); + return; + } + const tagInput = input.value.trim(); + if (!tagInput) { + new import_obsidian.Notice("Please enter at least one tag"); + return; + } + this.settings.selectedModel = modelSelect.value; + this.saveSettings(); + const tags = tagInput.split(",").map((tag) => tag.trim()).filter((tag) => tag); + resolve(tags); + modal.close(); + }); + modal.open(); + }); + } + addDeterministicTags(content, availableTags) { + const lowerContent = content.toLowerCase(); + const addedTags = /* @__PURE__ */ new Set(); + for (const tag of availableTags) { + const cleanTag = tag.replace(/^#/, "").toLowerCase(); + const regex = new RegExp(`\\b${cleanTag}\\b`, "i"); + if (regex.test(lowerContent)) { + addedTags.add(tag.startsWith("#") ? tag : `#${tag}`); + } + } + if (addedTags.size === 0) { + return content; + } + const frontMatterMatch = content.match(/^---\n[\s\S]*?\n---\n/); + if (frontMatterMatch) { + const frontMatter = frontMatterMatch[0]; + const restContent = content.slice(frontMatter.length); + return `${frontMatter}${Array.from(addedTags).join(" ")} ${restContent}`; + } else { + return `${Array.from(addedTags).join(" ")} ${content}`; + } + } + async processContentWithOllama(content, availableTags) { + if (!this.settings.selectedModel) { + throw new Error("No Ollama model selected"); + } + if (this.isAlreadyTagged(content)) { + return content; + } + let processedContent = this.addDeterministicTags(content, availableTags); + const response = await fetch("http://localhost:11434/api/generate", { + method: "POST", + headers: { + "Content-Type": "application/json" + }, + body: JSON.stringify({ + model: this.settings.selectedModel, + prompt: `You are an expert at analyzing and tagging markdown documents. Your task is to create a brief tagged summary of the document content. + +Available tags: ${availableTags.join(", ")} + +Instructions: +1. Create a brief 1-2 sentence summary of the content +2. Add relevant tags from the provided list that WEREN'T already matched by word (don't repeat tags) +3. Only use tags from the provided list +4. Each tag MUST start with a # symbol +5. Keep the summary concise and focused + +Content to analyze (with existing tags): +${processedContent} + +Provide a tagged summary:`, + stream: false + }) + }); + const data = await response.json(); + const taggedSummary = data.response.trim(); + if (taggedSummary) { + const timestamp = (/* @__PURE__ */ new Date()).toISOString(); + return `--- +LLM-tagged: ${timestamp} +--- + +${taggedSummary} + +--- + +${processedContent}`; + } + return processedContent; + } + shouldProcessFile(file) { + const lastTagged = this.settings.taggedFiles[file.path]; + if (!lastTagged) + return true; + return file.stat.mtime > lastTagged; + } + isAlreadyTagged(content) { + return content.includes("---\nLLM-tagged:"); + } + async addTagsToDocuments(view) { + if (!this.settings.selectedModel) { + new import_obsidian.Notice("Please select an Ollama model first"); + return; + } + const tags = await this.getUserDefinedTags(); + if (!tags) + return; + const files = this.app.vault.getMarkdownFiles(); + let processed = 0; + let modified = 0; + try { + for (const file of files) { + processed++; + if (view) { + view.updateProgress(processed, files.length, file.basename); + } + if (!this.shouldProcessFile(file)) { + console.log(`Skipping ${file.basename} - already tagged and not modified`); + continue; + } + try { + const content = await this.app.vault.read(file); + if (this.isAlreadyTagged(content)) { + console.log(`Skipping ${file.basename} - already has tag metadata`); + continue; + } + const taggedContent = await this.processContentWithOllama(content, tags); + if (taggedContent !== content) { + await this.app.vault.modify(file, taggedContent); + this.settings.taggedFiles[file.path] = Date.now(); + await this.saveSettings(); + modified++; + new import_obsidian.Notice(`Tagged: ${file.basename}`); + } + } catch (error) { + console.error(`Error processing ${file.basename}:`, error); + new import_obsidian.Notice(`Failed to process ${file.basename}: ${error.message}`); + } + } + new import_obsidian.Notice(`Completed! Tagged ${modified} of ${files.length} files`); + } finally { + if (view) { + view.resetProgress(); + } + } + } +}; +var LLMTaggerView = class extends import_obsidian.ItemView { + constructor(leaf, plugin) { + super(leaf); + this.plugin = plugin; + } + getViewType() { + return VIEW_TYPE; + } + getDisplayText() { + return "LLM Tagger"; + } + getIcon() { + return ICON_NAME; + } + async onOpen() { + const container = this.containerEl.children[1]; + container.empty(); + container.createEl("h2", { text: "LLM Tagger" }); + const modelContainer = container.createDiv(); + modelContainer.createEl("h3", { text: "Select Model" }); + const modelSelect = modelContainer.createEl("select"); + modelSelect.style.width = "100%"; + modelSelect.style.marginBottom = "1em"; + const placeholderOption = modelSelect.createEl("option"); + placeholderOption.value = ""; + placeholderOption.text = "Select a model..."; + placeholderOption.disabled = true; + placeholderOption.selected = !this.plugin.settings.selectedModel; + try { + const models = await this.plugin.getOllamaModels(); + models.forEach((model) => { + const option = modelSelect.createEl("option"); + option.value = model; + option.text = model; + if (model === this.plugin.settings.selectedModel) { + option.selected = true; + } + }); + } catch (error) { + console.error("Failed to load models:", error); + const option = modelSelect.createEl("option"); + option.text = "Failed to load models"; + option.disabled = true; + } + modelSelect.addEventListener("change", async () => { + this.plugin.settings.selectedModel = modelSelect.value || null; + await this.plugin.saveSettings(); + }); + const tagsContainer = container.createDiv(); + tagsContainer.createEl("h3", { text: "Enter Tags" }); + const tagsInput = tagsContainer.createEl("textarea"); + tagsInput.placeholder = "Enter tags separated by commas..."; + tagsInput.style.width = "100%"; + tagsInput.style.height = "100px"; + tagsInput.style.marginBottom = "1em"; + if (this.plugin.settings.defaultTags.length > 0) { + tagsInput.value = this.plugin.settings.defaultTags.join(", "); + } + const progressContainer = container.createDiv(); + progressContainer.createEl("h3", { text: "Progress" }); + this.progressBar = progressContainer.createEl("progress", { + attr: { value: "0", max: "100" } + }); + this.progressBar.style.width = "100%"; + this.progressBar.style.marginBottom = "0.5em"; + this.progressText = progressContainer.createDiv("progress-text"); + this.progressText.style.fontSize = "0.9em"; + this.progressText.style.color = "var(--text-muted)"; + this.progressText.style.marginBottom = "1em"; + this.progressText.textContent = "Ready to tag documents"; + const startButton = container.createEl("button", { + text: "Start Tagging", + cls: "mod-cta" + }); + startButton.style.width = "100%"; + startButton.addEventListener("click", async () => { + if (!modelSelect.value) { + new import_obsidian.Notice("Please select a model first"); + return; + } + const tagInput = tagsInput.value.trim(); + if (!tagInput) { + new import_obsidian.Notice("Please enter at least one tag"); + return; + } + const tags = tagInput.split(",").map((tag) => tag.trim()).filter((tag) => tag); + if (tagInput !== this.plugin.settings.defaultTags.join(", ")) { + this.plugin.settings.defaultTags = tags; + await this.plugin.saveSettings(); + } + startButton.disabled = true; + try { + await this.plugin.addTagsToDocuments(this); + } finally { + startButton.disabled = false; + } + }); + } + updateProgress(current, total, filename) { + const percentage = Math.round(current / total * 100); + this.progressBar.value = percentage; + this.progressText.textContent = `Processing ${filename} (${current}/${total})`; + } + resetProgress() { + this.progressBar.value = 0; + this.progressText.textContent = "Ready to tag documents"; + } +}; +var LLMTaggerSettingTab = class extends import_obsidian.PluginSettingTab { + constructor(app, plugin) { + super(app, plugin); + this.plugin = plugin; + } + async display() { + const { containerEl } = this; + containerEl.empty(); + containerEl.createEl("h2", { text: "LLM Tagger Settings" }); + new import_obsidian.Setting(containerEl).setName("Default Model").setDesc("Select the default Ollama model to use").addDropdown(async (dropdown) => { + dropdown.addOption("", "Select a model..."); + try { + const models = await this.plugin.getOllamaModels(); + models.forEach((model) => { + dropdown.addOption(model, model); + }); + if (this.plugin.settings.selectedModel) { + dropdown.setValue(this.plugin.settings.selectedModel); + } + } catch (error) { + console.error("Failed to load models:", error); + dropdown.addOption("error", "Failed to load models"); + dropdown.setDisabled(true); + } + dropdown.onChange(async (value) => { + this.plugin.settings.selectedModel = value || null; + await this.plugin.saveSettings(); + }); + }); + new import_obsidian.Setting(containerEl).setName("Default Tags").setDesc("Enter default tags (comma-separated) that will be pre-filled when adding tags").addTextArea((text) => text.setPlaceholder("tag1, tag2, tag3").setValue(this.plugin.settings.defaultTags.join(", ")).onChange(async (value) => { + this.plugin.settings.defaultTags = value.split(",").map((tag) => tag.trim()).filter((tag) => tag); + await this.plugin.saveSettings(); + })); + new import_obsidian.Setting(containerEl).setName("Auto-add Tags").setDesc("Automatically add tags to new documents").addToggle((toggle) => toggle.setValue(this.plugin.settings.autoAddTags).onChange(async (value) => { + this.plugin.settings.autoAddTags = value; + await this.plugin.saveSettings(); + })); + } +};