@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.
- package/GUIDE_TH.md +113 -0
- package/README.md +239 -76
- 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 +13 -1
- package/mint-cli.js +100 -9
- package/package.json +12 -4
- package/src/AI_Brain/Gemini_API.js +77 -20
- 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 +1 -1
- package/src/CLI/chat_router.js +3 -2
- package/src/CLI/chat_ui.js +263 -838
- package/src/CLI/code_agent.js +144 -42
- package/src/CLI/gmail_auth.js +210 -0
- package/src/CLI/list_features.js +2 -0
- 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,76 @@
|
|
|
1
|
+
const { readConfig } = require('./config_manager');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
|
|
5
|
+
class BridgeManager {
|
|
6
|
+
constructor() {
|
|
7
|
+
this.bridges = new Map();
|
|
8
|
+
this.channelsDir = path.join(__dirname, '..', 'Channels');
|
|
9
|
+
|
|
10
|
+
if (!fs.existsSync(this.channelsDir)) {
|
|
11
|
+
fs.mkdirSync(this.channelsDir, { recursive: true });
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
async init() {
|
|
16
|
+
const config = readConfig();
|
|
17
|
+
console.log('[BridgeManager] Initializing messaging bridges...');
|
|
18
|
+
|
|
19
|
+
// Load Discord Bridge
|
|
20
|
+
if (config.enableDiscordBridge && config.discordBotToken) {
|
|
21
|
+
await this.startBridge('discord', config.discordBotToken);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Load Telegram Bridge
|
|
25
|
+
if (config.enableTelegramBridge && config.telegramBotToken) {
|
|
26
|
+
await this.startBridge('telegram', config.telegramBotToken);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Load Slack Bridge
|
|
30
|
+
if (config.enableSlackBridge && config.slackBotToken && config.slackAppToken) {
|
|
31
|
+
await this.startBridge('slack', { botToken: config.slackBotToken, appToken: config.slackAppToken });
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Load LINE Bridge
|
|
35
|
+
if (config.enableLineBridge && config.lineChannelAccessToken && config.lineChannelSecret) {
|
|
36
|
+
await this.startBridge('line', { accessToken: config.lineChannelAccessToken, secret: config.lineChannelSecret, port: config.lineWebhookPort });
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Load WhatsApp Bridge
|
|
40
|
+
if (config.enableWhatsappBridge) {
|
|
41
|
+
await this.startBridge('whatsapp', null);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
async startBridge(type, credentials) {
|
|
46
|
+
try {
|
|
47
|
+
const bridgePath = path.join(this.channelsDir, `${type}_bridge.js`);
|
|
48
|
+
if (!fs.existsSync(bridgePath)) {
|
|
49
|
+
console.error(`[BridgeManager] Bridge file not found: ${bridgePath}`);
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const BridgeClass = require(bridgePath);
|
|
54
|
+
const bridge = new BridgeClass(credentials);
|
|
55
|
+
await bridge.connect();
|
|
56
|
+
this.bridges.set(type, bridge);
|
|
57
|
+
console.log(`[BridgeManager] ${type.toUpperCase()} bridge connected successfully.`);
|
|
58
|
+
} catch (err) {
|
|
59
|
+
console.error(`[BridgeManager] Failed to start ${type} bridge:`, err.message);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
async shutdown() {
|
|
64
|
+
for (const [type, bridge] of this.bridges.entries()) {
|
|
65
|
+
try {
|
|
66
|
+
await bridge.disconnect();
|
|
67
|
+
console.log(`[BridgeManager] ${type.toUpperCase()} bridge disconnected.`);
|
|
68
|
+
} catch (err) {
|
|
69
|
+
console.error(`[BridgeManager] Error disconnecting ${type} bridge:`, err.message);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
this.bridges.clear();
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
module.exports = new BridgeManager();
|
|
@@ -10,14 +10,32 @@ try {
|
|
|
10
10
|
app = null;
|
|
11
11
|
}
|
|
12
12
|
|
|
13
|
+
const CONFIG_DIR = path.join(os.homedir(), '.config', 'mint');
|
|
13
14
|
const MINT_DIR = path.join(os.homedir(), '.mint');
|
|
14
|
-
|
|
15
|
-
|
|
15
|
+
|
|
16
|
+
if (!fs.existsSync(CONFIG_DIR)) {
|
|
17
|
+
fs.mkdirSync(CONFIG_DIR, { recursive: true });
|
|
16
18
|
}
|
|
17
19
|
|
|
18
|
-
const CHAT_HISTORY_PATH =
|
|
19
|
-
|
|
20
|
-
|
|
20
|
+
const CHAT_HISTORY_PATH = path.join(CONFIG_DIR, 'mint-chat-history.json');
|
|
21
|
+
|
|
22
|
+
// Migration Logic: Consolidate from Electron userData or old ~/.mint to ~/.config/mint
|
|
23
|
+
if (!fs.existsSync(CHAT_HISTORY_PATH)) {
|
|
24
|
+
const electronUserData = app && app.getPath ? path.join(app.getPath('userData'), 'mint-chat-history.json') : null;
|
|
25
|
+
const legacyPath = path.join(MINT_DIR, 'mint-chat-history.json');
|
|
26
|
+
|
|
27
|
+
if (electronUserData && fs.existsSync(electronUserData)) {
|
|
28
|
+
try {
|
|
29
|
+
fs.copyFileSync(electronUserData, CHAT_HISTORY_PATH);
|
|
30
|
+
console.log('[History] Migrated chat history from Electron userData');
|
|
31
|
+
} catch (e) { console.error('[History] Migration from Electron failed:', e); }
|
|
32
|
+
} else if (fs.existsSync(legacyPath)) {
|
|
33
|
+
try {
|
|
34
|
+
fs.copyFileSync(legacyPath, CHAT_HISTORY_PATH);
|
|
35
|
+
console.log('[History] Migrated chat history from ~/.mint');
|
|
36
|
+
} catch (e) { console.error('[History] Migration from ~/.mint failed:', e); }
|
|
37
|
+
}
|
|
38
|
+
}
|
|
21
39
|
|
|
22
40
|
function readChatHistory() {
|
|
23
41
|
try {
|
|
@@ -53,21 +53,53 @@ const DEFAULT_CONFIG = {
|
|
|
53
53
|
ttsVolume: 1.0,
|
|
54
54
|
ttsSpeed: 1.0,
|
|
55
55
|
ttsPitch: 1.0,
|
|
56
|
-
pluginSpotifyEnabled: true,
|
|
57
56
|
pluginCalendarEnabled: false,
|
|
57
|
+
pluginGmailEnabled: false,
|
|
58
|
+
pluginNotionEnabled: false,
|
|
58
59
|
pluginDiscordEnabled: false,
|
|
59
60
|
showDesktopWidget: true,
|
|
60
61
|
mcpServers: {},
|
|
62
|
+
telegramBotToken: '',
|
|
63
|
+
enableTelegramBridge: false,
|
|
64
|
+
discordBotToken: '',
|
|
65
|
+
enableDiscordBridge: false,
|
|
66
|
+
slackBotToken: '',
|
|
67
|
+
slackAppToken: '',
|
|
68
|
+
enableSlackBridge: false,
|
|
69
|
+
lineChannelAccessToken: '',
|
|
70
|
+
lineChannelSecret: '',
|
|
71
|
+
enableLineBridge: false,
|
|
72
|
+
lineWebhookPort: 3000,
|
|
73
|
+
enableWhatsappBridge: false,
|
|
74
|
+
googleSearchApiKey: '',
|
|
75
|
+
googleSearchCx: '',
|
|
76
|
+
googleCalendarClientId: '',
|
|
77
|
+
googleCalendarClientSecret: '',
|
|
78
|
+
googleCalendarRefreshToken: '',
|
|
79
|
+
googleCalendarId: 'primary',
|
|
80
|
+
gmailClientId: '',
|
|
81
|
+
gmailClientSecret: '',
|
|
82
|
+
gmailRefreshToken: '',
|
|
83
|
+
gmailUserId: 'me',
|
|
84
|
+
notionApiKey: '',
|
|
85
|
+
notionDatabaseId: '',
|
|
86
|
+
notionPageId: '',
|
|
87
|
+
notionTitleProperty: 'Name',
|
|
88
|
+
braveSearchApiKey: '',
|
|
61
89
|
anthropicApiKey: '',
|
|
90
|
+
|
|
62
91
|
openaiApiKey: '',
|
|
63
92
|
hfApiKey: '',
|
|
64
93
|
anthropicModel: 'claude-3-5-sonnet-latest',
|
|
65
94
|
openaiModel: 'gpt-4o',
|
|
66
95
|
hfModel: 'meta-llama/Meta-Llama-3-8B-Instruct',
|
|
67
|
-
localApiBaseUrl: '
|
|
96
|
+
localApiBaseUrl: '',
|
|
68
97
|
localModelName: 'local-model',
|
|
69
|
-
ollamaHost: '
|
|
70
|
-
enableAgentCollaboration: false
|
|
98
|
+
ollamaHost: '',
|
|
99
|
+
enableAgentCollaboration: false,
|
|
100
|
+
enableAutoUpdate: true,
|
|
101
|
+
autoUpdateCheckIntervalHours: 24,
|
|
102
|
+
lastUpdateCheckAt: ''
|
|
71
103
|
};
|
|
72
104
|
|
|
73
105
|
|
|
@@ -100,8 +132,6 @@ function getAvailableProviders(config) {
|
|
|
100
132
|
const providers = [];
|
|
101
133
|
const cfg = config || readConfig();
|
|
102
134
|
|
|
103
|
-
const isPlaceholder = (val) => !val || val.startsWith('your_') || val.includes('key_here') || val.trim() === '';
|
|
104
|
-
|
|
105
135
|
// Check which providers have API keys or URLs configured
|
|
106
136
|
const anthropicKey = cfg.anthropicApiKey || process.env.ANTHROPIC_API_KEY;
|
|
107
137
|
if (!isPlaceholder(anthropicKey)) providers.push('anthropic');
|
|
@@ -123,4 +153,8 @@ function getAvailableProviders(config) {
|
|
|
123
153
|
return providers;
|
|
124
154
|
}
|
|
125
155
|
|
|
126
|
-
|
|
156
|
+
function isPlaceholder(val) {
|
|
157
|
+
return !val || val.startsWith('your_') || val.includes('key_here') || val.trim() === '';
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
module.exports = { readConfig, writeConfig, getAvailableProviders, isPlaceholder, CONFIG_PATH };
|
|
@@ -1,15 +1,27 @@
|
|
|
1
1
|
const fs = require('fs');
|
|
2
2
|
const path = require('path');
|
|
3
|
-
const
|
|
3
|
+
const os = require('os');
|
|
4
4
|
const { exec } = require('child_process');
|
|
5
5
|
|
|
6
|
+
// Handle electron dependency safely
|
|
7
|
+
let app, shell;
|
|
8
|
+
try {
|
|
9
|
+
const electron = require('electron');
|
|
10
|
+
app = electron.app;
|
|
11
|
+
shell = electron.shell;
|
|
12
|
+
} catch (e) {
|
|
13
|
+
app = null;
|
|
14
|
+
shell = null;
|
|
15
|
+
}
|
|
16
|
+
|
|
6
17
|
function escapeRegExp(text) {
|
|
7
18
|
return String(text).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
8
19
|
}
|
|
9
20
|
|
|
10
21
|
class CustomWorkflows {
|
|
11
22
|
constructor() {
|
|
12
|
-
|
|
23
|
+
const configDir = path.join(os.homedir(), '.config', 'mint');
|
|
24
|
+
this.configPath = path.join(configDir, 'workflows.json');
|
|
13
25
|
this.workflows = [];
|
|
14
26
|
this.lastTriggered = {};
|
|
15
27
|
this.cooldownMs = 60 * 60 * 1000; // 1 hour cooldown per rule
|
|
@@ -17,10 +29,27 @@ class CustomWorkflows {
|
|
|
17
29
|
this.timer = null;
|
|
18
30
|
this.webContents = null;
|
|
19
31
|
|
|
32
|
+
if (!fs.existsSync(configDir)) {
|
|
33
|
+
fs.mkdirSync(configDir, { recursive: true });
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
this.migrateConfig();
|
|
20
37
|
this.ensureConfigExists();
|
|
21
38
|
this.loadWorkflows();
|
|
22
39
|
}
|
|
23
40
|
|
|
41
|
+
migrateConfig() {
|
|
42
|
+
if (!fs.existsSync(this.configPath) && app && app.getPath) {
|
|
43
|
+
const electronPath = path.join(app.getPath('userData'), 'workflows.json');
|
|
44
|
+
if (fs.existsSync(electronPath)) {
|
|
45
|
+
try {
|
|
46
|
+
fs.copyFileSync(electronPath, this.configPath);
|
|
47
|
+
console.log('[CustomWorkflows] Migrated workflows from Electron userData');
|
|
48
|
+
} catch (e) { console.error('[CustomWorkflows] Migration failed:', e); }
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
24
53
|
ensureConfigExists() {
|
|
25
54
|
if (!fs.existsSync(this.configPath)) {
|
|
26
55
|
const defaultWorkflows = [
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
const MAX_GOOGLE_TTS_CHARS = 200;
|
|
2
|
+
|
|
3
|
+
function splitTextForTts(text, maxLength = MAX_GOOGLE_TTS_CHARS) {
|
|
4
|
+
const normalized = String(text || '').replace(/\s+/g, ' ').trim();
|
|
5
|
+
if (!normalized) return [];
|
|
6
|
+
|
|
7
|
+
const chunks = [];
|
|
8
|
+
let remaining = normalized;
|
|
9
|
+
|
|
10
|
+
while (remaining.length > maxLength) {
|
|
11
|
+
const slice = remaining.slice(0, maxLength + 1);
|
|
12
|
+
const splitAt = Math.max(
|
|
13
|
+
slice.lastIndexOf('.'),
|
|
14
|
+
slice.lastIndexOf('?'),
|
|
15
|
+
slice.lastIndexOf('!'),
|
|
16
|
+
slice.lastIndexOf(','),
|
|
17
|
+
slice.lastIndexOf(' ')
|
|
18
|
+
);
|
|
19
|
+
const safeSplit = splitAt > 0 ? splitAt : maxLength;
|
|
20
|
+
chunks.push(remaining.slice(0, safeSplit).trim());
|
|
21
|
+
remaining = remaining.slice(safeSplit).trim();
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
if (remaining) chunks.push(remaining);
|
|
25
|
+
return chunks;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function getGoogleTtsUrls(text, options = {}) {
|
|
29
|
+
const lang = options.lang || 'en';
|
|
30
|
+
const host = options.host || 'https://translate.google.com';
|
|
31
|
+
const chunks = splitTextForTts(text);
|
|
32
|
+
|
|
33
|
+
return chunks.map((chunk, index) => {
|
|
34
|
+
const params = new URLSearchParams({
|
|
35
|
+
ie: 'UTF-8',
|
|
36
|
+
q: chunk,
|
|
37
|
+
tl: lang,
|
|
38
|
+
client: 'tw-ob',
|
|
39
|
+
idx: String(index),
|
|
40
|
+
total: String(chunks.length),
|
|
41
|
+
textlen: String(chunk.length)
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
return {
|
|
45
|
+
shortText: chunk,
|
|
46
|
+
url: `${host}/translate_tts?${params.toString()}`
|
|
47
|
+
};
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
module.exports = { getGoogleTtsUrls, splitTextForTts };
|
|
@@ -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 };
|