diff --git a/.obsidian/graph.json b/.obsidian/graph.json index 5d367f9..993660d 100644 --- a/.obsidian/graph.json +++ b/.obsidian/graph.json @@ -67,6 +67,6 @@ "repelStrength": 20, "linkStrength": 1, "linkDistance": 80, - "scale": 0.4786753448718912, + "scale": 0.6620854838064228, "close": false } \ No newline at end of file diff --git a/.obsidian/workspace.json b/.obsidian/workspace.json index 39a1714..9cc7293 100644 --- a/.obsidian/workspace.json +++ b/.obsidian/workspace.json @@ -190,18 +190,22 @@ }, "active": "a31de1b302cdc5cf", "lastOpenFiles": [ + "README.md.tmp.224546.1775557396282", + "README.md.tmp.224546.1775557389171", + "README.md.tmp.224546.1775557379648", + "skills/wiki/references/plugins.md.tmp.224546.1775557354797", + "skills/wiki/SKILL.md.tmp.224546.1775557343437", + "skills/canvas/references/canvas-spec.md", + "skills/canvas/references/canvas-spec.md.tmp.224546.1775557327401", + "skills/canvas/SKILL.md", + "skills/canvas/SKILL.md.tmp.224546.1775557280886", + "commands/canvas.md", + "commands/canvas.md.tmp.224546.1775557227716", + "wiki/canvases", + "skills/canvas/references", + "skills/canvas", "wiki/comparisons/Wiki vs RAG.md", - "wiki/comparisons/Wiki vs RAG.md.tmp.224546.1775556215080", "wiki/questions/How does the LLM Wiki pattern work.md", - "wiki/questions/How does the LLM Wiki pattern work.md.tmp.224546.1775556203684", - "wiki/comparisons", - "wiki/questions", - "wiki/concepts/_index.md.tmp.224546.1775556164132", - "wiki/index.md.tmp.224546.1775556150463", - "wiki/log.md.tmp.224546.1775556137750", - "wiki/hot.md.tmp.224546.1775556127364", - "wiki/sources/_index.md.tmp.224546.1775556114770", - "wiki/entities/_index.md.tmp.224546.1775556102633", "wiki/meta/workflow-loop.gif", "wiki/meta/wiki-graph-grow.gif", "cover.gif", @@ -233,10 +237,6 @@ "wiki/Wiki Map.canvas", "skills/wiki/references/rest-api.md", "skills/wiki/references/plugins.md", - "skills/wiki/references/modes.md", - "skills/wiki/references/mcp-setup.md", - "skills/wiki/references/git-setup.md", - "skills/wiki/references/frontmatter.md", - "skills/wiki/references/css-snippets.md" + "skills/wiki/references/modes.md" ] } \ No newline at end of file diff --git a/README.md b/README.md index 8743b2f..ff8b516 100644 --- a/README.md +++ b/README.md @@ -81,6 +81,11 @@ Then scaffold the full wiki structure. | `/save` | File the current conversation as a wiki note | | `/save [name]` | Save with a specific title (skips the naming question) | | `/autoresearch [topic]` | Run the autonomous research loop: search, fetch, synthesize, file | +| `/canvas` | Open or create the visual canvas, list zones and nodes | +| `/canvas add image [path]` | Add an image (URL or local path) to the canvas with auto-layout | +| `/canvas add note [page]` | Pin a wiki page as a card on the canvas | +| `/canvas zone [name]` | Add a new labeled zone to organize visual content | +| `/canvas from banana` | Capture recently generated images onto the canvas | | `lint the wiki` | Health check: orphans, dead links, gaps, suggestions | | `update hot cache` | Refresh hot.md with latest context summary | @@ -198,16 +203,20 @@ cosmic-brain/ │ ├── wiki-query/ # QUERY operation │ ├── wiki-lint/ # LINT operation │ ├── save/ # /save — file conversations to wiki -│ └── autoresearch/ # /autoresearch — autonomous research loop +│ ├── autoresearch/ # /autoresearch — autonomous research loop +│ │ └── references/ +│ │ └── program.md # configurable research objectives +│ └── canvas/ # /canvas — visual layer (images, PDFs, notes) │ └── references/ -│ └── program.md # configurable research objectives +│ └── canvas-spec.md # Obsidian canvas JSON format reference ├── agents/ │ ├── wiki-ingest.md # parallel ingestion agent │ └── wiki-lint.md # health check agent ├── commands/ │ ├── wiki.md # /wiki bootstrap command │ ├── save.md # /save command -│ └── autoresearch.md # /autoresearch command +│ ├── autoresearch.md # /autoresearch command +│ └── canvas.md # /canvas visual layer command ├── hooks/ │ └── hooks.json # SessionStart + Stop hot cache hooks ├── _templates/ # Obsidian Templater templates diff --git a/commands/canvas.md b/commands/canvas.md new file mode 100644 index 0000000..2b8f789 --- /dev/null +++ b/commands/canvas.md @@ -0,0 +1,21 @@ +--- +description: Open, create, or update a visual canvas — add images, text, PDFs, wiki pages, and banana-generated assets to Obsidian canvas files. +--- + +Read the `canvas` skill. Then run the operation matching the user's command. + +| Command | What it does | +|---------|-------------| +| `/canvas` | Status check — report node counts, list zones, open instructions | +| `/canvas new [name]` | Create a new named canvas in wiki/canvases/ | +| `/canvas add image [path]` | Add image to canvas (download if URL, copy if outside vault) | +| `/canvas add text [content]` | Add a text card to the canvas | +| `/canvas add pdf [path]` | Add a PDF document node | +| `/canvas add note [page]` | Add a wiki page as a linked card | +| `/canvas zone [name] [color]` | Add a new labeled zone group | +| `/canvas list` | List all canvases with node counts | +| `/canvas from banana` | Find recent generated images and add them | + +Default canvas: `wiki/canvases/main.canvas` + +If the canvas file does not exist, create it before adding anything. diff --git a/skills/canvas/SKILL.md b/skills/canvas/SKILL.md new file mode 100644 index 0000000..3ecf8cb --- /dev/null +++ b/skills/canvas/SKILL.md @@ -0,0 +1,273 @@ +--- +name: canvas +description: > + Visual layer of the wiki. Add images, text cards, PDFs, and wiki pages to Obsidian + canvas files. Auto-positions nodes inside zones using bbox math. Integrates with + /banana skill for generated image capture. Triggers on: "/canvas", "canvas new", + "canvas add image", "canvas add text", "canvas add pdf", "canvas add note", + "canvas zone", "canvas list", "canvas from banana", "add to canvas", + "put this on the canvas", "open canvas", "create canvas". +allowed-tools: ["Read", "Write", "Edit", "Bash", "Glob", "Grep"] +--- + +# canvas — Visual Reference Layer + +The three knowledge capture layers: +- `/save` → text synthesis (wiki/questions/, wiki/concepts/) +- `/autoresearch` → structured knowledge (wiki/sources/, wiki/concepts/) +- `/canvas` → visual references (wiki/canvases/) + +A canvas is a JSON file Obsidian renders as an infinite visual board. This skill reads and writes canvas JSON directly. Read `references/canvas-spec.md` for the full format reference before making any edits. + +--- + +## Default Canvas + +`wiki/canvases/main.canvas` + +If it does not exist, create it: + +```json +{ + "nodes": [ + { + "id": "title", + "type": "text", + "text": "# Visual Reference\n\nDrop images, PDFs, and notes here.", + "x": -400, "y": -300, "width": 400, "height": 120, "color": "6" + }, + { + "id": "zone-default", + "type": "group", + "label": "General", + "x": -400, "y": -140, "width": 800, "height": 400, "color": "4" + } + ], + "edges": [] +} +``` + +--- + +## Operations + +### open / status (`/canvas` with no args) + +1. Check if `wiki/canvases/main.canvas` exists. +2. If yes: read it, count nodes by type, list all group node labels (zone names). + Report: "Canvas has N nodes: X images, Y text cards, Z wiki pages. Zones: [list]" +3. If no: create it with the starter structure above. + Report: "Created main.canvas with a General zone." +4. Tell user: "Open `wiki/canvases/main.canvas` in Obsidian to view." + +--- + +### new (`/canvas new [name]`) + +1. Slugify the name: lowercase, spaces → hyphens, strip special chars. +2. Create `wiki/canvases/[slug].canvas` with the starter structure, title updated to `# [Name]`. +3. Add entry to `wiki/index.md` under a "## Canvases" section (create the section if it doesn't exist). +4. Report: "Created wiki/canvases/[slug].canvas" + +--- + +### add image (`/canvas add image [path or url]`) + +**Resolve the image:** +- If URL (starts with `http`): download with `curl -sL [url] -o _attachments/images/canvas/[filename]` + Derive filename from URL path, or use `img-[timestamp].jpg` if unclear. +- If local path outside vault: `cp [path] _attachments/images/canvas/` +- If already vault-relative: use as-is. + +Create `_attachments/images/canvas/` if it doesn't exist. + +**Detect aspect ratio:** +Use `python3 -c "from PIL import Image; img=Image.open('[path]'); print(img.width, img.height)"` or `identify -format '%w %h' [path]`. +- 16:9 (width/height ≈ 1.7–2.0): width=420, height=236 +- 1:1 (ratio ≈ 1.0): width=280, height=280 +- 9:16 (ratio < 0.65): width=200, height=356 +- Unknown: width=320, height=240 + +**Position using auto-layout** (see Auto-Positioning section below). + +**Append node to canvas JSON and write.** + +Report: "Added [filename] to [zone] zone at position ([x], [y])." + +--- + +### add text (`/canvas add text [content]`) + +Create a text node: +```json +{ + "id": "text-[timestamp]", + "type": "text", + "text": "[content]", + "x": [auto], "y": [auto], + "width": 300, "height": 120, + "color": "4" +} +``` + +Position using auto-layout. Write and report. + +--- + +### add pdf (`/canvas add pdf [path]`) + +Same as add image. Obsidian renders PDFs natively as file nodes. +- Copy to `_attachments/pdfs/canvas/` if outside vault. +- Fixed size: width=400, height=520. +- Report page count if you can determine it. + +--- + +### add note (`/canvas add note [wiki-page]`) + +1. Search `wiki/` for a file matching the page name (case-insensitive, partial match ok). +2. Use the vault-relative path as the `file` field. +3. Create a file node: width=300, height=100. +4. Position using auto-layout. + +```json +{ + "id": "note-[timestamp]", + "type": "file", + "file": "wiki/concepts/LLM Wiki Pattern.md", + "x": [auto], "y": [auto], + "width": 300, "height": 100 +} +``` + +--- + +### zone (`/canvas zone [name] [color]`) + +1. Read canvas JSON. +2. Find max_y: `max(node.y + node.height for all nodes) + 60`. Use -140 if no nodes. +3. Create a group node: + +```json +{ + "id": "zone-[slug]", + "type": "group", + "label": "[name]", + "x": -400, + "y": [max_y], + "width": 1000, + "height": 400, + "color": "[color or '3']" +} +``` + +Valid colors: `"1"`=red `"2"`=orange `"3"`=yellow `"4"`=green `"5"`=cyan `"6"`=purple + +Write and report. + +--- + +### list (`/canvas list`) + +1. `glob wiki/canvases/*.canvas` +2. For each canvas: read JSON, count nodes by type. +3. Report: + +``` +wiki/canvases/main.canvas — 14 nodes (8 images, 3 text, 2 file, 1 group) +wiki/canvases/design-ideas.canvas — 42 nodes (30 images, 4 text, 8 groups) +``` + +--- + +### from banana (`/canvas from banana`) + +1. Check `wiki/canvases/.recent-images.txt` first (session log of newly written images). +2. If not found or empty: `find _attachments/images -name "*.png" -o -name "*.jpg" -newer [10min-ago-file]` + Create the reference file: `python3 -c "import time; open('/tmp/ten-min-ago','w').close(); import os; os.utime('/tmp/ten-min-ago', (time.time()-600, time.time()-600))"` + Then: `find _attachments/images -newer /tmp/ten-min-ago -name "*.png" -o -name "*.jpg"` +3. If still none: show the 5 most recently modified images. +4. Present list: "Found N recent images: [list]. Add to canvas? Which zone? (zone name / 'new [name]' / 'skip')" +5. On confirmation: add each using the add image logic. + +--- + +## Auto-Positioning Algorithm + +Read `references/canvas-spec.md` for the full coordinate system. + +```python +def next_position(canvas_nodes, target_zone_label, new_w, new_h): + # Find zone group node + zone = next((n for n in canvas_nodes + if n.get('type') == 'group' + and n.get('label') == target_zone_label), None) + + if zone is None: + # No zone — place below all content + max_y = max((n['y'] + n.get('height', 0) for n in canvas_nodes), default=-140) + return -400, max_y + 60 + + zx, zy = zone['x'], zone['y'] + zw, zh = zone['width'], zone['height'] + + # Nodes inside this zone + inside = [n for n in canvas_nodes + if n.get('type') != 'group' + and zx <= n['x'] < zx + zw + and zy <= n['y'] < zy + zh] + + if not inside: + return zx + 20, zy + 20 + + rightmost_x = max(n['x'] + n.get('width', 0) for n in inside) + next_x = rightmost_x + 40 + + if next_x + new_w > zx + zw: + # New row + max_row_y = max(n['y'] + n.get('height', 0) for n in inside) + return zx + 20, max_row_y + 20 + + # Same row — match the top y of existing row + current_row_y = min(n['y'] for n in inside if n['x'] > zx + zw / 2) \ + if any(n['x'] > zx + zw / 2 for n in inside) \ + else zy + 20 + return next_x, current_row_y +``` + +--- + +## ID Generation + +Read the canvas, collect all existing IDs. Never reuse one. + +Safe ID pattern: `[type]-[content-slug]-[unix-timestamp-last-4-digits]` + +Examples: `img-cover-7823`, `text-note-1045`, `zone-branding-3391` + +--- + +## Session Log (optional hook) + +If `wiki/canvases/.recent-images.txt` exists, append any new image path written to `_attachments/images/` during this session (one path per line, keep last 20). + +`/canvas from banana` reads this file first, making it instant without filesystem search. + +--- + +## Banana Integration + +After any `/banana` run in the same session, if the user says "add to canvas" or "put on canvas", treat it as `/canvas from banana`. + +When `/banana` finishes generating images, suggest: +> "Add generated images to canvas? Run `/canvas from banana`" + +--- + +## Summary + +1. Read canvas-spec.md before editing any canvas JSON. +2. Always read the canvas file before writing — parse existing nodes to avoid ID collisions and calculate auto-positions. +3. Create `_attachments/images/canvas/` for downloaded/copied images. +4. Update `wiki/index.md` when creating new canvases. +5. Report position and zone after every add operation. diff --git a/skills/canvas/references/canvas-spec.md b/skills/canvas/references/canvas-spec.md new file mode 100644 index 0000000..abb760b --- /dev/null +++ b/skills/canvas/references/canvas-spec.md @@ -0,0 +1,273 @@ +# Obsidian Canvas JSON Specification + +Canvas files are JSON with two top-level keys: `nodes` (array) and `edges` (array). +Obsidian reads and writes them as UTF-8 JSON files with `.canvas` extension. + +--- + +## Coordinate System + +``` + x increases → + ┌───────────────────────────────── + │ (-920, -2400) (0, -2400) + │ +y │ (-920, 0) (0, 0) ← origin +↓ │ + │ (-920, 540) (500, 540) +``` + +- **Origin** (0, 0) is the center of the canvas viewport. +- **x increases rightward.** Negative x = left of center. +- **y increases downward.** Negative y = above center. +- Node `x` and `y` are the **top-left corner** of the node, not the center. +- Obsidian pans to fit all nodes on first open. + +--- + +## Node Types + +### Text node + +Renders markdown content as a styled card. + +```json +{ + "id": "text-title-4821", + "type": "text", + "text": "# Heading\n\nParagraph with **bold** and `code`.", + "x": -400, + "y": -300, + "width": 400, + "height": 120, + "color": "6" +} +``` + +- `text`: markdown string. Use `\n` for newlines. +- Minimum readable size: width ≥ 200, height ≥ 60. +- `color` is optional. Omit for default (no color). + +--- + +### File node + +Renders an image, PDF, markdown note, or other vault file inline. + +```json +{ + "id": "img-cover-7823", + "type": "file", + "file": "_attachments/images/skool-hub/logo-pro.png", + "x": -900, + "y": -100, + "width": 420, + "height": 236 +} +``` + +- `file`: **vault-relative path** (not absolute, not `~/`). +- Supported: `.png` `.jpg` `.webp` `.gif` `.pdf` `.md` `.canvas` +- For `.md` files: renders as a preview card. +- For `.pdf` files: renders the first page as preview. +- No `color` field for file nodes — color is ignored. + +--- + +### Group node (Zone) + +A labeled rectangular region. Does not clip or contain nodes — it's a visual guide. +Nodes placed "inside" a group are just positioned within its bounding box. + +```json +{ + "id": "zone-branding-3391", + "type": "group", + "label": "Brand Identity", + "x": -920, + "y": -880, + "width": 1060, + "height": 290, + "color": "6" +} +``` + +- `label`: shown at the top of the group box. +- `color`: colors the group border and label. +- Groups do not affect auto-layout — they are purely visual containers. + +--- + +### Link node + +Renders a web URL as an embedded preview card. + +```json +{ + "id": "link-karpathy-2233", + "type": "link", + "url": "https://github.com/karpathy", + "x": 200, + "y": -300, + "width": 400, + "height": 120 +} +``` + +- `url`: must be a valid `https://` URL. +- Obsidian fetches the Open Graph preview (title, description, thumbnail). + +--- + +## Edges + +Connections between nodes. Usually empty for mood boards. + +```json +{ + "id": "e-hub-cidx", + "fromNode": "hub", + "fromSide": "right", + "toNode": "c-idx", + "toSide": "left", + "toEnd": "arrow", + "label": "concepts", + "color": "5" +} +``` + +- `fromSide` / `toSide`: `"top"` `"bottom"` `"left"` `"right"` +- `toEnd`: `"arrow"` (directed) or `"none"` (undirected line) +- `label` and `color` are optional. + +--- + +## Color Reference + +| Code | Color | Hex (approx) | Use case | +|------|-------|-------------|----------| +| `"1"` | Red / Tomato | #e03e3e | Warnings, archive | +| `"2"` | Orange | #d09035 | Active work | +| `"3"` | Yellow / Gold | #d0a023 | WIP, notes | +| `"4"` | Green / Teal | #448361 | Content, sources | +| `"5"` | Blue / Cyan | #3ea7d3 | Navigation, info | +| `"6"` | Purple / Violet | #9063d2 | Title, identity | + +Omit `color` entirely for the default (no border color, transparent label). + +--- + +## Image Sizing Guidelines + +Calculate from actual image dimensions using PIL or `identify`: + +```bash +python3 -c "from PIL import Image; img=Image.open('path.png'); print(img.width, img.height)" +# or +identify -format '%w %h' path.png +``` + +| Aspect ratio | Condition | Canvas width | Canvas height | +|-------------|-----------|-------------|--------------| +| 16:9 (wide) | ratio 1.6–2.0 | 420 | 236 | +| 2:1 (ultra wide) | ratio > 2.0 | 440 | 220 | +| 4:3 | ratio 1.2–1.6 | 380 | 285 | +| 1:1 (square) | ratio 0.9–1.1 | 280 | 280 | +| 3:4 | ratio 0.6–0.9 | 240 | 320 | +| 9:16 (portrait) | ratio < 0.6 | 200 | 356 | +| PDF | any | 400 | 520 | +| Unknown | fallback | 320 | 240 | + +--- + +## Auto-Positioning Pseudocode + +``` +function place_node(canvas, zone_label, new_w, new_h): + zone = find group node where label == zone_label + padding = 20 + + if zone not found: + max_y = max(n.y + n.height for n in canvas.nodes) + 60 + return (-400, max_y) + + # Nodes visually inside zone + inside = [n for n in canvas.nodes + if n.type != 'group' + and zone.x <= n.x < zone.x + zone.width + and zone.y <= n.y < zone.y + zone.height] + + if inside is empty: + return (zone.x + padding, zone.y + padding) + + # Rightmost point in zone + rightmost = max(n.x + n.width for n in inside) + next_x = rightmost + 40 + + if next_x + new_w > zone.x + zone.width - padding: + # Overflow → new row + bottom_of_row = max(n.y + n.height for n in inside) + return (zone.x + padding, bottom_of_row + padding) + + # Same row + row_y = min(n.y for n in inside) # align to top of existing row + return (next_x, row_y) +``` + +--- + +## Full Example: Two-Zone Canvas + +```json +{ + "nodes": [ + { + "id": "title-0001", + "type": "text", + "text": "# Brand Reference\n\n**AI Marketing Hub** visual assets", + "x": -920, "y": -2440, "width": 560, "height": 180, "color": "6" + }, + { + "id": "zone-logos", + "type": "group", + "label": "Logos & Icons", + "x": -920, "y": -2200, "width": 1800, "height": 320, "color": "6" + }, + { + "id": "img-logo-pro", + "type": "file", + "file": "_attachments/images/skool-hub/logo-pro.png", + "x": -900, "y": -2180, "width": 420, "height": 236 + }, + { + "id": "img-icon-free", + "type": "file", + "file": "_attachments/images/skool-hub/icon-free-v1.png", + "x": -440, "y": -2180, "width": 280, "height": 280 + }, + { + "id": "zone-covers", + "type": "group", + "label": "Skill Covers", + "x": -920, "y": -1820, "width": 1800, "height": 340, "color": "3" + }, + { + "id": "img-seo", + "type": "file", + "file": "_attachments/images/skool-hub/claude-seo-cover.png", + "x": -900, "y": -1800, "width": 420, "height": 236 + } + ], + "edges": [] +} +``` + +--- + +## Common Mistakes + +- **Wrong path format**: use `_attachments/images/file.png` not `/home/user/...` or `~/...` +- **ID collision**: always read existing IDs before generating a new one +- **Negative y confusion**: `y: -2400` is ABOVE `y: -1000` (more negative = higher up) +- **Group does not clip**: placing a node "inside" a group is just positioning it within the group's bounding box — there is no parent-child relationship in the JSON +- **Missing height on text nodes**: Obsidian will render the text but may clip it if height is too small. Use height ≥ content-lines × 24. diff --git a/skills/wiki/SKILL.md b/skills/wiki/SKILL.md index c99fde2..1362af3 100644 --- a/skills/wiki/SKILL.md +++ b/skills/wiki/SKILL.md @@ -107,6 +107,7 @@ Route to the correct operation based on what the user says: | "lint", "health check", "clean up" | LINT | `wiki-lint` | | "save this", "file this", "/save" | SAVE | `save` | | "/autoresearch [topic]", "research [topic]" | AUTORESEARCH | `autoresearch` | +| "/canvas", "add to canvas", "open canvas" | CANVAS | `canvas` | --- diff --git a/skills/wiki/references/plugins.md b/skills/wiki/references/plugins.md index 4d7335c..a6b20b5 100644 --- a/skills/wiki/references/plugins.md +++ b/skills/wiki/references/plugins.md @@ -50,9 +50,15 @@ Install via Settings > Community Plugins > Turn off Restricted Mode > Browse. | **Dataview** | Query vault as a database. Powers dashboards in `wiki/meta/`. | | **Templater** | Auto-populate frontmatter on note creation from `_templates/`. | | **Obsidian Git** | Auto-commit every 15 minutes. Protects against bad writes. | +| **Calendar** | Right-sidebar calendar with word count, task, and link indicators. Pre-installed in this vault via `.obsidian/plugins/calendar/`. | +| **Thino** | Quick memo capture panel in right sidebar. Pre-installed via `.obsidian/plugins/thino/`. | | **Iconize** | Visual folder icons for navigation. | | **Minimal Theme** | Best dark theme for dense information display. | +**Calendar and Thino are pre-installed** — they ship with this vault. Enable them in Settings → Community Plugins → toggle on. No download needed. + +If installing in a different vault: download `main.js` + `manifest.json` from their GitHub releases into `.obsidian/plugins/calendar/` and `.obsidian/plugins/thino/` respectively. + Optional additions: - **Smart Connections** — semantic search across all notes - **QuickAdd** — macros for fast note creation