@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
package/mint-cli.js CHANGED
@@ -9,6 +9,8 @@ process.emit = function (name, data, ...args) {
9
9
  return originalEmit.apply(process, [name, data, ...args]);
10
10
  };
11
11
  const { Command } = require('commander');
12
+ const fs = require('fs');
13
+ const path = require('path');
12
14
  const { handleChat, handleGeminiChatStream, resetChat, refreshApiKeyFromConfig, getChatTranscript } = require('./src/AI_Brain/Gemini_API');
13
15
  const agentOrchestrator = require('./src/AI_Brain/agent_orchestrator');
14
16
  const workspaceManager = require('./src/CLI/workspace_manager');
@@ -21,18 +23,31 @@ const { displayFeatures } = require('./src/CLI/list_features');
21
23
  const { readConfig, writeConfig } = require('./src/System/config_manager');
22
24
  const { executeCodeTask } = require('./src/CLI/code_agent');
23
25
  const { detectCodeIntent, runChatRoutedTask } = require('./src/CLI/chat_router');
26
+ const memoryStore = require('./src/AI_Brain/memory_store');
24
27
  const readline = require('readline');
25
28
  const { createChatUI } = require('./src/CLI/chat_ui');
29
+ const { runUpdate, runStartupAutoUpdate, shouldRunAutoUpdate } = require('./src/CLI/updater');
30
+ const { runGmailAuth } = require('./src/CLI/gmail_auth');
31
+ const { loadImageAsDataUri, loadClipboardImageAsDataUri } = require('./src/CLI/image_input');
26
32
 
27
33
  // Startup Info
28
34
  const startupConfig = readConfig();
29
- const startupModel = startupConfig.geminiModel || 'gemini-2.5-flash';
35
+ const startupProvider = startupConfig.aiProvider || 'gemini';
36
+ const startupModel = startupProvider === 'openai'
37
+ ? (startupConfig.openaiModel || 'gpt-4o')
38
+ : startupProvider === 'anthropic'
39
+ ? (startupConfig.anthropicModel || 'claude-3-5-sonnet-latest')
40
+ : startupProvider === 'local_openai'
41
+ ? (startupConfig.localModelName || 'local-model')
42
+ : startupProvider === 'ollama'
43
+ ? (startupConfig.ollamaModel || 'llama3:latest')
44
+ : (startupConfig.geminiModel || 'gemini-2.5-flash');
30
45
  const startupNow = new Date();
31
46
  const startupTime = startupNow.toLocaleString('th-TH', {
32
47
  day: '2-digit', month: '2-digit', year: 'numeric',
33
48
  hour: '2-digit', minute: '2-digit', hour12: false
34
49
  }).replace(',', '');
35
- console.log(`\x1b[38;5;121m[Mint] v${pkg.version} | ${startupTime} | Active Model: ${startupModel}\x1b[0m`);
50
+ console.log(`\x1b[38;5;121m[Mint] v${pkg.version} | ${startupTime} | Active AI: ${startupProvider} • ${startupModel}\x1b[0m`);
36
51
 
37
52
  // ANSI Colors
38
53
  const colors = {
@@ -45,6 +60,22 @@ const colors = {
45
60
  yellow: "\x1b[33m"
46
61
  };
47
62
 
63
+ let isExiting = false;
64
+
65
+ function exitWithGoodbye(code = 0) {
66
+ if (isExiting) return;
67
+ isExiting = true;
68
+
69
+ process.stdout.write('\x1b[?1000l\x1b[?1002l\x1b[?1003l\x1b[?1006l');
70
+ process.stdout.write('\x1b[?25h');
71
+ console.log(`\n${colors.pink}Goodbye! See you again soon!${colors.reset}\n`);
72
+ process.exit(code);
73
+ }
74
+
75
+ process.once('SIGINT', () => {
76
+ exitWithGoodbye(0);
77
+ });
78
+
48
79
  function formatProgress(info) {
49
80
  if (typeof info === 'string') return `${colors.gray}[Mint Code] ${info}${colors.reset}`;
50
81
 
@@ -77,6 +108,68 @@ function formatProgress(info) {
77
108
  return ` ${icon} ${colors.bright}${label}${colors.reset} ${color}${content}${colors.reset}`;
78
109
  }
79
110
 
111
+ function formatMemoryInteractions(interactions, title = 'Remembered interactions') {
112
+ if (!Array.isArray(interactions) || interactions.length === 0) {
113
+ return `${title}:\n(no memories found)`;
114
+ }
115
+
116
+ const lines = [`${title}:`];
117
+ interactions.forEach((item, index) => {
118
+ const when = item.created_at ? ` (${item.created_at})` : '';
119
+ const id = item.id ? `#${item.id} ` : '';
120
+ lines.push(`${index + 1}. ${id}User${when}: ${item.user_text}`);
121
+ lines.push(` Mint: ${item.ai_text}`);
122
+ });
123
+ return lines.join('\n');
124
+ }
125
+
126
+ const sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms));
127
+
128
+ function splitResponseSentences(text) {
129
+ const normalized = String(text || '').replace(/\r\n/g, '\n').trim();
130
+ if (!normalized) return [];
131
+
132
+ const sentences = [];
133
+ let buffer = '';
134
+ for (const char of normalized) {
135
+ buffer += char;
136
+ if (/[.!?。!?…\n]/u.test(char)) {
137
+ const sentence = buffer.trim();
138
+ if (sentence) sentences.push(sentence);
139
+ buffer = '';
140
+ }
141
+ }
142
+
143
+ const rest = buffer.trim();
144
+ if (rest) sentences.push(rest);
145
+ return sentences.length > 0 ? sentences : [normalized];
146
+ }
147
+
148
+ function learnSkillFile(filePath) {
149
+ const targetPath = path.resolve(process.cwd(), filePath);
150
+ if (!fs.existsSync(targetPath)) {
151
+ throw new Error(`File not found: ${targetPath}`);
152
+ }
153
+
154
+ const stat = fs.statSync(targetPath);
155
+ if (!stat.isFile()) {
156
+ throw new Error(`Path is not a file: ${targetPath}`);
157
+ }
158
+
159
+ const ext = path.extname(targetPath).toLowerCase();
160
+ if (ext !== '.md' && ext !== '.txt') {
161
+ throw new Error('Mint learn currently supports .md and .txt files only.');
162
+ }
163
+
164
+ const maxBytes = 256 * 1024;
165
+ if (stat.size > maxBytes) {
166
+ throw new Error(`File is too large (${stat.size} bytes). Limit is ${maxBytes} bytes.`);
167
+ }
168
+
169
+ const content = fs.readFileSync(targetPath, 'utf8');
170
+ return memoryStore.addLearnedSkill(path.basename(targetPath), targetPath, content);
171
+ }
172
+
80
173
  const program = new Command();
81
174
 
82
175
  program
@@ -84,13 +177,37 @@ program
84
177
  .description('Mint - Your Personal AI Assistant CLI')
85
178
  .version(pkg.version);
86
179
 
180
+ program.hook('preAction', async (thisCommand, actionCommand) => {
181
+ if (actionCommand.name() === 'update' || process.env.MINT_SKIP_AUTO_UPDATE === '1') {
182
+ return;
183
+ }
184
+
185
+ const config = readConfig();
186
+ if (config.enableAutoUpdate === false) {
187
+ return;
188
+ }
189
+
190
+ if (!shouldRunAutoUpdate(config)) {
191
+ return;
192
+ }
193
+
194
+ console.log(`${colors.gray}[Mint Update] Checking for updates...${colors.reset}`);
195
+ const result = await runStartupAutoUpdate(config, writeConfig);
196
+ if (result.status === 'updated') {
197
+ console.log(`${colors.mint}[Mint Update] ${result.message}${colors.reset}`);
198
+ } else if (result.status === 'error') {
199
+ console.log(`${colors.gray}[Mint Update] ${result.message}${colors.reset}`);
200
+ }
201
+ });
202
+
87
203
  // Chat Command (Interactive Mode)
88
204
  program
89
205
  .command('chat', { isDefault: true })
90
206
  .description('Start interactive chat session with Mint')
91
207
  .argument('[message]', 'Initial message to send to Mint')
92
- .action(async (message) => {
93
- await startInteractiveChat(message);
208
+ .option('-i, --image <path>', 'Attach an image file to the initial message')
209
+ .action(async (message, options) => {
210
+ await startInteractiveChat(message, { imagePath: options.image });
94
211
  });
95
212
 
96
213
  // Onboard Command
@@ -124,6 +241,57 @@ program
124
241
  displayFeatures();
125
242
  });
126
243
 
244
+ program
245
+ .command('learn')
246
+ .description('Read a local markdown/text file and remember it as a Mint skill')
247
+ .argument('[filePath]', 'Path to a .md or .txt skill/instruction file')
248
+ .option('--delete <idOrPathOrName>', 'Delete a learned skill by id, path, or name')
249
+ .option('--list', 'List learned skills')
250
+ .action((filePath, options) => {
251
+ try {
252
+ if (options.list) {
253
+ const skills = memoryStore.getLearnedSkills(50);
254
+ if (skills.length === 0) {
255
+ console.log(`\n${colors.gray}No learned skills stored.${colors.reset}\n`);
256
+ return;
257
+ }
258
+ console.log(`\n${colors.bright}Learned Skills:${colors.reset}`);
259
+ skills.forEach(skill => {
260
+ console.log(`${colors.mint}#${skill.id}${colors.reset} ${skill.name}`);
261
+ console.log(` ${colors.gray}${skill.source_path}${colors.reset}`);
262
+ });
263
+ console.log('');
264
+ return;
265
+ }
266
+
267
+ if (options.delete) {
268
+ const deleted = memoryStore.deleteLearnedSkill(options.delete);
269
+ if (deleted > 0) {
270
+ console.log(`\n${colors.mint}✓${colors.reset} Deleted learned skill: ${options.delete}\n`);
271
+ } else {
272
+ console.log(`\n${colors.pink}✗${colors.reset} Learned skill not found: ${options.delete}\n`);
273
+ process.exitCode = 1;
274
+ }
275
+ return;
276
+ }
277
+
278
+ if (!filePath) {
279
+ throw new Error('Usage: mint learn <path-to-skill.md>');
280
+ }
281
+
282
+ const learned = learnSkillFile(filePath);
283
+ console.log(`\n${colors.mint}✓${colors.reset} Learned skill: ${learned.name}`);
284
+ console.log(`${colors.gray}Path: ${learned.source_path}${colors.reset}`);
285
+ if (learned.stored_length < learned.content_length) {
286
+ console.log(`${colors.gray}Stored first ${learned.stored_length} of ${learned.content_length} characters.${colors.reset}`);
287
+ }
288
+ console.log('');
289
+ } catch (error) {
290
+ console.error(`\n${colors.pink}Learn failed:${colors.reset} ${error.message}\n`);
291
+ process.exitCode = 1;
292
+ }
293
+ });
294
+
127
295
  // Task Command (Autonomous Background Task)
128
296
  program
129
297
  .command('task')
@@ -139,6 +307,32 @@ program
139
307
  console.log(`${colors.gray}You will receive a notification when it's done.${colors.reset}\n`);
140
308
  });
141
309
 
310
+ program
311
+ .command('update')
312
+ .description('Check for and install the latest Mint CLI version from npm')
313
+ .option('--check', 'Only check whether an update is available')
314
+ .option('--dry-run', 'Show the npm update operation without installing')
315
+ .action(async (options) => {
316
+ console.log(`\n${colors.mint}${colors.bright}[Mint Update]${colors.reset} Checking npm for updates...`);
317
+
318
+ try {
319
+ const result = await runUpdate({
320
+ checkOnly: options.check === true,
321
+ dryRun: options.dryRun === true
322
+ });
323
+
324
+ const color = result.status === 'error' ? colors.pink : colors.mint;
325
+ console.log(`${color}${result.message}${colors.reset}\n`);
326
+
327
+ if (result.status === 'error') {
328
+ process.exitCode = 1;
329
+ }
330
+ } catch (error) {
331
+ console.error(`${colors.pink}Update failed: ${error.message}${colors.reset}\n`);
332
+ process.exitCode = 1;
333
+ }
334
+ });
335
+
142
336
  program
143
337
  .command('mcp')
144
338
  .description('Manage MCP (Model Context Protocol) servers')
@@ -212,17 +406,51 @@ program
212
406
  })
213
407
  );
214
408
 
409
+ program
410
+ .command('gmail')
411
+ .description('Manage Gmail integration')
412
+ .addCommand(new Command('auth')
413
+ .description('Open Google OAuth login and save a Gmail refresh token')
414
+ .option('--port <port>', 'Local callback port, defaults to a random available port')
415
+ .option('--no-open', 'Print the auth link without opening a browser')
416
+ .action(async (options) => {
417
+ try {
418
+ const result = await runGmailAuth({
419
+ port: options.port ? Number(options.port) : 0,
420
+ openBrowser: options.open,
421
+ logger: console
422
+ });
423
+ console.log(`\n${colors.mint}✓${colors.reset} Gmail connected for ${result.userId}. Refresh token saved.`);
424
+ console.log(`${colors.gray}Scopes: ${result.scopes.join(', ')}${colors.reset}\n`);
425
+ } catch (error) {
426
+ console.error(`\n${colors.pink}Gmail auth failed:${colors.reset} ${error.message}\n`);
427
+ process.exitCode = 1;
428
+ }
429
+ })
430
+ );
431
+
215
432
  program
216
433
  .command('code')
217
434
  .description('Run Mint in workspace-aware coding mode for the current project')
218
435
  .argument('<task>', 'Coding task to execute in the current working directory')
219
- .action(async (task) => {
436
+ .option('-i, --image <path>', 'Attach an image file as context for the coding task')
437
+ .action(async (task, options) => {
220
438
  console.log(`\n${colors.mint}${colors.bright}[Mint Code]${colors.reset} Workspace: ${process.cwd()}`);
221
- console.log(`${colors.gray}[Mint Code] Task: ${task}${colors.reset}\n`);
222
439
 
223
440
  try {
224
- const result = await executeCodeTask(task, {
441
+ let effectiveTask = task;
442
+ let image = null;
443
+ if (options.image) {
444
+ image = loadImageAsDataUri(options.image);
445
+ console.log(`${colors.gray}[Mint Code] Image: ${image.path}${colors.reset}`);
446
+ }
447
+
448
+ console.log(`${colors.gray}[Mint Code] Task: ${task}${colors.reset}\n`);
449
+
450
+ const result = await executeCodeTask(effectiveTask, {
225
451
  cwd: process.cwd(),
452
+ imageDataUri: image ? image.dataUri : null,
453
+ imagePath: image ? image.path : null,
226
454
  onProgress: (info) => {
227
455
  console.log(formatProgress(info));
228
456
  },
@@ -239,15 +467,108 @@ program
239
467
  }
240
468
  });
241
469
 
242
- program.parse(process.argv);
470
+ program.parseAsync(process.argv).catch((error) => {
471
+ console.error(`${colors.pink}${error.message}${colors.reset}`);
472
+ process.exitCode = 1;
473
+ });
243
474
 
244
475
  /**
245
476
  * The Interactive Chat Loop — Gemini-style TUI
246
477
  */
247
- async function startInteractiveChat(initialMessage = null) {
478
+ async function startInteractiveChat(initialMessage = null, options = {}) {
248
479
  let lastResponseText = "";
249
- const { screen, appendMessage, streamMessage, setThinking, updateStatusModel, copyLastResponse, requestApproval, setMode, appendCodeStep, updateWorkspace, askUser } = createChatUI({
250
- onSubmit: async (text) => {
480
+ let recentImageContextText = "";
481
+ const formatErrorMessage = (err) => err && err.message ? err.message : String(err || 'Unknown error');
482
+ const streamAssistantSentences = async (text, appendMessage, metadata = {}, streamMessage = null) => {
483
+ const sentences = splitResponseSentences(text);
484
+ if (typeof streamMessage === 'function') {
485
+ const stream = streamMessage(metadata);
486
+ for (let index = 0; index < sentences.length; index++) {
487
+ const prefix = index === 0 ? '' : ' ';
488
+ stream.appendChunk(`${prefix}${sentences[index]}`);
489
+ if (index < sentences.length - 1) {
490
+ await sleep(90);
491
+ }
492
+ }
493
+ stream.finalize();
494
+ return;
495
+ }
496
+
497
+ for (let index = 0; index < sentences.length; index++) {
498
+ appendMessage('assistant', sentences[index], index === 0 ? metadata : {});
499
+ if (index < sentences.length - 1) {
500
+ await sleep(90);
501
+ }
502
+ }
503
+ };
504
+ const sendImageMessage = async ({ images, image, prompt, appendMessage, streamMessage, setThinking, appendCodeStep }) => {
505
+ const imageList = images || (image ? [image] : []);
506
+ const message = prompt || 'Analyze this image.';
507
+ const labels = imageList.map((_, index) => `[Image #${index + 1}]`).join(' ');
508
+ const displayMessage = labels && message.includes(labels)
509
+ ? message
510
+ : `${message}\n${labels}`;
511
+ appendMessage('user', displayMessage);
512
+ if (appendCodeStep) {
513
+ appendCodeStep({
514
+ thought: imageList.length > 1
515
+ ? `Analyzing ${imageList.length} attached images before answering.`
516
+ : 'Analyzing the attached image before answering.'
517
+ });
518
+ }
519
+
520
+ let seconds = 0;
521
+ setThinking(true, seconds);
522
+ const timer = setInterval(() => {
523
+ seconds++;
524
+ setThinking(true, seconds);
525
+ }, 1000);
526
+
527
+ try {
528
+ const result = await handleChat(message, imageList.map(item => item.dataUri), null);
529
+ clearInterval(timer);
530
+ setThinking(false);
531
+ const responseText = result.response || '';
532
+ lastResponseText = responseText;
533
+ recentImageContextText = [
534
+ `Recent image context: the user attached ${imageList.length} image(s) labelled ${labels || '[Image #1]'}.`,
535
+ 'The terminal UI displays image attachments as labels only; it does not render thumbnails inside the chat.',
536
+ `Assistant response to those image(s): ${responseText}`
537
+ ].join('\n');
538
+ await streamAssistantSentences(responseText, appendMessage, { providerInfo: result.providerInfo }, streamMessage);
539
+ return responseText;
540
+ } catch (err) {
541
+ clearInterval(timer);
542
+ setThinking(false);
543
+ appendMessage('error', formatErrorMessage(err));
544
+ return '';
545
+ }
546
+ };
547
+
548
+ const ui = await createChatUI({
549
+ onPasteImage: async () => {
550
+ try {
551
+ const image = loadClipboardImageAsDataUri();
552
+ return { label: image.path, image };
553
+ } catch (err) {
554
+ throw new Error(formatErrorMessage(err));
555
+ }
556
+ },
557
+ onSubmit: async (text, submitOptions = {}) => {
558
+ const { screen, appendMessage, streamMessage, setThinking, updateStatusModel, copyLastResponse, requestApproval, setMode, appendCodeStep, updateWorkspace, askUser, attachImage, setInputText, setPendingPasteText, setFastMode, toggleFastMode, getFastMode } = ui;
559
+ if (submitOptions.images && submitOptions.images.length > 0) {
560
+ const images = submitOptions.images.map(item => item.image || item);
561
+ await sendImageMessage({
562
+ images,
563
+ prompt: text.trim() || 'Analyze this image.',
564
+ appendMessage,
565
+ streamMessage,
566
+ setThinking,
567
+ appendCodeStep
568
+ });
569
+ return;
570
+ }
571
+
251
572
  if (text.startsWith('/')) {
252
573
  if (text.startsWith('/agent')) {
253
574
  const args = text.split(' ');
@@ -339,14 +660,33 @@ async function startInteractiveChat(initialMessage = null) {
339
660
  } else {
340
661
  // Other slash commands
341
662
  const fakeRl = { close: () => { } };
342
- appendMessage('user', text);
343
- await handleSlashCommandUI(text, appendMessage, updateStatusModel, copyLastResponse, setThinking, requestApproval, setMode, appendCodeStep, updateWorkspace);
663
+ if (!text.startsWith('/image') && !text.startsWith('/paste')) {
664
+ appendMessage('user', text);
665
+ }
666
+ const slashResult = await handleSlashCommandUI(text, appendMessage, updateStatusModel, copyLastResponse, setThinking, requestApproval, setMode, appendCodeStep, updateWorkspace, {
667
+ sendImageMessage,
668
+ formatErrorMessage,
669
+ attachImage,
670
+ setInputText,
671
+ setPendingPasteText,
672
+ setFastMode,
673
+ toggleFastMode,
674
+ getFastMode,
675
+ streamAssistantSentences,
676
+ streamMessage
677
+ });
678
+ if (slashResult && slashResult.lastResponseText) {
679
+ lastResponseText = slashResult.lastResponseText;
680
+ }
344
681
  return;
345
682
  }
346
683
  }
347
684
  appendMessage('user', text);
348
685
 
349
686
  const transcript = await getChatTranscript();
687
+ const contextualHistory = recentImageContextText
688
+ ? [...transcript, { sender: 'system', text: recentImageContextText, timestamp: new Date().toISOString() }]
689
+ : transcript;
350
690
  if (setMode) setMode('Agent');
351
691
 
352
692
  let seconds = 0;
@@ -360,45 +700,63 @@ async function startInteractiveChat(initialMessage = null) {
360
700
  const config = require('./src/System/config_manager').readConfig();
361
701
  const availableProviders = require('./src/System/config_manager').getAvailableProviders(config);
362
702
  const preferredProvider = require('./src/CLI/code_agent')._helpers.selectSupportedCodeProvider(config, availableProviders);
703
+ let streamedFinalSummary = false;
363
704
 
364
705
  const result = await executeCodeTask(text, {
365
706
  cwd: process.cwd(),
366
707
  requestApproval,
367
708
  askUser,
368
709
  provider: preferredProvider,
369
- history: transcript,
710
+ history: contextualHistory,
370
711
  onProgress: (info) => {
371
712
  if (appendCodeStep) appendCodeStep(info);
713
+ },
714
+ onFinalSummary: async (info) => {
715
+ clearInterval(timer);
716
+ setThinking(false);
717
+ streamedFinalSummary = true;
718
+ await streamAssistantSentences(info.summary, appendMessage, { providerInfo: info.providerInfo }, streamMessage);
372
719
  }
373
720
  });
374
721
 
375
722
  clearInterval(timer);
376
723
  setThinking(false);
377
724
  lastResponseText = result.summary;
378
- appendMessage('assistant', result.summary);
725
+ if (!streamedFinalSummary) {
726
+ await streamAssistantSentences(result.summary, appendMessage, { providerInfo: result.providerInfo }, streamMessage);
727
+ }
379
728
 
380
729
  } catch (err) {
381
730
  clearInterval(timer);
382
731
  setThinking(false);
383
- appendMessage('error', err.message);
732
+ appendMessage('error', formatErrorMessage(err));
384
733
  } finally {
385
- if (setMode) setMode('Chat');
734
+ if (setMode) setMode('Agent');
386
735
  }
387
736
  },
388
737
  onExit: () => {
389
- screen.destroy();
390
- // Explicitly restore terminal state and disable ALL mouse tracking modes
391
- process.stdout.write('\x1b[?1000l\x1b[?1002l\x1b[?1003l\x1b[?1006l');
392
- process.stdout.write('\x1b[?25h'); // Show cursor
393
- console.log(`\n${colors.pink}Goodbye! See you again soon!${colors.reset}\n`);
394
- process.exit(0);
738
+ exitWithGoodbye(0);
395
739
  }
396
740
  });
397
741
 
742
+ // Handle initial image if passed via CLI option.
743
+ if (options.imagePath) {
744
+ const { appendMessage, streamMessage, setThinking, appendCodeStep } = ui;
745
+ const image = loadImageAsDataUri(options.imagePath);
746
+ const prompt = initialMessage || 'Analyze this image.';
747
+ await sendImageMessage({ images: [image], prompt, appendMessage, streamMessage, setThinking, appendCodeStep });
748
+
749
+ return;
750
+ }
751
+
398
752
  // Handle initial message if passed via CLI arg
399
753
  if (initialMessage) {
754
+ const { appendMessage, streamMessage, setThinking, updateStatusModel, copyLastResponse, requestApproval, setMode, appendCodeStep, updateWorkspace, askUser } = ui;
400
755
  appendMessage('user', initialMessage);
401
756
  const transcript = await getChatTranscript();
757
+ const contextualHistory = recentImageContextText
758
+ ? [...transcript, { sender: 'system', text: recentImageContextText, timestamp: new Date().toISOString() }]
759
+ : transcript;
402
760
  if (setMode) setMode('Agent');
403
761
 
404
762
  let seconds = 0;
@@ -412,29 +770,38 @@ async function startInteractiveChat(initialMessage = null) {
412
770
  const config = require('./src/System/config_manager').readConfig();
413
771
  const availableProviders = require('./src/System/config_manager').getAvailableProviders(config);
414
772
  const preferredProvider = require('./src/CLI/code_agent')._helpers.selectSupportedCodeProvider(config, availableProviders);
773
+ let streamedFinalSummary = false;
415
774
 
416
775
  const result = await executeCodeTask(initialMessage, {
417
776
  cwd: process.cwd(),
418
777
  requestApproval,
419
778
  askUser,
420
779
  provider: preferredProvider,
421
- history: transcript,
780
+ history: contextualHistory,
422
781
  onProgress: (info) => {
423
782
  if (appendCodeStep) appendCodeStep(info);
783
+ },
784
+ onFinalSummary: async (info) => {
785
+ clearInterval(timer);
786
+ setThinking(false);
787
+ streamedFinalSummary = true;
788
+ await streamAssistantSentences(info.summary, appendMessage, { providerInfo: info.providerInfo }, streamMessage);
424
789
  }
425
790
  });
426
791
 
427
792
  clearInterval(timer);
428
793
  setThinking(false);
429
794
  lastResponseText = result.summary;
430
- appendMessage('assistant', result.summary);
795
+ if (!streamedFinalSummary) {
796
+ await streamAssistantSentences(result.summary, appendMessage, { providerInfo: result.providerInfo }, streamMessage);
797
+ }
431
798
 
432
799
  } catch (err) {
433
800
  clearInterval(timer);
434
801
  setThinking(false);
435
- appendMessage('error', err.message);
802
+ appendMessage('error', formatErrorMessage(err));
436
803
  } finally {
437
- if (setMode) setMode('Chat');
804
+ if (setMode) setMode('Agent');
438
805
  }
439
806
  }
440
807
  }
@@ -442,7 +809,7 @@ async function startInteractiveChat(initialMessage = null) {
442
809
  /**
443
810
  * Handles slash commands within the TUI context
444
811
  */
445
- async function handleSlashCommandUI(input, appendMessage, updateStatusModel, copyLastResponse, setThinking, requestApproval, setMode, appendCodeStep, updateWorkspace) {
812
+ async function handleSlashCommandUI(input, appendMessage, updateStatusModel, copyLastResponse, setThinking, requestApproval, setMode, appendCodeStep, updateWorkspace, helpers = {}) {
446
813
  const parts = input.split(' ');
447
814
  const command = parts[0].toLowerCase();
448
815
  const args = parts.slice(1);
@@ -452,9 +819,14 @@ async function handleSlashCommandUI(input, appendMessage, updateStatusModel, cop
452
819
  case '/?':
453
820
  appendMessage('system', [
454
821
  'Mint Slash Commands:',
822
+ ' /image <path> [prompt] — Attach an image from your computer',
823
+ ' /paste [prompt] — Attach an image from your clipboard',
824
+ ' /fast [on|off] — Hide or show thinking/progress output',
825
+ ' /learn <path> — Remember a .md/.txt file as a Mint skill',
455
826
  ' /code <task> — Force workspace Code Mode',
456
827
  ' /cd <path> — Change current working directory',
457
828
  ' /models [name] — List or switch Gemini models',
829
+ ' /memory [cmd] — Manage long-term memory',
458
830
  ' /config — Show current configuration',
459
831
  ' /copy — Copy last response to clipboard',
460
832
  ' /clear — Clear conversation history',
@@ -463,6 +835,195 @@ async function handleSlashCommandUI(input, appendMessage, updateStatusModel, cop
463
835
  ].join('\n'));
464
836
  break;
465
837
 
838
+ case '/fast': {
839
+ if (!helpers.toggleFastMode || !helpers.setFastMode || !helpers.getFastMode) {
840
+ appendMessage('error', 'Fast mode is not available in this UI.');
841
+ break;
842
+ }
843
+
844
+ const option = (args[0] || '').toLowerCase();
845
+ let enabled;
846
+ if (option === 'on' || option === 'true' || option === '1') {
847
+ enabled = helpers.setFastMode(true);
848
+ } else if (option === 'off' || option === 'false' || option === '0') {
849
+ enabled = helpers.setFastMode(false);
850
+ } else if (option === 'status') {
851
+ enabled = helpers.getFastMode();
852
+ } else {
853
+ enabled = helpers.toggleFastMode();
854
+ }
855
+
856
+ appendMessage('system', `Fast mode: ${enabled ? 'ON' : 'OFF'}`);
857
+ break;
858
+ }
859
+
860
+ case '/learn': {
861
+ const filePath = input.slice(command.length).trim();
862
+ if (!filePath) {
863
+ appendMessage('system', 'Usage: /learn <path-to-skill.md>');
864
+ break;
865
+ }
866
+
867
+ try {
868
+ const learned = learnSkillFile(filePath);
869
+ appendMessage('system', [
870
+ `✓ Learned skill: ${learned.name}`,
871
+ `Path: ${learned.source_path}`,
872
+ learned.stored_length < learned.content_length
873
+ ? `Stored first ${learned.stored_length} of ${learned.content_length} characters.`
874
+ : `Stored ${learned.stored_length} characters.`
875
+ ].join('\n'));
876
+ } catch (err) {
877
+ appendMessage('error', err && err.message ? err.message : String(err || 'Unknown error'));
878
+ }
879
+ break;
880
+ }
881
+
882
+ case '/image': {
883
+ if (args.length === 0) {
884
+ appendMessage('system', 'Usage: /image <path> [prompt]');
885
+ break;
886
+ }
887
+
888
+ const imagePath = args[0];
889
+ const prompt = args.slice(1).join(' ').trim();
890
+
891
+ try {
892
+ const image = loadImageAsDataUri(imagePath);
893
+ if (helpers.attachImage) {
894
+ helpers.attachImage({ label: image.path, image });
895
+ if (prompt && helpers.setInputText) {
896
+ helpers.setInputText(prompt);
897
+ }
898
+ appendMessage('system', 'Attached image. Press Enter to send.');
899
+ } else {
900
+ appendMessage('error', 'Image attachment is not available in this UI.');
901
+ }
902
+ } catch (err) {
903
+ appendMessage('error', err && err.message ? err.message : String(err || 'Unknown error'));
904
+ }
905
+ break;
906
+ }
907
+
908
+ case '/paste': {
909
+ try {
910
+ const image = loadClipboardImageAsDataUri();
911
+ if (helpers.attachImage) {
912
+ helpers.attachImage({ label: image.path, image });
913
+ const prompt = args.join(' ').trim();
914
+ if (prompt && helpers.setInputText) {
915
+ helpers.setInputText(prompt);
916
+ }
917
+ appendMessage('system', 'Attached clipboard image. Press Enter to send.');
918
+ } else {
919
+ appendMessage('error', 'Image attachment is not available in this UI.');
920
+ }
921
+ } catch (err) {
922
+ appendMessage('error', helpers.formatErrorMessage ? helpers.formatErrorMessage(err) : (err && err.message ? err.message : String(err || 'Unknown error')));
923
+ }
924
+ break;
925
+ }
926
+
927
+ case '/memory': {
928
+ const subCommand = (args[0] || 'list').toLowerCase();
929
+ const query = args.slice(1).join(' ').trim();
930
+
931
+ if (subCommand === 'help') {
932
+ appendMessage('system', [
933
+ 'Memory Commands:',
934
+ ' /memory list [n] — Show recent remembered interactions',
935
+ ' /memory search <query> — Search remembered interactions',
936
+ ' /memory skills — Show learned skill files',
937
+ ' /memory skills delete <id|path|name> — Delete a learned skill',
938
+ ' /memory profile — Show remembered profile fields',
939
+ ' /memory context [q] — Show context Mint injects into prompts',
940
+ ' /memory delete <id> — Delete one remembered interaction',
941
+ ' /memory export [path] — Export memory snapshot as JSON',
942
+ ' /memory clear — Clear episodic interaction memories'
943
+ ].join('\n'));
944
+ break;
945
+ }
946
+
947
+ if (subCommand === 'profile') {
948
+ const profile = memoryStore.getAllProfile();
949
+ appendMessage('system', Object.keys(profile).length
950
+ ? JSON.stringify(profile, null, 2)
951
+ : 'No profile memory stored yet.');
952
+ break;
953
+ }
954
+
955
+ if (subCommand === 'skills') {
956
+ if ((args[1] || '').toLowerCase() === 'delete') {
957
+ const identifier = args.slice(2).join(' ').trim();
958
+ if (!identifier) {
959
+ appendMessage('system', 'Usage: /memory skills delete <id|path|name>');
960
+ break;
961
+ }
962
+ const deleted = memoryStore.deleteLearnedSkill(identifier);
963
+ appendMessage('system', deleted > 0
964
+ ? `Deleted learned skill: ${identifier}`
965
+ : `Learned skill not found: ${identifier}`);
966
+ break;
967
+ }
968
+
969
+ const skills = memoryStore.getLearnedSkills(20);
970
+ appendMessage('system', skills.length
971
+ ? [
972
+ 'Learned skills:',
973
+ ...skills.map((skill) => `#${skill.id} ${skill.name}\n ${skill.source_path}`)
974
+ ].join('\n')
975
+ : 'No learned skills stored yet.');
976
+ break;
977
+ }
978
+
979
+ if (subCommand === 'context') {
980
+ const ctx = memoryStore.getUserContext(query);
981
+ appendMessage('system', ctx || 'No memory context stored yet.');
982
+ break;
983
+ }
984
+
985
+ if (subCommand === 'search') {
986
+ if (!query) {
987
+ appendMessage('system', 'Usage: /memory search <query>');
988
+ break;
989
+ }
990
+ const results = memoryStore.searchInteractions(query, 10);
991
+ appendMessage('system', formatMemoryInteractions(results, `Search results for "${query}"`));
992
+ break;
993
+ }
994
+
995
+ if (subCommand === 'export') {
996
+ const exportPath = query
997
+ ? path.resolve(process.cwd(), query)
998
+ : path.join(process.cwd(), `mint-memory-export-${Date.now()}.json`);
999
+ fs.writeFileSync(exportPath, JSON.stringify(memoryStore.exportMemorySnapshot(), null, 2), 'utf8');
1000
+ appendMessage('system', `Memory exported to: ${exportPath}`);
1001
+ break;
1002
+ }
1003
+
1004
+ if (subCommand === 'delete') {
1005
+ const id = Number.parseInt(args[1] || '', 10);
1006
+ if (!Number.isFinite(id)) {
1007
+ appendMessage('system', 'Usage: /memory delete <id>');
1008
+ break;
1009
+ }
1010
+ const deleted = memoryStore.deleteInteractionMemory(id);
1011
+ appendMessage('system', deleted ? `Deleted memory #${id}.` : `Memory #${id} was not found.`);
1012
+ break;
1013
+ }
1014
+
1015
+ if (subCommand === 'clear') {
1016
+ memoryStore.clearInteractionMemories();
1017
+ appendMessage('system', 'Cleared episodic interaction memories. Profile memory is unchanged.');
1018
+ break;
1019
+ }
1020
+
1021
+ const limit = Number.parseInt(args[0] || '10', 10);
1022
+ const interactions = memoryStore.getRecentInteractions(Number.isFinite(limit) ? limit : 10);
1023
+ appendMessage('system', formatMemoryInteractions(interactions, 'Recent remembered interactions'));
1024
+ break;
1025
+ }
1026
+
466
1027
  case '/cd':
467
1028
  if (args.length === 0) {
468
1029
  appendMessage('system', `Current Directory: ${process.cwd()}`);
@@ -539,8 +1100,12 @@ async function handleSlashCommandUI(input, appendMessage, updateStatusModel, cop
539
1100
  await runChatRoutedTask(`/code ${args.join(' ')}`, {
540
1101
  appendMessage,
541
1102
  setThinking,
1103
+ requestApproval,
542
1104
  appendCodeStep,
543
1105
  setMode,
1106
+ streamAssistantSentences: helpers.streamAssistantSentences,
1107
+ streamMessage: helpers.streamMessage,
1108
+ askUser: () => Promise.resolve(''),
544
1109
  history: await getChatTranscript()
545
1110
  });
546
1111
  break;
@@ -575,7 +1140,7 @@ async function handleSlashCommandUI(input, appendMessage, updateStatusModel, cop
575
1140
 
576
1141
  case '/exit':
577
1142
  case '/quit':
578
- process.exit(0);
1143
+ exitWithGoodbye(0);
579
1144
  break;
580
1145
 
581
1146
  default: