diff --git a/README.md b/README.md index e89731e..41deafe 100644 --- a/README.md +++ b/README.md @@ -1,328 +1,244 @@ # Visual Notes -> A living concept map of your day, generated from your Obsidian daily notes. +> Turn an Obsidian daily note into a living concept map. -You write in your daily note. Visual Notes reads it, extracts the day's -concepts and how they connect, and renders an interactive graph at the -top of the note — refreshed automatically as you write. +Visual Notes watches the markdown files you already use for daily notes, +asks Claude to extract the main concepts and relationships, and renders an +interactive Cytoscape.js graph directly inside Obsidian. -It captures **everything in the file** — AI session summaries, manually -typed notes, mobile edits, content from any source. The markdown is the -universal interface; whatever lands there ends up in the visual. +The markdown note stays the source of truth. Manual notes, AI session +summaries, mobile edits, Templater output, and synced changes all flow +through the same pipeline: if it lands in a watched note, it can appear in +the visual. -``` - ┌─────────────────────────────────┐ - │ 20260501-overview.html │ - │ │ - │ ┌─[hook fix]──┐ │ - │ │ ▼ │ - │ │ ┌─────────────┐ │ - │ │ │ matcher/if │ │ - │ │ │ split │ │ - │ │ └──────┬──────┘ │ - │ │ │ powers │ - │ │ ▼ │ - │ │ ┌─────────────┐ │ - │ │ │ PostToolUse │ │ - │ │ │ hook │ ◄──┐ │ - │ │ └─────────────┘ │ │ - │ │ │ │ - │ │ ┌──[Brian's CDP]──┐ │ │ - │ │ ▼ │ │ │ - │ │ ◇ context ─ ─ ┘ │ │ - │ │ │ │ - │ └──── (cross-domain)──────┘ │ - │ dashed │ - └─────────────────────────────────┘ - inline at top of daily note -``` - -*(Above: stylized representation of an actual rendered overview. Real output -uses Catppuccin colors; rectangles for systems, ellipses for tasks, diamonds -for decisions; thick edges for strong relationships, dashed for cross-domain.)* +## Why it is useful ---- +- **A visual daily recap:** see the day's work, decisions, blockers, and + cross-domain connections at a glance. +- **Works with any note source:** Claude Code, OpenCode, Copilot, manual + typing, and mobile edits all become ordinary markdown input. +- **No separate graph editor:** edit the note; Visual Notes regenerates the + graph sidecar. +- **Useful for memory and navigation:** nodes summarize important concepts; + labeled edges explain why they matter. -## What this is - -The repo houses **two plugins** that share a JSON sidecar schema: - -| Plugin | What it does | Required? | -|---|---|---| -| **Obsidian plugin** (TypeScript) | Watches markdown, calls Claude API, renders Cytoscape inline | Yes (primary artifact) | -| **Claude Code plugin** (markdown + bash) | Lets AI agents pre-populate the sidecar before LLM extraction runs | Optional | - -Either plugin works alone. They compose if you have both. - ---- - -## How it works (concept) +## What it does ```mermaid flowchart LR - subgraph Sources["Anything that edits the note"] - A1[Claude Code] - A2[claude.ai mobile/web] - A3[Manual typing] - A4[Obsidian Sync
from another device] + classDef input fill:#eef2ff,stroke:#4f46e5,stroke-width:2px,color:#111827 + classDef plugin fill:#ecfeff,stroke:#0891b2,stroke-width:2px,color:#0f172a + classDef ai fill:#fef3c7,stroke:#d97706,stroke-width:2px,color:#111827 + classDef data fill:#f0fdf4,stroke:#16a34a,stroke-width:2px,color:#052e16 + classDef render fill:#fae8ff,stroke:#c026d3,stroke-width:2px,color:#111827 + classDef future fill:#f8fafc,stroke:#94a3b8,stroke-width:1px,stroke-dasharray: 5,5,color:#334155 + + subgraph Sources["Everyday note inputs"] + Manual["Manual notes"] + Sessions["AI session summaries"] + Mobile["Mobile edits"] + Templates["Templates and automations"] end - Note[("Daily note
YYYYMMDD.md")] - Plugin[Obsidian plugin] - API((Claude
API)) - Side[("YYYYMMDD-overview.json
sidecar")] - Viz["Concept map
rendered inline"] - - Sources --> Note - Note -->|on save| Plugin - Plugin -->|markdown content| API - API -->|graph JSON| Plugin - Plugin --> Side - Side --> Viz - Viz -. embedded in .-> Note -``` -The thesis: **the markdown file is the universal interface**. Anything that -edits it triggers the plugin; the visual reflects whatever lands in the -file, regardless of who wrote it. + Note[("Watched daily note
YYYYMMDD.md")] ---- + subgraph Plugin["Visual Notes Obsidian plugin"] + Watch["Watch + debounce"] + Hash["Hash unchanged notes"] + Extract["Extract graph"] + Validate["Validate schema"] + Render["Render inline"] + end -## Install + Claude(("Anthropic Claude")) + Sidecar[("Graph sidecar
YYYYMMDD-overview.json")] + Pane["Interactive Cytoscape map
inside the note"] + Future["Future: section-aware
idempotent updates"] -### For Obsidian users (recommended path) + Manual --> Note + Sessions --> Note + Mobile --> Note + Templates --> Note + Note --> Watch --> Hash --> Extract --> Claude + Claude --> Validate --> Sidecar --> Render --> Pane + Pane -. displayed above note content .-> Note + Future -. planned evolution .-> Hash -``` -1. Install BRAT in Obsidian -2. In BRAT settings, add: bobthearsonist/visual-notes -3. Open Visual Notes settings, paste your Anthropic API key -4. Add at least one folder to "Watched folders" (your daily-notes folder; add multiple if you keep separate work/personal/project journals) -5. Edit a daily note → visual appears at the top + class Manual,Sessions,Mobile,Templates,Note input + class Watch,Hash,Extract,Validate,Render plugin + class Claude ai + class Sidecar data + class Pane render + class Future future ``` -Full instructions: [`plugins/obsidian-plugin/README.md`](plugins/obsidian-plugin/README.md). +## What it looks like in Obsidian -### For Claude Code users (optional companion) +Visual Notes renders as a card at the top of the note in both reading and edit +views. The graph is interactive; the note remains normal markdown underneath. -``` -/plugin install bobthearsonist/visual-notes/plugins/claude-code-plugin -``` +![Example Obsidian note with a Visual Notes concept map card](docs/assets/obsidian-note-preview.svg) -Full instructions: [`plugins/claude-code-plugin/README.md`](plugins/claude-code-plugin/README.md). +Feature overview: ---- +- Watches one or more configured daily-note folders. +- Debounces saves and skips unchanged content using a markdown hash. +- Sends the full note markdown to the Anthropic Messages API. +- Validates the returned graph against the shared sidecar schema. +- Writes `{date}-overview.json` next to the note. +- Renders the sidecar inline in reading and source views. +- Supports pin/unpin/delete/regenerate commands for manual control. +- Shows extraction count and status in the Obsidian status bar. +- Tracks token usage and estimated cost metadata in the sidecar when the API + response includes usage. -## Settings +## Project pieces -``` -┌─ Visual Notes ─────────────────────────────────────┐ -│ │ -│ Anthropic API key [••••••••••••••••] [Show] │ -│ │ -│ Watched folders [Captains Log ] [×] │ -│ [0 Daily ADHD Brain ] [×] │ -│ [+ Add folder] │ -│ ⚠ Required. Empty = inactive │ -│ │ -│ Debounce (ms) [1500 ] │ -│ │ -│ Model [Haiku 4.5 ▼] │ -│ │ -│ ▶ Advanced (custom prompt, debug logging…) │ -│ │ -└────────────────────────────────────────────────────┘ -``` +This repository contains two plugins and one shared schema: -Watch as many folders as you want — work daily notes, personal daily -notes, per-project journals — all share the same model and prompt. -The list is intentionally empty by default so the plugin stays inert -until you configure it. - -Commands available in the command palette: - -- **Visual Notes: Extract from current note** — manual extraction -- **Visual Notes: Regenerate (force)** — discard the cached hash + ignore `_pinned`, with a 30s per-file cooldown -- **Visual Notes: Pin this overview** — sets `_pinned: true`; the plugin will skip auto-extraction for this note -- **Visual Notes: Unpin this overview** — clears `_pinned`; auto-extraction resumes -- **Visual Notes: Delete sidecar** — removes the sidecar JSON; next save auto-re-extracts +| Piece | Purpose | Status | +|---|---|---| +| [`plugins/obsidian-plugin`](plugins/obsidian-plugin/README.md) | Primary Obsidian plugin. Watches notes, calls Claude, writes sidecars, and renders Cytoscape inline. | MVP implementation in progress | +| [`plugins/claude-code-plugin`](plugins/claude-code-plugin/README.md) | Optional companion for agent-curated sidecars after AI session summaries. | Scaffolded; migration pending | +| [`shared/schema.json`](shared/schema.json) | JSON Schema contract for sidecar graph files. | Defined | +| [`docs/design.md`](docs/design.md) | Living design document for open/future work. | Maintained as decisions evolve | ---- +The Obsidian plugin is the main product. The Claude Code plugin is optional: +it can pre-populate or pin curated sidecars, but Visual Notes does not depend +on Claude Code. -## Detailed architecture +## Architecture at a glance -The high-level flow above hides the lifecycle. Here's the full pipeline: +```text +Daily note folder +├── 20260501.md # source markdown +└── 20260501-overview.json # generated graph sidecar -```mermaid -sequenceDiagram - autonumber - participant User - participant Vault as Obsidian Vault - participant Plugin as Visual Notes plugin - participant API as Anthropic API - participant View as Markdown view - - User->>Vault: save daily note (Cmd-S, idle save, etc.) - Vault->>Plugin: vault.on('modify') - Plugin->>Plugin: debounce 1.5s (configurable) - Plugin->>Plugin: SHA-256 the markdown body - Plugin->>Vault: read existing sidecar (if any) - alt hash matches sidecar._lastProcessedHash - Plugin->>Plugin: skip — already extracted - else hash differs OR no sidecar - Plugin->>View: status-bar: "Visual Notes: extracting…" - Plugin->>API: messages.parse({system, user, output_config}) - API-->>Plugin: structured JSON {nodes, edges, ...} - Plugin->>Vault: write {date}-overview.json - Vault->>View: trigger MarkdownPostProcessor refresh - View->>View: load sidecar → mount/update Cytoscape - Plugin->>View: status-bar: clear - View-->>User: rendered concept map - end +Obsidian plugin +├── settings tab # API key, watched folders, debounce, model +├── file watcher # only watched markdown files +├── extractor # requestUrl -> Anthropic Messages API +├── schema validation # Zod + shared/schema.json +└── renderer # Cytoscape in MarkdownRenderChild ``` -### Component boundaries +Important invariants: -``` -┌────────────────────────────────────────────────────────────────┐ -│ visual-notes/ │ -│ │ -│ ┌─────────────────────────────┐ │ -│ │ shared/ │ │ -│ │ schema.json │ │ -│ │ (the contract: nodes, │ │ -│ │ edges, status, kind…) │ │ -│ └──────┬──────────────┬───────┘ │ -│ │ consumed by │ │ -│ ┌───────────┘ └─────────────┐ │ -│ ▼ ▼ │ -│ ┌─────────────────────┐ ┌──────────────────────┐ │ -│ │ Obsidian plugin │ │ Claude Code plugin │ │ -│ │ (TypeScript) │ │ (markdown + bash) │ │ -│ │ │ │ │ │ -│ │ PRIMARY │ │ OPTIONAL │ │ -│ │ │ │ │ │ -│ │ Distribution: │ │ Distribution: │ │ -│ │ community store / │ │ /plugin install … │ │ -│ │ BRAT │ │ │ │ -│ └─────────────────────┘ └──────────────────────┘ │ -│ │ -└────────────────────────────────────────────────────────────────┘ -``` +1. The `.md` note is read-only input for the plugin. +2. The sidecar JSON is the source of truth for rendered graph data. +3. `_pinned: true` on a sidecar suppresses automatic re-extraction unless the + user runs force regenerate. +4. The renderer tolerates unsupported future sidecar kinds by showing a + placeholder instead of crashing. -The two plugins do NOT depend on each other. Either alone provides full -functionality; both together compose with **last-writer-wins** sidecar -semantics, escape-hatched via `_pinned: true` (see [§5.2 of the design -doc](docs/design.md#52-coexistence-with-obsidian-plugin)). +## Install and setup -### Repository layout +### Obsidian plugin -``` -visual-notes/ -├── README.md # this file -├── LICENSE # MIT -├── docs/ -│ └── design.md # full design — start here for contributing -├── shared/ -│ └── schema.json # the JSON Schema contract -└── plugins/ - ├── claude-code-plugin/ - │ ├── README.md - │ ├── .claude-plugin/plugin.json - │ ├── hooks/ - │ └── skills/visual-notes/ - └── obsidian-plugin/ - ├── README.md - ├── manifest.json - ├── package.json - ├── prompts/ - │ └── extract-graph.md - └── src/ +Until a release is published, use the development workflow: + +```bash +pnpm install +pnpm --filter @visual-notes/obsidian-plugin build ``` ---- +Then copy or symlink `plugins/obsidian-plugin` into your vault's +`.obsidian/plugins/visual-notes` directory and enable **Visual Notes** in +Obsidian's Community plugins settings. -## Coexistence: when both plugins are installed +After enabling: -If you install both, here's what happens: +1. Open **Settings → Visual Notes**. +2. Paste an Anthropic API key. +3. Add at least one watched folder, such as `Daily Notes` or `Captains Log`. +4. Choose a debounce and model, or keep the defaults. +5. Save or manually extract a note with + **Visual Notes: Extract from current note**. -``` -agent appends session summary to daily note - │ - │ Bash hook fires - ▼ - agent writes curated sidecar ── _pinned: true (sticky) ──┐ - │ │ - │ 1.5s debounce │ - ▼ │ - Obsidian plugin's file watcher │ - │ │ - ├─ check sidecar: is _pinned: true? ───────────────┤ - │ │ - │ yes (from agent) no │ - │ │ │ │ - │ ▼ ▼ │ - │ skip extraction extract via Claude API ──> overwrite sidecar -``` +See the plugin README for detailed Obsidian-specific instructions: +[`plugins/obsidian-plugin/README.md`](plugins/obsidian-plugin/README.md). -The agent path is **deliberate, curated** content. The plugin path is -**LLM extraction** of whatever's in the file. `_pinned` lets the agent -say "I'm authoritative; don't overwrite me." Without it, the plugin's -extraction always wins eventually (last-writer-wins). +### Claude Code companion plugin ---- +The Claude Code companion is scaffolded but not yet migrated from the +original private workflow. Its intended install path is: -## Status +```text +/plugin install bobthearsonist/visual-notes/plugins/claude-code-plugin +``` -| Component | Status | -|---|---| -| Design doc | ✅ Complete (see `docs/design.md`) | -| Repo scaffold | ✅ Complete | -| Sidecar schema | ✅ Defined (`shared/schema.json`) | -| Obsidian plugin | 🚧 Phase 1 — scaffold only | -| Claude Code plugin | 🚧 Migration pending from private dotfiles repo | -| Distribution | ⏳ Awaiting first release | +See [`plugins/claude-code-plugin/README.md`](plugins/claude-code-plugin/README.md). ---- +## Commands -## Cost expectations +The Obsidian command palette exposes: -The Obsidian plugin sends the full markdown content of a daily note to -Anthropic's API on every (debounced, deduped) save. At default settings -(Claude Haiku 4.5, 1.5s debounce): +- **Visual Notes: Extract from current note** — manually extract the active + markdown file unless its sidecar is pinned. +- **Visual Notes: Regenerate (force)** — bypasses cached hash and pin state, + with a per-file cooldown. +- **Visual Notes: Pin this overview** — preserves the current sidecar from + automatic replacement. +- **Visual Notes: Unpin this overview** — resumes automatic extraction. +- **Visual Notes: Delete sidecar** — removes the generated graph sidecar. -- **~$0.006 per extraction** -- Typical day: 5–15 extractions -- Monthly cost: **~$2–5** +## Privacy and cost -Bring your own API key. The plugin does not proxy or aggregate usage. +Visual Notes sends the **full markdown content** of watched notes to the +Anthropic API when extraction runs. Do not add folders containing notes you do +not want sent to a third-party API. -A status-bar indicator shows today's extraction count so you can spot -runaway behavior without a full dashboard. +Bring your own API key; this project does not proxy requests or aggregate +usage. At the default Haiku model, a typical extraction is designed to cost +only a few fractions of a cent, but actual spend depends on note length, +model, save frequency, and retries. The plugin shows today's extraction count +and stores usage metadata when available. ---- +## Development -## Privacy +Requirements: -The plugin sends the **full content** of your daily note to the Claude -API for extraction. By default Anthropic does not retain content beyond -the request lifetime, but read [their privacy -policy](https://www.anthropic.com/legal/privacy). Don't put sensitive -content in any watched folder. +- Node.js 20+ +- pnpm ---- +Common commands: -## Contributing +```bash +pnpm install +pnpm build +pnpm typecheck +pnpm lint +``` -This repo is in design phase. Read [`docs/design.md`](docs/design.md) -before opening issues or PRs. +Repository layout: -- **Architecture decisions:** see `docs/design.md` §10 (Open Questions) -- **Patterns to follow:** see `docs/design.md` §4–7 -- **Implementation phases:** see `docs/design.md` §9 +```text +visual-notes/ +├── README.md +├── docs/ +│ └── design.md +├── shared/ +│ ├── package.json +│ └── schema.json +└── plugins/ + ├── obsidian-plugin/ + │ ├── README.md + │ ├── manifest.json + │ ├── prompts/extract-graph.md + │ └── src/ + └── claude-code-plugin/ + ├── README.md + ├── .claude-plugin/plugin.json + ├── hooks/ + └── skills/visual-notes/ +``` -Major design changes go via PR to `docs/design.md`. Decisions get an -ADR-style record under `docs/decisions/`. +## Deeper docs and planning ---- +- [Obsidian plugin README](plugins/obsidian-plugin/README.md) +- [Claude Code plugin README](plugins/claude-code-plugin/README.md) +- [Living design document](docs/design.md) +- [Project issues](https://github.com/bobthearsonist/visual-notes/issues) ## License diff --git a/docs/assets/obsidian-note-preview.svg b/docs/assets/obsidian-note-preview.svg new file mode 100644 index 0000000..1cd89f5 --- /dev/null +++ b/docs/assets/obsidian-note-preview.svg @@ -0,0 +1,121 @@ + + Visual Notes rendered inside an Obsidian daily note + An Obsidian note preview showing a Visual Notes concept map card with nodes, labeled edges, legend, and usage footer above ordinary markdown content. + + + + + + + + + + + + + + + + + + + + + Test Vault + Daily Notes + + 20260502.md + 20260502-overview.json + + + 20260502 + + Reading view + + + Daily Notes + + tags: daily, work, visual-notes • created: 2026-05-02 + + + + + Daily Overview + Visual summary generated from today's note + + + STATUS + + done + + active + + context + + blocked + TYPE + + system + + task + + decision + + + + + + drives + + updates + + reveals + + blocks + + uses + + summarizes + + + Daily note + source + + + Visual + extraction + + + Sidecar + JSON + + + Layout + decision + + + Marketplace + prep + + + Section-aware + updates + + + + + Last: 14,071 tokens (11,521 in / 2,550 out) ($0.0243) · Cumulative: 14,071 tokens ($0.0243) + + + + Tasks + + Review layout issue + + Prepare marketplace checklist + Notes + + + + + diff --git a/docs/design.md b/docs/design.md index 5d81d8c..e69e156 100644 --- a/docs/design.md +++ b/docs/design.md @@ -1,1185 +1,239 @@ -# Visual Notes — Design Document +# Visual Notes — Living Design Document -**Version:** 0.1 (design phase) -**Date:** 2026-05-02 -**Status:** Approved for implementation +**Status:** living design notes for remaining and future work +**Last updated:** 2026-05-02 -> A system that watches daily-note markdown in Obsidian, extracts a concept-map -> graph via an LLM, and renders an interactive visual at the top of the note. -> Replaces an earlier Claude-Code-hook-driven approach that could only see -> content the agent passed through tool calls — leaving manual edits invisible -> to the visual. +This document is intentionally future-facing. Product overview, current setup, +and the stable architecture summary live in the top-level +[`README.md`](../README.md). This file records design work that is still open, +planned, or likely to change. ---- +## Design principles to preserve -## 1. Vision & Goals +These are the constraints future changes should not casually break: -### Why this exists +1. **Markdown remains the universal input.** The Obsidian plugin reacts to + watched `.md` files and does not require a specific AI client. +2. **The plugin does not modify note markdown.** It reads notes and writes a + sibling sidecar (`{date}-overview.json`) only. +3. **The sidecar is the render contract.** The renderer consumes + `shared/schema.json`; optional producers may write the same schema. +4. **Every edge label carries meaning.** A graph with unlabeled or generic + edges is less useful than a smaller, accurate graph. +5. **Pinning is authoritative.** `_pinned: true` tells the Obsidian plugin not + to overwrite a curated sidecar unless the user explicitly force-regenerates. -Today's daily-note workflow blends: +## Current implementation snapshot -- AI-written session summaries appended via `obsidian append` (from Claude Code, OpenCode, and other AI clients) -- Manually-typed notes (meeting summaries, asides, status, ideas) -- Auto-generated content (Dataview queries, Templater output) +The Obsidian plugin has an MVP implementation: -A previous iteration generated an iframe-embedded Cytoscape concept map by -having the AI agent write a JSON sidecar after each session. That worked but -**structurally cannot capture content the agent didn't write itself**. Manual -edits, content from other AI clients, and offline-typed mobile notes are all -invisible to the visual. +- settings tab for API key, watched folders, debounce, and model +- watched-folder save handling with debounce +- content hash dedup via `_lastProcessedHash` +- Anthropic Messages API extraction through Obsidian `requestUrl` +- tool-based structured graph output validated with Zod +- sidecar writes stamped with producer/schema/hash/pin/usage metadata +- inline Cytoscape rendering in Obsidian +- status bar extraction count +- command palette controls for extract, force-regenerate, pin, unpin, and + delete sidecar -The plugin path makes the **markdown file the universal interface**: whoever -modifies it (Claude Code, claude.ai web/mobile, OpenCode, Cline, manual -typing on any device) → the plugin reacts and updates the visual. +The Claude Code plugin remains scaffolded. Its full hook/skill migration is +future work. -### Goals +## Remaining design work -1. **Categorical completeness.** The visual reflects the entire day's content, - not curated subsets. -2. **Cross-platform.** Runs wherever Obsidian runs (desktop + mobile). -3. **Cross-client decoupled.** Independent of which AI agent (if any) wrote - the source content. -4. **Self-contained installable artifact.** A user with no Claude Code - subscription can install just the Obsidian plugin and benefit. -5. **Deterministic rendering.** Same markdown → same visual structure (modulo - layout). Visual quality is the LLM's responsibility but consistency comes - from a fixed schema and prompt. +### Section-aware updates -### Before / after comparison +Current extraction reads the whole note and rewrites the whole sidecar. That is +simple and correct, but it can cost more than necessary and makes every save a +full-graph regeneration. -For users currently on the Claude-Code-hook-driven workflow: +Future design target: -| Concern | Before (hook-driven) | After (plugin-driven) | -|---|---|---| -| Trigger | Agent runs `obsidian append` | Anything that writes to the .md file | -| Sidecar author | Agent designs graph + writes JSON | LLM extraction reads the .md, produces JSON | -| Manual edits | Invisible to the visual | Captured on next save | -| Mobile edits | Invisible | Captured after sync | -| Multi-client (Copilot, OpenCode, etc.) | Each needs its own integration | Each writes markdown; plugin handles the rest | -| Cost basis | Agent token spend on graph design | Anthropic API spend per save (~$0.006) | -| Dependency on Claude Code | Required | Optional (works with no AI client at all) | - -### Non-goals - -- A general-purpose graph editor. -- Cross-note relationship visualization (that's ExcaliBrain's territory; we're - scoped to per-note concept extraction). -- Real-time collaborative editing. -- A fancy settings dashboard (cost UI, model picker, prompt-template editor). - Minimum viable settings: API key, watched folders (list), debounce ms. - ---- - -## 2. System Architecture - -### Three-component system - -``` -┌─────────────────────────────────────────────────────────────────┐ -│ Daily note folder (in vault) │ -│ │ -│ ┌─────────────────────┐ ┌─────────────────────┐ │ -│ │ 20260501.md │ │ 20260501- │ │ -│ │ (markdown, │ │ overview.json │ │ -│ │ the source of │ │ (sidecar, written │ │ -│ │ truth for input) │ │ by either plugin) │ │ -│ │ │ └─────────────────────┘ │ -│ │ Read by Obsidian │ ▲ │ -│ │ plugin's file │ │ read at render │ -│ │ watcher │ │ time + on change │ -│ └──────────┬──────────┘ │ │ -│ │ │ │ -│ │ vault.on('modify') │ │ -│ ▼ │ │ -│ ┌──────────────────────────────┐ │ │ -│ │ Obsidian plugin │──────┘ │ -│ │ - reads markdown │ writes sidecar │ -│ │ - calls Claude API │ │ -│ │ - mounts Cytoscape inline │ │ -│ │ via MarkdownPostProcessor │ │ -│ └──────────────────────────────┘ │ -│ │ -│ Note: the plugin never modifies the daily note (.md) itself. │ -│ Reading-only on .md, read+write on the .json sidecar. │ -└─────────────────────────────────────────────────────────────────┘ - ▲ ▲ - │ │ - ┌────────────┴───────────┐ ┌───────────────┴──────────────┐ - │ Obsidian plugin │ │ Claude Code plugin │ - │ (primary) │ │ (optional companion) │ - │ │ │ │ - │ - Watches markdown │ │ - Hook on `obsidian append` │ - │ - Calls Anthropic API │ │ - May write sidecar with │ - │ - Renders inline │ │ `_pinned: true` to claim │ - │ Cytoscape via │ │ authoritative ownership │ - │ MarkdownPostProc. │ │ - Skill: design heuristics │ - │ - Honors `_pinned` │ │ for agent pre-population │ - │ on the sidecar │ │ │ - └────────────────────────┘ └───────────────────────────────┘ -``` - -The two plugins **share a JSON schema** (defined in `shared/schema.json`) for -the sidecar file. They do NOT need to be installed together — either alone -suffices for the user's workflow. - -**Invariants** (any future contributor must preserve): - -1. The Obsidian plugin **only reads** the `.md` file. It writes the `.json` - sidecar. This avoids feedback loops with any future hook that watches - `.md` files. -2. The sidecar is the **single source of truth** for visual content. Anyone - wanting to influence the visual writes the sidecar. -3. `_pinned: true` on a sidecar **suppresses LLM extraction**. Treat as a - contract, not a hint. - -### Data flow: Obsidian plugin path (primary) - -``` -User saves daily note - ↓ -vault.on('modify') fires - ↓ -Debounce 1.5s (Obsidian's built-in debounce()) - ↓ -Hash check: SHA-256 of markdown body vs. sidecar's _lastProcessedHash - ↓ -If unchanged → skip (handles Obsidian Sync's per-device modify storm) -If changed ↓ - ↓ -Anthropic Messages API call (Haiku 4.5 default) - - System: extraction prompt + schema + few-shot examples - - User: full markdown content - - output_config: structured JSON via zodOutputFormat - ↓ -Parse response into typed object (zod-validated) - ↓ -Write {date}-overview.json sidecar (with new _lastProcessedHash) - ↓ -Trigger MarkdownPostProcessor refresh of the daily note's view - ↓ -Cytoscape mounts in MarkdownRenderChild container, reading the sidecar -``` - -### Data flow: Claude Code plugin path (legacy / complementary) - -The existing Claude-Code-hook-driven system continues to work. When the agent -runs `obsidian append` to a Captain's Log file, a PostToolUse hook injects a -prompt asking the agent to update the visual. The agent reads the daily note -and writes the sidecar JSON directly. Last-writer-wins on the sidecar; either -system can populate it. - -This is intentionally kept as a coexistence pattern, not a replacement: users -who don't have Claude Code installed still get full functionality from the -Obsidian plugin alone. - ---- - -## 3. Shared Sidecar Schema - -The contract between the two plugins. Lives at `shared/schema.json` as a -JSON Schema document; both plugins import it (the Obsidian plugin generates -TypeScript types via `json-schema-to-typescript`; the Claude Code plugin -references it in skill documentation). - -### Format - -```json -{ - "title": "Daily Overview - 2026-05-01", - "header": "Daily Overview", - "subtitle": "2026-05-01 — Hook fix · sidecar architecture · if-syntax decoded", - "_lastProcessedHash": "sha256:abc123...", - "_extractedBy": "obsidian-plugin@0.1.0", - "nodes": [ - { - "data": { "id": "kebab-id", "label": "Display\nLabel" }, - "classes": "system completed", - "position": { "x": 250, "y": 200 } - } - ], - "edges": [ - { - "data": { "source": "id-a", "target": "id-b", "label": "verb phrase" }, - "classes": "strong-edge" - } - ] -} -``` - -### Field semantics - -- `title` / `header` / `subtitle`: optional. Auto-derived from filename + date - when omitted. -- `_lastProcessedHash`: SHA-256 of the markdown content the sidecar was - generated from. Used to skip redundant API calls. -- `_extractedBy`: identifier for the producer (so debugging can attribute - whether the Obsidian plugin or the Claude Code skill wrote a given sidecar). -- `nodes[].classes`: one type class + one status class. - - **Type:** `system` (rectangle), `task` (ellipse), `decision` (diamond) - - **Status:** `completed` (green), `active` (yellow), `context` (blue), `blocked` (red) -- `edges[].classes`: optional `strong-edge` (thick line) or `weak-edge` - (dashed). Default is regular weight. -- `position`: pixel coordinates. **Currently provided by the LLM**; future - versions may switch to Cytoscape's native layout (decision deferred — see - §10 Open Questions). - -### Versioning - -Schema is at `v0.1.0` (matches initial repo version). Breaking changes bump -minor. Both plugins must agree on the schema version they support; the -sidecar gets an optional `_schemaVersion` field once we hit v1.0. - ---- - -## 4. Obsidian Plugin Design - -### 4.1 Components - -``` -plugins/obsidian-plugin/ -├── manifest.json # Plugin manifest (id, version, minAppVersion) -├── package.json # npm dependencies -├── esbuild.config.mjs # Build config (from obsidian-sample-plugin) -├── tsconfig.json -├── styles.css # Plugin styles + Cytoscape container CSS -├── prompts/ -│ └── extract-graph.md # The structured-output prompt template -└── src/ - ├── main.ts # Plugin entry, lifecycle, event registration - ├── extractor.ts # Anthropic API client, structured output - ├── renderer.ts # MarkdownPostProcessor, Cytoscape mount - ├── settings.ts # PluginSettingTab + persisted SettingsSchema - ├── theme.ts # Map Obsidian CSS vars → Cytoscape style - ├── storage.ts # API key storage (SecretStorage on desktop, data.json on mobile) - ├── debounce.ts # Wraps Obsidian's debounce() with content-hash dedup - └── schema.ts # Generated TypeScript types from shared/schema.json -``` - -### 4.2 Lifecycle - -**Plugin load (`onload()`):** -1. Load settings from `data.json` -2. Initialize secure storage helper (probe `app.vault.getAdapter().getSecretStorage()`) -3. Register `PluginSettingTab` -4. Register `vault.on('modify')` event with debounced + hash-checked handler -5. Register `MarkdownPostProcessor` for files matching any watched-folder pattern -6. Register `app.workspace.on('css-change')` for theme refresh - -**File save:** -1. `vault.on('modify')` fires -2. Path filter: only files inside any of the watched folders (recursive), - only `.md` files (not the sidecar `.json` itself, which would create - a feedback loop) -3. Pass to debounced handler (1.5s wait, configurable) -4. Hash check the markdown body; skip if unchanged from sidecar's - `_lastProcessedHash` -5. Call `extractor.extract(markdownContent)` -6. Write sidecar JSON via `vault.modify()` (atomic write, triggers another - modify event but the hash check prevents loop) -7. Notify any open MarkdownPostProcessor instances of the file to re-render - -**Markdown view render (`MarkdownPostProcessor`):** -1. For each rendered daily note, check for sibling `{date}-overview.json` -2. If sibling exists, mount a `MarkdownRenderChild` at the top of the rendered - markdown -3. The child loads the sidecar, applies theme variables, mounts Cytoscape -4. Cache the Cytoscape instance by file path (don't re-mount on every view) - -### 4.3 LLM Integration - -**Transport: hand-rolled `requestUrl` against the REST endpoint.** Do NOT -wrap the official `@anthropic-ai/sdk`. The SDK uses `fetch` internally; on -mobile (Capacitor WebView) `fetch` is restricted by CORS. `requestUrl` -from Obsidian is the only path that works cross-platform. Hand-rolling -~30 lines of REST + Zod validation is simpler than monkey-patching the -SDK's transport. - -**Validation: `zod ^3.25.0`.** Define a `GraphSchema` (Zod) mirroring -`shared/schema.json`; parse the response body through it. If validation -fails, treat as a soft error (log + retry with a "your previous response -was malformed JSON, please retry following the schema" follow-up message). - -**API request shape (concrete):** - -```typescript -const body = { - model: 'claude-haiku-4-5', // user-configurable - max_tokens: 2048, - system: systemPrompt, // bundled extraction prompt - messages: [{ role: 'user', content: markdownContent }], - output_config: { format: { type: 'json_schema', schema: graphJsonSchema } } -}; - -const response = await requestUrl({ - url: 'https://api.anthropic.com/v1/messages', - method: 'POST', - headers: { - 'x-api-key': apiKey, - 'anthropic-version': '2023-06-01', - 'content-type': 'application/json', - }, - body: JSON.stringify(body), - throw: false, // we handle non-2xx ourselves -}); -``` - -The response's `content[0].text` is the JSON string; parse + validate -against `GraphSchema`. - -**Type-safety pipeline.** Without the SDK, type safety lands client-side: - -``` -requestUrl(...) // returns RequestUrlResponse - ↓ .text // string - ↓ JSON.parse(...) // any (UNSAFE) - ↓ GraphSchema.parse(...) // typed Graph (Zod) ← belt - ↓ rendered // Cytoscape consumes -``` - -The Anthropic API's `output_config.format.schema` is the server-side -schema enforcer (suspenders). The Zod parse is the client-side -enforcer (belt). Both layers are kept because: -- Server-side enforcement reduces malformed responses (cheaper retry) -- Client-side validation produces typed objects (no `any` leaks) -- If they ever disagree, our code crashes loudly rather than silently - consuming bad data. - -**Default model:** Claude Haiku 4.5. Concept extraction is a -pattern-matching task, not deep reasoning. ~$0.006/call, ~$2-5/month at -10 extractions/day. Sonnet 4.6 available as a settings opt-in (3× cost). -Opus is overkill — not exposed as a default option. - -**Token budget pre-flight:** -- Estimate input tokens client-side (rough char/4 heuristic) before - sending; reject notes over 100k tokens with a Notice. -- Typical daily note: 1,250–3,750 input tokens. -- System prompt: ~2,000 tokens. -- Output: 500–1,000 tokens. - -**Error handling (bounded retry):** - -| HTTP status | Action | -|---|---| -| 200 | Parse + validate against Zod schema. On Zod fail, retry once with schema-correction prompt. | -| 400 (bad request) | Log + Notice. Don't retry. | -| 401 (auth) | Notice with "open settings" affordance. Don't retry. | -| 429 (rate limit) | Honor `retry-after` header, exponential backoff, **max 3 retries**, then queue for next manual trigger (status-bar widget shows "queued"). | -| 5xx | Exponential backoff, **max 3 retries**, then queue. | -| Network failure | Treat as 5xx. | - -All retries respect a **single `AbortController`** held on the Plugin -instance for the plugin's lifetime. Created in `onload()`; `abort()` -called in `onunload()`. Every `requestUrl` call passes the -controller's `.signal`, so plugin-disable cancels both in-flight -requests and any backoff-waiting queue entries. One controller, not -one-per-call — keeps lifecycle simple. - -**Sidecar reload event-emitter** lives on the Plugin instance as -`this.sidecarEvents = new Events()` (Obsidian's built-in `Events` -class). Each `MarkdownRenderChild` constructor receives a reference to -the plugin and subscribes to `sidecarEvents.on('changed', filePath, …)` -in `onload()`, unsubscribes in `onunload()`. The extractor fires the -event after a successful write. No module-level singletons; teardown -is clean when the plugin is disabled. - -**Logging strategy:** use `console.debug` for routine operations, -`console.warn` for recoverable problems (sidecar kind unknown, -malformed sidecar repaired), `console.error` for terminal errors that -also fire a Notice. No telemetry / analytics in v0.1. The Plugin class -exposes a `log(level, msg, data?)` helper that namespaces every -message with `[visual-notes]` so users filtering DevTools can find -plugin output quickly. - -**No streaming.** Fire-and-forget extraction; the JSON response is small -enough (~750 output tokens) to render instantly when complete. - -### 4.4 Rendering: Cytoscape inline - -**Pattern:** `registerMarkdownPostProcessor()` → `MarkdownRenderChild` → -mount Cytoscape into a `
` injected at the top of the rendered note. - -**Theme integration:** Read Obsidian CSS variables at mount time: -```typescript -const cs = getComputedStyle(document.documentElement); -const theme = { - bg: cs.getPropertyValue('--background-primary').trim(), - text: cs.getPropertyValue('--text-normal').trim(), - accent: cs.getPropertyValue('--accent-color').trim(), - // ... map to Cytoscape's style format -}; -``` - -Subscribe to `app.workspace.on('css-change')` → rebuild Cytoscape style on -theme toggle (no re-mount needed, just `cy.style().fromJson(...).update()`). - -**Caching strategy.** Don't use a static `Map` — -Obsidian creates multiple `MarkdownRenderChild` instances for the same -file across split panes, hover previews, embedded references, and tab -switches. A path-keyed singleton causes one view's `onunload()` to -dispose a Cytoscape instance another view is using. - -Instead: **the Cytoscape instance lives on the `MarkdownRenderChild`**. -Each child gets its own instance, mounted on `onload()` and disposed in -`onunload()`. Multiple views of the same file = multiple Cytoscape -instances; this is fine because graph data is small and instance -construction is fast (~tens of ms). - -If profiling later shows this is too expensive, fall back to a -`WeakMap` keyed by the container element — but only if measurement -demands it. - -**Sidecar reload:** When the sidecar JSON is rewritten by extraction, -notify all live `MarkdownRenderChild` instances watching that file via -a shared event-emitter. Each instance loads the new graph data into its -own Cytoscape via `cy.json({ elements: { ... } })`. No remount, no -flicker — just a data update. - -**Why not iframe?** The legacy approach used a `file://` iframe with a -self-contained HTML file. Inside a plugin, we own the renderer — direct -Cytoscape mount is simpler, faster, theme-integrated, no sandbox -concerns. The iframe path is dropped in v0.1. - -### 4.5 Settings UI - -Minimum viable. `PluginSettingTab` with these fields: - -| Setting | Type | Default | Storage | -|---|---|---|---| -| Anthropic API key | text (password style) | empty | SecretStorage on desktop, data.json on mobile (with warning) | -| Watched folders | list of text inputs (add/remove buttons) | empty list (forces explicit config) | data.json | -| Debounce (ms) | number | 1500 | data.json | -| Model | dropdown (Haiku 4.5 / Sonnet 4.6) | Haiku 4.5 | data.json | - -**Watched folders is a list, not a single value.** Many users have -multiple daily-note folders (work + personal, or per-project). The -plugin watches all of them; each folder produces its own per-day -sidecars in-place. There is no per-folder configuration — same prompt, -same model, same schema across folders. If a user needs different -behavior per folder (e.g., different model for personal vs work), -that's a future feature; v0.1 keeps the dial uniform. - -The "Watched folders" default is **deliberately empty**. The plugin -stays inert until the user adds at least one folder. On plugin load, if -the list is empty AND the API key is set, surface a one-time Notice: -"Visual Notes: add a watched folder in Settings to enable extraction." - -Watcher logic: `vault.on('modify')` fires for any file change. The -plugin checks whether the file's parent (or any ancestor) is in the -watched-folders list. If yes, queue for extraction. If no, ignore. -Subfolders inherit watch by default (e.g., adding `Captains Log` -also watches `Captains Log/2026/`). - -**Command palette entries** (always available): - -| Command | Behavior | -|---|---| -| `Visual Notes: Extract from current note` | Manual extraction. Bypasses debounce, runs immediately. Useful for "the visual is stale, force it." Honors `_pinned: true` and silently no-ops on pinned sidecars (with a Notice "sidecar is pinned — unpin first"). | -| `Visual Notes: Regenerate (force)` | Discards the cached `_lastProcessedHash` AND ignores `_pinned: true`. Useful when the LLM produced a bad graph and you want a fresh attempt regardless of pin state. **Rate-limit guard: 30-second cooldown per file.** Repeated invocations within the cooldown silent-no-op with a Notice "regenerate cooldown — wait Ns"; protects against rage-click rate-limit blowouts. | -| `Visual Notes: Pin this overview` | Sets `_pinned: true` on the current note's sidecar. Suppresses future LLM extractions until unpinned. Use this when the current visual is exactly what you want kept (e.g., agent-curated graph you don't want overwritten). | -| `Visual Notes: Unpin this overview` | Sets `_pinned: false`. Resumes auto-extraction on next save. | -| `Visual Notes: Delete sidecar` | Removes the sidecar JSON (and the rendered visual). Escape hatch. Fires Notice "sidecar deleted — next save will re-extract" so the user knows the recovery path. | - -**Status bar:** A small indicator showing today's extraction count and a -spinner during in-flight calls ("Visual Notes: extracting…"). Replaces a -full cost-tracking dashboard for v0.1; gives users enough visibility to -catch runaway behavior without complexity. - -- **"Today" boundary**: local midnight in the OS timezone (vault has no - timezone concept; OS is the closest stable proxy). Persisted to - `data.json` as `{date: "YYYY-MM-DD", count: N}`. Resets on first - extraction after midnight. -- **Color/state**: gray when configured + idle; yellow with spinner - during in-flight; red when configuration is incomplete (no API key - OR empty watched-folders list). Red state is the "high-discoverability cue" - for first-run users who haven't finished setup. -- **First-run Notice**: after the first successful extraction in a - fresh install, fire a one-time Notice (gated by `firstRunComplete: false` - in `data.json`): _"Visual Notes: first extraction succeeded. Cost ~$0.006 - per save at default model. See settings to change."_ Sets cost - expectations at the moment they matter. -- **401 affordance**: on 401, fire a `Notice` with text _"Visual Notes: - API key invalid. Open Settings → Visual Notes."_ at 8s duration on - desktop, 15s on mobile. Obsidian Notices don't natively support - clickable links; the text-instruction pattern is the standard - Obsidian-plugin idiom. - -**Cut from MVP:** -- Custom prompt override (textarea). Premature on day 1; users haven't - yet hit cases where the bundled prompt fails them. -- Cost dashboard. Status-bar count is sufficient. -- Per-folder configs. - -**Settings migration.** `data.json` carries an internal field -`_settingsVersion` (semver string, separate from the plugin's manifest -version). Plugin `onload()` reads `_settingsVersion`; if the value is -older than the current code expects, runs an idempotent migration -function before settings are bound to the UI. The migration list is a -chain of `(from → to)` transformations checked in declared order; each -should be safe to re-run. v0.1 ships with `_settingsVersion: "0.1.0"` -and an empty migration list — the convention is established before it -becomes painful to add. - -### 4.6 Sync collision handling - -Multi-device problem: every device running Obsidian Sync sees its own -`vault.on('modify')` for the same change. Without dedup, N devices each -hit the API for the same content. - -Solution: **content-hash dedup**. - -```typescript -// Schema requires the "sha256:" prefix; prepend it once at compute time. -const hash = 'sha256:' + sha256(markdownContent); // hex-digest, 64 chars -const sidecar = await readSidecarIfExists(notePath); -if (sidecar?._lastProcessedHash === hash) { - return; // Already extracted this exact content -} -const result = await extract(markdownContent); -result._lastProcessedHash = hash; -await writeSidecar(notePath, result); -``` - -This solves the most common sync race. Two narrower windows remain: - -**Mid-flight race:** Device A starts extraction at T=0; device B -receives the same markdown via Sync at T=0.5; B's hash check sees no -sidecar yet (A hasn't written) → B starts a duplicate extraction. Both -write sidecars; last-writer-wins, but the user paid the API call twice. - -Mitigation (deferred to Phase 6, documented as known limitation here): -write a `.lock` placeholder sidecar before the API call (atomic rename -on completion). Or use a content-addressed-temp-file + rename pattern. - -**Concurrent edits across devices:** Device A and B both edit a daily -note simultaneously while offline; Obsidian Sync resolves the markdown -conflict; both devices then trigger extraction on the merged result. In -practice this manifests as one extra API call (the second device sees -its own merged hash mismatch the first device's sidecar). Acceptable -for v0.1. - -**Document as known limitation in the README + plugin settings help -text.** No data corruption, just occasional duplicate API spend. - ---- - -## 5. Claude Code Plugin Design - -### 5.1 Components - -``` -plugins/claude-code-plugin/ -├── .claude-plugin/ -│ └── plugin.json # Manifest (name, version, description) -├── hooks/ -│ ├── hooks.json # Wrapper format (PostToolUse → Bash matcher) -│ ├── post-obsidian-append.md # Frontmatter + prompt body -│ └── run-hook.sh # Generic hook runner; honors match_content frontmatter -└── skills/ - └── visual-notes/ - ├── SKILL.md # Stripped-down: points users at the Obsidian plugin - └── references/ # (optional, may stay empty post-migration) -``` - -### 5.2 Coexistence with Obsidian plugin - -Both plugins write the same sidecar schema. The Claude Code path stays -useful for: - -- Users who write detailed session summaries via AI clients and want the - agent to control visualization narrative (e.g., highlight specific nodes) -- Workflows where the agent needs to inject curated graph state that the - LLM extraction wouldn't produce automatically - -The two systems compose: -1. Agent appends session summary to daily note -2. Obsidian plugin's file-watcher kicks in (debounced 1.5s) -3. Plugin extracts a fresh graph from the FULL note content -4. Sidecar gets overwritten with the LLM's view +- Track hashes per markdown section, not only per full note. +- Re-extract only changed sections when the graph can be patched safely. +- Preserve stable node IDs across partial updates so existing layout and pins + remain useful. +- Fall back to full-note extraction when: + - headings are heavily reorganized + - too many sections changed + - a changed section participates in many cross-section edges + - the sidecar schema version is older than the section-aware format -OR (alternative ordering): -1. Agent appends session summary -2. Claude Code hook fires, agent writes sidecar with curated graph -3. Obsidian's debounced extraction triggers next, overwrites with LLM view +Open questions: -**Last writer wins WITH a `_pinned` escape hatch (v0.1, not deferred).** -Both producers target the same schema. The Obsidian plugin honors -`_pinned: true` on read: if the existing sidecar has `_pinned: true`, -the plugin skips its own extraction and respects the existing data. +- Should the sidecar store a `sections` map with heading slug, source range, + hash, and associated node IDs? +- Should section-level extraction return graph patches, or should the plugin + ask Claude to merge old graph + changed markdown into a new full graph? +- How should manual edits to headings affect historical node IDs? -This protects deliberate, curated agent-authored graphs from being -silently overwritten by probabilistic LLM extraction. Without `_pinned`, -the design ships data loss as a feature — unacceptable. +### Layout strategy -Implementation cost: ~5 lines in the Obsidian plugin's -`shouldSkipExtraction()` check. Deferring it to a "future feature" was -called out by the architecture review as ship-blocking; landing it on -day 1. +v0.1 uses LLM-provided `position: {x, y}` coordinates and Cytoscape's +`preset` layout. This keeps the prompt in control of visual grouping, but it +can produce overlaps or off-canvas nodes. -**Default behavior:** the Claude Code plugin's hook does NOT -automatically set `_pinned`. The agent has to choose to pin a sidecar -explicitly when it wants its content to be authoritative. This avoids -surprising the user when they install both plugins and lose the -LLM-extraction behavior they signed up for. +Future options: -### 5.3 Migration path +1. **Keep LLM positions.** + - Pro: preserves explicit semantic clustering. + - Con: prompt quality directly affects readability. +2. **Switch to Cytoscape layout such as `cose-bilkent`.** + - Pro: less prompt burden, likely fewer overlaps. + - Con: may lose deliberate "daily narrative" placement. +3. **Hybrid approach.** + - LLM returns clusters and ranks; Cytoscape computes positions inside + cluster constraints. -- **Phase 0 (today):** Claude Code plugin handles everything. Obsidian - plugin doesn't exist yet. -- **Phase 1:** Obsidian plugin shipped, sidecar schema unchanged. Both - systems coexist; user installs both. -- **Phase 2 (optional):** Strip the visual-notes skill down to a stub - pointing at the Obsidian plugin. Claude Code plugin keeps the - obsidian-append hook for note-writing assistance only. +Decision for now: keep preset LLM positions, but A/B against a force-directed +layout before a stable release. ---- +Design tasks: -## 6. The Extraction Prompt +- Add a repeatable layout comparison fixture with the same sidecar rendered + under preset and force-directed strategies. +- Decide whether `position` remains required in schema v1. +- Define behavior for nodes outside schema coordinate bounds. -Lives at `plugins/obsidian-plugin/prompts/extract-graph.md`. Bundled with -the plugin; user can override via settings (advanced). +### Marketplace and release planning -### Structure +Obsidian plugin release path: -```markdown -You are extracting a concept map from an Obsidian daily note. Read the -markdown below and return a JSON object describing the day's main concepts -and their relationships. +1. Finish MVP hardening. +2. Create a beta release consumable by BRAT. +3. Verify install/update behavior in a clean test vault. +4. Tag releases with an Obsidian-specific prefix, e.g. `obsidian-v0.1.0`. +5. Submit to the Obsidian community plugin store after beta feedback. -# Heuristics (apply all) +Claude Code plugin release path: -1. **Every edge has a label.** The label IS the insight ("caused by", - "blocks", "is part of", "led to"). No bare connections. -2. **Hierarchy encodes importance.** Central concepts have multiple edges; - peripheral ones have one or two. -3. **Max 30 nodes total.** If the note covers more, group related items - into cluster nodes labeled with a short summary like "build issues (4)". -4. **Semantic status colors:** - - `completed` (green) — finished outcomes, decisions made - - `active` (yellow) — in-progress work, open questions - - `context` (blue) — background facts, references, dependencies - - `blocked` (red) — explicitly stuck items -5. **Shape encodes type:** - - `system` (rectangle) — tools, services, codebases, files - - `task` (ellipse) — actions, work items - - `decision` (diamond) — choices made, discoveries, design points -6. **Cross-domain links are gold.** When the note connects unrelated areas, - surface those as weak edges (dashed style) — they're often the most - interesting findings. +1. Migrate hook and skill content into `plugins/claude-code-plugin`. +2. Keep the skill focused on agent-curated sidecar pre-population, not on + duplicating the Obsidian plugin's automatic extraction behavior. +3. Add marketplace metadata when the plugin is functional. +4. Tag releases with a Claude-specific prefix, e.g. `claude-v0.1.0`. + +CI/CD still needs to be designed and added: + +- PR checks for lint/typecheck/build. +- JSON schema validation. +- Obsidian release packaging for `manifest.json`, `main.js`, and `styles.css`. +- Claude Code plugin metadata validation. -# Schema +### Future schema changes -Return JSON only, matching this structure: +Current schema supports: -{ - "title": "Daily Overview - YYYY-MM-DD", - "subtitle": "", - "nodes": [ - { - "data": { "id": "kebab-id", "label": "Display\nLabel" }, - "classes": " ", - "position": { "x": , "y": } - } - ], - "edges": [ - { - "data": { "source": "id1", "target": "id2", "label": "verb phrase" }, - "classes": "" - } - ] -} +- `kind`: `daily-overview`, `session-whiteboard`, `rollup` +- graph nodes with type/status classes and required positions +- labeled edges with optional strength classes +- producer metadata (`_extractedBy`, `_schemaVersion`) +- extraction metadata (`_lastProcessedHash`, `_usage`) +- pinning (`_pinned`) -# Layout +Potential schema evolution: -Place clusters in a horizontal sweep across the canvas. Each major theme -gets its own cluster (~250px apart vertically, 250px between nodes within -a cluster). Major clusters are separated by ~450px horizontally. +- Make `_schemaVersion` required at v1. +- Add section provenance for click-to-source and section-aware updates. +- Add stable cluster/group metadata separate from visual node classes. +- Add layout metadata so `position` can become optional or layout-specific. +- Add confidence/grounding fields for nodes and edges. +- Define producer ownership semantics if multiple producers cooperate on one + sidecar. -# Examples +Compatibility rule: renderers should warn and skip unsupported `kind` values +without deleting or rewriting data they do not understand. -[Two or three short markdown→JSON examples, ~100 words of markdown each] +### Claude Code companion migration -# Markdown +The companion plugin should not compete with automatic Obsidian extraction. It +should exist for workflows where an agent intentionally curates graph content. -{full_markdown_content} -``` +Migration checklist: -### Few-shot examples +- Port the existing hook runner and visual-notes skill from the private + workflow. +- Update the skill to reference this repository's schema and extraction + prompt as canonical heuristics. +- Ensure agent-authored sidecars use `_pinned: true` only when the agent is + deliberately claiming ownership. +- Document how users recover from a stale curated sidecar: unpin or force + regenerate in Obsidian. -Two examples bundled: -1. A short engineering session (3-5 nodes, demonstrates type/status mapping) -2. A multi-cluster day (15+ nodes, demonstrates clustering + cross-domain - weak edges) +### Privacy and storage -These are the highest-leverage prompt-engineering investment. Iterate on -the examples first when extraction quality is off. +Current first pass stores the Anthropic API key in plugin `data.json`. -### Prompt-engineering anti-patterns to avoid +Future design work: -- ❌ "Best represent this as a concept map" → vague -- ❌ Generic node names like "Concept", "Idea", "Thing" -- ❌ Asking the LLM to determine "max nodes" itself -- ✅ Concrete constraints, schema with example values, explicit do/don't lists +- Investigate Obsidian desktop secret storage support and mobile limitations. +- Decide whether mobile should keep plaintext storage, warn more strongly, or + require a separate low-risk key. +- Consider per-folder warnings for sensitive folders. +- Add documentation for what is sent to Anthropic and when. ---- +### Cost controls -## 7. Look & Feel +Existing controls: -### Visual style +- watched folders default to empty +- debounce between saves +- content hash dedup +- force-regenerate cooldown +- status bar count +- usage metadata when available -Cytoscape rendered with **Catppuccin** color palette (Latte for light, -Mocha for dark). Theme variables read from Obsidian's CSS at mount time. +Potential additions: -| Status | Latte (light) bg | Latte border | Mocha (dark) bg | Mocha border | -|---|---|---|---|---| -| `completed` | `#a6e3a1` | `#40a02b` | `#a6e3a1` | `#94e2d5` | -| `active` | `#f9e2af` | `#df8e1d` | `#f9e2af` | `#fab387` | -| `context` | `#89b4fa` | `#1e66f5` | `#89b4fa` | `#74c7ec` | -| `blocked` | `#f38ba8` | `#d20f39` | `#f38ba8` | `#eba0ac` | +- Daily or monthly soft budget warnings. +- Per-folder extraction enable/disable. +- "Manual only" watched-folder mode. +- Token-count preflight using Anthropic's token counting endpoint instead of a + character heuristic. -Status fill colors stay the same across themes (Catppuccin's accent -palette is consistent); the page background, text, and borders shift -between Latte and Mocha. Read all values from Obsidian's CSS variables -at mount time — don't hardcode in the plugin. +### Rendering and navigation polish -### Shapes +Open areas: -- `system` — `round-rectangle`, padding 12px, label inside -- `task` — `ellipse`, padding 12px -- `decision` — `diamond`, padding 16px (more padding because diamonds visually - shrink vs. rectangles at the same node-size setting) +- Click node to jump to the best matching markdown heading or text span. +- Keyboard shortcuts for fit/reset. +- Better mobile canvas height and touch behavior. +- More accessible legend and ARIA labels. +- Placeholder states for missing API key, missing sidecar, malformed sidecar, + unsupported sidecar kind, and stale pinned graphs. +- Multiple split panes showing the same file without duplicate containers or + lifecycle leaks. -### Edges +## Known limitations -- Default — 2px solid, bezier curve, small triangle arrowhead -- `.strong-edge` — 3px, darker color, same shape -- `.weak-edge` — 1px dashed, lighter color, indicates cross-domain links +- Full-note extraction can duplicate API spend during tight multi-device sync + races. +- LLM-generated positions can overlap or produce less readable layouts. +- The plugin cannot guarantee graph quality; bad extraction requires manual + regenerate or prompt/schema improvement. +- API key storage is plaintext in the current implementation. +- Claude Code companion hooks are scaffolded but not functional yet. +- The renderer currently supports daily overview sidecars; other `kind` values + are reserved for future use. -### Layout (current) +## Open decisions -LLM produces explicit `{x, y}` positions following the prompt's layout -guidance. Cytoscape uses `layout: { name: 'preset' }` to honor them. - -**Future:** A/B against `cose-bilkent` force-directed layout (let Cytoscape -position nodes; LLM only produces structure). Decision deferred — see §10. - -### Interactivity - -- **Click a node → scroll the markdown to the related section.** This is - the killer interaction for journal use — turns the visual into a - navigation aid, not just decoration. **Match precedence:** - 1. **Heading-slug match.** Compare the node's `data.id` (already - kebab-case) to each markdown heading slugified the same way. First - match wins. Most reliable signal because the LLM derives ids from - headings. - 2. **Label substring match (case-insensitive).** Search the markdown - body for the first occurrence of the node's `data.label` (with `\n` - replaced by space). - 3. **No match.** Brief Notice "no match in markdown for '$label'"; no - scroll. Indicates LLM hallucinated a node not grounded in text; - useful debug signal. - Document this precedence in the implementation notes for renderer.ts - so behavior stays consistent across versions. -- **Hover** on a node → bold border, highlight all connected edges -- **Mouse out** → restore default -- **Pan** with click-drag, **zoom** with wheel/pinch (touch on mobile) -- **No node-drag.** Preset layout is authoritative; users who don't like - positioning fix the markdown, not the visual. -- Min zoom 0.3, max zoom 3.0 -- Keyboard: `f` fits viewport to graph, `r` resets zoom - -### Mobile-specific - -- Cytoscape canvas height: 350px on phones (vs. 450px on desktop) to - preserve note-text real estate on narrow viewports. -- Touch gestures: pinch-zoom + drag-pan. No hover; tap a node for the - jump-to-section behavior. -- Settings page: avoid horizontal layouts; vertical stack of text inputs. - The "Advanced" disclosure stays collapsed by default on mobile. - -### Header - -Top-left corner of the canvas: -- `

` with the `header` field (e.g., "Daily Overview") -- Subtitle below in muted text (e.g., "2026-05-01 — Hook fix · sidecar - architecture") - -### Legend - -Top-right corner: small floating box with status dots (completed/active/ -context/blocked) and shape labels (system/task/decision). Helps the user -parse the visual the first time they see it. - ---- - -## 8. Repo Structure - -``` -visual-notes/ -├── README.md # Top-level: project overview, install paths -├── LICENSE # MIT -├── .gitignore -├── pnpm-workspace.yaml # Declares plugins/* and shared/ as workspaces -├── package.json # Root: dev tooling (typescript, eslint, prettier) -│ -├── docs/ -│ ├── design.md # THIS document -│ ├── architecture.md # (Optional) deeper technical spec when impl starts -│ └── decisions/ # ADR-style records as decisions accumulate -│ └── 0001-shared-schema.md -│ -├── shared/ -│ ├── schema.json # JSON Schema for sidecar -│ └── package.json # Workspace package; generates TS types -│ -└── plugins/ - ├── claude-code-plugin/ - │ ├── README.md # Install: /plugin install ... - │ ├── .claude-plugin/ - │ │ └── plugin.json - │ ├── hooks/ - │ │ ├── hooks.json - │ │ ├── post-obsidian-append.md - │ │ └── run-hook.sh - │ └── skills/ - │ └── visual-notes/ - │ └── SKILL.md - │ - └── obsidian-plugin/ - ├── README.md # Install: BRAT or community store - ├── package.json - ├── manifest.json - ├── tsconfig.json - ├── esbuild.config.mjs - ├── styles.css - ├── prompts/ - │ └── extract-graph.md # System prompt + few-shot examples - └── src/ - ├── main.ts - ├── extractor.ts - ├── renderer.ts - ├── settings.ts - ├── theme.ts - ├── storage.ts - ├── debounce.ts - └── schema.ts -``` - -### Why pnpm workspaces - -- Shared schema package is consumed by both plugins; workspace symlinks - avoid copying. -- Single root `node_modules` keeps disk usage and install time manageable. -- Per-plugin `package.json` keeps dependencies scoped (Obsidian plugin - has Anthropic SDK + cytoscape; Claude Code plugin has nothing). - -### CI/CD - -`.github/workflows/`: -- `obsidian-release.yml` — on tag `obsidian-v*`: build, package, create - GitHub release with `manifest.json` + `main.js` + `styles.css` as - assets (BRAT/community-store consumable) -- `claude-code-release.yml` — on tag `claude-v*`: validate plugin.json - schema, no build artifacts (Claude Code plugins distribute as git refs) -- `ci.yml` — on PR: typecheck, lint, validate JSON schemas - -Path filters limit each workflow to its component's files. - -### Versioning - -**Independent.** Plugins evolve at different cadences. Tag prefix -distinguishes: -- `obsidian-v0.1.0` — Obsidian plugin release -- `claude-v0.1.0` — Claude Code plugin release -- `schema-v0.1.0` — Sidecar schema bump (forces both plugins to declare - compatibility) - ---- - -## 9. Implementation Phases - -### Phase 1 — Scaffold + Obsidian plugin MVP (1.5–2 weeks) - -Budget honestly: this is a fresh TypeScript Obsidian plugin with esbuild, -zod, an external API integration, and inline-rendered Cytoscape. A week -is doable for someone who's shipped Obsidian plugins before; budget 1.5–2 -weeks otherwise. - -- Repo scaffolded ✅ (initial commit landed) -- Extraction prompt authored ✅ (`prompts/extract-graph.md`, with two - few-shot examples). The plugin has nothing to send without this. -- Copy `esbuild.config.mjs`, `tsconfig.json`, `version-bump.mjs`, - `versions.json`, `styles.css` from - [obsidian-sample-plugin](https://github.com/obsidianmd/obsidian-sample-plugin). - Don't reinvent. -- Obsidian plugin skeleton: manifest, plugin entry that registers a - no-op `MarkdownPostProcessor` and `PluginSettingTab` -- Settings tab with API key field (plaintext, no SecretStorage yet) -- Generate `src/schema.ts` from `shared/schema.json` via - `json-schema-to-typescript` -- Manual command: `Visual Notes: Extract from current note`. No - file-watching, no debouncing yet. -- `extractor.ts`: hand-rolled `requestUrl` to Anthropic Messages API, - Zod-validate response -- Write sidecar JSON next to the note -- Verify happy path on one daily note end-to-end - -### Phase 2 — Auto-extraction + rendering (week 2) - -- File-watcher with debounce + content-hash dedup -- MarkdownPostProcessor mounts Cytoscape from sidecar JSON -- Theme integration (CSS vars → Cytoscape style) -- Caching: Map with onload/onunload lifecycle - -### Phase 3 — Settings polish + storage (week 3) - -- SecretStorage on desktop, plaintext warning on mobile -- Per-folder configurability (different model/prompt per watched folder — v0.1 keeps the dial uniform) -- Model dropdown (Haiku / Sonnet) -- Custom prompt override (textarea) - -### Phase 4 — Distribution (week 4) - -- BRAT release (tag `obsidian-v0.1.0-beta.0`) -- Submit to Obsidian community plugin store -- Document install paths in README - -### Phase 5 — Claude Code plugin migration (week 5) - -- Pull existing `~/ai/skills/visual-notes/` and `~/ai/hooks/` content - into `plugins/claude-code-plugin/` -- Strip skill down to "the Obsidian plugin handles this; write good notes" -- Create `.claude-plugin/marketplace.json` for distribution -- Both plugins coexist; users can install either or both - -### Phase 6 — Polish (ongoing) - -- A/B test LLM-positioned vs `cose-bilkent` layout -- Cost-tracking widget (optional, originally cut from MVP) -- Multi-vault config -- More few-shot prompt examples - ---- - -## 9b. Failure modes & lifecycle states - -Every external dependency can fail; designing for it up front is cheaper -than debugging in the wild. - -### Lifecycle state machine - -```mermaid -stateDiagram-v2 - [*] --> Idle - Idle --> Debouncing: vault.modify (md in scope) - Debouncing --> Debouncing: another modify resets timer - Debouncing --> Hashing: 1.5s elapsed - Hashing --> Idle: hash matches sidecar._lastProcessedHash - Hashing --> CheckPin: hash differs OR no sidecar - CheckPin --> Idle: sidecar._pinned == true - CheckPin --> Extracting: not pinned - - Idle --> Extracting: cmd: Extract from current note (skips Hashing+CheckPin if pinned-aware) - Idle --> Extracting: cmd: Regenerate (force) — bypasses _pinned + hash, 30s cooldown - Idle --> Idle: cmd: Pin this overview (writes _pinned=true) - Idle --> Idle: cmd: Unpin this overview (writes _pinned=false) - Idle --> Idle: cmd: Delete sidecar - - Extracting --> Writing: 200 OK + Zod-valid - Extracting --> Extracting: Zod fail (1 retry with correction) - Extracting --> Queued: 429 / 5xx (with backoff, max 3) - Extracting --> Failed: 401 / 400 (terminal) - Extracting --> Idle: AbortController.abort() (plugin unload) - Queued --> Extracting: backoff timer - Queued --> Idle: AbortController.abort() (plugin unload) - Queued --> Failed: 3 retries exhausted - Writing --> Idle: sidecar written, render triggered - Failed --> Idle: user dismisses Notice or fixes config -``` - -The `Idle → Extracting (cmd: Regenerate force)` transition is the -unpin-and-extract escape hatch. The user is never stuck with a pinned -sidecar; force-regen overrides. - -### Failure scenarios + handling - -| Scenario | Handling | -|---|---| -| **API key missing** on plugin load | Status-bar shows "Visual Notes: configure API key". File watcher stays inert. | -| **Watched folders list empty** on plugin load | Status-bar shows "Visual Notes: add a watched folder". One-time Notice on first load. | -| **A configured folder doesn't exist in the vault** | Notice on plugin load naming the missing folder. Other configured folders continue to be watched normally; the missing one is rechecked next load. | -| **Anthropic API down** (5xx) | Bounded retry (3×, exponential backoff). On exhaustion, queue for next manual trigger; status-bar shows "queued". | -| **Network failure** (no internet) | Treated as 5xx. Same retry/queue behavior. | -| **Rate limit** (429) | Honor `retry-after`, exponential backoff, max 3 retries. | -| **Auth failure** (401) | Notice with "open settings" affordance. Don't retry. | -| **Bad request** (400) | Log to console, Notice. Suggests model/prompt issue; don't retry. | -| **Malformed response from API** (Zod-invalid JSON) | One retry with schema-correction prompt. Then fail. | -| **Sidecar JSON malformed** (someone wrote bad JSON) | Renderer logs the error, displays a "⚠ malformed sidecar" placeholder in the note. Does NOT crash the post-processor. | -| **Daily note deleted mid-flight** | Catch the `vault.modify()` error on sidecar write; discard result silently. | -| **Sidecar exists but markdown doesn't** (orphaned) | Renderer shows the visual as-is (data is still valid). User can manually delete via the "Delete sidecar" command. | -| **Plugin disabled mid-flight** | `AbortController.abort()` in `onunload()` cancels the in-flight `requestUrl`. No write happens. | -| **Two views of same file open** | Each `MarkdownRenderChild` owns its own Cytoscape instance. No cache collision. | -| **Obsidian Sync delivers sidecar mid-extraction** | Hash check at extraction completion; if the just-arrived sidecar's hash matches what we just extracted, no-op. | -| **Sidecar `kind` is non-default** (`session-whiteboard`, `rollup`) | v0.1 renders only `kind: "daily-overview"` (or sidecars with `kind` omitted, which default to that). For unsupported kinds, the renderer logs `console.warn("Visual Notes: unsupported sidecar kind '${k}', skipping render")` and skips mounting. The sidecar is preserved unchanged. | -| **Sidecar `kind` field schema-valid but unknown to plugin** (future kind we don't yet support) | Same as above — log + skip, don't crash. | -| **API key valid but user hits org quota** (529 overloaded) | Treat as 5xx: bounded retry with backoff, then queue. | -| **User pastes API key with leading/trailing whitespace** | Trim on save in settings handler. | - -### What we explicitly DON'T handle in v0.1 - -- Multi-vault settings divergence (Obsidian vault model is per-vault by default) -- Recovery from a corrupted plugin `data.json` (Obsidian re-creates from defaults) -- API key rotation mid-session (user restarts plugin or Obsidian) -- LLM hallucination of nonsense graphs (manual `Regenerate (force)` is the escape hatch) - ---- - -## 10. Open Questions / Decisions for the Implementation Team - -### Layout algorithm - -**Question:** Stick with LLM-produced positions (current schema), or strip -positions from the schema and use Cytoscape's `cose-bilkent` force-directed -layout? - -**Decision:** **Stick with LLM positions for v0.1.** A/B against `cose-bilkent` -as a Phase 6 polish item. The LLM-positions approach preserves the -hand-crafted clustered layout the user has been using; switching costs -prompt re-engineering and a visual style change. - -### Mobile API key UX - -**Question:** Mobile can't use SecretStorage. Three options: -- (a) Plaintext in `data.json` with a stark warning -- (b) Disable extraction on mobile entirely -- (c) Require user to set up a separate mobile-scoped API key with - reduced permissions - -**Decision:** **(a)** for v0.1. Mobile users opt-in by entering the key, -warned in the settings description. Future: explore (c) when Anthropic -ships fine-grained API key scoping. - -### Multi-vault config - -**Question:** What if a user has multiple Obsidian vaults and wants -different settings per vault? - -**Decision:** Out of scope for v0.1. Settings are per-vault by Obsidian -default (each vault has its own `.obsidian/plugins//data.json`). -That's good enough. - -### Obsidian Sync race conditions - -**Question:** Two devices simultaneously extract the same note. Each -writes a sidecar. Which wins? - -**Decision:** Last-writer-wins via Obsidian Sync's natural file-merge -semantics. Content-hash dedup prevents the extraction from happening on -both devices in the common case (one device extracts first, sidecar syncs, -second device sees matching hash and skips). - -### Custom prompts - -**Question:** Should we support per-folder or per-tag prompt customization? - -**Decision:** Out of scope. v0.1 is one global prompt. Power users can -override the whole prompt via settings. - ---- - -## 11. References - -### Obsidian Plugin Development - -- [Obsidian Plugin Docs](https://docs.obsidian.md/Home) -- [Sample Plugin Repository](https://github.com/obsidianmd/obsidian-sample-plugin) -- [MarkdownPostProcessor reference](https://docs.obsidian.md/Reference/TypeScript+API/MarkdownPostProcessor) -- [PluginSettingTab reference](https://docs.obsidian.md/Reference/TypeScript+API/PluginSettingTab) -- [Vault API docs](https://docs.obsidian.md/Plugins/Vault) -- [Obsidian community plugin store submission](https://github.com/obsidianmd/obsidian-releases) -- [BRAT (Beta Reviewers Auto-update Tool)](https://github.com/TfTHacker/obsidian42-brat) -- [Juggl plugin (Cytoscape reference impl)](https://github.com/HEmile/juggl) - -### Anthropic API - -- [Claude API quickstart](https://platform.claude.com/docs/en/docs/quickstart) -- [Structured outputs guide](https://platform.claude.com/docs/en/build-with-claude/structured-outputs) -- [Token counting API](https://platform.claude.com/docs/en/build-with-claude/token-counting) -- [Error handling reference](https://platform.claude.com/docs/en/api/errors) -- [Prompt engineering best practices](https://platform.claude.com/docs/en/build-with-claude/prompt-engineering/claude-prompting-best-practices) -- [Pricing](https://platform.claude.com/pricing) -- [`@anthropic-ai/sdk` npm](https://www.npmjs.com/package/@anthropic-ai/sdk) - -### Claude Code Plugins - -- [Claude Code plugin docs](https://code.claude.com/docs/en/plugins) -- [Plugin reference](https://code.claude.com/docs/en/plugins-reference) -- [Plugin marketplaces](https://code.claude.com/docs/en/plugin-marketplaces) -- [Hooks docs](https://code.claude.com/docs/en/hooks) -- [Anthropic's official marketplace](https://github.com/anthropics/claude-plugins-official) - -### Cytoscape.js - -- [Cytoscape.js docs](https://js.cytoscape.org/) -- [cose-bilkent layout](https://github.com/cytoscape/cytoscape.js-cose-bilkent) -- [cytoscape-css-variables extension](https://github.com/lukethacoder/cytoscape-css-variables) - -### Tooling - -- [pnpm workspaces](https://pnpm.io/workspaces) -- [json-schema-to-typescript](https://github.com/bcherny/json-schema-to-typescript) -- [Catppuccin palette](https://github.com/catppuccin/catppuccin) - ---- - -## Appendix A — Glossary - -| Term | Meaning | -|---|---| -| **Sidecar** | The `{date}-overview.json` file that lives next to the daily note, holding the extracted graph data. | -| **Watched folders** | The list of Obsidian folders the plugin monitors for daily-note changes. Empty by default; user must add at least one. Subfolders are watched recursively. | -| **Daily note** | A markdown file named `YYYYMMDD.md` in any of the watched folders. | -| **Overview** | The visual concept map rendered for a daily note. | -| **Session-whiteboard** (legacy) | Per-conversation `{date}-session-{n}.json` sidecars from the original visual-notes skill. **Note:** "session" elsewhere in this doc refers to a conversational unit (an "AI session summary" in a daily note). When ambiguous, use "session-whiteboard" for the file format and "session summary" or "conversation" for the content unit. | -| **Producer** | Any code that writes a sidecar. The Obsidian plugin and the Claude Code plugin are the two producers. | -| **`_pinned`** | A boolean field in the sidecar that, when `true`, suppresses LLM extraction by the Obsidian plugin. Used by the Claude Code plugin to claim authoritative ownership of a sidecar. User toggles via the Pin/Unpin commands; force-regenerate command bypasses. | -| **MarkdownPostProcessor** | Obsidian API hook for post-render content injection. The plugin uses it to inject the Cytoscape canvas into the rendered markdown view of any daily note that has a sidecar. | -| **MarkdownRenderChild** | Obsidian lifecycle wrapper for rendered content. Each instance owns one Cytoscape canvas and is torn down when the view closes. | -| **BRAT** | Beta Reviewers Auto-update Tool. The standard Obsidian-plugin distributor for pre-release builds. Users add a repo URL and BRAT pulls the latest tagged release. | - -## Appendix B — Debugging Visual Notes - -For maintainers and the user when something doesn't work as expected. - -| Symptom | Where to look | -|---|---| -| Visual doesn't appear | Open DevTools (Ctrl+Shift+I in Obsidian). Filter console for `[visual-notes]`. Check for "no watched folders configured", "API key invalid", or extraction errors. Also: confirm the note's folder is in the watched-folders list. | -| Visual is stale | Run `Visual Notes: Regenerate (force)` from command palette. Bypasses pin and cached hash. | -| Visual shows wrong content | Check the sidecar: open `{date}-overview.json` next to the daily note. Is `_pinned: true`? An agent may have locked it; run `Visual Notes: Unpin this overview` then regenerate. | -| Repeated API calls visible in status-bar count | Check that the `_lastProcessedHash` field is being written to the sidecar (look for `sha256:` prefix). If absent, the dedup is broken. | -| Plugin loads then errors immediately | Verify `manifest.json` `minAppVersion ≤` your Obsidian version. Otherwise `getSecretStorage` may be unavailable. | -| Extraction succeeds but render is blank | Validate the sidecar against `shared/schema.json` — possibly an unknown `kind`, malformed JSON, or out-of-bounds positions. | -| Settings UI fields are gone after upgrade | Check `data.json` `_settingsVersion`. The migration step may have failed; restore from a backup of `data.json` (Obsidian writes `data.json.bak` on save). | - -The Plugin instance exposes `(window as any).__visualNotes` in dev -builds — gives DevTools console access to the extractor, sidecar -events, and current settings. Useful for live debugging without -reaching for the source. - ---- - -## Appendix C — Things explicitly cut from MVP - -These are intentional non-goals for the first release. Documenting them so -future contributors don't relitigate. - -- Cost transparency UI (running token spend display in settings) -- Streaming API responses -- Per-folder configs -- Per-tag configs -- Custom theme palettes -- Layout algorithm picker (deferred to Phase 6) -- Obsidian Canvas (.canvas) output format -- Native Obsidian Graph View integration -- Mind-map export -- PNG/SVG export of the rendered graph -- Bidirectional sync (visual edits writing back to markdown) +| Topic | Current leaning | Decision needed before | +|---|---|---| +| Section-aware sidecar metadata | Add after MVP; do not block v0.1 | schema v1 | +| Layout algorithm | Keep preset positions; A/B force-directed | stable release | +| API key storage | Improve desktop storage; document mobile caveat | public beta | +| Cost dashboard | Keep status count for MVP | after beta feedback | +| Claude Code pin defaults | Do not pin automatically | companion migration | +| Rollup/session sidecar rendering | Preserve schema values, skip in v0.1 renderer | adding those modes | + +## Reference links + +- [Top-level README](../README.md) +- [Obsidian plugin README](../plugins/obsidian-plugin/README.md) +- [Claude Code plugin README](../plugins/claude-code-plugin/README.md) +- [Shared schema](../shared/schema.json) +- [Extraction prompt](../plugins/obsidian-plugin/prompts/extract-graph.md) +- [Issue #4](https://github.com/bobthearsonist/visual-notes/issues/4) diff --git a/plugins/claude-code-plugin/README.md b/plugins/claude-code-plugin/README.md index c36f9d8..23fc151 100644 --- a/plugins/claude-code-plugin/README.md +++ b/plugins/claude-code-plugin/README.md @@ -20,8 +20,8 @@ common case without it. repo and is being ported here. Until then, the manifest and directory structure are scaffolded but the hooks/skills are placeholders. -See [`../../docs/design.md`](../../docs/design.md) §5 for the design and §9 -Phase 5 for the migration plan. +See [`../../docs/design.md`](../../docs/design.md) for the remaining migration +plan and open decisions. ## Install (after migration) diff --git a/plugins/claude-code-plugin/skills/visual-notes/SKILL.md b/plugins/claude-code-plugin/skills/visual-notes/SKILL.md index e36ee21..75fc4d4 100644 --- a/plugins/claude-code-plugin/skills/visual-notes/SKILL.md +++ b/plugins/claude-code-plugin/skills/visual-notes/SKILL.md @@ -6,8 +6,8 @@ description: Concept-map visualization for daily notes. Documents the sidecar JS # Visual Notes (Claude Code skill) > **Scaffold placeholder.** Migration of the full skill from the private -> dotfiles repo is pending. See [`../../../docs/design.md`](../../../docs/design.md) -> §5 for the migration plan. +> dotfiles repo is pending. See the living design document for the migration +> plan. ## What this skill is for @@ -46,5 +46,5 @@ agent pre-populating a sidecar should follow them. Quick summary: If the Obsidian plugin is also installed, both producers can write the sidecar. **Last writer wins.** For workflows where you want the agent's -curated graph to stick, set `_pinned: true` in the sidecar (future feature) -to suppress auto-extraction. +curated graph to stick, set `_pinned: true` in the sidecar to suppress +auto-extraction until the user unpins or force-regenerates. diff --git a/plugins/obsidian-plugin/README.md b/plugins/obsidian-plugin/README.md index 1415814..83d0eb2 100644 --- a/plugins/obsidian-plugin/README.md +++ b/plugins/obsidian-plugin/README.md @@ -7,11 +7,14 @@ the top of the rendered note view. ## Status -🚧 **Phase 1 MVP.** The plugin now includes the Obsidian build scaffold, -settings tab, manual and watched-folder extraction, sidecar writing, -pin/unpin/delete commands, and inline Cytoscape rendering from the sidecar. -See [`../../docs/design.md`](../../docs/design.md) §4 for the full design -and §9 for implementation phases. +🚧 **MVP implementation in progress.** The plugin includes the Obsidian build +scaffold, settings tab, manual and watched-folder extraction, sidecar writing, +pin/unpin/delete commands, status-bar extraction count, token-usage metadata, +and inline Cytoscape rendering from the sidecar. + +The top-level [`../../README.md`](../../README.md) has the product and +architecture overview. The living [`../../docs/design.md`](../../docs/design.md) +tracks remaining design work and open decisions. ## Install (after first release) @@ -73,7 +76,8 @@ sequenceDiagram Plugin->>User: render Cytoscape inline ``` -See [`../../docs/design.md`](../../docs/design.md) §4.2 for the full lifecycle. +See the architecture overview in [`../../README.md`](../../README.md) and the +future-facing notes in [`../../docs/design.md`](../../docs/design.md). ## Cost