Personal entertainment-media skills for NanoClaw: Trakt watch-history sync, TV-show and audiobook recommendations, watchlist release checks, YouTube channel-comment digests, and Audible backup — with a weekly cadence companion. NanoClaw per-chat overlay tile.
73
92%
Does it follow best practices?
Impact
—
No eval scenarios have been run
Risky
Do not use without reviewing
#!/usr/bin/env python3
"""Precheck for `tessl__entertainment-sync`.
Filesystem cadence cap (weekly cron, sub-weekly cap). Mirrors the slice-3/4/5/7/8
contracts: read `<state_dir>/entertainment-sync-cursor.json`; wake when
the cursor is missing, malformed, schema-mismatched, or older than
CADENCE; otherwise emit `wake_agent: false`.
A per-source delta gate (Trakt API delta, Audible API delta) was
rejected — the inner skills already gate, and a precheck-time
multi-API call is the failure-mode-leak case slice 1 documented.
See `references/cadence-rationale.md`.
"""
from __future__ import annotations
import json
import os
import sys
from datetime import datetime, timedelta, timezone
from pathlib import Path
# 6d, not the 168h weekly cron interval: the cursor stamps at run completion,
# so a 168h cap perpetually near-misses (next same-time fire age ~167.8h < 168h)
# and skips forever. The slack absorbs run latency + DST. nanoclaw-admin#353.
CADENCE = timedelta(days=6)
DEFAULT_CURSOR_PATH = "/workspace/group/state/entertainment-sync-cursor.json"
SUPPORTED_SCHEMA_VERSION = 1
def _parse_iso(value: str) -> datetime | None:
try:
normalised = value[:-1] + "+00:00" if value.endswith("Z") else value
return datetime.fromisoformat(normalised)
except (TypeError, ValueError):
return None
def _read_cursor(path: Path) -> tuple[str | None, str | None]:
try:
text = path.read_text(encoding="utf-8")
except FileNotFoundError:
return None, None
except OSError as exc:
return None, f"cursor read failed: {exc}"
except UnicodeDecodeError as exc:
return None, f"cursor not valid UTF-8: {exc}"
if not text.strip():
return None, None
try:
data = json.loads(text)
except json.JSONDecodeError as exc:
return None, f"cursor JSON malformed: {exc}"
if not isinstance(data, dict):
return None, f"cursor root is {type(data).__name__}; expected object"
schema = data.get("schema_version")
if schema != SUPPORTED_SCHEMA_VERSION:
return None, f"cursor schema_version={schema!r}; expected {SUPPORTED_SCHEMA_VERSION}"
last_run = data.get("last_run")
if not isinstance(last_run, str):
return None, f"cursor last_run is {type(last_run).__name__}; expected string"
return last_run, None
def decide(now_utc: datetime, cursor_path: Path) -> dict:
last_run_iso, err = _read_cursor(cursor_path)
if err is not None:
return {
"wake_agent": True,
"data": {"reason": "cursor_error", "error": err, "cursor_path": str(cursor_path)},
}
if last_run_iso is None:
return {
"wake_agent": True,
"data": {"reason": "no_cursor", "cursor_path": str(cursor_path)},
}
last_run = _parse_iso(last_run_iso)
if last_run is None:
return {
"wake_agent": True,
"data": {
"reason": "cursor_unparseable",
"last_run_raw": last_run_iso,
"cursor_path": str(cursor_path),
},
}
if last_run.tzinfo is None:
return {
"wake_agent": True,
"data": {
"reason": "cursor_naive_datetime",
"last_run_raw": last_run_iso,
"cursor_path": str(cursor_path),
},
}
age = now_utc - last_run
age_hours = age.total_seconds() / 3600.0
if age < timedelta(0):
return {
"wake_agent": True,
"data": {
"reason": "cursor_future",
"last_run": last_run_iso,
"age_hours": round(age_hours, 2),
},
}
if age >= CADENCE:
return {
"wake_agent": True,
"data": {
"reason": "cadence_elapsed",
"last_run": last_run_iso,
"age_hours": round(age_hours, 2),
"cadence_hours": CADENCE.total_seconds() / 3600.0,
},
}
return {
"wake_agent": False,
"data": {
"reason": "within_cadence",
"last_run": last_run_iso,
"age_hours": round(age_hours, 2),
"cadence_hours": CADENCE.total_seconds() / 3600.0,
},
}
def main() -> int:
cursor_path = Path(os.environ.get("ENTERTAINMENT_SYNC_CURSOR", DEFAULT_CURSOR_PATH))
now_utc = datetime.now(timezone.utc)
payload = decide(now_utc, cursor_path)
sys.stdout.write(json.dumps(payload) + "\n")
return 0
if __name__ == "__main__":
sys.exit(main())skills
audible-backup
scripts
check-watchlist
entertainment-sync
recommend-books
recommend-shows
trakt-watch-history
youtube-comment-check