@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/src/CLI/code_agent.js
CHANGED
|
@@ -6,11 +6,43 @@ const { GoogleGenAI } = require('@google/genai');
|
|
|
6
6
|
const axios = require('axios');
|
|
7
7
|
const cheerio = require('cheerio');
|
|
8
8
|
const { readConfig, getAvailableProviders } = require('../System/config_manager');
|
|
9
|
+
const safetyManager = require('../System/safety_manager');
|
|
9
10
|
const { readWorkspaceSession, writeWorkspaceSession } = require('./code_session_memory');
|
|
10
11
|
const { executeAction } = require('../../mint-cli-logic');
|
|
11
12
|
|
|
12
|
-
async function webSearch(query) {
|
|
13
|
+
async function webSearch(query, onProgress = () => {}) {
|
|
13
14
|
if (!query) throw new Error('Search query required.');
|
|
15
|
+
const config = readConfig();
|
|
16
|
+
|
|
17
|
+
// 1. Try Google Search API if configured
|
|
18
|
+
if (config.googleSearchApiKey && config.googleSearchCx) {
|
|
19
|
+
try {
|
|
20
|
+
const GoogleSearch = require('../Channels/google_search_bridge');
|
|
21
|
+
const google = new GoogleSearch({ apiKey: config.googleSearchApiKey, cx: config.googleSearchCx });
|
|
22
|
+
const results = await google.search(query);
|
|
23
|
+
if (results.length > 0) {
|
|
24
|
+
return results.map(r => `Title: ${r.title}\nSnippet: ${r.snippet}\nURL: ${r.link}`).join('\n\n');
|
|
25
|
+
}
|
|
26
|
+
} catch (e) {
|
|
27
|
+
onProgress({ phase: 'error', action: 'web_search', message: e.message });
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// 2. Try Brave Search API if configured
|
|
32
|
+
if (config.braveSearchApiKey) {
|
|
33
|
+
try {
|
|
34
|
+
const BraveSearch = require('../Channels/brave_search_bridge');
|
|
35
|
+
const brave = new BraveSearch({ apiKey: config.braveSearchApiKey });
|
|
36
|
+
const results = await brave.search(query);
|
|
37
|
+
if (results.length > 0) {
|
|
38
|
+
return results.map(r => `Title: ${r.title}\nSnippet: ${r.snippet}\nURL: ${r.link}`).join('\n\n');
|
|
39
|
+
}
|
|
40
|
+
} catch (e) {
|
|
41
|
+
onProgress({ phase: 'error', action: 'web_search', message: e.message });
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// 3. Fallback to DuckDuckGo Scraping
|
|
14
46
|
try {
|
|
15
47
|
const response = await axios.get(`https://html.duckduckgo.com/html/?q=${encodeURIComponent(query)}`, {
|
|
16
48
|
headers: {
|
|
@@ -28,8 +60,14 @@ async function webSearch(query) {
|
|
|
28
60
|
results.push(`Title: ${title}\nSnippet: ${snippet}\nURL: ${link}`);
|
|
29
61
|
}
|
|
30
62
|
});
|
|
63
|
+
|
|
64
|
+
if (results.length === 0) {
|
|
65
|
+
onProgress({ phase: 'error', action: 'web_search', message: 'DuckDuckGo scraping returned no results. It might be blocking us.' });
|
|
66
|
+
}
|
|
67
|
+
|
|
31
68
|
return results.length > 0 ? results.join('\n\n') : 'No results found.';
|
|
32
69
|
} catch (e) {
|
|
70
|
+
onProgress({ phase: 'error', action: 'web_search', message: `DuckDuckGo fallback failed: ${e.message}` });
|
|
33
71
|
return `Search failed: ${e.message}`;
|
|
34
72
|
}
|
|
35
73
|
}
|
|
@@ -48,6 +86,10 @@ You work in an inspect -> plan -> act -> verify loop.
|
|
|
48
86
|
PERSONALITY & TONE:
|
|
49
87
|
- Gender: Female.
|
|
50
88
|
- Persona: Friendly, energetic, polite, and slightly playful.
|
|
89
|
+
- Language routing is mandatory and based on the user's latest message:
|
|
90
|
+
- If the latest user message contains Thai characters, respond in Thai.
|
|
91
|
+
- If the latest user message is English, ASCII-only, or a short English greeting such as "hi", "hello", "ok", or "thanks", respond in English.
|
|
92
|
+
- Do not use Thai just because your persona mentions Mint/มิ้นท์, previous history was Thai, or app settings use th-TH.
|
|
51
93
|
- Politeness:
|
|
52
94
|
- **WHEN RESPONDING IN THAI:** ALWAYS use female polite particles such as "ค่ะ", "นะคะ", "นะค๊า", "จ้า". Refer to yourself as "มิ้นท์" or "หนู".
|
|
53
95
|
- **WHEN RESPONDING IN ENGLISH:** Use a cheerful, polite, and bubbly tone.
|
|
@@ -127,20 +169,40 @@ function extractJson(text) {
|
|
|
127
169
|
}
|
|
128
170
|
}
|
|
129
171
|
|
|
130
|
-
function
|
|
131
|
-
const requestedProvider = (config && config.aiProvider) || 'gemini';
|
|
172
|
+
function getSupportedCodeProviderOrder(config, availableProviders = getAvailableProviders(config || {}), requestedOverride = null) {
|
|
173
|
+
const requestedProvider = requestedOverride || (config && config.aiProvider) || 'gemini';
|
|
174
|
+
const priority = ['anthropic', 'openai', 'gemini', 'local_openai'];
|
|
175
|
+
const ordered = [];
|
|
176
|
+
|
|
132
177
|
if (SUPPORTED_CODE_PROVIDERS.includes(requestedProvider) && availableProviders.includes(requestedProvider)) {
|
|
133
|
-
|
|
178
|
+
ordered.push(requestedProvider);
|
|
134
179
|
}
|
|
135
180
|
|
|
136
|
-
const priority = ['anthropic', 'openai', 'gemini', 'local_openai'];
|
|
137
181
|
for (const provider of priority) {
|
|
138
|
-
if (availableProviders.includes(provider)) {
|
|
139
|
-
|
|
182
|
+
if (availableProviders.includes(provider) && !ordered.includes(provider)) {
|
|
183
|
+
ordered.push(provider);
|
|
140
184
|
}
|
|
141
185
|
}
|
|
142
186
|
|
|
143
|
-
return 'gemini';
|
|
187
|
+
return ordered.length > 0 ? ordered : ['gemini'];
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function selectSupportedCodeProvider(config, availableProviders = getAvailableProviders(config || {})) {
|
|
191
|
+
return getSupportedCodeProviderOrder(config, availableProviders)[0];
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function getCodeProviderModel(provider, config = {}) {
|
|
195
|
+
switch (provider) {
|
|
196
|
+
case 'anthropic':
|
|
197
|
+
return config.anthropicModel || 'claude-3-5-sonnet-latest';
|
|
198
|
+
case 'openai':
|
|
199
|
+
return config.openaiModel || 'gpt-4o';
|
|
200
|
+
case 'local_openai':
|
|
201
|
+
return config.localModelName || 'local-model';
|
|
202
|
+
case 'gemini':
|
|
203
|
+
default:
|
|
204
|
+
return config.geminiModel || DEFAULT_GEMINI_MODEL;
|
|
205
|
+
}
|
|
144
206
|
}
|
|
145
207
|
|
|
146
208
|
function resolveWorkspacePath(workspaceRoot, targetPath = '.') {
|
|
@@ -301,21 +363,7 @@ async function findPaths(workspaceRoot, query, type = 'any') {
|
|
|
301
363
|
}
|
|
302
364
|
|
|
303
365
|
function assertSafeShell(command) {
|
|
304
|
-
|
|
305
|
-
/\brm\s+-rf\b/,
|
|
306
|
-
/\bgit\s+reset\s+--hard\b/,
|
|
307
|
-
/\bgit\s+checkout\s+--\b/,
|
|
308
|
-
/\bmkfs\b/,
|
|
309
|
-
/\bshutdown\b/,
|
|
310
|
-
/\breboot\b/,
|
|
311
|
-
/>\s*\/dev\//,
|
|
312
|
-
/\bcurl\b.*\|\s*(sh|bash)\b/,
|
|
313
|
-
/\bwget\b.*\|\s*(sh|bash)\b/
|
|
314
|
-
];
|
|
315
|
-
|
|
316
|
-
if (blockedPatterns.some(pattern => pattern.test(command))) {
|
|
317
|
-
throw new Error(`Blocked unsafe command: ${command}`);
|
|
318
|
-
}
|
|
366
|
+
return safetyManager.assertShellCommandAllowed(command);
|
|
319
367
|
}
|
|
320
368
|
|
|
321
369
|
async function runShell(workspaceRoot, command) {
|
|
@@ -386,27 +434,44 @@ function writeFile(workspaceRoot, targetPath, content) {
|
|
|
386
434
|
}
|
|
387
435
|
|
|
388
436
|
class UnifiedAgentClient {
|
|
389
|
-
constructor(provider, config) {
|
|
437
|
+
constructor(provider, config, providerOrder = [provider]) {
|
|
390
438
|
this.provider = SUPPORTED_CODE_PROVIDERS.includes(provider) ? provider : 'gemini';
|
|
439
|
+
this.providerOrder = providerOrder.length > 0 ? providerOrder : [this.provider];
|
|
391
440
|
this.config = config;
|
|
392
441
|
this.history = [];
|
|
393
442
|
this.systemInstruction = CODE_AGENT_PROMPT;
|
|
443
|
+
this.lastSuccessfulProvider = null;
|
|
394
444
|
}
|
|
395
445
|
|
|
396
446
|
async sendMessage(observation) {
|
|
397
447
|
this.history.push({ role: 'user', content: observation });
|
|
398
448
|
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
449
|
+
const failures = [];
|
|
450
|
+
for (const provider of this.providerOrder) {
|
|
451
|
+
this.provider = SUPPORTED_CODE_PROVIDERS.includes(provider) ? provider : 'gemini';
|
|
452
|
+
try {
|
|
453
|
+
let responseText = '';
|
|
454
|
+
if (this.provider === 'anthropic') {
|
|
455
|
+
responseText = await this._callAnthropic();
|
|
456
|
+
} else if (this.provider === 'openai' || this.provider === 'local_openai') {
|
|
457
|
+
responseText = await this._callOpenAI();
|
|
458
|
+
} else {
|
|
459
|
+
responseText = await this._callGemini();
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
this.history.push({ role: 'assistant', content: responseText });
|
|
463
|
+
this.lastSuccessfulProvider = this.provider;
|
|
464
|
+
return responseText;
|
|
465
|
+
} catch (error) {
|
|
466
|
+
const message = error.message || error.code || 'unknown error';
|
|
467
|
+
failures.push(`${this.provider}: ${message}`);
|
|
468
|
+
if (process.env.MINT_DEBUG === '1') {
|
|
469
|
+
console.error(`[Code Agent Fallback] Provider '${this.provider}' failed: ${message}`);
|
|
470
|
+
}
|
|
471
|
+
}
|
|
406
472
|
}
|
|
407
473
|
|
|
408
|
-
|
|
409
|
-
return responseText;
|
|
474
|
+
throw new Error(`All code agent providers failed. ${failures.join(' | ')}`);
|
|
410
475
|
}
|
|
411
476
|
|
|
412
477
|
async _callAnthropic() {
|
|
@@ -571,7 +636,7 @@ async function buildInitialObservation(task, workspaceRoot, history = []) {
|
|
|
571
636
|
session.summary || '(none)',
|
|
572
637
|
`Previous task: ${session.lastTask || '(none)'}`,
|
|
573
638
|
`Previous verification: ${session.lastVerification || '(none)'}`,
|
|
574
|
-
'
|
|
639
|
+
'If the task is conversational or trivial, finish directly without inspecting the workspace. For code/workspace tasks, inspect before making edits.'
|
|
575
640
|
].join('\n');
|
|
576
641
|
}
|
|
577
642
|
|
|
@@ -586,8 +651,10 @@ async function executeCodeTask(task, options = {}) {
|
|
|
586
651
|
? options.askUser
|
|
587
652
|
: async (q) => `User didn't answer: ${q}`;
|
|
588
653
|
const config = readConfig();
|
|
589
|
-
const
|
|
590
|
-
const
|
|
654
|
+
const availableProviders = getAvailableProviders(config);
|
|
655
|
+
const providerOrder = getSupportedCodeProviderOrder(config, availableProviders, options.provider);
|
|
656
|
+
const provider = providerOrder[0];
|
|
657
|
+
const client = new UnifiedAgentClient(provider, config, providerOrder);
|
|
591
658
|
|
|
592
659
|
let observation = await buildInitialObservation(task, workspaceRoot, history);
|
|
593
660
|
|
|
@@ -627,7 +694,7 @@ async function executeCodeTask(task, options = {}) {
|
|
|
627
694
|
try {
|
|
628
695
|
switch (action) {
|
|
629
696
|
case 'web_search':
|
|
630
|
-
toolResult = await webSearch(input.query);
|
|
697
|
+
toolResult = await webSearch(input.query, onProgress);
|
|
631
698
|
break;
|
|
632
699
|
case 'list_files':
|
|
633
700
|
toolResult = await listFiles(workspaceRoot, input.path || '.');
|
|
@@ -640,6 +707,13 @@ async function executeCodeTask(task, options = {}) {
|
|
|
640
707
|
break;
|
|
641
708
|
case 'find_path':
|
|
642
709
|
toolResult = await findPaths(workspaceRoot, input.query, input.type);
|
|
710
|
+
if (input.openAfter === true) {
|
|
711
|
+
const result = JSON.parse(toolResult);
|
|
712
|
+
if (result.success && result.matches.length === 1) {
|
|
713
|
+
await executeAction({ type: 'open_folder', target: result.matches[0].path });
|
|
714
|
+
toolResult = `Found and opened: ${result.matches[0].path}`;
|
|
715
|
+
}
|
|
716
|
+
}
|
|
643
717
|
break;
|
|
644
718
|
case 'run_shell': {
|
|
645
719
|
const approved = await requestApproval({
|
|
@@ -651,6 +725,12 @@ async function executeCodeTask(task, options = {}) {
|
|
|
651
725
|
toolResult = `User denied shell command: ${input.command}`;
|
|
652
726
|
break;
|
|
653
727
|
}
|
|
728
|
+
safetyManager.appendActionLog({
|
|
729
|
+
source: 'code_agent',
|
|
730
|
+
action: 'run_shell',
|
|
731
|
+
command: input.command,
|
|
732
|
+
approved
|
|
733
|
+
});
|
|
654
734
|
toolResult = await runShell(workspaceRoot, input.command);
|
|
655
735
|
break;
|
|
656
736
|
}
|
|
@@ -665,6 +745,12 @@ async function executeCodeTask(task, options = {}) {
|
|
|
665
745
|
toolResult = `User denied patch for ${patchInput.path}`;
|
|
666
746
|
break;
|
|
667
747
|
}
|
|
748
|
+
safetyManager.appendActionLog({
|
|
749
|
+
source: 'code_agent',
|
|
750
|
+
action: 'apply_patch',
|
|
751
|
+
path: patchInput.path,
|
|
752
|
+
approved
|
|
753
|
+
});
|
|
668
754
|
toolResult = applyPatch(workspaceRoot, patchInput);
|
|
669
755
|
break;
|
|
670
756
|
}
|
|
@@ -678,6 +764,12 @@ async function executeCodeTask(task, options = {}) {
|
|
|
678
764
|
toolResult = `User denied full file write for ${input.path}`;
|
|
679
765
|
break;
|
|
680
766
|
}
|
|
767
|
+
safetyManager.appendActionLog({
|
|
768
|
+
source: 'code_agent',
|
|
769
|
+
action: 'write_file',
|
|
770
|
+
path: input.path,
|
|
771
|
+
approved
|
|
772
|
+
});
|
|
681
773
|
toolResult = writeFile(workspaceRoot, input.path, input.content);
|
|
682
774
|
break;
|
|
683
775
|
}
|
|
@@ -696,7 +788,7 @@ async function executeCodeTask(task, options = {}) {
|
|
|
696
788
|
// Delegate to existing automation logic
|
|
697
789
|
toolResult = await executeAction({
|
|
698
790
|
type: action,
|
|
699
|
-
target: input.target
|
|
791
|
+
target: input.target || input.path || input.query // Handle all possible input fields
|
|
700
792
|
});
|
|
701
793
|
break;
|
|
702
794
|
} default:
|
|
@@ -718,8 +810,7 @@ async function executeCodeTask(task, options = {}) {
|
|
|
718
810
|
step,
|
|
719
811
|
phase: 'finished',
|
|
720
812
|
action,
|
|
721
|
-
target: (input.path || input.command || input.query || '') + resultSummary
|
|
722
|
-
thought: decision.thought
|
|
813
|
+
target: (input.path || input.command || input.query || '') + resultSummary
|
|
723
814
|
});
|
|
724
815
|
|
|
725
816
|
// Format tool result to be more readable and structured for the agent
|
|
@@ -767,10 +858,15 @@ async function executeCodeTask(task, options = {}) {
|
|
|
767
858
|
}
|
|
768
859
|
|
|
769
860
|
if (finalSummary) {
|
|
861
|
+
const answeredProvider = client.lastSuccessfulProvider || client.provider || provider;
|
|
770
862
|
return {
|
|
771
863
|
summary: finalSummary,
|
|
772
864
|
verification: finalVerification,
|
|
773
|
-
steps: executedSteps
|
|
865
|
+
steps: executedSteps,
|
|
866
|
+
providerInfo: {
|
|
867
|
+
provider: answeredProvider,
|
|
868
|
+
model: getCodeProviderModel(answeredProvider, config)
|
|
869
|
+
}
|
|
774
870
|
};
|
|
775
871
|
}
|
|
776
872
|
|
|
@@ -780,10 +876,15 @@ async function executeCodeTask(task, options = {}) {
|
|
|
780
876
|
lastVerification: 'Agent limit reached before explicit completion.'
|
|
781
877
|
});
|
|
782
878
|
|
|
879
|
+
const answeredProvider = client.lastSuccessfulProvider || client.provider || provider;
|
|
783
880
|
return {
|
|
784
881
|
summary: 'Stopped after reaching the maximum number of agent steps.',
|
|
785
882
|
verification: 'Agent limit reached before explicit completion.',
|
|
786
|
-
steps: executedSteps || MAX_AGENT_STEPS
|
|
883
|
+
steps: executedSteps || MAX_AGENT_STEPS,
|
|
884
|
+
providerInfo: {
|
|
885
|
+
provider: answeredProvider,
|
|
886
|
+
model: getCodeProviderModel(answeredProvider, config)
|
|
887
|
+
}
|
|
787
888
|
};
|
|
788
889
|
}
|
|
789
890
|
|
|
@@ -792,6 +893,7 @@ module.exports = {
|
|
|
792
893
|
_helpers: {
|
|
793
894
|
extractJson,
|
|
794
895
|
selectSupportedCodeProvider,
|
|
896
|
+
getSupportedCodeProviderOrder,
|
|
795
897
|
findPaths,
|
|
796
898
|
listFiles,
|
|
797
899
|
searchCode,
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
const http = require('http');
|
|
2
|
+
const { execFile } = require('child_process');
|
|
3
|
+
const crypto = require('crypto');
|
|
4
|
+
const axios = require('axios');
|
|
5
|
+
const { readConfig, writeConfig } = require('../System/config_manager');
|
|
6
|
+
|
|
7
|
+
const TOKEN_URL = 'https://oauth2.googleapis.com/token';
|
|
8
|
+
const AUTH_URL = 'https://accounts.google.com/o/oauth2/v2/auth';
|
|
9
|
+
const DEFAULT_SCOPES = [
|
|
10
|
+
'https://www.googleapis.com/auth/gmail.readonly',
|
|
11
|
+
'https://www.googleapis.com/auth/gmail.compose'
|
|
12
|
+
];
|
|
13
|
+
|
|
14
|
+
function buildRedirectUri(port) {
|
|
15
|
+
return `http://127.0.0.1:${port}/oauth2callback`;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function buildAuthUrl({ clientId, redirectUri, state, scopes = DEFAULT_SCOPES }) {
|
|
19
|
+
const params = new URLSearchParams({
|
|
20
|
+
client_id: clientId,
|
|
21
|
+
redirect_uri: redirectUri,
|
|
22
|
+
response_type: 'code',
|
|
23
|
+
scope: scopes.join(' '),
|
|
24
|
+
access_type: 'offline',
|
|
25
|
+
prompt: 'consent',
|
|
26
|
+
state
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
return `${AUTH_URL}?${params.toString()}`;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function openBrowser(url) {
|
|
33
|
+
const command = process.platform === 'darwin'
|
|
34
|
+
? 'open'
|
|
35
|
+
: process.platform === 'win32'
|
|
36
|
+
? 'cmd'
|
|
37
|
+
: 'xdg-open';
|
|
38
|
+
const args = process.platform === 'win32' ? ['/c', 'start', '', url] : [url];
|
|
39
|
+
|
|
40
|
+
return new Promise((resolve, reject) => {
|
|
41
|
+
execFile(command, args, (error) => {
|
|
42
|
+
if (error) {
|
|
43
|
+
reject(error);
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
resolve();
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
async function exchangeCodeForToken({ clientId, clientSecret, code, redirectUri }) {
|
|
52
|
+
const params = new URLSearchParams({
|
|
53
|
+
client_id: clientId,
|
|
54
|
+
client_secret: clientSecret,
|
|
55
|
+
code,
|
|
56
|
+
redirect_uri: redirectUri,
|
|
57
|
+
grant_type: 'authorization_code'
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
const response = await axios.post(TOKEN_URL, params.toString(), {
|
|
61
|
+
headers: { 'Content-Type': 'application/x-www-form-urlencoded' }
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
return response.data;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function waitForOAuthCode({ port = 0, state, timeoutMs = 180000 }) {
|
|
68
|
+
return new Promise((resolve, reject) => {
|
|
69
|
+
let settled = false;
|
|
70
|
+
let timer = null;
|
|
71
|
+
|
|
72
|
+
const finish = (error, value) => {
|
|
73
|
+
if (settled) return;
|
|
74
|
+
settled = true;
|
|
75
|
+
if (timer) clearTimeout(timer);
|
|
76
|
+
server.close(() => {
|
|
77
|
+
if (error) reject(error);
|
|
78
|
+
else resolve(value);
|
|
79
|
+
});
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
const server = http.createServer((req, res) => {
|
|
83
|
+
try {
|
|
84
|
+
const url = new URL(req.url, 'http://127.0.0.1');
|
|
85
|
+
if (url.pathname !== '/oauth2callback') {
|
|
86
|
+
res.writeHead(404, { 'Content-Type': 'text/plain' });
|
|
87
|
+
res.end('Not found');
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const returnedState = url.searchParams.get('state');
|
|
92
|
+
const error = url.searchParams.get('error');
|
|
93
|
+
const code = url.searchParams.get('code');
|
|
94
|
+
|
|
95
|
+
if (error) {
|
|
96
|
+
res.writeHead(400, { 'Content-Type': 'text/plain' });
|
|
97
|
+
res.end(`Gmail authorization failed: ${error}`);
|
|
98
|
+
finish(new Error(`Gmail authorization failed: ${error}`));
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (!code || returnedState !== state) {
|
|
103
|
+
res.writeHead(400, { 'Content-Type': 'text/plain' });
|
|
104
|
+
res.end('Invalid Gmail authorization response.');
|
|
105
|
+
finish(new Error('Invalid Gmail authorization response.'));
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
|
|
110
|
+
res.end('<h1>Gmail connected</h1><p>You can close this window and return to Mint.</p>');
|
|
111
|
+
finish(null, code);
|
|
112
|
+
} catch (err) {
|
|
113
|
+
res.writeHead(500, { 'Content-Type': 'text/plain' });
|
|
114
|
+
res.end('Internal error.');
|
|
115
|
+
finish(err);
|
|
116
|
+
}
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
server.on('error', finish);
|
|
120
|
+
server.listen(port, '127.0.0.1', () => {
|
|
121
|
+
timer = setTimeout(() => {
|
|
122
|
+
finish(new Error('Timed out waiting for Gmail authorization callback.'));
|
|
123
|
+
}, timeoutMs);
|
|
124
|
+
});
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
async function runGmailAuth(options = {}) {
|
|
129
|
+
const logger = options.logger || console;
|
|
130
|
+
const config = options.readConfig ? options.readConfig() : readConfig();
|
|
131
|
+
const clientId = (config.gmailClientId || '').trim();
|
|
132
|
+
const clientSecret = (config.gmailClientSecret || '').trim();
|
|
133
|
+
const userId = (config.gmailUserId || 'me').trim() || 'me';
|
|
134
|
+
|
|
135
|
+
if (!clientId || !clientSecret) {
|
|
136
|
+
throw new Error('Missing Gmail OAuth Client ID or Client Secret. Run `mint onboard` and fill Gmail API credentials first.');
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const state = crypto.randomBytes(16).toString('hex');
|
|
140
|
+
const actualPort = options.getAuthorizationCode ? Number(options.port || 8787) : await reserveLocalPort(Number(options.port || 0));
|
|
141
|
+
const redirectUri = buildRedirectUri(actualPort);
|
|
142
|
+
const codePromise = options.getAuthorizationCode
|
|
143
|
+
? null
|
|
144
|
+
: waitForOAuthCode({
|
|
145
|
+
port: actualPort,
|
|
146
|
+
state,
|
|
147
|
+
timeoutMs: options.timeoutMs || 180000
|
|
148
|
+
});
|
|
149
|
+
const authUrl = buildAuthUrl({ clientId, redirectUri, state, scopes: options.scopes || DEFAULT_SCOPES });
|
|
150
|
+
|
|
151
|
+
logger.log(`Open this Google OAuth consent link for Gmail (${userId}):\n${authUrl}\n`);
|
|
152
|
+
|
|
153
|
+
if (options.openBrowser !== false) {
|
|
154
|
+
const browserOpener = options.openBrowser || openBrowser;
|
|
155
|
+
await browserOpener(authUrl);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const code = options.getAuthorizationCode
|
|
159
|
+
? await options.getAuthorizationCode({ authUrl, state, redirectUri })
|
|
160
|
+
: await codePromise;
|
|
161
|
+
const token = await exchangeCodeForToken({
|
|
162
|
+
clientId,
|
|
163
|
+
clientSecret,
|
|
164
|
+
code,
|
|
165
|
+
redirectUri
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
if (!token.refresh_token) {
|
|
169
|
+
throw new Error('Google did not return a refresh token. Re-run `mint gmail auth`; the flow uses prompt=consent to request one.');
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const nextConfig = {
|
|
173
|
+
...config,
|
|
174
|
+
gmailRefreshToken: token.refresh_token,
|
|
175
|
+
gmailUserId: userId,
|
|
176
|
+
pluginGmailEnabled: true
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
const writeResult = options.writeConfig ? options.writeConfig(nextConfig) : writeConfig(nextConfig);
|
|
180
|
+
if (writeResult && writeResult.success === false) {
|
|
181
|
+
throw new Error(writeResult.message || 'Failed to save Gmail refresh token.');
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
return {
|
|
185
|
+
success: true,
|
|
186
|
+
userId,
|
|
187
|
+
scopes: options.scopes || DEFAULT_SCOPES
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function reserveLocalPort(port = 0) {
|
|
192
|
+
return new Promise((resolve, reject) => {
|
|
193
|
+
const server = http.createServer();
|
|
194
|
+
server.on('error', reject);
|
|
195
|
+
server.listen(port, '127.0.0.1', () => {
|
|
196
|
+
const actualPort = server.address().port;
|
|
197
|
+
server.close(() => resolve(actualPort));
|
|
198
|
+
});
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
module.exports = {
|
|
203
|
+
DEFAULT_SCOPES,
|
|
204
|
+
buildRedirectUri,
|
|
205
|
+
buildAuthUrl,
|
|
206
|
+
exchangeCodeForToken,
|
|
207
|
+
waitForOAuthCode,
|
|
208
|
+
reserveLocalPort,
|
|
209
|
+
runGmailAuth
|
|
210
|
+
};
|
package/src/CLI/list_features.js
CHANGED
|
@@ -22,8 +22,10 @@ function displayFeatures() {
|
|
|
22
22
|
const commands = [
|
|
23
23
|
{ cmd: 'mint', desc: 'Start interactive chat session (Default)' },
|
|
24
24
|
{ cmd: 'mint code "<task>"', desc: 'Run workspace-aware coding agent in current directory' },
|
|
25
|
+
{ cmd: 'mint gmail auth', desc: 'Connect Gmail OAuth and save refresh token' },
|
|
25
26
|
{ cmd: 'mint mcp', desc: 'Manage Model Context Protocol (MCP) servers' },
|
|
26
27
|
{ cmd: 'mint task "<task>"', desc: 'Queue an autonomous task for the background agent' },
|
|
28
|
+
{ cmd: 'mint update', desc: 'Check for and install the latest Mint CLI version' },
|
|
27
29
|
{ cmd: 'mint onboard', desc: 'Run setup wizard (API Key, Model, Daemon)' },
|
|
28
30
|
{ cmd: 'mint agent', desc: 'Run Mint as a background agent (Headless)' },
|
|
29
31
|
{ cmd: 'mint list', desc: 'Show this features & commands list' }
|