diff --git a/.gitignore b/.gitignore
index 6f3e00a..a765044 100644
--- a/.gitignore
+++ b/.gitignore
@@ -8,6 +8,8 @@ yarn-error.log*
dist/
build/
*.js.map
+main.js
+data.json
# IDE and editor files
.idea/
diff --git a/main.js b/main.js
deleted file mode 100644
index 553163a..0000000
--- a/main.js
+++ /dev/null
@@ -1,489 +0,0 @@
-/*
-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();
- });
- }
- async processContentWithOllama(content, availableTags) {
- if (!this.settings.selectedModel) {
- throw new Error("No Ollama model selected");
- }
- if (this.isAlreadyTagged(content)) {
- return content;
- }
- 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
-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:
-${content}
-
-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}
-
----
-
-${content}`;
- }
- return content;
- }
- 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();
- }));
- }
-};
diff --git a/main.ts b/main.ts
index 3ff6be1..4f099aa 100644
--- a/main.ts
+++ b/main.ts
@@ -276,6 +276,40 @@ export default class LLMTaggerPlugin extends Plugin {
});
}
+ private addDeterministicTags(content: string, availableTags: string[]): string {
+ // Convert content to lowercase for case-insensitive matching
+ const lowerContent = content.toLowerCase();
+
+ // Create a set to track which tags we've added to avoid duplicates
+ const addedTags = new Set();
+
+ // Find matching tags
+ for (const tag of availableTags) {
+ // Remove # if present and convert to lowercase
+ const cleanTag = tag.replace(/^#/, '').toLowerCase();
+
+ // Check if the tag exists as a whole word in the content
+ const regex = new RegExp(`\\b${cleanTag}\\b`, 'i');
+ if (regex.test(lowerContent)) {
+ addedTags.add(tag.startsWith('#') ? tag : `#${tag}`);
+ }
+ }
+
+ if (addedTags.size === 0) {
+ return content;
+ }
+
+ // Add tags at the start of the content, after any existing frontmatter
+ 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: string, availableTags: string[]): Promise {
if (!this.settings.selectedModel) {
throw new Error('No Ollama model selected');
@@ -286,7 +320,10 @@ export default class LLMTaggerPlugin extends Plugin {
return content;
}
- // Get tag suggestions and placement from Ollama
+ // First, add deterministic tags based on word matches
+ let processedContent = this.addDeterministicTags(content, availableTags);
+
+ // Get tag suggestions and placement from Ollama for additional semantic tagging
const response = await fetch('http://localhost:11434/api/generate', {
method: 'POST',
headers: {
@@ -300,13 +337,13 @@ Available tags: ${availableTags.join(', ')}
Instructions:
1. Create a brief 1-2 sentence summary of the content
-2. Add relevant tags from the provided list
+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:
-${content}
+Content to analyze (with existing tags):
+${processedContent}
Provide a tagged summary:`,
stream: false
@@ -328,10 +365,10 @@ ${taggedSummary}
---
-${content}`;
+${processedContent}`;
}
- return content;
+ return processedContent;
}
private shouldProcessFile(file: TFile): boolean {