@pheem49/mint 1.4.2 → 1.5.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/GUIDE_TH.md +113 -0
- package/README.md +239 -76
- package/assets/CLI_Screen.png +0 -0
- package/docs/assets/CLI_Screen.png +0 -0
- package/docs/guide.html +632 -0
- package/docs/index.html +5 -4
- package/main.js +66 -894
- package/mint-cli-logic.js +13 -1
- package/mint-cli.js +100 -9
- package/package.json +12 -4
- package/src/AI_Brain/Gemini_API.js +77 -20
- package/src/AI_Brain/autonomous_brain.js +10 -0
- package/src/AI_Brain/behavior_memory.js +26 -5
- package/src/AI_Brain/headless_agent.js +4 -0
- package/src/AI_Brain/knowledge_base.js +61 -8
- package/src/AI_Brain/memory_store.js +55 -7
- package/src/Automation_Layer/file_operations.js +1 -1
- package/src/CLI/chat_router.js +3 -2
- package/src/CLI/chat_ui.js +263 -838
- package/src/CLI/code_agent.js +144 -42
- package/src/CLI/gmail_auth.js +210 -0
- package/src/CLI/list_features.js +2 -0
- package/src/CLI/onboarding.js +307 -55
- package/src/CLI/updater.js +208 -0
- package/src/Channels/brave_search_bridge.js +35 -0
- package/src/Channels/discord_bridge.js +68 -0
- package/src/Channels/google_search_bridge.js +38 -0
- package/src/Channels/line_bridge.js +60 -0
- package/src/Channels/slack_bridge.js +53 -0
- package/src/Channels/telegram_bridge.js +49 -0
- package/src/Channels/whatsapp_bridge.js +55 -0
- package/src/Command_Parser/parser.js +12 -1
- package/src/Plugins/gmail.js +251 -0
- package/src/Plugins/google_calendar.js +245 -19
- package/src/Plugins/notion.js +256 -0
- package/src/System/action_executor.js +129 -0
- package/src/System/bridge_manager.js +76 -0
- package/src/System/chat_history_manager.js +23 -5
- package/src/System/config_manager.js +41 -7
- package/src/System/custom_workflows.js +31 -2
- package/src/System/google_tts_urls.js +51 -0
- package/src/System/ipc_handlers.js +238 -0
- package/src/System/proactive_loop.js +137 -0
- package/src/System/safety_manager.js +165 -0
- package/src/System/screen_capture.js +175 -0
- package/src/System/task_manager.js +15 -5
- package/src/System/window_manager.js +210 -0
- package/src/UI/renderer.js +33 -7
- package/src/UI/settings.html +24 -0
- package/src/UI/settings.js +14 -4
- package/src/UI/styles.css +14 -1
- package/tests/action_executor_safety.test.js +67 -0
- package/tests/gmail.test.js +135 -0
- package/tests/gmail_auth.test.js +129 -0
- package/tests/google_calendar.test.js +113 -0
- package/tests/google_tts_urls.test.js +24 -0
- package/tests/notion.test.js +121 -0
- package/tests/provider_routing.test.js +17 -1
- package/tests/safety_manager.test.js +40 -0
- package/tests/updater.test.js +32 -0
|
@@ -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
|
-
|
|
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
|
+
};
|