@pheem49/mint 1.4.2 → 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 (60) hide show
  1. package/GUIDE_TH.md +113 -0
  2. package/README.md +239 -76
  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 +13 -1
  9. package/mint-cli.js +100 -9
  10. package/package.json +12 -4
  11. package/src/AI_Brain/Gemini_API.js +77 -20
  12. package/src/AI_Brain/autonomous_brain.js +10 -0
  13. package/src/AI_Brain/behavior_memory.js +26 -5
  14. package/src/AI_Brain/headless_agent.js +4 -0
  15. package/src/AI_Brain/knowledge_base.js +61 -8
  16. package/src/AI_Brain/memory_store.js +55 -7
  17. package/src/Automation_Layer/file_operations.js +1 -1
  18. package/src/CLI/chat_router.js +3 -2
  19. package/src/CLI/chat_ui.js +263 -838
  20. package/src/CLI/code_agent.js +144 -42
  21. package/src/CLI/gmail_auth.js +210 -0
  22. package/src/CLI/list_features.js +2 -0
  23. package/src/CLI/onboarding.js +307 -55
  24. package/src/CLI/updater.js +208 -0
  25. package/src/Channels/brave_search_bridge.js +35 -0
  26. package/src/Channels/discord_bridge.js +68 -0
  27. package/src/Channels/google_search_bridge.js +38 -0
  28. package/src/Channels/line_bridge.js +60 -0
  29. package/src/Channels/slack_bridge.js +53 -0
  30. package/src/Channels/telegram_bridge.js +49 -0
  31. package/src/Channels/whatsapp_bridge.js +55 -0
  32. package/src/Command_Parser/parser.js +12 -1
  33. package/src/Plugins/gmail.js +251 -0
  34. package/src/Plugins/google_calendar.js +245 -19
  35. package/src/Plugins/notion.js +256 -0
  36. package/src/System/action_executor.js +129 -0
  37. package/src/System/bridge_manager.js +76 -0
  38. package/src/System/chat_history_manager.js +23 -5
  39. package/src/System/config_manager.js +41 -7
  40. package/src/System/custom_workflows.js +31 -2
  41. package/src/System/google_tts_urls.js +51 -0
  42. package/src/System/ipc_handlers.js +238 -0
  43. package/src/System/proactive_loop.js +137 -0
  44. package/src/System/safety_manager.js +165 -0
  45. package/src/System/screen_capture.js +175 -0
  46. package/src/System/task_manager.js +15 -5
  47. package/src/System/window_manager.js +210 -0
  48. package/src/UI/renderer.js +33 -7
  49. package/src/UI/settings.html +24 -0
  50. package/src/UI/settings.js +14 -4
  51. package/src/UI/styles.css +14 -1
  52. package/tests/action_executor_safety.test.js +67 -0
  53. package/tests/gmail.test.js +135 -0
  54. package/tests/gmail_auth.test.js +129 -0
  55. package/tests/google_calendar.test.js +113 -0
  56. package/tests/google_tts_urls.test.js +24 -0
  57. package/tests/notion.test.js +121 -0
  58. package/tests/provider_routing.test.js +17 -1
  59. package/tests/safety_manager.test.js +40 -0
  60. package/tests/updater.test.js +32 -0
@@ -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
  };
@@ -0,0 +1,256 @@
1
+ const axios = require('axios');
2
+ const { readConfig } = require('../System/config_manager');
3
+
4
+ const NOTION_API_BASE = 'https://api.notion.com/v1';
5
+ const NOTION_VERSION = '2022-06-28';
6
+
7
+ function hasNotionConfig(config) {
8
+ return Boolean(config.notionApiKey);
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 || 'create_page'),
20
+ ...parsed
21
+ };
22
+ }
23
+ } catch {
24
+ // Plain text creates a note/page.
25
+ }
26
+
27
+ const lower = raw.toLowerCase();
28
+ if (lower === 'help') return { action: 'help' };
29
+ if (lower === 'list' || lower.startsWith('list database')) return { action: 'query_database' };
30
+ if (lower.startsWith('read database')) return { action: 'query_database' };
31
+
32
+ const [firstLine, ...rest] = raw.split('\n');
33
+ return {
34
+ action: 'create_page',
35
+ title: firstLine.trim() || 'Mint Note',
36
+ content: rest.join('\n').trim() || raw
37
+ };
38
+ }
39
+
40
+ function normalizeAction(action) {
41
+ const normalized = String(action || '').toLowerCase();
42
+ if (['create', 'create_note', 'note', 'create_page', 'page'].includes(normalized)) return 'create_page';
43
+ if (['list', 'read', 'query', 'query_database', 'read_database'].includes(normalized)) return 'query_database';
44
+ if (['append', 'append_block', 'append_to_page'].includes(normalized)) return 'append_block';
45
+ return normalized;
46
+ }
47
+
48
+ function notionHeaders(config) {
49
+ return {
50
+ Authorization: `Bearer ${config.notionApiKey}`,
51
+ 'Notion-Version': NOTION_VERSION,
52
+ 'Content-Type': 'application/json'
53
+ };
54
+ }
55
+
56
+ function textBlock(text) {
57
+ return {
58
+ object: 'block',
59
+ type: 'paragraph',
60
+ paragraph: {
61
+ rich_text: [
62
+ {
63
+ type: 'text',
64
+ text: { content: String(text || '') }
65
+ }
66
+ ]
67
+ }
68
+ };
69
+ }
70
+
71
+ function headingBlock(text) {
72
+ return {
73
+ object: 'block',
74
+ type: 'heading_2',
75
+ heading_2: {
76
+ rich_text: [
77
+ {
78
+ type: 'text',
79
+ text: { content: String(text || '') }
80
+ }
81
+ ]
82
+ }
83
+ };
84
+ }
85
+
86
+ function buildChildren(input) {
87
+ const content = input.content || input.body || input.text || '';
88
+ const blocks = [];
89
+
90
+ if (Array.isArray(input.children)) return input.children;
91
+ if (input.heading) blocks.push(headingBlock(input.heading));
92
+
93
+ const paragraphs = String(content || '')
94
+ .split(/\n\s*\n/)
95
+ .map(part => part.trim())
96
+ .filter(Boolean);
97
+
98
+ for (const paragraph of paragraphs.length ? paragraphs : ['Created by Mint.']) {
99
+ blocks.push(textBlock(paragraph));
100
+ }
101
+
102
+ return blocks;
103
+ }
104
+
105
+ function buildDatabaseProperties(input, config) {
106
+ const title = input.title || input.summary || input.name || 'Mint Note';
107
+ const titleProperty = input.titleProperty || config.notionTitleProperty || 'Name';
108
+ const properties = {
109
+ [titleProperty]: {
110
+ title: [
111
+ {
112
+ text: { content: title }
113
+ }
114
+ ]
115
+ }
116
+ };
117
+
118
+ if (input.properties && typeof input.properties === 'object') {
119
+ return { ...properties, ...input.properties };
120
+ }
121
+
122
+ return properties;
123
+ }
124
+
125
+ function formatNotionTitle(properties = {}) {
126
+ for (const property of Object.values(properties)) {
127
+ if (property && property.type === 'title' && Array.isArray(property.title)) {
128
+ const title = property.title.map(part => part.plain_text || part.text?.content || '').join('');
129
+ if (title) return title;
130
+ }
131
+ }
132
+ return '(Untitled)';
133
+ }
134
+
135
+ async function createPage(config, input) {
136
+ const databaseId = input.databaseId || config.notionDatabaseId;
137
+ const pageId = input.pageId || config.notionPageId;
138
+
139
+ if (!databaseId && !pageId) {
140
+ throw new Error('Missing Notion databaseId or pageId. Configure one in onboarding or pass it in the instruction JSON.');
141
+ }
142
+
143
+ const payload = databaseId
144
+ ? {
145
+ parent: { database_id: databaseId },
146
+ properties: buildDatabaseProperties(input, config),
147
+ children: buildChildren(input)
148
+ }
149
+ : {
150
+ parent: { page_id: pageId },
151
+ properties: {
152
+ title: [
153
+ {
154
+ text: { content: input.title || input.summary || input.name || 'Mint Note' }
155
+ }
156
+ ]
157
+ },
158
+ children: buildChildren(input)
159
+ };
160
+
161
+ const response = await axios.post(`${NOTION_API_BASE}/pages`, payload, {
162
+ headers: notionHeaders(config)
163
+ });
164
+
165
+ const page = response.data || {};
166
+ const title = input.title || input.summary || input.name || 'Mint Note';
167
+ return `Created Notion page "${title}".${page.url ? `\n${page.url}` : ''}`;
168
+ }
169
+
170
+ async function queryDatabase(config, input) {
171
+ const databaseId = input.databaseId || config.notionDatabaseId;
172
+ if (!databaseId) {
173
+ throw new Error('Missing Notion databaseId. Configure one in onboarding or pass it in the instruction JSON.');
174
+ }
175
+
176
+ const payload = {
177
+ page_size: Number(input.pageSize || input.limit || 10)
178
+ };
179
+
180
+ if (input.filter) payload.filter = input.filter;
181
+ if (input.sorts) payload.sorts = input.sorts;
182
+
183
+ const response = await axios.post(`${NOTION_API_BASE}/databases/${databaseId}/query`, payload, {
184
+ headers: notionHeaders(config)
185
+ });
186
+
187
+ const results = response.data.results || [];
188
+ if (results.length === 0) return 'No Notion database pages found.';
189
+
190
+ const lines = results.map((page, index) => {
191
+ const title = formatNotionTitle(page.properties);
192
+ return `${index + 1}. ${title}${page.url ? ` — ${page.url}` : ''}`;
193
+ });
194
+
195
+ return `Notion database pages:\n${lines.join('\n')}`;
196
+ }
197
+
198
+ async function appendBlock(config, input) {
199
+ const pageId = input.pageId || config.notionPageId;
200
+ if (!pageId) {
201
+ throw new Error('Missing Notion pageId. Configure one in onboarding or pass it in the instruction JSON.');
202
+ }
203
+
204
+ const response = await axios.patch(`${NOTION_API_BASE}/blocks/${pageId}/children`, {
205
+ children: buildChildren(input)
206
+ }, {
207
+ headers: notionHeaders(config)
208
+ });
209
+
210
+ const count = response.data.results ? response.data.results.length : buildChildren(input).length;
211
+ return `Appended ${count} block(s) to Notion page.`;
212
+ }
213
+
214
+ function helpText() {
215
+ return [
216
+ 'Notion plugin commands:',
217
+ '- Create page: {"action":"create_page","title":"Note title","content":"Body text"}',
218
+ '- Query database: {"action":"query_database","limit":5}',
219
+ '- Append to page: {"action":"append_block","pageId":"...","content":"Text"}',
220
+ 'Plain text creates a Notion page using the configured default database or page.'
221
+ ].join('\n');
222
+ }
223
+
224
+ module.exports = {
225
+ name: 'notion',
226
+ description: 'Manage Notion. Target can be JSON: {"action":"create_page","title":"Note","content":"Body","databaseId":"optional","pageId":"optional"}, {"action":"query_database","databaseId":"optional","limit":10}, or {"action":"append_block","pageId":"optional","content":"Text"}. Plain text creates a note.',
227
+
228
+ async execute(instruction) {
229
+ const config = readConfig();
230
+ const input = parseInstruction(instruction);
231
+
232
+ if (input.action === 'help') return helpText();
233
+ if (!hasNotionConfig(config)) {
234
+ return 'Notion API is not configured. Add a Notion internal integration secret with `mint onboard`, then share your Notion page/database with that integration.';
235
+ }
236
+
237
+ switch (input.action) {
238
+ case 'create_page':
239
+ return await createPage(config, input);
240
+ case 'query_database':
241
+ return await queryDatabase(config, input);
242
+ case 'append_block':
243
+ return await appendBlock(config, input);
244
+ default:
245
+ throw new Error(`Unsupported Notion action: ${input.action}`);
246
+ }
247
+ },
248
+
249
+ _helpers: {
250
+ parseInstruction,
251
+ buildChildren,
252
+ buildDatabaseProperties,
253
+ formatNotionTitle,
254
+ hasNotionConfig
255
+ }
256
+ };
@@ -0,0 +1,129 @@
1
+ const { clipboard: electronClipboard } = require('electron');
2
+ const { openApp } = require('../Automation_Layer/open_app');
3
+ const { openWebsite, openSearch } = require('../Automation_Layer/open_website');
4
+ const { performWebAutomation } = require('../Automation_Layer/browser_automation');
5
+ const { createFolder, openFile, deleteFile, findPath } = require('../Automation_Layer/file_operations');
6
+ const { indexFile, indexFolder } = require('../AI_Brain/knowledge_base');
7
+ const pluginManager = require('../Plugins/plugin_manager');
8
+ const mcpManager = require('../Plugins/mcp_manager');
9
+ const granularAutomation = require('./granular_automation');
10
+ const SystemAutomation = require('./system_automation');
11
+ const safetyManager = require('./safety_manager');
12
+
13
+ async function executeAction(action, options = {}) {
14
+ console.log("Executing action:", action);
15
+ const clipboard = options.clipboard || electronClipboard;
16
+ const safety = safetyManager.assertActionAllowed(action, {
17
+ allowDangerous: options.allowDangerous === true
18
+ });
19
+ safetyManager.appendActionLog({
20
+ source: options.source || 'action_executor',
21
+ action: action.type,
22
+ target: action.target || action.path || '',
23
+ tier: safety.tier,
24
+ approved: options.allowDangerous === true || safety.tier !== safetyManager.TIERS.DANGEROUS
25
+ });
26
+
27
+ switch (action.type) {
28
+ case 'open_url':
29
+ openWebsite(action.target);
30
+ break;
31
+ case 'search':
32
+ openSearch(action.target);
33
+ break;
34
+ case 'open_app':
35
+ openApp(action.target);
36
+ break;
37
+ case 'web_automation':
38
+ return await performWebAutomation(action.target);
39
+ case 'create_folder':
40
+ createFolder(action.target);
41
+ break;
42
+ case 'open_file': {
43
+ const fileRes = await openFile(action.target);
44
+ return fileRes || `Successfully opened file: ${action.target} ✅`;
45
+ }
46
+ case 'open_folder': {
47
+ const folderRes = await openFile(action.target);
48
+ return folderRes || `Successfully opened folder: ${action.target} ✅`;
49
+ }
50
+ case 'delete_file':
51
+ await deleteFile(action.target);
52
+ break;
53
+ case 'find_path':
54
+ return await executeFindPath(action);
55
+ case 'clipboard_write':
56
+ clipboard.writeText(action.target);
57
+ break;
58
+ case 'learn_file':
59
+ return await indexFile(action.target);
60
+ case 'learn_folder':
61
+ return await indexFolder(action.target);
62
+ case 'mcp_tool': {
63
+ const mcpResult = await mcpManager.callTool(action.server, action.target, action.args);
64
+ return JSON.stringify(mcpResult.content);
65
+ }
66
+ case 'mouse_move':
67
+ return await granularAutomation.mouseMove(action.x, action.y);
68
+ case 'mouse_click':
69
+ return await granularAutomation.mouseClick(action.x, action.y, action.button || 1);
70
+ case 'type_text':
71
+ return await granularAutomation.typeText(action.target);
72
+ case 'key_tap':
73
+ return await granularAutomation.keyTap(action.target);
74
+ case 'plugin':
75
+ return await pluginManager.executePlugin(action.pluginName, action.target);
76
+ case 'system_automation':
77
+ return await handleSystemAutomation(action.target);
78
+ default:
79
+ return undefined;
80
+ }
81
+ }
82
+
83
+ async function executeFindPath(action) {
84
+ const result = findPath(action.target, {
85
+ type: action.pathType,
86
+ maxResults: 10
87
+ });
88
+ if (!result.success) {
89
+ return result.message;
90
+ }
91
+
92
+ if (action.openAfter === true) {
93
+ if (result.matches.length === 1) {
94
+ const match = result.matches[0];
95
+ const openResult = await openFile(match.path);
96
+ return openResult || `Successfully found and opened ${match.type === 'dir' ? 'folder' : 'file'}: ${match.path} ✅`;
97
+ }
98
+ return `Found multiple matches for "${action.target}". Please be more specific:\n${result.matches.map(m => `- [${m.type}] ${m.path}`).join('\n')}`;
99
+ }
100
+
101
+ return `Found matches for "${action.target}":\n${result.matches.map(m => `- [${m.type}] ${m.path}`).join('\n')}`;
102
+ }
103
+
104
+ async function handleSystemAutomation(target) {
105
+ const [cmd, value] = target.split(':');
106
+ switch (cmd) {
107
+ case 'volume':
108
+ return await SystemAutomation.setVolume(parseInt(value));
109
+ case 'mute':
110
+ return await SystemAutomation.mute();
111
+ case 'brightness':
112
+ return await SystemAutomation.setBrightness(parseInt(value));
113
+ case 'sleep':
114
+ return await SystemAutomation.sleep();
115
+ case 'restart':
116
+ return await SystemAutomation.restart();
117
+ case 'shutdown':
118
+ return await SystemAutomation.shutdown();
119
+ case 'minimize_all':
120
+ return await SystemAutomation.minimizeAll();
121
+ default:
122
+ if (SystemAutomation[target]) {
123
+ return await SystemAutomation[target]();
124
+ }
125
+ throw new Error(`Unknown system automation command: ${target}`);
126
+ }
127
+ }
128
+
129
+ module.exports = { executeAction, handleSystemAutomation };