Calendar Sync Skill (universal)
Upload / update calendar events through a single, decoupled entry. The module is
pure JavaScript (ESM, zero runtime dependencies, Node ≥ 18), so it runs in
any agent host — WorkBuddy, Qoder, Claude, a CI job, or a plain shell — with
no vendor SDK and no account of its own. It talks to a calendar backend only
through a token that the user (or the user's auth system) provides.
What it needs (and does NOT need)
- Needs: a calendar access token (JWT) issued by the user's auth backend.
The token carries the user id (sub), the target calendar id (cid), the
scope (calendar:write), and optionally the backend issuer (iss). For known
issuers the skill only parses the payload locally; the calendar backend itself
verifies the signature via theAuthorization: Bearerheader. The skill writes
only to the calendar the token is scoped to. - Does NOT need: the user's password, an OAuth flow, any API key, or a
vendor SDK. The token is the single credential and stays in memory for one turn.
The contract the skill speaks (canonical)
The bundled rest adapter is vendor-agnostic. By default (wireFormat: 'iso')
it speaks this canonical contract, which any backend can implement:
- Auth:
Authorization: Bearer <token>on every request. - Create:
POST {baseUrl}/calendars/{calendarId}/events - body = normalized Event JSON (
start/endare ISO:YYYY-MM-DDTHH:mm[:ss];
all-day usesYYYY-MM-DD; optionaltitle/location/description/timezone/
reminders/recurrence). - response
200with{ "id": "<eventId>", ... }. - Update:
PATCH {baseUrl}/calendars/{calendarId}/events/{eventId} - Delete:
DELETE .../events/{eventId}(soft — flips status, never hard-deletes) - List (conflict check):
GET .../events?from=&to=→ returns a JSON array.
If your backend uses a different shape, you do not write code — you just
set three service options (see below). No subclass, no fork.
Known services (token-only, zero config)
The skill ships with built-in service configurations. If a token includes an
iss field that matches a known issuer, the skill automatically selects the
right baseUrl, wire format, and response id field — the agent only needs the
token.
| Service | iss (token payload) |
Base URL | Wire format | Id field |
|---|---|---|---|---|
uni-app-calendar |
https://prod-d5gzz0x1s8252853f.service.tcloudbase.com/api |
same as issuer | date-hm |
_id |
Example token payload for uni-app-calendar:
{
"sub": "u_xxxx",
"cid": "717c23bf6a58db0d02fa15a42597e90b",
"iss": "https://prod-d5gzz0x1s8252853f.service.tcloudbase.com/api",
"scope": ["calendar:write"],
"iat": 1784632426,
"exp": 1785237226,
"jti": "..."
}
Two ways to use it
A) CLI (simplest — any agent can shell out)
Token-only mode (recommended for known services — no secret, no baseUrl):
node bin/cal-sync.mjs \
--token "$CALENDAR_TOKEN" \
--text "明天9点到10点去北京城市图书馆"
# prints one JSON line: the unified response
Explicit backend mode (for custom backends or when you want local HMAC verification):
node bin/cal-sync.mjs \
--token "$CALENDAR_TOKEN" \
--text "明天9点到10点去北京城市图书馆" \
--base-url "https://your-host/api" \
--secret "$CALENDAR_SYNC_TOKEN_SECRET" \
--wire-format date-hm --response-id-field _id
Flags: --token (or env CALENDAR_TOKEN), --text/--nl, --event '<json>',
--action create|update|delete|recover|batch, --event-id, --service,
--base-url (or env CALENDAR_BASE_URL), --secret (or env
CALENDAR_SYNC_TOKEN_SECRET), --wire-format iso|date-hm,
--response-id-field id|_id, --path-template, --calendar, --timezone,
--idempotency-key, --fail-on-conflict, --events '<json-array>'. Run
node bin/cal-sync.mjs --help for all flags.
The CLI always prints a single JSON line (even on error) so the calling agent
gets a structured result, never a silent failure.
B) Library (programmatic)
Token-only (auto-detect by iss):
import { createCalendarSync } from './scripts/calendar-sync/src/index.js';
// No secret/baseUrl needed for known issuers; services auto-selected from token.iss.
const sync = createCalendarSync();
const res = await sync.invoke(
{ action: 'create', token: '<jwt>', naturalLanguage: '明天下午3点到5点开会' },
// token may also be passed via a SkillContext; it is wiped after the turn.
);
// res -> { ok, status, code, data:{ eventId }, error, meta }
Explicit service (custom backend):
const sync = createCalendarSync({
token: { secret: process.env.CALENDAR_SYNC_TOKEN_SECRET },
services: {
'my-cal': {
adapter: 'rest',
baseUrl: 'https://your-host/api',
defaultCalendarId: '', // leave ''; token's cid is used
wireFormat: 'iso', // 'iso' (canonical) | 'date-hm'
responseIdField: 'id', // backend returns id in this field ('_id' for Mongo)
pathTemplate: '/calendars/{calendarId}/events',
},
},
defaultService: 'my-cal',
});
Pointing it at YOUR backend (no code changes)
Set these per-service options when you build the config:
| option | default | use when your backend… |
|---|---|---|
wireFormat |
'iso' |
…expects date (YYYY-MM-DD) + start/end (HH:mm), not ISO → set 'date-hm'. |
responseIdField |
'id' |
…returns the new id in _id (Mongo) → set '_id'. |
pathTemplate |
'/calendars/{calendarId}/events' |
…uses a different route → e.g. '/v1/calendars/{calendarId}/events'. |
Example for a backend that stores date+HH:mm and returns _id:
services: { 'my-cal': { adapter:'rest', baseUrl:'https://host/api',
wireFormat:'date-hm', responseIdField:'_id' } }
To register a new known service so agents only need a token, add it to both
token.knownIssuers and services in src/config.js:
token: {
knownIssuers: {
'https://your-host/api': 'your-service-name',
},
},
services: {
'your-service-name': {
adapter: 'rest',
baseUrl: 'https://your-host/api',
wireFormat: 'iso',
responseIdField: 'id',
},
},
Natural language
naturalLanguage (alias text) is parsed into a normalized event: Chinese date
words (今天/明天/后天/周X/日期), time ranges (9点到10点, 15:00-17:00, 下午3点),
location after 在/地点:/位于/@/前往/去往/去 (e.g. "去北京城市图书馆"), reminders
(提前30分钟提醒), and recurrence (每周). Pass an explicit event object to
skip NL. eventOverrides merges after NL (handy when NL misses a field).
Token & auth
- For known issuers (
token.knownIssuers), the skill does not need the
HMAC secret. It parses the token payload locally to obtainsub/cid/
scope/exp/iss, and lets the calendar backend verify the signature via
theAuthorization: Bearerheader. This is the default "token-only" mode. - For custom backends (unknown issuer) or if you want local verification, set
the same HS256 secret the issuing backend used (env
CALENDAR_SYNC_TOKEN_SECRET,--secret, ortoken.secretin config). - Tokens may carry an
issfield. Whenissmatches a known issuer in
token.knownIssuers, the skill auto-selects the matching service; no baseUrl,
wire-format, or secret questions are needed. - On success the skill reads
sub/cid/scope. The token'scidis
enforced: even if a caller passes a differentcalendarId, a scoped token can
only write its own calendar (least privilege / closes IDOR). Tokens without
calendar:writeare rejected. - The plaintext token lives only in memory, only for the turn; call
context.dispose()(the CLI does this automatically) and never log it. - To delegate verification to a real auth backend instead of local HMAC, call
setVerifier(async (token, config) => payload)fromsrc/token.js. - Disable token-only fallback with
token.trustKnownIssuers: false.
Reliability & compliance
Timeouts + retry + 429/503 Retry-After honoring · per-item idempotency keys
(retries are safe) · conflict detection before write · offline queue + replay on
transient failure · soft-delete/recover (never hard-delete) · audit log
(actor = userId) · optional AES-256-GCM field encryption · healthCheck() for
graceful degradation.
Unified response
Always { ok, status, code, data:{eventId?,…}, error:{code,message} | null, meta }.
error.message is always safe (no token/secret/expiry leaked). See
references/schema.md for the full contract and references/adapters.md for the
adapter interface.
Testing
cd scripts/calendar-sync && npm test (built-in node --test, zero deps):
NL parsing, validation, idempotency, conflict, token safety, soft-delete/recover,
batch, offline queue, and end-to-end via the in-memory adapter and the
configurable rest adapter (iso + date-hm).