@pheem49/mint 1.4.2 → 1.5.1

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 (97) hide show
  1. package/GUIDE_TH.md +113 -0
  2. package/README.md +267 -78
  3. package/assets/CLI_Screen.png +0 -0
  4. package/main.js +76 -890
  5. package/mint-cli-logic.js +3 -107
  6. package/mint-cli.js +594 -29
  7. package/models/Shiroko_Model/Shiroko/Shiroko_Core/72d86db84cfa9730b894c241fd24c0db.png +0 -0
  8. package/models/Shiroko_Model/Shiroko/Shiroko_Core/items_pinned_to_model.json +14 -0
  9. package/models/Shiroko_Model/Shiroko/Shiroko_Core//345/221/206/347/214/253.exp3.json +10 -0
  10. package/models/Shiroko_Model/Shiroko/Shiroko_Core//345/221/206/347/214/253/347/234/274/347/217/240/346/221/207/346/231/203.exp3.json +15 -0
  11. package/models/Shiroko_Model/Shiroko/Shiroko_Core//345/233/264/350/243/231.exp3.json +10 -0
  12. package/models/Shiroko_Model/Shiroko/Shiroko_Core//346/213/215/347/205/247.exp3.json +50 -0
  13. package/models/Shiroko_Model/Shiroko/Shiroko_Core//346/213/277/347/254/224.exp3.json +10 -0
  14. package/models/Shiroko_Model/Shiroko/Shiroko_Core//347/202/271/344/270/200/344/270/213.exp3.json +10 -0
  15. package/models/Shiroko_Model/Shiroko/Shiroko_Core//347/214/253/345/222/252/346/273/244/351/225/234.exp3.json +10 -0
  16. package/models/Shiroko_Model/Shiroko/Shiroko_Core//347/234/274/351/225/234.exp3.json +10 -0
  17. package/models/Shiroko_Model/Shiroko/Shiroko_Core//351/235/242/351/245/2740.4096/texture_00.png +0 -0
  18. package/models/Shiroko_Model/Shiroko/Shiroko_Core//351/235/242/351/245/2740.4096/texture_01.png +0 -0
  19. package/models/Shiroko_Model/Shiroko/Shiroko_Core//351/235/242/351/245/2740.4096/texture_02.png +0 -0
  20. package/models/Shiroko_Model/Shiroko/Shiroko_Core//351/235/242/351/245/2740.4096/texture_03.png +0 -0
  21. package/models/Shiroko_Model/Shiroko/Shiroko_Core//351/235/242/351/245/2740.cdi3.json +1498 -0
  22. package/models/Shiroko_Model/Shiroko/Shiroko_Core//351/235/242/351/245/2740.moc3 +0 -0
  23. package/models/Shiroko_Model/Shiroko/Shiroko_Core//351/235/242/351/245/2740.model3.json +47 -0
  24. package/models/Shiroko_Model/Shiroko/Shiroko_Core//351/235/242/351/245/2740.physics3.json +6658 -0
  25. package/models/Shiroko_Model/Shiroko/Shiroko_Core//351/235/242/351/245/2740.vtube.json +1299 -0
  26. package/models/Shiroko_Model/Shiroko//342/232/241/351/253/230/344/272/256/342/232/241/344/275/277/347/224/250/346/225/231/347/250/213/344/270/216/346/263/250/346/204/217/344/272/213/351/241/271.txt +23 -0
  27. package/package.json +37 -4
  28. package/src/AI_Brain/Gemini_API.js +223 -65
  29. package/src/AI_Brain/autonomous_brain.js +11 -0
  30. package/src/AI_Brain/behavior_memory.js +26 -5
  31. package/src/AI_Brain/headless_agent.js +4 -0
  32. package/src/AI_Brain/knowledge_base.js +61 -8
  33. package/src/AI_Brain/memory_store.js +354 -10
  34. package/src/Automation_Layer/file_operations.js +1 -1
  35. package/src/CLI/chat_router.js +20 -7
  36. package/src/CLI/chat_ui.js +596 -825
  37. package/src/CLI/code_agent.js +347 -56
  38. package/src/CLI/gmail_auth.js +210 -0
  39. package/src/CLI/image_input.js +90 -0
  40. package/src/CLI/list_features.js +2 -0
  41. package/src/CLI/onboarding.js +364 -55
  42. package/src/CLI/updater.js +210 -0
  43. package/src/Channels/brave_search_bridge.js +35 -0
  44. package/src/Channels/discord_bridge.js +68 -0
  45. package/src/Channels/google_search_bridge.js +38 -0
  46. package/src/Channels/line_bridge.js +60 -0
  47. package/src/Channels/slack_bridge.js +53 -0
  48. package/src/Channels/telegram_bridge.js +49 -0
  49. package/src/Channels/whatsapp_bridge.js +55 -0
  50. package/src/Command_Parser/parser.js +12 -1
  51. package/src/Plugins/gmail.js +251 -0
  52. package/src/Plugins/google_calendar.js +245 -19
  53. package/src/Plugins/notion.js +256 -0
  54. package/src/System/action_executor.js +178 -0
  55. package/src/System/bridge_manager.js +76 -0
  56. package/src/System/chat_history_manager.js +23 -5
  57. package/src/System/config_manager.js +71 -7
  58. package/src/System/custom_workflows.js +31 -2
  59. package/src/System/google_tts_urls.js +51 -0
  60. package/src/System/granular_automation.js +122 -53
  61. package/src/System/ipc_handlers.js +238 -0
  62. package/src/System/proactive_loop.js +153 -0
  63. package/src/System/safety_manager.js +273 -0
  64. package/src/System/sandbox_runner.js +182 -0
  65. package/src/System/screen_capture.js +175 -0
  66. package/src/System/system_automation.js +127 -81
  67. package/src/System/system_info.js +70 -0
  68. package/src/System/task_manager.js +15 -5
  69. package/src/System/tool_registry.js +280 -0
  70. package/src/System/window_manager.js +212 -0
  71. package/src/UI/live2d_manager.js +368 -0
  72. package/src/UI/renderer.js +208 -24
  73. package/src/UI/settings.html +24 -0
  74. package/src/UI/settings.js +14 -4
  75. package/src/UI/styles.css +466 -32
  76. package/.codex +0 -0
  77. package/docs/assets/Agent_Mint.png +0 -0
  78. package/docs/assets/CLI_Screen.png +0 -0
  79. package/docs/assets/Settings.png +0 -0
  80. package/docs/assets/icon.png +0 -0
  81. package/docs/index.html +0 -132
  82. package/docs/style.css +0 -579
  83. package/index.html +0 -16
  84. package/src/UI/index.html +0 -126
  85. package/tech_news.txt +0 -3
  86. package/test_knowledge.txt +0 -3
  87. package/tests/agent_orchestrator.test.js +0 -41
  88. package/tests/chat_router.test.js +0 -42
  89. package/tests/code_agent.test.js +0 -69
  90. package/tests/config_manager.test.js +0 -141
  91. package/tests/docker.test.js +0 -46
  92. package/tests/file_operations.test.js +0 -57
  93. package/tests/memory_store.test.js +0 -185
  94. package/tests/provider_routing.test.js +0 -67
  95. package/tests/spotify.test.js +0 -201
  96. package/tests/system_monitor.test.js +0 -37
  97. package/tests/workspace_manager.test.js +0 -56
@@ -2,6 +2,36 @@ const fs = require('fs');
2
2
  const path = require('path');
3
3
  const { readConfig, writeConfig } = require('../System/config_manager');
4
4
  const { installDaemon } = require('../System/daemon_manager');
5
+ const { runGmailAuth } = require('./gmail_auth');
6
+
7
+ const CUSTOM_MODEL_VALUE = '__custom_model__';
8
+ const ANTHROPIC_MODEL_CHOICES = [
9
+ 'claude-3-5-sonnet-latest',
10
+ 'claude-3-opus-latest',
11
+ 'claude-3-5-haiku-latest'
12
+ ];
13
+ const OPENAI_MODEL_CHOICES = [
14
+ 'gpt-4o',
15
+ 'gpt-4o-mini',
16
+ 'o1-preview',
17
+ 'o1-mini'
18
+ ];
19
+
20
+ function buildModelChoices(models, currentModel) {
21
+ const choices = models.map(model => ({ name: model, value: model }));
22
+ if (currentModel && !models.includes(currentModel)) {
23
+ choices.push({ name: `${currentModel} (current)`, value: currentModel });
24
+ }
25
+ choices.push({ name: 'Custom...', value: CUSTOM_MODEL_VALUE });
26
+ return choices;
27
+ }
28
+
29
+ function resolveCustomModelSelection(answers, modelKey, customKey, fallbackModel) {
30
+ if (answers[modelKey] !== CUSTOM_MODEL_VALUE) return;
31
+ const customModel = typeof answers[customKey] === 'string' ? answers[customKey].trim() : '';
32
+ answers[modelKey] = customModel || fallbackModel;
33
+ delete answers[customKey];
34
+ }
5
35
 
6
36
  /**
7
37
  * Onboarding Wizard for Mint CLI
@@ -12,84 +42,363 @@ async function runOnboarding(options = {}) {
12
42
 
13
43
  console.log('\nWelcome to Mint Onboarding! Let\'s get you set up.\n');
14
44
 
15
- const config = readConfig();
45
+ let config = readConfig();
16
46
 
17
- const questions = [
47
+ // 1. Basic Setup (Gemini is mandatory for core features)
48
+ const basicAnswers = await inquirer.prompt([
18
49
  {
19
50
  type: 'input',
20
51
  name: 'apiKey',
21
- message: 'Enter your Google Gemini API Key (Required for basic features):',
52
+ message: 'Enter your Google Gemini API Key:',
22
53
  default: config.apiKey || undefined,
23
54
  validate: (input) => input.trim().length > 0 ? true : 'API Key is required.'
24
55
  },
25
56
  {
26
57
  type: 'list',
27
- name: 'geminiModelChoice',
28
- message: 'Select the primary Gemini model to use:',
58
+ name: 'geminiModel',
59
+ message: 'Select primary Gemini model:',
29
60
  choices: [
30
61
  'gemini-2.5-flash',
31
- 'gemini-2.0-pro-exp-02-05',
32
62
  'gemini-3.1-flash-lite-preview',
33
- 'gemini-3.1-flash-lite',
34
- 'Custom model name'
63
+ 'gemini-1.5-pro'
35
64
  ],
36
65
  default: config.geminiModel || 'gemini-2.5-flash'
37
- },
38
- {
39
- type: 'input',
40
- name: 'customGeminiModel',
41
- message: 'Enter your custom Gemini model name:',
42
- when: (answers) => answers.geminiModelChoice === 'Custom model name',
43
- validate: (input) => input.trim().length > 0 ? true : 'Please enter a valid model name.'
44
- },
45
- {
46
- type: 'input',
47
- name: 'anthropicApiKey',
48
- message: 'Enter your Anthropic API Key (Optional, press Enter to skip):',
49
- default: config.anthropicApiKey || ''
50
- },
51
- {
52
- type: 'input',
53
- name: 'openaiApiKey',
54
- message: 'Enter your OpenAI API Key (Optional, press Enter to skip):',
55
- default: config.openaiApiKey || ''
56
- },
57
- {
58
- type: 'input',
59
- name: 'hfApiKey',
60
- message: 'Enter your Hugging Face API Key (Optional, press Enter to skip):',
61
- default: config.hfApiKey || ''
62
- },
63
- {
64
- type: 'input',
65
- name: 'localApiBaseUrl',
66
- message: 'Enter your Local AI (LM Studio/OpenAI Compatible) Base URL (Optional, press Enter to skip):',
67
- default: config.localApiBaseUrl || ''
68
- },
66
+ }
67
+ ]);
68
+
69
+ config = { ...config, ...basicAnswers };
70
+
71
+ // 2. Interactive Channel/Provider Selection (QuickStart Style)
72
+ const { selections } = await inquirer.prompt([
69
73
  {
70
- type: 'input',
71
- name: 'localModelName',
72
- message: 'Enter your Local Model Name (Optional, press Enter to skip):',
73
- default: config.localModelName || ''
74
+ type: 'checkbox',
75
+ name: 'selections',
76
+ message: 'Select channels/providers to configure (QuickStart):',
77
+ pageSize: 20,
78
+ choices: [
79
+ { name: 'Telegram (Bot API)', value: 'telegram', checked: config.enableTelegramBridge },
80
+ { name: 'WhatsApp (QR link)', value: 'whatsapp', checked: config.enableWhatsappBridge },
81
+ { name: 'Discord (Bot API)', value: 'discord', checked: config.enableDiscordBridge },
82
+ { name: 'Slack (Socket Mode)', value: 'slack', checked: config.enableSlackBridge },
83
+ { name: 'LINE (Messaging API)', value: 'line', checked: config.enableLineBridge },
84
+ { name: 'Google Calendar API', value: 'google_calendar', checked: config.pluginCalendarEnabled },
85
+ { name: 'Gmail API', value: 'gmail', checked: config.pluginGmailEnabled },
86
+ { name: 'Notion API', value: 'notion', checked: config.pluginNotionEnabled },
87
+ new inquirer.Separator(),
88
+ { name: 'Anthropic (Claude)', value: 'anthropic', checked: !!config.anthropicApiKey },
89
+ { name: 'OpenAI (GPT-4o)', value: 'openai', checked: !!config.openaiApiKey },
90
+ { name: 'Hugging Face', value: 'hf', checked: !!config.hfApiKey },
91
+ { name: 'Local AI (LM Studio/Ollama)', value: 'local', checked: !!(config.localApiBaseUrl && config.localApiBaseUrl.length > 0) },
92
+ new inquirer.Separator(),
93
+ { name: 'Google Search API', value: 'google_search', checked: !!config.googleSearchApiKey },
94
+ { name: 'Brave Search API', value: 'brave_search', checked: !!config.braveSearchApiKey },
95
+ new inquirer.Separator(),
96
+ { name: 'Skip for now', value: 'skip' }
97
+ ]
74
98
  }
75
- ];
99
+ ]);
76
100
 
77
- const answers = await inquirer.prompt(questions);
101
+ // 3. Configure selected items
102
+ const dynamicQuestions = [];
78
103
 
79
- // Resolve custom gemini model if selected
80
- const geminiModel = answers.geminiModelChoice === 'Custom model name'
81
- ? answers.customGeminiModel
82
- : answers.geminiModelChoice;
104
+ // Reset enabled flags if we are not skipping
105
+ if (!selections.includes('skip')) {
106
+ config.enableTelegramBridge = selections.includes('telegram');
107
+ config.enableWhatsappBridge = selections.includes('whatsapp');
108
+ config.enableDiscordBridge = selections.includes('discord');
109
+ config.enableSlackBridge = selections.includes('slack');
110
+ config.enableLineBridge = selections.includes('line');
111
+ }
112
+
113
+ // If "Skip for now" is selected or nothing is selected, we move to save
114
+ if (selections.includes('skip')) {
115
+ console.log('\n⏩ Skipping optional configuration...');
116
+ } else {
117
+ if (selections.includes('google_search')) {
118
+ dynamicQuestions.push({
119
+ type: 'input',
120
+ name: 'googleSearchApiKey',
121
+ message: 'Enter Google Search API Key:',
122
+ default: config.googleSearchApiKey
123
+ });
124
+ dynamicQuestions.push({
125
+ type: 'input',
126
+ name: 'googleSearchCx',
127
+ message: 'Enter Google Search CX (Engine ID):',
128
+ default: config.googleSearchCx
129
+ });
130
+ }
83
131
 
84
- // Remove temporary choice fields before saving
85
- delete answers.geminiModelChoice;
86
- delete answers.customGeminiModel;
132
+ if (selections.includes('brave_search')) {
133
+ dynamicQuestions.push({
134
+ type: 'input',
135
+ name: 'braveSearchApiKey',
136
+ message: 'Enter Brave Search API Key:',
137
+ default: config.braveSearchApiKey
138
+ });
139
+ }
140
+ if (selections.includes('discord')) {
141
+ dynamicQuestions.push({
142
+ type: 'input',
143
+ name: 'discordBotToken',
144
+ message: 'Enter Discord Bot Token:',
145
+ default: config.discordBotToken
146
+ });
147
+ }
148
+
149
+ if (selections.includes('telegram')) {
150
+ dynamicQuestions.push({
151
+ type: 'input',
152
+ name: 'telegramBotToken',
153
+ message: 'Enter Telegram Bot Token:',
154
+ default: config.telegramBotToken
155
+ });
156
+ }
157
+
158
+ if (selections.includes('slack')) {
159
+ dynamicQuestions.push({
160
+ type: 'input',
161
+ name: 'slackBotToken',
162
+ message: 'Enter Slack Bot Token (xoxb-...):',
163
+ default: config.slackBotToken
164
+ });
165
+ dynamicQuestions.push({
166
+ type: 'input',
167
+ name: 'slackAppToken',
168
+ message: 'Enter Slack App Token (xapp-...):',
169
+ default: config.slackAppToken
170
+ });
171
+ }
172
+
173
+ if (selections.includes('line')) {
174
+ dynamicQuestions.push({
175
+ type: 'input',
176
+ name: 'lineChannelAccessToken',
177
+ message: 'Enter LINE Channel Access Token:',
178
+ default: config.lineChannelAccessToken
179
+ });
180
+ dynamicQuestions.push({
181
+ type: 'input',
182
+ name: 'lineChannelSecret',
183
+ message: 'Enter LINE Channel Secret:',
184
+ default: config.lineChannelSecret
185
+ });
186
+ dynamicQuestions.push({
187
+ type: 'number',
188
+ name: 'lineWebhookPort',
189
+ message: 'Enter LINE Webhook Port (Local):',
190
+ default: config.lineWebhookPort || 3000
191
+ });
192
+ }
193
+
194
+ if (selections.includes('google_calendar')) {
195
+ dynamicQuestions.push({
196
+ type: 'input',
197
+ name: 'googleCalendarClientId',
198
+ message: 'Enter Google Calendar OAuth Client ID:',
199
+ default: config.googleCalendarClientId
200
+ });
201
+ dynamicQuestions.push({
202
+ type: 'input',
203
+ name: 'googleCalendarClientSecret',
204
+ message: 'Enter Google Calendar OAuth Client Secret:',
205
+ default: config.googleCalendarClientSecret
206
+ });
207
+ dynamicQuestions.push({
208
+ type: 'input',
209
+ name: 'googleCalendarRefreshToken',
210
+ message: 'Enter Google Calendar Refresh Token:',
211
+ default: config.googleCalendarRefreshToken
212
+ });
213
+ dynamicQuestions.push({
214
+ type: 'input',
215
+ name: 'googleCalendarId',
216
+ message: 'Enter Google Calendar ID:',
217
+ default: config.googleCalendarId || 'primary'
218
+ });
219
+ config.pluginCalendarEnabled = true;
220
+ } else {
221
+ config.pluginCalendarEnabled = false;
222
+ }
223
+
224
+ if (selections.includes('gmail')) {
225
+ dynamicQuestions.push({
226
+ type: 'input',
227
+ name: 'gmailClientId',
228
+ message: 'Enter Gmail OAuth Client ID:',
229
+ default: config.gmailClientId
230
+ });
231
+ dynamicQuestions.push({
232
+ type: 'input',
233
+ name: 'gmailClientSecret',
234
+ message: 'Enter Gmail OAuth Client Secret:',
235
+ default: config.gmailClientSecret
236
+ });
237
+ dynamicQuestions.push({
238
+ type: 'input',
239
+ name: 'gmailRefreshToken',
240
+ message: 'Enter Gmail Refresh Token:',
241
+ default: config.gmailRefreshToken
242
+ });
243
+ dynamicQuestions.push({
244
+ type: 'input',
245
+ name: 'gmailUserId',
246
+ message: 'Enter Gmail User ID:',
247
+ default: config.gmailUserId || 'me'
248
+ });
249
+ config.pluginGmailEnabled = true;
250
+ } else {
251
+ config.pluginGmailEnabled = false;
252
+ }
253
+
254
+ if (selections.includes('notion')) {
255
+ dynamicQuestions.push({
256
+ type: 'input',
257
+ name: 'notionApiKey',
258
+ message: 'Enter Notion Internal Integration Secret:',
259
+ default: config.notionApiKey
260
+ });
261
+ dynamicQuestions.push({
262
+ type: 'input',
263
+ name: 'notionDatabaseId',
264
+ message: 'Enter default Notion Database ID (optional):',
265
+ default: config.notionDatabaseId
266
+ });
267
+ dynamicQuestions.push({
268
+ type: 'input',
269
+ name: 'notionPageId',
270
+ message: 'Enter default Notion Page ID (optional):',
271
+ default: config.notionPageId
272
+ });
273
+ dynamicQuestions.push({
274
+ type: 'input',
275
+ name: 'notionTitleProperty',
276
+ message: 'Enter database title property name:',
277
+ default: config.notionTitleProperty || 'Name'
278
+ });
279
+ config.pluginNotionEnabled = true;
280
+ } else {
281
+ config.pluginNotionEnabled = false;
282
+ }
283
+
284
+ if (selections.includes('anthropic')) {
285
+ dynamicQuestions.push({
286
+ type: 'input',
287
+ name: 'anthropicApiKey',
288
+ message: 'Enter Anthropic API Key:',
289
+ default: config.anthropicApiKey,
290
+ validate: (input) => input.trim().length > 0 ? true : 'Anthropic API Key is required when Anthropic is selected.'
291
+ });
292
+ dynamicQuestions.push({
293
+ type: 'list',
294
+ name: 'anthropicModel',
295
+ message: 'Select Anthropic model:',
296
+ choices: buildModelChoices(ANTHROPIC_MODEL_CHOICES, config.anthropicModel),
297
+ default: config.anthropicModel || 'claude-3-5-sonnet-latest'
298
+ });
299
+ dynamicQuestions.push({
300
+ type: 'input',
301
+ name: 'anthropicModelCustom',
302
+ message: 'Enter custom Anthropic model:',
303
+ default: config.anthropicModel || 'claude-3-5-sonnet-latest',
304
+ when: (answers) => answers.anthropicModel === CUSTOM_MODEL_VALUE,
305
+ validate: (input) => input.trim().length > 0 ? true : 'Model name is required.'
306
+ });
307
+ }
308
+
309
+ if (selections.includes('openai')) {
310
+ dynamicQuestions.push({
311
+ type: 'input',
312
+ name: 'openaiApiKey',
313
+ message: 'Enter OpenAI API Key:',
314
+ default: config.openaiApiKey,
315
+ validate: (input) => input.trim().length > 0 ? true : 'OpenAI API Key is required when OpenAI is selected.'
316
+ });
317
+ dynamicQuestions.push({
318
+ type: 'list',
319
+ name: 'openaiModel',
320
+ message: 'Select OpenAI model:',
321
+ choices: buildModelChoices(OPENAI_MODEL_CHOICES, config.openaiModel),
322
+ default: config.openaiModel || 'gpt-4o'
323
+ });
324
+ dynamicQuestions.push({
325
+ type: 'input',
326
+ name: 'openaiModelCustom',
327
+ message: 'Enter custom OpenAI model:',
328
+ default: config.openaiModel || 'gpt-4o',
329
+ when: (answers) => answers.openaiModel === CUSTOM_MODEL_VALUE,
330
+ validate: (input) => input.trim().length > 0 ? true : 'Model name is required.'
331
+ });
332
+ }
333
+
334
+ if (selections.includes('hf')) {
335
+ dynamicQuestions.push({
336
+ type: 'input',
337
+ name: 'hfApiKey',
338
+ message: 'Enter Hugging Face API Key:',
339
+ default: config.hfApiKey
340
+ });
341
+ }
342
+
343
+ if (selections.includes('local')) {
344
+ dynamicQuestions.push({
345
+ type: 'input',
346
+ name: 'localApiBaseUrl',
347
+ message: 'Enter Local AI Base URL:',
348
+ default: config.localApiBaseUrl || 'http://localhost:1234/v1'
349
+ });
350
+ dynamicQuestions.push({
351
+ type: 'input',
352
+ name: 'localModelName',
353
+ message: 'Enter Local Model Name:',
354
+ default: config.localModelName || 'local-model'
355
+ });
356
+ }
357
+ }
358
+
359
+ if (dynamicQuestions.length > 0) {
360
+ const extraAnswers = await inquirer.prompt(dynamicQuestions);
361
+ resolveCustomModelSelection(extraAnswers, 'anthropicModel', 'anthropicModelCustom', config.anthropicModel || 'claude-3-5-sonnet-latest');
362
+ resolveCustomModelSelection(extraAnswers, 'openaiModel', 'openaiModelCustom', config.openaiModel || 'gpt-4o');
363
+ config = { ...config, ...extraAnswers };
364
+
365
+ }
366
+
367
+ // Onboarding treats Gemini as the primary AI. Other providers are optional
368
+ // configured backends that become available only when credentials are set.
369
+ config.aiProvider = 'gemini';
87
370
 
88
371
  // Save configuration
89
- const newConfig = { ...config, ...answers, geminiModel };
90
- writeConfig(newConfig);
372
+ writeConfig(config);
91
373
  console.log('\n✅ Configuration saved successfully!');
92
374
 
375
+ if (!selections.includes('skip') && selections.includes('gmail') && !config.gmailRefreshToken) {
376
+ const { runGmailAuthNow } = await inquirer.prompt([
377
+ {
378
+ type: 'confirm',
379
+ name: 'runGmailAuthNow',
380
+ message: 'Gmail Refresh Token is empty. Start Gmail OAuth now?',
381
+ default: true
382
+ }
383
+ ]);
384
+
385
+ if (runGmailAuthNow) {
386
+ console.log('\n🔐 Starting Gmail OAuth. Open the link below, sign in, and approve access.');
387
+ try {
388
+ const result = await runGmailAuth({
389
+ logger: console,
390
+ openBrowser: false
391
+ });
392
+ console.log(`✅ Gmail connected for ${result.userId}. Refresh token saved.`);
393
+ } catch (err) {
394
+ console.error(`❌ Gmail OAuth failed: ${err.message}`);
395
+ console.log('You can retry later with: mint gmail auth');
396
+ }
397
+ } else {
398
+ console.log('You can connect Gmail later with: mint gmail auth');
399
+ }
400
+ }
401
+
93
402
  // Install Daemon if requested
94
403
  if (options.installDaemon) {
95
404
  console.log('\n🚀 Installing Mint Background Agent (Daemon)...');
@@ -0,0 +1,210 @@
1
+ const { execFile } = require('child_process');
2
+ const pkg = require('../../package.json');
3
+
4
+ const NPM_COMMAND = process.platform === 'win32' ? 'npm.cmd' : 'npm';
5
+ const DEFAULT_AUTO_UPDATE_INTERVAL_HOURS = 24;
6
+
7
+ function execFilePromise(command, args, options = {}) {
8
+ return new Promise((resolve, reject) => {
9
+ execFile(command, args, options, (error, stdout, stderr) => {
10
+ if (error) {
11
+ error.stdout = stdout;
12
+ error.stderr = stderr;
13
+ reject(error);
14
+ return;
15
+ }
16
+ resolve({ stdout, stderr });
17
+ });
18
+ });
19
+ }
20
+
21
+ function parseVersion(version) {
22
+ return String(version || '')
23
+ .trim()
24
+ .replace(/^v/, '')
25
+ .split('-')[0]
26
+ .split('.')
27
+ .map((part) => Number.parseInt(part, 10) || 0);
28
+ }
29
+
30
+ function compareVersions(a, b) {
31
+ const left = parseVersion(a);
32
+ const right = parseVersion(b);
33
+ const length = Math.max(left.length, right.length, 3);
34
+
35
+ for (let i = 0; i < length; i++) {
36
+ const l = left[i] || 0;
37
+ const r = right[i] || 0;
38
+ if (l > r) return 1;
39
+ if (l < r) return -1;
40
+ }
41
+ return 0;
42
+ }
43
+
44
+ function normalizeNpmVersionOutput(output) {
45
+ const trimmed = String(output || '').trim();
46
+ if (!trimmed) return '';
47
+
48
+ try {
49
+ const parsed = JSON.parse(trimmed);
50
+ if (typeof parsed === 'string') return parsed;
51
+ } catch (err) {
52
+ // npm may return plain text depending on config/version.
53
+ }
54
+
55
+ return trimmed.replace(/^['"]|['"]$/g, '');
56
+ }
57
+
58
+ function getAutoUpdateIntervalMs(config = {}) {
59
+ const hours = Number(config.autoUpdateCheckIntervalHours);
60
+ const safeHours = Number.isFinite(hours) && hours > 0
61
+ ? hours
62
+ : DEFAULT_AUTO_UPDATE_INTERVAL_HOURS;
63
+ return safeHours * 60 * 60 * 1000;
64
+ }
65
+
66
+ function shouldRunAutoUpdate(config = {}, now = Date.now()) {
67
+ if (config.enableAutoUpdate === false) return false;
68
+
69
+ const lastCheck = Date.parse(config.lastUpdateCheckAt || '');
70
+ if (!Number.isFinite(lastCheck)) return true;
71
+
72
+ return now - lastCheck >= getAutoUpdateIntervalMs(config);
73
+ }
74
+
75
+ async function getLatestVersion(packageName = pkg.name) {
76
+ const { stdout } = await execFilePromise(NPM_COMMAND, ['view', packageName, 'version', '--json'], {
77
+ maxBuffer: 1024 * 1024,
78
+ timeout: 30000
79
+ });
80
+ return normalizeNpmVersionOutput(stdout);
81
+ }
82
+
83
+ async function installLatest(packageName = pkg.name, options = {}) {
84
+ const args = ['install', '-g', `${packageName}@latest`];
85
+ if (options.dryRun) {
86
+ args.push('--dry-run');
87
+ }
88
+
89
+ return await execFilePromise(NPM_COMMAND, args, {
90
+ maxBuffer: 1024 * 1024 * 8,
91
+ timeout: 5 * 60 * 1000
92
+ });
93
+ }
94
+
95
+ function formatUpdateError(error) {
96
+ const detail = [error.stderr, error.stdout, error.message].filter(Boolean).join('\n').trim();
97
+ if (/EACCES|permission denied|Access is denied/i.test(detail)) {
98
+ return [
99
+ 'Update failed because npm does not have permission to modify the global install directory.',
100
+ `Run manually: npm install -g ${pkg.name}@latest`,
101
+ 'If your npm global packages require sudo, run that command with sudo.'
102
+ ].join('\n');
103
+ }
104
+
105
+ if (/E404|404 Not Found|not in this registry/i.test(detail)) {
106
+ return [
107
+ `Could not find ${pkg.name} on the npm registry.`,
108
+ 'Publish the package first, or update Mint from the source/release channel you installed from.'
109
+ ].join('\n');
110
+ }
111
+
112
+ return `Update failed: ${detail || 'Unknown npm error'}`;
113
+ }
114
+
115
+ async function runUpdate(options = {}) {
116
+ const currentVersion = pkg.version;
117
+ let latestVersion = '';
118
+
119
+ try {
120
+ latestVersion = await getLatestVersion(pkg.name);
121
+ } catch (error) {
122
+ return {
123
+ status: 'error',
124
+ currentVersion,
125
+ latestVersion,
126
+ message: formatUpdateError(error)
127
+ };
128
+ }
129
+
130
+ if (!latestVersion) {
131
+ return {
132
+ status: 'error',
133
+ currentVersion,
134
+ latestVersion: '',
135
+ message: 'Could not determine the latest Mint version from npm.'
136
+ };
137
+ }
138
+
139
+ const comparison = compareVersions(currentVersion, latestVersion);
140
+ if (comparison >= 0) {
141
+ return {
142
+ status: 'current',
143
+ currentVersion,
144
+ latestVersion,
145
+ message: `Mint is already up to date (${currentVersion}).`
146
+ };
147
+ }
148
+
149
+ if (options.checkOnly) {
150
+ return {
151
+ status: 'available',
152
+ currentVersion,
153
+ latestVersion,
154
+ message: `Mint ${latestVersion} is available. Current version: ${currentVersion}.`
155
+ };
156
+ }
157
+
158
+ try {
159
+ await installLatest(pkg.name, { dryRun: options.dryRun });
160
+ return {
161
+ status: options.dryRun ? 'dry-run' : 'updated',
162
+ currentVersion,
163
+ latestVersion,
164
+ message: options.dryRun
165
+ ? `Dry run complete. Mint would update from ${currentVersion} to ${latestVersion}.`
166
+ : `Mint updated from ${currentVersion} to ${latestVersion}. Restart mint to use the new version.`
167
+ };
168
+ } catch (error) {
169
+ return {
170
+ status: 'error',
171
+ currentVersion,
172
+ latestVersion,
173
+ message: formatUpdateError(error)
174
+ };
175
+ }
176
+ }
177
+
178
+ async function runStartupAutoUpdate(config, writeConfig, options = {}) {
179
+ const now = options.now || Date.now();
180
+ if (!shouldRunAutoUpdate(config, now)) {
181
+ return {
182
+ status: 'skipped',
183
+ message: 'Auto-update check skipped by cooldown.'
184
+ };
185
+ }
186
+
187
+ const result = await runUpdate({ checkOnly: false });
188
+ if (typeof writeConfig === 'function') {
189
+ writeConfig({
190
+ ...config,
191
+ lastUpdateCheckAt: new Date(now).toISOString()
192
+ });
193
+ }
194
+ return result;
195
+ }
196
+
197
+ module.exports = {
198
+ compareVersions,
199
+ getLatestVersion,
200
+ installLatest,
201
+ normalizeNpmVersionOutput,
202
+ runUpdate,
203
+ runStartupAutoUpdate,
204
+ shouldRunAutoUpdate,
205
+ _private: {
206
+ parseVersion,
207
+ formatUpdateError,
208
+ getAutoUpdateIntervalMs
209
+ }
210
+ };