From 93f6c8711373e7c257604e92d216a2ef9597b620 Mon Sep 17 00:00:00 2001 From: Jason Woltje Date: Thu, 29 Jan 2026 21:11:50 -0600 Subject: [PATCH] feat(#24): implement mosaic-plugin-calendar skill --- packages/skills/README.md | 35 +++ packages/skills/calendar/SKILL.md | 150 ++++++++++ packages/skills/calendar/scripts/calendar.js | 294 +++++++++++++++++++ 3 files changed, 479 insertions(+) create mode 100644 packages/skills/README.md create mode 100644 packages/skills/calendar/SKILL.md create mode 100755 packages/skills/calendar/scripts/calendar.js diff --git a/packages/skills/README.md b/packages/skills/README.md new file mode 100644 index 0000000..de4ce1d --- /dev/null +++ b/packages/skills/README.md @@ -0,0 +1,35 @@ +# Mosaic Stack Skills + +This directory contains Clawdbot skills for integrating with Mosaic Stack APIs. + +## Skills + +### Calendar (`calendar/`) + +Integration with Mosaic Stack's Events API for calendar management. + +**Features:** +- Create events with rich metadata +- Query events with flexible filtering +- Update and reschedule events +- Delete/cancel events +- Natural language interaction support + +**Usage:** See `calendar/SKILL.md` for complete documentation. + +## Skill Structure + +Each skill follows Clawdbot skill conventions: + +``` +skill-name/ +├── SKILL.md # Skill documentation and metadata +└── scripts/ # Executable tools for skill functionality +``` + +## Adding New Skills + +1. Create a new directory under `packages/skills/` +2. Add `SKILL.md` with YAML frontmatter and documentation +3. Add any necessary scripts or resources +4. Update this README diff --git a/packages/skills/calendar/SKILL.md b/packages/skills/calendar/SKILL.md new file mode 100644 index 0000000..cd08adf --- /dev/null +++ b/packages/skills/calendar/SKILL.md @@ -0,0 +1,150 @@ +--- +name: mosaic-plugin-calendar +description: Integration with Mosaic Stack's Events API for calendar management. Use when the user wants to schedule, view, update, or cancel events/meetings/appointments in their Mosaic calendar, including queries like "schedule a meeting", "what's on my calendar", "upcoming events", "cancel meeting", or "reschedule appointment". +--- + +# Mosaic Calendar + +Integration with Mosaic Stack's Events API for comprehensive calendar management. + +## Quick Start + +Use `scripts/calendar.js` for all calendar operations. The script handles authentication and API communication automatically. + +### Creating Events + +```bash +node scripts/calendar.js create \ + --title "Team Meeting" \ + --start "2024-01-30T15:00:00Z" \ + --end "2024-01-30T16:00:00Z" \ + --description "Weekly sync" \ + --location "Conference Room A" +``` + +Natural language examples: +- "Schedule a meeting tomorrow at 3pm" +- "Create an event called 'dentist appointment' on Friday at 2pm" +- "Add a recurring standup every weekday at 9am" + +### Querying Events + +```bash +# List all upcoming events +node scripts/calendar.js list + +# Events in a date range +node scripts/calendar.js list \ + --from "2024-01-30" \ + --to "2024-02-05" + +# Events for a specific project +node scripts/calendar.js list --project-id "uuid-here" +``` + +Natural language examples: +- "What's on my calendar this week?" +- "Show me upcoming events" +- "What do I have scheduled for tomorrow?" +- "Any meetings today?" + +### Updating Events + +```bash +node scripts/calendar.js update EVENT_ID \ + --title "Updated Title" \ + --start "2024-01-30T16:00:00Z" +``` + +Natural language examples: +- "Move my 3pm meeting to 4pm" +- "Reschedule tomorrow's dentist appointment to Friday" +- "Change the location of my team meeting to Zoom" + +### Deleting Events + +```bash +node scripts/calendar.js delete EVENT_ID +``` + +Natural language examples: +- "Cancel my meeting with Sarah" +- "Delete tomorrow's standup" +- "Remove the 3pm appointment" + +## Event Fields + +- **title** (required): Event name (1-255 characters) +- **startTime** (required): ISO 8601 date string (e.g., "2024-01-30T15:00:00Z") +- **endTime** (optional): ISO 8601 date string +- **description** (optional): Event details (max 10,000 characters) +- **location** (optional): Physical or virtual location (max 500 characters) +- **allDay** (optional): Boolean flag for all-day events +- **recurrence** (optional): Recurrence rules (object) +- **projectId** (optional): Link event to a Mosaic project (UUID) +- **metadata** (optional): Custom key-value data (object) + +## Date Handling + +When processing natural language date/time requests: + +1. Convert to ISO 8601 format (e.g., "2024-01-30T15:00:00Z") +2. Handle relative dates ("tomorrow", "next week", "Friday") +3. Infer end time if not specified (default: 1 hour after start) +4. Use user's timezone (America/Chicago) unless specified otherwise + +## Response Format + +All operations return JSON: + +```json +{ + "id": "uuid", + "title": "Team Meeting", + "startTime": "2024-01-30T15:00:00Z", + "endTime": "2024-01-30T16:00:00Z", + "description": "Weekly sync", + "location": "Conference Room A", + "allDay": false, + "workspaceId": "uuid", + "createdBy": "uuid", + "projectId": null, + "recurrence": null, + "metadata": {}, + "createdAt": "2024-01-29T10:00:00Z", + "updatedAt": "2024-01-29T10:00:00Z" +} +``` + +List queries return paginated results with metadata: + +```json +{ + "data": [...], + "pagination": { + "page": 1, + "limit": 50, + "total": 100, + "totalPages": 2 + } +} +``` + +## Environment + +The script reads configuration from: +- `MOSAIC_API_URL` (default: http://localhost:3001) +- `MOSAIC_WORKSPACE_ID` (required) +- `MOSAIC_API_TOKEN` (required for authentication) + +Ensure these are set in the environment or `.env` file. + +## Error Handling + +Common errors: +- **401 Unauthorized**: Missing or invalid API token +- **403 Forbidden**: Insufficient workspace permissions +- **404 Not Found**: Event ID doesn't exist +- **400 Bad Request**: Invalid date format or missing required fields + +When operations fail, the script outputs clear error messages with guidance on resolution. diff --git a/packages/skills/calendar/scripts/calendar.js b/packages/skills/calendar/scripts/calendar.js new file mode 100755 index 0000000..c76346e --- /dev/null +++ b/packages/skills/calendar/scripts/calendar.js @@ -0,0 +1,294 @@ +#!/usr/bin/env node + +/** + * Mosaic Calendar CLI + * Interacts with Mosaic Stack's Events API for calendar management + */ + +const https = require('https'); +const http = require('http'); +const { URL } = require('url'); + +// Configuration +const API_URL = process.env.MOSAIC_API_URL || 'http://localhost:3001'; +const WORKSPACE_ID = process.env.MOSAIC_WORKSPACE_ID; +const API_TOKEN = process.env.MOSAIC_API_TOKEN; + +/** + * Make HTTP request to Mosaic API + */ +function apiRequest(method, path, body = null) { + return new Promise((resolve, reject) => { + const url = new URL(path, API_URL); + const isHttps = url.protocol === 'https:'; + const client = isHttps ? https : http; + + const options = { + method, + hostname: url.hostname, + port: url.port || (isHttps ? 443 : 80), + path: url.pathname + url.search, + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${API_TOKEN}`, + 'X-Workspace-ID': WORKSPACE_ID + } + }; + + const req = client.request(options, (res) => { + let data = ''; + res.on('data', (chunk) => data += chunk); + res.on('end', () => { + try { + const parsed = data ? JSON.parse(data) : {}; + if (res.statusCode >= 200 && res.statusCode < 300) { + resolve(parsed); + } else { + reject(new Error(`HTTP ${res.statusCode}: ${parsed.message || data}`)); + } + } catch (err) { + reject(new Error(`Failed to parse response: ${data}`)); + } + }); + }); + + req.on('error', reject); + + if (body) { + req.write(JSON.stringify(body)); + } + + req.end(); + }); +} + +/** + * Parse command-line arguments + */ +function parseArgs(args) { + const parsed = { _: [] }; + for (let i = 0; i < args.length; i++) { + const arg = args[i]; + if (arg.startsWith('--')) { + const key = arg.slice(2); + const value = args[i + 1]; + if (value && !value.startsWith('--')) { + parsed[key] = value; + i++; + } else { + parsed[key] = true; + } + } else { + parsed._.push(arg); + } + } + return parsed; +} + +/** + * Create a new event + */ +async function createEvent(args) { + if (!args.title || !args.start) { + throw new Error('Required: --title and --start (ISO 8601 date)'); + } + + const body = { + title: args.title, + startTime: args.start, + }; + + if (args.end) body.endTime = args.end; + if (args.description) body.description = args.description; + if (args.location) body.location = args.location; + if (args['all-day'] !== undefined) body.allDay = args['all-day'] === 'true'; + if (args['project-id']) body.projectId = args['project-id']; + if (args.metadata) body.metadata = JSON.parse(args.metadata); + + const result = await apiRequest('POST', '/api/events', body); + return result; +} + +/** + * List events with optional filters + */ +async function listEvents(args) { + const params = new URLSearchParams(); + + if (args.from) params.append('startFrom', args.from); + if (args.to) params.append('startTo', args.to); + if (args['project-id']) params.append('projectId', args['project-id']); + if (args['all-day'] !== undefined) params.append('allDay', args['all-day']); + if (args.page) params.append('page', args.page); + if (args.limit) params.append('limit', args.limit); + + const query = params.toString(); + const path = `/api/events${query ? '?' + query : ''}`; + + const result = await apiRequest('GET', path); + return result; +} + +/** + * Get a specific event by ID + */ +async function getEvent(eventId) { + if (!eventId) { + throw new Error('Event ID required'); + } + + const result = await apiRequest('GET', `/api/events/${eventId}`); + return result; +} + +/** + * Update an event + */ +async function updateEvent(eventId, args) { + if (!eventId) { + throw new Error('Event ID required'); + } + + const body = {}; + + if (args.title) body.title = args.title; + if (args.start) body.startTime = args.start; + if (args.end) body.endTime = args.end; + if (args.description) body.description = args.description; + if (args.location) body.location = args.location; + if (args['all-day'] !== undefined) body.allDay = args['all-day'] === 'true'; + if (args['project-id']) body.projectId = args['project-id']; + if (args.metadata) body.metadata = JSON.parse(args.metadata); + + const result = await apiRequest('PATCH', `/api/events/${eventId}`, body); + return result; +} + +/** + * Delete an event + */ +async function deleteEvent(eventId) { + if (!eventId) { + throw new Error('Event ID required'); + } + + const result = await apiRequest('DELETE', `/api/events/${eventId}`); + return result; +} + +/** + * Main CLI handler + */ +async function main() { + const args = parseArgs(process.argv.slice(2)); + const command = args._[0]; + + // Validate environment + if (!WORKSPACE_ID) { + console.error('Error: MOSAIC_WORKSPACE_ID environment variable required'); + process.exit(1); + } + + if (!API_TOKEN) { + console.error('Error: MOSAIC_API_TOKEN environment variable required'); + process.exit(1); + } + + try { + let result; + + switch (command) { + case 'create': + result = await createEvent(args); + console.log(JSON.stringify(result, null, 2)); + break; + + case 'list': + result = await listEvents(args); + console.log(JSON.stringify(result, null, 2)); + break; + + case 'get': + result = await getEvent(args._[1]); + console.log(JSON.stringify(result, null, 2)); + break; + + case 'update': + result = await updateEvent(args._[1], args); + console.log(JSON.stringify(result, null, 2)); + break; + + case 'delete': + result = await deleteEvent(args._[1]); + console.log(JSON.stringify(result, null, 2)); + break; + + case 'help': + default: + console.log(` +Mosaic Calendar CLI + +Usage: + node calendar.js [options] + +Commands: + create Create a new event + list List events with optional filters + get Get a specific event by ID + update Update an event + delete Delete an event + +Create Options: + --title Event title (required) + --start Start time in ISO 8601 format (required) + --end End time in ISO 8601 format + --description Event description + --location Event location + --all-day All-day event (true/false) + --project-id Link to project UUID + --metadata JSON metadata object + +List Options: + --from Start date filter (ISO 8601) + --to End date filter (ISO 8601) + --project-id Filter by project UUID + --all-day Filter all-day events (true/false) + --page Page number (default: 1) + --limit Results per page (default: 50, max: 100) + +Update Options: + Same as create options (all optional) + +Examples: + node calendar.js create --title "Meeting" --start "2024-01-30T15:00:00Z" + node calendar.js list --from "2024-01-30" --to "2024-02-05" + node calendar.js get abc-123-def + node calendar.js update abc-123-def --title "Updated Meeting" + node calendar.js delete abc-123-def + +Environment: + MOSAIC_API_URL API base URL (default: http://localhost:3001) + MOSAIC_WORKSPACE_ID Workspace UUID (required) + MOSAIC_API_TOKEN Authentication token (required) + `); + break; + } + } catch (error) { + console.error('Error:', error.message); + process.exit(1); + } +} + +// Run if called directly +if (require.main === module) { + main(); +} + +module.exports = { + createEvent, + listEvents, + getEvent, + updateEvent, + deleteEvent, + apiRequest +};