@glance-apps/intents
v1.0.1
Published
Shared intent protocol for the GLANCE family of apps
Downloads
225
Readme
@glance-apps/intents
Shared intent protocol for the GLANCE family of apps. Implements the protocol specified in dayglance-intent-protocol.md, consumed by dayGLANCE, lastGLANCE, and lifeGLANCE.
The package provides Zod schemas, normalizers, idempotency helpers, and WebDAV envelope helpers — the building blocks. HTTP clients, polling loops, GC cadence, and UI feedback stay app-side.
Installation
npm install @glance-apps/intents
# or: pnpm add @glance-apps/intents
# or: yarn add @glance-apps/intentsRequires Node 20+ (uses Web Crypto via globalThis.crypto). Browser-compatible.
Usage
Validate an inbound intent
When a consumer reads an event file off WebDAV (or receives an Android broadcast, or parses a URL), it passes the raw JSON through parseEnvelope:
import { parseEnvelope } from '@glance-apps/intents';
const raw = JSON.parse(await fetchEventFile(filename));
const envelope = parseEnvelope(raw);
// envelope is now a typed, discriminated-union Envelope.
switch (envelope.action) {
case 'create':
// envelope.payload is CreatePayload
break;
case 'notify':
// envelope.payload is NotifyPayload
break;
// ...
}parseEnvelope throws on validation failure — malformed envelopes are exceptional, not silently filterable.
Build an outbound intent
When emitting an event (e.g. dayGLANCE emitting a notify after a task completion), construct the envelope with buildEnvelope and write it to WebDAV using your existing client:
import { ACTIONS, EVENTS, buildEnvelope, filenameFor } from '@glance-apps/intents';
const envelope = buildEnvelope({
action: ACTIONS.NOTIFY,
emittedBy: 'app.dayglance',
payload: {
event_id: 'evt_42',
source_app: 'app.lastglance',
source_entity_id: 'chore_42',
event: EVENTS.COMPLETED,
task_id: 'tsk_8a91',
title: 'Replace HVAC filter',
timestamp: '2026-05-17T14:30:22Z',
completed_at: '2026-05-17T14:30:22Z',
},
});
await webdavClient.put(`/GLANCE/events/${filenameFor(envelope)}`, JSON.stringify(envelope));buildEnvelope is generic over action so the payload is type-checked against the matching schema — passing a create action with a notify-shaped payload is a compile error.
emitted_at defaults to new Date() and event_id defaults to a generated eventId() (with timestamp aligned to emitted_at) — pass them explicitly only for testability or replay.
Normalize user input before validation
Schemas validate shape; normalizers turn flexible inputs into canonical forms. The handler typically composes them:
import { normalizeDue, normalizePriority, normalizeTags } from '@glance-apps/intents';
normalizePriority('HIGH'); // → 3
normalizePriority(2); // → 2
normalizeDue('tomorrow'); // → { due: '2026-05-18', all_day: true }
normalizeDue('2026-05-20T14:00:00Z'); // → { due: '2026-05-20T14:00:00Z', all_day: false }
normalizeTags({ title: 'fix sink #home #urgent', tags: 'errands' });
// → { title: 'fix sink', tags: ['home', 'urgent', 'errands'] }normalizeRecurring expands the shorthand strings (daily, weekdays, weekly, monthly, yearly) to canonical RRULEs and passes full RRULE strings through unchanged.
Generate idempotency keys
At-least-once delivery is the norm across every transport. Two helpers cover the dedup concerns:
import { createKey, eventId } from '@glance-apps/intents';
// Emitter uses eventId() to stamp each outbound event.
const id = eventId(); // → '20260517T143022Z-7f3a9c'
// Receiver uses createKey() to recognize that an inbound create matches
// an existing task (same source_app + source_entity_id + due → same key).
const key = await createKey('app.lastglance', 'chore_42', '2026-05-20');
// → '<64 hex chars, SHA-256>'createKey is async (Web Crypto); eventId is sync. Inputs to createKey are expected to be already-normalized — the handler composes normalizeDue → createKey.
Public API
Organized by module. Every named export is intentional and governed by semver.
Constants
SCHEMA_VERSION, ACTIONS, EVENTS, ENTITY_TYPES, PRIORITY, PRIORITY_ALIASES, TABS, QUERY_RETURN_VARS, UNIVERSAL_RETURN_VARS, RETURN_VARS, RETURN_VAR_TYPES, ANDROID_ACTIONS, SOURCE_APPS, UPDATED_FIELDS. Each ships with a matching narrow-literal type (Action, Event, EntityType, PriorityLevel, Tab, QueryReturnVar, UniversalReturnVar, ReturnVar, AndroidAction, SourceApp, UpdatedField).
Schemas
Flat: CreateSchema, CompleteSchema, OpenSchema, QuerySchema, NotifySchema, EnvelopeSchema, plus inferred types CreatePayload, CompletePayload, OpenPayload, QueryPayload, NotifyPayload, Envelope.
Versioned: schemas.v1.* for the same surface, so consumers that explicitly pin a protocol version can do so without breaking when v2 lands alongside.
Normalizers
normalizePriority, normalizeRecurring, normalizeTags, normalizeDue. Types: NormalizeTagsInput, NormalizeTagsOutput, NormalizeDueOutput.
Idempotency
eventId(now?), createKey(source_app, source_entity_id, due).
WebDAV envelope helpers
buildEnvelope, parseEnvelope, filenameFor, parseFilename. Types: ActionPayloadMap, BuildEnvelopeArgs, ParsedFilename.
Versioning
The package version tracks the protocol's schema_version directly:
1.x.y→ protocol v12.0.0→ protocol v2 (breaking, coordinated multi-app upgrade)
Within v1, additive minor bumps are non-breaking; consumers can upgrade freely. When v2 ships, src/schemas/v2/* lives alongside src/schemas/v1/* so consumers can validate against both.
Full versioning policy (what counts as breaking, minor, patch) is documented in dayglance-intent-protocol.md under "Versioning."
Notes for consumers
NotifySchemavalidates shape only. Event-conditional rules from the spec —previous_duepresent whenevent === 'rescheduled',completed_at == timestampwhenevent === 'completed'— are enforced by the emitter, not by the schema. If you need to enforce them at receive time, do a second-pass check on your end.- Normalize before validating in some flows. The schema accepts ISO date strings as
due;normalizeDueacceptstoday/tomorrow/undefinedand produces ISO strings. Call the normalizer first, then validate. createKeyassumes normalized inputs. The handler is expected to callnormalizeDuebeforecreateKeyso relative inputs like"today"and the literal date they resolve to produce the same key.schemas.v1.*is the stable namespace for protocol v1. Use the namespaced form if you want to be explicit about which version you validate against; use the flat form for convenience.- Runtime dependency:
zod. Kept as a single external import; tsup does not bundle it. Consumers can use their own zod version (any^4).
License
MIT — see LICENSE.
