From 3abfa18182efb00a99d47f78759071ce3010eb18 Mon Sep 17 00:00:00 2001 From: Moritz Graf Date: Sun, 11 Oct 2026 15:07:35 +0200 Subject: [PATCH] feat(datastore): support --since in stop command, trigger website update from render_views, update docs --- skills/datastore/SKILL.md | 16 +++++++++++-- skills/datastore/scripts/datastore.py | 30 +++++++++++++++++++++--- skills/datastore/scripts/render_views.py | 14 +++++++++++ 3 files changed, 55 insertions(+), 5 deletions(-) diff --git a/skills/datastore/SKILL.md b/skills/datastore/SKILL.md index a81297c..94bb644 100644 --- a/skills/datastore/SKILL.md +++ b/skills/datastore/SKILL.md @@ -60,7 +60,9 @@ Closes the currently active open interval event (e.g. when Mila wakes up). - **Parameters:** - `kind` (string, required): `'sleep.session'`. - `subject` (string, required): `'mila'`. - - `end_time` (string, optional): ISO timestamp (defaults to current time). + - `end_time` (string, optional): ISO timestamp (e.g. `'2026-10-11T11:40:00+02:00'`). Defaults to current time if omitted. + - `since` (string, optional): Relative wake time offset backwards from now (e.g. `'10m'`, `'15m'`). + - `event_id` (string, optional): Specific open event ID to close. ### 3. `datastore_get_summary` Retrieves daily nutrition, sleep, and active status totals computed by DuckDB. @@ -113,10 +115,20 @@ uv run /home/node/mop-skills/mop-skills/skills/datastore/scripts/datastore.py ad --since "15m" \ --payload '{"type": "nap", "location": "bett"}' -# Stop Mila's nap (closes open interval) +# Stop Mila's nap (closes open interval at current time) uv run /home/node/mop-skills/mop-skills/skills/datastore/scripts/datastore.py stop sleep.session \ --subject mila +# Stop Mila's nap retroactively with specific wake time +uv run /home/node/mop-skills/mop-skills/skills/datastore/scripts/datastore.py stop sleep.session \ + --subject mila \ + --end "2026-10-11T11:40:00+02:00" + +# Stop Mila's nap retroactively with relative wake time (e.g. woke up 15 min ago) +uv run /home/node/mop-skills/mop-skills/skills/datastore/scripts/datastore.py stop sleep.session \ + --subject mila \ + --since "15m" + # Query daily summary uv run /home/node/mop-skills/mop-skills/skills/datastore/scripts/datastore.py summary --subject moritz diff --git a/skills/datastore/scripts/datastore.py b/skills/datastore/scripts/datastore.py index d06023f..119c5e5 100755 --- a/skills/datastore/scripts/datastore.py +++ b/skills/datastore/scripts/datastore.py @@ -594,10 +594,14 @@ class DatastoreEngine: kind: str, subject: str, end_time: Optional[str] = None, + since: Optional[str] = None, event_id: Optional[str] = None, actor: Optional[str] = None, ) -> Dict[str, Any]: """Finds the open event (end IS NULL) for kind and subject, and closes it.""" + if since and end_time: + raise ValueError("Cannot specify both 'since' and 'end_time'. Use 'since' for relative times or 'end_time' for absolute times.") + if event_id: row = self.con.execute( """ @@ -622,13 +626,22 @@ class DatastoreEngine: resolved_event_id, start_str = row[0], str(row[1]) now_dt = datetime.datetime.now(datetime.timezone.utc) - if end_time: + if since: + delta = parse_duration(since) + end_dt = (now_dt - delta).astimezone(BERLIN_TZ) + elif end_time: end_dt = datetime.datetime.fromisoformat(end_time) if end_dt.tzinfo is None: end_dt = end_dt.replace(tzinfo=BERLIN_TZ) else: end_dt = now_dt.astimezone(BERLIN_TZ) + start_dt = datetime.datetime.fromisoformat(start_str.replace("Z", "+00:00")) + if start_dt.tzinfo is None: + start_dt = start_dt.replace(tzinfo=BERLIN_TZ) + if end_dt < start_dt: + raise ValueError(f"End time {end_dt.isoformat()} cannot be earlier than start time {start_dt.isoformat()}") + end_iso = end_dt.isoformat() now_iso = now_dt.isoformat() @@ -985,6 +998,8 @@ def run_mcp_server(): "kind": {"type": "string", "description": "e.g. 'sleep.session'"}, "subject": {"type": "string", "description": "'mila', 'moritz', 'amelie'"}, "end_time": {"type": "string", "description": "ISO timestamp (optional, defaults to now)"}, + "since": {"type": "string", "description": "Relative end offset backwards from now (e.g. '5m', '15m')"}, + "event_id": {"type": "string", "description": "Specific open event ID to close (optional)"}, }, "required": ["kind", "subject"], }, @@ -1100,6 +1115,8 @@ def run_mcp_server(): kind=args["kind"], subject=args["subject"], end_time=args.get("end_time"), + since=args.get("since"), + event_id=args.get("event_id"), ) elif name == "datastore_get_summary": data = engine.get_summary(subject=args["subject"], date_str=args.get("date")) @@ -1177,7 +1194,8 @@ def run_cli(): p_stop.add_argument("--kind", dest="kind_flag", help="e.g. sleep.session") p_stop.add_argument("--subject", required=True, help="mila, moritz") p_stop.add_argument("--id", dest="event_id", help="Specific event ID to close") - p_stop.add_argument("--end", help="ISO end time (defaults to now)") + p_stop.add_argument("--end", dest="end_time", help="ISO end time (defaults to now)") + p_stop.add_argument("--since", help="Relative end offset backwards from now (e.g. '5m', '15m')") # Query p_query = subparsers.add_parser("query", help="Execute read-only SQL query") @@ -1226,7 +1244,13 @@ def run_cli(): kind = args.kind or args.kind_flag if not kind: parser.error("the 'stop' command requires 'kind' (positional or via --kind)") - res = engine.stop_event(kind=kind, subject=args.subject, end_time=args.end, event_id=getattr(args, "event_id", None)) + res = engine.stop_event( + kind=kind, + subject=args.subject, + end_time=getattr(args, "end_time", None), + since=getattr(args, "since", None), + event_id=getattr(args, "event_id", None), + ) print(json.dumps(res, indent=2, ensure_ascii=False)) elif args.command == "query": res = engine.query(args.sql) diff --git a/skills/datastore/scripts/render_views.py b/skills/datastore/scripts/render_views.py index fa0fe48..4dd94ca 100644 --- a/skills/datastore/scripts/render_views.py +++ b/skills/datastore/scripts/render_views.py @@ -579,6 +579,20 @@ def main(): if not args.no_html: render_html_dashboards(engine, html_dir) + # Automatically trigger update_website.py for live publishing to hosting + try: + update_script = Path(__file__).resolve().parent / "update_website.py" + if update_script.exists(): + import subprocess, shutil + uv_bin = shutil.which("uv") or "/home/node/.openclaw/bin/uv" + if Path(uv_bin).is_file(): + cmd = [str(uv_bin), "run", str(update_script)] + else: + cmd = [sys.executable, str(update_script)] + subprocess.run(cmd, check=False) + except Exception as e: + print(f"Warning: could not trigger update_website.py from render_views: {e}") + if __name__ == "__main__": main()