147 lines
4.2 KiB
Python
147 lines
4.2 KiB
Python
"""Obsidian integration for Icarus fabric entries.
|
|
|
|
Opt-in via ICARUS_OBSIDIAN=1 environment variable.
|
|
|
|
- format_entry: appends wikilinks for review_of/revises refs
|
|
- ensure_daily_note: links new entries in a daily note
|
|
- init_obsidian: one-time vault setup
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import logging
|
|
import os
|
|
import re
|
|
from datetime import datetime, timezone
|
|
from pathlib import Path
|
|
from typing import Optional
|
|
|
|
from .state import _parse_head
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
LINKS_START = "<!-- ICARUS_OBSIDIAN_LINKS_START -->"
|
|
LINKS_END = "<!-- ICARUS_OBSIDIAN_LINKS_END -->"
|
|
|
|
|
|
def _find_entry_file(ref: str, fabric_dir: Path) -> Optional[str]:
|
|
"""Resolve agent:id ref to a filename (without .md extension)."""
|
|
if ":" not in ref:
|
|
return None
|
|
agent, entry_id = ref.split(":", 1)
|
|
if not agent or not entry_id:
|
|
return None
|
|
for d in (fabric_dir, fabric_dir / "cold"):
|
|
if not d.exists():
|
|
continue
|
|
for f in d.glob("*.md"):
|
|
h = _parse_head(f)
|
|
if h.get("agent") == agent and h.get("id") == entry_id:
|
|
return f.stem # filename without .md
|
|
return None
|
|
|
|
|
|
def format_entry(filepath: Path, fabric_dir: Path, review_of: str = "", revises: str = ""):
|
|
"""Append wikilinks section to a fabric entry for Obsidian navigation."""
|
|
if not review_of and not revises:
|
|
return
|
|
|
|
text = filepath.read_text("utf-8")
|
|
links = []
|
|
|
|
if review_of:
|
|
target = _find_entry_file(review_of, fabric_dir)
|
|
if target:
|
|
links.append(f"- Reviews: [[{target}]]")
|
|
else:
|
|
links.append(f"- Reviews: {review_of}")
|
|
|
|
if revises:
|
|
target = _find_entry_file(revises, fabric_dir)
|
|
if target:
|
|
links.append(f"- Revises: [[{target}]]")
|
|
else:
|
|
links.append(f"- Revises: {revises}")
|
|
|
|
if not links:
|
|
return
|
|
|
|
generated = (
|
|
f"\n\n{LINKS_START}\n"
|
|
"## Links\n"
|
|
+ "\n".join(links)
|
|
+ f"\n{LINKS_END}\n"
|
|
)
|
|
|
|
existing = re.compile(
|
|
rf"\n*{re.escape(LINKS_START)}.*?{re.escape(LINKS_END)}\n*",
|
|
re.DOTALL,
|
|
)
|
|
text = existing.sub("\n", text).rstrip()
|
|
filepath.write_text(text + generated, "utf-8")
|
|
|
|
|
|
def ensure_daily_note(fabric_dir: Path, entry_filename: str, summary: str):
|
|
"""Add a link to the new entry in today's daily note."""
|
|
daily_dir = fabric_dir / "daily"
|
|
daily_dir.mkdir(parents=True, exist_ok=True)
|
|
|
|
today = datetime.now().strftime("%Y-%m-%d")
|
|
daily_path = daily_dir / f"{today}.md"
|
|
|
|
stem = entry_filename.replace(".md", "")
|
|
link_line = f"- [[{stem}]] {summary}\n"
|
|
|
|
if daily_path.exists():
|
|
content = daily_path.read_text("utf-8")
|
|
if f"[[{stem}]]" in content:
|
|
return # already linked
|
|
daily_path.write_text(content.rstrip() + "\n" + link_line, "utf-8")
|
|
else:
|
|
daily_path.write_text(f"# {today}\n\n{link_line}", "utf-8")
|
|
|
|
|
|
def _vault_dir_for(fabric_dir: Path) -> Path:
|
|
"""Resolve the Obsidian vault root for config files."""
|
|
configured = os.environ.get("OBSIDIAN_VAULT_PATH", "").strip()
|
|
if configured:
|
|
return Path(configured).expanduser()
|
|
return fabric_dir
|
|
|
|
|
|
def init_obsidian(fabric_dir: Path) -> dict:
|
|
"""One-time Obsidian vault setup for a fabric directory."""
|
|
created = []
|
|
|
|
daily_dir = fabric_dir / "daily"
|
|
if not daily_dir.exists():
|
|
daily_dir.mkdir(parents=True, exist_ok=True)
|
|
created.append(str(daily_dir))
|
|
|
|
vault_dir = _vault_dir_for(fabric_dir)
|
|
vault_dir.mkdir(parents=True, exist_ok=True)
|
|
|
|
obsidian_dir = vault_dir / ".obsidian"
|
|
if not obsidian_dir.exists():
|
|
obsidian_dir.mkdir(parents=True, exist_ok=True)
|
|
created.append(str(obsidian_dir))
|
|
|
|
app_json = obsidian_dir / "app.json"
|
|
if not app_json.exists():
|
|
app_json.write_text(json.dumps({
|
|
"showFrontmatter": True,
|
|
"readableLineLength": True,
|
|
}, indent=2), "utf-8")
|
|
created.append(str(app_json))
|
|
|
|
if created:
|
|
logger.info("icarus: obsidian init created %s", created)
|
|
|
|
return {
|
|
"status": "initialized" if created else "already_initialized",
|
|
"fabric_dir": str(fabric_dir),
|
|
"vault_dir": str(vault_dir),
|
|
"created": created,
|
|
}
|