#!/usr/bin/env python3 """odysseus-notes — shell wrapper around the notes feature. Reads the same SQLite the web UI uses. Output JSON; pipe-friendly. odysseus-notes list --label calendar | jq -r '.[] | .title' odysseus-notes show NOTE_ID odysseus-notes search "invoice" odysseus-notes create --title "Buy milk" --content "..." odysseus-notes delete NOTE_ID """ from __future__ import annotations import sys import os, sys sys.path.insert(0, os.path.join(os.path.dirname(__file__), "_lib")) from cli import quiet_logs, emit, fail, common_parser, run, REPO_ROOT as _REPO_ROOT quiet_logs() import argparse, json, logging, os, sys, uuid from datetime import datetime from pathlib import Path try: from core.database import SessionLocal, Note quiet_logs() except ModuleNotFoundError as e: sys.stderr.write(f"error: {e}\nhint: run from repo root with venv active.\n") sys.exit(2) def _serialize(n: "Note") -> dict: return { "id": n.id, "title": n.title or "", "content": n.content or "", "items": json.loads(n.items) if n.items else [], "note_type": n.note_type or "note", "color": n.color or "", "label": n.label or "", "pinned": bool(n.pinned), "archived": bool(n.archived), "due_date": n.due_date or "", "source": n.source or "user", "created_at": n.created_at.isoformat() if n.created_at else "", "updated_at": n.updated_at.isoformat() if n.updated_at else "", } def cmd_list(args): db = SessionLocal() try: q = db.query(Note) if not args.archived: q = q.filter(Note.archived == False) # noqa: E712 if args.label: q = q.filter(Note.label == args.label) if args.pinned: q = q.filter(Note.pinned == True) # noqa: E712 q = q.order_by(Note.pinned.desc(), Note.sort_order.asc(), Note.updated_at.desc()).limit(args.limit) emit([_serialize(n) for n in q.all()], args) finally: db.close() def cmd_show(args): db = SessionLocal() try: n = db.get(Note, args.id) if not n: fail(f"no note with id {args.id!r}") emit(_serialize(n), args) finally: db.close() def cmd_search(args): db = SessionLocal() try: like = f"%{args.query}%" rows = db.query(Note).filter( (Note.title.ilike(like)) | (Note.content.ilike(like)) ).order_by(Note.updated_at.desc()).limit(args.limit).all() emit([_serialize(n) for n in rows], args) finally: db.close() def cmd_create(args): db = SessionLocal() try: n = Note( id=str(uuid.uuid4()), title=args.title, content=args.content or "", note_type=args.type, color=args.color or None, label=args.label or None, pinned=bool(args.pin), source="user", ) db.add(n) db.commit() db.refresh(n) emit(_serialize(n), args) finally: db.close() def cmd_delete(args): db = SessionLocal() try: n = db.get(Note, args.id) if not n: fail(f"no note with id {args.id!r}") snap = _serialize(n) db.delete(n) db.commit() emit({"ok": True, "deleted": snap}, args) finally: db.close() def _build_parser(): common = argparse.ArgumentParser(add_help=False) common.add_argument("--pretty", action="store_true") p = argparse.ArgumentParser(prog="odysseus-notes", parents=[common]) sub = p.add_subparsers(dest="cmd", required=True) pl = sub.add_parser("list", parents=[common]) pl.add_argument("--label") pl.add_argument("--archived", action="store_true", help="include archived") pl.add_argument("--pinned", action="store_true", help="pinned only") pl.add_argument("--limit", type=int, default=50) pl.set_defaults(func=cmd_list) psh = sub.add_parser("show", parents=[common]) psh.add_argument("id") psh.set_defaults(func=cmd_show) ps = sub.add_parser("search", parents=[common]) ps.add_argument("query") ps.add_argument("--limit", type=int, default=50) ps.set_defaults(func=cmd_search) pc = sub.add_parser("create", parents=[common]) pc.add_argument("--title", required=True) pc.add_argument("--content", default="") pc.add_argument("--type", choices=["note", "checklist"], default="note") pc.add_argument("--color") pc.add_argument("--label") pc.add_argument("--pin", action="store_true") pc.set_defaults(func=cmd_create) pd = sub.add_parser("delete", parents=[common]) pd.add_argument("id") pd.set_defaults(func=cmd_delete) return p if __name__ == "__main__": sys.exit(run(_build_parser()))