@pheem49/mint 1.2.4 → 1.4.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/mint-cli.js CHANGED
@@ -1,12 +1,18 @@
1
1
  #!/usr/bin/env node
2
2
  require('dotenv').config({ quiet: true });
3
3
  const { Command } = require('commander');
4
- const { handleChat, resetChat } = require('./src/AI_Brain/Gemini_API');
4
+ const { handleChat, handleGeminiChatStream, resetChat, refreshApiKeyFromConfig } = require('./src/AI_Brain/Gemini_API');
5
+ const agentOrchestrator = require('./src/AI_Brain/agent_orchestrator');
6
+ const workspaceManager = require('./src/CLI/workspace_manager');
7
+ const systemMonitor = require('./src/Plugins/system_monitor');
8
+ const { sendNotification } = require('./src/System/notifications');
5
9
  const pkg = require('./package.json');
6
10
  const { runOnboarding } = require('./src/CLI/onboarding');
7
11
  const { startAgent } = require('./src/AI_Brain/headless_agent');
8
12
  const { displayFeatures } = require('./src/CLI/list_features');
9
13
  const { readConfig, writeConfig } = require('./src/System/config_manager');
14
+ const { executeCodeTask } = require('./src/CLI/code_agent');
15
+ const { detectCodeIntent, runChatRoutedTask } = require('./src/CLI/chat_router');
10
16
  const readline = require('readline');
11
17
  const { createChatUI } = require('./src/CLI/chat_ui');
12
18
 
@@ -93,23 +99,138 @@ program
93
99
  console.log(`${colors.gray}You will receive a notification when it's done.${colors.reset}\n`);
94
100
  });
95
101
 
102
+ program
103
+ .command('code')
104
+ .description('Run Mint in workspace-aware coding mode for the current project')
105
+ .argument('<task>', 'Coding task to execute in the current working directory')
106
+ .action(async (task) => {
107
+ console.log(`\n${colors.mint}${colors.bright}[Mint Code]${colors.reset} Workspace: ${process.cwd()}`);
108
+ console.log(`${colors.gray}[Mint Code] Task: ${task}${colors.reset}\n`);
109
+
110
+ try {
111
+ const result = await executeCodeTask(task, {
112
+ cwd: process.cwd(),
113
+ onProgress: (message) => {
114
+ console.log(`${colors.gray}[Mint Code] ${message}${colors.reset}`);
115
+ },
116
+ requestApproval: requestCodeApproval
117
+ });
118
+
119
+ console.log(`\n${colors.mint}${colors.bright}Summary${colors.reset}`);
120
+ console.log(result.summary);
121
+ console.log(`\n${colors.cyan}Verification:${colors.reset} ${result.verification}`);
122
+ console.log(`${colors.gray}Completed in ${result.steps} step(s).${colors.reset}\n`);
123
+ } catch (error) {
124
+ console.error(`\n${colors.pink}[Mint Code Error]${colors.reset} ${error.message}\n`);
125
+ process.exitCode = 1;
126
+ }
127
+ });
128
+
96
129
  program.parse(process.argv);
97
130
 
98
131
  /**
99
132
  * The Interactive Chat Loop — Gemini-style TUI
100
133
  */
101
134
  async function startInteractiveChat(initialMessage = null) {
102
- const { screen, appendMessage, setThinking, updateStatusModel, copyLastResponse } = createChatUI({
135
+ let lastResponseText = "";
136
+ const { screen, appendMessage, streamMessage, setThinking, updateStatusModel, copyLastResponse, requestApproval, setMode } = createChatUI({
103
137
  onSubmit: async (text) => {
104
138
  if (text.startsWith('/')) {
105
- // Slash commands via fake rl-compatible object
106
- const fakeRl = { close: () => { } };
107
- appendMessage('user', text);
108
- await handleSlashCommandUI(text, appendMessage, updateStatusModel, copyLastResponse);
109
- return;
139
+ if (text.startsWith('/agent')) {
140
+ const args = text.split(' ');
141
+ if (args[1] === 'list') {
142
+ appendMessage('system', `Available Agents: ${agentOrchestrator.listAgents().join(', ')}`);
143
+ } else if (args[1]) {
144
+ const success = agentOrchestrator.setAgent(args[1]);
145
+ if (success) {
146
+ const agent = agentOrchestrator.getCurrentAgent();
147
+ appendMessage('system', `Switched to Agent: ${agent.icon} ${agent.name}`);
148
+ updateStatusModel(null, agent.name); // Pass name to status bar
149
+ resetChat(); // Reset to apply new system prompt
150
+ } else {
151
+ appendMessage('error', `Agent "${args[1]}" not found. Try /agent list`);
152
+ }
153
+ } else {
154
+ const agent = agentOrchestrator.getCurrentAgent();
155
+ appendMessage('system', `Current Agent: ${agent.icon} ${agent.name}\nUsage: /agent <type> or /agent list`);
156
+ }
157
+ return;
158
+ }
159
+
160
+ if (text.startsWith('/stats')) {
161
+ appendMessage('system', '📊 Fetching system statistics...');
162
+ const stats = await systemMonitor.execute('stats');
163
+ appendMessage('system', stats);
164
+ return;
165
+ }
166
+
167
+ if (text.startsWith('/workspace')) {
168
+ const args = text.split(' ');
169
+ const subCmd = args[1];
170
+
171
+ if (subCmd === 'add') {
172
+ const name = args[2];
173
+ const wsPath = args[3] || '.';
174
+ const instructions = args.slice(4).join(' ');
175
+ if (!name) {
176
+ appendMessage('error', 'Usage: /workspace add <name> [path] [instructions]');
177
+ } else {
178
+ workspaceManager.addWorkspace(name, wsPath, instructions);
179
+ appendMessage('system', `Workspace "${name}" registered at ${path.resolve(wsPath)}`);
180
+ resetChat();
181
+ }
182
+ } else if (subCmd === 'list') {
183
+ const all = workspaceManager.listWorkspaces();
184
+ let listMsg = "Registered Workspaces:\n";
185
+ for (const n in all) listMsg += `- ${n}: ${all[n].path}\n`;
186
+ appendMessage('system', Object.keys(all).length ? listMsg : "No workspaces registered.");
187
+ } else if (subCmd === 'remove') {
188
+ const name = args[2];
189
+ if (workspaceManager.removeWorkspace(name)) {
190
+ appendMessage('system', `Removed workspace "${name}"`);
191
+ resetChat();
192
+ } else {
193
+ appendMessage('error', `Workspace "${name}" not found.`);
194
+ }
195
+ } else {
196
+ const ws = workspaceManager.getWorkspaceByPath(process.cwd());
197
+ appendMessage('system', ws ? `Current Workspace: ${ws.name}\nPath: ${ws.path}` : "Not currently in a registered workspace.\nUsage: /workspace <add|list|remove>");
198
+ }
199
+ return;
200
+ }
201
+
202
+ if (text.startsWith('/review')) {
203
+ if (!lastResponseText) {
204
+ appendMessage('error', 'Nothing to review yet. Get a response first.');
205
+ return;
206
+ }
207
+ agentOrchestrator.setAgent('reviewer');
208
+ appendMessage('system', '⚖️ Requesting second-pass review from Mint Reviewer...');
209
+ text = `Please review this previous response and provide a critique:\n\n${lastResponseText}`;
210
+ } else {
211
+ // Other slash commands
212
+ const fakeRl = { close: () => { } };
213
+ appendMessage('user', text);
214
+ await handleSlashCommandUI(text, appendMessage, updateStatusModel, copyLastResponse, setThinking, requestApproval, setMode);
215
+ return;
216
+ }
110
217
  }
111
218
  appendMessage('user', text);
112
219
 
220
+ const routeDecision = await detectCodeIntent(text, process.cwd());
221
+ if (routeDecision.route === 'code') {
222
+ appendMessage('system', `Router: entering Code Mode. ${routeDecision.reason}`);
223
+ await runChatRoutedTask(text, {
224
+ appendMessage,
225
+ setThinking,
226
+ requestApproval,
227
+ setMode
228
+ });
229
+ return;
230
+ }
231
+
232
+ setMode('Chat');
233
+
113
234
  // Start thinking timer
114
235
  let seconds = 0;
115
236
  setThinking(true, seconds);
@@ -119,16 +240,92 @@ async function startInteractiveChat(initialMessage = null) {
119
240
  }, 1000);
120
241
 
121
242
  try {
122
- const response = await handleChat(text);
123
- clearInterval(timer);
124
- setThinking(false);
125
- appendMessage('assistant', response.response, response.timestamp);
243
+ const config = require('./src/System/config_manager').readConfig();
244
+ const provider = config.aiProvider || 'gemini';
245
+ const currentAgent = agentOrchestrator.getCurrentAgent();
246
+ updateStatusModel(null, currentAgent.name);
247
+ if (provider === 'gemini') {
248
+ // ── Streaming path (Gemini only) ──────────────────────────────────
249
+ // Gemini returns JSON so we buffer all chunks and progressively
250
+ // extract the "response" field as more of the JSON arrives.
251
+ clearInterval(timer);
252
+
253
+ let jsonBuffer = '';
254
+ let finalParsed = null;
255
+ let streamer = null;
256
+ let displayedChars = 0; // chars of response text already sent to TUI
257
+
258
+ try {
259
+ for await (const event of handleGeminiChatStream(text)) {
260
+ if (event.chunk) {
261
+ jsonBuffer += event.chunk;
262
+
263
+ // Progressively extract readable text from the growing JSON buffer
264
+ const match = jsonBuffer.match(/"response"\s*:\s*"((?:[^"\\]|\\.)*)"/s);
265
+ if (match) {
266
+ const fullText = match[1]
267
+ .replace(/\\n/g, '\n')
268
+ .replace(/\\"/g, '"')
269
+ .replace(/\\\\/g, '\\');
270
+ const newChars = fullText.slice(displayedChars);
271
+ if (newChars.length > 0) {
272
+ if (!streamer) {
273
+ setThinking(false);
274
+ streamer = streamMessage('assistant');
275
+ }
276
+ streamer.appendChunk(newChars);
277
+ displayedChars = fullText.length;
278
+ }
279
+ }
280
+ } else if (event.done) {
281
+ finalParsed = event.parsed;
282
+ // Flush any remaining response text not yet displayed
283
+ if (finalParsed && finalParsed.response) {
284
+ const remaining = finalParsed.response.slice(displayedChars);
285
+ if (!streamer) {
286
+ setThinking(false);
287
+ streamer = streamMessage('assistant');
288
+ }
289
+ if (remaining) streamer.appendChunk(remaining);
290
+ }
291
+ if (streamer) {
292
+ streamer.finalize(event.timestamp);
293
+ } else {
294
+ setThinking(false);
295
+ appendMessage('assistant',
296
+ finalParsed ? finalParsed.response : '',
297
+ event.timestamp);
298
+ }
299
+ }
300
+ }
301
+ } catch (streamErr) {
302
+ setThinking(false);
303
+ appendMessage('error', streamErr.message);
304
+ return;
305
+ }
306
+
307
+ // Execute Actions from the final parsed response
308
+ if (finalParsed) {
309
+ const { executeAction } = require('./mint-cli-logic');
310
+ if (finalParsed.action && finalParsed.action.type !== 'none') {
311
+ const result = await executeAction(finalParsed.action);
312
+ if (result) appendMessage('system', `Action: ${result}`);
313
+ }
314
+ }
126
315
 
127
- // Execute Actions
128
- const { executeAction } = require('./mint-cli-logic');
129
- if (response.action && response.action.type !== 'none') {
130
- const result = await executeAction(response.action);
131
- if (result) appendMessage('system', `Action: ${result}`);
316
+ } else {
317
+ // ── Non-streaming fallback (Ollama, Anthropic, OpenAI, etc.) ──
318
+ const response = await handleChat(text);
319
+ clearInterval(timer);
320
+ setThinking(false);
321
+ lastResponseText = response.response;
322
+ appendMessage('assistant', response.response, response.timestamp);
323
+
324
+ const { executeAction } = require('./mint-cli-logic');
325
+ if (response.action && response.action.type !== 'none') {
326
+ const result = await executeAction(response.action);
327
+ if (result) appendMessage('system', `Action: ${result}`);
328
+ }
132
329
  }
133
330
  } catch (err) {
134
331
  clearInterval(timer);
@@ -149,18 +346,30 @@ async function startInteractiveChat(initialMessage = null) {
149
346
  // Handle initial message if passed via CLI arg
150
347
  if (initialMessage) {
151
348
  appendMessage('user', initialMessage);
152
- let seconds = 0;
153
- setThinking(true, seconds);
154
- const timer = setInterval(() => { seconds++; setThinking(true, seconds); }, 1000);
155
- try {
156
- const response = await handleChat(initialMessage);
157
- clearInterval(timer);
158
- setThinking(false);
159
- appendMessage('assistant', response.response, response.timestamp);
160
- } catch (err) {
161
- clearInterval(timer);
162
- setThinking(false);
163
- appendMessage('error', err.message);
349
+ const routeDecision = await detectCodeIntent(initialMessage, process.cwd());
350
+ if (routeDecision.route === 'code') {
351
+ appendMessage('system', `Router: entering Code Mode. ${routeDecision.reason}`);
352
+ await runChatRoutedTask(initialMessage, {
353
+ appendMessage,
354
+ setThinking,
355
+ requestApproval,
356
+ setMode
357
+ });
358
+ } else {
359
+ setMode('Chat');
360
+ let seconds = 0;
361
+ setThinking(true, seconds);
362
+ const timer = setInterval(() => { seconds++; setThinking(true, seconds); }, 1000);
363
+ try {
364
+ const response = await handleChat(initialMessage);
365
+ clearInterval(timer);
366
+ setThinking(false);
367
+ appendMessage('assistant', response.response, response.timestamp);
368
+ } catch (err) {
369
+ clearInterval(timer);
370
+ setThinking(false);
371
+ appendMessage('error', err.message);
372
+ }
164
373
  }
165
374
  }
166
375
  }
@@ -168,7 +377,7 @@ async function startInteractiveChat(initialMessage = null) {
168
377
  /**
169
378
  * Handles slash commands within the TUI context
170
379
  */
171
- async function handleSlashCommandUI(input, appendMessage, updateStatusModel, copyLastResponse) {
380
+ async function handleSlashCommandUI(input, appendMessage, updateStatusModel, copyLastResponse, setThinking, requestApproval, setMode) {
172
381
  const parts = input.split(' ');
173
382
  const command = parts[0].toLowerCase();
174
383
  const args = parts.slice(1);
@@ -178,6 +387,7 @@ async function handleSlashCommandUI(input, appendMessage, updateStatusModel, cop
178
387
  case '/?':
179
388
  appendMessage('system', [
180
389
  'Mint Slash Commands:',
390
+ ' /code <task> — Force workspace Code Mode',
181
391
  ' /models [name] — List or switch Gemini models',
182
392
  ' /config — Show current configuration',
183
393
  ' /copy — Copy last response to clipboard',
@@ -192,29 +402,63 @@ async function handleSlashCommandUI(input, appendMessage, updateStatusModel, cop
192
402
  const config = readConfig();
193
403
  if (args.length === 0) {
194
404
  appendMessage('system', [
195
- `Current Model: ${config.geminiModel}`,
196
- 'Available Presets:',
197
- ' - gemini-2.5-flash (Default)',
198
- ' - gemini-3.1-flash-lite-preview',
199
- ' - gemini-3.1-flash-lite',
200
- ' - ollama (local provider)',
405
+ `Current Provider: ${config.aiProvider}`,
406
+ `Current Gemini Model: ${config.geminiModel}`,
407
+ 'Available Providers/Presets:',
408
+ ' - gemini-2.5-flash (Default Gemini)',
409
+ ' - ollama (Local provider)',
410
+ ' - anthropic (Claude)',
411
+ ' - openai (GPT)',
412
+ ' - huggingface (Inference API)',
413
+ ' - local (LM Studio / OpenAI Compatible)',
201
414
  'Usage: /models <name> to switch'
202
415
  ].join('\n'));
203
416
  } else {
204
417
  const { writeConfig } = require('./src/System/config_manager');
205
418
  const newModel = args[0];
419
+ let newProvider = 'gemini';
420
+
206
421
  if (newModel === 'ollama') {
207
- config.aiProvider = 'ollama';
422
+ newProvider = 'ollama';
423
+ } else if (newModel === 'anthropic') {
424
+ newProvider = 'anthropic';
425
+ } else if (newModel === 'openai') {
426
+ newProvider = 'openai';
427
+ } else if (newModel === 'huggingface') {
428
+ newProvider = 'huggingface';
429
+ } else if (newModel === 'local' || newModel === 'local_openai') {
430
+ newProvider = 'local_openai';
431
+ } else if (newModel.startsWith('gpt-')) {
432
+ newProvider = 'openai';
433
+ config.openaiModel = newModel;
434
+ } else if (newModel.startsWith('claude-')) {
435
+ newProvider = 'anthropic';
436
+ config.anthropicModel = newModel;
208
437
  } else {
209
- config.aiProvider = 'gemini';
438
+ newProvider = 'gemini';
210
439
  config.geminiModel = newModel;
211
440
  }
441
+
442
+ config.aiProvider = newProvider;
212
443
  writeConfig(config);
213
- appendMessage('system', `✅ Switched to: ${newModel}`);
214
- if (updateStatusModel) updateStatusModel(newModel);
444
+ appendMessage('system', `✅ Switched to: ${newProvider} ${newProvider === 'gemini' ? `(${newModel})` : ''}`);
445
+ if (updateStatusModel) updateStatusModel(newProvider === 'gemini' ? newModel : newProvider);
215
446
  }
216
447
  break;
217
448
 
449
+ case '/code':
450
+ if (args.length === 0) {
451
+ appendMessage('system', 'Usage: /code <task>');
452
+ break;
453
+ }
454
+ await runChatRoutedTask(`/code ${args.join(' ')}`, {
455
+ appendMessage,
456
+ setThinking,
457
+ requestApproval,
458
+ setMode
459
+ });
460
+ break;
461
+
218
462
  case '/config':
219
463
  const currentCfg = readConfig();
220
464
  appendMessage('system', [
@@ -253,3 +497,36 @@ async function handleSlashCommandUI(input, appendMessage, updateStatusModel, cop
253
497
  }
254
498
  }
255
499
 
500
+ async function requestCodeApproval(request) {
501
+ const typeLabel = request.type === 'shell'
502
+ ? 'Shell Command'
503
+ : request.type === 'patch'
504
+ ? 'Patch Edit'
505
+ : 'File Write';
506
+
507
+ console.log(`\n${colors.yellow}${colors.bright}[Approval Required]${colors.reset} ${typeLabel}`);
508
+ if (request.label) {
509
+ console.log(`${colors.gray}${request.label}${colors.reset}`);
510
+ }
511
+ if (request.preview) {
512
+ console.log(`${colors.gray}${request.preview}${colors.reset}\n`);
513
+ }
514
+
515
+ const rl = readline.createInterface({
516
+ input: process.stdin,
517
+ output: process.stdout
518
+ });
519
+
520
+ const answer = await new Promise((resolve) => {
521
+ rl.question('Approve this action? [y/N]: ', (value) => {
522
+ rl.close();
523
+ resolve((value || '').trim().toLowerCase());
524
+ });
525
+ });
526
+
527
+ const approved = answer === 'y' || answer === 'yes';
528
+ console.log(approved
529
+ ? `${colors.mint}[Mint Code] Approved.${colors.reset}\n`
530
+ : `${colors.pink}[Mint Code] Denied.${colors.reset}\n`);
531
+ return approved;
532
+ }
package/package.json CHANGED
@@ -1,14 +1,24 @@
1
1
  {
2
2
  "name": "@pheem49/mint",
3
- "version": "1.2.4",
3
+ "version": "1.4.0",
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
  },
@@ -22,24 +32,31 @@
22
32
  "dependencies": {
23
33
  "@google/genai": "^1.44.0",
24
34
  "@inkjs/ui": "^2.0.0",
35
+ "@modelcontextprotocol/sdk": "^1.29.0",
25
36
  "axios": "^1.13.6",
26
37
  "blessed": "^0.1.81",
27
38
  "cheerio": "^1.2.0",
28
39
  "commander": "^14.0.3",
29
40
  "dotenv": "^17.3.1",
41
+ "framer-motion": "^12.38.0",
30
42
  "google-tts-api": "^2.0.2",
31
43
  "ink": "^7.0.1",
32
44
  "ink-text-input": "^6.0.0",
33
45
  "inquirer": "^13.4.1",
46
+ "lucide-react": "^1.9.0",
34
47
  "mammoth": "^1.12.0",
35
48
  "pdf-parse": "^2.4.5",
36
49
  "puppeteer": "^24.38.0",
37
50
  "react": "^19.2.5",
51
+ "react-dom": "^19.2.5",
38
52
  "xlsx": "^0.18.5"
39
53
  },
40
54
  "devDependencies": {
55
+ "@vitejs/plugin-react": "^6.0.1",
41
56
  "electron": "^40.7.0",
42
- "electron-builder": "^26.8.1"
57
+ "electron-builder": "^26.8.1",
58
+ "jest": "^30.4.0",
59
+ "vite": "^8.0.10"
43
60
  },
44
61
  "build": {
45
62
  "appId": "com.pheem49.mint",