@pheem49/mint 1.5.0 → 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 (78) hide show
  1. package/README.md +27 -1
  2. package/main.js +28 -14
  3. package/mint-cli-logic.js +3 -119
  4. package/mint-cli.js +497 -23
  5. package/models/Shiroko_Model/Shiroko/Shiroko_Core/72d86db84cfa9730b894c241fd24c0db.png +0 -0
  6. package/models/Shiroko_Model/Shiroko/Shiroko_Core/items_pinned_to_model.json +14 -0
  7. package/models/Shiroko_Model/Shiroko/Shiroko_Core//345/221/206/347/214/253.exp3.json +10 -0
  8. 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
  9. package/models/Shiroko_Model/Shiroko/Shiroko_Core//345/233/264/350/243/231.exp3.json +10 -0
  10. package/models/Shiroko_Model/Shiroko/Shiroko_Core//346/213/215/347/205/247.exp3.json +50 -0
  11. package/models/Shiroko_Model/Shiroko/Shiroko_Core//346/213/277/347/254/224.exp3.json +10 -0
  12. package/models/Shiroko_Model/Shiroko/Shiroko_Core//347/202/271/344/270/200/344/270/213.exp3.json +10 -0
  13. package/models/Shiroko_Model/Shiroko/Shiroko_Core//347/214/253/345/222/252/346/273/244/351/225/234.exp3.json +10 -0
  14. package/models/Shiroko_Model/Shiroko/Shiroko_Core//347/234/274/351/225/234.exp3.json +10 -0
  15. package/models/Shiroko_Model/Shiroko/Shiroko_Core//351/235/242/351/245/2740.4096/texture_00.png +0 -0
  16. package/models/Shiroko_Model/Shiroko/Shiroko_Core//351/235/242/351/245/2740.4096/texture_01.png +0 -0
  17. package/models/Shiroko_Model/Shiroko/Shiroko_Core//351/235/242/351/245/2740.4096/texture_02.png +0 -0
  18. package/models/Shiroko_Model/Shiroko/Shiroko_Core//351/235/242/351/245/2740.4096/texture_03.png +0 -0
  19. package/models/Shiroko_Model/Shiroko/Shiroko_Core//351/235/242/351/245/2740.cdi3.json +1498 -0
  20. package/models/Shiroko_Model/Shiroko/Shiroko_Core//351/235/242/351/245/2740.moc3 +0 -0
  21. package/models/Shiroko_Model/Shiroko/Shiroko_Core//351/235/242/351/245/2740.model3.json +47 -0
  22. package/models/Shiroko_Model/Shiroko/Shiroko_Core//351/235/242/351/245/2740.physics3.json +6658 -0
  23. package/models/Shiroko_Model/Shiroko/Shiroko_Core//351/235/242/351/245/2740.vtube.json +1299 -0
  24. 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
  25. package/package.json +26 -1
  26. package/src/AI_Brain/Gemini_API.js +147 -46
  27. package/src/AI_Brain/autonomous_brain.js +2 -1
  28. package/src/AI_Brain/memory_store.js +299 -3
  29. package/src/CLI/chat_router.js +18 -6
  30. package/src/CLI/chat_ui.js +396 -50
  31. package/src/CLI/code_agent.js +203 -14
  32. package/src/CLI/image_input.js +90 -0
  33. package/src/CLI/onboarding.js +72 -15
  34. package/src/CLI/updater.js +6 -4
  35. package/src/System/action_executor.js +59 -10
  36. package/src/System/config_manager.js +31 -1
  37. package/src/System/granular_automation.js +122 -53
  38. package/src/System/proactive_loop.js +19 -3
  39. package/src/System/safety_manager.js +108 -0
  40. package/src/System/sandbox_runner.js +182 -0
  41. package/src/System/system_automation.js +127 -81
  42. package/src/System/system_info.js +70 -0
  43. package/src/System/tool_registry.js +280 -0
  44. package/src/System/window_manager.js +4 -2
  45. package/src/UI/live2d_manager.js +368 -0
  46. package/src/UI/renderer.js +176 -18
  47. package/src/UI/styles.css +452 -31
  48. package/.codex +0 -0
  49. package/docs/assets/Agent_Mint.png +0 -0
  50. package/docs/assets/CLI_Screen.png +0 -0
  51. package/docs/assets/Settings.png +0 -0
  52. package/docs/assets/icon.png +0 -0
  53. package/docs/guide.html +0 -632
  54. package/docs/index.html +0 -133
  55. package/docs/style.css +0 -579
  56. package/index.html +0 -16
  57. package/src/UI/index.html +0 -126
  58. package/tech_news.txt +0 -3
  59. package/test_knowledge.txt +0 -3
  60. package/tests/action_executor_safety.test.js +0 -67
  61. package/tests/agent_orchestrator.test.js +0 -41
  62. package/tests/chat_router.test.js +0 -42
  63. package/tests/code_agent.test.js +0 -69
  64. package/tests/config_manager.test.js +0 -141
  65. package/tests/docker.test.js +0 -46
  66. package/tests/file_operations.test.js +0 -57
  67. package/tests/gmail.test.js +0 -135
  68. package/tests/gmail_auth.test.js +0 -129
  69. package/tests/google_calendar.test.js +0 -113
  70. package/tests/google_tts_urls.test.js +0 -24
  71. package/tests/memory_store.test.js +0 -185
  72. package/tests/notion.test.js +0 -121
  73. package/tests/provider_routing.test.js +0 -83
  74. package/tests/safety_manager.test.js +0 -40
  75. package/tests/spotify.test.js +0 -201
  76. package/tests/system_monitor.test.js +0 -37
  77. package/tests/updater.test.js +0 -32
  78. package/tests/workspace_manager.test.js +0 -56
@@ -7,8 +7,11 @@ const axios = require('axios');
7
7
  const cheerio = require('cheerio');
8
8
  const { readConfig, getAvailableProviders } = require('../System/config_manager');
9
9
  const safetyManager = require('../System/safety_manager');
10
+ const memoryStore = require('../AI_Brain/memory_store');
10
11
  const { readWorkspaceSession, writeWorkspaceSession } = require('./code_session_memory');
11
- const { executeAction } = require('../../mint-cli-logic');
12
+ const { executeAction } = require('../System/action_executor');
13
+ const toolRegistry = require('../System/tool_registry');
14
+ const sandboxRunner = require('../System/sandbox_runner');
12
15
 
13
16
  async function webSearch(query, onProgress = () => {}) {
14
17
  if (!query) throw new Error('Search query required.');
@@ -169,6 +172,109 @@ function extractJson(text) {
169
172
  }
170
173
  }
171
174
 
175
+ function normalizeExecutorAction(action, input = {}) {
176
+ return {
177
+ type: action,
178
+ target: input.target || input.path || input.query || '',
179
+ path: input.path,
180
+ pathType: input.type,
181
+ openAfter: input.openAfter,
182
+ pluginName: input.pluginName,
183
+ server: input.server,
184
+ args: input.args,
185
+ x: input.x,
186
+ y: input.y,
187
+ button: input.button
188
+ };
189
+ }
190
+
191
+ function formatActionPreview(action, input = {}) {
192
+ if (input.command) return input.command;
193
+ if (input.path) return input.path;
194
+ if (input.target) return input.target;
195
+ if (input.query) return input.query;
196
+ return action;
197
+ }
198
+
199
+ function evaluateActionResult(action, toolResult = '') {
200
+ if (!toolRegistry.isImportantAction(action)) {
201
+ return null;
202
+ }
203
+
204
+ const text = String(toolResult || '');
205
+ if (/^Error:|blocked|denied|failed|exception|not found/i.test(text)) {
206
+ return {
207
+ status: 'failed',
208
+ message: `Evaluator: ${action} may have failed. Review the observation before continuing.`
209
+ };
210
+ }
211
+
212
+ if (action === 'run_shell' && /(ERR!|Error:|FAIL|failed|not found|permission denied)/i.test(text)) {
213
+ return {
214
+ status: 'warning',
215
+ message: 'Evaluator: shell output contains error-like text; verify before claiming success.'
216
+ };
217
+ }
218
+
219
+ return {
220
+ status: 'passed',
221
+ message: `Evaluator: ${action} completed without obvious errors.`
222
+ };
223
+ }
224
+
225
+ function splitDataUri(dataUri = '') {
226
+ const match = String(dataUri).match(/^data:([^;]+);base64,([\s\S]+)$/);
227
+ if (!match) return null;
228
+ return {
229
+ mimeType: match[1],
230
+ data: match[2]
231
+ };
232
+ }
233
+
234
+ function contentToText(content) {
235
+ if (content && typeof content === 'object' && !Array.isArray(content)) {
236
+ return String(content.text || '');
237
+ }
238
+ return String(content || '');
239
+ }
240
+
241
+ function contentToGeminiParts(content) {
242
+ const text = contentToText(content);
243
+ const parts = text ? [{ text }] : [];
244
+ if (content && typeof content === 'object' && content.imageDataUri) {
245
+ const image = splitDataUri(content.imageDataUri);
246
+ if (image) {
247
+ parts.push({ inlineData: { mimeType: image.mimeType, data: image.data } });
248
+ }
249
+ }
250
+ return parts.length > 0 ? parts : [{ text: '' }];
251
+ }
252
+
253
+ function contentToOpenAIContent(content) {
254
+ const text = contentToText(content) || 'Analyze this input.';
255
+ if (content && typeof content === 'object' && content.imageDataUri) {
256
+ return [
257
+ { type: 'text', text },
258
+ { type: 'image_url', image_url: { url: content.imageDataUri } }
259
+ ];
260
+ }
261
+ return text;
262
+ }
263
+
264
+ function contentToAnthropicContent(content) {
265
+ const text = contentToText(content) || 'Analyze this input.';
266
+ if (content && typeof content === 'object' && content.imageDataUri) {
267
+ const image = splitDataUri(content.imageDataUri);
268
+ if (image) {
269
+ return [
270
+ { type: 'image', source: { type: 'base64', media_type: image.mimeType, data: image.data } },
271
+ { type: 'text', text }
272
+ ];
273
+ }
274
+ }
275
+ return text;
276
+ }
277
+
172
278
  function getSupportedCodeProviderOrder(config, availableProviders = getAvailableProviders(config || {}), requestedOverride = null) {
173
279
  const requestedProvider = requestedOverride || (config && config.aiProvider) || 'gemini';
174
280
  const priority = ['anthropic', 'openai', 'gemini', 'local_openai'];
@@ -371,7 +477,8 @@ async function runShell(workspaceRoot, command) {
371
477
  throw new Error('Shell command is required.');
372
478
  }
373
479
  assertSafeShell(command);
374
- const { stdout, stderr } = await execFileAsync('bash', ['-lc', command], {
480
+ const { stdout, stderr } = await sandboxRunner.runShell(command, {
481
+ source: 'code_agent',
375
482
  cwd: workspaceRoot,
376
483
  maxBuffer: 1024 * 1024 * 4
377
484
  });
@@ -478,7 +585,7 @@ class UnifiedAgentClient {
478
585
  const apiKey = this.config.anthropicApiKey || process.env.ANTHROPIC_API_KEY;
479
586
  const messages = this.history.map(m => ({
480
587
  role: m.role,
481
- content: m.content
588
+ content: contentToAnthropicContent(m.content)
482
589
  }));
483
590
 
484
591
  const response = await axios.post('https://api.anthropic.com/v1/messages', {
@@ -504,7 +611,10 @@ class UnifiedAgentClient {
504
611
 
505
612
  const messages = [
506
613
  { role: 'system', content: this.systemInstruction },
507
- ...this.history
614
+ ...this.history.map(m => ({
615
+ role: m.role,
616
+ content: contentToOpenAIContent(m.content)
617
+ }))
508
618
  ];
509
619
 
510
620
  const response = await axios.post(`${baseUrl.replace(/\/$/, '')}/chat/completions`, {
@@ -525,13 +635,15 @@ class UnifiedAgentClient {
525
635
  const model = this.config.geminiModel || DEFAULT_GEMINI_MODEL;
526
636
  const ai = new GoogleGenAI({ apiKey });
527
637
 
638
+ const recentHistory = this.history.slice(-16);
639
+ const priorHistory = recentHistory.slice(0, -1);
640
+ const lastEntry = recentHistory[recentHistory.length - 1] || { content: '' };
641
+
528
642
  // Convert history for Gemini, ensuring parts are correctly structured
529
- const geminiHistory = this.history.slice(-16).map(m => ({
643
+ const geminiHistory = priorHistory.map(m => ({
530
644
  role: m.role === 'assistant' ? 'model' : 'user',
531
- parts: [{ text: String(m.content || '') }]
645
+ parts: contentToGeminiParts(m.content)
532
646
  }));
533
-
534
- const lastMessage = String(this.history[this.history.length - 1].content || '');
535
647
 
536
648
  const chat = ai.chats.create({
537
649
  model,
@@ -542,7 +654,7 @@ class UnifiedAgentClient {
542
654
  history: geminiHistory
543
655
  });
544
656
 
545
- const response = await chat.sendMessage({ message: [{ text: lastMessage }] });
657
+ const response = await chat.sendMessage({ message: contentToGeminiParts(lastEntry.content) });
546
658
  return typeof response.text === 'function' ? response.text() : response.text;
547
659
  }
548
660
  }
@@ -616,6 +728,7 @@ async function buildInitialObservation(task, workspaceRoot, history = []) {
616
728
  const session = readWorkspaceSession(workspaceRoot);
617
729
  const gitContext = await getGitContext(workspaceRoot);
618
730
  const testCommands = detectTestCommands(workspaceRoot);
731
+ const userContext = memoryStore.getUserContext(task);
619
732
 
620
733
  const contextStr = history.length > 0
621
734
  ? `Recent Context:\n${history.slice(-10).map(m => `${m.sender}: ${m.text}`).join('\n')}\n`
@@ -636,6 +749,8 @@ async function buildInitialObservation(task, workspaceRoot, history = []) {
636
749
  session.summary || '(none)',
637
750
  `Previous task: ${session.lastTask || '(none)'}`,
638
751
  `Previous verification: ${session.lastVerification || '(none)'}`,
752
+ 'Long-term user context:',
753
+ userContext || '(none)',
639
754
  'If the task is conversational or trivial, finish directly without inspecting the workspace. For code/workspace tasks, inspect before making edits.'
640
755
  ].join('\n');
641
756
  }
@@ -644,6 +759,7 @@ async function executeCodeTask(task, options = {}) {
644
759
  const workspaceRoot = path.resolve(options.cwd || process.cwd());
645
760
  const history = options.history || [];
646
761
  const onProgress = typeof options.onProgress === 'function' ? options.onProgress : () => {};
762
+ const onFinalSummary = typeof options.onFinalSummary === 'function' ? options.onFinalSummary : null;
647
763
  const requestApproval = typeof options.requestApproval === 'function'
648
764
  ? options.requestApproval
649
765
  : async () => true;
@@ -656,7 +772,24 @@ async function executeCodeTask(task, options = {}) {
656
772
  const provider = providerOrder[0];
657
773
  const client = new UnifiedAgentClient(provider, config, providerOrder);
658
774
 
659
- let observation = await buildInitialObservation(task, workspaceRoot, history);
775
+ const initialObservationText = await buildInitialObservation(task, workspaceRoot, history);
776
+ const relevantMemoryCount = memoryStore.searchInteractions(task, 5).length;
777
+ onProgress({
778
+ phase: 'memory',
779
+ action: 'memory_context',
780
+ message: `Loaded memory: profile + recent history, ${relevantMemoryCount} direct match${relevantMemoryCount === 1 ? '' : 'es'}`
781
+ });
782
+ let observation = options.imageDataUri
783
+ ? {
784
+ text: [
785
+ initialObservationText,
786
+ '',
787
+ `[Attached image: ${options.imagePath || 'command-line image'}]`,
788
+ 'Use the attached image as visual context when planning and answering.'
789
+ ].join('\n'),
790
+ imageDataUri: options.imageDataUri
791
+ }
792
+ : initialObservationText;
660
793
 
661
794
  let finalSummary = '';
662
795
  let finalVerification = '';
@@ -669,6 +802,17 @@ async function executeCodeTask(task, options = {}) {
669
802
  const decision = await getAgentDecision(client, observation, { onProgress, step });
670
803
  const action = decision.action;
671
804
  const input = decision.input || {};
805
+ try {
806
+ toolRegistry.validateToolInput(action, input);
807
+ } catch (e) {
808
+ observation = [
809
+ `Previous thought: ${decision.thought || '(none)'}`,
810
+ `Action: ${action || '(none)'}`,
811
+ 'Observation:',
812
+ `Error: ${e.message}`
813
+ ].join('\n');
814
+ continue;
815
+ }
672
816
 
673
817
  // Immediately show the agent's thought/reasoning
674
818
  onProgress({
@@ -682,6 +826,16 @@ async function executeCodeTask(task, options = {}) {
682
826
  finalSessionSummary = input.sessionSummary || input.summary || task;
683
827
  finalSummary = input.summary || 'Task complete.';
684
828
  finalVerification = input.verification || 'Not specified.';
829
+ if (onFinalSummary) {
830
+ await onFinalSummary({
831
+ summary: finalSummary,
832
+ verification: finalVerification,
833
+ providerInfo: {
834
+ provider: client.lastSuccessfulProvider || client.provider || provider,
835
+ model: getCodeProviderModel(client.lastSuccessfulProvider || client.provider || provider, config)
836
+ }
837
+ });
838
+ }
685
839
  writeWorkspaceSession(workspaceRoot, {
686
840
  summary: finalSessionSummary,
687
841
  lastTask: task,
@@ -785,10 +939,28 @@ async function executeCodeTask(task, options = {}) {
785
939
  case 'create_folder':
786
940
  case 'system_info':
787
941
  case 'system_automation': {
788
- // Delegate to existing automation logic
789
- toolResult = await executeAction({
790
- type: action,
791
- target: input.target || input.path || input.query // Handle all possible input fields
942
+ const executorAction = normalizeExecutorAction(action, input);
943
+ const safety = safetyManager.classifyAction(executorAction);
944
+ let allowDangerous = false;
945
+ let allowApproval = false;
946
+ if (safety.tier === safetyManager.TIERS.APPROVAL || safety.tier === safetyManager.TIERS.DANGEROUS) {
947
+ const approved = await requestApproval({
948
+ type: action,
949
+ label: formatActionPreview(action, input),
950
+ preview: `${action}: ${formatActionPreview(action, input)}\nSafety: ${safety.tier} (${safety.reason})`
951
+ });
952
+ if (!approved) {
953
+ toolResult = `User denied ${action}: ${formatActionPreview(action, input)}`;
954
+ break;
955
+ }
956
+ allowApproval = safety.tier === safetyManager.TIERS.APPROVAL;
957
+ allowDangerous = safety.tier === safetyManager.TIERS.DANGEROUS;
958
+ }
959
+
960
+ toolResult = await executeAction(executorAction, {
961
+ source: 'code_agent',
962
+ allowApproval,
963
+ allowDangerous
792
964
  });
793
965
  break;
794
966
  } default:
@@ -797,6 +969,22 @@ async function executeCodeTask(task, options = {}) {
797
969
  toolResult = `Error: ${e.message}`;
798
970
  }
799
971
 
972
+ const evaluation = evaluateActionResult(action, toolResult);
973
+ if (evaluation) {
974
+ onProgress({
975
+ step,
976
+ phase: 'evaluating',
977
+ action: 'evaluator',
978
+ message: `${evaluation.status}: ${evaluation.message}`
979
+ });
980
+ toolResult = [
981
+ toolResult,
982
+ '',
983
+ 'Evaluation:',
984
+ `${evaluation.status}: ${evaluation.message}`
985
+ ].join('\n');
986
+ }
987
+
800
988
  // Log the finished step with result
801
989
  let resultSummary = '';
802
990
  if (action === 'search_code') {
@@ -858,6 +1046,7 @@ async function executeCodeTask(task, options = {}) {
858
1046
  }
859
1047
 
860
1048
  if (finalSummary) {
1049
+ memoryStore.recordInteraction(task, finalSummary);
861
1050
  const answeredProvider = client.lastSuccessfulProvider || client.provider || provider;
862
1051
  return {
863
1052
  summary: finalSummary,
@@ -0,0 +1,90 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+ const { execFileSync } = require('child_process');
4
+
5
+ const IMAGE_MIME_TYPES = {
6
+ '.png': 'image/png',
7
+ '.jpg': 'image/jpeg',
8
+ '.jpeg': 'image/jpeg',
9
+ '.webp': 'image/webp',
10
+ '.gif': 'image/gif'
11
+ };
12
+
13
+ function resolveImagePath(imagePath, cwd = process.cwd()) {
14
+ if (!imagePath || typeof imagePath !== 'string') {
15
+ throw new Error('Image path is required.');
16
+ }
17
+
18
+ return path.resolve(cwd, imagePath);
19
+ }
20
+
21
+ function getImageMimeType(imagePath) {
22
+ const ext = path.extname(imagePath).toLowerCase();
23
+ const mimeType = IMAGE_MIME_TYPES[ext];
24
+ if (!mimeType) {
25
+ throw new Error(`Unsupported image type "${ext || '(none)'}". Supported: ${Object.keys(IMAGE_MIME_TYPES).join(', ')}`);
26
+ }
27
+ return mimeType;
28
+ }
29
+
30
+ function loadImageAsDataUri(imagePath, cwd = process.cwd()) {
31
+ const resolved = resolveImagePath(imagePath, cwd);
32
+ if (!fs.existsSync(resolved)) {
33
+ throw new Error(`Image file not found: ${imagePath}`);
34
+ }
35
+
36
+ const stat = fs.statSync(resolved);
37
+ if (!stat.isFile()) {
38
+ throw new Error(`Image path is not a file: ${imagePath}`);
39
+ }
40
+
41
+ const mimeType = getImageMimeType(resolved);
42
+ const data = fs.readFileSync(resolved).toString('base64');
43
+ return {
44
+ path: resolved,
45
+ mimeType,
46
+ dataUri: `data:${mimeType};base64,${data}`
47
+ };
48
+ }
49
+
50
+ function tryReadClipboardCommand(command, args) {
51
+ try {
52
+ return execFileSync(command, args, {
53
+ encoding: 'buffer',
54
+ maxBuffer: 1024 * 1024 * 20,
55
+ stdio: ['ignore', 'pipe', 'ignore']
56
+ });
57
+ } catch (_) {
58
+ return null;
59
+ }
60
+ }
61
+
62
+ function loadClipboardImageAsDataUri() {
63
+ const attempts = [
64
+ { command: 'wl-paste', args: ['--type', 'image/png', '--no-newline'] },
65
+ { command: 'xclip', args: ['-selection', 'clipboard', '-t', 'image/png', '-o'] }
66
+ ];
67
+
68
+ for (const attempt of attempts) {
69
+ const data = tryReadClipboardCommand(attempt.command, attempt.args);
70
+ if (data && data.length > 0) {
71
+ return {
72
+ path: 'clipboard',
73
+ mimeType: 'image/png',
74
+ dataUri: `data:image/png;base64,${data.toString('base64')}`
75
+ };
76
+ }
77
+ }
78
+
79
+ throw new Error('No clipboard image found. On Linux, install wl-clipboard or xclip, then copy an image and try Ctrl+V again.');
80
+ }
81
+
82
+ module.exports = {
83
+ loadImageAsDataUri,
84
+ loadClipboardImageAsDataUri,
85
+ _helpers: {
86
+ getImageMimeType,
87
+ resolveImagePath,
88
+ tryReadClipboardCommand
89
+ }
90
+ };
@@ -4,6 +4,35 @@ const { readConfig, writeConfig } = require('../System/config_manager');
4
4
  const { installDaemon } = require('../System/daemon_manager');
5
5
  const { runGmailAuth } = require('./gmail_auth');
6
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
+ }
35
+
7
36
  /**
8
37
  * Onboarding Wizard for Mint CLI
9
38
  */
@@ -56,10 +85,10 @@ async function runOnboarding(options = {}) {
56
85
  { name: 'Gmail API', value: 'gmail', checked: config.pluginGmailEnabled },
57
86
  { name: 'Notion API', value: 'notion', checked: config.pluginNotionEnabled },
58
87
  new inquirer.Separator(),
59
- { name: 'Anthropic (Claude)', value: 'anthropic', checked: config.aiProvider === 'anthropic' || !!config.anthropicApiKey },
60
- { name: 'OpenAI (GPT-4o)', value: 'openai', checked: config.aiProvider === 'openai' || !!config.openaiApiKey },
61
- { name: 'Hugging Face', value: 'hf', checked: config.aiProvider === 'huggingface' || !!config.hfApiKey },
62
- { name: 'Local AI (LM Studio/Ollama)', value: 'local', checked: config.aiProvider === 'local_openai' || (!!config.localApiBaseUrl && config.localApiBaseUrl.length > 0) },
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) },
63
92
  new inquirer.Separator(),
64
93
  { name: 'Google Search API', value: 'google_search', checked: !!config.googleSearchApiKey },
65
94
  { name: 'Brave Search API', value: 'brave_search', checked: !!config.braveSearchApiKey },
@@ -257,7 +286,23 @@ async function runOnboarding(options = {}) {
257
286
  type: 'input',
258
287
  name: 'anthropicApiKey',
259
288
  message: 'Enter Anthropic API Key:',
260
- default: config.anthropicApiKey
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.'
261
306
  });
262
307
  }
263
308
 
@@ -266,7 +311,23 @@ async function runOnboarding(options = {}) {
266
311
  type: 'input',
267
312
  name: 'openaiApiKey',
268
313
  message: 'Enter OpenAI API Key:',
269
- default: config.openaiApiKey
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.'
270
331
  });
271
332
  }
272
333
 
@@ -297,19 +358,15 @@ async function runOnboarding(options = {}) {
297
358
 
298
359
  if (dynamicQuestions.length > 0) {
299
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');
300
363
  config = { ...config, ...extraAnswers };
301
364
 
302
365
  }
303
366
 
304
- // Ensure aiProvider reflects the selected primary AI. If no optional AI
305
- // provider is selected, keep Gemini as the safe default from basic setup.
306
- if (!selections.includes('skip')) {
307
- if (selections.includes('anthropic')) config.aiProvider = 'anthropic';
308
- else if (selections.includes('openai')) config.aiProvider = 'openai';
309
- else if (selections.includes('hf')) config.aiProvider = 'huggingface';
310
- else if (selections.includes('local')) config.aiProvider = 'local_openai';
311
- else config.aiProvider = 'gemini';
312
- }
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';
313
370
 
314
371
  // Save configuration
315
372
  writeConfig(config);
@@ -74,7 +74,8 @@ function shouldRunAutoUpdate(config = {}, now = Date.now()) {
74
74
 
75
75
  async function getLatestVersion(packageName = pkg.name) {
76
76
  const { stdout } = await execFilePromise(NPM_COMMAND, ['view', packageName, 'version', '--json'], {
77
- maxBuffer: 1024 * 1024
77
+ maxBuffer: 1024 * 1024,
78
+ timeout: 30000
78
79
  });
79
80
  return normalizeNpmVersionOutput(stdout);
80
81
  }
@@ -86,7 +87,8 @@ async function installLatest(packageName = pkg.name, options = {}) {
86
87
  }
87
88
 
88
89
  return await execFilePromise(NPM_COMMAND, args, {
89
- maxBuffer: 1024 * 1024 * 8
90
+ maxBuffer: 1024 * 1024 * 8,
91
+ timeout: 5 * 60 * 1000
90
92
  });
91
93
  }
92
94
 
@@ -182,14 +184,14 @@ async function runStartupAutoUpdate(config, writeConfig, options = {}) {
182
184
  };
183
185
  }
184
186
 
187
+ const result = await runUpdate({ checkOnly: false });
185
188
  if (typeof writeConfig === 'function') {
186
189
  writeConfig({
187
190
  ...config,
188
191
  lastUpdateCheckAt: new Date(now).toISOString()
189
192
  });
190
193
  }
191
-
192
- return await runUpdate({ checkOnly: false });
194
+ return result;
193
195
  }
194
196
 
195
197
  module.exports = {