Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 52 additions & 0 deletions code_puppy/plugins/obsidian_agent/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
# Obsidian Agent

The Obsidian Agent adds a specialized Code Puppy agent for working with Obsidian vaults through the official `obsidian` CLI.

## What it does

- Discovers vaults, notes, folders, links, tags, tasks, templates, and related Obsidian state.
- Uses the Obsidian CLI for vault-aware operations instead of blindly editing files.
- Follows a read-before-write workflow for note changes.
- Requires explicit confirmation for destructive, broad, or hard-to-reverse operations.
- Helps troubleshoot common CLI setup issues.

## Requirements

- Obsidian desktop 1.12.7 or newer.
- Obsidian's command line interface enabled in Settings → General.
- The Obsidian desktop app running, or available to be launched by the CLI.
- The `obsidian` command available on `PATH`.

Useful checks:

```bash
which obsidian
obsidian version
obsidian help
obsidian vaults total verbose
```

## Usage

Switch to the agent from Code Puppy and ask for an Obsidian task. Include a vault name or vault-relative path when relevant.

Examples:

- "Find notes about release planning in my work vault."
- "Create a meeting note from my weekly meeting template."
- "Append this task to today's daily note."
- "Show unresolved links and propose a cleanup plan."

For broad updates, the agent should present a plan and ask for confirmation before making changes. Tiny bureaucracy, but the useful kind.

## Safety notes

The agent intentionally avoids personal defaults. It does not include any user-specific vault names, local paths, or private note structure.

It treats these as confirmation-required operations:

- Permanent deletes, moves, and renames.
- History or sync restores.
- Publish changes.
- Plugin, theme, snippet, sync, reload, restart, and developer/debug commands.
- Arbitrary JavaScript evaluation.
8 changes: 8 additions & 0 deletions code_puppy/plugins/obsidian_agent/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
"""Obsidian Agent plugin.

Registers a specialized agent for safe Obsidian CLI workflows.
"""

from .agent_obsidian import ObsidianAgent

__all__ = ["ObsidianAgent"]
147 changes: 147 additions & 0 deletions code_puppy/plugins/obsidian_agent/agent_obsidian.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
"""Obsidian Agent for working with Obsidian vaults via the official CLI."""

from code_puppy.agents.base_agent import BaseAgent


class ObsidianAgent(BaseAgent):
"""Specialized agent for safe Obsidian CLI automation."""

@property
def name(self) -> str:
return "obsidian-agent"

@property
def display_name(self) -> str:
return "Obsidian Agent 🪨"

@property
def description(self) -> str:
return (
"Automates and assists with Obsidian vaults via the official "
"obsidian CLI using careful discovery and safe write workflows."
)

def get_available_tools(self) -> list[str]:
"""Tools used for CLI execution and explicit user confirmation."""
return ["agent_run_shell_command", "ask_user_question"]

def get_user_prompt(self) -> str:
"""Prompt shown when users switch to the agent directly."""
return (
"What would you like me to do in Obsidian? Tell me the vault name "
"or path if you want a specific vault targeted."
)

def get_system_prompt(self) -> str:
"""Return the Obsidian Agent system prompt."""
return """
You are Obsidian Agent 🪨, a careful, pragmatic assistant for helping users work with Obsidian vaults through the official `obsidian` CLI.

## Purpose

- Query, inspect, and update Obsidian vault content from the terminal.
- Prefer Obsidian-aware CLI operations over direct filesystem edits when links, properties, templates, tasks, sync, publish, or app state matter.
- Never claim a vault was changed unless you actually used tools and verified the result.

## Obsidian CLI assumptions

The official CLI requires:
- Obsidian desktop 1.12.7 or newer.
- Settings → General → Command line interface enabled.
- The Obsidian desktop app running, or available to be launched by the first CLI command.

Useful diagnostics:
- `which obsidian`
- `obsidian version`
- `obsidian help`
- `obsidian help <command>`
- `obsidian vaults total verbose`

## Command construction

- Use `agent_run_shell_command` for every actual Obsidian CLI operation.
- Commands are one-shot, for example `obsidian read path='Projects/Plan.md'`.
- Use parameters as `key=value`; flags are bare words.
- Prefer `format=json` when supported so results are easier to parse.
- When targeting a specific vault, put it first: `obsidian vault='<name-or-id>' <command> ...`.
- Prefer `path='<vault-relative-path>'` when the exact path is known.
- Use `file='<note name>'` only when Obsidian link/name resolution is desired.
- Quote user-provided vault names, paths, queries, and content carefully. Be especially cautious with apostrophes, semicolons, backticks, dollar signs, pipes, ampersands, and newlines.

## Safety policy

- Discover before modifying whenever practical: list, search, or read before write.
- Read before write for note edits.
- Preserve user content, frontmatter/properties, links, task status, and metadata.
- Prefer minimal commands such as `append`, `prepend`, `property:set`, task status changes, or template-based creation.
- Do not output entire notes unnecessarily.
- Ask for explicit confirmation before destructive, broad, or hard-to-reverse operations.
- For broad or multi-file changes, first provide a concise plan with target vault, affected files, intended changes, backup/history considerations, and command classes to run.

## Read-only operations

Generally safe to run for discovery:
- `help`, `version`, `vault`, `vaults`
- `file`, `files`, `folder`, `folders`, `read`
- `search`, `search:context`
- `backlinks`, `links`, `unresolved`, `orphans`, `deadends`
- `outline`, `tags`, `tag`, `tasks`, `properties`, `aliases`
- `templates`, `template:read`
- `plugins`, `plugins:enabled`, `themes`, `theme`
- `wordcount`, `recents`, `tabs`, `bookmarks`
- `history`, `history:list`, `history:read`, `diff`
- `sync:status`, `sync:history`, `sync:read`, `sync:deleted`
- `publish:site`, `publish:list`, `publish:status`
- `workspaces`, `workspace`, `commands`, `hotkeys`, `hotkey`

## Write operations needing clear user intent

Run only when the user's request clearly asks for the action:
- `create`, `append`, `prepend`
- `daily`, `daily:append`, `daily:prepend`
- `property:set`, `property:remove`
- `task toggle`, `task done`, `task todo`, `task status`
- `open`, `search:open`, `template:insert`
- `workspace:save`, `workspace:load`
- `bookmark`, `base:create`, `unique`, `web`, `tab:open`

## Destructive or scary operations needing explicit confirmation

Always ask first unless the user has already explicitly confirmed the exact operation:
- `delete`, especially `delete permanent`
- `move`, `rename`
- `history:restore`, `sync:restore`
- `publish:add changed`, `publish:remove`
- `plugin:install`, `plugin:uninstall`, `plugin:enable`, `plugin:disable`, `plugin:reload`
- `theme:install`, `theme:uninstall`, `theme:set`
- `snippet:enable`, `snippet:disable`
- `plugins:restrict on/off`
- `sync on`, `sync off`
- `reload`, `restart`
- `eval`, `dev:cdp`, `dev:debug`, `dev:mobile`

Never run arbitrary JavaScript through `eval` unless explicitly requested and the code is understood.

## Common workflows

- Find notes: run `obsidian search query='<text>' format=json`, inspect results, then read or use `search:context` for relevant files.
- Add a daily note task: run `obsidian daily:append content='- [ ] Task text'` after resolving any vault targeting ambiguity.
- Mark a task done: use `obsidian task ref='<path:line>' done` when a line reference is available.
- Create from a template: list or read templates if needed, then run `create path='<path>' template='<template>' open`.
- Clean unresolved links: run `unresolved verbose format=json`, present a plan and affected files, then ask for confirmation before edits.

## Troubleshooting

If a command fails:
1. Report the failure clearly.
2. Run the smallest relevant diagnostic command.
3. Explain the likely cause and next step.
4. Do not retry destructive or write commands blindly.

## Tool usage rules

- Use `agent_run_shell_command(command, cwd=None, timeout=60)` for Obsidian CLI commands, tests, and diagnostics.
- Use `ask_user_question(questions)` for explicit confirmation or structured input.
- Continue independently for safe read-only discovery.
- Stop and ask when vault targeting, destructive operations, broad changes, or ambiguous intent require user input.
"""
13 changes: 13 additions & 0 deletions code_puppy/plugins/obsidian_agent/register_callbacks.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
"""Register the Obsidian Agent plugin."""

from code_puppy.callbacks import register_callback

from .agent_obsidian import ObsidianAgent


def register_agents() -> list[dict[str, object]]:
"""Register the Obsidian Agent with Code Puppy's agent catalog."""
return [{"name": "obsidian-agent", "class": ObsidianAgent}]


register_callback("register_agents", register_agents)
55 changes: 55 additions & 0 deletions tests/plugins/test_obsidian_agent.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
"""Tests for the built-in Obsidian Agent plugin."""

from code_puppy.plugins.obsidian_agent.agent_obsidian import ObsidianAgent
from code_puppy.plugins.obsidian_agent.register_callbacks import register_agents


PRIVATE_DEFAULT_MARKERS = [
"/" + "users/",
"c:" + "\\users\\",
"library/" + "mobile" + " documents",
"daily reports",
"example-vault-that-should-not-be-hard-coded",
]


def test_obsidian_agent_metadata_and_tools() -> None:
agent = ObsidianAgent()

assert agent.name == "obsidian-agent"
assert agent.display_name == "Obsidian Agent 🪨"
assert "Obsidian" in agent.description
assert agent.get_available_tools() == [
"agent_run_shell_command",
"ask_user_question",
]


def test_obsidian_agent_prompt_includes_safety_guidance() -> None:
prompt = ObsidianAgent().get_system_prompt()

assert "official `obsidian` CLI" in prompt
assert "Discover before modifying" in prompt
assert "Ask for explicit confirmation" in prompt
assert "Never run arbitrary JavaScript" in prompt
assert "agent_run_shell_command" in prompt


def test_obsidian_agent_prompt_has_no_personal_defaults() -> None:
agent = ObsidianAgent()
searchable = "\n".join(
[
agent.description,
agent.get_user_prompt(),
agent.get_system_prompt(),
]
).lower()

for marker in PRIVATE_DEFAULT_MARKERS:
assert marker not in searchable


def test_obsidian_agent_registers_with_plugin_hook() -> None:
registrations = register_agents()

assert registrations == [{"name": "obsidian-agent", "class": ObsidianAgent}]
Loading