Build an LLM Journaling Reflection Plugin for Obsidian

Demo of Obsidian reflective journaling

Building an LLM Journaling Reflection Plugin for Obsidian

Journaling has been a game changer for me. It's an exercise that forces me to structure my thoughts into language and helps me identify where I lack mental clarity. But sometimes when my thoughts are a big hazy mess, it's difficult to write with structure.

Enter LLMs! I've found a writing paradigm that enforces clarity and draws out new thoughts is one where AI gives me reflective questions throughout the writing process.

That's why I built an Obsidian plugin to do exactly that. As I journal, I can invoke AI to give me a reflective question to guide my writing. This post documents how I built this Obsidian plugin that uses Claude (Anthropic's AI model) to help make journaling more insightful.

If you just want to see the full source code, take a look at the repo here.

Prequisites

You'll need:

  • Obsidian desktop app
  • Node.js (the LTS version works great)
  • A package manager (I like pnpm, but npm or yarn work too)
  • A Claude API key from Anthropic (head over to their site to get one)

Setting Up Our Project

Let's get our files organized. Here's what we need:

obsidian-reflective-journal-plugin
├── main.ts
├── package.json
├── manifest.json
└── esbuild.config.mjs

Nothing too fancy here - just the essentials for an Obsidian plugin. The real magic happens in main.ts.

The Good Stuff: Our Plugin Code

I'll walk you through the most important parts of our code. First up, here's our main plugin file:

// main.ts
import Anthropic from "@anthropic-ai/sdk";
import { TextBlock } from "@anthropic-ai/sdk/resources/index.mjs";
import {
  App,
  MarkdownView,
  Notice,
  Plugin,
  PluginSettingTab,
  Setting
} from "obsidian";
  
  // Define the plugin’s settings.
  interface ReflectiveJournalSettings {
    claudeApiKey: string;
  }
  
  //Default settings if none are stored yet.
  const DEFAULT_SETTINGS: ReflectiveJournalSettings = {
    claudeApiKey: "",
  };
  
  export default class ReflectiveJournalPlugin extends Plugin {
    settings: ReflectiveJournalSettings;
  
    async onload() {
      console.log("Loading Reflective Journal Plugin...");
  
      // Load settings from disk
      this.settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData());
  
      // Add the settings tab in Obsidian
      this.addSettingTab(new ReflectiveJournalSettingTab(this.app, this));
  
      // Add a command to Obsidian's command palette
      this.addCommand({
        id: "reflect-button",
        name: "Reflect",
        callback: async () => {
          // Get the current active Markdown editor
          const currentView = this.app.workspace.getActiveViewOfType(MarkdownView);
          if (!currentView) {
            new Notice("No active markdown editor found.");
            return;
          }
  
          // Get the current document text
          const docContent = currentView.editor.getValue();
  
          // Call Claude API to get a reflective journaling prompt
          const suggestedQuestion = await this.getSuggestedQuestion(docContent);
  
          // If we successfully got a question, insert it as a blockquote
          if (suggestedQuestion) {
            // Insert as a block quote at the current cursor position
            const formattedQuestion = suggestedQuestion.split('\n').map(line => `> ${line}`).join('\n');
            currentView.editor.replaceSelection(formattedQuestion + '\n\n\n');
 
            new Notice("Reflective question inserted!");
          } else {
            new Notice("Failed to fetch reflective question.");
          }
        },
      });
    }
  
    // Unload lifecycle hook. 
    onunload() {
      console.log("Unloading Reflective Journal Plugin...");
    }
  
    // Save settings to disk.
    async saveSettings() {
      await this.saveData(this.settings);
    }

This is the core setup of our plugin minus the Claude API integration. The logic grabs the content of the active editor, and sends that to the getSuggestedQuestion method which we will implement next. It then inserts it into the editor as a Markdown blockquote.

Now we need to add the logic behind the getSuggestedQuestion function. In the ReflectiveJournalPlugin class, add this function:

// main.ts
/**
     * Calls Anthropic's Claude API to get a reflective journaling question.
     * Adjust the model, prompt, and parameters according to your needs.
     */
    private async getSuggestedQuestion(documentText: string): Promise<string | null> {
      try {
        if (!this.settings.claudeApiKey) {
          new Notice("No Claude API key set. Please go to plugin settings.");
          return null;
        }
  
        const system = `
        You are a helpful journaling assistant that helps the user reflect while they journal.
        You will be given a user's journal entry as markdown. Blockquotes represent past reflection questions asked by you.
        Your job is to read the journal and suggest a reflection question to further stimulate the writer's thoughts and guide their thinking.
        Return the reflection question in raw text and not a markdown blockquote.
        `;
        const prompt = `
        Suggest the reflection question for this journal:
 
        ${documentText}
        `;
 
  
        const anthropic = new Anthropic({
            apiKey: this.settings.claudeApiKey,
            dangerouslyAllowBrowser: true
        });
 
        const response = await anthropic.messages.create({
            model: "claude-3-5-sonnet-20241022",
            system: system,
            max_tokens: 1024,
            messages: [
              {"role": "user", "content": prompt}
            ]
          });
 
        const content = response.content[0] as TextBlock
        return content.text 
      } catch (error) {
        console.error("Error calling Claude API: ", error);
        return error;
      }
    }
  }

The last thing we need to add is the ReflectiveJournalSettingTab class to render a settings UI in Obsidian settings:

// main.ts
/** Settings tab for storing the user’s Claude API key. */
  class ReflectiveJournalSettingTab extends PluginSettingTab {
    plugin: ReflectiveJournalPlugin;
  
    constructor(app: App, plugin: ReflectiveJournalPlugin) {
      super(app, plugin);
      this.plugin = plugin;
    }
  
    display(): void {
      const { containerEl } = this;
      containerEl.empty();
  
      containerEl.createEl("h2", { text: "Reflective Journal Plugin Settings" });
  
      // Text field for the user to enter their Claude API key
      new Setting(containerEl)
        .setName("Claude API Key")
        .setDesc("Enter your Anthropic Claude API key.")
        .addText((text) =>
          text
            .setPlaceholder("Enter your key")
            .setValue(this.plugin.settings.claudeApiKey)
            .onChange(async (value) => {
              this.plugin.settings.claudeApiKey = value.trim();
              await this.plugin.saveSettings();
            })
        );
    }
  }

Let's break down everything that's happening in our plugin:

  1. We're creating a settings interface to store your Claude API key (gotta keep that safe!)
  2. When the plugin loads, it:
    • Grabs your saved settings
    • Adds a settings tab
    • Creates a "Reflect" command you can use anytime
  3. When you trigger "Reflect", it sends your journal entry to Claude and gets back a personalized question

Making It All Work Together

Our package.json is pretty straightforward:

// package.json
{
  "name": "obsidian-reflective-journal-plugin",
  "version": "1.0.0",
  "main": "main.js",
  "scripts": {
    "dev": "node esbuild.config.mjs --watch",
    "build": "node esbuild.config.mjs"
  },
  "devDependencies": {
    "esbuild": "^0.17.0",
    "obsidian": "latest",
    "typescript": "^4.8.4"
  },
  "dependencies": {
    "@anthropic-ai/sdk": "^0.33.1"
  }
}

Taking It For a Spin

Ready to try it out? Here's how:

  1. First, find your Obsidian vault's .obsidian/plugins folder. Make sure that this project is in the plugins folder (or create a symlink from your original folder to this one). I won't be covering that here for brevity, but ask your LLM for help if you're unsure. :)

  2. Next, install everything:

pnpm install 
  1. To build the plugin, run:
pnpm dev # pnpm run build if you don't want to watch for file changes

This will watch your files and rebuild whenever you make changes - super handy while you're tweaking things.

  1. When you're ready to use it in Obsidian:
  • Go to settings -> Community plugins -> Enable community plugins
  • If your plugin doesn't automatically appear, restart Obsidian
  • In your Settings sidebar, there should be a "Reflective Journal Plugin" tab now. Click it and enter your Anthropic API key in the text box.
  • Now when you're journaling, use cmd+P and trigger the plugin whenever you want a reflective question from Claude!

What's Next?

Now that you've got this working, the fun part begins! You could:

  • Play with different prompting styles to get different kinds of questions
  • Add features like sentiment analysis
  • Create different reflection modes (gratitude, goal-setting, problem-solving)

Wrapping Up

And there you have it! A plugin that makes journaling a bit more structured with some AI-powered help. It's pretty cool what you can build when you combine Obsidian's powerful plugin system with Claude's ability to understand and respond to your writing.

Got ideas for making this better? Or just want to discuss anything? Drop me a message on X.