@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.
- package/GUIDE_TH.md +113 -0
- package/README.md +214 -142
- package/assets/CLI_Screen.png +0 -0
- package/docs/assets/CLI_Screen.png +0 -0
- package/docs/guide.html +632 -0
- package/docs/index.html +5 -4
- package/main.js +66 -894
- package/mint-cli-logic.js +15 -8
- package/mint-cli.js +305 -195
- package/package.json +12 -4
- package/src/AI_Brain/Gemini_API.js +77 -20
- package/src/AI_Brain/agent_orchestrator.js +6 -6
- package/src/AI_Brain/autonomous_brain.js +10 -0
- package/src/AI_Brain/behavior_memory.js +26 -5
- package/src/AI_Brain/headless_agent.js +4 -0
- package/src/AI_Brain/knowledge_base.js +61 -8
- package/src/AI_Brain/memory_store.js +55 -7
- package/src/Automation_Layer/file_operations.js +14 -3
- package/src/CLI/chat_router.js +21 -7
- package/src/CLI/chat_ui.js +264 -710
- package/src/CLI/code_agent.js +370 -124
- package/src/CLI/gmail_auth.js +210 -0
- package/src/CLI/list_features.js +5 -1
- package/src/CLI/onboarding.js +307 -55
- package/src/CLI/updater.js +208 -0
- package/src/Channels/brave_search_bridge.js +35 -0
- package/src/Channels/discord_bridge.js +68 -0
- package/src/Channels/google_search_bridge.js +38 -0
- package/src/Channels/line_bridge.js +60 -0
- package/src/Channels/slack_bridge.js +53 -0
- package/src/Channels/telegram_bridge.js +49 -0
- package/src/Channels/whatsapp_bridge.js +55 -0
- package/src/Command_Parser/parser.js +12 -1
- package/src/Plugins/gmail.js +251 -0
- package/src/Plugins/google_calendar.js +245 -19
- package/src/Plugins/notion.js +256 -0
- package/src/System/action_executor.js +129 -0
- package/src/System/bridge_manager.js +76 -0
- package/src/System/chat_history_manager.js +23 -5
- package/src/System/config_manager.js +41 -7
- package/src/System/custom_workflows.js +31 -2
- package/src/System/google_tts_urls.js +51 -0
- package/src/System/ipc_handlers.js +238 -0
- package/src/System/proactive_loop.js +137 -0
- package/src/System/safety_manager.js +165 -0
- package/src/System/screen_capture.js +175 -0
- package/src/System/task_manager.js +15 -5
- package/src/System/window_manager.js +210 -0
- package/src/UI/renderer.js +33 -7
- package/src/UI/settings.html +24 -0
- package/src/UI/settings.js +14 -4
- package/src/UI/styles.css +14 -1
- package/tests/action_executor_safety.test.js +67 -0
- package/tests/gmail.test.js +135 -0
- package/tests/gmail_auth.test.js +129 -0
- package/tests/google_calendar.test.js +113 -0
- package/tests/google_tts_urls.test.js +24 -0
- package/tests/notion.test.js +121 -0
- package/tests/provider_routing.test.js +17 -1
- package/tests/safety_manager.test.js +40 -0
- package/tests/updater.test.js +32 -0
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
function registerIpcHandlers({
|
|
2
|
+
app,
|
|
3
|
+
ipcMain,
|
|
4
|
+
shell,
|
|
5
|
+
clipboard,
|
|
6
|
+
windowManager,
|
|
7
|
+
proactiveLoop,
|
|
8
|
+
screenCapture,
|
|
9
|
+
services
|
|
10
|
+
}) {
|
|
11
|
+
const {
|
|
12
|
+
handleChat,
|
|
13
|
+
resetChat,
|
|
14
|
+
getChatTranscript,
|
|
15
|
+
refreshApiKeyFromConfig,
|
|
16
|
+
getSystemInfo,
|
|
17
|
+
getWeather,
|
|
18
|
+
readConfig,
|
|
19
|
+
writeConfig,
|
|
20
|
+
parseCommand,
|
|
21
|
+
executeAction,
|
|
22
|
+
getGoogleTtsUrls,
|
|
23
|
+
customWorkflows
|
|
24
|
+
} = services;
|
|
25
|
+
|
|
26
|
+
ipcMain.handle('chat-message', async (event, message, base64Image = null, base64Audio = null) => {
|
|
27
|
+
try {
|
|
28
|
+
const rawResponse = await handleChat(message, base64Image, base64Audio);
|
|
29
|
+
const aiResponse = parseCommand(rawResponse);
|
|
30
|
+
|
|
31
|
+
if (aiResponse.action && aiResponse.action.type !== 'none') {
|
|
32
|
+
try {
|
|
33
|
+
const actionResult = await executeAction(aiResponse.action, { clipboard });
|
|
34
|
+
if (actionResult && typeof actionResult === 'string') {
|
|
35
|
+
aiResponse.response += `\n\n${actionResult}`;
|
|
36
|
+
}
|
|
37
|
+
} catch (err) {
|
|
38
|
+
console.error("Action execution error:", err);
|
|
39
|
+
aiResponse.response += "\n\n(Note: I tried to execute the action, but an error occurred.)";
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return aiResponse;
|
|
44
|
+
} catch (error) {
|
|
45
|
+
console.error('Chat error:', error);
|
|
46
|
+
return { response: 'Error communicating with Gemini API. Check your console and API key.', action: { type: 'none' } };
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
ipcMain.on('close-window', () => {
|
|
51
|
+
const mainWindow = windowManager.getMainWindow();
|
|
52
|
+
if (mainWindow) mainWindow.hide();
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
ipcMain.on('minimize-window', () => {
|
|
56
|
+
const mainWindow = windowManager.getMainWindow();
|
|
57
|
+
if (mainWindow) mainWindow.minimize();
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
ipcMain.on('quit-app', () => {
|
|
61
|
+
app.isQuiting = true;
|
|
62
|
+
app.quit();
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
ipcMain.on('maximize-window', () => {
|
|
66
|
+
const mainWindow = windowManager.getMainWindow();
|
|
67
|
+
if (!mainWindow) return;
|
|
68
|
+
if (mainWindow.isMaximized()) {
|
|
69
|
+
mainWindow.unmaximize();
|
|
70
|
+
} else {
|
|
71
|
+
mainWindow.maximize();
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
ipcMain.handle('reset-chat', () => {
|
|
76
|
+
resetChat();
|
|
77
|
+
return { success: true };
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
ipcMain.handle('get-chat-history', () => getChatTranscript());
|
|
81
|
+
|
|
82
|
+
ipcMain.handle('open-settings', () => {
|
|
83
|
+
windowManager.createSettingsWindow();
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
ipcMain.handle('get-settings', () => readConfig());
|
|
87
|
+
|
|
88
|
+
ipcMain.handle('save-settings', (event, config) => {
|
|
89
|
+
console.log('[Settings] Saving new config. MCP Servers count:', Object.keys(config.mcpServers || {}).length);
|
|
90
|
+
const result = writeConfig(config);
|
|
91
|
+
refreshApiKeyFromConfig();
|
|
92
|
+
|
|
93
|
+
const mainWindow = windowManager.getMainWindow();
|
|
94
|
+
if (mainWindow && !mainWindow.isDestroyed()) {
|
|
95
|
+
mainWindow.webContents.send('settings-changed', config);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (proactiveLoop.isRunning()) {
|
|
99
|
+
proactiveLoop.start(config.proactiveInterval);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (config.enableCustomWorkflows !== false) {
|
|
103
|
+
customWorkflows.startMonitoring(mainWindow.webContents);
|
|
104
|
+
} else {
|
|
105
|
+
customWorkflows.stopMonitoring();
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (config.showDesktopWidget === false) {
|
|
109
|
+
windowManager.closeWidgetWindow();
|
|
110
|
+
} else {
|
|
111
|
+
windowManager.ensureWidgetWindow();
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return result;
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
ipcMain.on('set-ai-state', (event, state) => {
|
|
118
|
+
const widgetWindow = windowManager.getWidgetWindow();
|
|
119
|
+
if (widgetWindow && !widgetWindow.isDestroyed()) {
|
|
120
|
+
widgetWindow.webContents.send('widget-state', state);
|
|
121
|
+
}
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
ipcMain.on('close-settings', () => {
|
|
125
|
+
const settingsWindow = windowManager.getSettingsWindow();
|
|
126
|
+
if (settingsWindow) settingsWindow.close();
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
ipcMain.handle('open-custom-workflows', () => {
|
|
130
|
+
customWorkflows.openConfigFile();
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
ipcMain.handle('reload-custom-workflows', () => {
|
|
134
|
+
customWorkflows.loadWorkflows();
|
|
135
|
+
return { success: true };
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
ipcMain.on('spotlight-close', () => {
|
|
139
|
+
const spotlightWindow = windowManager.getSpotlightWindow();
|
|
140
|
+
if (spotlightWindow) spotlightWindow.close();
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
ipcMain.on('spotlight-hide', () => {
|
|
144
|
+
const spotlightWindow = windowManager.getSpotlightWindow();
|
|
145
|
+
if (spotlightWindow) spotlightWindow.hide();
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
ipcMain.on('spotlight-submit', async (event, query) => {
|
|
149
|
+
console.log('[Spotlight] Submit:', query);
|
|
150
|
+
const spotlightWindow = windowManager.getSpotlightWindow();
|
|
151
|
+
if (spotlightWindow) spotlightWindow.hide();
|
|
152
|
+
|
|
153
|
+
const mainWindow = windowManager.getMainWindow();
|
|
154
|
+
if (mainWindow) {
|
|
155
|
+
mainWindow.show();
|
|
156
|
+
mainWindow.webContents.send('spotlight-to-chat', query);
|
|
157
|
+
}
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
ipcMain.on('spotlight-resize', (event, width, height) => {
|
|
161
|
+
const spotlightWindow = windowManager.getSpotlightWindow();
|
|
162
|
+
if (spotlightWindow) spotlightWindow.setSize(width, height);
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
ipcMain.handle('open-external', (event, url) => {
|
|
166
|
+
shell.openExternal(url);
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
ipcMain.handle('clipboard-read', () => clipboard.readText());
|
|
170
|
+
|
|
171
|
+
ipcMain.handle('clipboard-write', (event, text) => {
|
|
172
|
+
clipboard.writeText(text);
|
|
173
|
+
return { success: true };
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
ipcMain.handle('get-tts-urls', async (event, text) => {
|
|
177
|
+
try {
|
|
178
|
+
const isThai = /[\u0E00-\u0E7F]/.test(text);
|
|
179
|
+
return getGoogleTtsUrls(text, {
|
|
180
|
+
lang: isThai ? 'th' : 'en',
|
|
181
|
+
host: 'https://translate.google.com',
|
|
182
|
+
});
|
|
183
|
+
} catch (e) {
|
|
184
|
+
console.error("TTS Error:", e);
|
|
185
|
+
return [];
|
|
186
|
+
}
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
ipcMain.handle('get-system-info', async () => getSystemInfo());
|
|
190
|
+
ipcMain.handle('get-weather', async (event, city) => getWeather(city));
|
|
191
|
+
|
|
192
|
+
ipcMain.handle('start-screen-capture', () => screenCapture.startScreenCapture());
|
|
193
|
+
ipcMain.on('vision-selection', (event, base64Image) => screenCapture.handleSelection(base64Image));
|
|
194
|
+
ipcMain.on('vision-translate-start', (event, rect) => screenCapture.startLiveTranslate(rect));
|
|
195
|
+
ipcMain.on('vision-translate-stop', () => screenCapture.stopLiveTranslate());
|
|
196
|
+
ipcMain.on('vision-overlay-interactable', (event, isInteractable) => screenCapture.setOverlayInteractable(isInteractable));
|
|
197
|
+
ipcMain.on('vision-cancel', () => screenCapture.cancel());
|
|
198
|
+
ipcMain.handle('capture-silent-screen', () => screenCapture.captureSilentScreen());
|
|
199
|
+
|
|
200
|
+
ipcMain.on('toggle-proactive', (event, isOn) => {
|
|
201
|
+
if (isOn) {
|
|
202
|
+
proactiveLoop.start();
|
|
203
|
+
} else {
|
|
204
|
+
proactiveLoop.stop();
|
|
205
|
+
}
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
ipcMain.on('record-behavior', (event, contextDescription) => {
|
|
209
|
+
proactiveLoop.recordBehavior(contextDescription);
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
ipcMain.handle('execute-proactive-action', async (event, action) => {
|
|
213
|
+
if (!action || action.type === 'none') {
|
|
214
|
+
return { success: false, message: 'ไม่มี action ที่จะดำเนินการค่ะ' };
|
|
215
|
+
}
|
|
216
|
+
try {
|
|
217
|
+
const result = await executeAction(action, { clipboard });
|
|
218
|
+
const messages = {
|
|
219
|
+
open_url: `เปิดเว็บไซต์ให้แล้วค่ะ 🌐`,
|
|
220
|
+
open_app: `เปิดแอป ${action.target} ให้แล้วค่ะ 🚀`,
|
|
221
|
+
search: `ค้นหา "${action.target}" ให้แล้วค่ะ 🔍`,
|
|
222
|
+
web_automation: result || 'ดำเนินการเสร็จแล้วค่ะ ✅',
|
|
223
|
+
create_folder: `สร้างโฟลเดอร์ "${action.target}" แล้วค่ะ 📁`,
|
|
224
|
+
clipboard_write: `คัดลอกข้อความแล้วค่ะ 📋`,
|
|
225
|
+
learn_file: result || `เรียนรู้เอกสารเรียบร้อยค่ะ 📚`,
|
|
226
|
+
};
|
|
227
|
+
return {
|
|
228
|
+
success: true,
|
|
229
|
+
message: messages[action.type] || 'ดำเนินการเสร็จแล้วค่ะ ✅'
|
|
230
|
+
};
|
|
231
|
+
} catch (err) {
|
|
232
|
+
console.error('[ProactiveAction] Error:', err);
|
|
233
|
+
return { success: false, message: `เกิดข้อผิดพลาด: ${err.message}` };
|
|
234
|
+
}
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
module.exports = { registerIpcHandlers };
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
const { BrowserWindow, desktopCapturer, screen, powerMonitor } = require('electron');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const { analyzeAndSuggest } = require('../AI_Brain/proactive_engine');
|
|
4
|
+
const { recordBehavior, getBehaviorSummary } = require('../AI_Brain/behavior_memory');
|
|
5
|
+
|
|
6
|
+
const IDLE_THRESHOLD_SEC = 300;
|
|
7
|
+
|
|
8
|
+
function createProactiveLoop({ app, projectRoot, readConfig, getMainWindow }) {
|
|
9
|
+
let proactiveGlowWindow = null;
|
|
10
|
+
let proactiveIntervalHandle = null;
|
|
11
|
+
let idleWatcherHandle = null;
|
|
12
|
+
|
|
13
|
+
async function runProactiveCycle() {
|
|
14
|
+
const mainWindow = getMainWindow();
|
|
15
|
+
if (!mainWindow || mainWindow.isDestroyed()) return;
|
|
16
|
+
|
|
17
|
+
try {
|
|
18
|
+
showProactiveGlow();
|
|
19
|
+
|
|
20
|
+
const primaryDisplay = screen.getPrimaryDisplay();
|
|
21
|
+
const width = Math.floor(primaryDisplay.size.width * 0.5);
|
|
22
|
+
const height = Math.floor(primaryDisplay.size.height * 0.5);
|
|
23
|
+
const sources = await desktopCapturer.getSources({
|
|
24
|
+
types: ['screen'],
|
|
25
|
+
thumbnailSize: { width, height }
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
const primarySource = sources[0];
|
|
29
|
+
if (!primarySource || !primarySource.thumbnail) return;
|
|
30
|
+
|
|
31
|
+
const base64Image = primarySource.thumbnail.toJPEG(60).toString('base64');
|
|
32
|
+
const result = await analyzeAndSuggest(base64Image, getBehaviorSummary());
|
|
33
|
+
|
|
34
|
+
if (result && result.message && Array.isArray(result.suggestions)) {
|
|
35
|
+
if (result.context) recordBehavior(result.context);
|
|
36
|
+
|
|
37
|
+
const currentMainWindow = getMainWindow();
|
|
38
|
+
if (currentMainWindow && !currentMainWindow.isDestroyed()) {
|
|
39
|
+
currentMainWindow.webContents.send('proactive-suggestion', result);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
hideProactiveGlow();
|
|
44
|
+
} catch (err) {
|
|
45
|
+
console.error('[Proactive] Cycle error:', err.message);
|
|
46
|
+
hideProactiveGlow();
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function createProactiveGlowWindow() {
|
|
51
|
+
if (proactiveGlowWindow) return proactiveGlowWindow;
|
|
52
|
+
const { width, height } = screen.getPrimaryDisplay().bounds;
|
|
53
|
+
|
|
54
|
+
proactiveGlowWindow = new BrowserWindow({
|
|
55
|
+
width,
|
|
56
|
+
height,
|
|
57
|
+
x: 0,
|
|
58
|
+
y: 0,
|
|
59
|
+
frame: false,
|
|
60
|
+
transparent: true,
|
|
61
|
+
alwaysOnTop: true,
|
|
62
|
+
skipTaskbar: true,
|
|
63
|
+
focusable: false,
|
|
64
|
+
show: false,
|
|
65
|
+
webPreferences: {
|
|
66
|
+
nodeIntegration: false,
|
|
67
|
+
contextIsolation: true
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
proactiveGlowWindow.setIgnoreMouseEvents(true);
|
|
72
|
+
proactiveGlowWindow.setVisibleOnAllWorkspaces(true, { visibleOnFullScreen: true });
|
|
73
|
+
proactiveGlowWindow.setAlwaysOnTop(true, 'screen-saver');
|
|
74
|
+
proactiveGlowWindow.loadFile(path.join(projectRoot, 'src/UI/proactive-glow.html'));
|
|
75
|
+
proactiveGlowWindow.on('closed', () => { proactiveGlowWindow = null; });
|
|
76
|
+
return proactiveGlowWindow;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function showProactiveGlow() {
|
|
80
|
+
if (!proactiveGlowWindow) createProactiveGlowWindow();
|
|
81
|
+
if (proactiveGlowWindow) proactiveGlowWindow.showInactive();
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function hideProactiveGlow() {
|
|
85
|
+
if (proactiveGlowWindow && !proactiveGlowWindow.isDestroyed()) {
|
|
86
|
+
proactiveGlowWindow.hide();
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function start(intervalSec) {
|
|
91
|
+
stop();
|
|
92
|
+
const cfg = readConfig();
|
|
93
|
+
const ms = (intervalSec || cfg.proactiveInterval || 60) * 1000;
|
|
94
|
+
console.log(`[Proactive] Starting loop — interval: ${ms / 1000}s`);
|
|
95
|
+
proactiveIntervalHandle = setInterval(runProactiveCycle, ms);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function stop() {
|
|
99
|
+
if (proactiveIntervalHandle) {
|
|
100
|
+
clearInterval(proactiveIntervalHandle);
|
|
101
|
+
proactiveIntervalHandle = null;
|
|
102
|
+
console.log('[Proactive] Stopped proactive loop.');
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function startIdleWatcher() {
|
|
107
|
+
if (idleWatcherHandle) return;
|
|
108
|
+
idleWatcherHandle = setInterval(() => {
|
|
109
|
+
if (!proactiveIntervalHandle) return;
|
|
110
|
+
if (!app.isReady()) return;
|
|
111
|
+
|
|
112
|
+
const idleSec = powerMonitor.getSystemIdleTime();
|
|
113
|
+
if (idleSec < IDLE_THRESHOLD_SEC) return;
|
|
114
|
+
|
|
115
|
+
console.log(`[System Idle] User idle for ${idleSec}s. Pausing Proactive loop to save resources.`);
|
|
116
|
+
stop();
|
|
117
|
+
|
|
118
|
+
const resumeChecker = setInterval(() => {
|
|
119
|
+
if (powerMonitor.getSystemIdleTime() < 10) {
|
|
120
|
+
console.log('[System Idle] User returned. Resuming Proactive loop.');
|
|
121
|
+
clearInterval(resumeChecker);
|
|
122
|
+
start();
|
|
123
|
+
}
|
|
124
|
+
}, 5000);
|
|
125
|
+
}, 60000);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return {
|
|
129
|
+
start,
|
|
130
|
+
stop,
|
|
131
|
+
startIdleWatcher,
|
|
132
|
+
isRunning: () => Boolean(proactiveIntervalHandle),
|
|
133
|
+
recordBehavior
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
module.exports = { createProactiveLoop };
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const os = require('os');
|
|
3
|
+
const path = require('path');
|
|
4
|
+
|
|
5
|
+
const TIERS = Object.freeze({
|
|
6
|
+
SAFE: 'safe',
|
|
7
|
+
APPROVAL: 'approval',
|
|
8
|
+
DANGEROUS: 'dangerous',
|
|
9
|
+
BLOCKED: 'blocked'
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
const BLOCKED_COMMAND_PATTERNS = [
|
|
13
|
+
{ pattern: /\brm\s+(-[^\s]*r[^\s]*f|-rf|-fr)\b/, reason: 'recursive force delete' },
|
|
14
|
+
{ pattern: /\bgit\s+reset\s+--hard\b/, reason: 'destructive git reset' },
|
|
15
|
+
{ pattern: /\bgit\s+checkout\s+--\b/, reason: 'destructive git checkout path restore' },
|
|
16
|
+
{ pattern: /\bgit\s+clean\b.*\s-[^\s]*f/, reason: 'destructive git clean' },
|
|
17
|
+
{ pattern: /\bmkfs(?:\.\w+)?\b/, reason: 'filesystem formatting' },
|
|
18
|
+
{ pattern: /\bdd\s+.*\bof=\/dev\//, reason: 'raw disk write' },
|
|
19
|
+
{ pattern: />\s*\/dev\/(?:sd|nvme|hd|mapper)/, reason: 'write redirection to block device' },
|
|
20
|
+
{ pattern: /\b(shutdown|reboot|poweroff|halt)\b/, reason: 'system power command' },
|
|
21
|
+
{ pattern: /\bsudo\b/, reason: 'privilege escalation' },
|
|
22
|
+
{ pattern: /\bchmod\s+-R\s+777\b/, reason: 'unsafe recursive permissions' },
|
|
23
|
+
{ pattern: /\bchown\s+-R\b/, reason: 'unsafe recursive ownership change' },
|
|
24
|
+
{ pattern: /\bcurl\b.*\|\s*(sh|bash|zsh)\b/, reason: 'remote script piping' },
|
|
25
|
+
{ pattern: /\bwget\b.*\|\s*(sh|bash|zsh)\b/, reason: 'remote script piping' }
|
|
26
|
+
];
|
|
27
|
+
|
|
28
|
+
const DANGEROUS_ACTIONS = new Set([
|
|
29
|
+
'delete_file',
|
|
30
|
+
'system_automation'
|
|
31
|
+
]);
|
|
32
|
+
|
|
33
|
+
const SAFE_ACTIONS = new Set([
|
|
34
|
+
'open_url',
|
|
35
|
+
'search',
|
|
36
|
+
'open_app',
|
|
37
|
+
'open_file',
|
|
38
|
+
'open_folder',
|
|
39
|
+
'find_path',
|
|
40
|
+
'clipboard_write',
|
|
41
|
+
'learn_file',
|
|
42
|
+
'learn_folder',
|
|
43
|
+
'mcp_tool',
|
|
44
|
+
'mouse_move',
|
|
45
|
+
'mouse_click',
|
|
46
|
+
'type_text',
|
|
47
|
+
'key_tap',
|
|
48
|
+
'plugin',
|
|
49
|
+
'web_automation',
|
|
50
|
+
'create_folder'
|
|
51
|
+
]);
|
|
52
|
+
|
|
53
|
+
const DANGEROUS_SYSTEM_COMMANDS = new Set(['shutdown', 'restart', 'reboot', 'poweroff', 'sleep']);
|
|
54
|
+
|
|
55
|
+
function normalizeCommand(command) {
|
|
56
|
+
return String(command || '').replace(/\s+/g, ' ').trim();
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function classifyShellCommand(command) {
|
|
60
|
+
const normalized = normalizeCommand(command);
|
|
61
|
+
if (!normalized) {
|
|
62
|
+
return { tier: TIERS.BLOCKED, reason: 'empty shell command' };
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
for (const rule of BLOCKED_COMMAND_PATTERNS) {
|
|
66
|
+
if (rule.pattern.test(normalized)) {
|
|
67
|
+
return { tier: TIERS.BLOCKED, reason: rule.reason };
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return { tier: TIERS.APPROVAL, reason: 'shell command requires approval' };
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function assertShellCommandAllowed(command) {
|
|
75
|
+
const result = classifyShellCommand(command);
|
|
76
|
+
if (result.tier === TIERS.BLOCKED) {
|
|
77
|
+
throw new Error(`Blocked unsafe command (${result.reason}): ${command}`);
|
|
78
|
+
}
|
|
79
|
+
return result;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function classifyAction(action = {}) {
|
|
83
|
+
const type = action.type || 'none';
|
|
84
|
+
if (type === 'none') return { tier: TIERS.SAFE, reason: 'no-op action' };
|
|
85
|
+
|
|
86
|
+
if (type === 'system_automation') {
|
|
87
|
+
const command = String(action.target || '').split(':')[0];
|
|
88
|
+
if (DANGEROUS_SYSTEM_COMMANDS.has(command)) {
|
|
89
|
+
return { tier: TIERS.DANGEROUS, reason: `system automation command '${command}'` };
|
|
90
|
+
}
|
|
91
|
+
return { tier: TIERS.APPROVAL, reason: 'system automation requires approval' };
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (DANGEROUS_ACTIONS.has(type)) {
|
|
95
|
+
return { tier: TIERS.DANGEROUS, reason: `${type} can affect user data or system state` };
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (SAFE_ACTIONS.has(type)) {
|
|
99
|
+
return { tier: TIERS.SAFE, reason: 'allowed action' };
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return { tier: TIERS.APPROVAL, reason: 'unknown action requires approval' };
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function assertActionAllowed(action, options = {}) {
|
|
106
|
+
const classification = classifyAction(action);
|
|
107
|
+
const allowDangerous = options.allowDangerous === true;
|
|
108
|
+
|
|
109
|
+
if (classification.tier === TIERS.BLOCKED) {
|
|
110
|
+
throw new Error(`Blocked action (${classification.reason}): ${action.type}`);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if (classification.tier === TIERS.DANGEROUS && !allowDangerous) {
|
|
114
|
+
throw new Error(`Dangerous action requires explicit permission (${classification.reason}): ${action.type}`);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return classification;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function resolveWithinRoot(root, targetPath) {
|
|
121
|
+
if (!root) throw new Error('Root path is required.');
|
|
122
|
+
if (!targetPath) throw new Error('Target path is required.');
|
|
123
|
+
|
|
124
|
+
const resolvedRoot = path.resolve(root);
|
|
125
|
+
const expandedTarget = String(targetPath).startsWith('~/')
|
|
126
|
+
? path.join(os.homedir(), String(targetPath).slice(2))
|
|
127
|
+
: targetPath;
|
|
128
|
+
const resolvedTarget = path.resolve(resolvedRoot, expandedTarget);
|
|
129
|
+
const relative = path.relative(resolvedRoot, resolvedTarget);
|
|
130
|
+
|
|
131
|
+
if (relative.startsWith('..') || path.isAbsolute(relative)) {
|
|
132
|
+
throw new Error(`Path is outside allowed root: ${targetPath}`);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return resolvedTarget;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function appendActionLog(entry, options = {}) {
|
|
139
|
+
const logPath = options.logPath || path.join(os.homedir(), '.config', 'mint', 'action-log.jsonl');
|
|
140
|
+
const payload = {
|
|
141
|
+
time: new Date().toISOString(),
|
|
142
|
+
...entry
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
try {
|
|
146
|
+
fs.mkdirSync(path.dirname(logPath), { recursive: true });
|
|
147
|
+
fs.appendFileSync(logPath, `${JSON.stringify(payload)}\n`, 'utf8');
|
|
148
|
+
} catch (error) {
|
|
149
|
+
if (process.env.MINT_DEBUG === '1') {
|
|
150
|
+
console.error('[Safety] Failed to append action log:', error.message);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
return payload;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
module.exports = {
|
|
158
|
+
TIERS,
|
|
159
|
+
classifyShellCommand,
|
|
160
|
+
assertShellCommandAllowed,
|
|
161
|
+
classifyAction,
|
|
162
|
+
assertActionAllowed,
|
|
163
|
+
resolveWithinRoot,
|
|
164
|
+
appendActionLog
|
|
165
|
+
};
|