@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
package/mint-cli-logic.js
CHANGED
|
@@ -2,11 +2,23 @@
|
|
|
2
2
|
const { openApp } = require('./src/Automation_Layer/open_app');
|
|
3
3
|
const { openWebsite, openSearch } = require('./src/Automation_Layer/open_website');
|
|
4
4
|
const { createFolder, openFile, deleteFile, findPath } = require('./src/Automation_Layer/file_operations');
|
|
5
|
+
const safetyManager = require('./src/System/safety_manager');
|
|
5
6
|
|
|
6
|
-
async function executeAction(action) {
|
|
7
|
+
async function executeAction(action, options = {}) {
|
|
7
8
|
if (!action || action.type === 'none') return null;
|
|
8
9
|
|
|
9
10
|
try {
|
|
11
|
+
const safety = safetyManager.assertActionAllowed(action, {
|
|
12
|
+
allowDangerous: options.allowDangerous === true
|
|
13
|
+
});
|
|
14
|
+
safetyManager.appendActionLog({
|
|
15
|
+
source: options.source || 'mint_cli_logic',
|
|
16
|
+
action: action.type,
|
|
17
|
+
target: action.target || action.path || '',
|
|
18
|
+
tier: safety.tier,
|
|
19
|
+
approved: options.allowDangerous === true || safety.tier !== safetyManager.TIERS.DANGEROUS
|
|
20
|
+
});
|
|
21
|
+
|
|
10
22
|
switch (action.type) {
|
|
11
23
|
case 'open_url':
|
|
12
24
|
openWebsite(action.target);
|
package/mint-cli.js
CHANGED
|
@@ -23,16 +23,27 @@ const { executeCodeTask } = require('./src/CLI/code_agent');
|
|
|
23
23
|
const { detectCodeIntent, runChatRoutedTask } = require('./src/CLI/chat_router');
|
|
24
24
|
const readline = require('readline');
|
|
25
25
|
const { createChatUI } = require('./src/CLI/chat_ui');
|
|
26
|
+
const { runUpdate, runStartupAutoUpdate, shouldRunAutoUpdate } = require('./src/CLI/updater');
|
|
27
|
+
const { runGmailAuth } = require('./src/CLI/gmail_auth');
|
|
26
28
|
|
|
27
29
|
// Startup Info
|
|
28
30
|
const startupConfig = readConfig();
|
|
29
|
-
const
|
|
31
|
+
const startupProvider = startupConfig.aiProvider || 'gemini';
|
|
32
|
+
const startupModel = startupProvider === 'openai'
|
|
33
|
+
? (startupConfig.openaiModel || 'gpt-4o')
|
|
34
|
+
: startupProvider === 'anthropic'
|
|
35
|
+
? (startupConfig.anthropicModel || 'claude-3-5-sonnet-latest')
|
|
36
|
+
: startupProvider === 'local_openai'
|
|
37
|
+
? (startupConfig.localModelName || 'local-model')
|
|
38
|
+
: startupProvider === 'ollama'
|
|
39
|
+
? (startupConfig.ollamaModel || 'llama3:latest')
|
|
40
|
+
: (startupConfig.geminiModel || 'gemini-2.5-flash');
|
|
30
41
|
const startupNow = new Date();
|
|
31
42
|
const startupTime = startupNow.toLocaleString('th-TH', {
|
|
32
43
|
day: '2-digit', month: '2-digit', year: 'numeric',
|
|
33
44
|
hour: '2-digit', minute: '2-digit', hour12: false
|
|
34
45
|
}).replace(',', '');
|
|
35
|
-
console.log(`\x1b[38;5;121m[Mint] v${pkg.version} | ${startupTime} | Active
|
|
46
|
+
console.log(`\x1b[38;5;121m[Mint] v${pkg.version} | ${startupTime} | Active AI: ${startupProvider} • ${startupModel}\x1b[0m`);
|
|
36
47
|
|
|
37
48
|
// ANSI Colors
|
|
38
49
|
const colors = {
|
|
@@ -84,6 +95,29 @@ program
|
|
|
84
95
|
.description('Mint - Your Personal AI Assistant CLI')
|
|
85
96
|
.version(pkg.version);
|
|
86
97
|
|
|
98
|
+
program.hook('preAction', async (thisCommand, actionCommand) => {
|
|
99
|
+
if (actionCommand.name() === 'update' || process.env.MINT_SKIP_AUTO_UPDATE === '1') {
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const config = readConfig();
|
|
104
|
+
if (config.enableAutoUpdate === false) {
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (!shouldRunAutoUpdate(config)) {
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
console.log(`${colors.gray}[Mint Update] Checking for updates...${colors.reset}`);
|
|
113
|
+
const result = await runStartupAutoUpdate(config, writeConfig);
|
|
114
|
+
if (result.status === 'updated') {
|
|
115
|
+
console.log(`${colors.mint}[Mint Update] ${result.message}${colors.reset}`);
|
|
116
|
+
} else if (result.status === 'error') {
|
|
117
|
+
console.log(`${colors.gray}[Mint Update] ${result.message}${colors.reset}`);
|
|
118
|
+
}
|
|
119
|
+
});
|
|
120
|
+
|
|
87
121
|
// Chat Command (Interactive Mode)
|
|
88
122
|
program
|
|
89
123
|
.command('chat', { isDefault: true })
|
|
@@ -139,6 +173,32 @@ program
|
|
|
139
173
|
console.log(`${colors.gray}You will receive a notification when it's done.${colors.reset}\n`);
|
|
140
174
|
});
|
|
141
175
|
|
|
176
|
+
program
|
|
177
|
+
.command('update')
|
|
178
|
+
.description('Check for and install the latest Mint CLI version from npm')
|
|
179
|
+
.option('--check', 'Only check whether an update is available')
|
|
180
|
+
.option('--dry-run', 'Show the npm update operation without installing')
|
|
181
|
+
.action(async (options) => {
|
|
182
|
+
console.log(`\n${colors.mint}${colors.bright}[Mint Update]${colors.reset} Checking npm for updates...`);
|
|
183
|
+
|
|
184
|
+
try {
|
|
185
|
+
const result = await runUpdate({
|
|
186
|
+
checkOnly: options.check === true,
|
|
187
|
+
dryRun: options.dryRun === true
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
const color = result.status === 'error' ? colors.pink : colors.mint;
|
|
191
|
+
console.log(`${color}${result.message}${colors.reset}\n`);
|
|
192
|
+
|
|
193
|
+
if (result.status === 'error') {
|
|
194
|
+
process.exitCode = 1;
|
|
195
|
+
}
|
|
196
|
+
} catch (error) {
|
|
197
|
+
console.error(`${colors.pink}Update failed: ${error.message}${colors.reset}\n`);
|
|
198
|
+
process.exitCode = 1;
|
|
199
|
+
}
|
|
200
|
+
});
|
|
201
|
+
|
|
142
202
|
program
|
|
143
203
|
.command('mcp')
|
|
144
204
|
.description('Manage MCP (Model Context Protocol) servers')
|
|
@@ -212,6 +272,29 @@ program
|
|
|
212
272
|
})
|
|
213
273
|
);
|
|
214
274
|
|
|
275
|
+
program
|
|
276
|
+
.command('gmail')
|
|
277
|
+
.description('Manage Gmail integration')
|
|
278
|
+
.addCommand(new Command('auth')
|
|
279
|
+
.description('Open Google OAuth login and save a Gmail refresh token')
|
|
280
|
+
.option('--port <port>', 'Local callback port, defaults to a random available port')
|
|
281
|
+
.option('--no-open', 'Print the auth link without opening a browser')
|
|
282
|
+
.action(async (options) => {
|
|
283
|
+
try {
|
|
284
|
+
const result = await runGmailAuth({
|
|
285
|
+
port: options.port ? Number(options.port) : 0,
|
|
286
|
+
openBrowser: options.open,
|
|
287
|
+
logger: console
|
|
288
|
+
});
|
|
289
|
+
console.log(`\n${colors.mint}✓${colors.reset} Gmail connected for ${result.userId}. Refresh token saved.`);
|
|
290
|
+
console.log(`${colors.gray}Scopes: ${result.scopes.join(', ')}${colors.reset}\n`);
|
|
291
|
+
} catch (error) {
|
|
292
|
+
console.error(`\n${colors.pink}Gmail auth failed:${colors.reset} ${error.message}\n`);
|
|
293
|
+
process.exitCode = 1;
|
|
294
|
+
}
|
|
295
|
+
})
|
|
296
|
+
);
|
|
297
|
+
|
|
215
298
|
program
|
|
216
299
|
.command('code')
|
|
217
300
|
.description('Run Mint in workspace-aware coding mode for the current project')
|
|
@@ -239,15 +322,21 @@ program
|
|
|
239
322
|
}
|
|
240
323
|
});
|
|
241
324
|
|
|
242
|
-
program.
|
|
325
|
+
program.parseAsync(process.argv).catch((error) => {
|
|
326
|
+
console.error(`${colors.pink}${error.message}${colors.reset}`);
|
|
327
|
+
process.exitCode = 1;
|
|
328
|
+
});
|
|
243
329
|
|
|
244
330
|
/**
|
|
245
331
|
* The Interactive Chat Loop — Gemini-style TUI
|
|
246
332
|
*/
|
|
247
333
|
async function startInteractiveChat(initialMessage = null) {
|
|
248
334
|
let lastResponseText = "";
|
|
249
|
-
const
|
|
335
|
+
const formatErrorMessage = (err) => err && err.message ? err.message : String(err || 'Unknown error');
|
|
336
|
+
|
|
337
|
+
const ui = await createChatUI({
|
|
250
338
|
onSubmit: async (text) => {
|
|
339
|
+
const { screen, appendMessage, streamMessage, setThinking, updateStatusModel, copyLastResponse, requestApproval, setMode, appendCodeStep, updateWorkspace, askUser } = ui;
|
|
251
340
|
if (text.startsWith('/')) {
|
|
252
341
|
if (text.startsWith('/agent')) {
|
|
253
342
|
const args = text.split(' ');
|
|
@@ -375,18 +464,17 @@ async function startInteractiveChat(initialMessage = null) {
|
|
|
375
464
|
clearInterval(timer);
|
|
376
465
|
setThinking(false);
|
|
377
466
|
lastResponseText = result.summary;
|
|
378
|
-
appendMessage('assistant', result.summary);
|
|
467
|
+
appendMessage('assistant', result.summary, { providerInfo: result.providerInfo });
|
|
379
468
|
|
|
380
469
|
} catch (err) {
|
|
381
470
|
clearInterval(timer);
|
|
382
471
|
setThinking(false);
|
|
383
|
-
appendMessage('error', err
|
|
472
|
+
appendMessage('error', formatErrorMessage(err));
|
|
384
473
|
} finally {
|
|
385
474
|
if (setMode) setMode('Chat');
|
|
386
475
|
}
|
|
387
476
|
},
|
|
388
477
|
onExit: () => {
|
|
389
|
-
screen.destroy();
|
|
390
478
|
// Explicitly restore terminal state and disable ALL mouse tracking modes
|
|
391
479
|
process.stdout.write('\x1b[?1000l\x1b[?1002l\x1b[?1003l\x1b[?1006l');
|
|
392
480
|
process.stdout.write('\x1b[?25h'); // Show cursor
|
|
@@ -397,6 +485,7 @@ async function startInteractiveChat(initialMessage = null) {
|
|
|
397
485
|
|
|
398
486
|
// Handle initial message if passed via CLI arg
|
|
399
487
|
if (initialMessage) {
|
|
488
|
+
const { appendMessage, streamMessage, setThinking, updateStatusModel, copyLastResponse, requestApproval, setMode, appendCodeStep, updateWorkspace, askUser } = ui;
|
|
400
489
|
appendMessage('user', initialMessage);
|
|
401
490
|
const transcript = await getChatTranscript();
|
|
402
491
|
if (setMode) setMode('Agent');
|
|
@@ -427,12 +516,12 @@ async function startInteractiveChat(initialMessage = null) {
|
|
|
427
516
|
clearInterval(timer);
|
|
428
517
|
setThinking(false);
|
|
429
518
|
lastResponseText = result.summary;
|
|
430
|
-
appendMessage('assistant', result.summary);
|
|
519
|
+
appendMessage('assistant', result.summary, { providerInfo: result.providerInfo });
|
|
431
520
|
|
|
432
521
|
} catch (err) {
|
|
433
522
|
clearInterval(timer);
|
|
434
523
|
setThinking(false);
|
|
435
|
-
appendMessage('error', err
|
|
524
|
+
appendMessage('error', formatErrorMessage(err));
|
|
436
525
|
} finally {
|
|
437
526
|
if (setMode) setMode('Chat');
|
|
438
527
|
}
|
|
@@ -539,8 +628,10 @@ async function handleSlashCommandUI(input, appendMessage, updateStatusModel, cop
|
|
|
539
628
|
await runChatRoutedTask(`/code ${args.join(' ')}`, {
|
|
540
629
|
appendMessage,
|
|
541
630
|
setThinking,
|
|
631
|
+
requestApproval,
|
|
542
632
|
appendCodeStep,
|
|
543
633
|
setMode,
|
|
634
|
+
askUser: () => Promise.resolve(''),
|
|
544
635
|
history: await getChatTranscript()
|
|
545
636
|
});
|
|
546
637
|
break;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pheem49/mint",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.5.0",
|
|
4
4
|
"description": "A powerful Electron-based AI desktop assistant powered by Google Gemini, featuring screen vision, web automation, and proactive suggestions.",
|
|
5
5
|
"main": "main.js",
|
|
6
6
|
"scripts": {
|
|
@@ -12,7 +12,9 @@
|
|
|
12
12
|
},
|
|
13
13
|
"jest": {
|
|
14
14
|
"testEnvironment": "node",
|
|
15
|
-
"testMatch": [
|
|
15
|
+
"testMatch": [
|
|
16
|
+
"**/tests/**/*.test.js"
|
|
17
|
+
],
|
|
16
18
|
"collectCoverageFrom": [
|
|
17
19
|
"src/AI_Brain/memory_store.js",
|
|
18
20
|
"src/AI_Brain/knowledge_base.js",
|
|
@@ -32,14 +34,17 @@
|
|
|
32
34
|
"dependencies": {
|
|
33
35
|
"@google/genai": "^1.44.0",
|
|
34
36
|
"@inkjs/ui": "^2.0.0",
|
|
37
|
+
"@line/bot-sdk": "^11.0.0",
|
|
35
38
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
39
|
+
"@slack/bolt": "^4.7.2",
|
|
36
40
|
"axios": "^1.13.6",
|
|
37
41
|
"blessed": "^0.1.81",
|
|
38
42
|
"cheerio": "^1.2.0",
|
|
39
43
|
"commander": "^14.0.3",
|
|
44
|
+
"discord.js": "^14.26.4",
|
|
40
45
|
"dotenv": "^17.3.1",
|
|
46
|
+
"express": "^5.2.1",
|
|
41
47
|
"framer-motion": "^12.38.0",
|
|
42
|
-
"google-tts-api": "^2.0.2",
|
|
43
48
|
"ink": "^7.0.1",
|
|
44
49
|
"ink-text-input": "^6.0.0",
|
|
45
50
|
"inquirer": "^13.4.1",
|
|
@@ -47,9 +52,12 @@
|
|
|
47
52
|
"mammoth": "^1.12.0",
|
|
48
53
|
"pdf-parse": "^2.4.5",
|
|
49
54
|
"puppeteer": "^24.38.0",
|
|
55
|
+
"qrcode-terminal": "^0.12.0",
|
|
50
56
|
"react": "^19.2.5",
|
|
51
57
|
"react-dom": "^19.2.5",
|
|
52
|
-
"
|
|
58
|
+
"read-excel-file": "^9.0.9",
|
|
59
|
+
"telegraf": "^4.16.3",
|
|
60
|
+
"whatsapp-web.js": "^1.34.7"
|
|
53
61
|
},
|
|
54
62
|
"devDependencies": {
|
|
55
63
|
"@vitejs/plugin-react": "^6.0.1",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
const { GoogleGenAI } = require('@google/genai');
|
|
2
2
|
const { readChatHistory, writeChatHistory, clearChatHistory } = require('../System/chat_history_manager');
|
|
3
|
-
const { readConfig, getAvailableProviders } = require('../System/config_manager');
|
|
3
|
+
const { readConfig, getAvailableProviders, isPlaceholder } = require('../System/config_manager');
|
|
4
4
|
const pluginManager = require('../Plugins/plugin_manager');
|
|
5
5
|
const mcpManager = require('../Plugins/mcp_manager');
|
|
6
6
|
const memoryStore = require('./memory_store');
|
|
@@ -41,12 +41,14 @@ PERSONALITY & TONE:
|
|
|
41
41
|
- Use a professional yet sweet tone when needed, but prioritize being a lovable assistant.
|
|
42
42
|
|
|
43
43
|
NATURAL CHAT FLOW:
|
|
44
|
-
-
|
|
45
|
-
-
|
|
46
|
-
-
|
|
44
|
+
- Be an independent thinker. Analyze requests deeply before responding.
|
|
45
|
+
- While brevity is good for simple tasks, feel free to provide detailed, comprehensive explanations or creative ideas when the user asks complex questions or seeks inspiration.
|
|
46
|
+
- You have the autonomy to suggest better ways to achieve a goal, provide alternative perspectives, and take initiative in helping the user.
|
|
47
|
+
- Separate distinct points with blank lines (double newline) for readability.
|
|
48
|
+
- Ask follow-up questions only when they add significant value to the task or conversation.
|
|
47
49
|
|
|
48
50
|
GOAL:
|
|
49
|
-
Your goal is to help the user with their queries. If they ask to open an application, open a website, search, manage files, or get system info, you must
|
|
51
|
+
Your goal is to help the user with their queries. If they ask to open an application, open a website, search, manage files, or get system info, you must trigger an action in the structured JSON format below. **NEVER provide a conversational response about performing an action without including the actual "action" object in your JSON.**
|
|
50
52
|
|
|
51
53
|
CREATOR INFO:
|
|
52
54
|
- The creator is Pheem49.
|
|
@@ -176,15 +178,68 @@ function resolveGeminiModel() {
|
|
|
176
178
|
function getProviderAttemptOrder(config) {
|
|
177
179
|
const provider = config.aiProvider || 'gemini';
|
|
178
180
|
const availableProviders = getAvailableProviders(config);
|
|
179
|
-
const
|
|
180
|
-
|
|
181
|
+
const ordered = availableProviders.includes(provider)
|
|
182
|
+
? [provider, ...availableProviders.filter(p => p !== provider)]
|
|
183
|
+
: availableProviders;
|
|
184
|
+
return ordered.length > 0 ? ordered : ['gemini'];
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function getProviderModel(provider, config = {}) {
|
|
188
|
+
switch (provider) {
|
|
189
|
+
case 'gemini':
|
|
190
|
+
return (config.geminiModel || DEFAULT_GEMINI_MODEL).trim() || DEFAULT_GEMINI_MODEL;
|
|
191
|
+
case 'anthropic':
|
|
192
|
+
return config.anthropicModel || 'claude-3-5-sonnet-latest';
|
|
193
|
+
case 'openai':
|
|
194
|
+
return config.openaiModel || 'gpt-4o';
|
|
195
|
+
case 'local_openai':
|
|
196
|
+
return config.localModelName || 'local-model';
|
|
197
|
+
case 'huggingface':
|
|
198
|
+
return config.hfModel || 'meta-llama/Meta-Llama-3-8B-Instruct';
|
|
199
|
+
case 'ollama':
|
|
200
|
+
return config.ollamaModel || 'llama3:latest';
|
|
201
|
+
default:
|
|
202
|
+
return '';
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function withProviderInfo(result, provider, config = {}) {
|
|
207
|
+
const normalized = (result && typeof result === 'object')
|
|
208
|
+
? result
|
|
209
|
+
: { response: String(result || ''), action: { type: 'none', target: '' } };
|
|
210
|
+
const providerInfo = {
|
|
211
|
+
provider,
|
|
212
|
+
model: getProviderModel(provider, config)
|
|
213
|
+
};
|
|
214
|
+
|
|
215
|
+
attachProviderInfoToLatestHistory(providerInfo);
|
|
216
|
+
|
|
217
|
+
return {
|
|
218
|
+
...normalized,
|
|
219
|
+
providerInfo
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function attachProviderInfoToLatestHistory(providerInfo) {
|
|
224
|
+
try {
|
|
225
|
+
const history = readChatHistory();
|
|
226
|
+
for (let i = history.length - 1; i >= 0; i -= 1) {
|
|
227
|
+
if (history[i] && history[i].role === 'model') {
|
|
228
|
+
history[i].providerInfo = providerInfo;
|
|
229
|
+
writeChatHistory(history);
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
} catch (error) {
|
|
234
|
+
console.warn('[Provider Info] Failed to persist provider metadata:', error.message);
|
|
235
|
+
}
|
|
181
236
|
}
|
|
182
237
|
|
|
183
238
|
// Chat session — maintains conversation history within the session
|
|
184
239
|
let chat = null;
|
|
185
240
|
let activeModel = resolveGeminiModel();
|
|
186
241
|
let lastLoggedModel = '';
|
|
187
|
-
const MAX_HISTORY_MESSAGES =
|
|
242
|
+
const MAX_HISTORY_MESSAGES = 40; // Increased context for deeper reasoning
|
|
188
243
|
|
|
189
244
|
function createChat(history = []) {
|
|
190
245
|
// Truncate history and strip custom fields like 'timestamp' before passing to SDK
|
|
@@ -251,28 +306,28 @@ async function handleChat(message, base64Image = null, base64Audio = null) {
|
|
|
251
306
|
const currentProv = providersToTry[i];
|
|
252
307
|
try {
|
|
253
308
|
if (currentProv === 'ollama') {
|
|
254
|
-
return await handleOllamaChat(finalMessage, base64Image, base64Audio, config);
|
|
309
|
+
return withProviderInfo(await handleOllamaChat(finalMessage, base64Image, base64Audio, config), currentProv, config);
|
|
255
310
|
}
|
|
256
311
|
if (currentProv === 'anthropic') {
|
|
257
|
-
return await handleAnthropicChat(finalMessage, base64Image, config);
|
|
312
|
+
return withProviderInfo(await handleAnthropicChat(finalMessage, base64Image, config), currentProv, config);
|
|
258
313
|
}
|
|
259
314
|
if (currentProv === 'openai') {
|
|
260
|
-
return await handleOpenAIChat(finalMessage, base64Image, config);
|
|
315
|
+
return withProviderInfo(await handleOpenAIChat(finalMessage, base64Image, config), currentProv, config);
|
|
261
316
|
}
|
|
262
317
|
if (currentProv === 'local_openai') {
|
|
263
|
-
return await handleLocalOpenAIChat(finalMessage, base64Image, config);
|
|
318
|
+
return withProviderInfo(await handleLocalOpenAIChat(finalMessage, base64Image, config), currentProv, config);
|
|
264
319
|
}
|
|
265
320
|
if (currentProv === 'huggingface') {
|
|
266
|
-
return await handleHuggingFaceChat(finalMessage, base64Image, config);
|
|
321
|
+
return withProviderInfo(await handleHuggingFaceChat(finalMessage, base64Image, config), currentProv, config);
|
|
267
322
|
}
|
|
268
323
|
|
|
269
324
|
const currentKey = resolveApiKey();
|
|
270
325
|
if (!currentKey) {
|
|
271
326
|
if (i === providersToTry.length - 1) {
|
|
272
|
-
return {
|
|
327
|
+
return withProviderInfo({
|
|
273
328
|
response: "I couldn't find your Gemini API Key. Please run 'mint onboard' to set it up!",
|
|
274
329
|
action: { type: "none", target: "" }
|
|
275
|
-
};
|
|
330
|
+
}, currentProv, config);
|
|
276
331
|
}
|
|
277
332
|
console.warn("[Fallback System] Gemini API key missing. Skipping Gemini provider.");
|
|
278
333
|
continue;
|
|
@@ -283,7 +338,7 @@ async function handleChat(message, base64Image = null, base64Audio = null) {
|
|
|
283
338
|
createChat(readChatHistory());
|
|
284
339
|
}
|
|
285
340
|
|
|
286
|
-
return await handleGeminiChat(finalMessage, base64Image, base64Audio);
|
|
341
|
+
return withProviderInfo(await handleGeminiChat(finalMessage, base64Image, base64Audio), currentProv, config);
|
|
287
342
|
} catch (error) {
|
|
288
343
|
console.error(`[Fallback System] Provider '${currentProv}' failed:`, error.message);
|
|
289
344
|
if (i === providersToTry.length - 1) {
|
|
@@ -522,7 +577,7 @@ async function* handleGeminiChatStream(finalMessage, base64Image, base64Audio) {
|
|
|
522
577
|
async function handleAnthropicChat(finalMessage, base64Image, config) {
|
|
523
578
|
const history = readChatHistory() || [];
|
|
524
579
|
const apiKey = config.anthropicApiKey || process.env.ANTHROPIC_API_KEY;
|
|
525
|
-
if (
|
|
580
|
+
if (isPlaceholder(apiKey)) return { response: "กรุณาใส่ Anthropic API Key ในการตั้งค่าก่อนนะคะ", action: { type: "none" } };
|
|
526
581
|
|
|
527
582
|
const systemPrompt = buildSystemPrompt();
|
|
528
583
|
|
|
@@ -569,7 +624,7 @@ async function handleAnthropicChat(finalMessage, base64Image, config) {
|
|
|
569
624
|
async function handleOpenAIChat(finalMessage, base64Image, config) {
|
|
570
625
|
const history = readChatHistory() || [];
|
|
571
626
|
const apiKey = config.openaiApiKey || process.env.OPENAI_API_KEY;
|
|
572
|
-
if (
|
|
627
|
+
if (isPlaceholder(apiKey)) return { response: "กรุณาใส่ OpenAI API Key ในการตั้งค่าก่อนนะคะ", action: { type: "none" } };
|
|
573
628
|
|
|
574
629
|
const systemPrompt = buildSystemPrompt();
|
|
575
630
|
|
|
@@ -656,7 +711,7 @@ async function handleLocalOpenAIChat(finalMessage, base64Image, config) {
|
|
|
656
711
|
async function handleHuggingFaceChat(finalMessage, base64Image, config) {
|
|
657
712
|
const history = readChatHistory() || [];
|
|
658
713
|
const apiKey = config.hfApiKey || process.env.HF_API_KEY;
|
|
659
|
-
if (
|
|
714
|
+
if (isPlaceholder(apiKey)) return { response: "กรุณาใส่ Hugging Face API Key ในการตั้งค่าก่อนนะคะ", action: { type: "none" } };
|
|
660
715
|
|
|
661
716
|
const modelId = config.hfModel || 'meta-llama/Meta-Llama-3-8B-Instruct';
|
|
662
717
|
const baseUrl = `https://api-inference.huggingface.co/models/${modelId}/v1/chat/completions`;
|
|
@@ -778,6 +833,7 @@ async function handleOllamaChat(finalMessage, base64Image, base64Audio, config)
|
|
|
778
833
|
|
|
779
834
|
function resetChat() {
|
|
780
835
|
clearChatHistory();
|
|
836
|
+
memoryStore.clearConversationScopedProfile();
|
|
781
837
|
createChat([]);
|
|
782
838
|
console.log("Chat history cleared.");
|
|
783
839
|
}
|
|
@@ -820,7 +876,8 @@ function historyToTranscript(history) {
|
|
|
820
876
|
transcript.push({
|
|
821
877
|
sender,
|
|
822
878
|
text,
|
|
823
|
-
timestamp: content.timestamp || new Date().toISOString()
|
|
879
|
+
timestamp: content.timestamp || new Date().toISOString(),
|
|
880
|
+
providerInfo: content.providerInfo || null
|
|
824
881
|
});
|
|
825
882
|
}
|
|
826
883
|
return transcript;
|
|
@@ -3,6 +3,7 @@ const { readConfig } = require('../System/config_manager');
|
|
|
3
3
|
const { performWebAutomation } = require('../Automation_Layer/browser_automation');
|
|
4
4
|
const { createFolder, deleteFile } = require('../Automation_Layer/file_operations');
|
|
5
5
|
const { searchKnowledge } = require('./knowledge_base');
|
|
6
|
+
const safetyManager = require('../System/safety_manager');
|
|
6
7
|
const fs = require('fs');
|
|
7
8
|
const path = require('path');
|
|
8
9
|
|
|
@@ -99,8 +100,16 @@ async function executeAutonomousTask(taskDescription, notifyCallback) {
|
|
|
99
100
|
break;
|
|
100
101
|
case 'write_file':
|
|
101
102
|
const filePath = expandHome(actionObj.target);
|
|
103
|
+
safetyManager.resolveWithinRoot(os.homedir(), filePath);
|
|
102
104
|
if (notifyCallback) notifyCallback(`✍️ กำลังบันทึกไฟล์: ${actionObj.target}`);
|
|
103
105
|
try {
|
|
106
|
+
safetyManager.appendActionLog({
|
|
107
|
+
source: 'autonomous_brain',
|
|
108
|
+
action: 'write_file',
|
|
109
|
+
target: filePath,
|
|
110
|
+
tier: safetyManager.TIERS.APPROVAL,
|
|
111
|
+
approved: true
|
|
112
|
+
});
|
|
104
113
|
fs.writeFileSync(filePath, actionObj.data || '');
|
|
105
114
|
observation = `File written successfully to ${actionObj.target}`;
|
|
106
115
|
} catch (e) {
|
|
@@ -109,6 +118,7 @@ async function executeAutonomousTask(taskDescription, notifyCallback) {
|
|
|
109
118
|
break;
|
|
110
119
|
case 'delete_file':
|
|
111
120
|
const delPath = expandHome(actionObj.target);
|
|
121
|
+
safetyManager.assertActionAllowed({ type: 'delete_file', target: delPath });
|
|
112
122
|
if (notifyCallback) notifyCallback(`🗑️ มิ้นท์ขอย้ายไฟล์ไปที่ถังขยะ: ${actionObj.target}`);
|
|
113
123
|
const resDel = await deleteFile(delPath);
|
|
114
124
|
observation = resDel.success ? "File moved to trash." : `Failed: ${resDel.message}`;
|
|
@@ -1,12 +1,33 @@
|
|
|
1
1
|
const fs = require('fs');
|
|
2
2
|
const path = require('path');
|
|
3
|
-
const
|
|
3
|
+
const os = require('os');
|
|
4
|
+
|
|
5
|
+
// Handle electron dependency safely
|
|
6
|
+
let app;
|
|
7
|
+
try {
|
|
8
|
+
const electron = require('electron');
|
|
9
|
+
app = electron.app;
|
|
10
|
+
} catch (e) {
|
|
11
|
+
app = null;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const CONFIG_DIR = path.join(os.homedir(), '.config', 'mint');
|
|
15
|
+
if (!fs.existsSync(CONFIG_DIR)) {
|
|
16
|
+
fs.mkdirSync(CONFIG_DIR, { recursive: true });
|
|
17
|
+
}
|
|
4
18
|
|
|
5
|
-
|
|
6
|
-
// Behavior Memory — Tracks user behavior patterns over time
|
|
7
|
-
// ============================================================
|
|
19
|
+
const MEMORY_FILE = path.join(CONFIG_DIR, 'behavior_memory.json');
|
|
8
20
|
|
|
9
|
-
|
|
21
|
+
// Migration Logic: Move from Electron userData to ~/.config/mint
|
|
22
|
+
if (!fs.existsSync(MEMORY_FILE) && app && app.getPath) {
|
|
23
|
+
const electronPath = path.join(app.getPath('userData'), 'behavior_memory.json');
|
|
24
|
+
if (fs.existsSync(electronPath)) {
|
|
25
|
+
try {
|
|
26
|
+
fs.copyFileSync(electronPath, MEMORY_FILE);
|
|
27
|
+
console.log('[BehaviorMemory] Migrated memory from Electron userData');
|
|
28
|
+
} catch (e) { console.error('[BehaviorMemory] Migration failed:', e); }
|
|
29
|
+
}
|
|
30
|
+
}
|
|
10
31
|
const MAX_CONTEXT_HISTORY = 20; // Keep last 20 context snapshots
|
|
11
32
|
|
|
12
33
|
/**
|
|
@@ -27,6 +27,10 @@ async function startAgent() {
|
|
|
27
27
|
// Initialize System Monitoring
|
|
28
28
|
systemEvents.startMonitoring();
|
|
29
29
|
|
|
30
|
+
// Initialize Messaging Bridges
|
|
31
|
+
const bridgeManager = require('../System/bridge_manager');
|
|
32
|
+
bridgeManager.init().catch(err => console.error('[BridgeManager] Init Error:', err));
|
|
33
|
+
|
|
30
34
|
// Listen for Battery Events
|
|
31
35
|
systemEvents.on('low-battery', (level) => {
|
|
32
36
|
sendNotification(
|
|
@@ -5,7 +5,7 @@ const crypto = require('crypto');
|
|
|
5
5
|
const { GoogleGenAI } = require('@google/genai');
|
|
6
6
|
const pdf = require('pdf-parse');
|
|
7
7
|
const mammoth = require('mammoth');
|
|
8
|
-
const
|
|
8
|
+
const readXlsxFile = require('read-excel-file/node');
|
|
9
9
|
const { readConfig } = require('../System/config_manager');
|
|
10
10
|
|
|
11
11
|
// Handle electron dependency safely
|
|
@@ -44,12 +44,32 @@ function getAiClient() {
|
|
|
44
44
|
|
|
45
45
|
function getDbPath() {
|
|
46
46
|
const fileName = 'mint-knowledge.sqlite';
|
|
47
|
-
|
|
48
|
-
|
|
47
|
+
const configDir = path.join(os.homedir(), '.config', 'mint');
|
|
48
|
+
const dbPath = path.join(configDir, fileName);
|
|
49
|
+
|
|
50
|
+
if (!fs.existsSync(configDir)) {
|
|
51
|
+
fs.mkdirSync(configDir, { recursive: true });
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Migration Logic
|
|
55
|
+
if (!fs.existsSync(dbPath)) {
|
|
56
|
+
const electronDb = app && app.getPath ? path.join(app.getPath('userData'), fileName) : null;
|
|
57
|
+
const legacyDb = path.join(os.homedir(), '.mint', fileName);
|
|
58
|
+
|
|
59
|
+
if (electronDb && fs.existsSync(electronDb)) {
|
|
60
|
+
try {
|
|
61
|
+
fs.copyFileSync(electronDb, dbPath);
|
|
62
|
+
console.log('[RAG] Migrated database from Electron userData');
|
|
63
|
+
} catch (e) { console.error('[RAG] Migration from Electron failed:', e); }
|
|
64
|
+
} else if (fs.existsSync(legacyDb)) {
|
|
65
|
+
try {
|
|
66
|
+
fs.copyFileSync(legacyDb, dbPath);
|
|
67
|
+
console.log('[RAG] Migrated database from ~/.mint');
|
|
68
|
+
} catch (e) { console.error('[RAG] Migration from ~/.mint failed:', e); }
|
|
69
|
+
}
|
|
49
70
|
}
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
return path.join(mintDir, fileName);
|
|
71
|
+
|
|
72
|
+
return dbPath;
|
|
53
73
|
}
|
|
54
74
|
|
|
55
75
|
function getDatabaseSync() {
|
|
@@ -67,8 +87,13 @@ function getDb() {
|
|
|
67
87
|
const Database = getDatabaseSync();
|
|
68
88
|
dbInstance = new Database(dbPath);
|
|
69
89
|
|
|
90
|
+
// Enable WAL mode for better concurrency
|
|
91
|
+
dbInstance.exec('PRAGMA journal_mode = WAL;');
|
|
92
|
+
dbInstance.exec('PRAGMA synchronous = NORMAL;');
|
|
93
|
+
|
|
70
94
|
// Create Tables
|
|
71
95
|
dbInstance.exec(`
|
|
96
|
+
-- Shared knowledge tables
|
|
72
97
|
CREATE TABLE IF NOT EXISTS sources (
|
|
73
98
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
74
99
|
path TEXT UNIQUE,
|
|
@@ -84,6 +109,29 @@ function getDb() {
|
|
|
84
109
|
FOREIGN KEY(source_id) REFERENCES sources(id) ON DELETE CASCADE
|
|
85
110
|
);
|
|
86
111
|
CREATE INDEX IF NOT EXISTS idx_chunks_source ON chunks(source_id);
|
|
112
|
+
|
|
113
|
+
-- Shared memory tables (ensuring consistency)
|
|
114
|
+
CREATE TABLE IF NOT EXISTS user_profile (
|
|
115
|
+
key TEXT PRIMARY KEY,
|
|
116
|
+
value TEXT,
|
|
117
|
+
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
|
118
|
+
);
|
|
119
|
+
CREATE TABLE IF NOT EXISTS session_memories (
|
|
120
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
121
|
+
summary TEXT NOT NULL,
|
|
122
|
+
tags TEXT DEFAULT '',
|
|
123
|
+
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
|
124
|
+
);
|
|
125
|
+
CREATE TABLE IF NOT EXISTS usage_patterns (
|
|
126
|
+
pattern TEXT PRIMARY KEY,
|
|
127
|
+
count INTEGER DEFAULT 1,
|
|
128
|
+
last_used DATETIME DEFAULT CURRENT_TIMESTAMP
|
|
129
|
+
);
|
|
130
|
+
CREATE TABLE IF NOT EXISTS response_cache (
|
|
131
|
+
query_hash TEXT PRIMARY KEY,
|
|
132
|
+
response TEXT NOT NULL,
|
|
133
|
+
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
|
134
|
+
);
|
|
87
135
|
`);
|
|
88
136
|
return dbInstance;
|
|
89
137
|
}
|
|
@@ -154,8 +202,13 @@ async function indexFile(filePath) {
|
|
|
154
202
|
const res = await mammoth.extractRawText({ path: filePath });
|
|
155
203
|
content = res.value;
|
|
156
204
|
} else if (ext === '.xlsx') {
|
|
157
|
-
const
|
|
158
|
-
content =
|
|
205
|
+
const sheets = await readXlsxFile(filePath);
|
|
206
|
+
content = sheets
|
|
207
|
+
.map(({ sheet, data }) => [
|
|
208
|
+
`Sheet: ${sheet}`,
|
|
209
|
+
...data.map(row => row.map(value => value == null ? '' : String(value)).join(','))
|
|
210
|
+
].join('\n'))
|
|
211
|
+
.join('\n');
|
|
159
212
|
} else {
|
|
160
213
|
content = fs.readFileSync(filePath, 'utf8');
|
|
161
214
|
}
|