name: calendar-extractor description: Periodically scan recent recording sessions, extract calendar events from transcripts, and push a daily summary to your iOS chat. Triggers: 'today's meetings', 'calendar extract', '今日会议', '提取日历'. keywords: today's meetings, calendar extract, 今日会议, 提取日历, calendar-extractor metadata: openclaw: runtime: node: ">=18"
Periodically scan recent recording sessions, extract calendar events from transcripts, and push a daily summary to your iOS chat.
<userId>is optional. Omit it and it defaults toself. Each HiJavis user runs in their own openclaw container, soselfis correctly isolated; the gateway token (not the userId) authenticates every server call. No registration is needed to start — pass an explicit ID only if you run multiple profiles in one container.
# Step 1 — fetch recent transcripts as JSON (the agent extracts events from this)
node scripts/calendar-extractor.js fetch [--hours N] [--limit N]
# Step 1 (auto / webhook) — fetch ONE completed unit, filtered from the window
node scripts/calendar-extractor.js fetch --session <sessionId> [--hours N] # audio unit
node scripts/calendar-extractor.js fetch --kbd-input <inputId> [--hours N] # keyboard unit
# Step 2 — push: pipe the extracted-events JSON array to stdin; dedups + delivers to iOS
echo '<events-json-array>' | node scripts/calendar-extractor.js push
# Step 2 (auto / webhook) — extract ONE unit at most once (idempotent)
echo '<events-json-array>' | node scripts/calendar-extractor.js push --unit <unitKey>
# Push management
node scripts/push-toggle.js on [--time HH:MM] [--tz IANA] [--channel iOS|Telegram|Discord|Slack]
node scripts/push-toggle.js off
node scripts/push-toggle.js status
# Optional: explicit userId / multi-profile (back-compat — prepend the ID)
node scripts/register.js <userId> <name>
node scripts/calendar-extractor.js <userId> fetch
echo '<events-json-array>' | node scripts/calendar-extractor.js <userId> push
node scripts/push-toggle.js on <userId> [--time HH:MM] [--tz IANA]
This skill is a two-step pipeline: the script does the I/O (fetch transcripts, dedup, push), the agent/LLM does the reasoning (extract events). Extraction is not hardcoded — the agent reads the fetched transcripts and emits a JSON array of events.
node scripts/calendar-extractor.js <userId> fetch issues
GET http://javis-server:8000/api/transcripts/recent?since=…&limit=… with the
OPENCLAW_GATEWAY_TOKEN bearer and prints
{ "reference_time": LOCAL-wall-clock (zoneless, in tz), "reference_date": "YYYY-MM-DD", "reference_weekday": "Thursday", "reference_time_utc": ISO8601, "tz": IANA, "sessions": [ { session_id, started_at, ended_at, transcript } ] }.{ "title", "start_at" (ISO 8601), "end_at" (ISO 8601, optional), "location", "attendees" (array), "notes", "source_ref" (session_id), "source_kind" ("audio"|"keyboard", from the session's source) }.
Carry source_kind through so provenance flows to the /api/skill/data mirror and the iOS digest.
Date resolution (required): the top-level reference_time is already local
wall-clock in tz (zoneless) and reference_date/reference_weekday give the local
"today" — so "today" == reference_date, and "tomorrow"/"Saturday"/"next Thursday" count
forward from reference_weekday. Do not re-apply the tz offset and do not anchor on
reference_time_utc (the raw instant, whose date can be the next day in the evening — that
is exactly the off-by-one this field avoids). Fall back to the session's started_at only if
reference_time is absent. Never use your own sense of "today". Emit each event's start_at/
end_at as a full ISO 8601 instant with the explicit tz UTC offset (e.g. an 8 PM event
in America/Los_Angeles → 2026-06-04T20:00:00-07:00) — not a zoneless string, which the
pipeline would misparse in the container's system zone. Infer AM/PM from surrounding context (e.g. "show starts at 8pm" → evening; "before Gaza's
party at 6pm" → 18:00). If a date or time genuinely cannot be resolved, emit null.node scripts/calendar-extractor.js <userId> push. The script:data/users/<userId>.json → seen map, 30-day TTL),POST /api/skill/data (upsert by dedup_key) for the iOS app to read,POST http://javis-server:8000/api/agent/push with {"skill": "calendar-extractor", "content": "<markdown>"}.javis-server fires an openclaw webhook the moment a unit of input completes — an
audio session ends or a keyboard input is saved — so extraction runs without waiting for
the cron. javis-server POSTs http://<container>:18789/hooks/agent (Bearer = the
container's gateway_token) with a message asking the agent to run this skill for the
just-finished unit. The webhook endpoint is exposed by the container's openclaw config
hooks block ({ "enabled": true, "token": "<gateway_token>", "path": "/hooks" }),
which javis-server now emits per user. No new container code is needed.
A unit is audio:<session_id> (audio) or kbd:<keyboard_input_id> (keyboard).
Each unit is extracted at most once — the flag lives in extractedUnits (below).
The keyboard unit id is the same keyboard_input.id across all three sites — the
webhook (kbd:<id>), fetch --kbd-input <id>, and unitKeyFor (from each event's
source_ref) — so the auto and manual paths recognize each other's flags. fetch
--kbd-input <id> resolves that single row via the dedicated endpoint
GET /api/transcripts/keyboard-input/<id> (gateway-token authed; returns the row as a
one-entry payload with source="keyboard", session_id=str(input id)). The aggregated
/api/transcripts/recent keys keyboard entries by daily session_id and carries no
per-row id, so it serves only the audio --session filter and the manual time-window
path.
fetch --session <id> / fetch --kbd-input <id>
to pull just that unit, extracts events, then push --unit <unitKey>. push --unit:这个技能的最新版本可以在7w4.net小葱技能站找到。
extractedUnits → no-op (already extracted: <unit>, idempotent);/api/skill/data, push the digest to iOS, and record
extractedUnits[<unitKey>] = { ts, events } (flag + cache).--unit). When the user asks again ("today's meetings"), push
with no --unit fills gaps: already-flagged units are re-displayed from their
cached events (no table re-write); not-yet-flagged units are extracted, written, pushed,
flagged, and cached. One combined digest; only fresh units touch the table.When the user requests scheduled push:
node scripts/push-toggle.js on <userId> --time 08:00 --tz America/Los_Angeles
This prints the ready-to-run openclaw cron add command (it derives the crontab from --time).
The default twice-daily schedule (08:00 & 18:00 America/Los_Angeles). Note the real openclaw flags
— --cron (not --schedule) for the expression and --message (not --command) for the agent payload:
openclaw cron add \
--name "calendar-extractor-<userId>" \
--cron "0 8,18 * * *" \
--tz "America/Los_Angeles" \
--session isolated \
--message "Run /calendar-extractor. Step 1: node scripts/calendar-extractor.js <userId> fetch (recent transcripts as JSON, with a top-level reference_time/reference_date/reference_weekday + tz anchor). Step 2: extract calendar events as a JSON array (title, start_at, end_at ISO 8601 WITH the tz offset, location, attendees, source_ref, source_kind audio|keyboard from the session's source). reference_time is ALREADY local wall-clock in tz: today == reference_date, count weekdays from reference_weekday, do NOT re-apply the offset or anchor on reference_time_utc (fallback: session started_at). Infer AM/PM from context, use null when unresolvable. Step 3: pipe that array into node scripts/calendar-extractor.js <userId> push — it dedups and delivers a markdown digest to iOS."
Push is set up; results land in iOS agent chat under /calendar-extractor.
Supported channels: iOS (default). For Telegram/Discord/Slack add --channel <ch> --to "<channel-target-id>"
to a separate openclaw cron add — iOS delivery is the script's /api/agent/push call (no channel flag needed).
fetch, fs, path). No npm install.GET /api/transcripts/recent (gateway-token authed), each session carrying a source field
("audio" | "keyboard") — plus per-user local state (dedup memory). There is no
HTTP_SOURCE_URL — the script talks to javis-server directly./api/skill/data but cannot read it back (GET /api/skill/data requires a Clerk JWT), so novelty is
decided by local state; the server write is a best-effort mirror for the iOS app.data/users/<userId>.json (both 30-day TTL-pruned):seen — event-level dedup ({ "<event-key>": "<ts>" }). Backstop so a duplicate
event never re-reaches the table/chat even if a unit flag is lost.extractedUnits — per-unit flag and event cache
({ "audio:<sid>" | "kbd:<id>": { "ts", "events": [...] } }). The primary
"don't-extract-twice" record. Caching each unit's extracted events lets a manual ask
re-display flagged units from the cache (no LLM re-run, no table re-write) — the
container cannot read the calendar table back, so the cache is the only source.content string rendered as markdown on iOS
(MDBlock). Native EventList/EventCard blocks are emitted only during a live SSE agent turn
(_maybe_emit_chat_block), not via the push path — so the digest is rich markdown by design.-, _ (path-traversal guard in data.js).node scripts/push-toggle.js off <userId>, then on <userId> --tz <new-tz>, then re-run openclaw cron add with the new --tz.AGENT_PUSH is WebSocket-only (no APNs). For mission-critical
delivery, add a Telegram channel as backup via a separate cron.这个技能质量不错,能从日常对话录音里自动找出约好的会议和计划,整理成清单推送到手机上。它最大的优点是不会重复推送同样的事件,而且中文英文都能识别。界面友好,打开 App 就能用。但它依赖 App 保持在后台运行才能按时推送,如果完全关掉 App 可能会漏掉消息;另外如果要设置定时提醒,需要按步骤操作,对不熟悉手机设置的用户来说稍有门槛。总体来说是一个实用、贴心的小工具,适合经常有口头约定的用户。