@conversionpros/aiva 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (152) hide show
  1. package/README.md +148 -0
  2. package/auto-deploy.js +190 -0
  3. package/bin/aiva.js +81 -0
  4. package/cli-sync.js +126 -0
  5. package/d2a-prompt-template.txt +106 -0
  6. package/diagnostics-api.js +304 -0
  7. package/docs/ara-dedup-fix-scope.md +112 -0
  8. package/docs/ara-fix-round2-scope.md +61 -0
  9. package/docs/ara-greeting-fix-scope.md +70 -0
  10. package/docs/calendar-date-fix-scope.md +28 -0
  11. package/docs/getting-started.md +115 -0
  12. package/docs/network-architecture-rollout-scope.md +43 -0
  13. package/docs/scope-google-oauth-integration.md +351 -0
  14. package/docs/settings-page-scope.md +50 -0
  15. package/docs/xai-imagine-scope.md +116 -0
  16. package/docs/xai-voice-integration-scope.md +115 -0
  17. package/docs/xai-voice-tools-scope.md +165 -0
  18. package/email-router.js +512 -0
  19. package/follow-up-handler.js +606 -0
  20. package/gateway-monitor.js +158 -0
  21. package/google-email.js +379 -0
  22. package/google-oauth.js +310 -0
  23. package/grok-imagine.js +97 -0
  24. package/health-reporter.js +287 -0
  25. package/invisible-prefix-base.txt +206 -0
  26. package/invisible-prefix-owner.txt +26 -0
  27. package/invisible-prefix-slim.txt +10 -0
  28. package/invisible-prefix.txt +43 -0
  29. package/knowledge-base.js +472 -0
  30. package/lib/cli.js +19 -0
  31. package/lib/config.js +124 -0
  32. package/lib/health.js +57 -0
  33. package/lib/process.js +207 -0
  34. package/lib/server.js +42 -0
  35. package/lib/setup.js +472 -0
  36. package/meta-capi.js +206 -0
  37. package/meta-leads.js +411 -0
  38. package/notion-oauth.js +323 -0
  39. package/package.json +61 -0
  40. package/public/agent-config.html +241 -0
  41. package/public/aiva-avatar-anime.png +0 -0
  42. package/public/css/docs.css.bak +688 -0
  43. package/public/css/onboarding.css +543 -0
  44. package/public/diagrams/claude-subscription-pool.html +329 -0
  45. package/public/diagrams/claude-subscription-pool.png +0 -0
  46. package/public/docs-icon.png +0 -0
  47. package/public/escalation.html +237 -0
  48. package/public/group-config.html +300 -0
  49. package/public/icon-192.png +0 -0
  50. package/public/icon-512.png +0 -0
  51. package/public/icons/agents.svg +1 -0
  52. package/public/icons/attach.svg +1 -0
  53. package/public/icons/characters.svg +1 -0
  54. package/public/icons/chat.svg +1 -0
  55. package/public/icons/docs.svg +1 -0
  56. package/public/icons/heartbeat.svg +1 -0
  57. package/public/icons/messages.svg +1 -0
  58. package/public/icons/mic.svg +1 -0
  59. package/public/icons/notes.svg +1 -0
  60. package/public/icons/settings.svg +1 -0
  61. package/public/icons/tasks.svg +1 -0
  62. package/public/images/onboarding/p0-communication-layer.png +0 -0
  63. package/public/images/onboarding/p0-infinite-surface.png +0 -0
  64. package/public/images/onboarding/p0-learning-model.png +0 -0
  65. package/public/images/onboarding/p0-meet-aiva.png +0 -0
  66. package/public/images/onboarding/p4-contact-intelligence.png +0 -0
  67. package/public/images/onboarding/p4-context-compounds.png +0 -0
  68. package/public/images/onboarding/p4-message-router.png +0 -0
  69. package/public/images/onboarding/p4-per-contact-rules.png +0 -0
  70. package/public/images/onboarding/p4-send-messages.png +0 -0
  71. package/public/images/onboarding/p6-be-precise.png +0 -0
  72. package/public/images/onboarding/p6-review-escalations.png +0 -0
  73. package/public/images/onboarding/p6-voice-input.png +0 -0
  74. package/public/images/onboarding/p7-completion.png +0 -0
  75. package/public/index.html +11594 -0
  76. package/public/js/onboarding.js +699 -0
  77. package/public/manifest.json +24 -0
  78. package/public/messages-v2.html +2824 -0
  79. package/public/permission-approve.html.bak +107 -0
  80. package/public/permissions.html +150 -0
  81. package/public/styles/design-system.css +68 -0
  82. package/router-db.js +604 -0
  83. package/router-utils.js +28 -0
  84. package/router-v2/adapters/imessage.js +191 -0
  85. package/router-v2/adapters/quo.js +82 -0
  86. package/router-v2/adapters/whatsapp.js +192 -0
  87. package/router-v2/contact-manager.js +234 -0
  88. package/router-v2/conversation-engine.js +498 -0
  89. package/router-v2/data/knowledge-base.json +176 -0
  90. package/router-v2/data/router-v2.db +0 -0
  91. package/router-v2/data/router-v2.db-shm +0 -0
  92. package/router-v2/data/router-v2.db-wal +0 -0
  93. package/router-v2/data/router.db +0 -0
  94. package/router-v2/db.js +457 -0
  95. package/router-v2/escalation-bridge.js +540 -0
  96. package/router-v2/follow-up-engine.js +347 -0
  97. package/router-v2/index.js +441 -0
  98. package/router-v2/ingestion.js +213 -0
  99. package/router-v2/knowledge-base.js +231 -0
  100. package/router-v2/lead-qualifier.js +152 -0
  101. package/router-v2/learning-loop.js +202 -0
  102. package/router-v2/outbound-sender.js +160 -0
  103. package/router-v2/package.json +13 -0
  104. package/router-v2/permission-gate.js +86 -0
  105. package/router-v2/playbook.js +177 -0
  106. package/router-v2/prompts/base.js +52 -0
  107. package/router-v2/prompts/first-contact.js +38 -0
  108. package/router-v2/prompts/lead-qualification.js +37 -0
  109. package/router-v2/prompts/scheduling.js +72 -0
  110. package/router-v2/prompts/style-overrides.js +22 -0
  111. package/router-v2/scheduler.js +301 -0
  112. package/router-v2/scripts/migrate-v1-to-v2.js +215 -0
  113. package/router-v2/scripts/seed-faq.js +67 -0
  114. package/router-v2/seed-knowledge-base.js +39 -0
  115. package/router-v2/utils/ai.js +129 -0
  116. package/router-v2/utils/phone.js +52 -0
  117. package/router-v2/utils/response-validator.js +98 -0
  118. package/router-v2/utils/sanitize.js +222 -0
  119. package/router.js +5005 -0
  120. package/routes/google-calendar.js +186 -0
  121. package/scripts/deploy.sh +62 -0
  122. package/scripts/macos-calendar.sh +232 -0
  123. package/scripts/onboard-device.sh +466 -0
  124. package/server.js +5131 -0
  125. package/start.sh +24 -0
  126. package/templates/AGENTS.md +548 -0
  127. package/templates/IDENTITY.md +15 -0
  128. package/templates/docs-agents.html +132 -0
  129. package/templates/docs-app.html +130 -0
  130. package/templates/docs-home.html +83 -0
  131. package/templates/docs-imessage.html +121 -0
  132. package/templates/docs-tasks.html +123 -0
  133. package/templates/docs-tips.html +175 -0
  134. package/templates/getting-started.html +809 -0
  135. package/templates/invisible-prefix-base.txt +171 -0
  136. package/templates/invisible-prefix-owner.txt +282 -0
  137. package/templates/invisible-prefix.txt +338 -0
  138. package/templates/manifest.json +61 -0
  139. package/templates/memory-org/clients.md +7 -0
  140. package/templates/memory-org/credentials.md +9 -0
  141. package/templates/memory-org/devices.md +7 -0
  142. package/templates/updates.html +464 -0
  143. package/templates/workspace/AGENTS.md.tmpl +161 -0
  144. package/templates/workspace/HEARTBEAT.md.tmpl +17 -0
  145. package/templates/workspace/IDENTITY.md.tmpl +15 -0
  146. package/templates/workspace/MEMORY.md.tmpl +16 -0
  147. package/templates/workspace/SOUL.md.tmpl +51 -0
  148. package/templates/workspace/USER.md.tmpl +25 -0
  149. package/tts-proxy.js +96 -0
  150. package/voice-call-local.js +731 -0
  151. package/voice-call.js +732 -0
  152. package/wa-listener.js +354 -0
@@ -0,0 +1,186 @@
1
+ /**
2
+ * Google Calendar API v3 Endpoints - Phase 3
3
+ *
4
+ * All endpoints mounted under /api/integrations/google/calendars
5
+ * Uses OAuth tokens from google-oauth.js (auto-refresh)
6
+ *
7
+ * Query param `accountId` selects which linked Google account to use.
8
+ * If omitted and only one account exists, uses that one.
9
+ */
10
+
11
+ const express = require('express');
12
+ const router = express.Router();
13
+ const { getValidAccessToken, loadTokens } = require('../google-oauth');
14
+
15
+ const CALENDAR_API = 'https://www.googleapis.com/calendar/v3';
16
+
17
+ // ─── Helpers ──────────────────────────────────────────────────────────────────
18
+
19
+ async function resolveAccountId(accountId) {
20
+ if (accountId) return accountId;
21
+ const store = loadTokens();
22
+ const accounts = store.google?.accounts || [];
23
+ if (accounts.length === 1) return accounts[0].id;
24
+ if (accounts.length === 0) throw new Error('No Google accounts connected');
25
+ throw new Error('Multiple accounts connected — accountId is required');
26
+ }
27
+
28
+ function errorResponse(res, err) {
29
+ const message = err.message || 'Unknown error';
30
+ if (message === 'GOOGLE_AUTH_EXPIRED') {
31
+ return res.status(401).json({ success: false, error: 'GOOGLE_AUTH_EXPIRED', message: 'Google account requires re-authentication' });
32
+ }
33
+ if (message === 'Account not found') {
34
+ return res.status(404).json({ success: false, error: 'ACCOUNT_NOT_FOUND', message });
35
+ }
36
+ console.error('[google-calendar]', message);
37
+ return res.status(500).json({ success: false, error: message });
38
+ }
39
+
40
+ async function gcalFetch(token, path, options = {}) {
41
+ const url = `${CALENDAR_API}${path}`;
42
+ const resp = await fetch(url, {
43
+ ...options,
44
+ headers: {
45
+ Authorization: `Bearer ${token}`,
46
+ 'Content-Type': 'application/json',
47
+ ...options.headers,
48
+ },
49
+ });
50
+ if (!resp.ok) {
51
+ const body = await resp.text();
52
+ const err = new Error(`Google Calendar API ${resp.status}: ${body}`);
53
+ err.status = resp.status;
54
+ throw err;
55
+ }
56
+ if (resp.status === 204) return null;
57
+ return resp.json();
58
+ }
59
+
60
+ // ─── Routes ───────────────────────────────────────────────────────────────────
61
+
62
+ // GET / — List calendars
63
+ router.get('/', async (req, res) => {
64
+ try {
65
+ const accountId = await resolveAccountId(req.query.accountId);
66
+ const { token } = await getValidAccessToken(accountId);
67
+ const data = await gcalFetch(token, '/users/me/calendarList');
68
+ res.json({ success: true, data: data.items || [] });
69
+ } catch (e) {
70
+ errorResponse(res, e);
71
+ }
72
+ });
73
+
74
+ // GET /:calendarId/events — List events
75
+ router.get('/:calendarId/events', async (req, res) => {
76
+ try {
77
+ const accountId = await resolveAccountId(req.query.accountId);
78
+ const { token } = await getValidAccessToken(accountId);
79
+ const calId = encodeURIComponent(req.params.calendarId);
80
+
81
+ const params = new URLSearchParams();
82
+ if (req.query.timeMin) params.set('timeMin', req.query.timeMin);
83
+ if (req.query.timeMax) params.set('timeMax', req.query.timeMax);
84
+ params.set('maxResults', req.query.maxResults || '50');
85
+ params.set('singleEvents', req.query.singleEvents || 'true');
86
+ params.set('orderBy', req.query.orderBy || 'startTime');
87
+ if (req.query.pageToken) params.set('pageToken', req.query.pageToken);
88
+ if (req.query.q) params.set('q', req.query.q);
89
+
90
+ const data = await gcalFetch(token, `/calendars/${calId}/events?${params}`);
91
+ res.json({ success: true, data: { events: data.items || [], nextPageToken: data.nextPageToken || null } });
92
+ } catch (e) {
93
+ errorResponse(res, e);
94
+ }
95
+ });
96
+
97
+ // GET /:calendarId/events/:eventId — Get single event
98
+ router.get('/:calendarId/events/:eventId', async (req, res) => {
99
+ try {
100
+ const accountId = await resolveAccountId(req.query.accountId);
101
+ const { token } = await getValidAccessToken(accountId);
102
+ const calId = encodeURIComponent(req.params.calendarId);
103
+ const eventId = encodeURIComponent(req.params.eventId);
104
+
105
+ const data = await gcalFetch(token, `/calendars/${calId}/events/${eventId}`);
106
+ res.json({ success: true, data });
107
+ } catch (e) {
108
+ errorResponse(res, e);
109
+ }
110
+ });
111
+
112
+ // POST /:calendarId/events — Create event
113
+ router.post('/:calendarId/events', async (req, res) => {
114
+ try {
115
+ const accountId = await resolveAccountId(req.body.accountId || req.query.accountId);
116
+ const { token } = await getValidAccessToken(accountId);
117
+ const calId = encodeURIComponent(req.params.calendarId);
118
+
119
+ const { summary, start, end, description, attendees, location, recurrence, reminders, timeZone } = req.body;
120
+ const event = {};
121
+ if (summary) event.summary = summary;
122
+ if (description) event.description = description;
123
+ if (location) event.location = location;
124
+ if (recurrence) event.recurrence = recurrence;
125
+ if (reminders) event.reminders = reminders;
126
+ if (start) event.start = typeof start === 'string' ? { dateTime: start, timeZone: timeZone || 'America/Los_Angeles' } : start;
127
+ if (end) event.end = typeof end === 'string' ? { dateTime: end, timeZone: timeZone || 'America/Los_Angeles' } : end;
128
+ if (attendees) event.attendees = Array.isArray(attendees) ? attendees.map(a => typeof a === 'string' ? { email: a } : a) : attendees;
129
+
130
+ const data = await gcalFetch(token, `/calendars/${calId}/events`, {
131
+ method: 'POST',
132
+ body: JSON.stringify(event),
133
+ });
134
+ res.json({ success: true, data });
135
+ } catch (e) {
136
+ errorResponse(res, e);
137
+ }
138
+ });
139
+
140
+ // PUT /:calendarId/events/:eventId — Update event
141
+ router.put('/:calendarId/events/:eventId', async (req, res) => {
142
+ try {
143
+ const accountId = await resolveAccountId(req.body.accountId || req.query.accountId);
144
+ const { token } = await getValidAccessToken(accountId);
145
+ const calId = encodeURIComponent(req.params.calendarId);
146
+ const eventId = encodeURIComponent(req.params.eventId);
147
+
148
+ // First fetch existing event, then merge
149
+ const existing = await gcalFetch(token, `/calendars/${calId}/events/${eventId}`);
150
+ const { summary, start, end, description, attendees, location, recurrence, reminders, timeZone } = req.body;
151
+
152
+ if (summary !== undefined) existing.summary = summary;
153
+ if (description !== undefined) existing.description = description;
154
+ if (location !== undefined) existing.location = location;
155
+ if (recurrence !== undefined) existing.recurrence = recurrence;
156
+ if (reminders !== undefined) existing.reminders = reminders;
157
+ if (start) existing.start = typeof start === 'string' ? { dateTime: start, timeZone: timeZone || 'America/Los_Angeles' } : start;
158
+ if (end) existing.end = typeof end === 'string' ? { dateTime: end, timeZone: timeZone || 'America/Los_Angeles' } : end;
159
+ if (attendees) existing.attendees = Array.isArray(attendees) ? attendees.map(a => typeof a === 'string' ? { email: a } : a) : attendees;
160
+
161
+ const data = await gcalFetch(token, `/calendars/${calId}/events/${eventId}`, {
162
+ method: 'PUT',
163
+ body: JSON.stringify(existing),
164
+ });
165
+ res.json({ success: true, data });
166
+ } catch (e) {
167
+ errorResponse(res, e);
168
+ }
169
+ });
170
+
171
+ // DELETE /:calendarId/events/:eventId — Delete event
172
+ router.delete('/:calendarId/events/:eventId', async (req, res) => {
173
+ try {
174
+ const accountId = await resolveAccountId(req.body.accountId || req.query.accountId);
175
+ const { token } = await getValidAccessToken(accountId);
176
+ const calId = encodeURIComponent(req.params.calendarId);
177
+ const eventId = encodeURIComponent(req.params.eventId);
178
+
179
+ await gcalFetch(token, `/calendars/${calId}/events/${eventId}`, { method: 'DELETE' });
180
+ res.json({ success: true, data: { deleted: true } });
181
+ } catch (e) {
182
+ errorResponse(res, e);
183
+ }
184
+ });
185
+
186
+ module.exports = router;
@@ -0,0 +1,62 @@
1
+ #!/bin/bash
2
+ # AIVA App Deploy Script
3
+ # Pushes latest code to GitHub and deploys to all remote devices
4
+ # Usage: bash scripts/deploy.sh [message]
5
+
6
+ set -e
7
+
8
+ COMMIT_MSG="${1:-Auto-deploy from Mac Studio}"
9
+ REMOTE_DEVICES=(
10
+ "aiva@100.95.26.113" # nates-mac-mini
11
+ # "jamesbrown@100.88.226.81" # james-mac-mini (offline)
12
+ # "brandonburgan@100.72.95.78" # conversion-mac-mini
13
+ )
14
+ REMOTE_PASS="Ttsrgr812!"
15
+
16
+ echo "📦 Committing and pushing to GitHub..."
17
+ cd ~/.openclaw/workspace/aiva-tasks
18
+ git add -A
19
+ git commit -m "$COMMIT_MSG" 2>/dev/null || echo "Nothing to commit"
20
+ git push origin main
21
+
22
+ echo ""
23
+ echo "🚀 Deploying to remote devices..."
24
+
25
+ for DEVICE in "${REMOTE_DEVICES[@]}"; do
26
+ USER=$(echo $DEVICE | cut -d@ -f1)
27
+ IP=$(echo $DEVICE | cut -d@ -f2)
28
+ # Adjust app dir per user
29
+ APP_DIR="/Users/$USER/.openclaw/workspace/aiva-tasks"
30
+
31
+ echo " → Deploying to $DEVICE..."
32
+
33
+ # Check if device is reachable
34
+ if ! sshpass -p "$REMOTE_PASS" ssh -o ConnectTimeout=5 -o StrictHostKeyChecking=no "$DEVICE" "echo ok" 2>/dev/null; then
35
+ echo " ⚠️ $DEVICE unreachable, skipping"
36
+ continue
37
+ fi
38
+
39
+ # Check if repo exists, clone or pull
40
+ sshpass -p "$REMOTE_PASS" ssh -o StrictHostKeyChecking=no "$DEVICE" "
41
+ export PATH=/opt/homebrew/bin:/usr/local/bin:\$PATH
42
+ if [ -d '$APP_DIR/.git' ]; then
43
+ cd '$APP_DIR' && git pull origin main
44
+ else
45
+ # First time: clone the repo
46
+ git clone https://github.com/mistermakeithappen/aiva-app.git '$APP_DIR'
47
+ fi
48
+
49
+ # Install dependencies if package.json changed
50
+ cd '$APP_DIR' && npm install --production 2>/dev/null
51
+
52
+ # Restart the app
53
+ pm2 delete aiva-app 2>/dev/null
54
+ pm2 start server.js --name aiva-app
55
+ echo 'Done!'
56
+ " 2>&1 | sed "s/^/ /"
57
+
58
+ echo " ✅ $DEVICE deployed"
59
+ done
60
+
61
+ echo ""
62
+ echo "✅ Deploy complete!"
@@ -0,0 +1,232 @@
1
+ #!/bin/bash
2
+ # macos-calendar.sh — Calendar.app utility for reading/writing Google Calendar events via JXA
3
+ # Replaces `gog calendar` on non-Brandon devices where OAuth tokens break.
4
+ # Uses osascript (JavaScript for Automation) to interact with Calendar.app directly.
5
+
6
+ set -euo pipefail
7
+
8
+ TIMEZONE="America/Los_Angeles"
9
+
10
+ usage() {
11
+ cat <<'EOF'
12
+ macos-calendar.sh — macOS Calendar.app utility (JXA-based)
13
+
14
+ USAGE:
15
+ macos-calendar.sh list --from <date> --to <date> [--calendar <name>]
16
+ macos-calendar.sh create --title <t> --start <dt> --end <dt> [--calendar <c>] [--location <l>] [--notes <n>] [--attendee <email>]...
17
+ macos-calendar.sh delete --title <t> --date <d> [--calendar <c>]
18
+
19
+ DATES:
20
+ "today", "tomorrow", or YYYY-MM-DD / YYYY-MM-DDTHH:MM:SS
21
+
22
+ EXAMPLES:
23
+ macos-calendar.sh list --from today --to today
24
+ macos-calendar.sh list --from 2026-02-16 --to 2026-02-17 --calendar "nate@conversionmarketingpros.com"
25
+ macos-calendar.sh create --title "Standup" --start "2026-02-17T09:00:00" --end "2026-02-17T09:30:00" --calendar "nate@conversionmarketingpros.com"
26
+ macos-calendar.sh delete --title "Standup" --date 2026-02-17
27
+ EOF
28
+ exit 0
29
+ }
30
+
31
+ resolve_date() {
32
+ local d="$1"
33
+ case "$d" in
34
+ today) date +%Y-%m-%d ;;
35
+ tomorrow) date -v+1d +%Y-%m-%d ;;
36
+ *) echo "$d" ;;
37
+ esac
38
+ }
39
+
40
+ # ─── LIST ────────────────────────────────────────────────────────────────────
41
+ do_list() {
42
+ local from_date="" to_date="" calendar_filter=""
43
+ while [[ $# -gt 0 ]]; do
44
+ case "$1" in
45
+ --from) from_date="$(resolve_date "$2")"; shift 2 ;;
46
+ --to) to_date="$(resolve_date "$2")"; shift 2 ;;
47
+ --calendar) calendar_filter="$2"; shift 2 ;;
48
+ *) echo "Unknown option: $1" >&2; exit 1 ;;
49
+ esac
50
+ done
51
+
52
+ [[ -z "$from_date" || -z "$to_date" ]] && { echo "Error: --from and --to required" >&2; exit 1; }
53
+
54
+ # If to_date has no time component, set end of day
55
+ [[ "$to_date" != *T* ]] && to_date="${to_date}T23:59:59"
56
+ [[ "$from_date" != *T* ]] && from_date="${from_date}T00:00:00"
57
+
58
+ osascript -l JavaScript <<JXAEOF
59
+ var app = Application('Calendar');
60
+ var cals = app.calendars();
61
+ var fromDate = new Date('${from_date}');
62
+ var toDate = new Date('${to_date}');
63
+ var calFilter = '${calendar_filter}';
64
+ var results = [];
65
+
66
+ for (var i = 0; i < cals.length; i++) {
67
+ var cal = cals[i];
68
+ var calName = cal.name();
69
+ if (calFilter && calName !== calFilter) continue;
70
+
71
+ var events = cal.events.whose({
72
+ _and: [
73
+ { startDate: { _greaterThan: new Date(fromDate.getTime() - 1000) } },
74
+ { startDate: { _lessThan: toDate } }
75
+ ]
76
+ })();
77
+
78
+ for (var j = 0; j < events.length; j++) {
79
+ var e = events[j];
80
+ var s = e.startDate();
81
+ var en = e.endDate();
82
+ var allDay = false;
83
+ try { allDay = e.alldayEvent(); } catch(ex) {}
84
+ var loc = '';
85
+ try { loc = e.location() || ''; } catch(ex) {}
86
+ var notes = '';
87
+ try { notes = e.description() || ''; } catch(ex) {}
88
+
89
+ var pad = function(n) { return n < 10 ? '0' + n : '' + n; };
90
+ var fmt = function(d) {
91
+ return d.getFullYear() + '-' + pad(d.getMonth()+1) + '-' + pad(d.getDate()) +
92
+ 'T' + pad(d.getHours()) + ':' + pad(d.getMinutes()) + ':' + pad(d.getSeconds());
93
+ };
94
+
95
+ results.push({
96
+ title: e.summary(),
97
+ start: fmt(s),
98
+ end: fmt(en),
99
+ location: loc,
100
+ calendar: calName,
101
+ notes: notes,
102
+ allDay: allDay
103
+ });
104
+ }
105
+ }
106
+
107
+ // Sort by start time
108
+ results.sort(function(a, b) { return a.start < b.start ? -1 : 1; });
109
+ JSON.stringify(results, null, 2);
110
+ JXAEOF
111
+ }
112
+
113
+ # ─── CREATE ──────────────────────────────────────────────────────────────────
114
+ do_create() {
115
+ local title="" start="" end="" calendar="" location="" notes=""
116
+ local -a attendees=()
117
+ while [[ $# -gt 0 ]]; do
118
+ case "$1" in
119
+ --title) title="$2"; shift 2 ;;
120
+ --start) start="$2"; shift 2 ;;
121
+ --end) end="$2"; shift 2 ;;
122
+ --calendar) calendar="$2"; shift 2 ;;
123
+ --location) location="$2"; shift 2 ;;
124
+ --notes) notes="$2"; shift 2 ;;
125
+ --attendee) attendees+=("$2"); shift 2 ;;
126
+ *) echo "Unknown option: $1" >&2; exit 1 ;;
127
+ esac
128
+ done
129
+
130
+ [[ -z "$title" || -z "$start" || -z "$end" ]] && { echo "Error: --title, --start, --end required" >&2; exit 1; }
131
+
132
+ # Build attendee JS array
133
+ local attendee_js="[]"
134
+ if [[ ${#attendees[@]} -gt 0 ]]; then
135
+ attendee_js="["
136
+ for a in "${attendees[@]}"; do
137
+ attendee_js+="'${a}',"
138
+ done
139
+ attendee_js+="]"
140
+ fi
141
+
142
+ osascript -l JavaScript <<JXAEOF
143
+ var app = Application('Calendar');
144
+ var calName = '${calendar}';
145
+ var cal;
146
+
147
+ if (calName) {
148
+ var cals = app.calendars.whose({ name: calName })();
149
+ if (cals.length === 0) {
150
+ throw new Error('Calendar not found: ' + calName);
151
+ }
152
+ cal = cals[0];
153
+ } else {
154
+ cal = app.defaultCalendar();
155
+ }
156
+
157
+ var evt = app.Event({
158
+ summary: $(printf '%s' "$title" | python3 -c "import sys,json; print(json.dumps(sys.stdin.read()))"),
159
+ startDate: new Date('${start}'),
160
+ endDate: new Date('${end}'),
161
+ location: $(printf '%s' "$location" | python3 -c "import sys,json; print(json.dumps(sys.stdin.read()))"),
162
+ description: $(printf '%s' "$notes" | python3 -c "import sys,json; print(json.dumps(sys.stdin.read()))")
163
+ });
164
+
165
+ cal.events.push(evt);
166
+
167
+ // Add attendees if any
168
+ var attendeeEmails = ${attendee_js};
169
+ for (var i = 0; i < attendeeEmails.length; i++) {
170
+ var att = app.Attendee({ email: attendeeEmails[i] });
171
+ try { evt.attendees.push(att); } catch(ex) { /* Calendar.app may not support adding attendees via JXA */ }
172
+ }
173
+
174
+ JSON.stringify({ status: 'created', title: evt.summary(), start: '${start}', end: '${end}', calendar: cal.name() });
175
+ JXAEOF
176
+ }
177
+
178
+ # ─── DELETE ──────────────────────────────────────────────────────────────────
179
+ do_delete() {
180
+ local title="" date="" calendar=""
181
+ while [[ $# -gt 0 ]]; do
182
+ case "$1" in
183
+ --title) title="$2"; shift 2 ;;
184
+ --date) date="$(resolve_date "$2")"; shift 2 ;;
185
+ --calendar) calendar="$2"; shift 2 ;;
186
+ *) echo "Unknown option: $1" >&2; exit 1 ;;
187
+ esac
188
+ done
189
+
190
+ [[ -z "$title" || -z "$date" ]] && { echo "Error: --title and --date required" >&2; exit 1; }
191
+
192
+ osascript -l JavaScript <<JXAEOF
193
+ var app = Application('Calendar');
194
+ var cals = app.calendars();
195
+ var targetTitle = $(printf '%s' "$title" | python3 -c "import sys,json; print(json.dumps(sys.stdin.read()))");
196
+ var dayStart = new Date('${date}T00:00:00');
197
+ var dayEnd = new Date('${date}T23:59:59');
198
+ var calFilter = '${calendar}';
199
+ var deleted = 0;
200
+
201
+ for (var i = 0; i < cals.length; i++) {
202
+ var cal = cals[i];
203
+ if (calFilter && cal.name() !== calFilter) continue;
204
+
205
+ var events = cal.events.whose({
206
+ _and: [
207
+ { summary: targetTitle },
208
+ { startDate: { _greaterThan: new Date(dayStart.getTime() - 1000) } },
209
+ { startDate: { _lessThan: dayEnd } }
210
+ ]
211
+ })();
212
+
213
+ for (var j = events.length - 1; j >= 0; j--) {
214
+ app.delete(events[j]);
215
+ deleted++;
216
+ }
217
+ }
218
+
219
+ JSON.stringify({ status: 'deleted', count: deleted, title: targetTitle, date: '${date}' });
220
+ JXAEOF
221
+ }
222
+
223
+ # ─── MAIN ────────────────────────────────────────────────────────────────────
224
+ [[ $# -eq 0 ]] && usage
225
+
226
+ case "$1" in
227
+ list) shift; do_list "$@" ;;
228
+ create) shift; do_create "$@" ;;
229
+ delete) shift; do_delete "$@" ;;
230
+ --help|-h|help) usage ;;
231
+ *) echo "Unknown command: $1" >&2; usage ;;
232
+ esac