feat: Claude Agent integration + cookbook reconnect + UI polish

- Claude Agent integration: AGENT_CONFIGS.claude, INTG_TYPES.claude,
  setup_claude_routes + integrations/claude/ skill bundle. Wired in
  app.py alongside the existing Codex integration; same scope-gated
  /api/codex/* backend; agent form has new description so users know
  it's setup for an external CLI, not an agent streamed inside Odysseus.
- Remove mark_email_boundaries action: not good enough yet. Stripped
  from task UI, scheduler defaults, registry, tool schema, clear-cache
  route. Added to RETIRED_HOUSEKEEPING_ACTIONS so existing rows + their
  task_runs auto-purge on startup.
- Cookbook download reliability: "Reconnect" fix button in the crash
  diagnosis runs _reconnectTask after probing has-session. 30s confirm
  window before marking a download "done" — kills the Finished/Downloading
  flicker when tmux briefly drops between captures.
- Mobile UX: tap anywhere on a note card body opens the editor;
  Update button morphs to Archive when no text was edited; bell icon
  accent-colored; chip-trashing notif pills fade so only the icon
  rotates into the trash zone.
- Settings integrations: SVG-per-provider in email + API preset
  dropdowns, custom drop-up-aware menus, accent sub-header icons
  (IMAP/SMTP), consistent card styling between list + edit, contacts
  Edit/Delete icons, agent form description copy.
This commit is contained in:
pewdiepie-archdaemon
2026-06-04 08:27:26 +09:00
parent 6e80d0de08
commit 089246614d
17 changed files with 1301 additions and 387 deletions
@@ -0,0 +1,110 @@
---
name: odysseus
description: Use when the user asks Claude Code to read or write Odysseus data (todos, email, calendar, memory, documents) through the scoped Claude Agent API. Requires ODYSSEUS_URL and ODYSSEUS_API_TOKEN.
---
# Odysseus
Use this skill when a user asks to interact with Odysseus from Claude Code.
## Configuration
Expect these environment variables:
- `ODYSSEUS_URL`: Base URL for the user's Odysseus instance, for example `http://127.0.0.1:7000`.
- `ODYSSEUS_API_TOKEN`: Scoped API token created in Odysseus Settings > Integrations > Add Integration > Claude Agent.
If either value is missing, do not guess credentials. Tell the user to create a Claude Agent token in Odysseus Settings and expose both values to the terminal session.
## When to use what
- **Reminder ("remind me at 5pm to do X")** → TODO with `due_date`. The due_date IS the reminder — it fires a notification automatically via the user's configured channel (browser/email/ntfy). **Do NOT create a calendar event for a reminder.** Creating a calendar event named "Reminder" does NOT trigger a notification — it's just a time block on the calendar.
- **Calendar event ("meeting at 3pm", "dentist Tuesday 10am")** → calendar event. Use for scheduled time blocks, meetings, appointments, recurring schedules. These show up on the calendar grid; reminders for them are configured separately in Odysseus settings.
- **Note / freeform info ("note that the wifi password is ...")** → memory or todo without a due_date (depending on whether it's a fact about the user or an action item).
- **Persistent fact / preference about the user** → memory.
If the user says "reminder" + a time, default to TODO with due_date. Only switch to calendar if the user explicitly says "calendar", "event", "meeting", "appointment", or describes a time *range*.
## Safety
- All Odysseus data access MUST go through the scoped HTTP API under `/api/codex/*` (the canonical scope-gated agent API, shared by all agent integrations).
- Check `/api/codex/capabilities` before using a tool surface.
- Treat `403` as an intentional Settings restriction. Do not work around it.
- Do not use SSH, Docker, direct Python imports, SQLite queries, MCP internals, browser cookies, or local files to read/write Odysseus user data.
- Do not call helpers like `do_manage_notes`, email MCP internals, or database sessions directly for user data, even if shell access exists.
- Never send email directly unless the user explicitly asks to send and the token has a send-capable scope.
- Keep actions scoped to the token owner.
## Todos
The scoped agent API supports todos/checklists:
- `GET /api/codex/todos`
- `POST /api/codex/todos`
Use the bundled helper script when available:
```bash
python3 ~/.claude/skills/odysseus/scripts/odysseus_api.py capabilities
python3 ~/.claude/skills/odysseus/scripts/odysseus_api.py todos list
python3 ~/.claude/skills/odysseus/scripts/odysseus_api.py todos add "Follow up"
```
Supported todo actions are `list`, `add`, `update`, `delete`, and `toggle_item`.
**Reminders (todos with a due date)** — the backend parses natural language. Send `due_date` in the body via the generic POST so the time becomes a structured reminder, NOT a literal substring inside the title. The `todos add TITLE` shortcut only sets the title, so use the POST form for anything with a time:
```bash
python3 ~/.claude/skills/odysseus/scripts/odysseus_api.py POST /api/codex/todos '{"action":"add","title":"Call dentist","due_date":"tomorrow at 5pm"}'
```
The backend accepts both ISO timestamps and natural language like `"tomorrow 5pm"`, `"next Monday 9am"`, `"in 2 hours"`. It anchors to the user's timezone.
## Email
The scoped agent API supports email reads:
- `GET /api/codex/emails?folder=INBOX&limit=10&offset=0&filter=all`
- `GET /api/codex/emails/{uid}?folder=INBOX`
Use the bundled helper script when available:
```bash
python3 ~/.claude/skills/odysseus/scripts/odysseus_api.py emails list 5
python3 ~/.claude/skills/odysseus/scripts/odysseus_api.py emails read UID
```
If `/api/codex/capabilities` does not show `email.read: true`, do not inspect email. Ask the user to enable Email read in the Claude Agent settings.
## Memory
- `GET /api/codex/memory` — list memories for the token owner.
- `POST /api/codex/memory` — body `{"text": "...", "category": "fact", "source": "user", "session_id": null}`. Requires `memory:write`.
- `DELETE /api/codex/memory/{memory_id}` — remove a memory entry. Requires `memory:write`.
```bash
python3 ~/.claude/skills/odysseus/scripts/odysseus_api.py GET /api/codex/memory
python3 ~/.claude/skills/odysseus/scripts/odysseus_api.py POST /api/codex/memory '{"text":"User prefers SI units","category":"preference"}'
```
## Calendar
- `GET /api/codex/calendar/events?start=ISO&end=ISO` — list events in window.
- `POST /api/codex/calendar/events` — body matches `EventCreate` (`summary`, `dtstart`, `dtend`, `all_day`, `description`, `location`, `calendar_href`, `rrule`, `color`). Requires `calendar:write`.
- `DELETE /api/codex/calendar/events/{uid}` — delete event by uid (the value returned in the POST response). Requires `calendar:write`.
## Documents
- `GET /api/codex/documents?search=...&limit=50` — paginated library.
- `GET /api/codex/documents/{doc_id}` — fetch one document.
- `POST /api/codex/documents` — body `{"session_id": "...", "title": "...", "content": "...", "language": "markdown"}`. Requires `documents:write`.
- `DELETE /api/codex/documents/{doc_id}` — delete a document. Requires `documents:write`.
## Email draft + send
- `POST /api/codex/emails/draft` — body matches `SendEmailRequest` (`to`, `cc`, `bcc`, `subject`, `body`, `body_html`, `attachments`, `account_id`, `in_reply_to`, `references`). Requires `email:draft` (or `email:send`).
- `POST /api/codex/emails/send` — same body. Requires `email:send`. Never send without explicit user instruction.
## Forbidden Bypass Pattern
If you are about to reach the Odysseus host/container, import app internals, query the database, or call MCP helper modules directly, stop. Those paths bypass Odysseus Settings and token scopes. Ask the user to enable the relevant Claude Agent tool toggle instead.
+122
View File
@@ -0,0 +1,122 @@
#!/usr/bin/env python3
"""Small Odysseus scoped API helper for Codex terminal sessions."""
from __future__ import annotations
import json
import os
import sys
import urllib.error
import urllib.request
def _usage() -> int:
print("usage:", file=sys.stderr)
print(" odysseus_api.py capabilities", file=sys.stderr)
print(" odysseus_api.py todos list", file=sys.stderr)
print(" odysseus_api.py todos add TITLE", file=sys.stderr)
print(" odysseus_api.py emails list [limit]", file=sys.stderr)
print(" odysseus_api.py emails read UID", file=sys.stderr)
print(" odysseus_api.py METHOD /api/codex/path [json-body]", file=sys.stderr)
return 2
def _config() -> tuple[str, str] | None:
base_url = os.environ.get("ODYSSEUS_URL", "").strip().rstrip("/")
token = os.environ.get("ODYSSEUS_API_TOKEN", "").strip()
missing = []
if not base_url:
missing.append("ODYSSEUS_URL")
if not token:
missing.append("ODYSSEUS_API_TOKEN")
if missing:
print(f"missing {', '.join(missing)}; create a Codex Agent token in Odysseus Settings", file=sys.stderr)
return None
return base_url, token
def main() -> int:
if len(sys.argv) < 2:
return _usage()
command = sys.argv[1].lower()
if command == "capabilities":
method = "GET"
path = "/api/codex/capabilities"
body = None
elif command == "todos":
if len(sys.argv) < 3:
return _usage()
action = sys.argv[2].lower()
path = "/api/codex/todos"
if action == "list":
method = "GET"
body = None
elif action == "add" and len(sys.argv) >= 4:
method = "POST"
body = json.dumps({"action": "add", "title": " ".join(sys.argv[3:])})
else:
return _usage()
elif command == "emails":
if len(sys.argv) < 3:
return _usage()
action = sys.argv[2].lower()
if action == "list":
method = "GET"
limit = sys.argv[3] if len(sys.argv) >= 4 else "10"
path = f"/api/codex/emails?folder=INBOX&limit={limit}&offset=0&filter=all"
body = None
elif action == "read" and len(sys.argv) >= 4:
method = "GET"
path = f"/api/codex/emails/{sys.argv[3]}"
body = None
else:
return _usage()
else:
if len(sys.argv) < 3:
return _usage()
method = sys.argv[1].upper()
path = sys.argv[2]
body = sys.argv[3] if len(sys.argv) > 3 else None
if not path.startswith("/"):
path = "/" + path
if not path.startswith("/api/codex/"):
print("refusing non-/api/codex path; use scoped Odysseus integration endpoints only", file=sys.stderr)
return 2
config = _config()
if config is None:
return 2
base_url, token = config
data = None
headers = {
"Accept": "application/json",
"Authorization": f"Bearer {token}",
}
if body is not None:
try:
parsed = json.loads(body)
except json.JSONDecodeError as exc:
print(f"invalid json body: {exc}", file=sys.stderr)
return 2
data = json.dumps(parsed).encode("utf-8")
headers["Content-Type"] = "application/json"
req = urllib.request.Request(base_url + path, data=data, headers=headers, method=method)
try:
with urllib.request.urlopen(req, timeout=20) as resp:
print(resp.read().decode("utf-8"))
return 0
except urllib.error.HTTPError as exc:
text = exc.read().decode("utf-8", errors="replace")
print(text or f"HTTP {exc.code}", file=sys.stderr)
return 1
except OSError as exc:
print(f"request failed: {exc}", file=sys.stderr)
return 1
if __name__ == "__main__":
raise SystemExit(main())