Odysseus v1.0
This commit is contained in:
199
scripts/odysseus-docs
Executable file
199
scripts/odysseus-docs
Executable file
@@ -0,0 +1,199 @@
|
||||
#!/usr/bin/env python3
|
||||
"""odysseus-docs — shell wrapper for the living-document store.
|
||||
|
||||
Documents are the AI-editable text/markdown/code files the assistant
|
||||
creates in-place (think: a scratchpad it can revise). Each document
|
||||
has a content blob + version history.
|
||||
|
||||
odysseus-docs list [--active] [--limit N]
|
||||
odysseus-docs show DOC_ID # current content + metadata
|
||||
odysseus-docs versions DOC_ID # version history (no content)
|
||||
odysseus-docs export DOC_ID --version N # full content of a specific version
|
||||
odysseus-docs search "invoice" # title + current_content match
|
||||
odysseus-docs delete DOC_ID # soft-delete (is_active=False)
|
||||
|
||||
Reads `data/app.db` directly.
|
||||
"""
|
||||
|
||||
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
|
||||
from pathlib import Path
|
||||
|
||||
try:
|
||||
from core.database import SessionLocal, Document, DocumentVersion
|
||||
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(d: "Document", include_content: bool = False) -> dict:
|
||||
out = {
|
||||
"id": d.id,
|
||||
"title": d.title,
|
||||
"language": d.language or "",
|
||||
"session_id": d.session_id or "",
|
||||
"version_count": d.version_count or 1,
|
||||
"is_active": bool(d.is_active),
|
||||
"tidy_verdict": d.tidy_verdict or "",
|
||||
"content_length": len(d.current_content or ""),
|
||||
"created_at": d.created_at.isoformat() if d.created_at else "",
|
||||
"updated_at": d.updated_at.isoformat() if d.updated_at else "",
|
||||
}
|
||||
if include_content:
|
||||
out["current_content"] = d.current_content or ""
|
||||
return out
|
||||
|
||||
|
||||
def cmd_list(args):
|
||||
db = SessionLocal()
|
||||
try:
|
||||
q = db.query(Document)
|
||||
if args.active:
|
||||
q = q.filter(Document.is_active == True) # noqa: E712
|
||||
if args.session:
|
||||
q = q.filter(Document.session_id == args.session)
|
||||
q = q.order_by(Document.updated_at.desc()).limit(args.limit)
|
||||
emit([_serialize(d) for d in q.all()], args)
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
|
||||
def cmd_show(args):
|
||||
db = SessionLocal()
|
||||
try:
|
||||
d = db.get(Document, args.id)
|
||||
if not d:
|
||||
fail(f"no document with id {args.id!r}")
|
||||
emit(_serialize(d, include_content=True), args)
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
|
||||
def cmd_versions(args):
|
||||
db = SessionLocal()
|
||||
try:
|
||||
d = db.get(Document, args.id)
|
||||
if not d:
|
||||
fail(f"no document with id {args.id!r}")
|
||||
rows = db.query(DocumentVersion).filter(
|
||||
DocumentVersion.document_id == args.id
|
||||
).order_by(DocumentVersion.version_number.desc()).all()
|
||||
emit([
|
||||
{
|
||||
"version_number": v.version_number,
|
||||
"summary": v.summary or "",
|
||||
"source": v.source or "ai",
|
||||
"content_length": len(v.content or ""),
|
||||
} for v in rows
|
||||
], args)
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
|
||||
def cmd_export(args):
|
||||
"""Print the full content of a specific version as the JSON
|
||||
payload's `content` field, or raw text via --raw."""
|
||||
db = SessionLocal()
|
||||
try:
|
||||
d = db.get(Document, args.id)
|
||||
if not d:
|
||||
fail(f"no document with id {args.id!r}")
|
||||
if args.version is None:
|
||||
content = d.current_content or ""
|
||||
version = d.version_count or 1
|
||||
else:
|
||||
v = db.query(DocumentVersion).filter(
|
||||
DocumentVersion.document_id == args.id,
|
||||
DocumentVersion.version_number == args.version,
|
||||
).first()
|
||||
if not v:
|
||||
fail(f"no version {args.version} on doc {args.id!r}")
|
||||
content = v.content or ""
|
||||
version = v.version_number
|
||||
if args.raw:
|
||||
sys.stdout.write(content)
|
||||
if not content.endswith("\n"):
|
||||
sys.stdout.write("\n")
|
||||
return
|
||||
emit({
|
||||
"id": d.id,
|
||||
"title": d.title,
|
||||
"version": version,
|
||||
"content": content,
|
||||
}, args)
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
|
||||
def cmd_search(args):
|
||||
db = SessionLocal()
|
||||
try:
|
||||
like = f"%{args.query}%"
|
||||
rows = db.query(Document).filter(
|
||||
(Document.title.ilike(like)) | (Document.current_content.ilike(like))
|
||||
).order_by(Document.updated_at.desc()).limit(args.limit).all()
|
||||
emit([_serialize(d) for d in rows], args)
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
|
||||
def cmd_delete(args):
|
||||
db = SessionLocal()
|
||||
try:
|
||||
d = db.get(Document, args.id)
|
||||
if not d:
|
||||
fail(f"no document with id {args.id!r}")
|
||||
d.is_active = False
|
||||
db.commit()
|
||||
emit({"ok": True, "id": d.id, "soft_deleted": True}, 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-docs", parents=[common])
|
||||
sub = p.add_subparsers(dest="cmd", required=True)
|
||||
|
||||
pl = sub.add_parser("list", parents=[common])
|
||||
pl.add_argument("--active", action="store_true", help="only is_active=True")
|
||||
pl.add_argument("--session", help="filter by session_id")
|
||||
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)
|
||||
|
||||
pv = sub.add_parser("versions", parents=[common])
|
||||
pv.add_argument("id")
|
||||
pv.set_defaults(func=cmd_versions)
|
||||
|
||||
pe = sub.add_parser("export", parents=[common])
|
||||
pe.add_argument("id")
|
||||
pe.add_argument("--version", type=int, help="specific version (default: current)")
|
||||
pe.add_argument("--raw", action="store_true", help="write raw content to stdout")
|
||||
pe.set_defaults(func=cmd_export)
|
||||
|
||||
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)
|
||||
|
||||
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()))
|
||||
Reference in New Issue
Block a user