@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,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
|
-
|
|
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:
|
|
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 };
|