CtrlK
BlogDocsLog inGet started
Tessl Logo

jbaruch/nanoclaw-travel

Travel assistant for NanoClaw: byAir flight notifications (delay, gate, connection risk, inbound aircraft delay, time-to-leave, arrival logistics), traffic-aware drive planning for in-person meetings (auto drive blocks + leave-by traffic rechecks), travel-booking gap checks, and nightly TripIt sync. Per-chat overlay plugin.

Quality

Does it follow best practices?

Run evals on this skill

Adds up to 20 points to the overall score

View guide

SecuritybySnyk

Low

Low-risk findings worth noting

Overview
Quality
Evals
Security
Files

skip_drive.pyskills/drive-engine/

"""Skip a meeting's drive: delete its blocks now and record a skip so the sweep
won't recreate them.

The operator triggers this by replying "skip <n>" to a drive-added notification.
The agent maps the local index to the meeting NAME (never an internal id — the
message only ever showed the name) and invokes this with that summary. Resolution
is by summary, position-immune: a unique match is skipped; several meetings that
share a name are handed back as candidates for the agent to disambiguate
conversationally.

Only MEETING drives are skippable — an airport drive to/from a flight is never
offered, so this never matches one (it filters to `meeting_*` blocks).

The skip suppresses the whole meeting (both legs) via `add_skip(meeting_id)`;
the unified block's identity IS the meeting id (`meeting_source` sets
`identity=meeting.meeting_id`), which is the `skip_state` key `scan` honors.

CLI: `python3 skip_drive.py '<json-request>'` where the request is
`{"summary": "Massage"}`. The skip is timestamped by the process clock (a live
skip happens "now"), so the request carries no time field. Always prints a JSON
result to stdout (never a bare traceback — the skill parses stdout):
  {"skipped": true, "meeting": "Massage", "removed": 2}
  {"skipped": false, "unmatched": "Massage"}
  {"skipped": false, "ambiguous": "Swimming Practice", "candidates": [{"when": ...}, ...]}
  {"skipped": false, "error": "<Type>: <message>"}   # operational failure

Exit codes: 0 = a result was produced (including unmatched / ambiguous — the
script ran fine, the meeting just wasn't uniquely resolved); 1 = an operational
failure (a gateway/tier problem, transport error, skip-store write failure) — the
`error` shape above; 2 = a caller/usage error (missing or non-JSON argument).
"""

from __future__ import annotations

import json
import sys
from dataclasses import dataclass
from datetime import datetime, timedelta, timezone
from pathlib import Path

_BUNDLE_DIR = Path(__file__).resolve().parent
if str(_BUNDLE_DIR) not in sys.path:
    sys.path.insert(0, str(_BUNDLE_DIR))

from block_codec import parse_block  # noqa: E402
from skip_state import add_skip  # noqa: E402

# A skip must outlast the meeting so no sweep between now and the meeting's end
# recreates the block; once the meeting is past, `scan` filters it anyway. This
# pad over the latest leg anchor (meeting end for a round trip) covers a long
# meeting without meaningfully over-suppressing (a past meeting is filtered).
_SKIP_EXPIRY_PAD = timedelta(hours=4)
_DRIVE_SUMMARY_PREFIX = "Drive: "


def _on_path(name: str) -> None:
    runtime = Path(f"/home/node/.claude/skills/tessl__{name}")
    target = runtime if runtime.is_dir() else _BUNDLE_DIR.parent / name
    if str(target) not in sys.path:
        sys.path.insert(0, str(target))


@dataclass(frozen=True)
class SkipTarget:
    identity: str
    event_ids: tuple[str, ...]
    expires: datetime


def _event_when(event: dict) -> str | None:
    """A human local time for a candidate event, from its (offset-carrying)
    start — so the agent can tell same-named meetings apart when disambiguating."""
    start = event.get("start")
    raw = start.get("dateTime") if isinstance(start, dict) else None
    if not isinstance(raw, str):
        return None
    try:
        dt = datetime.fromisoformat(raw)
    except ValueError:
        return None
    return dt.strftime("%a %b %d, %H:%M")


def resolve_skip(
    events: list, *, summary: str, now: datetime
) -> tuple[SkipTarget | None, list[dict]]:
    """Find the meeting to skip by summary among current Drive blocks.

    Returns `(target, candidates)`: `target` is set only when exactly ONE meeting
    identity matches; when several same-named meetings match, `target` is None and
    `candidates` lists them (with a `when`) for the agent to disambiguate. No
    match returns `(None, [])`. Only unified `meeting_*` blocks are considered.
    """
    wanted = f"{_DRIVE_SUMMARY_PREFIX}{summary}"
    by_identity: dict[str, dict] = {}
    for event in events:
        if not isinstance(event, dict):
            continue
        event_summary = event.get("summary")
        if not (isinstance(event_summary, str) and event_summary.strip() == wanted):
            continue
        block = parse_block(event)
        if block is None or block.identity is None:
            continue
        if not (block.kind or "").startswith("meeting_"):
            continue
        slot = by_identity.setdefault(
            block.identity, {"event_ids": [], "anchors": [], "when": _event_when(event)}
        )
        if block.event_id:
            slot["event_ids"].append(block.event_id)
        if block.anchor is not None:
            slot["anchors"].append(block.anchor)

    if not by_identity:
        return None, []
    if len(by_identity) > 1:
        return None, [{"meeting": summary, "when": s["when"]} for s in by_identity.values()]

    identity, slot = next(iter(by_identity.items()))
    latest = max(slot["anchors"]) if slot["anchors"] else now
    return SkipTarget(identity, tuple(slot["event_ids"]), latest + _SKIP_EXPIRY_PAD), []


def _fetch_drive_events(calendar, now: datetime) -> list:
    _on_path("flight-assist")
    from calendar_reconcile import _find_events_args, _items

    raw = calendar.find_events(
        _find_events_args(
            calendar_id="primary",
            time_min=(now - timedelta(days=2)).isoformat(),
            time_max=(now + timedelta(days=21)).isoformat(),
        )
    )
    return _items(raw)


def skip_meeting_drive(request: dict, *, calendar=None, now: datetime | None = None) -> dict:
    """Resolve + apply a skip. See module docstring for the request/result shapes."""
    summary = request.get("summary")
    if not isinstance(summary, str) or not summary.strip():
        return {"skipped": False, "error": "request must name a meeting `summary`"}
    summary = summary.strip()
    if now is None:
        now = datetime.now(timezone.utc)

    _on_path("flight-assist")
    from google_calendar_client import GoogleCalendarClient, GoogleCalendarError

    if calendar is None:
        calendar = GoogleCalendarClient()

    events = _fetch_drive_events(calendar, now)
    target, candidates = resolve_skip(events, summary=summary, now=now)
    if target is None:
        if candidates:
            return {"skipped": False, "ambiguous": summary, "candidates": candidates}
        return {"skipped": False, "unmatched": summary}

    # Record the skip first so a sweep that races this delete still suppresses the
    # meeting (the block reappearing for one cycle is worse than a redundant skip).
    add_skip(target.identity, expires=target.expires, now=now)

    removed = 0
    for event_id in target.event_ids:
        try:
            calendar.delete_event({"calendar_id": "primary", "event_id": event_id})
        except GoogleCalendarError as exc:
            if getattr(exc, "status_code", None) != 404:  # 404 = already gone = done
                raise
        removed += 1
    return {"skipped": True, "meeting": summary, "removed": removed}


def _fail(message: str, code: int) -> int:
    """Emit the structured failure on stdout (skill contract) AND a concise
    diagnostic on stderr (standard script failure contract — an error stream for
    CI / operator logs to inspect), then return the exit code."""
    print(json.dumps({"skipped": False, "error": message}))
    print(f"skip_drive: {message}", file=sys.stderr)
    return code


def main(argv: list[str]) -> int:
    if len(argv) < 2:
        return _fail("usage: skip_drive.py '<json-request>'", 2)
    try:
        request = json.loads(argv[1])
    except json.JSONDecodeError as exc:
        return _fail(f"invalid JSON request: {exc}", 2)
    try:
        result = skip_meeting_drive(request)
    except Exception as exc:  # noqa: BLE001 — outer-boundary-process-contract
        # The skill invokes this as a subprocess and parses ONLY stdout JSON. An
        # uncaught exception (an unauthenticated gateway, a transport
        # error from the fetch / delete, a skip-store write failure) would emit a
        # Python traceback the skill can't read — it would look like "no result".
        # Emit the documented `{"skipped": false, "error": ...}` shape (plus a
        # stderr diagnostic) and a non-zero exit so the skill reports the failure
        # instead of hanging on unparseable output. KeyboardInterrupt /
        # SystemExit still propagate.
        return _fail(f"{type(exc).__name__}: {exc}", 1)
    print(json.dumps(result))
    return 0


if __name__ == "__main__":
    raise SystemExit(main(sys.argv))

README.md

tile.json