@pheem49/mint 1.4.1 → 1.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (61) hide show
  1. package/GUIDE_TH.md +113 -0
  2. package/README.md +214 -142
  3. package/assets/CLI_Screen.png +0 -0
  4. package/docs/assets/CLI_Screen.png +0 -0
  5. package/docs/guide.html +632 -0
  6. package/docs/index.html +5 -4
  7. package/main.js +66 -894
  8. package/mint-cli-logic.js +15 -8
  9. package/mint-cli.js +305 -195
  10. package/package.json +12 -4
  11. package/src/AI_Brain/Gemini_API.js +77 -20
  12. package/src/AI_Brain/agent_orchestrator.js +6 -6
  13. package/src/AI_Brain/autonomous_brain.js +10 -0
  14. package/src/AI_Brain/behavior_memory.js +26 -5
  15. package/src/AI_Brain/headless_agent.js +4 -0
  16. package/src/AI_Brain/knowledge_base.js +61 -8
  17. package/src/AI_Brain/memory_store.js +55 -7
  18. package/src/Automation_Layer/file_operations.js +14 -3
  19. package/src/CLI/chat_router.js +21 -7
  20. package/src/CLI/chat_ui.js +264 -710
  21. package/src/CLI/code_agent.js +370 -124
  22. package/src/CLI/gmail_auth.js +210 -0
  23. package/src/CLI/list_features.js +5 -1
  24. package/src/CLI/onboarding.js +307 -55
  25. package/src/CLI/updater.js +208 -0
  26. package/src/Channels/brave_search_bridge.js +35 -0
  27. package/src/Channels/discord_bridge.js +68 -0
  28. package/src/Channels/google_search_bridge.js +38 -0
  29. package/src/Channels/line_bridge.js +60 -0
  30. package/src/Channels/slack_bridge.js +53 -0
  31. package/src/Channels/telegram_bridge.js +49 -0
  32. package/src/Channels/whatsapp_bridge.js +55 -0
  33. package/src/Command_Parser/parser.js +12 -1
  34. package/src/Plugins/gmail.js +251 -0
  35. package/src/Plugins/google_calendar.js +245 -19
  36. package/src/Plugins/notion.js +256 -0
  37. package/src/System/action_executor.js +129 -0
  38. package/src/System/bridge_manager.js +76 -0
  39. package/src/System/chat_history_manager.js +23 -5
  40. package/src/System/config_manager.js +41 -7
  41. package/src/System/custom_workflows.js +31 -2
  42. package/src/System/google_tts_urls.js +51 -0
  43. package/src/System/ipc_handlers.js +238 -0
  44. package/src/System/proactive_loop.js +137 -0
  45. package/src/System/safety_manager.js +165 -0
  46. package/src/System/screen_capture.js +175 -0
  47. package/src/System/task_manager.js +15 -5
  48. package/src/System/window_manager.js +210 -0
  49. package/src/UI/renderer.js +33 -7
  50. package/src/UI/settings.html +24 -0
  51. package/src/UI/settings.js +14 -4
  52. package/src/UI/styles.css +14 -1
  53. package/tests/action_executor_safety.test.js +67 -0
  54. package/tests/gmail.test.js +135 -0
  55. package/tests/gmail_auth.test.js +129 -0
  56. package/tests/google_calendar.test.js +113 -0
  57. package/tests/google_tts_urls.test.js +24 -0
  58. package/tests/notion.test.js +121 -0
  59. package/tests/provider_routing.test.js +17 -1
  60. package/tests/safety_manager.test.js +40 -0
  61. package/tests/updater.test.js +32 -0
@@ -0,0 +1,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 };
@@ -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
- if (!fs.existsSync(MINT_DIR)) {
15
- fs.mkdirSync(MINT_DIR, { recursive: true });
15
+
16
+ if (!fs.existsSync(CONFIG_DIR)) {
17
+ fs.mkdirSync(CONFIG_DIR, { recursive: true });
16
18
  }
17
19
 
18
- const CHAT_HISTORY_PATH = app && app.getPath
19
- ? path.join(app.getPath('userData'), 'mint-chat-history.json')
20
- : path.join(MINT_DIR, 'mint-chat-history.json');
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: 'http://localhost:1234/v1',
96
+ localApiBaseUrl: '',
68
97
  localModelName: 'local-model',
69
- ollamaHost: 'http://localhost:11434',
70
- enableAgentCollaboration: true
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
- module.exports = { readConfig, writeConfig, getAvailableProviders, CONFIG_PATH };
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 { app, shell } = require('electron');
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
- this.configPath = path.join(app.getPath('userData'), 'workflows.json');
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 };