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.
90%
Does it follow best practices?
Run evals on this skill
Adds up to 20 points to the overall score
Low
Low-risk findings worth noting
"""Send one chat message through the NanoClaw host and return its own result (#370).
The script hands the text to the host exactly as the `send_message` tool
does: a `message` request in `/workspace/ipc/messages/`. It then waits for
the host's reply in this session's input directory. `{"stdout": ...}` is a
send the host confirmed; anything else, or no reply in time, raises
SendError. The notice then stays pending and its journal re-offers it later.
The model takes no part in delivery.
"""
from __future__ import annotations
import json
import os
import sys
import time
import uuid
from collections.abc import Callable
from datetime import UTC, datetime
from pathlib import Path
IPC_ROOT = Path("/workspace/ipc")
REPLY_TIMEOUT_SECONDS = 15.0
MAX_TEXT_CHARS = 4000
class SendError(Exception):
"""The host did not confirm the send."""
def send_message(
text: str,
*,
root: Path = IPC_ROOT,
timeout: float = REPLY_TIMEOUT_SECONDS,
monotonic: Callable[[], float] = time.monotonic,
sleep: Callable[[float], None] = time.sleep,
) -> None:
chat, folder = os.environ.get("NANOCLAW_CHAT_JID"), os.environ.get("NANOCLAW_GROUP_FOLDER")
if not chat or not folder:
raise SendError("NANOCLAW_CHAT_JID/NANOCLAW_GROUP_FOLDER are unset; run inside a chat")
if not text.strip() or len(text) > MAX_TEXT_CHARS:
raise SendError(f"a notice must be 1..{MAX_TEXT_CHARS} characters")
request_id = uuid.uuid4().hex
request = {
"type": "message",
"chatJid": chat,
"groupFolder": folder,
"text": text,
"requestId": request_id,
"sessionName": os.environ.get("NANOCLAW_SESSION_NAME", "default"),
"timestamp": datetime.now(UTC).isoformat(),
}
outbox = root / "messages"
temporary = outbox / f".flight-{request_id}.tmp"
# The message holds private chat text: create it exclusively at 0600 and
# never leave a temporary copy behind.
try:
handle = os.open(temporary, os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o600)
with os.fdopen(handle, "w", encoding="utf-8") as stream:
stream.write(json.dumps(request, ensure_ascii=False))
temporary.rename(outbox / f"flight-{request_id}.json")
except OSError as error:
temporary.unlink(missing_ok=True)
raise SendError(f"cannot queue the message for the host: {error}") from None
reply = root / "input" / f"_script_result_{request_id}.json"
deadline = monotonic() + timeout
while monotonic() < deadline:
try:
result = json.loads(reply.read_text(encoding="utf-8"))
except FileNotFoundError:
sleep(0.25)
continue
except ValueError:
sleep(0.25) # the host may still be writing it; read again
continue
except OSError as error:
raise SendError(f"unreadable host reply: {error}") from None
# The reply decides the outcome; cleaning it up never does.
try:
reply.unlink()
except OSError as error:
print(f"host_send: reply left for the host sweep ({error.strerror})", file=sys.stderr)
if isinstance(result, dict) and "stdout" in result:
return
raise SendError(f"the host refused the send: {str(result)[:300]}")
raise SendError(f"no host reply within {timeout:.0f}s; the send is unconfirmed").tessl-plugin
rules
skills
check-travel-bookings
drive-engine
expertflyer
flight-assist
references
nightly-travel-sync
sync-tripit