@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.
- package/README.md +148 -0
- package/auto-deploy.js +190 -0
- package/bin/aiva.js +81 -0
- package/cli-sync.js +126 -0
- package/d2a-prompt-template.txt +106 -0
- package/diagnostics-api.js +304 -0
- package/docs/ara-dedup-fix-scope.md +112 -0
- package/docs/ara-fix-round2-scope.md +61 -0
- package/docs/ara-greeting-fix-scope.md +70 -0
- package/docs/calendar-date-fix-scope.md +28 -0
- package/docs/getting-started.md +115 -0
- package/docs/network-architecture-rollout-scope.md +43 -0
- package/docs/scope-google-oauth-integration.md +351 -0
- package/docs/settings-page-scope.md +50 -0
- package/docs/xai-imagine-scope.md +116 -0
- package/docs/xai-voice-integration-scope.md +115 -0
- package/docs/xai-voice-tools-scope.md +165 -0
- package/email-router.js +512 -0
- package/follow-up-handler.js +606 -0
- package/gateway-monitor.js +158 -0
- package/google-email.js +379 -0
- package/google-oauth.js +310 -0
- package/grok-imagine.js +97 -0
- package/health-reporter.js +287 -0
- package/invisible-prefix-base.txt +206 -0
- package/invisible-prefix-owner.txt +26 -0
- package/invisible-prefix-slim.txt +10 -0
- package/invisible-prefix.txt +43 -0
- package/knowledge-base.js +472 -0
- package/lib/cli.js +19 -0
- package/lib/config.js +124 -0
- package/lib/health.js +57 -0
- package/lib/process.js +207 -0
- package/lib/server.js +42 -0
- package/lib/setup.js +472 -0
- package/meta-capi.js +206 -0
- package/meta-leads.js +411 -0
- package/notion-oauth.js +323 -0
- package/package.json +61 -0
- package/public/agent-config.html +241 -0
- package/public/aiva-avatar-anime.png +0 -0
- package/public/css/docs.css.bak +688 -0
- package/public/css/onboarding.css +543 -0
- package/public/diagrams/claude-subscription-pool.html +329 -0
- package/public/diagrams/claude-subscription-pool.png +0 -0
- package/public/docs-icon.png +0 -0
- package/public/escalation.html +237 -0
- package/public/group-config.html +300 -0
- package/public/icon-192.png +0 -0
- package/public/icon-512.png +0 -0
- package/public/icons/agents.svg +1 -0
- package/public/icons/attach.svg +1 -0
- package/public/icons/characters.svg +1 -0
- package/public/icons/chat.svg +1 -0
- package/public/icons/docs.svg +1 -0
- package/public/icons/heartbeat.svg +1 -0
- package/public/icons/messages.svg +1 -0
- package/public/icons/mic.svg +1 -0
- package/public/icons/notes.svg +1 -0
- package/public/icons/settings.svg +1 -0
- package/public/icons/tasks.svg +1 -0
- package/public/images/onboarding/p0-communication-layer.png +0 -0
- package/public/images/onboarding/p0-infinite-surface.png +0 -0
- package/public/images/onboarding/p0-learning-model.png +0 -0
- package/public/images/onboarding/p0-meet-aiva.png +0 -0
- package/public/images/onboarding/p4-contact-intelligence.png +0 -0
- package/public/images/onboarding/p4-context-compounds.png +0 -0
- package/public/images/onboarding/p4-message-router.png +0 -0
- package/public/images/onboarding/p4-per-contact-rules.png +0 -0
- package/public/images/onboarding/p4-send-messages.png +0 -0
- package/public/images/onboarding/p6-be-precise.png +0 -0
- package/public/images/onboarding/p6-review-escalations.png +0 -0
- package/public/images/onboarding/p6-voice-input.png +0 -0
- package/public/images/onboarding/p7-completion.png +0 -0
- package/public/index.html +11594 -0
- package/public/js/onboarding.js +699 -0
- package/public/manifest.json +24 -0
- package/public/messages-v2.html +2824 -0
- package/public/permission-approve.html.bak +107 -0
- package/public/permissions.html +150 -0
- package/public/styles/design-system.css +68 -0
- package/router-db.js +604 -0
- package/router-utils.js +28 -0
- package/router-v2/adapters/imessage.js +191 -0
- package/router-v2/adapters/quo.js +82 -0
- package/router-v2/adapters/whatsapp.js +192 -0
- package/router-v2/contact-manager.js +234 -0
- package/router-v2/conversation-engine.js +498 -0
- package/router-v2/data/knowledge-base.json +176 -0
- package/router-v2/data/router-v2.db +0 -0
- package/router-v2/data/router-v2.db-shm +0 -0
- package/router-v2/data/router-v2.db-wal +0 -0
- package/router-v2/data/router.db +0 -0
- package/router-v2/db.js +457 -0
- package/router-v2/escalation-bridge.js +540 -0
- package/router-v2/follow-up-engine.js +347 -0
- package/router-v2/index.js +441 -0
- package/router-v2/ingestion.js +213 -0
- package/router-v2/knowledge-base.js +231 -0
- package/router-v2/lead-qualifier.js +152 -0
- package/router-v2/learning-loop.js +202 -0
- package/router-v2/outbound-sender.js +160 -0
- package/router-v2/package.json +13 -0
- package/router-v2/permission-gate.js +86 -0
- package/router-v2/playbook.js +177 -0
- package/router-v2/prompts/base.js +52 -0
- package/router-v2/prompts/first-contact.js +38 -0
- package/router-v2/prompts/lead-qualification.js +37 -0
- package/router-v2/prompts/scheduling.js +72 -0
- package/router-v2/prompts/style-overrides.js +22 -0
- package/router-v2/scheduler.js +301 -0
- package/router-v2/scripts/migrate-v1-to-v2.js +215 -0
- package/router-v2/scripts/seed-faq.js +67 -0
- package/router-v2/seed-knowledge-base.js +39 -0
- package/router-v2/utils/ai.js +129 -0
- package/router-v2/utils/phone.js +52 -0
- package/router-v2/utils/response-validator.js +98 -0
- package/router-v2/utils/sanitize.js +222 -0
- package/router.js +5005 -0
- package/routes/google-calendar.js +186 -0
- package/scripts/deploy.sh +62 -0
- package/scripts/macos-calendar.sh +232 -0
- package/scripts/onboard-device.sh +466 -0
- package/server.js +5131 -0
- package/start.sh +24 -0
- package/templates/AGENTS.md +548 -0
- package/templates/IDENTITY.md +15 -0
- package/templates/docs-agents.html +132 -0
- package/templates/docs-app.html +130 -0
- package/templates/docs-home.html +83 -0
- package/templates/docs-imessage.html +121 -0
- package/templates/docs-tasks.html +123 -0
- package/templates/docs-tips.html +175 -0
- package/templates/getting-started.html +809 -0
- package/templates/invisible-prefix-base.txt +171 -0
- package/templates/invisible-prefix-owner.txt +282 -0
- package/templates/invisible-prefix.txt +338 -0
- package/templates/manifest.json +61 -0
- package/templates/memory-org/clients.md +7 -0
- package/templates/memory-org/credentials.md +9 -0
- package/templates/memory-org/devices.md +7 -0
- package/templates/updates.html +464 -0
- package/templates/workspace/AGENTS.md.tmpl +161 -0
- package/templates/workspace/HEARTBEAT.md.tmpl +17 -0
- package/templates/workspace/IDENTITY.md.tmpl +15 -0
- package/templates/workspace/MEMORY.md.tmpl +16 -0
- package/templates/workspace/SOUL.md.tmpl +51 -0
- package/templates/workspace/USER.md.tmpl +25 -0
- package/tts-proxy.js +96 -0
- package/voice-call-local.js +731 -0
- package/voice-call.js +732 -0
- 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
|