Build an LLM Journaling Reflection Plugin for Obsidian
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:
- We're creating a settings interface to store your Claude API key (gotta keep that safe!)
- When the plugin loads, it:
- Grabs your saved settings
- Adds a settings tab
- Creates a "Reflect" command you can use anytime
- 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:
-
First, find your Obsidian vault's
.obsidian/plugins
folder. Make sure that this project is in theplugins
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. :) -
Next, install everything:
pnpm install
- 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.
- 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.