@pheem49/mint 1.3.0 → 1.4.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 (38) hide show
  1. package/.codex +0 -0
  2. package/README.md +174 -126
  3. package/main.js +21 -1
  4. package/mint-cli-logic.js +21 -1
  5. package/mint-cli.js +287 -45
  6. package/package.json +13 -2
  7. package/src/AI_Brain/Gemini_API.js +331 -64
  8. package/src/AI_Brain/agent_orchestrator.js +73 -0
  9. package/src/AI_Brain/autonomous_brain.js +2 -0
  10. package/src/AI_Brain/memory_store.js +318 -0
  11. package/src/AI_Brain/proactive_engine.js +2 -8
  12. package/src/Automation_Layer/file_operations.js +123 -4
  13. package/src/Automation_Layer/open_app.js +72 -43
  14. package/src/Automation_Layer/open_website.js +3 -3
  15. package/src/CLI/chat_router.js +57 -9
  16. package/src/CLI/chat_ui.js +117 -11
  17. package/src/CLI/code_agent.js +249 -36
  18. package/src/CLI/onboarding.js +53 -6
  19. package/src/CLI/workspace_manager.js +90 -0
  20. package/src/Plugins/docker.js +12 -10
  21. package/src/Plugins/spotify.js +168 -40
  22. package/src/Plugins/system_monitor.js +72 -0
  23. package/src/System/config_manager.js +35 -2
  24. package/src/System/custom_workflows.js +9 -2
  25. package/src/System/notifications.js +23 -0
  26. package/src/UI/settings.html +143 -65
  27. package/src/UI/settings.js +155 -41
  28. package/tests/agent_orchestrator.test.js +41 -0
  29. package/tests/chat_router.test.js +42 -0
  30. package/tests/code_agent.test.js +69 -0
  31. package/tests/config_manager.test.js +141 -0
  32. package/tests/docker.test.js +46 -0
  33. package/tests/file_operations.test.js +57 -0
  34. package/tests/memory_store.test.js +185 -0
  35. package/tests/provider_routing.test.js +67 -0
  36. package/tests/spotify.test.js +201 -0
  37. package/tests/system_monitor.test.js +37 -0
  38. package/tests/workspace_manager.test.js +56 -0
package/mint-cli.js CHANGED
@@ -1,7 +1,19 @@
1
1
  #!/usr/bin/env node
2
2
  require('dotenv').config({ quiet: true });
3
+ // Suppress experimental SQLite warning
4
+ const originalEmit = process.emit;
5
+ process.emit = function (name, data, ...args) {
6
+ if (name === 'warning' && typeof data === 'object' && data.name === 'ExperimentalWarning' && data.message.includes('SQLite')) {
7
+ return false;
8
+ }
9
+ return originalEmit.apply(process, [name, data, ...args]);
10
+ };
3
11
  const { Command } = require('commander');
4
- const { handleChat, resetChat } = require('./src/AI_Brain/Gemini_API');
12
+ const { handleChat, handleGeminiChatStream, resetChat, refreshApiKeyFromConfig, getChatTranscript } = require('./src/AI_Brain/Gemini_API');
13
+ const agentOrchestrator = require('./src/AI_Brain/agent_orchestrator');
14
+ const workspaceManager = require('./src/CLI/workspace_manager');
15
+ const systemMonitor = require('./src/Plugins/system_monitor');
16
+ const { sendNotification } = require('./src/System/notifications');
5
17
  const pkg = require('./package.json');
6
18
  const { runOnboarding } = require('./src/CLI/onboarding');
7
19
  const { startAgent } = require('./src/AI_Brain/headless_agent');
@@ -128,27 +140,117 @@ program.parse(process.argv);
128
140
  * The Interactive Chat Loop — Gemini-style TUI
129
141
  */
130
142
  async function startInteractiveChat(initialMessage = null) {
131
- const { screen, appendMessage, setThinking, updateStatusModel, copyLastResponse, requestApproval, setMode } = createChatUI({
143
+ let lastResponseText = "";
144
+ const { screen, appendMessage, streamMessage, setThinking, updateStatusModel, copyLastResponse, requestApproval, setMode } = createChatUI({
132
145
  onSubmit: async (text) => {
133
146
  if (text.startsWith('/')) {
134
- // Slash commands via fake rl-compatible object
135
- const fakeRl = { close: () => { } };
136
- appendMessage('user', text);
137
- await handleSlashCommandUI(text, appendMessage, updateStatusModel, copyLastResponse, setThinking, requestApproval, setMode);
138
- return;
147
+ if (text.startsWith('/agent')) {
148
+ const args = text.split(' ');
149
+ if (args[1] === 'list') {
150
+ appendMessage('system', `Available Agents: ${agentOrchestrator.listAgents().join(', ')}`);
151
+ } else if (args[1]) {
152
+ const success = agentOrchestrator.setAgent(args[1]);
153
+ if (success) {
154
+ const agent = agentOrchestrator.getCurrentAgent();
155
+ appendMessage('system', `Switched to Agent: ${agent.icon} ${agent.name}`);
156
+ updateStatusModel(agent.name); // Pass name to status bar
157
+ resetChat(); // Reset to apply new system prompt
158
+ } else {
159
+ appendMessage('error', `Agent "${args[1]}" not found. Try /agent list`);
160
+ }
161
+ } else {
162
+ const agent = agentOrchestrator.getCurrentAgent();
163
+ appendMessage('system', `Current Agent: ${agent.icon} ${agent.name}\nUsage: /agent <type> or /agent list`);
164
+ }
165
+ return;
166
+ }
167
+
168
+ if (text.startsWith('/stats')) {
169
+ appendMessage('system', '📊 Fetching system statistics...');
170
+ const stats = await systemMonitor.execute('stats');
171
+ appendMessage('system', stats);
172
+ return;
173
+ }
174
+
175
+ if (text.startsWith('/workspace')) {
176
+ const args = text.split(' ');
177
+ const subCmd = args[1];
178
+
179
+ if (subCmd === 'add') {
180
+ const name = args[2];
181
+ const wsPath = args[3] || '.';
182
+ const instructions = args.slice(4).join(' ');
183
+ if (!name) {
184
+ appendMessage('error', 'Usage: /workspace add <name> [path] [instructions]');
185
+ } else {
186
+ workspaceManager.addWorkspace(name, wsPath, instructions);
187
+ appendMessage('system', `Workspace "${name}" registered at ${path.resolve(wsPath)}`);
188
+ resetChat();
189
+ }
190
+ } else if (subCmd === 'list') {
191
+ const all = workspaceManager.listWorkspaces();
192
+ let listMsg = "Registered Workspaces:\n";
193
+ for (const n in all) listMsg += `- ${n}: ${all[n].path}\n`;
194
+ appendMessage('system', Object.keys(all).length ? listMsg : "No workspaces registered.");
195
+ } else if (subCmd === 'remove') {
196
+ const name = args[2];
197
+ if (workspaceManager.removeWorkspace(name)) {
198
+ appendMessage('system', `Removed workspace "${name}"`);
199
+ resetChat();
200
+ } else {
201
+ appendMessage('error', `Workspace "${name}" not found.`);
202
+ }
203
+ } else {
204
+ const ws = workspaceManager.getWorkspaceByPath(process.cwd());
205
+ appendMessage('system', ws ? `Current Workspace: ${ws.name}\nPath: ${ws.path}` : "Not currently in a registered workspace.\nUsage: /workspace <add|list|remove>");
206
+ }
207
+ return;
208
+ }
209
+
210
+ if (text.startsWith('/review')) {
211
+ if (!lastResponseText) {
212
+ appendMessage('error', 'Nothing to review yet. Get a response first.');
213
+ return;
214
+ }
215
+ agentOrchestrator.setAgent('reviewer');
216
+ appendMessage('system', '⚖️ Requesting second-pass review from Mint Reviewer...');
217
+ text = `Please review this previous response and provide a critique:\n\n${lastResponseText}`;
218
+ } else {
219
+ // Other slash commands
220
+ const fakeRl = { close: () => { } };
221
+ appendMessage('user', text);
222
+ await handleSlashCommandUI(text, appendMessage, updateStatusModel, copyLastResponse, setThinking, requestApproval, setMode);
223
+ return;
224
+ }
139
225
  }
140
226
  appendMessage('user', text);
141
227
 
142
- const routeDecision = await detectCodeIntent(text, process.cwd());
228
+ const transcript = await getChatTranscript();
229
+ const routeDecision = await detectCodeIntent(text, process.cwd(), transcript);
143
230
  if (routeDecision.route === 'code') {
144
- appendMessage('system', `Router: entering Code Mode. ${routeDecision.reason}`);
145
- await runChatRoutedTask(text, {
146
- appendMessage,
147
- setThinking,
148
- requestApproval,
149
- setMode
231
+ const approved = await requestApproval({
232
+ type: 'code_mode',
233
+ label: 'Mint wants to switch this request into Code Mode.',
234
+ preview: [
235
+ `Request: ${text}`,
236
+ `Reason: ${routeDecision.reason}`,
237
+ '',
238
+ 'Code Mode is better for larger coding tasks that may inspect the workspace, run checks, or edit files.'
239
+ ].join('\n')
150
240
  });
151
- return;
241
+ if (!approved) {
242
+ appendMessage('system', `Router stayed in Chat Mode. ${routeDecision.reason}`);
243
+ } else {
244
+ appendMessage('system', `Router: entering Code Mode. ${routeDecision.reason}`);
245
+ await runChatRoutedTask(text, {
246
+ appendMessage,
247
+ setThinking,
248
+ requestApproval,
249
+ setMode,
250
+ history: transcript
251
+ });
252
+ return;
253
+ }
152
254
  }
153
255
 
154
256
  setMode('Chat');
@@ -162,16 +264,92 @@ async function startInteractiveChat(initialMessage = null) {
162
264
  }, 1000);
163
265
 
164
266
  try {
165
- const response = await handleChat(text);
166
- clearInterval(timer);
167
- setThinking(false);
168
- appendMessage('assistant', response.response, response.timestamp);
267
+ const config = require('./src/System/config_manager').readConfig();
268
+ const provider = config.aiProvider || 'gemini';
269
+ const currentAgent = agentOrchestrator.getCurrentAgent();
270
+ updateStatusModel(currentAgent.name);
271
+ if (provider === 'gemini') {
272
+ // ── Streaming path (Gemini only) ──────────────────────────────────
273
+ // Gemini returns JSON so we buffer all chunks and progressively
274
+ // extract the "response" field as more of the JSON arrives.
275
+ clearInterval(timer);
276
+
277
+ let jsonBuffer = '';
278
+ let finalParsed = null;
279
+ let streamer = null;
280
+ let displayedChars = 0; // chars of response text already sent to TUI
281
+
282
+ try {
283
+ for await (const event of handleGeminiChatStream(text)) {
284
+ if (event.chunk) {
285
+ jsonBuffer += event.chunk;
286
+
287
+ // Progressively extract readable text from the growing JSON buffer
288
+ const match = jsonBuffer.match(/"response"\s*:\s*"((?:[^"\\]|\\.)*)"/s);
289
+ if (match) {
290
+ const fullText = match[1]
291
+ .replace(/\\n/g, '\n')
292
+ .replace(/\\"/g, '"')
293
+ .replace(/\\\\/g, '\\');
294
+ const newChars = fullText.slice(displayedChars);
295
+ if (newChars.length > 0) {
296
+ if (!streamer) {
297
+ setThinking(false);
298
+ streamer = streamMessage('assistant');
299
+ }
300
+ streamer.appendChunk(newChars);
301
+ displayedChars = fullText.length;
302
+ }
303
+ }
304
+ } else if (event.done) {
305
+ finalParsed = event.parsed;
306
+ // Flush any remaining response text not yet displayed
307
+ if (finalParsed && finalParsed.response) {
308
+ const remaining = finalParsed.response.slice(displayedChars);
309
+ if (!streamer) {
310
+ setThinking(false);
311
+ streamer = streamMessage('assistant');
312
+ }
313
+ if (remaining) streamer.appendChunk(remaining);
314
+ }
315
+ if (streamer) {
316
+ streamer.finalize(event.timestamp);
317
+ } else {
318
+ setThinking(false);
319
+ appendMessage('assistant',
320
+ finalParsed ? finalParsed.response : '',
321
+ event.timestamp);
322
+ }
323
+ }
324
+ }
325
+ } catch (streamErr) {
326
+ setThinking(false);
327
+ appendMessage('error', streamErr.message);
328
+ return;
329
+ }
330
+
331
+ // Execute Actions from the final parsed response
332
+ if (finalParsed) {
333
+ const { executeAction } = require('./mint-cli-logic');
334
+ if (finalParsed.action && finalParsed.action.type !== 'none') {
335
+ const result = await executeAction(finalParsed.action);
336
+ if (result) appendMessage('system', `Action: ${result}`);
337
+ }
338
+ }
169
339
 
170
- // Execute Actions
171
- const { executeAction } = require('./mint-cli-logic');
172
- if (response.action && response.action.type !== 'none') {
173
- const result = await executeAction(response.action);
174
- if (result) appendMessage('system', `Action: ${result}`);
340
+ } else {
341
+ // ── Non-streaming fallback (Ollama, Anthropic, OpenAI, etc.) ──
342
+ const response = await handleChat(text);
343
+ clearInterval(timer);
344
+ setThinking(false);
345
+ lastResponseText = response.response;
346
+ appendMessage('assistant', response.response, response.timestamp);
347
+
348
+ const { executeAction } = require('./mint-cli-logic');
349
+ if (response.action && response.action.type !== 'none') {
350
+ const result = await executeAction(response.action);
351
+ if (result) appendMessage('system', `Action: ${result}`);
352
+ }
175
353
  }
176
354
  } catch (err) {
177
355
  clearInterval(timer);
@@ -192,16 +370,52 @@ async function startInteractiveChat(initialMessage = null) {
192
370
  // Handle initial message if passed via CLI arg
193
371
  if (initialMessage) {
194
372
  appendMessage('user', initialMessage);
195
- const routeDecision = await detectCodeIntent(initialMessage, process.cwd());
196
- if (routeDecision.route === 'code') {
197
- appendMessage('system', `Router: entering Code Mode. ${routeDecision.reason}`);
198
- await runChatRoutedTask(initialMessage, {
199
- appendMessage,
200
- setThinking,
201
- requestApproval,
202
- setMode
203
- });
204
- } else {
373
+ const transcript = await getChatTranscript();
374
+ const routeDecision = await detectCodeIntent(initialMessage, process.cwd(), transcript);
375
+ if (routeDecision.route === 'code') {
376
+ const approved = await requestApproval({
377
+ type: 'code_mode',
378
+ label: 'Mint wants to switch this request into Code Mode.',
379
+ preview: [
380
+ `Request: ${initialMessage}`,
381
+ `Reason: ${routeDecision.reason}`,
382
+ '',
383
+ 'Code Mode is better for larger coding tasks that may inspect the workspace, run checks, or edit files.'
384
+ ].join('\n')
385
+ });
386
+ if (approved) {
387
+ appendMessage('system', `Router: entering Code Mode. ${routeDecision.reason}`);
388
+ await runChatRoutedTask(initialMessage, {
389
+ appendMessage,
390
+ setThinking,
391
+ requestApproval,
392
+ setMode,
393
+ history: transcript
394
+ });
395
+ } else {
396
+ appendMessage('system', `Router stayed in Chat Mode. ${routeDecision.reason}`);
397
+ setMode('Chat');
398
+ let seconds = 0;
399
+ setThinking(true, seconds);
400
+ const timer = setInterval(() => { seconds++; setThinking(true, seconds); }, 1000);
401
+ try {
402
+ const response = await handleChat(initialMessage);
403
+ clearInterval(timer);
404
+ setThinking(false);
405
+ appendMessage('assistant', response.response, response.timestamp);
406
+ lastResponseText = response.response;
407
+ const { executeAction } = require('./mint-cli-logic');
408
+ if (response.action && response.action.type !== 'none') {
409
+ const result = await executeAction(response.action);
410
+ if (result) appendMessage('system', `Action: ${result}`);
411
+ }
412
+ } catch (err) {
413
+ clearInterval(timer);
414
+ setThinking(false);
415
+ appendMessage('error', err.message);
416
+ }
417
+ }
418
+ } else {
205
419
  setMode('Chat');
206
420
  let seconds = 0;
207
421
  setThinking(true, seconds);
@@ -211,6 +425,12 @@ async function startInteractiveChat(initialMessage = null) {
211
425
  clearInterval(timer);
212
426
  setThinking(false);
213
427
  appendMessage('assistant', response.response, response.timestamp);
428
+ lastResponseText = response.response;
429
+ const { executeAction } = require('./mint-cli-logic');
430
+ if (response.action && response.action.type !== 'none') {
431
+ const result = await executeAction(response.action);
432
+ if (result) appendMessage('system', `Action: ${result}`);
433
+ }
214
434
  } catch (err) {
215
435
  clearInterval(timer);
216
436
  setThinking(false);
@@ -248,26 +468,47 @@ async function handleSlashCommandUI(input, appendMessage, updateStatusModel, cop
248
468
  const config = readConfig();
249
469
  if (args.length === 0) {
250
470
  appendMessage('system', [
251
- `Current Model: ${config.geminiModel}`,
252
- 'Available Presets:',
253
- ' - gemini-2.5-flash (Default)',
254
- ' - gemini-3.1-flash-lite-preview',
255
- ' - gemini-3.1-flash-lite',
256
- ' - ollama (local provider)',
471
+ `Current Provider: ${config.aiProvider}`,
472
+ `Current Gemini Model: ${config.geminiModel}`,
473
+ 'Available Providers/Presets:',
474
+ ' - gemini-2.5-flash (Default Gemini)',
475
+ ' - ollama (Local provider)',
476
+ ' - anthropic (Claude)',
477
+ ' - openai (GPT)',
478
+ ' - huggingface (Inference API)',
479
+ ' - local (LM Studio / OpenAI Compatible)',
257
480
  'Usage: /models <name> to switch'
258
481
  ].join('\n'));
259
482
  } else {
260
483
  const { writeConfig } = require('./src/System/config_manager');
261
484
  const newModel = args[0];
485
+ let newProvider = 'gemini';
486
+
262
487
  if (newModel === 'ollama') {
263
- config.aiProvider = 'ollama';
488
+ newProvider = 'ollama';
489
+ } else if (newModel === 'anthropic') {
490
+ newProvider = 'anthropic';
491
+ } else if (newModel === 'openai') {
492
+ newProvider = 'openai';
493
+ } else if (newModel === 'huggingface') {
494
+ newProvider = 'huggingface';
495
+ } else if (newModel === 'local' || newModel === 'local_openai') {
496
+ newProvider = 'local_openai';
497
+ } else if (newModel.startsWith('gpt-')) {
498
+ newProvider = 'openai';
499
+ config.openaiModel = newModel;
500
+ } else if (newModel.startsWith('claude-')) {
501
+ newProvider = 'anthropic';
502
+ config.anthropicModel = newModel;
264
503
  } else {
265
- config.aiProvider = 'gemini';
504
+ newProvider = 'gemini';
266
505
  config.geminiModel = newModel;
267
506
  }
507
+
508
+ config.aiProvider = newProvider;
268
509
  writeConfig(config);
269
- appendMessage('system', `✅ Switched to: ${newModel}`);
270
- if (updateStatusModel) updateStatusModel(newModel);
510
+ appendMessage('system', `✅ Switched to: ${newProvider} ${newProvider === 'gemini' ? `(${newModel})` : ''}`);
511
+ if (updateStatusModel) updateStatusModel(newProvider === 'gemini' ? newModel : newProvider);
271
512
  }
272
513
  break;
273
514
 
@@ -280,7 +521,8 @@ async function handleSlashCommandUI(input, appendMessage, updateStatusModel, cop
280
521
  appendMessage,
281
522
  setThinking,
282
523
  requestApproval,
283
- setMode
524
+ setMode,
525
+ history: await getChatTranscript()
284
526
  });
285
527
  break;
286
528
 
package/package.json CHANGED
@@ -1,14 +1,24 @@
1
1
  {
2
2
  "name": "@pheem49/mint",
3
- "version": "1.3.0",
3
+ "version": "1.4.1",
4
4
  "description": "A powerful Electron-based AI desktop assistant powered by Google Gemini, featuring screen vision, web automation, and proactive suggestions.",
5
5
  "main": "main.js",
6
6
  "scripts": {
7
7
  "start": "electron .",
8
- "test": "echo \"Error: no test specified\" && exit 1",
8
+ "test": "jest --testPathPatterns=tests/",
9
+ "test:watch": "jest --testPathPatterns=tests/ --watch",
9
10
  "build:linux": "electron-builder --linux",
10
11
  "cli": "node mint-cli.js"
11
12
  },
13
+ "jest": {
14
+ "testEnvironment": "node",
15
+ "testMatch": ["**/tests/**/*.test.js"],
16
+ "collectCoverageFrom": [
17
+ "src/AI_Brain/memory_store.js",
18
+ "src/AI_Brain/knowledge_base.js",
19
+ "src/System/config_manager.js"
20
+ ]
21
+ },
12
22
  "bin": {
13
23
  "mint": "mint-cli.js"
14
24
  },
@@ -45,6 +55,7 @@
45
55
  "@vitejs/plugin-react": "^6.0.1",
46
56
  "electron": "^40.7.0",
47
57
  "electron-builder": "^26.8.1",
58
+ "jest": "^30.4.0",
48
59
  "vite": "^8.0.10"
49
60
  },
50
61
  "build": {