@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
@@ -0,0 +1,35 @@
1
+ const axios = require('axios');
2
+
3
+ class BraveSearchBridge {
4
+ constructor(credentials) {
5
+ this.apiKey = credentials.apiKey;
6
+ }
7
+
8
+ async search(query) {
9
+ if (!this.apiKey) {
10
+ throw new Error('Brave Search API Key is required.');
11
+ }
12
+
13
+ try {
14
+ const response = await axios.get('https://api.search.brave.com/res/v1/web/search', {
15
+ params: { q: query, count: 5 },
16
+ headers: {
17
+ 'Accept': 'application/json',
18
+ 'Accept-Encoding': 'gzip',
19
+ 'X-Subscription-Token': this.apiKey
20
+ }
21
+ });
22
+
23
+ const results = response.data.web ? response.data.web.results : [];
24
+ return results.map(item => ({
25
+ title: item.title,
26
+ snippet: item.description,
27
+ link: item.url
28
+ }));
29
+ } catch (err) {
30
+ throw new Error(`Brave Search Failed: ${err.message}`);
31
+ }
32
+ }
33
+ }
34
+
35
+ module.exports = BraveSearchBridge;
@@ -0,0 +1,68 @@
1
+ const { Client, GatewayIntentBits, Partials } = require('discord.js');
2
+ const { handleChat } = require('../AI_Brain/Gemini_API');
3
+
4
+ class DiscordBridge {
5
+ constructor(token) {
6
+ this.token = token;
7
+ this.client = new Client({
8
+ intents: [
9
+ GatewayIntentBits.Guilds,
10
+ GatewayIntentBits.GuildMessages,
11
+ GatewayIntentBits.MessageContent,
12
+ GatewayIntentBits.DirectMessages
13
+ ],
14
+ partials: [Partials.Channel]
15
+ });
16
+ }
17
+
18
+ async connect() {
19
+ this.client.on('ready', () => {
20
+ console.log(`[Discord Bridge] Logged in as ${this.client.user.tag}!`);
21
+ });
22
+
23
+ this.client.on('messageCreate', async (message) => {
24
+ // Ignore bot messages
25
+ if (message.author.bot) return;
26
+
27
+ // Handle DMs or Mentions
28
+ const isDM = !message.guild;
29
+ const isMentioned = message.mentions.has(this.client.user);
30
+
31
+ if (isDM || isMentioned) {
32
+ try {
33
+ // Clean up the message if it's a mention
34
+ let cleanContent = message.content;
35
+ if (isMentioned) {
36
+ cleanContent = message.content.replace(`<@!${this.client.user.id}>`, '').replace(`<@${this.client.user.id}>`, '').trim();
37
+ }
38
+
39
+ if (!cleanContent) return;
40
+
41
+ // Show typing indicator
42
+ await message.channel.sendTyping();
43
+
44
+ // Send to Mint AI Brain
45
+ const result = await handleChat(cleanContent);
46
+
47
+ // Reply to user
48
+ if (result && result.response) {
49
+ await message.reply(result.response);
50
+ }
51
+ } catch (err) {
52
+ console.error('[Discord Bridge] Error processing message:', err);
53
+ await message.reply('ขออภัยค่ะ เกิดข้อผิดพลาดบางอย่างในการประมวลผลข้อความ');
54
+ }
55
+ }
56
+ });
57
+
58
+ await this.client.login(this.token);
59
+ }
60
+
61
+ async disconnect() {
62
+ if (this.client) {
63
+ await this.client.destroy();
64
+ }
65
+ }
66
+ }
67
+
68
+ module.exports = DiscordBridge;
@@ -0,0 +1,38 @@
1
+ const axios = require('axios');
2
+
3
+ class GoogleSearchBridge {
4
+ constructor(credentials) {
5
+ this.apiKey = credentials.apiKey;
6
+ this.cx = credentials.cx; // Custom Search Engine ID
7
+ }
8
+
9
+ async search(query) {
10
+ if (!this.apiKey || !this.cx) {
11
+ throw new Error('Google Search API Key and CX are required.');
12
+ }
13
+
14
+ try {
15
+ const response = await axios.get('https://www.googleapis.com/customsearch/v1', {
16
+ params: {
17
+ key: this.apiKey,
18
+ cx: this.cx,
19
+ q: query,
20
+ num: 5
21
+ }
22
+ });
23
+
24
+ const items = response.data.items || [];
25
+ return items.map(item => ({
26
+ title: item.title,
27
+ snippet: item.snippet,
28
+ link: item.link
29
+ }));
30
+ } catch (err) {
31
+ throw new Error(err.response && err.response.data && err.response.data.error
32
+ ? `Google Search API Error: ${err.response.data.error.message}`
33
+ : `Google Search Failed: ${err.message}`);
34
+ }
35
+ }
36
+ }
37
+
38
+ module.exports = GoogleSearchBridge;
@@ -0,0 +1,60 @@
1
+ const line = require('@line/bot-sdk');
2
+ const express = require('express');
3
+ const { handleChat } = require('../AI_Brain/Gemini_API');
4
+
5
+ class LineBridge {
6
+ constructor(credentials) {
7
+ this.config = {
8
+ channelAccessToken: credentials.accessToken,
9
+ channelSecret: credentials.secret,
10
+ };
11
+ this.port = credentials.port || 3000;
12
+ this.client = new line.messagingApi.MessagingApiClient({
13
+ channelAccessToken: credentials.accessToken
14
+ });
15
+ this.app = express();
16
+ }
17
+
18
+ async connect() {
19
+ this.app.post('/callback', line.middleware(this.config), (req, res) => {
20
+ Promise
21
+ .all(req.body.events.map(event => this.handleEvent(event)))
22
+ .then((result) => res.json(result))
23
+ .catch((err) => {
24
+ console.error('[LINE Bridge] Error:', err);
25
+ res.status(500).end();
26
+ });
27
+ });
28
+
29
+ this.server = this.app.listen(this.port, () => {
30
+ console.log(`[LINE Bridge] Listening for webhooks on port ${this.port}`);
31
+ console.log(`[LINE Bridge] Webhook URL should be: <YOUR_PUBLIC_URL>/callback`);
32
+ });
33
+ }
34
+
35
+ async handleEvent(event) {
36
+ if (event.type !== 'message' || event.message.type !== 'text') {
37
+ return Promise.resolve(null);
38
+ }
39
+
40
+ try {
41
+ const result = await handleChat(event.message.text);
42
+ if (result && result.response) {
43
+ return this.client.replyMessage({
44
+ replyToken: event.replyToken,
45
+ messages: [{ type: 'text', text: result.response }],
46
+ });
47
+ }
48
+ } catch (err) {
49
+ console.error('[LINE Bridge] Error processing event:', err);
50
+ }
51
+ }
52
+
53
+ async disconnect() {
54
+ if (this.server) {
55
+ this.server.close();
56
+ }
57
+ }
58
+ }
59
+
60
+ module.exports = LineBridge;
@@ -0,0 +1,53 @@
1
+ const { App } = require('@slack/bolt');
2
+ const { handleChat } = require('../AI_Brain/Gemini_API');
3
+
4
+ class SlackBridge {
5
+ constructor(credentials) {
6
+ this.app = new App({
7
+ token: credentials.botToken,
8
+ appToken: credentials.appToken,
9
+ socketMode: true
10
+ });
11
+ }
12
+
13
+ async connect() {
14
+ this.app.event('app_mention', async ({ event, say }) => {
15
+ try {
16
+ const text = event.text.replace(/<@.*?>/g, '').trim();
17
+ if (!text) return;
18
+
19
+ const result = await handleChat(text);
20
+ if (result && result.response) {
21
+ await say(result.response);
22
+ }
23
+ } catch (err) {
24
+ console.error('[Slack Bridge] Error processing app_mention:', err);
25
+ }
26
+ });
27
+
28
+ this.app.event('message', async ({ event, say }) => {
29
+ // Only respond in DMs
30
+ if (event.channel_type === 'im') {
31
+ try {
32
+ const result = await handleChat(event.text);
33
+ if (result && result.response) {
34
+ await say(result.response);
35
+ }
36
+ } catch (err) {
37
+ console.error('[Slack Bridge] Error processing message:', err);
38
+ }
39
+ }
40
+ });
41
+
42
+ await this.app.start();
43
+ console.log('[Slack Bridge] App started in Socket Mode!');
44
+ }
45
+
46
+ async disconnect() {
47
+ if (this.app) {
48
+ await this.app.stop();
49
+ }
50
+ }
51
+ }
52
+
53
+ module.exports = SlackBridge;
@@ -0,0 +1,49 @@
1
+ const { Telegraf } = require('telegraf');
2
+ const { handleChat } = require('../AI_Brain/Gemini_API');
3
+
4
+ class TelegramBridge {
5
+ constructor(token) {
6
+ this.token = token;
7
+ this.bot = new Telegraf(token);
8
+ }
9
+
10
+ async connect() {
11
+ this.bot.start((ctx) => ctx.reply('สวัสดีค่ะ! มิ้นท์พร้อมช่วยเหลือคุณใน Telegram แล้วนะคะ ✨'));
12
+
13
+ this.bot.on('text', async (ctx) => {
14
+ try {
15
+ // Show typing status
16
+ await ctx.sendChatAction('typing');
17
+
18
+ const message = ctx.message.text;
19
+ if (!message) return;
20
+
21
+ // Send to Mint AI Brain
22
+ const result = await handleChat(message);
23
+
24
+ // Reply to user
25
+ if (result && result.response) {
26
+ await ctx.reply(result.response);
27
+ }
28
+ } catch (err) {
29
+ console.error('[Telegram Bridge] Error processing message:', err);
30
+ await ctx.reply('ขออภัยค่ะ เกิดข้อผิดพลาดบางอย่างในการประมวลผลข้อความ');
31
+ }
32
+ });
33
+
34
+ this.bot.launch();
35
+ console.log('[Telegram Bridge] Bot started!');
36
+
37
+ // Enable graceful stop
38
+ process.once('SIGINT', () => this.bot.stop('SIGINT'));
39
+ process.once('SIGTERM', () => this.bot.stop('SIGTERM'));
40
+ }
41
+
42
+ async disconnect() {
43
+ if (this.bot) {
44
+ await this.bot.stop();
45
+ }
46
+ }
47
+ }
48
+
49
+ module.exports = TelegramBridge;
@@ -0,0 +1,55 @@
1
+ const { Client, LocalAuth } = require('whatsapp-web.js');
2
+ const qrcode = require('qrcode-terminal');
3
+ const { handleChat } = require('../AI_Brain/Gemini_API');
4
+
5
+ class WhatsappBridge {
6
+ constructor() {
7
+ this.client = new Client({
8
+ authStrategy: new LocalAuth({
9
+ dataPath: require('path').join(require('os').homedir(), '.config', 'mint', 'whatsapp-session')
10
+ }),
11
+ puppeteer: {
12
+ args: ['--no-sandbox']
13
+ }
14
+ });
15
+ }
16
+
17
+ async connect() {
18
+ this.client.on('qr', (qr) => {
19
+ console.log('[WhatsApp Bridge] Scan this QR code to login:');
20
+ qrcode.generate(qr, { small: true });
21
+ });
22
+
23
+ this.client.on('ready', () => {
24
+ console.log('[WhatsApp Bridge] Client is ready!');
25
+ });
26
+
27
+ this.client.on('message', async (msg) => {
28
+ try {
29
+ // Ignore messages from groups unless mentioned (simple implementation)
30
+ const chat = await msg.getChat();
31
+ if (chat.isGroup) {
32
+ // For groups, we could add a mention check here if desired
33
+ return;
34
+ }
35
+
36
+ const result = await handleChat(msg.body);
37
+ if (result && result.response) {
38
+ await msg.reply(result.response);
39
+ }
40
+ } catch (err) {
41
+ console.error('[WhatsApp Bridge] Error processing message:', err);
42
+ }
43
+ });
44
+
45
+ await this.client.initialize();
46
+ }
47
+
48
+ async disconnect() {
49
+ if (this.client) {
50
+ await this.client.destroy();
51
+ }
52
+ }
53
+ }
54
+
55
+ module.exports = WhatsappBridge;
@@ -1,6 +1,8 @@
1
1
  function parseCommand(aiResponse) {
2
2
  let action = { type: 'none', target: '' };
3
3
  let responseText = '';
4
+ let timestamp = null;
5
+ let providerInfo = null;
4
6
 
5
7
  if (typeof aiResponse === 'string') {
6
8
  // Attempt to parse string to JSON
@@ -8,6 +10,8 @@ function parseCommand(aiResponse) {
8
10
  const parsed = JSON.parse(aiResponse);
9
11
  action = parsed.action || action;
10
12
  responseText = parsed.response || '';
13
+ timestamp = parsed.timestamp || null;
14
+ providerInfo = parsed.providerInfo || null;
11
15
  } catch (e) {
12
16
  // Fallback for markdown
13
17
  const jsonMatch = aiResponse.match(/```json\n([\s\S]*?)\n```/) || aiResponse.match(/\{[\s\S]*\}/);
@@ -16,6 +20,8 @@ function parseCommand(aiResponse) {
16
20
  const parsed = JSON.parse(jsonMatch[jsonMatch.length > 1 ? 1 : 0]);
17
21
  action = parsed.action || action;
18
22
  responseText = parsed.response || '';
23
+ timestamp = parsed.timestamp || null;
24
+ providerInfo = parsed.providerInfo || null;
19
25
  } catch (err) {
20
26
  responseText = aiResponse;
21
27
  }
@@ -26,9 +32,14 @@ function parseCommand(aiResponse) {
26
32
  } else if (typeof aiResponse === 'object') {
27
33
  action = aiResponse.action || action;
28
34
  responseText = aiResponse.response || '';
35
+ timestamp = aiResponse.timestamp || null;
36
+ providerInfo = aiResponse.providerInfo || null;
29
37
  }
30
38
 
31
- return { response: responseText, action };
39
+ const parsedResponse = { response: responseText, action };
40
+ if (timestamp) parsedResponse.timestamp = timestamp;
41
+ if (providerInfo) parsedResponse.providerInfo = providerInfo;
42
+ return parsedResponse;
32
43
  }
33
44
 
34
45
  module.exports = { parseCommand };
@@ -0,0 +1,251 @@
1
+ const axios = require('axios');
2
+ const { readConfig } = require('../System/config_manager');
3
+
4
+ const TOKEN_URL = 'https://oauth2.googleapis.com/token';
5
+ const GMAIL_API_BASE = 'https://gmail.googleapis.com/gmail/v1';
6
+
7
+ function hasGmailConfig(config) {
8
+ return Boolean(config.gmailClientId && config.gmailClientSecret && config.gmailRefreshToken);
9
+ }
10
+
11
+ function parseInstruction(instruction) {
12
+ const raw = (instruction || '').trim();
13
+ if (!raw) return { action: 'help' };
14
+
15
+ try {
16
+ const parsed = JSON.parse(raw);
17
+ if (parsed && typeof parsed === 'object') {
18
+ return {
19
+ action: normalizeAction(parsed.action || 'search'),
20
+ ...parsed
21
+ };
22
+ }
23
+ } catch {
24
+ // Plain text searches Gmail.
25
+ }
26
+
27
+ const lower = raw.toLowerCase();
28
+ if (lower === 'help') return { action: 'help' };
29
+ if (lower === 'unread') return { action: 'search', query: 'is:unread' };
30
+ if (lower === 'inbox') return { action: 'search', query: 'in:inbox' };
31
+ if (lower.startsWith('read ')) return { action: 'read', id: raw.slice(5).trim() };
32
+ if (lower.startsWith('draft ')) return { action: 'draft', body: raw.slice(6).trim() };
33
+ if (lower.startsWith('search ')) return { action: 'search', query: raw.slice(7).trim() };
34
+
35
+ return { action: 'search', query: raw };
36
+ }
37
+
38
+ function normalizeAction(action) {
39
+ const normalized = String(action || '').toLowerCase();
40
+ if (['list', 'search', 'inbox', 'unread'].includes(normalized)) return 'search';
41
+ if (['get', 'read', 'read_email', 'message'].includes(normalized)) return 'read';
42
+ if (['draft', 'create_draft', 'compose', 'write'].includes(normalized)) return 'draft';
43
+ return normalized;
44
+ }
45
+
46
+ function gmailUserId(config) {
47
+ return encodeURIComponent(config.gmailUserId || 'me');
48
+ }
49
+
50
+ async function getAccessToken(config) {
51
+ const params = new URLSearchParams({
52
+ client_id: config.gmailClientId,
53
+ client_secret: config.gmailClientSecret,
54
+ refresh_token: config.gmailRefreshToken,
55
+ grant_type: 'refresh_token'
56
+ });
57
+
58
+ const response = await axios.post(TOKEN_URL, params.toString(), {
59
+ headers: { 'Content-Type': 'application/x-www-form-urlencoded' }
60
+ });
61
+
62
+ return response.data.access_token;
63
+ }
64
+
65
+ function gmailHeaders(accessToken) {
66
+ return {
67
+ Authorization: `Bearer ${accessToken}`,
68
+ 'Content-Type': 'application/json'
69
+ };
70
+ }
71
+
72
+ function decodeBase64Url(data = '') {
73
+ const normalized = String(data).replace(/-/g, '+').replace(/_/g, '/');
74
+ const padded = normalized.padEnd(normalized.length + ((4 - normalized.length % 4) % 4), '=');
75
+ return Buffer.from(padded, 'base64').toString('utf8');
76
+ }
77
+
78
+ function encodeBase64Url(data = '') {
79
+ return Buffer.from(String(data), 'utf8')
80
+ .toString('base64')
81
+ .replace(/\+/g, '-')
82
+ .replace(/\//g, '_')
83
+ .replace(/=+$/g, '');
84
+ }
85
+
86
+ function getHeader(message, name) {
87
+ const headers = message.payload?.headers || [];
88
+ const found = headers.find(header => header.name && header.name.toLowerCase() === name.toLowerCase());
89
+ return found ? found.value : '';
90
+ }
91
+
92
+ function findTextPart(payload) {
93
+ if (!payload) return '';
94
+ if (payload.mimeType === 'text/plain' && payload.body?.data) {
95
+ return decodeBase64Url(payload.body.data);
96
+ }
97
+ if (payload.mimeType === 'text/html' && payload.body?.data) {
98
+ return decodeBase64Url(payload.body.data).replace(/<[^>]+>/g, ' ').replace(/\s+/g, ' ').trim();
99
+ }
100
+ for (const part of payload.parts || []) {
101
+ const text = findTextPart(part);
102
+ if (text) return text;
103
+ }
104
+ return payload.body?.data ? decodeBase64Url(payload.body.data) : '';
105
+ }
106
+
107
+ function formatMessageSummary(message) {
108
+ const subject = getHeader(message, 'Subject') || '(No subject)';
109
+ const from = getHeader(message, 'From') || '(Unknown sender)';
110
+ const date = getHeader(message, 'Date');
111
+ const snippet = message.snippet || '';
112
+ return [
113
+ `ID: ${message.id}`,
114
+ `From: ${from}`,
115
+ `Subject: ${subject}`,
116
+ date ? `Date: ${date}` : '',
117
+ snippet ? `Snippet: ${snippet}` : ''
118
+ ].filter(Boolean).join('\n');
119
+ }
120
+
121
+ async function fetchMessage(config, accessToken, id, format = 'metadata') {
122
+ const response = await axios.get(`${GMAIL_API_BASE}/users/${gmailUserId(config)}/messages/${encodeURIComponent(id)}`, {
123
+ headers: gmailHeaders(accessToken),
124
+ params: {
125
+ format,
126
+ metadataHeaders: ['From', 'To', 'Subject', 'Date']
127
+ }
128
+ });
129
+ return response.data;
130
+ }
131
+
132
+ async function searchMessages(config, input, accessToken) {
133
+ const query = input.query || input.q || 'in:inbox';
134
+ const maxResults = Number(input.maxResults || input.limit || 10);
135
+ const response = await axios.get(`${GMAIL_API_BASE}/users/${gmailUserId(config)}/messages`, {
136
+ headers: gmailHeaders(accessToken),
137
+ params: {
138
+ q: query,
139
+ maxResults
140
+ }
141
+ });
142
+
143
+ const messages = response.data.messages || [];
144
+ if (messages.length === 0) return `No Gmail messages found for query: ${query}`;
145
+
146
+ const detailed = [];
147
+ for (const message of messages.slice(0, maxResults)) {
148
+ const full = await fetchMessage(config, accessToken, message.id, 'metadata');
149
+ detailed.push(formatMessageSummary(full));
150
+ }
151
+
152
+ return `Gmail search results for "${query}":\n\n${detailed.join('\n\n')}`;
153
+ }
154
+
155
+ async function readMessage(config, input, accessToken) {
156
+ const id = input.id || input.messageId;
157
+ if (!id) throw new Error('Missing Gmail message id.');
158
+
159
+ const message = await fetchMessage(config, accessToken, id, 'full');
160
+ const body = findTextPart(message.payload);
161
+ return [
162
+ formatMessageSummary(message),
163
+ '',
164
+ body ? `Body:\n${body.slice(0, Number(input.maxChars || 4000))}` : 'Body: (No readable text body found)'
165
+ ].join('\n');
166
+ }
167
+
168
+ function sanitizeHeader(value = '') {
169
+ return String(value).replace(/[\r\n]+/g, ' ').trim();
170
+ }
171
+
172
+ function buildRawEmail(input) {
173
+ const to = sanitizeHeader(input.to || input.recipient || '');
174
+ if (!to) throw new Error('Missing email recipient.');
175
+
176
+ const cc = sanitizeHeader(input.cc || '');
177
+ const bcc = sanitizeHeader(input.bcc || '');
178
+ const subject = sanitizeHeader(input.subject || '(No subject)');
179
+ const body = String(input.body || input.content || input.text || '');
180
+
181
+ const headers = [
182
+ `To: ${to}`,
183
+ cc ? `Cc: ${cc}` : '',
184
+ bcc ? `Bcc: ${bcc}` : '',
185
+ `Subject: ${subject}`,
186
+ 'MIME-Version: 1.0',
187
+ 'Content-Type: text/plain; charset="UTF-8"'
188
+ ].filter(Boolean);
189
+
190
+ return encodeBase64Url(`${headers.join('\r\n')}\r\n\r\n${body}`);
191
+ }
192
+
193
+ async function createDraft(config, input, accessToken) {
194
+ const raw = buildRawEmail(input);
195
+ const response = await axios.post(`${GMAIL_API_BASE}/users/${gmailUserId(config)}/drafts`, {
196
+ message: { raw }
197
+ }, {
198
+ headers: gmailHeaders(accessToken)
199
+ });
200
+
201
+ const draft = response.data || {};
202
+ return `Created Gmail draft${draft.id ? ` ${draft.id}` : ''} for ${sanitizeHeader(input.to || input.recipient)}. Review it in Gmail before sending.`;
203
+ }
204
+
205
+ function helpText() {
206
+ return [
207
+ 'Gmail plugin commands:',
208
+ '- Search inbox: {"action":"search","query":"in:inbox newer_than:7d","limit":5}',
209
+ '- Read message: {"action":"read","id":"MESSAGE_ID"}',
210
+ '- Create draft: {"action":"draft","to":"person@example.com","subject":"Hello","body":"Draft body"}',
211
+ 'For safety, this plugin creates drafts only. It does not send email automatically.'
212
+ ].join('\n');
213
+ }
214
+
215
+ module.exports = {
216
+ name: 'gmail',
217
+ description: 'Manage Gmail safely. Target can be JSON: {"action":"search","query":"in:inbox is:unread","limit":10}, {"action":"read","id":"MESSAGE_ID"}, or {"action":"draft","to":"person@example.com","subject":"Subject","body":"Body"}. This plugin creates drafts only and does not send email.',
218
+
219
+ async execute(instruction) {
220
+ const config = readConfig();
221
+ const input = parseInstruction(instruction);
222
+
223
+ if (input.action === 'help') return helpText();
224
+ if (!hasGmailConfig(config)) {
225
+ return 'Gmail API is not configured. Add Gmail OAuth credentials with `mint onboard`. Use scopes for gmail.readonly and gmail.compose.';
226
+ }
227
+
228
+ const accessToken = await getAccessToken(config);
229
+
230
+ switch (input.action) {
231
+ case 'search':
232
+ return await searchMessages(config, input, accessToken);
233
+ case 'read':
234
+ return await readMessage(config, input, accessToken);
235
+ case 'draft':
236
+ return await createDraft(config, input, accessToken);
237
+ default:
238
+ throw new Error(`Unsupported Gmail action: ${input.action}`);
239
+ }
240
+ },
241
+
242
+ _helpers: {
243
+ parseInstruction,
244
+ buildRawEmail,
245
+ decodeBase64Url,
246
+ encodeBase64Url,
247
+ findTextPart,
248
+ formatMessageSummary,
249
+ hasGmailConfig
250
+ }
251
+ };