Obsidian Plugin Integration¶
Build a new Obsidian plugin integration for work-buddy — probe, wrap, package, and optionally collect
When to use¶
When the user invokes /wb-dev-plugin-integration or asks to integrate a new Obsidian plugin into work-buddy
Slash command: /wb-dev-plugin-integration
Directions¶
Build a new Obsidian plugin integration for work-buddy.
Essential reading (in this order)¶
-
work_buddy/obsidian/smart/README.md— read the Reverse-Engineering Methodology section. It explains how to probe bundled Obsidian plugins at runtime via eval_js (object graph walking, prototype method discovery, function source extraction, capability probing). Use these exact techniques. -
work_buddy/obsidian/smart/env.py— the template pattern: JS snippets in_js/directory,_run_js()helper, Python wrapper functions with docstrings. Follow this pattern exactly. -
work_buddy/obsidian/bridge.py— the eval_js transport layer.bridge.eval_js(code, timeout)executes JS inside Obsidian with access to theappobject. Consent forobsidian.eval_jsis already permanently granted. -
work_buddy/calendar/— a simple, complete reference integration (Google Calendar). Readenv.py,_js/, and the collector to see the full pattern end-to-end. -
work_buddy/obsidian/tasks/— a more complex integration (Obsidian Tasks plugin). Shows mutations, intelligence layer, and task-specific patterns.
Finding the plugin¶
The target plugin lives under <vault-root>\.obsidian\plugins\<plugin-id>\. The main.js is bundled/minified — you cannot read the source on disk. Use runtime probing via eval_js instead.
Check the manifest:
cat "<vault-root>/.obsidian/plugins/<plugin-id>/manifest.json"
Probe approach¶
Use dedicated JS probe files for complex exploration. Write a .js file in the _js/ directory and execute it:
from work_buddy.obsidian import bridge
from pathlib import Path
js = Path('work_buddy/obsidian/<plugin>/_js/_probe_<thing>.js').read_text()
result = bridge.eval_js(js, timeout=15)
This avoids quote escaping hell (PowerShell → Python → JS), is readable/editable, and can be version controlled. Delete probe files after documenting findings — prefix with _probe_ to mark them as temporary.
What to probe (in this order)¶
-
Check if plugin is loaded:
app.plugins.plugins["<plugin-id>"] -
Walk top-level keys:
Object.keys(plugin)— find methods, API objects, settings, cache. -
Check for a public API: Look for
plugin.api,plugin.apiV1, etc. Walk methods and get sources. Public APIs are stable; internal methods may break on update. -
Explore stateful objects: Plugins often have a
cache,store, orstateobject holding live data. Walk its keys, check if it has getter methods, count items. -
Get method signatures:
fn.toString()gives minified but readable source. Look for parameter names and return structures. -
Probe prototypes:
Object.getPrototypeOf(obj)+Object.getOwnPropertyNames(proto)to find inherited methods including those from parent classes. -
Check registered commands:
app.commands.commands["<plugin-id>:*"]— these may haveeditorCheckCallbackorcallbackfunctions with useful logic. Read their source. -
Test calls — verify side effects: Before assuming a method is pure, check whether it mutates in-memory objects or writes to disk. Read the file before and after calling. Example:
const contentBefore = await app.vault.read(file); const result = task.handleNewStatus(newStatus); // call the method const contentAfter = await app.vault.read(file); return {file_changed: contentBefore !== contentAfter, ...}; -
Test serialization: Task/note objects often have circular references (parent<->children). You can't return them raw from eval_js. Instead, call serialization methods (
toString(),toFileLineString()) inside JS and return the plain string.
Important: Plugins use window.moment for dates (Obsidian bundles Moment.js), not native Date objects.
Package structure¶
Decide where the integration lives based on scope:
- Obsidian-specific (reading plugin runtime) → work_buddy/obsidian/<plugin>/
- Cross-cutting capability (calendar, email, etc.) → work_buddy/<capability>/
Create:
work_buddy/<location>/
├── __init__.py # Public API re-exports
├── env.py # Python wrappers (_run_js pattern from smart/env.py)
├── README.md # Integration docs + runtime surface + stale warnings
└── _js/ # JavaScript snippets
├── check_ready.js # Always include a readiness check
└── ... # One snippet per capability
JS snippet rules¶
- Wrap in async IIFE:
return (async () => { ... })() - Use
__PLACEHOLDER__for parameter injection (replaced by Python before execution) - Return
{error: "message"}on failure, data on success - Handle missing plugins gracefully:
if (!plugin) return {error: "Plugin not found"} - Keep snippets focused — one operation per file
Python wrapper rules¶
- Follow
work_buddy/calendar/env.pyorwork_buddy/obsidian/tasks/env.pyexactly _load_js(name)+_run_js(snippet, replacements, timeout)helperscheck_ready()function always first- Docstrings on every public function with return type descriptions
- Use
_escape_js()for user-provided text going into JS strings
Context collection (if applicable)¶
If the integration produces data useful for context bundles:
1. Create work_buddy/collectors/<name>_collector.py following calendar_collector.py
2. Add to COLLECTORS set and import in work_buddy/collect.py
3. Write output as <name>_summary.md in the bundle directory
4. Always degrade gracefully (return unavailable report, don't crash)
Running Python¶
powershell.exe -Command "cd <vault-root>\repos\work-buddy; conda activate work-buddy; <command>"
Set WORK_BUDDY_SESSION_ID env var before any work_buddy imports (consent system needs it).
Testing¶
Write a temporary _test_<name>.py script that exercises every function. Run it, verify output, then delete it. Test:
1. check_ready() — plugin found and functional
2. Each query function — returns expected structure
3. Collector output — markdown is clean and informative
4. Graceful degradation — what happens when Obsidian is closed?
Checklist¶
- [ ] Plugin probed, runtime surface documented in README
- [ ]
check_ready.js+ readiness function - [ ] Core query/read functions working
- [ ]
__init__.pywith clean re-exports - [ ] README with stale/maintenance warnings if applicable
- [ ] Collector integrated into context bundle system (if applicable)
- [ ] End-to-end test passing
- [ ] Temp files cleaned up
- [ ] CLAUDE.md repo structure updated (if new top-level package)