@pheem49/mint 1.4.1 → 1.5.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 (61) hide show
  1. package/GUIDE_TH.md +113 -0
  2. package/README.md +214 -142
  3. package/assets/CLI_Screen.png +0 -0
  4. package/docs/assets/CLI_Screen.png +0 -0
  5. package/docs/guide.html +632 -0
  6. package/docs/index.html +5 -4
  7. package/main.js +66 -894
  8. package/mint-cli-logic.js +15 -8
  9. package/mint-cli.js +305 -195
  10. package/package.json +12 -4
  11. package/src/AI_Brain/Gemini_API.js +77 -20
  12. package/src/AI_Brain/agent_orchestrator.js +6 -6
  13. package/src/AI_Brain/autonomous_brain.js +10 -0
  14. package/src/AI_Brain/behavior_memory.js +26 -5
  15. package/src/AI_Brain/headless_agent.js +4 -0
  16. package/src/AI_Brain/knowledge_base.js +61 -8
  17. package/src/AI_Brain/memory_store.js +55 -7
  18. package/src/Automation_Layer/file_operations.js +14 -3
  19. package/src/CLI/chat_router.js +21 -7
  20. package/src/CLI/chat_ui.js +264 -710
  21. package/src/CLI/code_agent.js +370 -124
  22. package/src/CLI/gmail_auth.js +210 -0
  23. package/src/CLI/list_features.js +5 -1
  24. package/src/CLI/onboarding.js +307 -55
  25. package/src/CLI/updater.js +208 -0
  26. package/src/Channels/brave_search_bridge.js +35 -0
  27. package/src/Channels/discord_bridge.js +68 -0
  28. package/src/Channels/google_search_bridge.js +38 -0
  29. package/src/Channels/line_bridge.js +60 -0
  30. package/src/Channels/slack_bridge.js +53 -0
  31. package/src/Channels/telegram_bridge.js +49 -0
  32. package/src/Channels/whatsapp_bridge.js +55 -0
  33. package/src/Command_Parser/parser.js +12 -1
  34. package/src/Plugins/gmail.js +251 -0
  35. package/src/Plugins/google_calendar.js +245 -19
  36. package/src/Plugins/notion.js +256 -0
  37. package/src/System/action_executor.js +129 -0
  38. package/src/System/bridge_manager.js +76 -0
  39. package/src/System/chat_history_manager.js +23 -5
  40. package/src/System/config_manager.js +41 -7
  41. package/src/System/custom_workflows.js +31 -2
  42. package/src/System/google_tts_urls.js +51 -0
  43. package/src/System/ipc_handlers.js +238 -0
  44. package/src/System/proactive_loop.js +137 -0
  45. package/src/System/safety_manager.js +165 -0
  46. package/src/System/screen_capture.js +175 -0
  47. package/src/System/task_manager.js +15 -5
  48. package/src/System/window_manager.js +210 -0
  49. package/src/UI/renderer.js +33 -7
  50. package/src/UI/settings.html +24 -0
  51. package/src/UI/settings.js +14 -4
  52. package/src/UI/styles.css +14 -1
  53. package/tests/action_executor_safety.test.js +67 -0
  54. package/tests/gmail.test.js +135 -0
  55. package/tests/gmail_auth.test.js +129 -0
  56. package/tests/google_calendar.test.js +113 -0
  57. package/tests/google_tts_urls.test.js +24 -0
  58. package/tests/notion.test.js +121 -0
  59. package/tests/provider_routing.test.js +17 -1
  60. package/tests/safety_manager.test.js +40 -0
  61. package/tests/updater.test.js +32 -0
@@ -0,0 +1,251 @@
1
+ const axios = require('axios');
2
+ const { readConfig } = require('../System/config_manager');
3
+
4
+ const TOKEN_URL = 'https://oauth2.googleapis.com/token';
5
+ const GMAIL_API_BASE = 'https://gmail.googleapis.com/gmail/v1';
6
+
7
+ function hasGmailConfig(config) {
8
+ return Boolean(config.gmailClientId && config.gmailClientSecret && config.gmailRefreshToken);
9
+ }
10
+
11
+ function parseInstruction(instruction) {
12
+ const raw = (instruction || '').trim();
13
+ if (!raw) return { action: 'help' };
14
+
15
+ try {
16
+ const parsed = JSON.parse(raw);
17
+ if (parsed && typeof parsed === 'object') {
18
+ return {
19
+ action: normalizeAction(parsed.action || 'search'),
20
+ ...parsed
21
+ };
22
+ }
23
+ } catch {
24
+ // Plain text searches Gmail.
25
+ }
26
+
27
+ const lower = raw.toLowerCase();
28
+ if (lower === 'help') return { action: 'help' };
29
+ if (lower === 'unread') return { action: 'search', query: 'is:unread' };
30
+ if (lower === 'inbox') return { action: 'search', query: 'in:inbox' };
31
+ if (lower.startsWith('read ')) return { action: 'read', id: raw.slice(5).trim() };
32
+ if (lower.startsWith('draft ')) return { action: 'draft', body: raw.slice(6).trim() };
33
+ if (lower.startsWith('search ')) return { action: 'search', query: raw.slice(7).trim() };
34
+
35
+ return { action: 'search', query: raw };
36
+ }
37
+
38
+ function normalizeAction(action) {
39
+ const normalized = String(action || '').toLowerCase();
40
+ if (['list', 'search', 'inbox', 'unread'].includes(normalized)) return 'search';
41
+ if (['get', 'read', 'read_email', 'message'].includes(normalized)) return 'read';
42
+ if (['draft', 'create_draft', 'compose', 'write'].includes(normalized)) return 'draft';
43
+ return normalized;
44
+ }
45
+
46
+ function gmailUserId(config) {
47
+ return encodeURIComponent(config.gmailUserId || 'me');
48
+ }
49
+
50
+ async function getAccessToken(config) {
51
+ const params = new URLSearchParams({
52
+ client_id: config.gmailClientId,
53
+ client_secret: config.gmailClientSecret,
54
+ refresh_token: config.gmailRefreshToken,
55
+ grant_type: 'refresh_token'
56
+ });
57
+
58
+ const response = await axios.post(TOKEN_URL, params.toString(), {
59
+ headers: { 'Content-Type': 'application/x-www-form-urlencoded' }
60
+ });
61
+
62
+ return response.data.access_token;
63
+ }
64
+
65
+ function gmailHeaders(accessToken) {
66
+ return {
67
+ Authorization: `Bearer ${accessToken}`,
68
+ 'Content-Type': 'application/json'
69
+ };
70
+ }
71
+
72
+ function decodeBase64Url(data = '') {
73
+ const normalized = String(data).replace(/-/g, '+').replace(/_/g, '/');
74
+ const padded = normalized.padEnd(normalized.length + ((4 - normalized.length % 4) % 4), '=');
75
+ return Buffer.from(padded, 'base64').toString('utf8');
76
+ }
77
+
78
+ function encodeBase64Url(data = '') {
79
+ return Buffer.from(String(data), 'utf8')
80
+ .toString('base64')
81
+ .replace(/\+/g, '-')
82
+ .replace(/\//g, '_')
83
+ .replace(/=+$/g, '');
84
+ }
85
+
86
+ function getHeader(message, name) {
87
+ const headers = message.payload?.headers || [];
88
+ const found = headers.find(header => header.name && header.name.toLowerCase() === name.toLowerCase());
89
+ return found ? found.value : '';
90
+ }
91
+
92
+ function findTextPart(payload) {
93
+ if (!payload) return '';
94
+ if (payload.mimeType === 'text/plain' && payload.body?.data) {
95
+ return decodeBase64Url(payload.body.data);
96
+ }
97
+ if (payload.mimeType === 'text/html' && payload.body?.data) {
98
+ return decodeBase64Url(payload.body.data).replace(/<[^>]+>/g, ' ').replace(/\s+/g, ' ').trim();
99
+ }
100
+ for (const part of payload.parts || []) {
101
+ const text = findTextPart(part);
102
+ if (text) return text;
103
+ }
104
+ return payload.body?.data ? decodeBase64Url(payload.body.data) : '';
105
+ }
106
+
107
+ function formatMessageSummary(message) {
108
+ const subject = getHeader(message, 'Subject') || '(No subject)';
109
+ const from = getHeader(message, 'From') || '(Unknown sender)';
110
+ const date = getHeader(message, 'Date');
111
+ const snippet = message.snippet || '';
112
+ return [
113
+ `ID: ${message.id}`,
114
+ `From: ${from}`,
115
+ `Subject: ${subject}`,
116
+ date ? `Date: ${date}` : '',
117
+ snippet ? `Snippet: ${snippet}` : ''
118
+ ].filter(Boolean).join('\n');
119
+ }
120
+
121
+ async function fetchMessage(config, accessToken, id, format = 'metadata') {
122
+ const response = await axios.get(`${GMAIL_API_BASE}/users/${gmailUserId(config)}/messages/${encodeURIComponent(id)}`, {
123
+ headers: gmailHeaders(accessToken),
124
+ params: {
125
+ format,
126
+ metadataHeaders: ['From', 'To', 'Subject', 'Date']
127
+ }
128
+ });
129
+ return response.data;
130
+ }
131
+
132
+ async function searchMessages(config, input, accessToken) {
133
+ const query = input.query || input.q || 'in:inbox';
134
+ const maxResults = Number(input.maxResults || input.limit || 10);
135
+ const response = await axios.get(`${GMAIL_API_BASE}/users/${gmailUserId(config)}/messages`, {
136
+ headers: gmailHeaders(accessToken),
137
+ params: {
138
+ q: query,
139
+ maxResults
140
+ }
141
+ });
142
+
143
+ const messages = response.data.messages || [];
144
+ if (messages.length === 0) return `No Gmail messages found for query: ${query}`;
145
+
146
+ const detailed = [];
147
+ for (const message of messages.slice(0, maxResults)) {
148
+ const full = await fetchMessage(config, accessToken, message.id, 'metadata');
149
+ detailed.push(formatMessageSummary(full));
150
+ }
151
+
152
+ return `Gmail search results for "${query}":\n\n${detailed.join('\n\n')}`;
153
+ }
154
+
155
+ async function readMessage(config, input, accessToken) {
156
+ const id = input.id || input.messageId;
157
+ if (!id) throw new Error('Missing Gmail message id.');
158
+
159
+ const message = await fetchMessage(config, accessToken, id, 'full');
160
+ const body = findTextPart(message.payload);
161
+ return [
162
+ formatMessageSummary(message),
163
+ '',
164
+ body ? `Body:\n${body.slice(0, Number(input.maxChars || 4000))}` : 'Body: (No readable text body found)'
165
+ ].join('\n');
166
+ }
167
+
168
+ function sanitizeHeader(value = '') {
169
+ return String(value).replace(/[\r\n]+/g, ' ').trim();
170
+ }
171
+
172
+ function buildRawEmail(input) {
173
+ const to = sanitizeHeader(input.to || input.recipient || '');
174
+ if (!to) throw new Error('Missing email recipient.');
175
+
176
+ const cc = sanitizeHeader(input.cc || '');
177
+ const bcc = sanitizeHeader(input.bcc || '');
178
+ const subject = sanitizeHeader(input.subject || '(No subject)');
179
+ const body = String(input.body || input.content || input.text || '');
180
+
181
+ const headers = [
182
+ `To: ${to}`,
183
+ cc ? `Cc: ${cc}` : '',
184
+ bcc ? `Bcc: ${bcc}` : '',
185
+ `Subject: ${subject}`,
186
+ 'MIME-Version: 1.0',
187
+ 'Content-Type: text/plain; charset="UTF-8"'
188
+ ].filter(Boolean);
189
+
190
+ return encodeBase64Url(`${headers.join('\r\n')}\r\n\r\n${body}`);
191
+ }
192
+
193
+ async function createDraft(config, input, accessToken) {
194
+ const raw = buildRawEmail(input);
195
+ const response = await axios.post(`${GMAIL_API_BASE}/users/${gmailUserId(config)}/drafts`, {
196
+ message: { raw }
197
+ }, {
198
+ headers: gmailHeaders(accessToken)
199
+ });
200
+
201
+ const draft = response.data || {};
202
+ return `Created Gmail draft${draft.id ? ` ${draft.id}` : ''} for ${sanitizeHeader(input.to || input.recipient)}. Review it in Gmail before sending.`;
203
+ }
204
+
205
+ function helpText() {
206
+ return [
207
+ 'Gmail plugin commands:',
208
+ '- Search inbox: {"action":"search","query":"in:inbox newer_than:7d","limit":5}',
209
+ '- Read message: {"action":"read","id":"MESSAGE_ID"}',
210
+ '- Create draft: {"action":"draft","to":"person@example.com","subject":"Hello","body":"Draft body"}',
211
+ 'For safety, this plugin creates drafts only. It does not send email automatically.'
212
+ ].join('\n');
213
+ }
214
+
215
+ module.exports = {
216
+ name: 'gmail',
217
+ description: 'Manage Gmail safely. Target can be JSON: {"action":"search","query":"in:inbox is:unread","limit":10}, {"action":"read","id":"MESSAGE_ID"}, or {"action":"draft","to":"person@example.com","subject":"Subject","body":"Body"}. This plugin creates drafts only and does not send email.',
218
+
219
+ async execute(instruction) {
220
+ const config = readConfig();
221
+ const input = parseInstruction(instruction);
222
+
223
+ if (input.action === 'help') return helpText();
224
+ if (!hasGmailConfig(config)) {
225
+ return 'Gmail API is not configured. Add Gmail OAuth credentials with `mint onboard`. Use scopes for gmail.readonly and gmail.compose.';
226
+ }
227
+
228
+ const accessToken = await getAccessToken(config);
229
+
230
+ switch (input.action) {
231
+ case 'search':
232
+ return await searchMessages(config, input, accessToken);
233
+ case 'read':
234
+ return await readMessage(config, input, accessToken);
235
+ case 'draft':
236
+ return await createDraft(config, input, accessToken);
237
+ default:
238
+ throw new Error(`Unsupported Gmail action: ${input.action}`);
239
+ }
240
+ },
241
+
242
+ _helpers: {
243
+ parseInstruction,
244
+ buildRawEmail,
245
+ decodeBase64Url,
246
+ encodeBase64Url,
247
+ findTextPart,
248
+ formatMessageSummary,
249
+ hasGmailConfig
250
+ }
251
+ };
@@ -1,26 +1,252 @@
1
- const { shell } = require('electron');
1
+ const axios = require('axios');
2
+ const { readConfig } = require('../System/config_manager');
3
+
4
+ let shell = null;
5
+ try {
6
+ ({ shell } = require('electron'));
7
+ } catch {
8
+ shell = null;
9
+ }
10
+
11
+ const TOKEN_URL = 'https://oauth2.googleapis.com/token';
12
+ const CALENDAR_API_BASE = 'https://www.googleapis.com/calendar/v3';
13
+
14
+ function hasCalendarApiConfig(config) {
15
+ return Boolean(
16
+ config.googleCalendarClientId &&
17
+ config.googleCalendarClientSecret &&
18
+ config.googleCalendarRefreshToken
19
+ );
20
+ }
21
+
22
+ function parseInstruction(instruction) {
23
+ const raw = (instruction || '').trim();
24
+ if (!raw) return { action: 'open' };
25
+
26
+ try {
27
+ const parsed = JSON.parse(raw);
28
+ if (parsed && typeof parsed === 'object') {
29
+ return {
30
+ action: (parsed.action || 'create').toLowerCase(),
31
+ ...parsed
32
+ };
33
+ }
34
+ } catch {
35
+ // Plain text remains supported for backward compatibility.
36
+ }
37
+
38
+ const lower = raw.toLowerCase();
39
+ if (['open', 'view', 'calendar'].includes(lower)) return { action: 'open' };
40
+ if (['today', 'list today'].includes(lower)) return { action: 'list', range: 'today' };
41
+ if (lower.startsWith('list') || lower.startsWith('upcoming')) return { action: 'list', range: 'upcoming' };
42
+
43
+ return { action: 'create', summary: raw };
44
+ }
45
+
46
+ async function getAccessToken(config) {
47
+ const params = new URLSearchParams({
48
+ client_id: config.googleCalendarClientId,
49
+ client_secret: config.googleCalendarClientSecret,
50
+ refresh_token: config.googleCalendarRefreshToken,
51
+ grant_type: 'refresh_token'
52
+ });
53
+
54
+ const response = await axios.post(TOKEN_URL, params.toString(), {
55
+ headers: { 'Content-Type': 'application/x-www-form-urlencoded' }
56
+ });
57
+
58
+ return response.data.access_token;
59
+ }
60
+
61
+ function getCalendarId(config) {
62
+ return config.googleCalendarId || 'primary';
63
+ }
64
+
65
+ function getLocalDayBounds(date = new Date()) {
66
+ const start = new Date(date);
67
+ start.setHours(0, 0, 0, 0);
68
+
69
+ const end = new Date(start);
70
+ end.setDate(end.getDate() + 1);
71
+
72
+ return { start, end };
73
+ }
74
+
75
+ function addDays(date, days) {
76
+ const next = new Date(date);
77
+ next.setDate(next.getDate() + days);
78
+ return next;
79
+ }
80
+
81
+ function addDaysToIsoDate(dateString, days) {
82
+ const [year, month, day] = dateString.split('-').map(Number);
83
+ const date = new Date(Date.UTC(year, month - 1, day));
84
+ date.setUTCDate(date.getUTCDate() + days);
85
+ return date.toISOString().slice(0, 10);
86
+ }
87
+
88
+ function formatEventTime(event) {
89
+ const start = event.start || {};
90
+ const end = event.end || {};
91
+ const startValue = start.dateTime || start.date;
92
+ const endValue = end.dateTime || end.date;
93
+
94
+ if (!startValue) return '';
95
+ if (start.date) return startValue;
96
+
97
+ const startText = new Date(startValue).toLocaleString('th-TH', {
98
+ dateStyle: 'medium',
99
+ timeStyle: 'short'
100
+ });
101
+ if (!endValue) return startText;
102
+
103
+ const endText = new Date(endValue).toLocaleTimeString('th-TH', {
104
+ hour: '2-digit',
105
+ minute: '2-digit'
106
+ });
107
+ return `${startText} - ${endText}`;
108
+ }
109
+
110
+ function buildEventPayload(input) {
111
+ const summary = (input.summary || input.title || input.name || '').trim();
112
+ if (!summary) {
113
+ throw new Error('Missing event summary/title.');
114
+ }
115
+
116
+ const payload = {
117
+ summary,
118
+ description: input.description || undefined,
119
+ location: input.location || undefined
120
+ };
121
+
122
+ if (input.start || input.startDateTime || input.end || input.endDateTime) {
123
+ const start = input.start || input.startDateTime;
124
+ const end = input.end || input.endDateTime;
125
+ if (!start) throw new Error('Missing event start time.');
126
+
127
+ payload.start = { dateTime: new Date(start).toISOString() };
128
+ payload.end = { dateTime: end ? new Date(end).toISOString() : new Date(new Date(start).getTime() + 60 * 60 * 1000).toISOString() };
129
+ } else if (input.date) {
130
+ payload.start = { date: input.date };
131
+ payload.end = { date: input.endDate || input.date };
132
+ if (payload.end.date === payload.start.date) {
133
+ payload.end.date = addDaysToIsoDate(input.date, 1);
134
+ }
135
+ } else {
136
+ const now = new Date();
137
+ const start = addDays(now, 1);
138
+ start.setHours(9, 0, 0, 0);
139
+ payload.start = { dateTime: start.toISOString() };
140
+ payload.end = { dateTime: new Date(start.getTime() + 60 * 60 * 1000).toISOString() };
141
+ }
142
+
143
+ return payload;
144
+ }
145
+
146
+ async function listEvents(config, input, accessToken) {
147
+ const now = new Date();
148
+ let timeMin = now;
149
+ let timeMax = addDays(now, Number(input.days || 7));
150
+
151
+ if (input.range === 'today') {
152
+ const bounds = getLocalDayBounds(now);
153
+ timeMin = bounds.start;
154
+ timeMax = bounds.end;
155
+ }
156
+
157
+ if (input.timeMin) timeMin = new Date(input.timeMin);
158
+ if (input.timeMax) timeMax = new Date(input.timeMax);
159
+
160
+ const calendarId = encodeURIComponent(getCalendarId(config));
161
+ const response = await axios.get(`${CALENDAR_API_BASE}/calendars/${calendarId}/events`, {
162
+ headers: { Authorization: `Bearer ${accessToken}` },
163
+ params: {
164
+ singleEvents: true,
165
+ orderBy: 'startTime',
166
+ maxResults: Number(input.maxResults || 10),
167
+ timeMin: timeMin.toISOString(),
168
+ timeMax: timeMax.toISOString()
169
+ }
170
+ });
171
+
172
+ const events = response.data.items || [];
173
+ if (events.length === 0) {
174
+ return input.range === 'today'
175
+ ? 'No Google Calendar events found for today. 📅'
176
+ : 'No upcoming Google Calendar events found. 📅';
177
+ }
178
+
179
+ const lines = events.map((event, index) => {
180
+ const when = formatEventTime(event);
181
+ return `${index + 1}. ${event.summary || '(Untitled)'}${when ? ` — ${when}` : ''}`;
182
+ });
183
+
184
+ return `Google Calendar events:\n${lines.join('\n')}`;
185
+ }
186
+
187
+ async function createEvent(config, input, accessToken) {
188
+ const payload = buildEventPayload(input);
189
+ const calendarId = encodeURIComponent(getCalendarId(config));
190
+ const response = await axios.post(`${CALENDAR_API_BASE}/calendars/${calendarId}/events`, payload, {
191
+ headers: {
192
+ Authorization: `Bearer ${accessToken}`,
193
+ 'Content-Type': 'application/json'
194
+ }
195
+ });
196
+
197
+ const event = response.data || {};
198
+ return `Created "${event.summary || payload.summary}" in Google Calendar. 📅${event.htmlLink ? `\n${event.htmlLink}` : ''}`;
199
+ }
200
+
201
+ function openCalendarFallback(input) {
202
+ if (!shell || typeof shell.openExternal !== 'function') {
203
+ return 'Google Calendar API is not configured, and this environment cannot open a browser.';
204
+ }
205
+
206
+ if (input.action === 'open') {
207
+ shell.openExternal('https://calendar.google.com/');
208
+ return 'Opening Google Calendar. 📅';
209
+ }
210
+
211
+ const title = encodeURIComponent(input.summary || input.title || input.name || 'New event');
212
+ const url = `https://calendar.google.com/calendar/r/eventedit?text=${title}`;
213
+ shell.openExternal(url);
214
+ return `Google Calendar API is not configured, so I opened the event creation page for "${decodeURIComponent(title)}" instead. 📅`;
215
+ }
2
216
 
3
217
  module.exports = {
4
218
  name: 'google_calendar',
5
- description: 'Quickly open Google Calendar to add a new event or view the calendar. Instruction should be the event title (e.g., "Meeting with team"). If no title, just put "open".',
6
-
219
+ description: 'Manage Google Calendar. Target can be JSON: {"action":"list","range":"today|upcoming","days":7} or {"action":"create","summary":"Meeting","start":"2026-05-15T10:00:00+07:00","end":"2026-05-15T11:00:00+07:00","description":"","location":""}. Plain text creates a new event title. Use action "open" to open Calendar.',
220
+
7
221
  async execute(instruction) {
8
- const inst = (instruction || '').trim();
9
-
10
- if (!inst || inst.toLowerCase() === 'open') {
11
- shell.openExternal('https://calendar.google.com/');
12
- return 'กำลังเปิดหน้าต่างปฏิทินให้ค่ะ 📅';
13
- }
14
-
15
- // Encode the event title for the URL
16
- const title = encodeURIComponent(inst);
17
- const url = `https://calendar.google.com/calendar/r/eventedit?text=${title}`;
18
-
19
- try {
20
- shell.openExternal(url);
21
- return `กำลังเปิดหน้าต่างสร้างกิจกรรม "${inst}" ใน Google Calendar ให้ลูกพี่ค่ะ 📅✨`;
22
- } catch (e) {
23
- return `เกิดข้อผิดพลาดในการเปิด Calendar ค่ะ: ${e.message}`;
222
+ const config = readConfig();
223
+ const input = parseInstruction(instruction);
224
+
225
+ if (!hasCalendarApiConfig(config)) {
226
+ return openCalendarFallback(input);
227
+ }
228
+
229
+ const accessToken = await getAccessToken(config);
230
+
231
+ if (input.action === 'list' || input.action === 'today' || input.action === 'upcoming') {
232
+ return await listEvents(config, input, accessToken);
233
+ }
234
+
235
+ if (input.action === 'open') {
236
+ return openCalendarFallback(input);
24
237
  }
238
+
239
+ if (input.action === 'create' || input.action === 'add') {
240
+ return await createEvent(config, input, accessToken);
241
+ }
242
+
243
+ throw new Error(`Unsupported Google Calendar action: ${input.action}`);
244
+ },
245
+
246
+ _helpers: {
247
+ parseInstruction,
248
+ buildEventPayload,
249
+ hasCalendarApiConfig,
250
+ addDaysToIsoDate
25
251
  }
26
252
  };