d89da1292d
- docs/capabilities/INDEX.md - docs/capabilities/obsidian.md - python/functions/core/render_markdown_table.md - python/functions/core/render_markdown_table.py - python/functions/core/render_markdown_table_test.py - python/functions/core/upsert_sentinel_block.md - python/functions/core/upsert_sentinel_block.py - python/functions/core/upsert_sentinel_block_test.py - python/functions/infra/duckdb_query_readonly.md Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
87 lines
7.3 KiB
Markdown
87 lines
7.3 KiB
Markdown
# Capability: obsidian
|
|
|
|
CRUD headless de vaults y notas de Obsidian, tratadas como Markdown plano con frontmatter YAML y wikilinks `[[...]]`. NO depende de la app GUI de Obsidian ni de su URI scheme — manipula los archivos `.md` directamente en disco. Scriptable, rapido, con telemetria del registry.
|
|
|
|
Los vaults de Obsidian del usuario viven en `/home/enmanuel/Obsidian/` y estan enlazados como vaults del registry en el project `obsidian` (`projects/obsidian/vaults/`). Ver `projects/obsidian/project.md`.
|
|
|
|
## Funciones
|
|
|
|
| ID | Firma | Que hace |
|
|
|---|---|---|
|
|
| `parse_obsidian_frontmatter_py_obsidian` | `parse_obsidian_frontmatter(content: str) -> {"frontmatter": dict, "body": str}` | **Pure.** Separa el frontmatter YAML (bloque `---` inicial) del cuerpo. Si no hay frontmatter valido devuelve `{}` + el contenido completo. |
|
|
| `extract_obsidian_wikilinks_py_obsidian` | `extract_obsidian_wikilinks(body: str) -> list` | **Pure.** Extrae los targets de los wikilinks `[[...]]` y embeds `![[...]]`. Normaliza `[[nota\|alias]]`, `[[nota#heading]]`, `[[nota#^block]]` -> `nota`. Dedup preservando orden. |
|
|
| `format_obsidian_note_py_obsidian` | `format_obsidian_note(frontmatter: dict, body: str) -> str` | **Pure.** Inversa de parse: serializa frontmatter (YAML entre `---`) + body a una nota `.md` completa. |
|
|
| `read_obsidian_note_py_obsidian` | `read_obsidian_note(path: str) -> dict` | Lee una nota: `{path, frontmatter, body, wikilinks, tags}`. Compone parse + extract. |
|
|
| `create_obsidian_note_py_obsidian` | `create_obsidian_note(vault_dir, rel_path, body="", frontmatter=None, overwrite=False) -> str` | Crea nota nueva (crea dirs padre, añade `.md`). Error si existe y `overwrite=False`. |
|
|
| `update_obsidian_note_py_obsidian` | `update_obsidian_note(path, body=None, set_frontmatter=None, append=None) -> str` | Edita nota existente: merge de frontmatter, reemplazo de body, o append al final. |
|
|
| `delete_obsidian_note_py_obsidian` | `delete_obsidian_note(path: str) -> bool` | Borra una nota (solo archivo, nunca directorio). Error si no existe. |
|
|
| `list_obsidian_notes_py_obsidian` | `list_obsidian_notes(vault_dir, subfolder="", tag="") -> list` | Lista paths de notas `.md` (recursivo). Excluye `.obsidian/` y `.trash/`. Filtro opcional por tag de frontmatter. |
|
|
| `search_obsidian_notes_py_obsidian` | `search_obsidian_notes(vault_dir, query, in_body=True, in_frontmatter=True) -> list` | Busca substring (case-insensitive) en las notas. Devuelve `[{path, matches:[{line, text}]}]`. |
|
|
| `list_obsidian_vaults_py_obsidian` | `list_obsidian_vaults(base_dir: str) -> list` | Lista los vaults (subdirs con `.obsidian/`) bajo `base_dir`. `[{name, path}]`. |
|
|
| `create_obsidian_vault_py_obsidian` | `create_obsidian_vault(parent_dir, name) -> str` | Crea un vault nuevo: carpeta + `.obsidian/app.json` minimo. Error si ya existe. |
|
|
| `slugify_obsidian_name_py_obsidian` | `slugify_obsidian_name(name: str) -> str` | **Pure.** Nombre/titulo -> slug kebab-case estable (translitera acentos, ñ->n). Estabiliza ids de nodo y nombres de archivo. |
|
|
| `extract_obsidian_embeds_py_obsidian` | `extract_obsidian_embeds(body: str) -> list` | **Pure.** Solo los embeds `![[...]]` (attachments incrustados), ignorando wikilinks normales. Dedup preservando orden. |
|
|
| `resolve_obsidian_embed_py_obsidian` | `resolve_obsidian_embed(vault_dir, embed_name) -> str` | Resuelve un embed `![[foto.jpg]]` a su path absoluto real (busca por basename unico en el vault). Cadena vacia si no existe. |
|
|
| `build_obsidian_graph_py_obsidian` | `build_obsidian_graph(vault_dir, include_dangling=True) -> {"nodes":[...], "edges":[...]}` | **Grafo agregado** del vault: cada nota = nodo tipado (`id`=slug, `label`, `tipo`, `frontmatter`); cada wikilink `[[...]]` = arista con `kind` por seccion. Wikilinks rotos -> nodos fantasma `dangling`. |
|
|
| `render_markdown_table_py_core` | `render_markdown_table(rows: list[dict], columns=None, max_rows=0) -> str` | **Pure** (vive en `core`). Lista de dicts -> tabla Markdown GFM. Escapa pipes, saltos de linea -> `<br>`, truncado opcional con pie `... N de M filas`. Base del render BD -> nota. |
|
|
| `upsert_sentinel_block_py_core` | `upsert_sentinel_block(text, block_id, content, marker="osintdb") -> str` | **Pure** (vive en `core`). Inserta o reemplaza un bloque gestionado entre sentinels `<!-- marker:begin id=X -->` / `<!-- marker:end id=X -->` dentro del body de una nota. Idempotente; ValueError si el bloque esta corrupto. |
|
|
|
|
## Ejemplo canonico
|
|
|
|
Componer varias funciones del grupo se hace por heredoc importando del registry (las funciones se importan, no se reescriben):
|
|
|
|
```bash
|
|
cd /home/enmanuel/fn_registry
|
|
python/.venv/bin/python3 - <<'PYEOF'
|
|
import sys
|
|
sys.path.insert(0, "python/functions")
|
|
from obsidian import (
|
|
list_obsidian_vaults, list_obsidian_notes, search_obsidian_notes,
|
|
create_obsidian_note, read_obsidian_note, update_obsidian_note, delete_obsidian_note,
|
|
)
|
|
|
|
# 1. Descubrir vaults del usuario
|
|
vaults = list_obsidian_vaults("/home/enmanuel/Obsidian")
|
|
print("vaults:", [v["name"] for v in vaults])
|
|
|
|
# 2. Listar y buscar notas en un vault
|
|
finanzas = "/home/enmanuel/Obsidian/Finanzas"
|
|
print("notas:", len(list_obsidian_notes(finanzas)))
|
|
print("hits:", [h["path"] for h in search_obsidian_notes(finanzas, "presupuesto")][:5])
|
|
|
|
# 3. CRUD de una nota (crear -> leer -> editar -> borrar)
|
|
p = create_obsidian_note(finanzas, "inbox/idea_x", body="Primera linea",
|
|
frontmatter={"tags": ["inbox"], "created": "2026-06-09"})
|
|
note = read_obsidian_note(p)
|
|
print("creada:", note["path"], note["frontmatter"], note["wikilinks"])
|
|
update_obsidian_note(p, set_frontmatter={"status": "done"}, append="Ver [[Otra Nota]]")
|
|
delete_obsidian_note(p)
|
|
PYEOF
|
|
```
|
|
|
|
Para una sola operacion con un id conocido, `fn run` tambien sirve:
|
|
|
|
```bash
|
|
./fn run list_obsidian_vaults /home/enmanuel/Obsidian
|
|
./fn run list_obsidian_notes /home/enmanuel/Obsidian/Finanzas
|
|
```
|
|
|
|
## Cuando usar el grupo
|
|
|
|
- Crear/editar/leer notas de cualquier vault de Obsidian desde un agente o script, sin abrir la app.
|
|
- Buscar o listar notas por contenido o tag (ingesta, migracion, reporting sobre el vault).
|
|
- Crear vaults nuevos o inventariar los existentes.
|
|
|
|
## Fronteras (que NO cubre)
|
|
|
|
- **No habla con la app GUI** (no usa el URI scheme `obsidian://`, no abre notas en la interfaz, no dispara plugins). Si la app esta abierta, escribir en disco puede chocar con sus locks/cache — cerrar la app o refrescar manualmente.
|
|
- **No resuelve wikilinks a paths** automaticamente (devuelve los targets crudos). Resolver `[[nota]]` -> archivo real es responsabilidad del caller (busqueda por nombre en el vault).
|
|
- **No renderiza Markdown** ni evalua Dataview/templating. Trata las notas como texto + frontmatter.
|
|
- **El grafo agregado** del vault ya lo cubre `build_obsidian_graph_py_obsidian` (nodos tipados + aristas con `kind` + nodos fantasma `dangling`). Es la base de la vista grafo (sigma.js) de la app `osint_web`. Lo que sigue fuera del grupo es el *layout* visual del grafo (force-directed) — eso vive en el frontend.
|
|
|
|
## Gotchas
|
|
|
|
- Vaults grandes son caros: `NotasDeObsidian` pesa ~554M. `list_obsidian_notes` / `search_obsidian_notes` recorren todo el arbol — filtra por `subfolder` cuando puedas.
|
|
- `delete_obsidian_note` borra de verdad (no manda a `.trash/`). Para acciones destructivas masivas, listar primero y confirmar.
|
|
- El frontmatter `tags` puede venir como lista o como CSV string; `read_obsidian_note` lo normaliza a lista.
|