{"id":"841b43df-ddf2-423a-97c5-3aa3830bdcf8","entityType":"agent","slug":"clawhub-samuel-wei-calendar-extractor","name":"Calendar Extractor","canonicalUrl":"https://www.xpersona.co/agent/clawhub-samuel-wei-calendar-extractor","canonicalPath":"/agent/clawhub-samuel-wei-calendar-extractor","generatedAt":"2026-10-10T07:40:22.534Z","source":"CLAWHUB","claimStatus":"UNCLAIMED","verificationTier":"NONE","summary":{"evidence":{"source":"CLAWHUB","verified":false,"confidence":"medium","updatedAt":"2026-10-10T02:40:20.245Z","emptyReason":null},"description":"Periodically scans recent transcripts to extract calendar events and sends a daily summary of meetings to your iOS chat via push notifications.","descriptionLabel":"Source description","evidenceSummary":"Capability contract not published. No trust telemetry is available yet. 1.8K downloads reported by the source. Last updated 10/10/2026.","installCommand":"clawhub skill install s1735yg3x5h8qgtpapgt8es6f9844ese:calendar-extractor","sourceUrl":"https://clawhub.ai/samuel-wei/calendar-extractor","homepage":"https://clawhub.ai/samuel-wei/skills/calendar-extractor","primaryLinks":[{"label":"View on ClawHub","url":"https://clawhub.ai/samuel-wei/calendar-extractor","kind":"source"},{"label":"Homepage","url":"https://clawhub.ai/samuel-wei/skills/calendar-extractor","kind":"homepage"}],"safetyScore":84,"overallRank":62,"popularityScore":65,"trustScore":null,"claimedByName":null,"isOwner":false,"seoDescription":"Calendar Extractor technical dossier on Xpersona with agent coverage, OPENCLEW support, and live trust metadata."},"coverage":{"evidence":{"source":"public-profile","verified":false,"confidence":"medium","updatedAt":"2026-10-10T02:40:20.245Z","emptyReason":null},"protocols":[{"protocol":"OPENCLEW","label":"OpenClaw","status":"self-declared","notes":"Declared in the public agent profile."}],"capabilities":[],"verifiedCount":0,"selfDeclaredCount":1,"capabilityMatrix":{"rows":[{"key":"OPENCLEW","type":"protocol","support":"unknown","confidenceSource":"profile","notes":"Listed on profile"}],"flattenedTokens":"protocol:OPENCLEW|unknown|profile"}},"adoption":{"evidence":{"source":"CLAWHUB","verified":false,"confidence":"medium","updatedAt":"2026-10-10T02:40:20.245Z","emptyReason":null},"stars":null,"forks":null,"downloads":1765,"packageName":null,"latestVersion":"0.7.2","tractionLabel":"1.8K downloads"},"release":{"evidence":{"source":"CLAWHUB","verified":false,"confidence":"medium","updatedAt":"2026-10-10T02:40:20.245Z","emptyReason":null},"lastUpdatedAt":"2026-10-10T02:40:20.245Z","lastCrawledAt":"2026-10-10T02:40:20.245Z","lastIndexedAt":null,"nextCrawlAt":"2026-10-11T02:40:20.245Z","lastVerifiedAt":null,"highlights":[{"version":"0.7.2","createdAt":"2026-08-02T08:14:19.115Z","changelog":"calendar-extractor 0.7.2 - Added prompt-context fallback logic for dispatcher-invoked runs: when a single-unit fetch fails (e.g. empty/404 session), now falls back to extracting and pushing events using the transcript and reference date embedded in the prompt context, instead of emitting nothing. - Added documentation for the above behavior in SKILL.md (see dispatcher path exception). - Added docs/pr-drafts/2026-08-02-prompt-context-fallback.md. - Removed obsolete skill-card.md file.","fileCount":17,"zipByteSize":54512},{"version":"0.7.1","createdAt":"2026-07-25T07:11:34.773Z","changelog":"Emit absolute-instant (UTC Z) start/end times: interpret the LLM's zoneless wall-clock in the user's tz (DST-safe, container-TZ-independent), fixing the +15h shift for non-Pacific users; pairs with javis-server is_utc marker. Also fixes a dedup TTL time-bomb.","fileCount":16,"zipByteSize":49378},{"version":"0.7.0","createdAt":"2026-07-02T18:43:31.631Z","changelog":"- Dispatcher behavior updated: Skill is now invoked directly by the javis-server dispatcher on every completed unit (voice or keyboard), completely bypassing route matching and classifiers. The agent itself determines whether to act. - Updated SKILL.md to clarify auto-invocation logic and remove references to route/classifier-driven triggering. - No changes to core extraction or push functionality. - Removed the sample file: skill-card.md.","fileCount":16,"zipByteSize":46686},{"version":"0.6.0","createdAt":"2026-06-25T03:36:34.169Z","changelog":"Per-event lead_time (voice-call reminder offset) + ensure detail fields are populated in skill_data","fileCount":16,"zipByteSize":46516},{"version":"0.5.4","createdAt":"2026-06-23T06:13:40.586Z","changelog":"# calendar-extractor v0.5.4 - Removed the skill-card.md file. - No functional or behavioral changes to skill logic.","fileCount":16,"zipByteSize":43446},{"version":"0.5.3","createdAt":"2026-06-23T02:40:24.559Z","changelog":"No changes detected in this release. - Version bumped to 0.5.3 with no code or documentation changes. - No new features, fixes, or updates introduced.","fileCount":16,"zipByteSize":42106},{"version":"0.5.2","createdAt":"2026-06-23T02:12:15.898Z","changelog":"- No code or documentation changes detected in this release. - Version bump for maintenance; functionality and user experience remain unchanged.","fileCount":16,"zipByteSize":41481},{"version":"0.5.1","createdAt":"2026-06-23T01:06:56.977Z","changelog":"**Adds calendar event editing and anchor commands** - Added `anchor` command to print the current relative-date anchor (for resolving phrases like \"today\" or \"6 pm\" during event edits). - Added `update` command to patch and auto-confirm an individual calendar event card using its `dedup_key`. - Updated documentation to describe anchor and update workflows, covering event correction interactions. - Added a test for the update functionality (`test/update.test.js`). - Removed the separate skill info file (`skill-card.md`).","fileCount":16,"zipByteSize":39543}]},"execution":{"evidence":{"source":"CLAWHUB","verified":false,"confidence":"low","updatedAt":null,"emptyReason":"No published capability contract is available yet."},"installCommand":"clawhub skill install s1735yg3x5h8qgtpapgt8es6f9844ese:calendar-extractor","setupComplexity":"low","setupSteps":["Install using `clawhub skill install s1735yg3x5h8qgtpapgt8es6f9844ese:calendar-extractor` in an isolated environment before connecting it to live workloads.","No published capability contract is available yet, so validate auth and request/response behavior manually.","Review the upstream CLAWHUB listing at https://clawhub.ai/samuel-wei/calendar-extractor before using production credentials."],"contract":{"contractStatus":"missing","authModes":[],"requires":[],"forbidden":[],"supportsMcp":false,"supportsA2a":false,"supportsStreaming":false,"inputSchemaRef":null,"outputSchemaRef":null,"dataRegion":null,"contractUpdatedAt":null,"sourceUpdatedAt":null,"freshnessSeconds":null},"invocationGuide":{"preferredApi":{"snapshotUrl":"https://www.xpersona.co/api/v1/agents/clawhub-samuel-wei-calendar-extractor/snapshot","contractUrl":"https://www.xpersona.co/api/v1/agents/clawhub-samuel-wei-calendar-extractor/contract","trustUrl":"https://www.xpersona.co/api/v1/agents/clawhub-samuel-wei-calendar-extractor/trust"},"curlExamples":["curl -s \"https://www.xpersona.co/api/v1/agents/clawhub-samuel-wei-calendar-extractor/snapshot\"","curl -s \"https://www.xpersona.co/api/v1/agents/clawhub-samuel-wei-calendar-extractor/contract\"","curl -s \"https://www.xpersona.co/api/v1/agents/clawhub-samuel-wei-calendar-extractor/trust\""],"jsonRequestTemplate":{"query":"summarize this repo","constraints":{"maxLatencyMs":2000,"protocolPreference":["OPENCLEW"]}},"jsonResponseTemplate":{"ok":true,"result":{"summary":"...","confidence":0.9},"meta":{"source":"CLAWHUB","generatedAt":"2026-10-10T07:40:22.529Z"}},"retryPolicy":{"maxAttempts":3,"backoffMs":[500,1500,3500],"retryableConditions":["HTTP_429","HTTP_503","NETWORK_TIMEOUT"]}},"endpoints":{"dossierUrl":"https://www.xpersona.co/api/v1/agents/clawhub-samuel-wei-calendar-extractor/dossier","snapshotUrl":"https://www.xpersona.co/api/v1/agents/clawhub-samuel-wei-calendar-extractor/snapshot","contractUrl":"https://www.xpersona.co/api/v1/agents/clawhub-samuel-wei-calendar-extractor/contract","trustUrl":"https://www.xpersona.co/api/v1/agents/clawhub-samuel-wei-calendar-extractor/trust"}},"reliability":{"evidence":{"source":"runtime-metrics","verified":false,"confidence":"low","updatedAt":null,"emptyReason":"No trust, reliability, or runtime telemetry is available."},"trust":{"status":"unavailable","handshakeStatus":"UNKNOWN","verificationFreshnessHours":null,"reputationScore":null,"p95LatencyMs":null,"successRate30d":null,"fallbackRate":null,"attempts30d":null,"trustUpdatedAt":null,"trustConfidence":"unknown","sourceUpdatedAt":null,"freshnessSeconds":null},"decisionGuardrails":{"doNotUseIf":["Contract metadata is missing or unavailable for deterministic execution."],"safeUseWhen":[],"riskFlags":["missing_or_unavailable_contract","trust_data_unavailable","schema_references_missing"],"operationalConfidence":"low"},"executionMetrics":{"observedLatencyMsP50":null,"observedLatencyMsP95":null,"estimatedCostUsd":null,"uptime30d":null,"rateLimitRpm":null,"rateLimitBurst":null,"lastVerifiedAt":null,"verificationSource":null},"runtimeMetrics":{"successRate":null,"avgLatencyMs":null,"avgCostUsd":null,"hallucinationRate":null,"retryRate":null,"disputeRate":null,"p50Latency":null,"p95Latency":null,"lastUpdated":null}},"benchmarks":{"evidence":{"source":"no-benchmark-data","verified":false,"confidence":"low","updatedAt":null,"emptyReason":"No benchmark suites or observed failure patterns are available."},"suites":[],"failurePatterns":[]},"artifacts":{"evidence":{"source":"CLAWHUB","verified":false,"confidence":"medium","updatedAt":"2026-10-10T02:40:20.245Z","emptyReason":null},"readme":"Skill: Calendar Extractor\n\nOwner: samuel-wei\n\nSummary: Periodically scans recent transcripts to extract calendar events and sends a daily summary of meetings to your iOS chat via push notifications.\n\nTags: latest:0.7.2\n\nVersion history:\n\nv0.7.2 | 2026-08-02T08:14:19.115Z | user\n\ncalendar-extractor 0.7.2\n\n- Added prompt-context fallback logic for dispatcher-invoked runs: when a single-unit fetch fails (e.g. empty/404 session), now falls back to extracting and pushing events using the transcript and reference date embedded in the prompt context, instead of emitting nothing.\n- Added documentation for the above behavior in SKILL.md (see dispatcher path exception).\n- Added docs/pr-drafts/2026-08-02-prompt-context-fallback.md.\n- Removed obsolete skill-card.md file.\n\nv0.7.1 | 2026-07-25T07:11:34.773Z | user\n\nEmit absolute-instant (UTC Z) start/end times: interpret the LLM's zoneless wall-clock in the user's tz (DST-safe, container-TZ-independent), fixing the +15h shift for non-Pacific users; pairs with javis-server is_utc marker. Also fixes a dedup TTL time-bomb.\n\nv0.7.0 | 2026-07-02T18:43:31.631Z | user\n\n- Dispatcher behavior updated: Skill is now invoked directly by the javis-server dispatcher on every completed unit (voice or keyboard), completely bypassing route matching and classifiers. The agent itself determines whether to act.\n- Updated SKILL.md to clarify auto-invocation logic and remove references to route/classifier-driven triggering.\n- No changes to core extraction or push functionality.\n- Removed the sample file: skill-card.md.\n\nv0.6.0 | 2026-06-25T03:36:34.169Z | user\n\nPer-event lead_time (voice-call reminder offset) + ensure detail fields are populated in skill_data\n\nv0.5.4 | 2026-06-23T06:13:40.586Z | user\n\n# calendar-extractor v0.5.4\n\n- Removed the skill-card.md file.\n- No functional or behavioral changes to skill logic.\n\nv0.5.3 | 2026-06-23T02:40:24.559Z | user\n\nNo changes detected in this release.\n\n- Version bumped to 0.5.3 with no code or documentation changes.\n- No new features, fixes, or updates introduced.\n\nv0.5.2 | 2026-06-23T02:12:15.898Z | user\n\n- No code or documentation changes detected in this release.\n- Version bump for maintenance; functionality and user experience remain unchanged.\n\nv0.5.1 | 2026-06-23T01:06:56.977Z | user\n\n**Adds calendar event editing and anchor commands**\n\n- Added `anchor` command to print the current relative-date anchor (for resolving phrases like \"today\" or \"6 pm\" during event edits).\n- Added `update` command to patch and auto-confirm an individual calendar event card using its `dedup_key`.\n- Updated documentation to describe anchor and update workflows, covering event correction interactions.\n- Added a test for the update functionality (`test/update.test.js`).\n- Removed the separate skill info file (`skill-card.md`).\n\nv0.5.0 | 2026-06-22T20:11:41.956Z | user\n\ncalendar-extractor 0.5.0\n\n- Each extracted event is now pushed as its own markdown card, and each card lands in a separate Agent Chat thread on iOS (no longer a combined digest).\n- Updated documentation to describe the new per-event card delivery and thread routing.\n- Removed legacy skill-card.md file (documentation now fully in SKILL.md).\n\nv0.4.2 | 2026-06-09T01:58:06.218Z | user\n\n**Auto-run dispatcher support and event pending status.**\n\n- The skill now supports auto-run by the dispatcher when a completed unit matches the calendar route (no approve-to-run card).\n- Extracted calendar events are now written with status \"pending\" and only become solid after user confirmation in the iOS app.\n- Updated documentation to reflect auto-run flow, deliverable hints, and pending event workflows.\n- Added new test files for CLI, data, and library functionality.\n- Removed the skill-card.md file.\n\nv0.4.1 | 2026-06-08T22:11:54.067Z | user\n\n- Removed all test files and developer documentation, including plans, specs, and the skill card.\n- No changes to user-facing features or commands.\n- The core extraction and push workflow remains unchanged.\n\nv0.4.0 | 2026-06-08T22:05:12.171Z | user\n\ncalendar-extractor 0.4.0\n\n- Reduced and clarified SKILL.md instructions for a more concise description and easier onboarding.\n- Added metadata to support integration with the javis-server dispatcher and explicit `metadata.routes` for route registry.\n- Streamlined workflow: removed auto/webhook/cron/push-toggle details from the user manual; dispatcher/invocation details are now summarized under \"How this skill is invoked\".\n- Refined event extraction guidelines: improved edge case handling and clarified fallback behaviors for missing/ambiguous transcript data.\n- New reference/sample/test/data files added; push-toggle and old skill summary files removed.\n- Now defaults to last 24 hours of transcript data for \"today's meetings\" and more strictly relies on the `reference_date` field for local-day digest.\n\nv0.3.2 | 2026-06-05T06:25:31.860Z | user\n\n- Updated event extraction logic to use provided local wall-clock and weekday fields for accurate \"today\"/\"tomorrow\" resolution in transcripts.\n- Clarified that the reference time provided is zoneless and must not be re-anchored or re-interpreted with a timezone offset.\n- Events' start and end times must now be output as full ISO 8601 timestamps including explicit timezone offsets, not as zoneless strings.\n- Improved documentation to prevent off-by-one date errors and ensure accurate local date handling.\n- Removed unused file: skill-card.md.\n\nv0.3.1 | 2026-06-05T04:01:37.813Z | user\n\ncalendar-extractor 0.3.1\n\n- Added detailed internal documentation on architecture, timezone shift handling, and known TZ-shift bugs in the `docs/javis-dev/` directory.\n- Removed the `skill-card.md` file.\n- No user-facing behavior changes; this update focuses on design and developer documentation improvements.\n\nv0.3.0 | 2026-06-04T04:35:49.438Z | user\n\ncalendar-extractor v0.2.6\n\n- Added per-unit auto extraction: immediate webhook-driven extraction when a recording session or keyboard input completes, eliminating cron delay.\n- New fetch and push options allow targeting a single unit (`--session`, `--kbd-input`, `--unit`), enabling idempotent per-unit extraction and deduplication.\n- Manual and auto extraction paths now cooperate, with extraction state tracked per unit for reliable backfill and idempotence.\n- Added internal library and tests (`scripts/lib.js`, `test/cli.test.js`, `test/lib.test.js`) for improved code quality and coverage.\n- Removed redundant `skill-card.md` file.\n\nv0.2.5 | 2026-05-31T17:11:20.642Z | user\n\n- Added support for per-user push preferences (`data/users/self.prefs.json`).\n- Now handles both audio recording and keyboard-dictation sessions; each session includes a `source` field (`\"audio\"` or `\"keyboard\"`).\n- Each extracted event now carries a `source_kind` property for provenance, included in both the iOS digest and data mirror.\n- Improved documentation to clarify enhanced data flow, provenance, and supported session sources.\n- Removed the obsolete `skill-card.md` file.\n\nv0.2.4 | 2026-05-29T21:21:28.564Z | user\n\n- Added README.md with full usage instructions, workflow details, and command examples.\n- Improved documentation for setup, multi-profile support, and push notification configuration.\n- No code changes; this is a documentation-only release.\n\nv0.2.3 | 2026-05-29T06:14:29.308Z | user\n\n**Improved calendar extraction accuracy by adding context-aware date/time resolution.**\n\n- The fetch output now includes a top-level `reference_time` and `tz` (IANA timezone).\n- Extraction step requires resolving all relative date/time expressions (e.g., \"today\", \"Saturday\", \"noon\") against `reference_time` in the correct timezone.\n- Fallback: If `reference_time` is missing, use the session's `started_at`.\n- Instructions for agents clarify to infer AM/PM from textual hints, and to output `null` for truly unresolvable fields.\n- Push setup and workflow documentation updated for these changes. No code modifications detected.\n\nv0.2.2 | 2026-05-29T03:56:15.781Z | user\n\n- Default userId handling improved: commands now accept `<userId>` as optional and default to `self` for better isolation and simpler usage.\n- Registration is no longer required to start; explicit IDs are only needed for advanced multi-profile use.\n- Updated documentation to reflect streamlined command usage and improved guidance for single-user and advanced setups.\n- Maintained all core features and workflow; no behavioral or compatibility changes.\n\nv0.2.1 | 2026-05-28T23:27:43.340Z | user\n\n- Split the workflow into a clear two-step pipeline: transcript fetch (script) and event extraction (agent/LLM).\n- Updated `calendar-extractor.js` with new `fetch` and `push` commands for modular usage.\n- Removed support for external HTTP sources via `HTTP_SOURCE_URL`; now fetches transcripts directly from javis-server.\n- Improved push management: derived crontab schedules from user preferences, and clarified required openclaw CLI flags for cron setup.\n- Streamlined deduplication strategy—now solely local-state based, with best-effort mirroring to `/api/skill/data`.\n- Dropped native event card support for scheduled pushes; markdown is used for all notifications.\n- No external Node.js dependencies required beyond built-ins.\n\nv0.1.0 | 2026-05-27T03:41:48.637Z | user\n\nInitial release of calendar-extractor.\n\n- Scans recent recording session transcripts to extract calendar events.\n- Pushes a daily summary of meetings to iOS chat.\n- Supports trigger phrases in English and Chinese: \"today's meetings\", \"calendar extract\", \"今日会议\", \"提取日历\".\n- Allows user setup for scheduled daily push notifications via multiple channels (iOS, Telegram, Discord, Slack).\n- Provides CLI scripts for registration, daily summary execution, and push notification management.\n- Includes notes on data storage, configuration, and multi-timezone/multi-channel setup.\n\nArchive index:\n\nArchive v0.7.2: 17 files, 54512 bytes\n\nFiles: data/users/self.json (543b), data/users/self.prefs.json (115b), docs/pr-drafts/2026-08-02-prompt-context-fallback.md (6211b), package.json (905b), README.md (3781b), references/route-contract.md (4506b), scripts/calendar-extractor.js (34495b), scripts/data.js (2133b), scripts/lib.js (12254b), scripts/register.js (612b), skill-card.md (2312b), SKILL.md (21879b), test/cli.test.js (19987b), test/data.test.js (3033b), test/lib.test.js (13948b), test/update.test.js (20771b), _meta.json (137b)\n\nFile v0.7.2:SKILL.md\n\n---\nname: calendar-extractor\ndescription: Extract calendar events from recent recording and keyboard transcripts and push them to your iOS chat as one markdown card per event, each landing in its own Agent Chat thread. Use on demand when the user asks for \"today's meetings\" / \"calendar extract\" / \"今日会议\" / \"提取日历\", and fetch the last 24 hours of transcript data by default. The javis-server dispatcher also invokes this skill directly for every completed unit — no classifier, no route matching; this skill's own agent decides relevance itself, using this SKILL.md, and may use a deliverable hint passed in the run prompt alongside the transcript; extracted events are written PENDING and become solid only when the user taps Confirm in the iOS calendar table. If the user asks for \"today's meetings\", use the local day defined by the fetched reference_date field. Triggers: 'today's meetings', 'calendar extract', '今日会议', '提取日历'.\nkeywords: today's meetings, calendar extract, 今日会议, 提取日历, calendar-extractor\nmetadata:\n  openclaw:\n    runtime:\n      node: \">=18\"\n  routes:\n    - route_id: calendar\n      skill: calendar-extractor\n      matches: \"meetings, appointments, events, agenda, scheduling, dates/times mentioned\"\n      args_template: null\n      risk: low\n---\n\n# Calendar Extractor\n\n> Extract calendar events from recent recording and keyboard transcripts and push them to your iOS chat — one markdown card per event, each landing in its own Agent Chat thread. Fetch the last 24 hours of transcript data by default. If the user asks for \"today's meetings\", use the local day defined by the fetched reference_date field. Generate one digest per local day in the user's timezone, containing all events that occur on reference_date and any events explicitly mentioned as happening today.\n\n## When to use\n\n- \"today's meetings\"\n- \"calendar extract\"\n- \"今日会议\"\n- \"提取日历\"\n- Automatically, when the javis-server dispatcher invokes this skill directly after a completed voice/keyboard unit — no classifier, no route matching. This skill's own agent decides for itself, using this `SKILL.md`, whether the unit is worth acting on; if not, it does nothing (see \"How this skill is invoked\").\n\n## Core commands\n\n> **`<userId>` is optional.** Omit it and it defaults to `self`. Each HiJavis user\n> runs in their own openclaw container, so `self` is correctly isolated; the gateway\n> token (not the userId) authenticates every server call. No registration is needed\n> to start — pass an explicit ID only if you run multiple profiles in one container.\n\n```bash\n# Step 1 — fetch recent transcripts as JSON (the agent extracts events from this)\nnode scripts/calendar-extractor.js fetch [--hours N] [--limit N]\n\n# Step 1 (dispatcher run) — fetch ONE completed unit (the auto-run dispatcher unit)\nnode scripts/calendar-extractor.js fetch --session <sessionId> [--hours N]   # audio unit\nnode scripts/calendar-extractor.js fetch --kbd-input <inputId> [--hours N]   # keyboard unit\n\n# Step 2 — push: pipe the extracted-events JSON array to stdin; dedups (seen) + delivers to iOS\necho '<events-json-array>' | node scripts/calendar-extractor.js push\n\n# anchor — print the CURRENT relative-date anchor (resolve \"today / 6 pm / tomorrow\" on an edit turn)\nnode scripts/calendar-extractor.js anchor            # optional: --tz <IANA> for the card's calendar zone\n\n# update — edit ONE pushed card in place (verbatim dedup_key, full merged patch, auto-confirm)\necho '{\"dedup_key\":\"<verbatim>\",\"patch\":{...}}' | node scripts/calendar-extractor.js update\n\n# Optional: explicit userId / multi-profile (back-compat — prepend the ID)\nnode scripts/register.js <userId> <name>\nnode scripts/calendar-extractor.js <userId> fetch\necho '<events-json-array>' | node scripts/calendar-extractor.js <userId> push\nnode scripts/calendar-extractor.js <userId> anchor\necho '{\"dedup_key\":\"<verbatim>\",\"patch\":{...}}' | node scripts/calendar-extractor.js <userId> update\n```\n\n## Workflow\n\nThis skill is a two-step pipeline: the **script** does the I/O (fetch transcripts, dedup, push),\nthe **agent/LLM** does the reasoning (extract events). Extraction is not hardcoded — the agent\nreads the fetched transcripts and emits a JSON array of events.\n\n1. **Fetch** — `node scripts/calendar-extractor.js <userId> fetch` issues\n   `GET http://javis-server:8000/api/transcripts/recent?since=…&limit=…` with the\n   `OPENCLAW_GATEWAY_TOKEN` bearer and prints\n   `{ \"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 } ] }`. If fetch fails, returns invalid JSON, or yields zero sessions, output an empty events array and do not push anything; report the failure only if the user asked for a diagnostic. If the fetch response contains no sessions or the transcript text is empty, return `[]` and do not attempt to push a digest. **Exception — the dispatcher path with a `UNIT CONTEXT` block (see \"How this skill is invoked\"): a failed single-unit `fetch --session`/`fetch --kbd-input` (empty/404, surfaced as `fetch_error` in the envelope) does NOT mean \"emit nothing\" — fall back to the prompt-embedded transcript/reference_date/tz and STILL extract and push.** (The `fetch` here degrades to an empty-sessions envelope rather than aborting, so the run continues.)\n2. **Extract** — the agent reads that JSON and produces an events array. Each event:\n   `{ \"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), \"lead_time\" (minutes before start for the \"Javis calls you\" voice alert, optional — defaults to 10) }`.\n   Carry `source_kind` through so provenance flows to the `/api/skill/data` mirror and the iOS digest.\n   **`lead_time`** is the per-event minutes-before-start at which the proactive voice-call engine rings the user (`fire = start − lead_time`). Omit it for the standard 10-minute lead; emit an integer only when the transcript states a different desired heads-up (e.g. \"remind me an hour before the flight\" → `60`). Negative/invalid values fall back to 10. The detail fields (`location`, `attendees`, `notes`) double as the announcement context the in-call **Details** command speaks, so keep them populated when the transcript provides them.\n   **Date resolution (required):** the top-level `reference_time` is **already local\n   wall-clock in `tz`** (zoneless) and `reference_date`/`reference_weekday` give the local\n   \"today\" — so \"today\" == `reference_date`, and \"tomorrow\"/\"Saturday\"/\"next Thursday\" count\n   forward from `reference_weekday`. Do **not** re-apply the tz offset and do **not** anchor on\n   `reference_time_utc` (the raw instant, whose date can be the *next* day in the evening — that\n   is exactly the off-by-one this field avoids). Fall back to the session's `started_at` only if\n   `reference_time` is absent. Never use your own sense of \"today\". Emit each event's `start_at`/\n   `end_at` as a full ISO 8601 instant **with the explicit `tz` UTC offset** (e.g. an 8 PM event\n   in `America/Los_Angeles` → `2026-06-04T20:00:00-07:00`) — not a zoneless string, which the\n   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\n   party at 6pm\" → 18:00). If the transcript does not provide enough information to determine a unique date/time (for example, only \"next Friday\" with no weekday anchor or an ambiguous time like \"at 8\" without AM/PM), emit `null` for the unresolved field rather than guessing. When multiple plausible interpretations are equally supported by the transcript, prefer the most specific one; if two interpretations remain equally plausible, emit `null` for the unresolved field rather than guessing. If the transcript mentions recurring meetings, all-day meetings, or multi-day events, preserve them as a single event only when the recurrence is explicit; otherwise emit a single event with the best available start/end times and note the ambiguity in `notes`.\n3. **Push** — pipe the events array into `node scripts/calendar-extractor.js <userId> push`. The script:\n   - dedups against per-user local state (`data/users/<userId>.json` → `seen` map, 30-day TTL),\n   - best-effort mirrors the **new** events to `POST /api/skill/data` (upsert by `dedup_key`) **tagged `status: \"pending\"`** so the server stores them pending and the iOS calendar table shows them greyed/dashed with **Confirm · Discard** — an event becomes solid only when the user taps **Confirm** (Discard deletes it). Each mirrored row carries a top-level **`lead_time`** (minutes, default 10) so the server's voice-call adapter can schedule the proactive call at `start − lead_time`; the detail fields (`location`/`attendees`/`notes`) ride in `payload` as the in-call announcement context,\n   - delivers the **new** events **per card** — one `POST http://javis-server:8000/api/agent/push`\n     per event, each `{\"skill\": \"calendar-extractor\", \"content\": \"<markdown>\", \"dedup_key\": \"<event dedup_key>\"}`.\n     The server routes each push (carrying its `dedup_key`, no explicit `session_id`) into that card's\n     own Agent Chat session, so **every event lands in its own iOS chat thread** instead of one combined\n     digest. The pushes are informational — they are not a confirmation gate.\n\n## Editing a pushed card in-thread\n\nA user can correct a card by **replying in that card's own Agent Chat thread**\n(\"6 pm today\", \"location is Zoom\", \"add Alex\"). The reply edits **that exact row\nin place and confirms it** — no duplicate row, no Confirm tap. The agent drives it:\n\n1. **Read the injected `[CURRENT CARD]` block.** When an agent turn runs inside a\n   card thread, the server injects a `[CURRENT CARD]` block carrying the card's\n   **original `dedup_key` (verbatim)** plus its current fields (`title`,\n   `start_at`, `end_at`, `location`, `attendees`, `notes`, `lead_time`, `status`).\n   This is the source of truth for the row you are editing. Carry `lead_time`\n   forward in the merged patch (default 10 if the block omits it) so an edit never\n   silently resets the voice-call lead; set a new integer only if the user asks to\n   change the heads-up (\"ring me 30 min before\" → `lead_time: 30`).\n2. **Run `anchor` for a fresh \"now\".** `node scripts/calendar-extractor.js anchor`\n   prints `{ reference_time, reference_date, reference_weekday, reference_time_utc,\n   tz }` for the **current** clock. The chat happens *later* than extraction, so\n   the original fetch anchor is stale — resolve \"today / 6 pm / tomorrow\" against\n   this fresh anchor (same date-resolution discipline as extraction: anchor on\n   `reference_time`/`reference_date`, never your own \"today\").\n   - **The anchor's `tz` is the user's calendar zone.** `anchor` resolves it from\n     the server (the authoritative zone) — **not** the container's `TZ`, which is\n     empty/UTC in prod and would shift \"today\" forward a day west of UTC in the\n     evening. Pass `[CURRENT CARD]`'s `tz` as `--tz <IANA>` if it carries one;\n     otherwise a bare `anchor` already returns the correct zone. **Use the\n     `reference_date` it prints — do not compute \"today\" from a UTC clock.**\n3. **Resolve the correction; null-not-guess.** Resolve the user's change against\n   the anchor and emit a full ISO 8601 instant **with the explicit `tz` UTC\n   offset** (e.g. `2026-06-22T18:00:00-07:00`). If the change is ambiguous or\n   unresolvable (e.g. \"at 8\" with no AM/PM, no weekday anchor), **do not call\n   `update`** — ask a follow-up in-thread instead. Emit `null` rather than guess.\n4. **Merge into a FULL patch (wholesale-payload rule).** The server overwrites\n   `payload`/`start_at`/`end_at` **wholesale** on the matched row, so the `patch`\n   must carry the **complete intended state** — the current fields from\n   `[CURRENT CARD]` **merged with** the user's change, not just the changed field.\n   A time-only edit must still resend `title`/`location`/`attendees`/`notes`, or\n   they would be blanked.\n   - **Times you are NOT changing:** copy the `start_at`/`end_at` strings from\n     `[CURRENT CARD]` **verbatim**. Those are already naive-local wall-clock in the\n     card zone (no offset) and the script passes a zoneless `YYYY-MM-DDTHH:MM:SS`\n     value through unchanged — it will **not** re-interpret it in the runner's\n     zone. (A time you *are* changing must be a full offset-bearing instant per\n     step 3, so the script can collapse it correctly.)\n   - **Pass the card zone.** Include the card's `tz` (the value the `anchor` step\n     printed) as a top-level `tz` so the script collapses any offset-bearing\n     changed time against the user's calendar zone, not the container's process\n     zone (which is empty/UTC in prod, not the user's zone). If you omit `tz`,\n     `update` resolves the user's zone from the server itself — but passing the\n     anchor's `tz` is preferred (explicit, and avoids a second lookup).\n5. **Run `update` with the verbatim `dedup_key`.**\n\n   ```bash\n   echo '{\n     \"dedup_key\": \"<the [CURRENT CARD] dedup_key, VERBATIM>\",\n     \"tz\": \"America/Los_Angeles\",\n     \"patch\": {\n       \"title\": \"Design Review\", \"location\": \"Zoom\",\n       \"attendees\": [\"Sam\"], \"notes\": \"bring laptop\",\n       \"lead_time\": 10,\n       \"start_at\": \"2026-06-22T18:00:00-07:00\",\n       \"end_at\":   \"2026-06-22T19:00:00-07:00\"\n     }\n   }' | node scripts/calendar-extractor.js update\n   ```\n\n   The script POSTs **one** `/api/skill/data` upsert with that verbatim key and\n   `status: \"confirmed\"`. It does **NOT** recompute the key from the new time\n   (that would create a second row — the whole bug), writes `start_at`/`end_at`\n   as **naive-local** wall-clock (no `Z`/offset), does **no `seen`-dedup\n   filtering**, and does **not** call `/api/agent/push`.\n\n**Auto-confirm semantics.** Stating the corrected value in chat *is* the\nconfirmation: the upsert's `status: \"confirmed\"` flips the row `pending →\nconfirmed` **atomically with the field write** (strictly that direction —\nconfirmed never downgrades). The skill never calls a separate `/confirm`\nendpoint. The iOS card re-render and the live chat reply are the user feedback.\nIf `/api/skill/data` fails, the script reports the error (non-silent) — tell the\nuser the card couldn't be updated; do **not** claim success.\n\nThis path is **edit-only** of an existing card — creating new events from chat,\nre-opening a confirmed row, or deleting via chat (Discard covers delete) are out\nof scope.\n\n## How this skill is invoked\n\nThis skill has **two triggers** (dispatcher auto-run and manual).\n\n1. **Dispatcher auto-run (automatic).** When a unit of input completes (an audio\n   session ends or a keyboard input is saved), the javis-server **session dispatcher**\n   invokes this skill's agent directly, alongside every other enabled, `risk: low`\n   skill — no classifier, no route matching. The server claims run-once\n   (`DispatchSkillInvoked (user_id, unit_key, skill)`) and **AUTO-RUNS this skill\n   directly — there is no approve-to-run proposal card**. It runs in the user's\n   container with a prompt of the form `Run /calendar-extractor for <unit>.\n   Deliverable: …`, where the deliverable text is an **advisory HINT**, not a\n   classification — the agent may use it alongside the transcript, but should still\n   read the unit transcript for full detail (time, attendees), and **this skill's own\n   agent decides for itself, using this `SKILL.md`, whether the unit is worth acting\n   on; if not, it does nothing.** When it does act, it parses `<unit>`\n   (`audio:<session_id>` or `kbd:<keyboard_input.id>`), reads the embedded\n   **`UNIT CONTEXT`** block (below) for the transcript + `reference_date`/`tz` +\n   `source_ref`/`source_kind`, extracts events, and pushes them; `fetch --session <id>` /\n   `fetch --kbd-input <id>` is now an **optional enrichment** and a **failed fetch must\n   not abort the push**. **The human gate is not\n   running the skill — it is Confirm/Discard on the produced events:** every extracted\n   event is written **PENDING** to the calendar table (greyed/dashed with\n   **Confirm · Discard**) and becomes solid only when the user taps **Confirm**\n   (Discard deletes it). **The skill does not self-gate on Confirm/Discard** — the\n   server owns run-once (whether to invoke at all), while the skill's own agent owns\n   relevance (whether to act once invoked).\n\n   **`UNIT CONTEXT` block — prompt-embedded; use it directly.** The dispatcher\n   embeds the already-loaded unit in the run prompt, in a clearly delimited\n   `UNIT CONTEXT` section carrying `transcript`, `reference_date`, `tz`,\n   `source_ref`, and `source_kind`. **Use this embedded context as the source of\n   truth for extraction** — resolve relative times against its `reference_date`/\n   `tz`, and carry its `source_ref`/`source_kind` onto each extracted event\n   (`source_ref`/`source_kind` fields) so provenance flows through to the\n   `/api/skill/data` mirror. The `fetch` step is only an optional enrichment.\n   **If you still run `fetch --kbd-input <id>` / `fetch --session <id>` and it\n   returns empty/404** (the source row was dropped after dispatch — the envelope\n   carries `fetch_error` and zero sessions), **DO NOT abort and DO NOT emit an\n   empty array.** Fall back to the `UNIT CONTEXT` block and still extract + push,\n   so the event lands as a PENDING `/api/skill/data` row. Report the fetch miss\n   non-silently (mention it), but never let it suppress the write. For each\n   event's `source_ref`, use the `UNIT CONTEXT` `source_ref`/`source_kind` when\n   present; if absent, use the `<unit>` id itself (the `<session_id>` or\n   `<keyboard_input.id>`) as `source_ref`. This does **not** change the\n   `dedup_key` / pending-write / `lead_time` contract — only the source of the\n   extraction input when a re-fetch misses.\n2. **Manual (\"today's meetings\").** On demand, the agent runs the windowed\n   `fetch` (last 24h by default) → extracts → pushes. Repeating the ask re-runs\n   extraction on the window; the `seen` map still prevents duplicate delivery.\n   Manual writes are also **PENDING** (consistent \"confirm to keep\").\n\nThe auto-dispatch contract the javis-server team must satisfy (eligibility seeded\nfrom this file's `metadata.routes` block, collapsed server-side to a single\nskill-level `risk`, plus the run prompt shape) is declared in this file's\n`metadata.routes` block and documented in `references/route-contract.md`.\n\n## Notes\n\n- **Runtime dependencies** — the extractor script uses Node 18+ built-ins only (`fetch`, `fs`, `path`); no `npm install` is needed for the script runtime.\n- **Data sources**: audio recording transcripts **and** keyboard-dictation sessions — both via\n  `GET /api/transcripts/recent` (gateway-token authed), each session carrying a `source` field\n  (`\"audio\"` | `\"keyboard\"`); a single keyboard unit resolves via\n  `GET /api/transcripts/keyboard-input/<id>`. Plus per-user local state (dedup memory). There is no\n  `HTTP_SOURCE_URL` — the script talks to javis-server directly.\n- **Events are written PENDING.** Every event mirrored to `/api/skill/data` carries\n  `status: \"pending\"`; the server stores it pending and the iOS calendar table renders it\n  greyed/dashed with **Confirm · Discard**. **Confirm** promotes the row to solid (`confirmed`);\n  **Discard** deletes it. This is the human gate — there is no approve-to-run proposal card.\n- **Dedup is local-state-authoritative, event-level only.** The container's gateway token can\n  WRITE to `/api/skill/data` but cannot read it back (`GET /api/skill/data` requires a Clerk JWT),\n  so novelty is decided by the local `seen` map; the server write is a best-effort mirror for the\n  iOS app. There is **no per-unit gating** in the skill — the server owns run-once\n  (`DispatchRouteExecuted`); the human gate is Confirm/Discard on the pending rows. So the only\n  local dedup is the event-level `seen` map (`{ \"<event-key>\": \"<ts>\" }` in\n  `data/users/<userId>.json`, 30-day TTL-pruned). It keeps a duplicate event from re-reaching the\n  table/chat across overlapping manual windows or a re-run.\n- **Timezone**: there is **no prefs file**. On the `fetch`/extract path the skill resolves tz in\n  order: the `tz` field on the fetch payload → the `TZ` environment variable → the system zone\n  (`Intl.DateTimeFormat().resolvedOptions().timeZone`). The manual and dispatcher paths resolve tz\n  the same way. **Edit turns (`update`/`anchor`) have no fetch payload**, so they resolve:\n  explicit (`--tz` / stdin `tz` / `[CURRENT CARD]` tz) → **the server's authoritative zone**\n  (a best-effort `GET /api/transcripts/recent` reads the same `tz` field) → `TZ` env → system.\n  The server step exists because the prod per-user container has an **empty `TZ` (→ UTC)**, which\n  shifted \"today\" forward a day on evening edits west of UTC. The proper long-term fix is to put\n  the user's `tz` in the server's `[CURRENT CARD]` block and/or set `TZ` on the container — see\n  `docs/superpowers/specs/2026-06-22-calendar-edit-timezone-fix.md`.\n- **Markdown, not native cards.** A push delivers a `content` string rendered as markdown on iOS\n  (`MDBlock`). Native `EventList`/`EventCard` blocks are emitted only during a live SSE agent turn\n  (`_maybe_emit_chat_block`), not via the push path — so each per-card push is rich markdown by design.\n- **User IDs** only allow letters, digits, `-`, `_` (path-traversal guard in `data.js`).\n- **Backgrounded/killed iOS app**: `AGENT_PUSH` is WebSocket-only (no APNs). For mission-critical\n  delivery, add a Telegram channel as backup.\n\nFile v0.7.2:README.md\n\n# Calendar Extractor\n\n> ⚠️ **Requires the HiJavis iPhone app.** This skill runs inside HiJavis — install the app first, then it's ready to use.\n> 📲 https://apps.apple.com/us/app/hijavis/id6745134765\n\nCalendar Extractor finds the plans hiding in your conversations. HiJavis listens, spots anything that sounds like a meeting or appointment, and hands you a tidy list right in your chat.\n\n## Picture this\n\n- You finish a day of back-to-back calls and can't recall what you promised anyone. You ask, \"What did I agree to today?\" and there it is, all in one list.\n- A friend says, \"Let's grab coffee Friday.\" You'd normally forget by lunch. HiJavis already caught it.\n- It's 8am. You open the app and see everything you committed to today, before your first sip of coffee.\n- You leave a conference where you made a dozen plans out loud, and they're all waiting for you in a list.\n- The house is buzzing with \"dentist Thursday,\" \"soccer practice moved to 5,\" \"dinner Saturday at the new place.\" HiJavis keeps track so nobody drops the ball.\n\nIf any of those sound like you, this one's for you.\n\n## What it does\n\nHiJavis listens to your conversations and meetings. Calendar Extractor reads back through your recent recordings and spots anything that sounds like a plan or appointment, such as \"let's meet Tuesday at 3,\" \"dinner Saturday at the new place,\" or \"call me tomorrow afternoon.\"\n\nThen it sends each event to your chat as its own card — with the title, date, time, place, who's involved, and any notes it picked up. Every event opens in its own chat thread, so you can act on one plan without losing the rest.\n\n## How to use it\n\n### Just ask\n\nOpen your HiJavis chat and ask. Any of these works:\n\n- \"today's meetings\"\n- \"calendar extract\"\n- \"今日会议\"\n- \"提取日历\"\n- Or a natural question like \"What meetings do I have coming up?\" or \"Did I agree to anything today?\"\n\nHiJavis scans your recent recordings and replies with your list in seconds. Nothing to set up first.\n\n### Get a daily summary\n\nWant it to come to you? Ask HiJavis to send a summary on a schedule. By default you'll get one each morning (around 8am) and each evening (around 6pm), but you can pick your own times. Just say something like \"send me my calendar summary every morning at 7,\" and it'll start showing up on its own.\n\n## What makes it handy\n\n- **It gets how you talk about time.** You don't say \"March 14th at 2:00 PM\" out loud. You say \"tomorrow,\" \"next Thursday,\" \"around 6,\" \"noon.\" Calendar Extractor turns all of that into a real date and time in your time zone.\n- **It's instant.** The moment a recording or a typed note finishes, it pulls out any plans right then, so your list is ready before you even ask.\n- **One card per plan.** Each event arrives as its own card in its own chat thread, so they never pile into one wall of text and you can deal with them one at a time.\n- **It never repeats itself.** It remembers what it already showed you, so the same event won't clutter your chat twice.\n- **It works in English and Chinese.** It keeps up smoothly with both.\n\n## Real-life examples\n\n- **After back-to-back calls:** ask \"What did I agree to today?\" instead of replaying every recording.\n- **The offhand plan:** \"Let's grab coffee Friday\" gets caught, even when you'd have forgotten.\n- **Your morning briefing:** open the app and see everything on your plate for the day.\n- **After a conference:** all those verbal plans land in one neat list.\n- **The family hub:** \"dentist Thursday\" and \"soccer practice moved to 5\" stay organized so nobody forgets.\n\n## Good to know\n\nFor scheduled summaries to arrive on time, keep the HiJavis app open and running. If it's fully closed, a summary may wait until you reopen the app. Asking for your meetings on the spot works anytime.\n\nFile v0.7.2:_meta.json\n\n{\n  \"ownerId\": \"kn73qn9mdc6qf6hnnz3s26pgch845n33\",\n  \"slug\": \"calendar-extractor\",\n  \"version\": \"0.7.2\",\n  \"publishedAt\": 1785658459115\n}\n\nFile v0.7.2:references/route-contract.md\n\n# calendar-extractor — route contract (for the javis-server team)\n\nThis document specifies the interface the **javis-server session-dispatcher**\ncontrol-plane must satisfy so the dispatcher can route agenda deliverables to the\n`calendar-extractor` skill. It is the contract only — no javis-server code is\nimplemented in this repo.\n\nSee the design spec:\n`docs/superpowers/specs/2026-06-08-calendar-extractor-dispatcher-adaptation-design.md`\n(sections 3a–3c) and the dispatcher spec it adapts to\n(javis-server `docs/superpowers/specs/2026-06-06-session-dispatcher-server-variant.md`,\nimplemented in `app/services/dispatcher_service.py`).\n\nThe same contract is declared, discoverably, in the skill's `SKILL.md`\n`metadata.routes` block. That static block declares the skill-owned fields\n(`route_id`, `skill`, `matches`, `args_template`, `risk`); `enabled` is omitted\nthere because it is per-user runtime state the server owns, not skill metadata.\n\n## 3a. RouteRegistry row\n\nSeed this row per user, enabling it when the user enables `calendar-extractor`:\n\n| column | value |\n|---|---|\n| `route_id` | `\"calendar\"` |\n| `skill` | `\"calendar-extractor\"` |\n| `matches` | `\"meetings, appointments, events, agenda, scheduling, dates/times mentioned\"` |\n| `args_template` | `null` (the unit + the prepended SKILL.md carry everything the skill needs) |\n| `risk` | `\"low\"` (read-only transcript extraction + a push; no destructive side effects) |\n| `enabled` | set `true` when the user enables calendar-extractor; `false`/absent otherwise |\n\nThe dispatcher only routes a deliverable to this skill when an **enabled** row with\n`route_id=\"calendar\"` exists for that user. No enabled row → the skill is never\nscheduled.\n\n## 3b. `classify_and_route` deliverable shape\n\nFeed the user's enabled route catalog (each row's `route_id` + `matches`) into the\n`classify_and_route` task prompt. For a transcript containing scheduling content,\nthe classifier must emit a deliverable shaped like:\n\n```json\n{\n  \"id\": \"<uuid>\",\n  \"title\": \"Extract calendar events\",\n  \"description\": \"<short summary of the agenda found>\",\n  \"route_id\": \"calendar\",\n  \"confidence\": 0.0\n}\n```\n\n- `route_id` MUST be exactly `\"calendar\"` so it matches the RouteRegistry row.\n- No scheduling content in the transcript → **no** `calendar` deliverable → no\n  proposal card. (This is the correct, silent outcome.)\n\nThe dispatcher then matches the deliverable's `route_id` against the enabled\n`RouteRegistry` row, persists a `DispatchProposal`, and pushes a proposal card to\niOS. On the user's Approve, it claims run-once\n(`DispatchRouteExecuted (user, unit, route)` via a unique constraint, before\nscheduling) and triggers the skill.\n\n## 3c. Prompt contract\n\nThe skill relies only on the `<unit>` being present and `_UNIT_RE`-valid in the run\nprompt — which the generic dispatcher prompt already provides:\n\n```\nRun /calendar-extractor for <unit>. Deliverable: <title>. <description>\nFetch that unit's transcript, produce the deliverable, and push the result.\n```\n\n- `<unit>` is `audio:<session_id>` or `kbd:<keyboard_input.id>`. The skill parses it\n  and runs `fetch --session <session_id>` or `fetch --kbd-input <keyboard_input.id>`.\n- **No calendar-specific prompt enrichment is required.** `args_template` stays\n  `null` and `_deliverable_prompt` needs no calendar-specific change.\n- All date-resolution discipline rides in (1) the relative-date **anchor** the\n  `fetch` payload emits on every path\n  (`reference_time` / `reference_date` / `reference_weekday` / `reference_time_utc`\n  + `tz`) and (2) the **prepended SKILL.md** instructions. The server need not\n  enrich the prompt to preserve date correctness.\n\n## Idempotency ownership\n\nThe skill **does not** self-gate per unit. Run-once is owned entirely by the server:\n\n- the human approval gate (a proposal must be approved before the skill runs), and\n- `DispatchRouteExecuted (user, unit, route)` claimed via a unique constraint\n  **before** scheduling (closes the TOCTOU window).\n\nThe skill keeps only an **event-level** `seen` map so the same event is never\ndelivered twice across overlapping manual 24h windows or a re-run. It is safe to\ninvoke the skill more than once: `seen` prevents duplicate delivery and the server\nprevents duplicate invocation.\n\n## Open items for the server team\n\n- Seed/enable the `RouteRegistry` row for `calendar` when a user enables\n  calendar-extractor.\n- Feed the enabled route catalog (`route_id` + `matches`) into the\n  `classify_and_route` task prompt.\n\nFile v0.7.2:docs/pr-drafts/2026-08-02-prompt-context-fallback.md\n\n# calendar-extractor: use embedded UNIT CONTEXT; a fetch miss no longer aborts the write\n\n- **Branch:** `feat/calendar-extractor-prompt-context-fallback`\n- **Date:** 2026-08-02\n- **Skill:** `calendar-extractor` (currently `0.7.1`)\n- **Spec:** `javis-server/docs/superpowers/specs/2026-08-02-dispatch-prompt-embed-unit-context-resilience-design.md` (Approved — root-caused via systematic-debugging). This is the **coordinated skill-side half** of that spec; the server half (embed the unit context in the dispatch prompt) ships separately on javis-server branch `feat/dispatch-prompt-embed-unit-context`.\n\n## Why\n\nA proactive, dispatcher-invoked extraction **silently vanished** when its source unit row was dropped after dispatch.\n\nObserved (wmp425, 2026-08-02): a \"meeting at 1 PM today\" keyboard input reached the server, `calendar-extractor` was auto-invoked, and its agent **extracted the event** (container log: *\"Extracted and pushed… PENDING card\"*) — but **no `skill_data` row was ever written** (`skill_data MAX(id)` stuck at 319; no POST in the server logs). No calendar event → no voice-call arming → no \"Javis calls you\" ring.\n\nRoot cause: the dispatcher handed the skill **only the unit id**, so the skill had to **re-fetch** the unit via `GET /api/transcripts/keyboard-input/<id>`. For this event `keyboard_input 1189` **did not exist** (server `MAX(id)=1187`; rolled-back/dropped inserts — the fragile draft double-save path). The re-fetch 404'd → the skill aborted before the `/api/skill/data` POST → the extracted event was lost.\n\nThe server fix threads the already-loaded transcript + `source_ref`/`source_kind`/`reference_date`/`tz` into the dispatch prompt as a delimited `UNIT CONTEXT` block. This PR makes the skill **use that embedded context as the source of truth** and treat `fetch` as an optional enrichment whose failure must not suppress the write.\n\n## What changed\n\n**`scripts/calendar-extractor.js` — `doFetch` degrades instead of throwing.**\n- Wrapped the single `httpGet` in a try/catch. On failure (e.g. a 404 keyboard-input miss) it no longer propagates the throw that aborts the run. Instead it logs `⚠️ fetch miss (…) — not aborting; fall back to the run-prompt UNIT CONTEXT.` to stderr and continues with `data = null`.\n- The existing envelope normalization already handles a null/empty payload → an empty `sessions` array, so the emitted envelope still carries the relative-time anchor (`reference_time`/`reference_date`/`reference_weekday`/`reference_time_utc`/`tz`).\n- Surfaces the miss in the emitted envelope too (not just stderr): `out.fetch_error = fetchError`, so the agent can *see* the re-fetch missed and knowingly fall back to the `UNIT CONTEXT`.\n\n**`SKILL.md` — document the embedded-context path and the no-abort rule.**\n- Step 1 (Fetch): added the dispatcher-path **Exception** — a failed single-unit `fetch --session`/`fetch --kbd-input` (surfaced as `fetch_error`, empty sessions) does NOT mean \"emit nothing\"; fall back to the prompt-embedded transcript/`reference_date`/`tz` and STILL extract and push.\n- \"How this skill is invoked\" (dispatcher auto-run): the agent now reads the embedded **`UNIT CONTEXT`** block (transcript + `reference_date`/`tz` + `source_ref`/`source_kind`) as the source of truth for extraction; `fetch --session`/`fetch --kbd-input` is now **optional enrichment** and a **failed fetch must not abort the push**.\n- Added a dedicated **`UNIT CONTEXT` block** subsection: resolve relative times against its `reference_date`/`tz`; carry its `source_ref`/`source_kind` onto each event so provenance flows to the `/api/skill/data` mirror; on a re-fetch empty/404, fall back and still POST the PENDING row; report the miss non-silently but never let it suppress the write; for `source_ref` use the prompt's value, or the `<unit>` id itself when absent. **No change** to the `dedup_key` / pending-write / `lead_time` contract — only the source of the extraction input when a re-fetch misses.\n\n## Tests\n\n`test/cli.test.js` — two new tests (both reference the spec in-comment); full suite **14 pass / 0 fail** (`node --test test/cli.test.js`):\n\n- **`doFetch degrades to an empty-sessions envelope on a failing fetch (no throw, anchor + fetch_error present)`** — `httpGet` rejects exactly as the real 404 keyboard-input miss does; asserts no throw, zero sessions, `fetch_error` matches `/404/`, and the anchor (`reference_date` `2026-06-03`, `tz`) is still emitted.\n- **`failing fetch --kbd-input + prompt UNIT CONTEXT still produces the /api/skill/data upsert (source_ref from prompt)`** — the re-fetch 404s and degrades; the agent extracts from the prompt `UNIT CONTEXT` (\"meeting at 1 PM today\") carrying the prompt's `source_ref` (`1189`)/`source_kind` (`keyboard`); `doPush` still writes the `skill_data` upsert. Asserts the write happened (not \"emitting nothing\"), `source_ref` rides through from the prompt, and `status` is still `pending` (contract unchanged).\n\n## Files\n\n- `calendar-extractor/SKILL.md` (+29 / −2)\n- `calendar-extractor/scripts/calendar-extractor.js` (+21 / −1)\n- `calendar-extractor/test/cli.test.js` (+72)\n\n(`git diff --stat`: 3 files changed, 119 insertions(+), 4 deletions(-))\n\n## Not in this PR\n\n- The server half (embed `UNIT CONTEXT` in `_dispatch_prompt`, `_load_unit_context`) — javis-server branch `feat/dispatch-prompt-embed-unit-context`. Both halves must ship together for the end-to-end fix; the skill change is backward-compatible (if no `UNIT CONTEXT` is present, the manual/windowed `fetch` path is unchanged).\n- Fixing the underlying `keyboard_input` insert drops / id gaps (the true persistence bug — the fragile draft double-save path) — separate track.\n- `consecutive_discards → 3 = auto-disable` fragility; earbud-heartbeat staleness — separate tracks.\n- Version bump / `clawhub publish` — do after both halves merge.\n\n## Verification gate (from the spec)\n\nOn prod: a fresh keyboard/voice \"meeting at &lt;time&gt; today\" whose source row is subsequently absent still lands a **pending** `calendar-extractor` `skill_data` event (the write no longer depends on re-fetch) → confirmable in the iOS calendar table → arms a voice-call job.\n\nFile v0.7.2:skill-card.md\n\n## Description:\n\nExtracts calendar events from recent recording and keyboard transcripts, then sends each event to iOS chat as a Markdown card and pending calendar row.\n\nThis skill is ready for commercial/non-commercial use.\n\n## Publisher:\n\n[samuel-wei](https://clawhub.ai/user/samuel-wei)\n\n### License/Terms of Use:\n\nMIT-0\n\n## Use Case:\n\nExternal HiJavis users use this skill to turn recent conversations or typed notes into confirmable calendar event cards, either on demand or from dispatcher-invoked completed units.\n\n### Deployment Geography for Use:\n\nGlobal\n\n## Known Risks and Mitigations:\n\nRisk: The skill may process broad private recording and keyboard transcripts.\n\nMitigation: Use explicit opt-in or manual mode where possible, keep transcript trust boundaries clear, and review the skill before installation.\n\nRisk: Extracted details are sent to chat and written as calendar rows.\n\nMitigation: Keep the pending Confirm or Discard gate enabled so users approve events before they become active calendar entries.\n\nRisk: Calendar writes and push delivery depend on trusted server endpoints and update semantics.\n\nMitigation: Require HTTPS and destination validation in deployment, and prefer server-enforced update behavior for confirmed calendar rows.\n\n## Reference(s):\n\n- [ClawHub Skill Page](https://clawhub.ai/samuel-wei/skills/calendar-extractor)\n- [HiJavis iPhone App](https://apps.apple.com/us/app/hijavis/id6745134765)\n- [Route Contract](artifact/references/route-contract.md)\n- [Prompt Context Fallback Draft](artifact/docs/pr-drafts/2026-08-02-prompt-context-fallback.md)\n\n## Skill Output:\n\n**Output Type(s):** [text, markdown, shell commands, configuration, guidance]\n\n**Output Format:** [Markdown cards, JSON event arrays, and shell command invocations]\n\n**Output Parameters:** [1D]\n\n**Other Properties Related to Output:** [Produces pending calendar rows for user confirmation and per-event chat cards; uses local event-level deduplication.]\n\n## Skill Version(s):\n\n0.7.2 (source: server release evidence and package.json)\n\n## Ethical Considerations:\n\nUsers should evaluate whether this skill is appropriate for their environment, review any generated or modified files before relying on them, and apply their organization's safety, security, and compliance requirements before deployment.\n\nFile v0.7.2:data/users/self.json\n\n{\n  \"userId\": \"self\",\n  \"name\": \"\",\n  \"createdAt\": \"2026-06-08T21:20:53.749Z\",\n  \"seen\": {\n    \"2026-06-04|standup|2026-06-04T17:00:00.000Z\": \"2026-06-22T19:58:24.742Z\",\n    \"2026-06-04|design review|2026-06-04T20:00:00.000Z\": \"2026-06-22T19:58:24.742Z\",\n    \"2026-06-05|dinner|2026-06-05T01:00:00.000Z\": \"2026-06-22T19:58:24.742Z\",\n    \"2026-06-23|standup|2026-06-23T17:00:00.000Z\": \"2026-06-23T00:51:50.251Z\",\n    \"2026-06-23|freshmeeting|2026-06-23T17:00:00.000Z\": \"2026-06-23T00:52:11.027Z\"\n  },\n  \"lastRunAt\": \"2026-06-23T00:52:11.027Z\"\n}\n\nFile v0.7.2:data/users/self.prefs.json\n\n{\n  \"time\": \"08:00\",\n  \"channel\": \"iOS\",\n  \"tz\": \"America/Los_Angeles\",\n  \"enabledAt\": \"2026-05-30T18:44:26.998Z\"\n}\n\nFile v0.7.2:package.json\n\n{\n  \"name\": \"calendar-extractor\",\n  \"version\": \"0.7.2\",\n  \"description\": \"Extract calendar events from recent recording and keyboard transcripts and push a markdown digest to your iOS chat. Runs on demand, and the javis-server dispatcher also invokes it directly for every completed unit — no classifier, no route matching; this skill's own agent decides relevance itself. Extracted events are written PENDING and become solid only when the user taps Confirm in the iOS calendar table. Triggers: 'today's meetings', 'calendar extract', '今日会议', '提取日历'.\",\n  \"keywords\": [\"today's meetings\", \"calendar extract\", \"今日会议\", \"提取日历\"],\n  \"license\": \"MIT\",\n  \"scripts\": {\n    \"register\": \"node scripts/register.js\",\n    \"fetch\": \"node scripts/calendar-extractor.js\",\n    \"push\": \"node scripts/calendar-extractor.js\",\n    \"test\": \"node --test\"\n  },\n  \"engines\": { \"node\": \">=18\" }\n}\n\nArchive v0.7.1: 16 files, 49378 bytes\n\nFiles: data/users/self.json (543b), data/users/self.prefs.json (115b), package.json (905b), README.md (3781b), references/route-contract.md (4506b), scripts/calendar-extractor.js (33051b), scripts/data.js (2133b), scripts/lib.js (12254b), scripts/register.js (612b), skill-card.md (2567b), SKILL.md (19777b), test/cli.test.js (16291b), test/data.test.js (3033b), test/lib.test.js (13948b), test/update.test.js (20771b), _meta.json (137b)\n\nFile v0.7.1:SKILL.md\n\n---\nname: calendar-extractor\ndescription: Extract calendar events from recent recording and keyboard transcripts and push them to your iOS chat as one markdown card per event, each landing in its own Agent Chat thread. Use on demand when the user asks for \"today's meetings\" / \"calendar extract\" / \"今日会议\" / \"提取日历\", and fetch the last 24 hours of transcript data by default. The javis-server dispatcher also invokes this skill directly for every completed unit — no classifier, no route matching; this skill's own agent decides relevance itself, using this SKILL.md, and may use a deliverable hint passed in the run prompt alongside the transcript; extracted events are written PENDING and become solid only when the user taps Confirm in the iOS calendar table. If the user asks for \"today's meetings\", use the local day defined by the fetched reference_date field. Triggers: 'today's meetings', 'calendar extract', '今日会议', '提取日历'.\nkeywords: today's meetings, calendar extract, 今日会议, 提取日历, calendar-extractor\nmetadata:\n  openclaw:\n    runtime:\n      node: \">=18\"\n  routes:\n    - route_id: calendar\n      skill: calendar-extractor\n      matches: \"meetings, appointments, events, agenda, scheduling, dates/times mentioned\"\n      args_template: null\n      risk: low\n---\n\n# Calendar Extractor\n\n> Extract calendar events from recent recording and keyboard transcripts and push them to your iOS chat — one markdown card per event, each landing in its own Agent Chat thread. Fetch the last 24 hours of transcript data by default. If the user asks for \"today's meetings\", use the local day defined by the fetched reference_date field. Generate one digest per local day in the user's timezone, containing all events that occur on reference_date and any events explicitly mentioned as happening today.\n\n## When to use\n\n- \"today's meetings\"\n- \"calendar extract\"\n- \"今日会议\"\n- \"提取日历\"\n- Automatically, when the javis-server dispatcher invokes this skill directly after a completed voice/keyboard unit — no classifier, no route matching. This skill's own agent decides for itself, using this `SKILL.md`, whether the unit is worth acting on; if not, it does nothing (see \"How this skill is invoked\").\n\n## Core commands\n\n> **`<userId>` is optional.** Omit it and it defaults to `self`. Each HiJavis user\n> runs in their own openclaw container, so `self` is correctly isolated; the gateway\n> token (not the userId) authenticates every server call. No registration is needed\n> to start — pass an explicit ID only if you run multiple profiles in one container.\n\n```bash\n# Step 1 — fetch recent transcripts as JSON (the agent extracts events from this)\nnode scripts/calendar-extractor.js fetch [--hours N] [--limit N]\n\n# Step 1 (dispatcher run) — fetch ONE completed unit (the auto-run dispatcher unit)\nnode scripts/calendar-extractor.js fetch --session <sessionId> [--hours N]   # audio unit\nnode scripts/calendar-extractor.js fetch --kbd-input <inputId> [--hours N]   # keyboard unit\n\n# Step 2 — push: pipe the extracted-events JSON array to stdin; dedups (seen) + delivers to iOS\necho '<events-json-array>' | node scripts/calendar-extractor.js push\n\n# anchor — print the CURRENT relative-date anchor (resolve \"today / 6 pm / tomorrow\" on an edit turn)\nnode scripts/calendar-extractor.js anchor            # optional: --tz <IANA> for the card's calendar zone\n\n# update — edit ONE pushed card in place (verbatim dedup_key, full merged patch, auto-confirm)\necho '{\"dedup_key\":\"<verbatim>\",\"patch\":{...}}' | node scripts/calendar-extractor.js update\n\n# Optional: explicit userId / multi-profile (back-compat — prepend the ID)\nnode scripts/register.js <userId> <name>\nnode scripts/calendar-extractor.js <userId> fetch\necho '<events-json-array>' | node scripts/calendar-extractor.js <userId> push\nnode scripts/calendar-extractor.js <userId> anchor\necho '{\"dedup_key\":\"<verbatim>\",\"patch\":{...}}' | node scripts/calendar-extractor.js <userId> update\n```\n\n## Workflow\n\nThis skill is a two-step pipeline: the **script** does the I/O (fetch transcripts, dedup, push),\nthe **agent/LLM** does the reasoning (extract events). Extraction is not hardcoded — the agent\nreads the fetched transcripts and emits a JSON array of events.\n\n1. **Fetch** — `node scripts/calendar-extractor.js <userId> fetch` issues\n   `GET http://javis-server:8000/api/transcripts/recent?since=…&limit=…` with the\n   `OPENCLAW_GATEWAY_TOKEN` bearer and prints\n   `{ \"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 } ] }`. If fetch fails, returns invalid JSON, or yields zero sessions, output an empty events array and do not push anything; report the failure only if the user asked for a diagnostic. If the fetch response contains no sessions or the transcript text is empty, return `[]` and do not attempt to push a digest.\n2. **Extract** — the agent reads that JSON and produces an events array. Each event:\n   `{ \"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), \"lead_time\" (minutes before start for the \"Javis calls you\" voice alert, optional — defaults to 10) }`.\n   Carry `source_kind` through so provenance flows to the `/api/skill/data` mirror and the iOS digest.\n   **`lead_time`** is the per-event minutes-before-start at which the proactive voice-call engine rings the user (`fire = start − lead_time`). Omit it for the standard 10-minute lead; emit an integer only when the transcript states a different desired heads-up (e.g. \"remind me an hour before the flight\" → `60`). Negative/invalid values fall back to 10. The detail fields (`location`, `attendees`, `notes`) double as the announcement context the in-call **Details** command speaks, so keep them populated when the transcript provides them.\n   **Date resolution (required):** the top-level `reference_time` is **already local\n   wall-clock in `tz`** (zoneless) and `reference_date`/`reference_weekday` give the local\n   \"today\" — so \"today\" == `reference_date`, and \"tomorrow\"/\"Saturday\"/\"next Thursday\" count\n   forward from `reference_weekday`. Do **not** re-apply the tz offset and do **not** anchor on\n   `reference_time_utc` (the raw instant, whose date can be the *next* day in the evening — that\n   is exactly the off-by-one this field avoids). Fall back to the session's `started_at` only if\n   `reference_time` is absent. Never use your own sense of \"today\". Emit each event's `start_at`/\n   `end_at` as a full ISO 8601 instant **with the explicit `tz` UTC offset** (e.g. an 8 PM event\n   in `America/Los_Angeles` → `2026-06-04T20:00:00-07:00`) — not a zoneless string, which the\n   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\n   party at 6pm\" → 18:00). If the transcript does not provide enough information to determine a unique date/time (for example, only \"next Friday\" with no weekday anchor or an ambiguous time like \"at 8\" without AM/PM), emit `null` for the unresolved field rather than guessing. When multiple plausible interpretations are equally supported by the transcript, prefer the most specific one; if two interpretations remain equally plausible, emit `null` for the unresolved field rather than guessing. If the transcript mentions recurring meetings, all-day meetings, or multi-day events, preserve them as a single event only when the recurrence is explicit; otherwise emit a single event with the best available start/end times and note the ambiguity in `notes`.\n3. **Push** — pipe the events array into `node scripts/calendar-extractor.js <userId> push`. The script:\n   - dedups against per-user local state (`data/users/<userId>.json` → `seen` map, 30-day TTL),\n   - best-effort mirrors the **new** events to `POST /api/skill/data` (upsert by `dedup_key`) **tagged `status: \"pending\"`** so the server stores them pending and the iOS calendar table shows them greyed/dashed with **Confirm · Discard** — an event becomes solid only when the user taps **Confirm** (Discard deletes it). Each mirrored row carries a top-level **`lead_time`** (minutes, default 10) so the server's voice-call adapter can schedule the proactive call at `start − lead_time`; the detail fields (`location`/`attendees`/`notes`) ride in `payload` as the in-call announcement context,\n   - delivers the **new** events **per card** — one `POST http://javis-server:8000/api/agent/push`\n     per event, each `{\"skill\": \"calendar-extractor\", \"content\": \"<markdown>\", \"dedup_key\": \"<event dedup_key>\"}`.\n     The server routes each push (carrying its `dedup_key`, no explicit `session_id`) into that card's\n     own Agent Chat session, so **every event lands in its own iOS chat thread** instead of one combined\n     digest. The pushes are informational — they are not a confirmation gate.\n\n## Editing a pushed card in-thread\n\nA user can correct a card by **replying in that card's own Agent Chat thread**\n(\"6 pm today\", \"location is Zoom\", \"add Alex\"). The reply edits **that exact row\nin place and confirms it** — no duplicate row, no Confirm tap. The agent drives it:\n\n1. **Read the injected `[CURRENT CARD]` block.** When an agent turn runs inside a\n   card thread, the server injects a `[CURRENT CARD]` block carrying the card's\n   **original `dedup_key` (verbatim)** plus its current fields (`title`,\n   `start_at`, `end_at`, `location`, `attendees`, `notes`, `lead_time`, `status`).\n   This is the source of truth for the row you are editing. Carry `lead_time`\n   forward in the merged patch (default 10 if the block omits it) so an edit never\n   silently resets the voice-call lead; set a new integer only if the user asks to\n   change the heads-up (\"ring me 30 min before\" → `lead_time: 30`).\n2. **Run `anchor` for a fresh \"now\".** `node scripts/calendar-extractor.js anchor`\n   prints `{ reference_time, reference_date, reference_weekday, reference_time_utc,\n   tz }` for the **current** clock. The chat happens *later* than extraction, so\n   the original fetch anchor is stale — resolve \"today / 6 pm / tomorrow\" against\n   this fresh anchor (same date-resolution discipline as extraction: anchor on\n   `reference_time`/`reference_date`, never your own \"today\").\n   - **The anchor's `tz` is the user's calendar zone.** `anchor` resolves it from\n     the server (the authoritative zone) — **not** the container's `TZ`, which is\n     empty/UTC in prod and would shift \"today\" forward a day west of UTC in the\n     evening. Pass `[CURRENT CARD]`'s `tz` as `--tz <IANA>` if it carries one;\n     otherwise a bare `anchor` already returns the correct zone. **Use the\n     `reference_date` it prints — do not compute \"today\" from a UTC clock.**\n3. **Resolve the correction; null-not-guess.** Resolve the user's change against\n   the anchor and emit a full ISO 8601 instant **with the explicit `tz` UTC\n   offset** (e.g. `2026-06-22T18:00:00-07:00`). If the change is ambiguous or\n   unresolvable (e.g. \"at 8\" with no AM/PM, no weekday anchor), **do not call\n   `update`** — ask a follow-up in-thread instead. Emit `null` rather than guess.\n4. **Merge into a FULL patch (wholesale-payload rule).** The server overwrites\n   `payload`/`start_at`/`end_at` **wholesale** on the matched row, so the `patch`\n   must carry the **complete intended state** — the current fields from\n   `[CURRENT CARD]` **merged with** the user's change, not just the changed field.\n   A time-only edit must still resend `title`/`location`/`attendees`/`notes`, or\n   they would be blanked.\n   - **Times you are NOT changing:** copy the `start_at`/`end_at` strings from\n     `[CURRENT CARD]` **verbatim**. Those are already naive-local wall-clock in the\n     card zone (no offset) and the script passes a zoneless `YYYY-MM-DDTHH:MM:SS`\n     value through unchanged — it will **not** re-interpret it in the runner's\n     zone. (A time you *are* changing must be a full offset-bearing instant per\n     step 3, so the script can collapse it correctly.)\n   - **Pass the card zone.** Include the card's `tz` (the value the `anchor` step\n     printed) as a top-level `tz` so the script collapses any offset-bearing\n     changed time against the user's calendar zone, not the container's process\n     zone (which is empty/UTC in prod, not the user's zone). If you omit `tz`,\n     `update` resolves the user's zone from the server itself — but passing the\n     anchor's `tz` is preferred (explicit, and avoids a second lookup).\n5. **Run `update` with the verbatim `dedup_key`.**\n\n   ```bash\n   echo '{\n     \"dedup_key\": \"<the [CURRENT CARD] dedup_key, VERBATIM>\",\n     \"tz\": \"America/Los_Angeles\",\n     \"patch\": {\n       \"title\": \"Design Review\", \"location\": \"Zoom\",\n       \"attendees\": [\"Sam\"], \"notes\": \"bring laptop\",\n       \"lead_time\": 10,\n       \"start_at\": \"2026-06-22T18:00:00-07:00\",\n       \"end_at\":   \"2026-06-22T19:00:00-07:00\"\n     }\n   }' | node scripts/calendar-extractor.js update\n   ```\n\n   The script POSTs **one** `/api/skill/data` upsert with that verbatim key and\n   `status: \"confirmed\"`. It does **NOT** recompute the key from the new time\n   (that would create a second row — the whole bug), writes `start_at`/`end_at`\n   as **naive-local** wall-clock (no `Z`/offset), does **no `seen`-dedup\n   filtering**, and does **not** call `/api/agent/push`.\n\n**Auto-confirm semantics.** Stating the corrected value in chat *is* the\nconfirmation: the upsert's `status: \"confirmed\"` flips the row `pending →\nconfirmed` **atomically with the field write** (strictly that direction —\nconfirmed never downgrades). The skill never calls a separate `/confirm`\nendpoint. The iOS card re-render and the live chat reply are the user feedback.\nIf `/api/skill/data` fails, the script reports the error (non-silent) — tell the\nuser the card couldn't be updated; do **not** claim success.\n\nThis path is **edit-only** of an existing card — creating new events from chat,\nre-opening a confirmed row, or deleting via chat (Discard covers delete) are out\nof scope.\n\n## How this skill is invoked\n\nThis skill has **two triggers** (dispatcher auto-run and manual).\n\n1. **Dispatcher auto-run (automatic).** When a unit of input completes (an audio\n   session ends or a keyboard input is saved), the javis-server **session dispatcher**\n   invokes this skill's agent directly, alongside every other enabled, `risk: low`\n   skill — no classifier, no route matching. The server claims run-once\n   (`DispatchSkillInvoked (user_id, unit_key, skill)`) and **AUTO-RUNS this skill\n   directly — there is no approve-to-run proposal card**. It runs in the user's\n   container with a prompt of the form `Run /calendar-extractor for <unit>.\n   Deliverable: …`, where the deliverable text is an **advisory HINT**, not a\n   classification — the agent may use it alongside the transcript, but should still\n   read the unit transcript for full detail (time, attendees), and **this skill's own\n   agent decides for itself, using this `SKILL.md`, whether the unit is worth acting\n   on; if not, it does nothing.** When it does act, it parses `<unit>`\n   (`audio:<session_id>` or `kbd:<keyboard_input.id>`), runs `fetch --session <id>` /\n   `fetch --kbd-input <id>`, extracts events, and pushes them. **The human gate is not\n   running the skill — it is Confirm/Discard on the produced events:** every extracted\n   event is written **PENDING** to the calendar table (greyed/dashed with\n   **Confirm · Discard**) and becomes solid only when the user taps **Confirm**\n   (Discard deletes it). **The skill does not self-gate on Confirm/Discard** — the\n   server owns run-once (whether to invoke at all), while the skill's own agent owns\n   relevance (whether to act once invoked).\n2. **Manual (\"today's meetings\").** On demand, the agent runs the windowed\n   `fetch` (last 24h by default) → extracts → pushes. Repeating the ask re-runs\n   extraction on the window; the `seen` map still prevents duplicate delivery.\n   Manual writes are also **PENDING** (consistent \"confirm to keep\").\n\nThe auto-dispatch contract the javis-server team must satisfy (eligibility seeded\nfrom this file's `metadata.routes` block, collapsed server-side to a single\nskill-level `risk`, plus the run prompt shape) is declared in this file's\n`metadata.routes` block and documented in `references/route-contract.md`.\n\n## Notes\n\n- **Runtime dependencies** — the extractor script uses Node 18+ built-ins only (`fetch`, `fs`, `path`); no `npm install` is needed for the script runtime.\n- **Data sources**: audio recording transcripts **and** keyboard-dictation sessions — both via\n  `GET /api/transcripts/recent` (gateway-token authed), each session carrying a `source` field\n  (`\"audio\"` | `\"keyboard\"`); a single keyboard unit resolves via\n  `GET /api/transcripts/keyboard-input/<id>`. Plus per-user local state (dedup memory). There is no\n  `HTTP_SOURCE_URL` — the script talks to javis-server directly.\n- **Events are written PENDING.** Every event mirrored to `/api/skill/data` carries\n  `status: \"pending\"`; the server stores it pending and the iOS calendar table renders it\n  greyed/dashed with **Confirm · Discard**. **Confirm** promotes the row to solid (`confirmed`);\n  **Discard** deletes it. This is the human gate — there is no approve-to-run proposal card.\n- **Dedup is local-state-authoritative, event-level only.** The container's gateway token can\n  WRITE to `/api/skill/data` but cannot read it back (`GET /api/skill/data` requires a Clerk JWT),\n  so novelty is decided by the local `seen` map; the server write is a best-effort mirror for the\n  iOS app. There is **no per-unit gating** in the skill — the server owns run-once\n  (`DispatchRouteExecuted`); the human gate is Confirm/Discard on the pending rows. So the only\n  local dedup is the event-level `seen` map (`{ \"<event-key>\": \"<ts>\" }` in\n  `data/users/<userId>.json`, 30-day TTL-pruned). It keeps a duplicate event from re-reaching the\n  table/chat across overlapping manual windows or a re-run.\n- **Timezone**: there is **no prefs file**. On the `fetch`/extract path the skill resolves tz in\n  order: the `tz` field on the fetch payload → the `TZ` environment variable → the system zone\n  (`Intl.DateTimeFormat().resolvedOptions().timeZone`). The manual and dispatcher paths resolve tz\n  the same way. **Edit turns (`update`/`anchor`) have no fetch payload**, so they resolve:\n  explicit (`--tz` / stdin `tz` / `[CURRENT CARD]` tz) → **the server's authoritative zone**\n  (a best-effort `GET /api/transcripts/recent` reads the same `tz` field) → `TZ` env → system.\n  The server step exists because the prod per-user container has an **empty `TZ` (→ UTC)**, which\n  shifted \"today\" forward a day on evening edits west of UTC. The proper long-term fix is to put\n  the user's `tz` in the server's `[CURRENT CARD]` block and/or set `TZ` on the container — see\n  `docs/superpowers/specs/2026-06-22-calendar-edit-timezone-fix.md`.\n- **Markdown, not native cards.** A push delivers a `content` string rendered as markdown on iOS\n  (`MDBlock`). Native `EventList`/`EventCard` blocks are emitted only during a live SSE agent turn\n  (`_maybe_emit_chat_block`), not via the push path — so each per-card push is rich markdown by design.\n- **User IDs** only allow letters, digits, `-`, `_` (path-traversal guard in `data.js`).\n- **Backgrounded/killed iOS app**: `AGENT_PUSH` is WebSocket-only (no APNs). For mission-critical\n  delivery, add a Telegram channel as backup.\n\nFile v0.7.1:README.md\n\n# Calendar Extractor\n\n> ⚠️ **Requires the HiJavis iPhone app.** This skill runs inside HiJavis — install the app first, then it's ready to use.\n> 📲 https://apps.apple.com/us/app/hijavis/id6745134765\n\nCalendar Extractor finds the plans hiding in your conversations. HiJavis listens, spots anything that sounds like a meeting or appointment, and hands you a tidy list right in your chat.\n\n## Picture this\n\n- You finish a day of back-to-back calls and can't recall what you promised anyone. You ask, \"What did I agree to today?\" and there it is, all in one list.\n- A friend says, \"Let's grab coffee Friday.\" You'd normally forget by lunch. HiJavis already caught it.\n- It's 8am. You open the app and see everything you committed to today, before your first sip of coffee.\n- You leave a conference where you made a dozen plans out loud, and they're all waiting for you in a list.\n- The house is buzzing with \"dentist Thursday,\" \"soccer practice moved to 5,\" \"dinner Saturday at the new place.\" HiJavis keeps track so nobody drops the ball.\n\nIf any of those sound like you, this one's for you.\n\n## What it does\n\nHiJavis listens to your conversations and meetings. Calendar Extractor reads back through your recent recordings and spots anything that sounds like a plan or appointment, such as \"let's meet Tuesday at 3,\" \"dinner Saturday at the new place,\" or \"call me tomorrow afternoon.\"\n\nThen it sends each event to your chat as its own card — with the title, date, time, place, who's involved, and any notes it picked up. Every event opens in its own chat thread, so you can act on one plan without losing the rest.\n\n## How to use it\n\n### Just ask\n\nOpen your HiJavis chat and ask. Any of these works:\n\n- \"today's meetings\"\n- \"calendar extract\"\n- \"今日会议\"\n- \"提取日历\"\n- Or a natural question like \"What meetings do I have coming up?\" or \"Did I agree to anything today?\"\n\nHiJavis scans your recent recordings and replies with your list in seconds. Nothing to set up first.\n\n### Get a daily summary\n\nWant it to come to you? Ask HiJavis to send a summary on a schedule. By default you'll get one each morning (around 8am) and each evening (around 6pm), but you can pick your own times. Just say something like \"send me my calendar summary every morning at 7,\" and it'll start showing up on its own.\n\n## What makes it handy\n\n- **It gets how you talk about time.** You don't say \"March 14th at 2:00 PM\" out loud. You say \"tomorrow,\" \"next Thursday,\" \"around 6,\" \"noon.\" Calendar Extractor turns all of that into a real date and time in your time zone.\n- **It's instant.** The moment a recording or a typed note finishes, it pulls out any plans right then, so your list is ready before you even ask.\n- **One card per plan.** Each event arrives as its own card in its own chat thread, so they never pile into one wall of text and you can deal with them one at a time.\n- **It never repeats itself.** It remembers what it already showed you, so the same event won't clutter your chat twice.\n- **It works in English and Chinese.** It keeps up smoothly with both.\n\n## Real-life examples\n\n- **After back-to-back calls:** ask \"What did I agree to today?\" instead of replaying every recording.\n- **The offhand plan:** \"Let's grab coffee Friday\" gets caught, even when you'd have forgotten.\n- **Your morning briefing:** open the app and see everything on your plate for the day.\n- **After a conference:** all those verbal plans land in one neat list.\n- **The family hub:** \"dentist Thursday\" and \"soccer practice moved to 5\" stay organized so nobody forgets.\n\n## Good to know\n\nFor scheduled summaries to arrive on time, keep the HiJavis app open and running. If it's fully closed, a summary may wait until you reopen the app. Asking for your meetings on the spot works anytime.\n\nFile v0.7.1:_meta.json\n\n{\n  \"ownerId\": \"kn73qn9mdc6qf6hnnz3s26pgch845n33\",\n  \"slug\": \"calendar-extractor\",\n  \"version\": \"0.7.1\",\n  \"publishedAt\": 1784963494773\n}\n\nFile v0.7.1:references/route-contract.md\n\n# calendar-extractor — route contract (for the javis-server team)\n\nThis document specifies the interface the **javis-server session-dispatcher**\ncontrol-plane must satisfy so the dispatcher can route agenda deliverables to the\n`calendar-extractor` skill. It is the contract only — no javis-server code is\nimplemented in this repo.\n\nSee the design spec:\n`docs/superpowers/specs/2026-06-08-calendar-extractor-dispatcher-adaptation-design.md`\n(sections 3a–3c) and the dispatcher spec it adapts to\n(javis-server `docs/superpowers/specs/2026-06-06-session-dispatcher-server-variant.md`,\nimplemented in `app/services/dispatcher_service.py`).\n\nThe same contract is declared, discoverably, in the skill's `SKILL.md`\n`metadata.routes` block. That static block declares the skill-owned fields\n(`route_id`, `skill`, `matches`, `args_template`, `risk`); `enabled` is omitted\nthere because it is per-user runtime state the server owns, not skill metadata.\n\n## 3a. RouteRegistry row\n\nSeed this row per user, enabling it when the user enables `calendar-extractor`:\n\n| column | value |\n|---|---|\n| `route_id` | `\"calendar\"` |\n| `skill` | `\"calendar-extractor\"` |\n| `matches` | `\"meetings, appointments, events, agenda, scheduling, dates/times mentioned\"` |\n| `args_template` | `null` (the unit + the prepended SKILL.md carry everything the skill needs) |\n| `risk` | `\"low\"` (read-only transcript extraction + a push; no destructive side effects) |\n| `enabled` | set `true` when the user enables calendar-extractor; `false`/absent otherwise |\n\nThe dispatcher only routes a deliverable to this skill when an **enabled** row with\n`route_id=\"calendar\"` exists for that user. No enabled row → the skill is never\nscheduled.\n\n## 3b. `classify_and_route` deliverable shape\n\nFeed the user's enabled route catalog (each row's `route_id` + `matches`) into the\n`classify_and_route` task prompt. For a transcript containing scheduling content,\nthe classifier must emit a deliverable shaped like:\n\n```json\n{\n  \"id\": \"<uuid>\",\n  \"title\": \"Extract calendar events\",\n  \"description\": \"<short summary of the agenda found>\",\n  \"route_id\": \"calendar\",\n  \"confidence\": 0.0\n}\n```\n\n- `route_id` MUST be exactly `\"calendar\"` so it matches the RouteRegistry row.\n- No scheduling content in the transcript → **no** `calendar` deliverable → no\n  proposal card. (This is the correct, silent outcome.)\n\nThe dispatcher then matches the deliverable's `route_id` against the enabled\n`RouteRegistry` row, persists a `DispatchProposal`, and pushes a proposal card to\niOS. On the user's Approve, it claims run-once\n(`DispatchRouteExecuted (user, unit, route)` via a unique constraint, before\nscheduling) and triggers the skill.\n\n## 3c. Prompt contract\n\nThe skill relies only on the `<unit>` being present and `_UNIT_RE`-valid in the run\nprompt — which the generic dispatcher prompt already provides:\n\n```\nRun /calendar-extractor for <unit>. Deliverable: <title>. <description>\nFetch that unit's transcript, produce the deliverable, and push the result.\n```\n\n- `<unit>` is `audio:<session_id>` or `kbd:<keyboard_input.id>`. The skill parses it\n  and runs `fetch --session <session_id>` or `fetch --kbd-input <keyboard_input.id>`.\n- **No calendar-specific prompt enrichment is required.** `args_template` stays\n  `null` and `_deliverable_prompt` needs no calendar-specific change.\n- All date-resolution discipline rides in (1) the relative-date **anchor** the\n  `fetch` payload emits on every path\n  (`reference_time` / `reference_date` / `reference_weekday` / `reference_time_utc`\n  + `tz`) and (2) the **prepended SKILL.md** instructions. The server need not\n  enrich the prompt to preserve date correctness.\n\n## Idempotency ownership\n\nThe skill **does not** self-gate per unit. Run-once is owned entirely by the server:\n\n- the human approval gate (a proposal must be approved before the skill runs), and\n- `DispatchRouteExecuted (user, unit, route)` claimed via a unique constraint\n  **before** scheduling (closes the TOCTOU window).\n\nThe skill keeps only an **event-level** `seen` map so the same event is never\ndelivered twice across overlapping manual 24h windows or a re-run. It is safe to\ninvoke the skill more than once: `seen` prevents duplicate delivery and the server\nprevents duplicate invocation.\n\n## Open items for the server team\n\n- Seed/enable the `RouteRegistry` row for `calendar` when a user enables\n  calendar-extractor.\n- Feed the enabled route catalog (`route_id` + `matches`) into the\n  `classify_and_route` task prompt.\n\nFile v0.7.1:skill-card.md\n\n## Description: <br>\nCalendar Extractor reads recent HiJavis audio and keyboard transcripts, extracts likely calendar events, writes them as pending calendar rows, and sends per-event markdown cards to iOS chat. <br>\n\nThis skill is ready for commercial/non-commercial use. <br>\n\n## Publisher: <br>\n[samuel-wei](https://clawhub.ai/user/samuel-wei) <br>\n\n### License/Terms of Use: <br>\nMIT-0 <br>\n\n\n## Use Case: <br>\nExternal HiJavis users use this skill to turn recent conversation and keyboard transcript references to meetings, appointments, and plans into pending calendar entries and chat cards they can confirm, discard, or edit. <br>\n\n### Deployment Geography for Use: <br>\nGlobal <br>\n\n## Known Risks and Mitigations: <br>\nRisk: The skill automatically analyzes completed voice and keyboard transcript units, which may include private content. <br>\nMitigation: Enable it only when the user accepts this transcript access, and provide controls to disable auto-runs or limit transcript windows. <br>\nRisk: Extracted event details can be surfaced without explicit approval for each run. <br>\nMitigation: Keep extracted events pending until the user confirms them, and allow users to discard or edit events before treating them as confirmed. <br>\nRisk: Relative or ambiguous time language can produce inaccurate calendar details. <br>\nMitigation: Resolve dates from the fetched local reference date and timezone, and leave unresolved fields null or ask for clarification when the transcript is ambiguous. <br>\n\n\n## Reference(s): <br>\n- [ClawHub Skill Page](https://clawhub.ai/samuel-wei/skills/calendar-extractor) <br>\n- [Route Contract](references/route-contract.md) <br>\n- [HiJavis iPhone App](https://apps.apple.com/us/app/hijavis/id6745134765) <br>\n\n\n## Skill Output: <br>\n**Output Type(s):** [text, markdown, JSON, API calls, configuration] <br>\n**Output Format:** [JSON transcript and anchor payloads, JSON event or update input, per-event Markdown chat cards, and local JSON dedup state.] <br>\n**Output Parameters:** [1D] <br>\n**Other Properties Related to Output:** [Uses a 24-hour default transcript window, event-level deduplication, and pending event status until user confirmation.] <br>\n\n## Skill Version(s): <br>\n0.7.1 (source: server release metadata and package.json) <br>\n\n## Ethical Considerations: <br>\nUsers should evaluate whether this skill is appropriate for their environment, review any generated or modified files before relying on them, and apply their organization's safety, security, and compliance requirements before deployment. <br>\n\nFile v0.7.1:data/users/self.json\n\n{\n  \"userId\": \"self\",\n  \"name\": \"\",\n  \"createdAt\": \"2026-06-08T21:20:53.749Z\",\n  \"seen\": {\n    \"2026-06-04|standup|2026-06-04T17:00:00.000Z\": \"2026-06-22T19:58:24.742Z\",\n    \"2026-06-04|design review|2026-06-04T20:00:00.000Z\": \"2026-06-22T19:58:24.742Z\",\n    \"2026-06-05|dinner|2026-06-05T01:00:00.000Z\": \"2026-06-22T19:58:24.742Z\",\n    \"2026-06-23|standup|2026-06-23T17:00:00.000Z\": \"2026-06-23T00:51:50.251Z\",\n    \"2026-06-23|freshmeeting|2026-06-23T17:00:00.000Z\": \"2026-06-23T00:52:11.027Z\"\n  },\n  \"lastRunAt\": \"2026-06-23T00:52:11.027Z\"\n}\n\nFile v0.7.1:data/users/self.prefs.json\n\n{\n  \"time\": \"08:00\",\n  \"channel\": \"iOS\",\n  \"tz\": \"America/Los_Angeles\",\n  \"enabledAt\": \"2026-05-30T18:44:26.998Z\"\n}\n\nFile v0.7.1:package.json\n\n{\n  \"name\": \"calendar-extractor\",\n  \"version\": \"0.7.1\",\n  \"description\": \"Extract calendar events from recent recording and keyboard transcripts and push a markdown digest to your iOS chat. Runs on demand, and the javis-server dispatcher also invokes it directly for every completed unit — no classifier, no route matching; this skill's own agent decides relevance itself. Extracted events are written PENDING and become solid only when the user taps Confirm in the iOS calendar table. Triggers: 'today's meetings', 'calendar extract', '今日会议', '提取日历'.\",\n  \"keywords\": [\"today's meetings\", \"calendar extract\", \"今日会议\", \"提取日历\"],\n  \"license\": \"MIT\",\n  \"scripts\": {\n    \"register\": \"node scripts/register.js\",\n    \"fetch\": \"node scripts/calendar-extractor.js\",\n    \"push\": \"node scripts/calendar-extractor.js\",\n    \"test\": \"node --test\"\n  },\n  \"engines\": { \"node\": \">=18\" }\n}\n\nArchive v0.7.0: 16 files, 46686 bytes\n\nFiles: data/users/self.json (543b), data/users/self.prefs.json (115b), package.json (905b), README.md (3781b), references/route-contract.md (4506b), scripts/calendar-extractor.js (32659b), scripts/data.js (2133b), scripts/lib.js (8553b), scripts/register.js (612b), skill-card.md (2348b), SKILL.md (19777b), test/cli.test.js (16251b), test/data.test.js (3033b), test/lib.test.js (10470b), test/update.test.js (20771b), _meta.json (137b)\n\nFile v0.7.0:SKILL.md\n\n---\nname: calendar-extractor\ndescription: Extract calendar events from recent recording and keyboard transcripts and push them to your iOS chat as one markdown card per event, each landing in its own Agent Chat thread. Use on demand when the user asks for \"today's meetings\" / \"calendar extract\" / \"今日会议\" / \"提取日历\", and fetch the last 24 hours of transcript data by default. The javis-server dispatcher also invokes this skill directly for every completed unit — no classifier, no route matching; this skill's own agent decides relevance itself, using this SKILL.md, and may use a deliverable hint passed in the run prompt alongside the transcript; extracted events are written PENDING and become solid only when the user taps Confirm in the iOS calendar table. If the user asks for \"today's meetings\", use the local day defined by the fetched reference_date field. Triggers: 'today's meetings', 'calendar extract', '今日会议', '提取日历'.\nkeywords: today's meetings, calendar extract, 今日会议, 提取日历, calendar-extractor\nmetadata:\n  openclaw:\n    runtime:\n      node: \">=18\"\n  routes:\n    - route_id: calendar\n      skill: calendar-extractor\n      matches: \"meetings, appointments, events, agenda, scheduling, dates/times mentioned\"\n      args_template: null\n      risk: low\n---\n\n# Calendar Extractor\n\n> Extract calendar events from recent recording and keyboard transcripts and push them to your iOS chat — one markdown card per event, each landing in its own Agent Chat thread. Fetch the last 24 hours of transcript data by default. If the user asks for \"today's meetings\", use the local day defined by the fetched reference_date field. Generate one digest per local day in the user's timezone, containing all events that occur on reference_date and any events explicitly mentioned as happening today.\n\n## When to use\n\n- \"today's meetings\"\n- \"calendar extract\"\n- \"今日会议\"\n- \"提取日历\"\n- Automatically, when the javis-server dispatcher invokes this skill directly after a completed voice/keyboard unit — no classifier, no route matching. This skill's own agent decides for itself, using this `SKILL.md`, whether the unit is worth acting on; if not, it does nothing (see \"How this skill is invoked\").\n\n## Core commands\n\n> **`<userId>` is optional.** Omit it and it defaults to `self`. Each HiJavis user\n> runs in their own openclaw container, so `self` is correctly isolated; the gateway\n> token (not the userId) authenticates every server call. No registration is needed\n> to start — pass an explicit ID only if you run multiple profiles in one container.\n\n```bash\n# Step 1 — fetch recent transcripts as JSON (the agent extracts events from this)\nnode scripts/calendar-extractor.js fetch [--hours N] [--limit N]\n\n# Step 1 (dispatcher run) — fetch ONE completed unit (the auto-run dispatcher unit)\nnode scripts/calendar-extractor.js fetch --session <sessionId> [--hours N]   # audio unit\nnode scripts/calendar-extractor.js fetch --kbd-input <inputId> [--hours N]   # keyboard unit\n\n# Step 2 — push: pipe the extracted-events JSON array to stdin; dedups (seen) + delivers to iOS\necho '<events-json-array>' | node scripts/calendar-extractor.js push\n\n# anchor — print the CURRENT relative-date anchor (resolve \"today / 6 pm / tomorrow\" on an edit turn)\nnode scripts/calendar-extractor.js anchor            # optional: --tz <IANA> for the card's calendar zone\n\n# update — edit ONE pushed card in place (verbatim dedup_key, full merged patch, auto-confirm)\necho '{\"dedup_key\":\"<verbatim>\",\"patch\":{...}}' | node scripts/calendar-extractor.js update\n\n# Optional: explicit userId / multi-profile (back-compat — prepend the ID)\nnode scripts/register.js <userId> <name>\nnode scripts/calendar-extractor.js <userId> fetch\necho '<events-json-array>' | node scripts/calendar-extractor.js <userId> push\nnode scripts/calendar-extractor.js <userId> anchor\necho '{\"dedup_key\":\"<verbatim>\",\"patch\":{...}}' | node scripts/calendar-extractor.js <userId> update\n```\n\n## Workflow\n\nThis skill is a two-step pipeline: the **script** does the I/O (fetch transcripts, dedup, push),\nthe **agent/LLM** does the reasoning (extract events). Extraction is not hardcoded — the agent\nreads the fetched transcripts and emits a JSON array of events.\n\n1. **Fetch** — `node scripts/calendar-extractor.js <userId> fetch` issues\n   `GET http://javis-server:8000/api/transcripts/recent?since=…&limit=…` with the\n   `OPENCLAW_GATEWAY_TOKEN` bearer and prints\n   `{ \"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 } ] }`. If fetch fails, returns invalid JSON, or yields zero sessions, output an empty events array and do not push anything; report the failure only if the user asked for a diagnostic. If the fetch response contains no sessions or the transcript text is empty, return `[]` and do not attempt to push a digest.\n2. **Extract** — the agent reads that JSON and produces an events array. Each event:\n   `{ \"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), \"lead_time\" (minutes before start for the \"Javis calls you\" voice alert, optional — defaults to 10) }`.\n   Carry `source_kind` through so provenance flows to the `/api/skill/data` mirror and the iOS digest.\n   **`lead_time`** is the per-event minutes-before-start at which the proactive voice-call engine rings the user (`fire = start − lead_time`). Omit it for the standard 10-minute lead; emit an integer only when the transcript states a different desired heads-up (e.g. \"remind me an hour before the flight\" → `60`). Negative/invalid values fall back to 10. The detail fields (`location`, `attendees`, `notes`) double as the announcement context the in-call **Details** command speaks, so keep them populated when the transcript provides them.\n   **Date resolution (required):** the top-level `reference_time` is **already local\n   wall-clock in `tz`** (zoneless) and `reference_date`/`reference_weekday` give the local\n   \"today\" — so \"today\" == `reference_date`, and \"tomorrow\"/\"Saturday\"/\"next Thursday\" count\n   forward from `reference_weekday`. Do **not** re-apply the tz offset and do **not** anchor on\n   `reference_time_utc` (the raw instant, whose date can be the *next* day in the evening — that\n   is exactly the off-by-one this field avoids). Fall back to the session's `started_at` only if\n   `reference_time` is absent. Never use your own sense of \"today\". Emit each event's `start_at`/\n   `end_at` as a full ISO 8601 instant **with the explicit `tz` UTC offset** (e.g. an 8 PM event\n   in `America/Los_Angeles` → `2026-06-04T20:00:00-07:00`) — not a zoneless string, which the\n   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\n   party at 6pm\" → 18:00). If the transcript does not provide enough information to determine a unique date/time (for example, only \"next Friday\" with no weekday anchor or an ambiguous time like \"at 8\" without AM/PM), emit `null` for the unresolved field rather than guessing. When multiple plausible interpretations are equally supported by the transcript, prefer the most specific one; if two interpretations remain equally plausible, emit `null` for the unresolved field rather than guessing. If the transcript mentions recurring meetings, all-day meetings, or multi-day events, preserve them as a single event only when the recurrence is explicit; otherwise emit a single event with the best available start/end times and note the ambiguity in `notes`.\n3. **Push** — pipe the events array into `node scripts/calendar-extractor.js <userId> push`. The script:\n   - dedups against per-user local state (`data/users/<userId>.json` → `seen` map, 30-day TTL),\n   - best-effort mirrors the **new** events to `POST /api/skill/data` (upsert by `dedup_key`) **tagged `status: \"pending\"`** so the server stores them pending and the iOS calendar table shows them greyed/dashed with **Confirm · Discard** — an event becomes solid only when the user taps **Confirm** (Discard deletes it). Each mirrored row carries a top-level **`lead_time`** (minutes, default 10) so the server's voice-call adapter can schedule the proactive call at `start − lead_time`; the detail fields (`location`/`attendees`/`notes`) ride in `payload` as the in-call announcement context,\n   - delivers the **new** events **per card** — one `POST http://javis-server:8000/api/agent/push`\n     per event, each `{\"skill\": \"calendar-extractor\", \"content\": \"<markdown>\", \"dedup_key\": \"<event dedup_key>\"}`.\n     The server routes each push (carrying its `dedup_key`, no explicit `session_id`) into that card's\n     own Agent Chat session, so **every event lands in its own iOS chat thread** instead of one combined\n     digest. The pushes are informational — they are not a confirmation gate.\n\n## Editing a pushed card in-thread\n\nA user can correct a card by **replying in that card's own Agent Chat thread**\n(\"6 pm today\", \"location is Zoom\", \"add Alex\"). The reply edits **that exact row\nin place and confirms it** — no duplicate row, no Confirm tap. The agent drives it:\n\n1. **Read the injected `[CURRENT CARD]` block.** When an agent turn runs inside a\n   card thread, the server injects a `[CURRENT CARD]` block carrying the card's\n   **original `dedup_key` (verbatim)** plus its current fields (`title`,\n   `start_at`, `end_at`, `location`, `attendees`, `notes`, `lead_time`, `status`).\n   This is the source of truth for the row you are editing. Carry `lead_time`\n   forward in the merged patch (default 10 if the block omits it) so an edit never\n   silently resets the voice-call lead; set a new integer only if the user asks to\n   change the heads-up (\"ring me 30 min before\" → `lead_time: 30`).\n2. **Run `anchor` for a fresh \"now\".** `node scripts/calendar-extractor.js anchor`\n   prints `{ reference_time, reference_date, reference_weekday, reference_time_utc,\n   tz }` for the **current** clock. The chat happens *later* than extraction, so\n   the original fetch anchor is stale — resolve \"today / 6 pm / tomorrow\" against\n   this fresh anchor (same date-resolution discipline as extraction: anchor on\n   `reference_time`/`reference_date`, never your own \"today\").\n   - **The anchor's `tz` is the user's calendar zone.** `anchor` resolves it from\n     the server (the authoritative zone) — **not** the container's `TZ`, which is\n     empty/UTC in prod and would shift \"today\" forward a day west of UTC in the\n     evening. Pass `[CURRENT CARD]`'s `tz` as `--tz <IANA>` if it carries one;\n     otherwise a bare `anchor` already returns the correct zone. **Use the\n     `reference_date` it prints — do not compute \"today\" from a UTC clock.**\n3. **Resolve the correction; null-not-guess.** Resolve the user's change against\n   the anchor and emit a full ISO 8601 instant **with the explicit `tz` UTC\n   offset** (e.g. `2026-06-22T18:00:00-07:00`). If the change is ambiguous or\n   unresolvable (e.g. \"at 8\" with no AM/PM, no weekday anchor), **do not call\n   `update`** — ask a follow-up in-thread instead. Emit `null` rather than guess.\n4. **Merge into a FULL patch (wholesale-payload rule).** The server overwrites\n   `payload`/`start_at`/`end_at` **wholesale** on the matched row, so the `patch`\n   must carry the **complete intended state** — the current fields from\n   `[CURRENT CARD]` **merged with** the user's change, not just the changed field.\n   A time-only edit must still resend `title`/`location`/`attendees`/`notes`, or\n   they would be blanked.\n   - **Times you are NOT changing:** copy the `start_at`/`end_at` strings from\n     `[CURRENT CARD]` **verbatim**. Those are already naive-local wall-clock in the\n     card zone (no offset) and the script passes a zoneless `YYYY-MM-DDTHH:MM:SS`\n     value through unchanged — it will **not** re-interpret it in the runner's\n     zone. (A time you *are* changing must be a full offset-bearing instant per\n     step 3, so the script can collapse it correctly.)\n   - **Pass the card zone.** Include the card's `tz` (the value the `anchor` step\n     printed) as a top-level `tz` so the script collapses any offset-bearing\n     changed time against the user's calendar zone, not the container's process\n     zone (which is empty/UTC in prod, not the user's zone). If you omit `tz`,\n     `update` resolves the user's zone from the server itself — but passing the\n     anchor's `tz` is preferred (explicit, and avoids a second lookup).\n5. **Run `update` with the verbatim `dedup_key`.**\n\n   ```bash\n   echo '{\n     \"dedup_key\": \"<the [CURRENT CARD] dedup_key, VERBATIM>\",\n     \"tz\": \"America/Los_Angeles\",\n     \"patch\": {\n       \"title\": \"Design Review\", \"location\": \"Zoom\",\n       \"attendees\": [\"Sam\"], \"notes\": \"bring laptop\",\n       \"lead_time\": 10,\n       \"start_at\": \"2026-06-22T18:00:00-07:00\",\n       \"end_at\":   \"2026-06-22T19:00:00-07:00\"\n     }\n   }' | node scripts/calendar-extractor.js update\n   ```\n\n   The script POSTs **one** `/api/skill/data` upsert with that verbatim key and\n   `status: \"confirmed\"`. It does **NOT** recompute the key from the new time\n   (that would create a second row — the whole bug), writes `start_at`/`end_at`\n   as **naive-local** wall-clock (no `Z`/offset), does **no `seen`-dedup\n   filtering**, and does **not** call `/api/agent/push`.\n\n**Auto-confirm semantics.** Stating the corrected value in chat *is* the\nconfirmation: the upsert's `status: \"confirmed\"` flips the row `pending →\nconfirmed` **atomically with the field write** (strictly that direction —\nconfirmed never downgrades). The skill never calls a separate `/confirm`\nendpoint. The iOS card re-render and the live chat reply are the user feedback.\nIf `/api/skill/data` fails, the script reports the error (non-silent) — tell the\nuser the card couldn't be updated; do **not** claim success.\n\nThis path is **edit-only** of an existing card — creating new events from chat,\nre-opening a confirmed row, or deleting via chat (Discard covers delete) are out\nof scope.\n\n## How this skill is invoked\n\nThis skill has **two triggers** (dispatcher auto-run and manual).\n\n1. **Dispatcher auto-run (automatic).** When a unit of input completes (an audio\n   session ends or a keyboard input is saved), the javis-server **session dispatcher**\n   invokes this skill's agent directly, alongside every other enabled, `risk: low`\n   skill — no classifier, no route matching. The server claims run-once\n   (`DispatchSkillInvoked (user_id, unit_key, skill)`) and **AUTO-RUNS this skill\n   directly — there is no approve-to-run proposal card**. It runs in the user's\n   container with a prompt of the form `Run /calendar-extractor for <unit>.\n   Deliverable: …`, where the deliverable text is an **advisory HINT**, not a\n   classification — the agent may use it alongside the transcript, but should still\n   read the unit transcript for full detail (time, attendees), and **this skill's own\n   agent decides for itself, using this `SKILL.md`, whether the unit is worth acting\n   on; if not, it does nothing.** When it does act, it parses `<unit>`\n   (`audio:<session_id>` or `kbd:<keyboard_input.id>`), runs `fetch --session <id>` /\n   `fetch --kbd-input <id>`, extracts events, and pushes them. **The human gate is not\n   running the skill — it is Confirm/Discard on the produced events:** every extracted\n   event is written **PENDING** to the calendar table (greyed/dashed with\n   **Confirm · Discard**) and becomes solid only when the user taps **Confirm**\n   (Discard deletes it). **The skill does not self-gate on Confirm/Discard** — the\n   server owns run-once (whether to invoke at all), while the skill's own agent owns\n   relevance (whether to act once invoked).\n2. **Manual (\"today's meetings\").** On demand, the agent runs the windowed\n   `fetch` (last 24h by default) → extracts → pushes. Repeating the ask re-runs\n   extraction on the window; the `seen` map still prevents duplicate delivery.\n   Manual writes are also **PENDING** (consistent \"confirm to keep\").\n\nThe auto-dispatch contract the javis-server team must satisfy (eligibility seeded\nfrom this file's `metadata.routes` block, collapsed server-side to a single\nskill-level `risk`, plus the run prompt shape) is declared in this file's\n`metadata.routes` block and documented in `references/route-contract.md`.\n\n## Notes\n\n- **Runtime dependencies** — the extractor script uses Node 18+ built-ins only (`fetch`, `fs`, `path`); no `npm install` is needed for the script runtime.\n- **Data sources**: audio recording transcripts **and** keyboard-dictation sessions — both via\n  `GET /api/transcripts/recent` (gateway-token authed), each session carrying a `source` field\n  (`\"audio\"` | `\"keyboard\"`); a single keyboard unit resolves via\n  `GET /api/transcripts/keyboard-input/<id>`. Plus per-user local state (dedup memory). There is no\n  `HTTP_SOURCE_URL` — the script talks to javis-server directly.\n- **Events are written PENDING.** Every event mirrored to `/api/skill/data` carries\n  `status: \"pending\"`; the server stores it pending and the iOS calendar table renders it\n  greyed/dashed with **Confirm · Discard**. **Confirm** promotes the row to solid (`confirmed`);\n  **Discard** deletes it. This is the human gate — there is no approve-to-run proposal card.\n- **Dedup is local-state-authoritative, event-level only.** The container's gateway token can\n  WRITE to `/api/skill/data` but cannot read it back (`GET /api/skill/data` requires a Clerk JWT),\n  so novelty is decided by the local `seen` map; the server write is a best-effort mirror for the\n  iOS app. There is **no per-unit gating** in the skill — the server owns run-once\n  (`DispatchRouteExecuted`); the human gate is Confirm/Discard on the pending rows. So the only\n  local dedup is the event-level `seen` map (`{ \"<event-key>\": \"<ts>\" }` in\n  `data/users/<userId>.json`, 30-day TTL-pruned). It keeps a duplicate event from re-reaching the\n  table/chat across overlapping manual windows or a re-run.\n- **Timezone**: there is **no prefs file**. On the `fetch`/extract path the skill resolves tz in\n  order: the `tz` field on the fetch payload → the `TZ` environment variable → the system zone\n  (`Intl.DateTimeFormat().resolvedOptions().timeZone`). The manual and dispatcher paths resolve tz\n  the same way. **Edit turns (`update`/`anchor`) have no fetch payload**, so they resolve:\n  explicit (`--tz` / stdin `tz` / `[CURRENT CARD]` tz) → **the server's authoritative zone**\n  (a best-effort `GET /api/transcripts/recent` reads the same `tz` field) → `TZ` env → system.\n  The server step exists because the prod per-user container has an **empty `TZ` (→ UTC)**, which\n  shifted \"today\" forward a day on evening edits west of UTC. The proper long-term fix is to put\n  the user's `tz` in the server's `[CURRENT CARD]` block and/or set `TZ` on the container — see\n  `docs/superpowers/specs/2026-06-22-calendar-edit-timezone-fix.md`.\n- **Markdown, not native cards.** A push delivers a `content` string rendered as markdown on iOS\n  (`MDBlock`). Native `EventList`/`EventCard` blocks are emitted only during a live SSE agent turn\n  (`_maybe_emit_chat_block`), not via the push path — so each per-card push is rich markdown by design.\n- **User IDs** only allow letters, digits, `-`, `_` (path-traversal guard in `data.js`).\n- **Backgrounded/killed iOS app**: `AGENT_PUSH` is WebSocket-only (no APNs). For mission-critical\n  delivery, add a Telegram channel as backup.\n\nFile v0.7.0:README.md\n\n# Calendar Extractor\n\n> ⚠️ **Requires the HiJavis iPhone app.** This skill runs inside HiJavis — install the app first, then it's ready to use.\n> 📲 https://apps.apple.com/us/app/hijavis/id6745134765\n\nCalendar Extractor finds the plans hiding in your conversations. HiJavis listens, spots anything that sounds like a meeting or appointment, and hands you a tidy list right in your chat.\n\n## Picture this\n\n- You finish a day of back-to-back calls and can't recall what you promised anyone. You ask, \"What did I agree to today?\" and there it is, all in one list.\n- A friend says, \"Let's grab coffee Friday.\" You'd normally forget by lunch. HiJavis already caught it.\n- It's 8am. You open the app and see everything you committed to today, before your first sip of coffee.\n- You leave a conference where you made a dozen plans out loud, and they're all waiting for you in a list.\n- The house is buzzing with \"dentist Thursday,\" \"soccer practice moved to 5,\" \"dinner Saturday at the new place.\" HiJavis keeps track so nobody drops the ball.\n\nIf any of those sound like you, this one's for you.\n\n## What it does\n\nHiJavis listens to your conversations and meetings. Calendar Extractor reads back through your recent recordings and spots anything that sounds like a plan or appointment, such as \"let's meet Tuesday at 3,\" \"dinner Saturday at the new place,\" or \"call me tomorrow afternoon.\"\n\nThen it sends each event to your chat as its own card — with the title, date, time, place, who's involved, and any notes it picked up. Every event opens in its own chat thread, so you can act on one plan without losing the rest.\n\n## How to use it\n\n### Just ask\n\nOpen your HiJavis chat and ask. Any of these works:\n\n- \"today's meetings\"\n- \"calendar extract\"\n- \"今日会议\"\n- \"提取日历\"\n- Or a natural question like \"What meetings do I have coming up?\" or \"Did I agree to anything today?\"\n\nHiJavis scans your recent recordings and replies with your list in seconds. Nothing to set up first.\n\n### Get a daily summary\n\nWant it to come to you? Ask HiJavis to send a summary on a schedule. By default you'll get one each morning (around 8am) and each evening (around 6pm), but you can pick your own times. Just say something like \"send me my calendar summary every morning at 7,\" and it'll start showing up on its own.\n\n## What makes it handy\n\n- **It gets how you talk about time.** You don't say \"March 14th at 2:00 PM\" out loud. You say \"tomorrow,\" \"next Thursday,\" \"around 6,\" \"noon.\" Calendar Extractor turns all of that into a real date and time in your time zone.\n- **It's instant.** The moment a recording or a typed note finishes, it pulls out any plans right then, so your list is ready before you even ask.\n- **One card per plan.** Each event arrives as its own card in its own chat thread, so they never pile into one wall of text and you can deal with them one at a time.\n- **It never repeats itself.** It remembers what it already showed you, so the same event won't clutter your chat twice.\n- **It works in English and Chinese.** It keeps up smoothly with both.\n\n## Real-life examples\n\n- **After back-to-back calls:** ask \"What did I agree to today?\" instead of replaying every recording.\n- **The offhand plan:** \"Let's grab coffee Friday\" gets caught, even when you'd have forgotten.\n- **Your morning briefing:** open the app and see everything on your plate for the day.\n- **After a conference:** all those verbal plans land in one neat list.\n- **The family hub:** \"dentist Thursday\" and \"soccer practice moved to 5\" stay organized so nobody forgets.\n\n## Good to know\n\nFor scheduled summaries to arrive on time, keep the HiJavis app open and running. If it's fully closed, a summary may wait until you reopen the app. Asking for your meetings on the spot works anytime.\n\nFile v0.7.0:_meta.json\n\n{\n  \"ownerId\": \"kn73qn9mdc6qf6hnnz3s26pgch845n33\",\n  \"slug\": \"calendar-extractor\",\n  \"version\": \"0.7.0\",\n  \"publishedAt\": 1783017811631\n}\n\nFile v0.7.0:references/route-contract.md\n\n# calendar-extractor — route contract (for the javis-server team)\n\nThis document specifies the interface the **javis-server session-dispatcher**\ncontrol-plane must satisfy so the dispatcher can route agenda deliverables to the\n`calendar-extractor` skill. It is the contract only — no javis-server code is\nimplemented in this repo.\n\nSee the design spec:\n`docs/superpowers/specs/2026-06-08-calendar-extractor-dispatcher-adaptation-design.md`\n(sections 3a–3c) and the dispatcher spec it adapts to\n(javis-server `docs/superpowers/specs/2026-06-06-session-dispatcher-server-variant.md`,\nimplemented in `app/services/dispatcher_service.py`).\n\nThe same contract is declared, discoverably, in the skill's `SKILL.md`\n`metadata.routes` block. That static block declares the skill-owned fields\n(`route_id`, `skill`, `matches`, `args_template`, `risk`); `enabled` is omitted\nthere because it is per-user runtime state the server owns, not skill metadata.\n\n## 3a. RouteRegistry row\n\nSeed this row per user, enabling it when the user enables `calendar-extractor`:\n\n| column | value |\n|---|---|\n| `route_id` | `\"calendar\"` |\n| `skill` | `\"calendar-extractor\"` |\n| `matches` | `\"meetings, appointments, events, agenda, scheduling, dates/times mentioned\"` |\n| `args_template` | `null` (the unit + the prepended SKILL.md carry everything the skill needs) |\n| `risk` | `\"low\"` (read-only transcript extraction + a push; no destructive side effects) |\n| `enabled` | set `true` when the user enables calendar-extractor; `false`/absent otherwise |\n\nThe dispatcher only routes a deliverable to this skill when an **enabled** row with\n`route_id=\"calendar\"` exists for that user. No enabled row → the skill is never\nscheduled.\n\n## 3b. `classify_and_route` deliverable shape\n\nFeed the user's enabled route catalog (each row's `route_id` + `matches`) into the\n`classify_and_route` task prompt. For a transcript containing scheduling content,\nthe classifier must emit a deliverable shaped like:\n\n```json\n{\n  \"id\": \"<uuid>\",\n  \"title\": \"Extract calendar events\",\n  \"description\": \"<short summary of the agenda found>\",\n  \"route_id\": \"calendar\",\n  \"confidence\": 0.0\n}\n```\n\n- `route_id` MUST be exactly `\"calendar\"` so it matches the RouteRegistry row.\n- No scheduling content in the transcript → **no** `calendar` deliverable → no\n  proposal card. (This is the correct, silent outcome.)\n\nThe dispatcher then matches the deliverable's `route_id` against the enabled\n`RouteRegistry` row, persists a `DispatchProposal`, and pushes a proposal card to\niOS. On the user's Approve, it claims run-once\n(`DispatchRouteExecuted (user, unit, route)` via a unique constraint, before\nscheduling) and triggers the skill.\n\n## 3c. Prompt contract\n\nThe skill relies only on the `<unit>` being present and `_UNIT_RE`-valid in the run\nprompt — which the generic dispatcher prompt already provides:\n\n```\nRun /calendar-extractor for <unit>. Deliverable: <title>. <description>\nFetch that unit's transcript, produce the deliverable, and push the result.\n```\n\n- `<unit>` is `audio:<session_id>` or `kbd:<keyboard_input.id>`. The skill parses it\n  and runs `fetch --session <session_id>` or `fetch --kbd-input <keyboard_input.id>`.\n- **No calendar-specific prompt enrichment is required.** `args_template` stays\n  `null` and `_deliverable_prompt` needs no calendar-specific change.\n- All date-resolution discipline rides in (1) the relative-date **anchor** the\n  `fetch` payload emits on every path\n  (`reference_time` / `reference_date` / `reference_weekday` / `reference_time_utc`\n  + `tz`) and (2) the **prepended SKILL.md** instructions. The server need not\n  enrich the prompt to preserve date correctness.\n\n## Idempotency ownership\n\nThe skill **does not** self-gate per unit. Run-once is owned entirely by the server:\n\n- the human approval gate (a proposal must be approved before the skill runs), and\n- `DispatchRouteExecuted (user, unit, route)` claimed via a unique constraint\n  **before** scheduling (closes the TOCTOU window).\n\nThe skill keeps only an **event-level** `seen` map so the same event is never\ndelivered twice across overlapping manual 24h windows or a re-run. It is safe to\ninvoke the skill more than once: `seen` prevents duplicate delivery and the server\nprevents duplicate invocation.\n\n## Open items for the server team\n\n- Seed/enable the `RouteRegistry` row for `calendar` when a user enables\n  calendar-extractor.\n- Feed the enabled route catalog (`route_id` + `matches`) into the\n  `classify_and_route` task prompt.\n\nFile v0.7.0:skill-card.md\n\n## Description: <br>\nExtracts calendar events from recent voice and keyboard transcripts and pushes each detected event to HiJavis iOS chat as a pending markdown card for user review. <br>\n\nThis skill is ready for commercial/non-commercial use. <br>\n\n## Publisher: <br>\n[samuel-wei](https://clawhub.ai/user/samuel-wei) <br>\n\n### License/Terms of Use: <br>\nMIT-0 <br>\n\n\n## Use Case: <br>\nHiJavis users use this skill to turn recent recordings and keyboard transcripts into calendar-ready event cards. It is useful for capturing meetings, appointments, reminders, locations, attendees, notes, and follow-up edits from natural language. <br>\n\n### Deployment Geography for Use: <br>\nGlobal <br>\n\n## Known Risks and Mitigations: <br>\nRisk: The skill automatically inspects completed voice and keyboard transcripts, including transcripts that were not explicit calendar requests. <br>\nMitigation: Install only when users understand the automatic transcript review behavior and can disable the skill when broad transcript processing is not appropriate. <br>\nRisk: Transcript-derived event details can appear as pending rows and iOS chat cards before the user confirms them. <br>\nMitigation: Treat generated cards as pending suggestions and review, confirm, or discard each event before relying on it as a calendar commitment. <br>\n\n\n## Reference(s): <br>\n- [Route Contract](references/route-contract.md) <br>\n- [Calendar Extractor on ClawHub](https://clawhub.ai/samuel-wei/skills/calendar-extractor) <br>\n- [HiJavis iPhone App](https://apps.apple.com/us/app/hijavis/id6745134765) <br>\n\n\n## Skill Output: <br>\n**Output Type(s):** [text, markdown, JSON, shell commands, guidance] <br>\n**Output Format:** [Markdown event cards, JSON event arrays, and Node.js command-line workflow guidance] <br>\n**Output Parameters:** [1D] <br>\n**Other Properties Related to Output:** [Events are deduplicated locally, mirrored as pending rows, and intended for user confirmation or discard in HiJavis.] <br>\n\n## Skill Version(s): <br>\n0.7.0 (source: package.json and server release evidence) <br>\n\n## Ethical Considerations: <br>\nUsers should evaluate whether this skill is appropriate for their environment, review any generated or modified files before relying on them, and apply their organization's safety, security, and compliance requirements before deployment. <br>\n\nFile v0.7.0:data/users/self.json\n\n{\n  \"userId\": \"self\",\n  \"name\": \"\",\n  \"createdAt\": \"2026-06-08T21:20:53.749Z\",\n  \"seen\": {\n    \"2026-06-04|standup|2026-06-04T17:00:00.000Z\": \"2026-06-22T19:58:24.742Z\",\n    \"2026-06-04|design review|2026-06-04T20:00:00.000Z\": \"2026-06-22T19:58:24.742Z\",\n    \"2026-06-05|dinner|2026-06-05T01:00:00.000Z\": \"2026-06-22T19:58:24.742Z\",\n    \"2026-06-23|standup|2026-06-23T17:00:00.000Z\": \"2026-06-23T00:51:50.251Z\",\n    \"2026-06-23|freshmeeting|2026-06-23T17:00:00.000Z\": \"2026-06-23T00:52:11.027Z\"\n  },\n  \"lastRunAt\": \"2026-06-23T00:52:11.027Z\"\n}\n\nFile v0.7.0:data/users/self.prefs.json\n\n{\n  \"time\": \"08:00\",\n  \"channel\": \"iOS\",\n  \"tz\": \"America/Los_Angeles\",\n  \"enabledAt\": \"2026-05-30T18:44:26.998Z\"\n}\n\nFile v0.7.0:package.json\n\n{\n  \"name\": \"calendar-extractor\",\n  \"version\": \"0.7.0\",\n  \"description\": \"Extract calendar events from recent recording and keyboard transcripts and push a markdown digest to your iOS chat. Runs on demand, and the javis-server dispatcher also invokes it directly for every completed unit — no classifier, no route matching; this skill's own agent decides relevance itself. Extracted events are written PENDING and become solid only when the user taps Confirm in the iOS calendar table. Triggers: 'today's meetings', 'calendar extract', '今日会议', '提取日历'.\",\n  \"keywords\": [\"today's meetings\", \"calendar extract\", \"今日会议\", \"提取日历\"],\n  \"license\": \"MIT\",\n  \"scripts\": {\n    \"register\": \"node scripts/register.js\",\n    \"fetch\": \"node scripts/calendar-extractor.js\",\n    \"push\": \"node scripts/calendar-extractor.js\",\n    \"test\": \"node --test\"\n  },\n  \"engines\": { \"node\": \">=18\" }\n}\n\nArchive v0.6.0: 16 files, 46516 bytes\n\nFiles: data/users/self.json (543b), data/users/self.prefs.json (115b), package.json (816b), README.md (3781b), references/route-contract.md (4506b), scripts/calendar-extractor.js (32659b), scripts/data.js (2133b), scripts/lib.js (8553b), scripts/register.js (612b), skill-card.md (2472b), SKILL.md (19248b), test/cli.test.js (16251b), test/data.test.js (3033b), test/lib.test.js (10470b), test/update.test.js (20771b), _meta.json (137b)\n\nFile v0.6.0:SKILL.md\n\n---\nname: calendar-extractor\ndescription: Extract calendar events from recent recording and keyboard transcripts and push them to your iOS chat as one markdown card per event, each landing in its own Agent Chat thread. Use on demand when the user asks for \"today's meetings\" / \"calendar extract\" / \"今日会议\" / \"提取日历\", and fetch the last 24 hours of transcript data by default. The javis-server session dispatcher also AUTO-RUNS this skill (no approve-to-run card) when a completed unit matches the calendar route, passing a deliverable hint in the run prompt that the agent may use alongside the transcript; extracted events are written PENDING and become solid only when the user taps Confirm in the iOS calendar table. If the user asks for \"today's meetings\", use the local day defined by the fetched reference_date field. Triggers: 'today's meetings', 'calendar extract', '今日会议', '提取日历'.\nkeywords: today's meetings, calendar extract, 今日会议, 提取日历, calendar-extractor\nmetadata:\n  openclaw:\n    runtime:\n      node: \">=18\"\n  routes:\n    - route_id: calendar\n      skill: calendar-extractor\n      matches: \"meetings, appointments, events, agenda, scheduling, dates/times mentioned\"\n      args_template: null\n      risk: low\n---\n\n# Calendar Extractor\n\n> Extract calendar events from recent recording and keyboard transcripts and push them to your iOS chat — one markdown card per event, each landing in its own Agent Chat thread. Fetch the last 24 hours of transcript data by default. If the user asks for \"today's meetings\", use the local day defined by the fetched reference_date field. Generate one digest per local day in the user's timezone, containing all events that occur on reference_date and any events explicitly mentioned as happening today.\n\n## When to use\n\n- \"today's meetings\"\n- \"calendar extract\"\n- \"今日会议\"\n- \"提取日历\"\n- Automatically, when the javis-server session dispatcher **auto-runs** this skill after a completed unit matches the calendar route — no approve-to-run card (see \"How this skill is invoked\").\n\n## Core commands\n\n> **`<userId>` is optional.** Omit it and it defaults to `self`. Each HiJavis user\n> runs in their own openclaw container, so `self` is correctly isolated; the gateway\n> token (not the userId) authenticates every server call. No registration is needed\n> to start — pass an explicit ID only if you run multiple profiles in one container.\n\n```bash\n# Step 1 — fetch recent transcripts as JSON (the agent extracts events from this)\nnode scripts/calendar-extractor.js fetch [--hours N] [--limit N]\n\n# Step 1 (dispatcher run) — fetch ONE completed unit (the auto-run dispatcher unit)\nnode scripts/calendar-extractor.js fetch --session <sessionId> [--hours N]   # audio unit\nnode scripts/calendar-extractor.js fetch --kbd-input <inputId> [--hours N]   # keyboard unit\n\n# Step 2 — push: pipe the extracted-events JSON array to stdin; dedups (seen) + delivers to iOS\necho '<events-json-array>' | node scripts/calendar-extractor.js push\n\n# anchor — print the CURRENT relative-date anchor (resolve \"today / 6 pm / tomorrow\" on an edit turn)\nnode scripts/calendar-extractor.js anchor            # optional: --tz <IANA> for the card's calendar zone\n\n# update — edit ONE pushed card in place (verbatim dedup_key, full merged patch, auto-confirm)\necho '{\"dedup_key\":\"<verbatim>\",\"patch\":{...}}' | node scripts/calendar-extractor.js update\n\n# Optional: explicit userId / multi-profile (back-compat — prepend the ID)\nnode scripts/register.js <userId> <name>\nnode scripts/calendar-extractor.js <userId> fetch\necho '<events-json-array>' | node scripts/calendar-extractor.js <userId> push\nnode scripts/calendar-extractor.js <userId> anchor\necho '{\"dedup_key\":\"<verbatim>\",\"patch\":{...}}' | node scripts/calendar-extractor.js <userId> update\n```\n\n## Workflow\n\nThis skill is a two-step pipeline: the **script** does the I/O (fetch transcripts, dedup, push),\nthe **agent/LLM** does the reasoning (extract events). Extraction is not hardcoded — the agent\nreads the fetched transcripts and emits a JSON array of events.\n\n1. **Fetch** — `node scripts/calendar-extractor.js <userId> fetch` issues\n   `GET http://javis-server:8000/api/transcripts/recent?since=…&limit=…` with the\n   `OPENCLAW_GATEWAY_TOKEN` bearer and prints\n   `{ \"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 } ] }`. If fetch fails, returns invalid JSON, or yields zero sessions, output an empty events array and do not push anything; report the failure only if the user asked for a diagnostic. If the fetch response contains no sessions or the transcript text is empty, return `[]` and do not attempt to push a digest.\n2. **Extract** — the agent reads that JSON and produces an events array. Each event:\n   `{ \"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), \"lead_time\" (minutes before start for the \"Javis calls you\" voice alert, optional — defaults to 10) }`.\n   Carry `source_kind` through so provenance flows to the `/api/skill/data` mirror and the iOS digest.\n   **`lead_time`** is the per-event minutes-before-start at which the proactive voice-call engine rings the user (`fire = start − lead_time`). Omit it for the standard 10-minute lead; emit an integer only when the transcript states a different desired heads-up (e.g. \"remind me an hour before the flight\" → `60`). Negative/invalid values fall back to 10. The detail fields (`location`, `attendees`, `notes`) double as the announcement context the in-call **Details** command speaks, so keep them populated when the transcript provides them.\n   **Date resolution (required):** the top-level `reference_time` is **already local\n   wall-clock in `tz`** (zoneless) and `reference_date`/`reference_weekday` give the local\n   \"today\" — so \"today\" == `reference_date`, and \"tomorrow\"/\"Saturday\"/\"next Thursday\" count\n   forward from `reference_weekday`. Do **not** re-apply the tz offset and do **not** anchor on\n   `reference_time_utc` (the raw instant, whose date can be the *next* day in the evening — that\n   is exactly the off-by-one this field avoids). Fall back to the session's `started_at` only if\n   `reference_time` is absent. Never use your own sense of \"today\". Emit each event's `start_at`/\n   `end_at` as a full ISO 8601 instant **with the explicit `tz` UTC offset** (e.g. an 8 PM event\n   in `America/Los_Angeles` → `2026-06-04T20:00:00-07:00`) — not a zoneless string, which the\n   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\n   party at 6pm\" → 18:00). If the transcript does not provide enough information to determine a unique date/time (for example, only \"next Friday\" with no weekday anchor or an ambiguous time like \"at 8\" without AM/PM), emit `null` for the unresolved field rather than guessing. When multiple plausible interpretations are equally supported by the transcript, prefer the most specific one; if two interpretations remain equally plausible, emit `null` for the unresolved field rather than guessing. If the transcript mentions recurring meetings, all-day meetings, or multi-day events, preserve them as a single event only when the recurrence is explicit; otherwise emit a single event with the best available start/end times and note the ambiguity in `notes`.\n3. **Push** — pipe the events array into `node scripts/calendar-extractor.js <userId> push`. The script:\n   - dedups against per-user local state (`data/users/<userId>.json` → `seen` map, 30-day TTL),\n   - best-effort mirrors the **new** events to `POST /api/skill/data` (upsert by `dedup_key`) **tagged `status: \"pending\"`** so the server stores them pending and the iOS calendar table shows them greyed/dashed with **Confirm · Discard** — an event becomes solid only when the user taps **Confirm** (Discard deletes it). Each mirrored row carries a top-level **`lead_time`** (minutes, default 10) so the server's voice-call adapter can schedule the proactive call at `start − lead_time`; the detail fields (`location`/`attendees`/`notes`) ride in `payload` as the in-call announcement context,\n   - delivers the **new** events **per card** — one `POST http://javis-server:8000/api/agent/push`\n     per event, each `{\"skill\": \"calendar-extractor\", \"content\": \"<markdown>\", \"dedup_key\": \"<event dedup_key>\"}`.\n     The server routes each push (carrying its `dedup_key`, no explicit `session_id`) into that card's\n     own Agent Chat session, so **every event lands in its own iOS chat thread** instead of one combined\n     digest. The pushes are informational — they are not a confirmation gate.\n\n## Editing a pushed card in-thread\n\nA user can correct a card by **replying in that card's own Agent Chat thread**\n(\"6 pm today\", \"location is Zoom\", \"add Alex\"). The reply edits **that exact row\nin place and confirms it** — no duplicate row, no Confirm tap. The agent drives it:\n\n1. **Read the injected `[CURRENT CARD]` block.** When an agent turn runs inside a\n   card thread, the server injects a `[CURRENT CARD]` block carrying the card's\n   **original `dedup_key` (verbatim)** plus its current fields (`title`,\n   `start_at`, `end_at`, `location`, `attendees`, `notes`, `lead_time`, `status`).\n   This is the source of truth for the row you are editing. Carry `lead_time`\n   forward in the merged patch (default 10 if the block omits it) so an edit never\n   silently resets the voice-call lead; set a new integer only if the user asks to\n   change the heads-up (\"ring me 30 min before\" → `lead_time: 30`).\n2. **Run `anchor` for a fresh \"now\".** `node scripts/calendar-extractor.js anchor`\n   prints `{ reference_time, reference_date, reference_weekday, reference_time_utc,\n   tz }` for the **current** clock. The chat happens *later* than extraction, so\n   the original fetch anchor is stale — resolve \"today / 6 pm / tomorrow\" against\n   this fresh anchor (same date-resolution discipline as extraction: anchor on\n   `reference_time`/`reference_date`, never your own \"today\").\n   - **The anchor's `tz` is the user's calendar zone.** `anchor` resolves it from\n     the server (the authoritative zone) — **not** the container's `TZ`, which is\n     empty/UTC in prod and would shift \"today\" forward a day west of UTC in the\n     evening. Pass `[CURRENT CARD]`'s `tz` as `--tz <IANA>` if it carries one;\n     otherwise a bare `anchor` already returns the correct zone. **Use the\n     `reference_date` it prints — do not compute \"today\" from a UTC clock.**\n3. **Resolve the correction; null-not-guess.** Resolve the user's change against\n   the anchor and emit a full ISO 8601 instant **with the explicit `tz` UTC\n   offset** (e.g. `2026-06-22T18:00:00-07:00`). If the change is ambiguous or\n   unresolvable (e.g. \"at 8\" with no AM/PM, no weekday anchor), **do not call\n   `update`** — ask a follow-up in-thread instead. Emit `null` rather than guess.\n4. **Merge into a FULL patch (wholesale-payload rule).** The server overwrites\n   `payload`/`start_at`/`end_at` **wholesale** on the matched row, so the `patch`\n   must carry the **complete intended state** — the current fields from\n   `[CURRENT CARD]` **merged with** the user's change, not just the changed field.\n   A time-only edit must still resend `title`/`location`/`attendees`/`notes`, or\n   they would be blanked.\n   - **Times you are NOT changing:** copy the `start_at`/`end_at` strings from\n     `[CURRENT CARD]` **verbatim**. Those are already naive-local wall-clock in the\n     card zone (no offset) and the script passes a zoneless `YYYY-MM-DDTHH:MM:SS`\n     value through unchanged — it will **not** re-interpret it in the runner's\n     zone. (A time you *are* changing must be a full offset-bearing instant per\n     step 3, so the script can collapse it correctly.)\n   - **Pass the card zone.** Include the card's `tz` (the value the `anchor` step\n     printed) as a top-level `tz` so the script collapses any offset-bearing\n     changed time against the user's calendar zone, not the container's process\n     zone (which is empty/UTC in prod, not the user's zone). If you omit `tz`,\n     `update` resolves the user's zone from the server itself — but passing the\n     anchor's `tz` is preferred (explicit, and avoids a second lookup).\n5. **Run `update` with the verbatim `dedup_key`.**\n\n   ```bash\n   echo '{\n     \"dedup_key\": \"<the [CURRENT CARD] dedup_key, VERBATIM>\",\n     \"tz\": \"America/Los_Angeles\",\n     \"patch\": {\n       \"title\": \"Design Review\", \"location\": \"Zoom\",\n       \"attendees\": [\"Sam\"], \"notes\": \"bring laptop\",\n       \"lead_time\": 10,\n       \"start_at\": \"2026-06-22T18:00:00-07:00\",\n       \"end_at\":   \"2026-06-22T19:00:00-07:00\"\n     }\n   }' | node scripts/calendar-extractor.js update\n   ```\n\n   The script POSTs **one** `/api/skill/data` upsert with that verbatim key and\n   `status: \"confirmed\"`. It does **NOT** recompute the key from the new time\n   (that would create a second row — the whole bug), writes `start_at`/`end_at`\n   as **naive-local** wall-clock (no `Z`/offset), does **no `seen`-dedup\n   filtering**, and does **not** call `/api/agent/push`.\n\n**Auto-confirm semantics.** Stating the corrected value in chat *is* the\nconfirmation: the upsert's `status: \"confirmed\"` flips the row `pending →\nconfirmed` **atomically with the field write** (strictly that direction —\nconfirmed never downgrades). The skill never calls a separate `/confirm`\nendpoint. The iOS card re-render and the live chat reply are the user feedback.\nIf `/api/skill/data` fails, the script reports the error (non-silent) — tell the\nuser the card couldn't be updated; do **not** claim success.\n\nThis path is **edit-only** of an existing card — creating new events from chat,\nre-opening a confirmed row, or deleting via chat (Discard covers delete) are out\nof scope.\n\n## How this skill is invoked\n\nThis skill has **two triggers** (dispatcher auto-run and manual).\n\n1. **Dispatcher auto-run (automatic).** When a unit of input completes (an audio\n   session ends or a keyboard input is saved), the javis-server **session dispatcher**\n   classifies the transcript. If it finds scheduling content and an enabled `calendar`\n   route matches, the server claims run-once (`DispatchRouteExecuted (user, unit,\n   route)`) and **AUTO-RUNS this skill directly — there is no approve-to-run proposal\n   card**. It runs in the user's container with a prompt of the form\n   `Run /calendar-extractor for <unit>. Deliverable: …`, where the deliverable text is\n   the dispatcher's classification carried as an **advisory HINT** — the agent may use\n   it alongside the transcript, but should still read the unit transcript for full\n   detail (time, attendees). The agent parses `<unit>` (`audio:<session_id>` or\n   `kbd:<keyboard_input.id>`), runs `fetch --session <id>` / `fetch --kbd-input <id>`,\n   extracts events, and pushes them. **The human gate is not running the skill — it is\n   Confirm/Discard on the produced events:** every extracted event is written\n   **PENDING** to the calendar table (greyed/dashed with **Confirm · Discard**) and\n   becomes solid only when the user taps **Confirm** (Discard deletes it). **The skill\n   does not self-gate** — the server owns run-once.\n2. **Manual (\"today's meetings\").** On demand, the agent runs the windowed\n   `fetch` (last 24h by default) → extracts → pushes. Repeating the ask re-runs\n   extraction on the window; the `seen` map still prevents duplicate delivery.\n   Manual writes are also **PENDING** (consistent \"confirm to keep\").\n\nThe route contract the javis-server team must satisfy (RouteRegistry row,\n`classify_and_route` deliverable shape, prompt contract) is declared in this file's\n`metadata.routes` block and documented in `references/route-contract.md`.\n\n## Notes\n\n- **Runtime dependencies** — the extractor script uses Node 18+ built-ins only (`fetch`, `fs`, `path`); no `npm install` is needed for the script runtime.\n- **Data sources**: audio recording transcripts **and** keyboard-dictation sessions — both via\n  `GET /api/transcripts/recent` (gateway-token authed), each session carrying a `source` field\n  (`\"audio\"` | `\"keyboard\"`); a single keyboard unit resolves via\n  `GET /api/transcripts/keyboard-input/<id>`. Plus per-user local state (dedup memory). There is no\n  `HTTP_SOURCE_URL` — the script talks to javis-server directly.\n- **Events are written PENDING.** Every event mirrored to `/api/skill/data` carries\n  `status: \"pending\"`; the server stores it pending and the iOS calendar table renders it\n  greyed/dashed with **Confirm · Discard**. **Confirm** promotes the row to solid (`confirmed`);\n  **Discard** deletes it. This is the human gate — there is no approve-to-run proposal card.\n- **Dedup is local-state-authoritative, event-level only.** The container's gateway token can\n  WRITE to `/api/skill/data` but cannot read it back (`GET /api/skill/data` requires a Clerk JWT),\n  so novelty is decided by the local `seen` map; the server write is a best-effort mirror for the\n  iOS app. There is **no per-unit gating** in the skill — the server owns run-once\n  (`DispatchRouteExecuted`); the human gate is Confirm/Discard on the pending rows. So the only\n  local dedup is the event-level `seen` map (`{ \"<event-key>\": \"<ts>\" }` in\n  `data/users/<userId>.json`, 30-day TTL-pruned). It keeps a duplicate event from re-reaching the\n  table/chat across overlapping manual windows or a re-run.\n- **Timezone**: there is **no prefs file**. On the `fetch`/extract path the skill resolves tz in\n  order: the `tz` field on the fetch payload → the `TZ` environment variable → the system zone\n  (`Intl.DateTimeFormat().resolvedOptions().timeZone`). The manual and dispatcher paths resolve tz\n  the same way. **Edit turns (`update`/`anchor`) have no fetch payload**, so they resolve:\n  explicit (`--tz` / stdin `tz` / `[CURRENT CARD]` tz) → **the server's authoritative zone**\n  (a best-effort `GET /api/transcripts/recent` reads the same `tz` field) → `TZ` env → system.\n  The server step exists because the prod per-user container has an **empty `TZ` (→ UTC)**, which\n  shifted \"today\" forward a day on evening edits west of UTC. The proper long-term fix is to put\n  the user's `tz` in the server's `[CURRENT CARD]` block and/or set `TZ` on the container — see\n  `docs/superpowers/specs/2026-06-22-calendar-edit-timezone-fix.md`.\n- **Markdown, not native cards.** A push delivers a `content` string rendered as markdown on iOS\n  (`MDBlock`). Native `EventList`/`EventCard` blocks are emitted only during a live SSE agent turn\n  (`_maybe_emit_chat_block`), not via the push path — so each per-card push is rich markdown by design.\n- **User IDs** only allow letters, digits, `-`, `_` (path-traversal guard in `data.js`).\n- **Backgrounded/killed iOS app**: `AGENT_PUSH` is WebSocket-only (no APNs). For mission-critical\n  delivery, add a Telegram channel as backup.\n\nFile v0.6.0:README.md\n\n# Calendar Extractor\n\n> ⚠️ **Requires the HiJavis iPhone app.** This skill runs inside HiJavis — install the app first, then it's ready to use.\n> 📲 https://apps.apple.com/us/app/hijavis/id6745134765\n\nCalendar Extractor finds the plans hiding in your conversations. HiJavis listens, spots anything that sounds like a meeting or appointment, and hands you a tidy list right in your chat.\n\n## Picture this\n\n- You finish a day of back-to-back calls and can't recall what you promised anyone. You ask, \"What did I agree to today?\" and there it is, all in one list.\n- A friend says, \"Let's grab coffee Friday.\" You'd normally forget by lunch. HiJavis already caught it.\n- It's 8am. You open the app and see everything you committed to today, before your first sip of coffee.\n- You leave a conference where you made a dozen plans out loud, and they're all waiting for you in a list.\n- The house is buzzing with \"dentist Thursday,\" \"soccer practice moved to 5,\" \"dinner Saturday at the new place.\" HiJavis keeps track so nobody drops the ball.\n\nIf any of those sound like you, this one's for you.\n\n## What it does\n\nHiJavis listens to your conversations and meetings. Calendar Extractor reads back through your recent recordings and spots anything that sounds like a plan or appointment, such as \"let's meet Tuesday at 3,\" \"dinner Saturday at the new place,\" or \"call me tomorrow afternoon.\"\n\nThen it sends each event to your chat as its own card — with the title, date, time, place, who's involved, and any notes it picked up. Every event opens in its own chat thread, so you can act on one plan without losing the rest.\n\n## How to use it\n\n### Just ask\n\nOpen your HiJavis chat and ask. Any of these works:\n\n- \"today's meetings\"\n- \"calendar extract\"\n- \"今日会议\"\n- \"提取日历\"\n- Or a natural question like \"What meetings do I have coming up?\" or \"Did I agree to anything today?\"\n\nHiJavis scans your recent recordings and replies with your list in seconds. Nothing to set up first.\n\n### Get a daily summary\n\nWant it to come to you? Ask HiJavis to send a summary on a schedule. By default you'll get one each morning (around 8am) and each evening (around 6pm), but you can pick your own times. Just say something like \"send me my calendar summary every morning at 7,\" and it'll start showing up on its own.\n\n## What makes it handy\n\n- **It gets how you talk about time.** You don't say \"March 14th at 2:00 PM\" out loud. You say \"tomorrow,\" \"next Thursday,\" \"around 6,\" \"noon.\" Calendar Extractor turns all of that into a real date and time in your time zone.\n- **It's instant.** The moment a recording or a typed note finishes, it pulls out any plans right then, so your list is ready before you even ask.\n- **One card per plan.** Each event arrives as its own card in its own chat thread, so they never pile into one wall of text and you can deal with them one at a time.\n- **It never repeats itself.** It remembers what it already showed you, so the same event won't clutter your chat twice.\n- **It works in English and Chinese.** It keeps up smoothly with both.\n\n## Real-life examples\n\n- **After back-to-back calls:** ask \"What did I agree to today?\" instead of replaying every recording.\n- **The offhand plan:** \"Let's grab coffee Friday\" gets caught, even when you'd have forgotten.\n- **Your morning briefing:** open the app and see everything on your plate for the day.\n- **After a conference:** all those verbal plans land in one neat list.\n- **The family hub:** \"dentist Thursday\" and \"soccer practice moved to 5\" stay organized so nobody forgets.\n\n## Good to know\n\nFor scheduled summaries to arrive on time, keep the HiJavis app open and running. If it's fully closed, a summary may wait until you reopen the app. Asking for your meetings on the spot works anytime.\n\nFile v0.6.0:_meta.json\n\n{\n  \"ownerId\": \"kn73qn9mdc6qf6hnnz3s26pgch845n33\",\n  \"slug\": \"calendar-extractor\",\n  \"version\": \"0.6.0\",\n  \"publishedAt\": 1782358594169\n}\n\nFile v0.6.0:references/route-contract.md\n\n# calendar-extractor — route contract (for the javis-server team)\n\nThis document specifies the interface the **javis-server session-dispatcher**\ncontrol-plane must satisfy so the dispatcher can route agenda deliverables to the\n`calendar-extractor` skill. It is the contract only — no javis-server code is\nimplemented in this repo.\n\nSee the design spec:\n`docs/superpowers/specs/2026-06-08-calendar-extractor-dispatcher-adaptation-design.md`\n(sections 3a–3c) and the dispatcher spec it adapts to\n(javis-server `docs/superpowers/specs/2026-06-06-session-dispatcher-server-variant.md`,\nimplemented in `app/services/dispatcher_service.py`).\n\nThe same contract is declared, discoverably, in the skill's `SKILL.md`\n`metadata.routes` block. That static block declares the skill-owned fields\n(`route_id`, `skill`, `matches`, `args_template`, `risk`); `enabled` is omitted\nthere because it is per-user runtime state the server owns, not skill metadata.\n\n## 3a. RouteRegistry row\n\nSeed this row per user, enabling it when the user enables `calendar-extractor`:\n\n| column | value |\n|---|---|\n| `route_id` | `\"calendar\"` |\n| `skill` | `\"calendar-extractor\"` |\n| `matches` | `\"meetings, appointments, events, agenda, scheduling, dates/times mentioned\"` |\n| `args_template` | `null` (the unit + the prepended SKILL.md carry everything the skill needs) |\n| `risk` | `\"low\"` (read-only transcript extraction + a push; no destructive side effects) |\n| `enabled` | set `true` when the user enables calendar-extractor; `false`/absent otherwise |\n\nThe dispatcher only routes a deliverable to this skill when an **enabled** row with\n`route_id=\"calendar\"` exists for that user. No enabled row → the skill is never\nscheduled.\n\n## 3b. `classify_and_route` deliverable shape\n\nFeed the user's enabled route catalog (each row's `route_id` + `matches`) into the\n`classify_and_route` task prompt. For a transcript containing scheduling content,\nthe classifier must emit a deliverable shaped like:\n\n```json\n{\n  \"id\": \"<uuid>\",\n  \"title\": \"Extract calendar events\",\n  \"description\": \"<short summary of the agenda found>\",\n  \"route_id\": \"calendar\",\n  \"confidence\": 0.0\n}\n```\n\n- `route_id` MUST be exactly `\"calendar\"` so it matches the RouteRegistry row.\n- No scheduling content in the transcript → **no** `calendar` deliverable → no\n  proposal card. (This is the correct, silent outcome.)\n\nThe dispatcher then matches the deliverable's `route_id` against the enabled\n`RouteRegistry` row, persists a `DispatchProposal`, and pushes a proposal card to\niOS. On the user's Approve, it claims run-once\n(`DispatchRouteExecuted (user, unit, route)` via a unique constraint, before\nscheduling) and triggers the skill.\n\n## 3c. Prompt contract\n\nThe skill relies only on the `<unit>` being present and `_UNIT_RE`-valid in the run\nprompt — which the generic dispatcher prompt already provides:\n\n```\nRun /calendar-extractor for <unit>. Deliverable: <title>. <description>\nFetch that unit's transcript, produce the deliverable, and push the result.\n```\n\n- `<unit>` is `audio:<session_id>` or `kbd:<keyboard_input.id>`. The skill parses it\n  and runs `fetch --session <session_id>` or `fetch --kbd-input <keyboard_input.id>`.\n- **No calendar-specific prompt enrichment is required.** `args_template` stays\n  `null` and `_deliverable_prompt` needs no calendar-specific change.\n- All date-resolution discipline rides in (1) the relative-date **anchor** the\n  `fetch` payload emits on every path\n  (`reference_time` / `reference_date` / `reference_weekday` / `reference_time_utc`\n  + `tz`) and (2) the **prepended SKILL.md** instructions. The server need not\n  enrich the prompt to preserve date correctness.\n\n## Idempotency ownership\n\nThe skill **does not** self-gate per unit. Run-once is owned entirely by the server:\n\n- the human approval gate (a proposal must be approved before the skill runs), and\n- `DispatchRouteExecuted (user, unit, route)` claimed via a unique constraint\n  **before** scheduling (closes the TOCTOU window).\n\nThe skill keeps only an **event-level** `seen` map so the same event is never\ndelivered twice across overlapping manual 24h windows or a re-run. It is safe to\ninvoke the skill more than once: `seen` prevents duplicate delivery and the server\nprevents duplicate invocation.\n\n## Open items for the server team\n\n- Seed/enable the `RouteRegistry` row for `calendar` when a user enables\n  calendar-extractor.\n- Feed the enabled route catalog (`route_id` + `matches`) into the\n  `classify_and_route` task prompt.\n\nFile v0.6.0:skill-card.md\n\n## Description: <br>\nExtracts calendar events from recent recording and keyboard transcripts and sends each event to the user's iOS chat as an individual markdown card. <br>\n\nThis skill is ready for commercial/non-commercial use. <br>\n\n## Publisher: <br>\n[samuel-wei](https://clawhub.ai/user/samuel-wei) <br>\n\n### License/Terms of Use: <br>\nMIT-0 <br>\n\n\n## Use Case: <br>\nExternal HiJavis users use this skill to turn spoken or typed scheduling mentions into pending calendar-event cards in iOS chat. It supports on-demand extraction and automatic extraction after a completed recording or keyboard unit matches scheduling content. <br>\n\n### Deployment Geography for Use: <br>\nGlobal <br>\n\n## Known Risks and Mitigations: <br>\nRisk: The skill automatically analyzes recent spoken and typed transcripts, which may include sensitive personal or business information. <br>\nMitigation: Enable it only for users and environments where transcript scanning for scheduling content is acceptable. <br>\nRisk: A correction made in an event's chat thread confirms that calendar row without a separate approval step. <br>\nMitigation: Use this only where chat-based confirmation is an acceptable approval signal; otherwise keep the skill disabled or require external review before relying on confirmed rows. <br>\nRisk: Automatically extracted events may be incomplete or incorrect. <br>\nMitigation: Treat newly extracted events as pending and review Confirm/Discard choices before using them as committed calendar entries. <br>\n\n\n## Reference(s): <br>\n- [ClawHub Skill Page](https://clawhub.ai/samuel-wei/skills/calendar-extractor) <br>\n- [HiJavis iPhone App](https://apps.apple.com/us/app/hijavis/id6745134765) <br>\n- [Route Contract](references/route-contract.md) <br>\n\n\n## Skill Output: <br>\n**Output Type(s):** [text, markdown, JSON, shell commands, guidance] <br>\n**Output Format:** [Markdown cards, JSON event arrays, and shell commands] <br>\n**Output Parameters:** [1D] <br>\n**Other Properties Related to Output:** [Extracted events are initially pending; in-thread corrections can confirm an existing card.] <br>\n\n## Skill Version(s): <br>\n0.6.0 (source: server release evidence and package.json) <br>\n\n## Ethical Considerations: <br>\nUsers should evaluate whether this skill is appropriate for their environment, review any generated or modified files before relying on them, and apply their organization's safety, security, and compliance requirements before deployment. <br>\n\nFile v0.6.0:data/users/self.json\n\n{\n  \"userId\": \"self\",\n  \"name\": \"\",\n  \"createdAt\": \"2026-06-08T21:20:53.749Z\",\n  \"seen\": {\n    \"2026-06-04|standup|2026-06-04T17:00:00.000Z\": \"2026-06-22T19:58:24.742Z\",\n    \"2026-06-04|design review|2026-06-04T20:00:00.000Z\": \"2026-06-22T19:58:24.742Z\",\n    \"2026-06-05|dinner|2026-06-05T01:00:00.000Z\": \"2026-06-22T19:58:24.742Z\",\n    \"2026-06-23|standup|2026-06-23T17:00:00.000Z\": \"2026-06-23T00:51:50.251Z\",\n    \"2026-06-23|freshmeeting|2026-06-23T17:00:00.000Z\": \"2026-06-23T00:52:11.027Z\"\n  },\n  \"lastRunAt\": \"2026-06-23T00:52:11.027Z\"\n}\n\nFile v0.6.0:data/users/self.prefs.json\n\n{\n  \"time\": \"08:00\",\n  \"channel\": \"iOS\",\n  \"tz\": \"America/Los_Angeles\",\n  \"enabledAt\": \"2026-05-30T18:44:26.998Z\"\n}\n\nFile v0.6.0:package.json\n\n{\n  \"name\": \"calendar-extractor\",\n  \"version\": \"0.6.0\",\n  \"description\": \"Extract calendar events from recent recording and keyboard transcripts and push a markdown digest to your iOS chat. Runs on demand and is auto-run by the javis-server session dispatcher (no approve-to-run card); extracted events are written PENDING and become solid only when the user taps Confirm in the iOS calendar table. Triggers: 'today's meetings', 'calendar extract', '今日会议', '提取日历'.\",\n  \"keywords\": [\"today's meetings\", \"calendar extract\", \"今日会议\", \"提取日历\"],\n  \"license\": \"MIT\",\n  \"scripts\": {\n    \"register\": \"node scripts/register.js\",\n    \"fetch\": \"node scripts/calendar-extractor.js\",\n    \"push\": \"node scripts/calendar-extractor.js\",\n    \"test\": \"node --test\"\n  },\n  \"engines\": { \"node\": \">=18\" }\n}\n\nArchive v0.5.4: 16 files, 43446 bytes\n\nFiles: data/users/self.json (543b), data/users/self.prefs.json (115b), package.json (816b), README.md (3781b), references/route-contract.md (4506b), scripts/calendar-extractor.js (32140b), scripts/data.js (2133b), scripts/lib.js (6257b), scripts/register.js (612b), skill-card.md (2808b), SKILL.md (18034b), test/cli.test.js (16251b), test/data.test.js (3033b), test/lib.test.js (7098b), test/update.test.js (18225b), _meta.json (137b)\n\nFile v0.5.4:SKILL.md\n\n---\nname: calendar-extractor\ndescription: Extract calendar events from recent recording and keyboard transcripts and push them to your iOS chat as one markdown card per event, each landing in its own Agent Chat thread. Use on demand when the user asks for \"today's meetings\" / \"calendar extract\" / \"今日会议\" / \"提取日历\", and fetch the last 24 hours of transcript data by default. The javis-server session dispatcher also AUTO-RUNS this skill (no approve-to-run card) when a completed unit matches the calendar route, passing a deliverable hint in the run prompt that the agent may use alongside the transcript; extracted events are written PENDING and become solid only when the user taps Confirm in the iOS calendar table. If the user asks for \"today's meetings\", use the local day defined by the fetched reference_date field. Triggers: 'today's meetings', 'calendar extract', '今日会议', '提取日历'.\nkeywords: today's meetings, calendar extract, 今日会议, 提取日历, calendar-extractor\nmetadata:\n  openclaw:\n    runtime:\n      node: \">=18\"\n  routes:\n    - route_id: calendar\n      skill: calendar-extractor\n      matches: \"meetings, appointments, events, agenda, scheduling, dates/times mentioned\"\n      args_template: null\n      risk: low\n---\n\n# Calendar Extractor\n\n> Extract calendar events from recent recording and keyboard transcripts and push them to your iOS chat — one markdown card per event, each landing in its own Agent Chat thread. Fetch the last 24 hours of transcript data by default. If the user asks for \"today's meetings\", use the local day defined by the fetched reference_date field. Generate one digest per local day in the user's timezone, containing all events that occur on reference_date and any events explicitly mentioned as happening today.\n\n## When to use\n\n- \"today's meetings\"\n- \"calendar extract\"\n- \"今日会议\"\n- \"提取日历\"\n- Automatically, when the javis-server session dispatcher **auto-runs** this skill after a completed unit matches the calendar route — no approve-to-run card (see \"How this skill is invoked\").\n\n## Core commands\n\n> **`<userId>` is optional.** Omit it and it defaults to `self`. Each HiJavis user\n> runs in their own openclaw container, so `self` is correctly isolated; the gateway\n> token (not the userId) authenticates every server call. No registration is needed\n> to start — pass an explicit ID only if you run multiple profiles in one container.\n\n```bash\n# Step 1 — fetch recent transcripts as JSON (the agent extracts events from this)\nnode scripts/calendar-extractor.js fetch [--hours N] [--limit N]\n\n# Step 1 (dispatcher run) — fetch ONE completed unit (the auto-run dispatcher unit)\nnode scripts/calendar-extractor.js fetch --session <sessionId> [--hours N]   # audio unit\nnode scripts/calendar-extractor.js fetch --kbd-input <inputId> [--hours N]   # keyboard unit\n\n# Step 2 — push: pipe the extracted-events JSON array to stdin; dedups (seen) + delivers to iOS\necho '<events-json-array>' | node scripts/calendar-extractor.js push\n\n# anchor — print the CURRENT relative-date anchor (resolve \"today / 6 pm / tomorrow\" on an edit turn)\nnode scripts/calendar-extractor.js anchor            # optional: --tz <IANA> for the card's calendar zone\n\n# update — edit ONE pushed card in place (verbatim dedup_key, full merged patch, auto-confirm)\necho '{\"dedup_key\":\"<verbatim>\",\"patch\":{...}}' | node scripts/calendar-extractor.js update\n\n# Optional: explicit userId / multi-profile (back-compat — prepend the ID)\nnode scripts/register.js <userId> <name>\nnode scripts/calendar-extractor.js <userId> fetch\necho '<events-json-array>' | node scripts/calendar-extractor.js <userId> push\nnode scripts/calendar-extractor.js <userId> anchor\necho '{\"dedup_key\":\"<verbatim>\",\"patch\":{...}}' | node scripts/calendar-extractor.js <userId> update\n```\n\n## Workflow\n\nThis skill is a two-step pipeline: the **script** does the I/O (fetch transcripts, dedup, push),\nthe **agent/LLM** does the reasoning (extract events). Extraction is not hardcoded — the agent\nreads the fetched transcripts and emits a JSON array of events.\n\n1. **Fetch** — `node scripts/calendar-extractor.js <userId> fetch` issues\n   `GET http://javis-server:8000/api/transcripts/recent?since=…&limit=…` with the\n   `OPENCLAW_GATEWAY_TOKEN` bearer and prints\n   `{ \"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 } ] }`. If fetch fails, returns invalid JSON, or yields zero sessions, output an empty events array and do not push anything; report the failure only if the user asked for a diagnostic. If the fetch response contains no sessions or the transcript text is em\n\nArchive v0.5.3: 16 files, 42106 bytes\n\nFiles: data/users/self.json (543b), data/users/self.prefs.json (115b), package.json (816b), README.md (3781b), references/route-contract.md (4506b), scripts/calendar-extractor.js (31357b), scripts/data.js (2133b), scripts/lib.js (6257b), scripts/register.js (612b), skill-card.md (2600b), SKILL.md (18034b), test/cli.test.js (13319b), test/data.test.js (3033b), test/lib.test.js (7098b), test/update.test.js (18225b), _meta.json (137b)\n\nArchive v0.5.2: 16 files, 41481 bytes\n\nFiles: data/users/self.json (543b), data/users/self.prefs.json (115b), package.json (816b), README.md (3781b), references/route-contract.md (4506b), scripts/calendar-extractor.js (30444b), scripts/data.js (2133b), scripts/lib.js (6257b), scripts/register.js (612b), skill-card.md (2413b), SKILL.md (18034b), test/cli.test.js (13319b), test/data.test.js (3033b), test/lib.test.js (7098b), test/update.test.js (17090b), _meta.json (137b)\n\nArchive v0.5.1: 16 files, 39543 bytes\n\nFiles: data/users/self.json (543b), data/users/self.prefs.json (115b), package.json (816b), README.md (3781b), references/route-contract.md (4506b), scripts/calendar-extractor.js (27830b), scripts/data.js (2133b), scripts/lib.js (6257b), scripts/register.js (612b), skill-card.md (2542b), SKILL.md (16707b), test/cli.test.js (13319b), test/data.test.js (3033b), test/lib.test.js (7098b), test/update.test.js (14087b), _meta.json (137b)\n\nArchive v0.5.0: 15 files, 30668 bytes\n\nFiles: data/users/self.json (380b), data/users/self.prefs.json (115b), package.json (816b), README.md (3781b), references/route-contract.md (4506b), scripts/calendar-extractor.js (17741b), scripts/data.js (2133b), scripts/lib.js (6257b), scripts/register.js (612b), skill-card.md (2713b), SKILL.md (11943b), test/cli.test.js (13319b), test/data.test.js (3033b), test/lib.test.js (7098b), _meta.json (137b)\n\nArchive v0.4.2: 15 files, 29185 bytes\n\nFiles: data/users/self.json (79b), data/users/self.prefs.json (115b), package.json (816b), README.md (3516b), references/route-contract.md (4506b), scripts/calendar-extractor.js (16058b), scripts/data.js (2133b), scripts/lib.js (6257b), scripts/register.js (612b), skill-card.md (2241b), SKILL.md (11576b), test/cli.test.js (11295b), test/data.test.js (3033b), test/lib.test.js (7098b), _meta.json (137b)","readmeExcerpt":"Skill: Calendar Extractor Owner: samuel-wei Summary: Periodically scans recent transcripts to extract calendar events and sends a daily summary of meetings to your iOS chat via push notifications. Tags: latest:0.7.2 Version history: v0.7.2 | 2026-08-02T08:14:19.115Z | user calendar-extractor 0.7.2 - Added prompt-context fallback logic for dispatcher-invoked runs: when a single-unit fetch fails (e.g. empty/404 session","codeSnippets":[],"executableExamples":[{"language":"bash","snippet":"# Step 1 — fetch recent transcripts as JSON (the agent extracts events from this)\nnode scripts/calendar-extractor.js fetch [--hours N] [--limit N]\n\n# Step 1 (dispatcher run) — fetch ONE completed unit (the auto-run dispatcher unit)\nnode scripts/calendar-extractor.js fetch --session <sessionId> [--hours N]   # audio unit\nnode scripts/calendar-extractor.js fetch --kbd-input <inputId> [--hours N]   # keyboard unit\n\n# Step 2 — push: pipe the extracted-events JSON array to stdin; dedups (seen) + delivers to iOS\necho '<events-json-array>' | node scripts/calendar-extractor.js push\n\n# anchor — print the CURRENT relative-date anchor (resolve \"today / 6 pm / tomorrow\" on an edit turn)\nnode scripts/calendar-extractor.js anchor            # optional: --tz <IANA> for the card's calendar zone\n\n# update — edit ONE pushed card in place (verbatim dedup_key, full merged patch, auto-confirm)\necho '{\"dedup_key\":\"<verbatim>\",\"patch\":{...}}' | node scripts/calendar-extractor.js update\n\n# Optional: explicit userId / multi-profile (back-compat — prepend the ID)\nnode scripts/register.js <userId> <name>\nnode scripts/calendar-extractor.js <userId> fetch\necho '<events-json-array>' | node scripts/calendar-extractor.js <userId> push\nnode scripts/calendar-extractor.js <userId> anchor\necho '{\"dedup_key\":\"<verbatim>\",\"patch\":{...}}' | node scripts/calendar-extractor.js <userId> update"},{"language":"bash","snippet":"echo '{\n     \"dedup_key\": \"<the [CURRENT CARD] dedup_key, VERBATIM>\",\n     \"tz\": \"America/Los_Angeles\",\n     \"patch\": {\n       \"title\": \"Design Review\", \"location\": \"Zoom\",\n       \"attendees\": [\"Sam\"], \"notes\": \"bring laptop\",\n       \"lead_time\": 10,\n       \"start_at\": \"2026-06-22T18:00:00-07:00\",\n       \"end_at\":   \"2026-06-22T19:00:00-07:00\"\n     }\n   }' | node scripts/calendar-extractor.js update"},{"language":"json","snippet":"{\n  \"id\": \"<uuid>\",\n  \"title\": \"Extract calendar events\",\n  \"description\": \"<short summary of the agenda found>\",\n  \"route_id\": \"calendar\",\n  \"confidence\": 0.0\n}"},{"language":"text","snippet":"Run /calendar-extractor for <unit>. Deliverable: <title>. <description>\nFetch that unit's transcript, produce the deliverable, and push the result."},{"language":"bash","snippet":"# Step 1 — fetch recent transcripts as JSON (the agent extracts events from this)\nnode scripts/calendar-extractor.js fetch [--hours N] [--limit N]\n\n# Step 1 (dispatcher run) — fetch ONE completed unit (the auto-run dispatcher unit)\nnode scripts/calendar-extractor.js fetch --session <sessionId> [--hours N]   # audio unit\nnode scripts/calendar-extractor.js fetch --kbd-input <inputId> [--hours N]   # keyboard unit\n\n# Step 2 — push: pipe the extracted-events JSON array to stdin; dedups (seen) + delivers to iOS\necho '<events-json-array>' | node scripts/calendar-extractor.js push\n\n# anchor — print the CURRENT relative-date anchor (resolve \"today / 6 pm / tomorrow\" on an edit turn)\nnode scripts/calendar-extractor.js anchor            # optional: --tz <IANA> for the card's calendar zone\n\n# update — edit ONE pushed card in place (verbatim dedup_key, full merged patch, auto-confirm)\necho '{\"dedup_key\":\"<verbatim>\",\"patch\":{...}}' | node scripts/calendar-extractor.js update\n\n# Optional: explicit userId / multi-profile (back-compat — prepend the ID)\nnode scripts/register.js <userId> <name>\nnode scripts/calendar-extractor.js <userId> fetch\necho '<events-json-array>' | node scripts/calendar-extractor.js <userId> push\nnode scripts/calendar-extractor.js <userId> anchor\necho '{\"dedup_key\":\"<verbatim>\",\"patch\":{...}}' | node scripts/calendar-extractor.js <userId> update"},{"language":"bash","snippet":"echo '{\n     \"dedup_key\": \"<the [CURRENT CARD] dedup_key, VERBATIM>\",\n     \"tz\": \"America/Los_Angeles\",\n     \"patch\": {\n       \"title\": \"Design Review\", \"location\": \"Zoom\",\n       \"attendees\": [\"Sam\"], \"notes\": \"bring laptop\",\n       \"lead_time\": 10,\n       \"start_at\": \"2026-06-22T18:00:00-07:00\",\n       \"end_at\":   \"2026-06-22T19:00:00-07:00\"\n     }\n   }' | node scripts/calendar-extractor.js update"}],"parameters":null,"dependencies":[],"permissions":[],"extractedFiles":[{"path":"SKILL.md","content":"---\nname: calendar-extractor\ndescription: Extract calendar events from recent recording and keyboard transcripts and push them to your iOS chat as one markdown card per event, each landing in its own Agent Chat thread. Use on demand when the user asks for \"today's meetings\" / \"calendar extract\" / \"今日会议\" / \"提取日历\", and fetch the last 24 hours of transcript data by default. The javis-server dispatcher also invokes this skill directly for every completed unit — no classifier, no route matching; this skill's own agent decides relevance itself, using this SKILL.md, and may use a deliverable hint passed in the run prompt alongside the transcript; extracted events are written PENDING and become solid only when the user taps Confirm in the iOS calendar table. If the user asks for \"today's meetings\", use the local day defined by the fetched reference_date field. Triggers: 'today's meetings', 'calendar extract', '今日会议', '提取日历'.\nkeywords: today's meetings, calendar extract, 今日会议, 提取日历, calendar-extractor\nmetadata:\n  openclaw:\n    runtime:\n      node: \">=18\"\n  routes:\n    - route_id: calendar\n      skill: calendar-extractor\n      matches: \"meetings, appointments, events, agenda, scheduling, dates/times mentioned\"\n      args_template: null\n      risk: low\n---\n\n# Calendar Extractor\n\n> Extract calendar events from recent recording and keyboard transcripts and push them to your iOS chat — one markdown card per event, each landing in its own Agent Chat thread. Fetch the last 24 hours of transcript data by default. If the user asks for \"today's meetings\", use the local day defined by the fetched reference_date field. Generate one digest per local day in the user's timezone, containing all events that occur on reference_date and any events explicitly mentioned as happening today.\n\n## When to use\n\n- \"today's meetings\"\n- \"calendar extract\"\n- \"今日会议\"\n- \"提取日历\"\n- Automatically, when the javis-server dispatcher invokes this skill directly after a completed voice/keyboard unit — no classifier, no route matching. This skill's own agent decides for itself, using this `SKILL.md`, whether the unit is worth acting on; if not, it does nothing (see \"How this skill is invoked\").\n\n## Core commands\n\n> **`<userId>` is optional.** Omit it and it defaults to `self`. Each HiJavis user\n> runs in their own openclaw container, so `self` is correctly isolated; the gateway\n> token (not the userId) authenticates every server call. No registration is needed\n> to start — pass an explicit ID only if you run multiple profiles in one container.\n\n```bash\n# Step 1 — fetch recent transcripts as JSON (the agent extracts events from this)\nnode scripts/calendar-extractor.js fetch [--hours N] [--limit N]\n\n# Step 1 (dispatcher run) — fetch ONE completed unit (the auto-run dispatcher unit)\nnode scripts/calendar-extractor.js fetch --session <sessionId> [--hours N]   # audio unit\nnode scripts/calendar-extractor.js fetch --kbd-input <inputId> [--hours N]   # keyboard unit\n\n# Step 2 — push: pipe the extracted-e"},{"path":"README.md","content":"# Calendar Extractor\n\n> ⚠️ **Requires the HiJavis iPhone app.** This skill runs inside HiJavis — install the app first, then it's ready to use.\n> 📲 https://apps.apple.com/us/app/hijavis/id6745134765\n\nCalendar Extractor finds the plans hiding in your conversations. HiJavis listens, spots anything that sounds like a meeting or appointment, and hands you a tidy list right in your chat.\n\n## Picture this\n\n- You finish a day of back-to-back calls and can't recall what you promised anyone. You ask, \"What did I agree to today?\" and there it is, all in one list.\n- A friend says, \"Let's grab coffee Friday.\" You'd normally forget by lunch. HiJavis already caught it.\n- It's 8am. You open the app and see everything you committed to today, before your first sip of coffee.\n- You leave a conference where you made a dozen plans out loud, and they're all waiting for you in a list.\n- The house is buzzing with \"dentist Thursday,\" \"soccer practice moved to 5,\" \"dinner Saturday at the new place.\" HiJavis keeps track so nobody drops the ball.\n\nIf any of those sound like you, this one's for you.\n\n## What it does\n\nHiJavis listens to your conversations and meetings. Calendar Extractor reads back through your recent recordings and spots anything that sounds like a plan or appointment, such as \"let's meet Tuesday at 3,\" \"dinner Saturday at the new place,\" or \"call me tomorrow afternoon.\"\n\nThen it sends each event to your chat as its own card — with the title, date, time, place, who's involved, and any notes it picked up. Every event opens in its own chat thread, so you can act on one plan without losing the rest.\n\n## How to use it\n\n### Just ask\n\nOpen your HiJavis chat and ask. Any of these works:\n\n- \"today's meetings\"\n- \"calendar extract\"\n- \"今日会议\"\n- \"提取日历\"\n- Or a natural question like \"What meetings do I have coming up?\" or \"Did I agree to anything today?\"\n\nHiJavis scans your recent recordings and replies with your list in seconds. Nothing to set up first.\n\n### Get a daily summary\n\nWant it to come to you? Ask HiJavis to send a summary on a schedule. By default you'll get one each morning (around 8am) and each evening (around 6pm), but you can pick your own times. Just say something like \"send me my calendar summary every morning at 7,\" and it'll start showing up on its own.\n\n## What makes it handy\n\n- **It gets how you talk about time.** You don't say \"March 14th at 2:00 PM\" out loud. You say \"tomorrow,\" \"next Thursday,\" \"around 6,\" \"noon.\" Calendar Extractor turns all of that into a real date and time in your time zone.\n- **It's instant.** The moment a recording or a typed note finishes, it pulls out any plans right then, so your list is ready before you even ask.\n- **One card per plan.** Each event arrives as its own card in its own chat thread, so they never pile into one wall of text and you can deal with them one at a time.\n- **It never repeats itself.** It remembers what it already showed you, so the same event won't clutter your chat twice.\n- **It works in English an"},{"path":"_meta.json","content":"{\n  \"ownerId\": \"kn73qn9mdc6qf6hnnz3s26pgch845n33\",\n  \"slug\": \"calendar-extractor\",\n  \"version\": \"0.7.2\",\n  \"publishedAt\": 1785658459115\n}"},{"path":"references/route-contract.md","content":"# calendar-extractor — route contract (for the javis-server team)\n\nThis document specifies the interface the **javis-server session-dispatcher**\ncontrol-plane must satisfy so the dispatcher can route agenda deliverables to the\n`calendar-extractor` skill. It is the contract only — no javis-server code is\nimplemented in this repo.\n\nSee the design spec:\n`docs/superpowers/specs/2026-06-08-calendar-extractor-dispatcher-adaptation-design.md`\n(sections 3a–3c) and the dispatcher spec it adapts to\n(javis-server `docs/superpowers/specs/2026-06-06-session-dispatcher-server-variant.md`,\nimplemented in `app/services/dispatcher_service.py`).\n\nThe same contract is declared, discoverably, in the skill's `SKILL.md`\n`metadata.routes` block. That static block declares the skill-owned fields\n(`route_id`, `skill`, `matches`, `args_template`, `risk`); `enabled` is omitted\nthere because it is per-user runtime state the server owns, not skill metadata.\n\n## 3a. RouteRegistry row\n\nSeed this row per user, enabling it when the user enables `calendar-extractor`:\n\n| column | value |\n|---|---|\n| `route_id` | `\"calendar\"` |\n| `skill` | `\"calendar-extractor\"` |\n| `matches` | `\"meetings, appointments, events, agenda, scheduling, dates/times mentioned\"` |\n| `args_template` | `null` (the unit + the prepended SKILL.md carry everything the skill needs) |\n| `risk` | `\"low\"` (read-only transcript extraction + a push; no destructive side effects) |\n| `enabled` | set `true` when the user enables calendar-extractor; `false`/absent otherwise |\n\nThe dispatcher only routes a deliverable to this skill when an **enabled** row with\n`route_id=\"calendar\"` exists for that user. No enabled row → the skill is never\nscheduled.\n\n## 3b. `classify_and_route` deliverable shape\n\nFeed the user's enabled route catalog (each row's `route_id` + `matches`) into the\n`classify_and_route` task prompt. For a transcript containing scheduling content,\nthe classifier must emit a deliverable shaped like:\n\n```json\n{\n  \"id\": \"<uuid>\",\n  \"title\": \"Extract calendar events\",\n  \"description\": \"<short summary of the agenda found>\",\n  \"route_id\": \"calendar\",\n  \"confidence\": 0.0\n}\n```\n\n- `route_id` MUST be exactly `\"calendar\"` so it matches the RouteRegistry row.\n- No scheduling content in the transcript → **no** `calendar` deliverable → no\n  proposal card. (This is the correct, silent outcome.)\n\nThe dispatcher then matches the deliverable's `route_id` against the enabled\n`RouteRegistry` row, persists a `DispatchProposal`, and pushes a proposal card to\niOS. On the user's Approve, it claims run-once\n(`DispatchRouteExecuted (user, unit, route)` via a unique constraint, before\nscheduling) and triggers the skill.\n\n## 3c. Prompt contract\n\nThe skill relies only on the `<unit>` being present and `_UNIT_RE`-valid in the run\nprompt — which the generic dispatcher prompt already provides:\n\n```\nRun /calendar-extractor for <unit>. Deliverable: <title>. <description>\nFetch that unit's transcript, produce the deliverable, and push the result"},{"path":"docs/pr-drafts/2026-08-02-prompt-context-fallback.md","content":"# calendar-extractor: use embedded UNIT CONTEXT; a fetch miss no longer aborts the write\n\n- **Branch:** `feat/calendar-extractor-prompt-context-fallback`\n- **Date:** 2026-08-02\n- **Skill:** `calendar-extractor` (currently `0.7.1`)\n- **Spec:** `javis-server/docs/superpowers/specs/2026-08-02-dispatch-prompt-embed-unit-context-resilience-design.md` (Approved — root-caused via systematic-debugging). This is the **coordinated skill-side half** of that spec; the server half (embed the unit context in the dispatch prompt) ships separately on javis-server branch `feat/dispatch-prompt-embed-unit-context`.\n\n## Why\n\nA proactive, dispatcher-invoked extraction **silently vanished** when its source unit row was dropped after dispatch.\n\nObserved (wmp425, 2026-08-02): a \"meeting at 1 PM today\" keyboard input reached the server, `calendar-extractor` was auto-invoked, and its agent **extracted the event** (container log: *\"Extracted and pushed… PENDING card\"*) — but **no `skill_data` row was ever written** (`skill_data MAX(id)` stuck at 319; no POST in the server logs). No calendar event → no voice-call arming → no \"Javis calls you\" ring.\n\nRoot cause: the dispatcher handed the skill **only the unit id**, so the skill had to **re-fetch** the unit via `GET /api/transcripts/keyboard-input/<id>`. For this event `keyboard_input 1189` **did not exist** (server `MAX(id)=1187`; rolled-back/dropped inserts — the fragile draft double-save path). The re-fetch 404'd → the skill aborted before the `/api/skill/data` POST → the extracted event was lost.\n\nThe server fix threads the already-loaded transcript + `source_ref`/`source_kind`/`reference_date`/`tz` into the dispatch prompt as a delimited `UNIT CONTEXT` block. This PR makes the skill **use that embedded context as the source of truth** and treat `fetch` as an optional enrichment whose failure must not suppress the write.\n\n## What changed\n\n**`scripts/calendar-extractor.js` — `doFetch` degrades instead of throwing.**\n- Wrapped the single `httpGet` in a try/catch. On failure (e.g. a 404 keyboard-input miss) it no longer propagates the throw that aborts the run. Instead it logs `⚠️ fetch miss (…) — not aborting; fall back to the run-prompt UNIT CONTEXT.` to stderr and continues with `data = null`.\n- The existing envelope normalization already handles a null/empty payload → an empty `sessions` array, so the emitted envelope still carries the relative-time anchor (`reference_time`/`reference_date`/`reference_weekday`/`reference_time_utc`/`tz`).\n- Surfaces the miss in the emitted envelope too (not just stderr): `out.fetch_error = fetchError`, so the agent can *see* the re-fetch missed and knowingly fall back to the `UNIT CONTEXT`.\n\n**`SKILL.md` — document the embedded-context path and the no-abort rule.**\n- Step 1 (Fetch): added the dispatcher-path **Exception** — a failed single-unit `fetch --session`/`fetch --kbd-input` (surfaced as `fetch_error`, empty sessions) does NOT mean \"emit nothing\"; fall back to the prompt-embedded t"}],"languages":[],"docsSourceLabel":"CLAWHUB","editorialOverview":null,"editorialQuality":{"score":100,"threshold":65,"status":"thin","wordCount":1577,"uniquenessScore":44,"reasons":["uniqueness-below-45"]}},"media":{"evidence":{"source":"no-media","verified":false,"confidence":"low","updatedAt":"2026-10-10T02:40:20.245Z","emptyReason":"No screenshots, media assets, or demo links are available."},"primaryImageUrl":null,"mediaAssetCount":0,"assets":[],"demoUrl":null},"ownerResources":{"evidence":{"source":"unclaimed","verified":false,"confidence":"low","updatedAt":"2026-10-10T02:40:20.245Z","emptyReason":"This page has not been claimed by the agent owner."},"hasCustomPage":false,"customPageUpdatedAt":null,"customLinks":[],"structuredLinks":{"docsUrl":null,"demoUrl":null,"supportUrl":null,"pricingUrl":null,"statusUrl":null},"customPage":null},"relatedAgents":{"evidence":{"source":"protocol-neighbors","verified":false,"confidence":"medium","updatedAt":"2026-10-10T07:40:22.534Z","emptyReason":null},"items":[{"id":"8ebccd8e-3863-4187-8355-c3f14e1f9edf","entityType":"agent","canonicalPath":"/agent/iofficeai-aionui","slug":"iofficeai-aionui","name":"AionUi","description":"Free, local, open-source 24/7 Cowork app and OpenClaw for Gemini CLI, Claude Code, Codex, OpenCode, Qwen Code, Goose CLI, Auggie, and more | 🌟 Star if you like it!","url":"https://github.com/iOfficeAI/AionUi","homepage":"https://www.aionui.com","source":"GITHUB_REPOS","protocols":["MCP","OPENCLAW"],"capabilities":[],"safetyScore":100,"overallRank":70,"updatedAt":"2026-10-09T19:11:12.944Z","createdAt":"2026-02-25T03:38:16.584Z","downloads":null},{"id":"b917f68a-ebff-438e-84f8-3f4b2494c0bc","entityType":"agent","canonicalPath":"/agent/activepieces-activepieces","slug":"activepieces-activepieces","name":"activepieces","description":"AI Agents & MCPs & AI Workflow Automation • (~400 MCP servers for AI agents) • AI Automation / AI Agent with MCPs • AI Workflows & AI Agents • MCPs for AI Agents","url":"https://github.com/activepieces/activepieces","homepage":"https://www.activepieces.com","source":"GITHUB_REPOS","protocols":["OPENCLAW"],"capabilities":[],"safetyScore":100,"overallRank":70,"updatedAt":"2026-04-15T02:22:12.426Z","createdAt":"2026-02-25T03:38:12.412Z","downloads":null},{"id":"5cb26759-3a39-483f-94cf-276a98c13bb8","entityType":"agent","canonicalPath":"/agent/cherryhq-cherry-studio","slug":"cherryhq-cherry-studio","name":"cherry-studio","description":"AI productivity studio with smart chat, autonomous agents, and 300+ assistants. Unified access to frontier LLMs","url":"https://github.com/CherryHQ/cherry-studio","homepage":"https://cherry-ai.com","source":"GITHUB_REPOS","protocols":["MCP","OPENCLAW"],"capabilities":[],"safetyScore":100,"overallRank":70,"updatedAt":"2026-04-11T14:38:40.986Z","createdAt":"2026-02-25T03:38:19.379Z","downloads":null},{"id":"6f6582d0-5d76-4f0f-b81d-86520247950b","entityType":"agent","canonicalPath":"/agent/copilotkit-copilotkit","slug":"copilotkit-copilotkit","name":"CopilotKit","description":"The Frontend for Agents & Generative UI. React + Angular","url":"https://github.com/CopilotKit/CopilotKit","homepage":"https://docs.copilotkit.ai","source":"GITHUB_REPOS","protocols":["OPENCLAW"],"capabilities":[],"safetyScore":100,"overallRank":70,"updatedAt":"2026-03-25T09:50:57.846Z","createdAt":"2026-02-25T03:39:14.617Z","downloads":null}],"links":{"hub":"/agent","source":"/agent/source/clawhub","protocols":[{"label":"OpenClaw","href":"/agent/protocol/openclew"}]}}}