@pheem49/mint 1.5.1 → 1.5.3
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 +7 -7
- package/README.md +140 -66
- package/assets/Agent_Mint.png +0 -0
- package/assets/Settings.png +0 -0
- package/main.js +12 -0
- package/mint-cli.js +148 -921
- package/models/Shiroko_Model/Shiroko/Shiroko_Core//345/221/206/347/214/253.exp3.json +31 -1
- package/models/Shiroko_Model/Shiroko/Shiroko_Core//347/202/271/344/270/200/344/270/213.exp3.json +6 -1
- package/package.json +20 -21
- package/preload.js +2 -0
- package/scripts/install_linux_desktop_entry.js +48 -0
- package/src/AI_Brain/Gemini_API.js +194 -491
- package/src/AI_Brain/autonomous_brain.js +46 -19
- package/src/AI_Brain/headless_agent.js +21 -2
- package/src/AI_Brain/proactive_engine.js +12 -2
- package/src/AI_Brain/provider_adapter.js +358 -0
- package/src/Automation_Layer/browser_automation.js +26 -24
- package/src/CLI/approval_handler.js +47 -0
- package/src/CLI/chat_router.js +7 -0
- package/src/CLI/chat_ui.js +586 -80
- package/src/CLI/cli_colors.js +115 -0
- package/src/CLI/cli_formatters.js +94 -0
- package/src/CLI/code_agent.js +825 -283
- package/src/CLI/intent_detectors.js +181 -0
- package/src/CLI/interactive_chat.js +641 -0
- package/src/CLI/list_features.js +3 -0
- package/src/CLI/repo_summarizer.js +282 -0
- package/src/CLI/semantic_code_search.js +312 -0
- package/src/CLI/skill_manager.js +41 -0
- package/src/CLI/slash_command_handler.js +418 -0
- package/src/CLI/symbol_indexer.js +231 -0
- package/src/CLI/updater.js +21 -1
- package/src/Channels/discord_bridge.js +11 -13
- package/src/Channels/line_bridge.js +10 -10
- package/src/Channels/slack_bridge.js +7 -12
- package/src/Channels/telegram_bridge.js +6 -14
- package/src/Channels/whatsapp_bridge.js +11 -9
- package/src/System/chat_history_manager.js +20 -12
- package/src/System/config_manager.js +4 -1
- package/src/System/ipc_handlers.js +10 -0
- package/src/System/optional_require.js +23 -0
- package/src/System/picture_store.js +109 -0
- package/src/System/task_manager.js +127 -0
- package/src/System/tool_registry.js +13 -0
- package/src/System/window_manager.js +16 -8
- package/src/UI/live2d_manager.js +246 -14
- package/src/UI/renderer.js +620 -45
- package/src/UI/settings.css +738 -439
- package/src/UI/settings.html +487 -432
- package/src/UI/settings.js +44 -10
- package/src/UI/styles.css +1403 -106
- package/privacy.txt +0 -1
|
@@ -1,9 +1,15 @@
|
|
|
1
|
-
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { requireOptional } = require('../System/optional_require');
|
|
2
4
|
const { handleChat } = require('../AI_Brain/Gemini_API');
|
|
3
5
|
|
|
4
6
|
class DiscordBridge {
|
|
5
7
|
constructor(token) {
|
|
6
8
|
this.token = token;
|
|
9
|
+
const { Client, GatewayIntentBits, Partials } = requireOptional(
|
|
10
|
+
'discord.js',
|
|
11
|
+
'npm install discord.js'
|
|
12
|
+
);
|
|
7
13
|
this.client = new Client({
|
|
8
14
|
intents: [
|
|
9
15
|
GatewayIntentBits.Guilds,
|
|
@@ -21,30 +27,22 @@ class DiscordBridge {
|
|
|
21
27
|
});
|
|
22
28
|
|
|
23
29
|
this.client.on('messageCreate', async (message) => {
|
|
24
|
-
// Ignore bot messages
|
|
25
30
|
if (message.author.bot) return;
|
|
26
|
-
|
|
27
|
-
// Handle DMs or Mentions
|
|
28
31
|
const isDM = !message.guild;
|
|
29
32
|
const isMentioned = message.mentions.has(this.client.user);
|
|
30
33
|
|
|
31
34
|
if (isDM || isMentioned) {
|
|
32
35
|
try {
|
|
33
|
-
// Clean up the message if it's a mention
|
|
34
36
|
let cleanContent = message.content;
|
|
35
37
|
if (isMentioned) {
|
|
36
|
-
cleanContent = message.content
|
|
38
|
+
cleanContent = message.content
|
|
39
|
+
.replace(`<@!${this.client.user.id}>`, '')
|
|
40
|
+
.replace(`<@${this.client.user.id}>`, '')
|
|
41
|
+
.trim();
|
|
37
42
|
}
|
|
38
|
-
|
|
39
43
|
if (!cleanContent) return;
|
|
40
|
-
|
|
41
|
-
// Show typing indicator
|
|
42
44
|
await message.channel.sendTyping();
|
|
43
|
-
|
|
44
|
-
// Send to Mint AI Brain
|
|
45
45
|
const result = await handleChat(cleanContent);
|
|
46
|
-
|
|
47
|
-
// Reply to user
|
|
48
46
|
if (result && result.response) {
|
|
49
47
|
await message.reply(result.response);
|
|
50
48
|
}
|
|
@@ -1,22 +1,25 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { requireOptional } = require('../System/optional_require');
|
|
3
4
|
const { handleChat } = require('../AI_Brain/Gemini_API');
|
|
4
5
|
|
|
5
6
|
class LineBridge {
|
|
6
7
|
constructor(credentials) {
|
|
8
|
+
this._line = requireOptional('@line/bot-sdk', 'npm install @line/bot-sdk express');
|
|
9
|
+
this._express = requireOptional('express', 'npm install @line/bot-sdk express');
|
|
7
10
|
this.config = {
|
|
8
11
|
channelAccessToken: credentials.accessToken,
|
|
9
12
|
channelSecret: credentials.secret,
|
|
10
13
|
};
|
|
11
|
-
this.port
|
|
12
|
-
this.client = new
|
|
14
|
+
this.port = credentials.port || 3000;
|
|
15
|
+
this.client = new this._line.messagingApi.MessagingApiClient({
|
|
13
16
|
channelAccessToken: credentials.accessToken
|
|
14
17
|
});
|
|
15
|
-
this.app
|
|
18
|
+
this.app = this._express();
|
|
16
19
|
}
|
|
17
20
|
|
|
18
21
|
async connect() {
|
|
19
|
-
this.app.post('/callback',
|
|
22
|
+
this.app.post('/callback', this._line.middleware(this.config), (req, res) => {
|
|
20
23
|
Promise
|
|
21
24
|
.all(req.body.events.map(event => this.handleEvent(event)))
|
|
22
25
|
.then((result) => res.json(result))
|
|
@@ -36,7 +39,6 @@ class LineBridge {
|
|
|
36
39
|
if (event.type !== 'message' || event.message.type !== 'text') {
|
|
37
40
|
return Promise.resolve(null);
|
|
38
41
|
}
|
|
39
|
-
|
|
40
42
|
try {
|
|
41
43
|
const result = await handleChat(event.message.text);
|
|
42
44
|
if (result && result.response) {
|
|
@@ -51,9 +53,7 @@ class LineBridge {
|
|
|
51
53
|
}
|
|
52
54
|
|
|
53
55
|
async disconnect() {
|
|
54
|
-
if (this.server)
|
|
55
|
-
this.server.close();
|
|
56
|
-
}
|
|
56
|
+
if (this.server) this.server.close();
|
|
57
57
|
}
|
|
58
58
|
}
|
|
59
59
|
|
|
@@ -1,8 +1,11 @@
|
|
|
1
|
-
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { requireOptional } = require('../System/optional_require');
|
|
2
4
|
const { handleChat } = require('../AI_Brain/Gemini_API');
|
|
3
5
|
|
|
4
6
|
class SlackBridge {
|
|
5
7
|
constructor(credentials) {
|
|
8
|
+
const { App } = requireOptional('@slack/bolt', 'npm install @slack/bolt');
|
|
6
9
|
this.app = new App({
|
|
7
10
|
token: credentials.botToken,
|
|
8
11
|
appToken: credentials.appToken,
|
|
@@ -15,24 +18,18 @@ class SlackBridge {
|
|
|
15
18
|
try {
|
|
16
19
|
const text = event.text.replace(/<@.*?>/g, '').trim();
|
|
17
20
|
if (!text) return;
|
|
18
|
-
|
|
19
21
|
const result = await handleChat(text);
|
|
20
|
-
if (result && result.response)
|
|
21
|
-
await say(result.response);
|
|
22
|
-
}
|
|
22
|
+
if (result && result.response) await say(result.response);
|
|
23
23
|
} catch (err) {
|
|
24
24
|
console.error('[Slack Bridge] Error processing app_mention:', err);
|
|
25
25
|
}
|
|
26
26
|
});
|
|
27
27
|
|
|
28
28
|
this.app.event('message', async ({ event, say }) => {
|
|
29
|
-
// Only respond in DMs
|
|
30
29
|
if (event.channel_type === 'im') {
|
|
31
30
|
try {
|
|
32
31
|
const result = await handleChat(event.text);
|
|
33
|
-
if (result && result.response)
|
|
34
|
-
await say(result.response);
|
|
35
|
-
}
|
|
32
|
+
if (result && result.response) await say(result.response);
|
|
36
33
|
} catch (err) {
|
|
37
34
|
console.error('[Slack Bridge] Error processing message:', err);
|
|
38
35
|
}
|
|
@@ -44,9 +41,7 @@ class SlackBridge {
|
|
|
44
41
|
}
|
|
45
42
|
|
|
46
43
|
async disconnect() {
|
|
47
|
-
if (this.app)
|
|
48
|
-
await this.app.stop();
|
|
49
|
-
}
|
|
44
|
+
if (this.app) await this.app.stop();
|
|
50
45
|
}
|
|
51
46
|
}
|
|
52
47
|
|
|
@@ -1,9 +1,12 @@
|
|
|
1
|
-
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { requireOptional } = require('../System/optional_require');
|
|
2
4
|
const { handleChat } = require('../AI_Brain/Gemini_API');
|
|
3
5
|
|
|
4
6
|
class TelegramBridge {
|
|
5
7
|
constructor(token) {
|
|
6
8
|
this.token = token;
|
|
9
|
+
const { Telegraf } = requireOptional('telegraf', 'npm install telegraf');
|
|
7
10
|
this.bot = new Telegraf(token);
|
|
8
11
|
}
|
|
9
12
|
|
|
@@ -12,19 +15,11 @@ class TelegramBridge {
|
|
|
12
15
|
|
|
13
16
|
this.bot.on('text', async (ctx) => {
|
|
14
17
|
try {
|
|
15
|
-
// Show typing status
|
|
16
18
|
await ctx.sendChatAction('typing');
|
|
17
|
-
|
|
18
19
|
const message = ctx.message.text;
|
|
19
20
|
if (!message) return;
|
|
20
|
-
|
|
21
|
-
// Send to Mint AI Brain
|
|
22
21
|
const result = await handleChat(message);
|
|
23
|
-
|
|
24
|
-
// Reply to user
|
|
25
|
-
if (result && result.response) {
|
|
26
|
-
await ctx.reply(result.response);
|
|
27
|
-
}
|
|
22
|
+
if (result && result.response) await ctx.reply(result.response);
|
|
28
23
|
} catch (err) {
|
|
29
24
|
console.error('[Telegram Bridge] Error processing message:', err);
|
|
30
25
|
await ctx.reply('ขออภัยค่ะ เกิดข้อผิดพลาดบางอย่างในการประมวลผลข้อความ');
|
|
@@ -34,15 +29,12 @@ class TelegramBridge {
|
|
|
34
29
|
this.bot.launch();
|
|
35
30
|
console.log('[Telegram Bridge] Bot started!');
|
|
36
31
|
|
|
37
|
-
// Enable graceful stop
|
|
38
32
|
process.once('SIGINT', () => this.bot.stop('SIGINT'));
|
|
39
33
|
process.once('SIGTERM', () => this.bot.stop('SIGTERM'));
|
|
40
34
|
}
|
|
41
35
|
|
|
42
36
|
async disconnect() {
|
|
43
|
-
if (this.bot)
|
|
44
|
-
await this.bot.stop();
|
|
45
|
-
}
|
|
37
|
+
if (this.bot) await this.bot.stop();
|
|
46
38
|
}
|
|
47
39
|
}
|
|
48
40
|
|
|
@@ -1,9 +1,16 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { requireOptional } = require('../System/optional_require');
|
|
3
4
|
const { handleChat } = require('../AI_Brain/Gemini_API');
|
|
4
5
|
|
|
5
6
|
class WhatsappBridge {
|
|
6
7
|
constructor() {
|
|
8
|
+
// Dynamic require — only loads if user has installed whatsapp-web.js
|
|
9
|
+
const { Client, LocalAuth } = requireOptional(
|
|
10
|
+
'whatsapp-web.js',
|
|
11
|
+
'npm install whatsapp-web.js qrcode-terminal'
|
|
12
|
+
);
|
|
13
|
+
this._qrcode = requireOptional('qrcode-terminal', 'npm install qrcode-terminal');
|
|
7
14
|
this.client = new Client({
|
|
8
15
|
authStrategy: new LocalAuth({
|
|
9
16
|
dataPath: require('path').join(require('os').homedir(), '.config', 'mint', 'whatsapp-session')
|
|
@@ -17,7 +24,7 @@ class WhatsappBridge {
|
|
|
17
24
|
async connect() {
|
|
18
25
|
this.client.on('qr', (qr) => {
|
|
19
26
|
console.log('[WhatsApp Bridge] Scan this QR code to login:');
|
|
20
|
-
|
|
27
|
+
this._qrcode.generate(qr, { small: true });
|
|
21
28
|
});
|
|
22
29
|
|
|
23
30
|
this.client.on('ready', () => {
|
|
@@ -26,13 +33,8 @@ class WhatsappBridge {
|
|
|
26
33
|
|
|
27
34
|
this.client.on('message', async (msg) => {
|
|
28
35
|
try {
|
|
29
|
-
// Ignore messages from groups unless mentioned (simple implementation)
|
|
30
36
|
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
|
-
|
|
37
|
+
if (chat.isGroup) return;
|
|
36
38
|
const result = await handleChat(msg.body);
|
|
37
39
|
if (result && result.response) {
|
|
38
40
|
await msg.reply(result.response);
|
|
@@ -19,21 +19,29 @@ if (!fs.existsSync(CONFIG_DIR)) {
|
|
|
19
19
|
|
|
20
20
|
const CHAT_HISTORY_PATH = path.join(CONFIG_DIR, 'mint-chat-history.json');
|
|
21
21
|
|
|
22
|
-
// Migration Logic: Consolidate from
|
|
22
|
+
// Migration Logic: Consolidate from various legacy locations to ~/.config/mint/
|
|
23
23
|
if (!fs.existsSync(CHAT_HISTORY_PATH)) {
|
|
24
24
|
const electronUserData = app && app.getPath ? path.join(app.getPath('userData'), 'mint-chat-history.json') : null;
|
|
25
|
-
const
|
|
25
|
+
const legacyDotMint = path.join(MINT_DIR, 'mint-chat-history.json');
|
|
26
|
+
// Legacy: file was written to the project root (CWD) before v1.5.2
|
|
27
|
+
const legacyProjectRoot = path.join(process.cwd(), 'mint-chat-history.json');
|
|
26
28
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
29
|
+
const candidates = [
|
|
30
|
+
electronUserData,
|
|
31
|
+
legacyDotMint,
|
|
32
|
+
legacyProjectRoot
|
|
33
|
+
].filter(Boolean);
|
|
34
|
+
|
|
35
|
+
for (const candidate of candidates) {
|
|
36
|
+
if (candidate !== CHAT_HISTORY_PATH && fs.existsSync(candidate)) {
|
|
37
|
+
try {
|
|
38
|
+
fs.copyFileSync(candidate, CHAT_HISTORY_PATH);
|
|
39
|
+
console.log(`[History] Migrated chat history from ${candidate}`);
|
|
40
|
+
} catch (e) {
|
|
41
|
+
console.error('[History] Migration failed:', e);
|
|
42
|
+
}
|
|
43
|
+
break;
|
|
44
|
+
}
|
|
37
45
|
}
|
|
38
46
|
}
|
|
39
47
|
|
|
@@ -39,6 +39,9 @@ const DEFAULT_CONFIG = {
|
|
|
39
39
|
customBgStart: '#0f172a',
|
|
40
40
|
customBgEnd: '#1e1b4b',
|
|
41
41
|
customPanelBg: '#1e293b',
|
|
42
|
+
glassBlur: 'blur(16px)',
|
|
43
|
+
fontFamily: "'Outfit', sans-serif",
|
|
44
|
+
fontSize: '15px',
|
|
42
45
|
apiKey: '',
|
|
43
46
|
geminiModel: 'gemini-2.5-flash',
|
|
44
47
|
language: 'th-TH',
|
|
@@ -187,4 +190,4 @@ function isPlaceholder(val) {
|
|
|
187
190
|
return !val || val.startsWith('your_') || val.includes('key_here') || val.trim() === '';
|
|
188
191
|
}
|
|
189
192
|
|
|
190
|
-
module.exports = { readConfig, writeConfig, getAvailableProviders, isPlaceholder, CONFIG_PATH };
|
|
193
|
+
module.exports = { readConfig, writeConfig, getAvailableProviders, isPlaceholder, CONFIG_PATH, CONFIG_DIR };
|
|
@@ -17,6 +17,8 @@ function registerIpcHandlers({
|
|
|
17
17
|
getWeather,
|
|
18
18
|
readConfig,
|
|
19
19
|
writeConfig,
|
|
20
|
+
saveChatImages,
|
|
21
|
+
listSavedPictures,
|
|
20
22
|
parseCommand,
|
|
21
23
|
executeAction,
|
|
22
24
|
getGoogleTtsUrls,
|
|
@@ -25,6 +27,10 @@ function registerIpcHandlers({
|
|
|
25
27
|
|
|
26
28
|
ipcMain.handle('chat-message', async (event, message, base64Image = null, base64Audio = null) => {
|
|
27
29
|
try {
|
|
30
|
+
if (base64Image && saveChatImages) {
|
|
31
|
+
saveChatImages(base64Image, { source: 'chat', message });
|
|
32
|
+
}
|
|
33
|
+
|
|
28
34
|
const rawResponse = await handleChat(message, base64Image, base64Audio);
|
|
29
35
|
const aiResponse = parseCommand(rawResponse);
|
|
30
36
|
|
|
@@ -79,6 +85,10 @@ function registerIpcHandlers({
|
|
|
79
85
|
|
|
80
86
|
ipcMain.handle('get-chat-history', () => getChatTranscript());
|
|
81
87
|
|
|
88
|
+
ipcMain.handle('list-saved-pictures', () => {
|
|
89
|
+
return listSavedPictures ? listSavedPictures() : [];
|
|
90
|
+
});
|
|
91
|
+
|
|
82
92
|
ipcMain.handle('open-settings', () => {
|
|
83
93
|
windowManager.createSettingsWindow();
|
|
84
94
|
});
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Helper: ลอง require package แบบ dynamic
|
|
5
|
+
* ถ้าหาไม่เจอให้ throw Error พร้อม install guide
|
|
6
|
+
*/
|
|
7
|
+
function requireOptional(pkg, installHint) {
|
|
8
|
+
try {
|
|
9
|
+
return require(pkg);
|
|
10
|
+
} catch (e) {
|
|
11
|
+
if (e.code === 'MODULE_NOT_FOUND') {
|
|
12
|
+
const hint = installHint || `npm install ${pkg}`;
|
|
13
|
+
throw new Error(
|
|
14
|
+
`[Mint] Optional package "${pkg}" is not installed.\n` +
|
|
15
|
+
`To use this feature, run: ${hint}\n` +
|
|
16
|
+
`(This package is not bundled by default to keep Mint lightweight.)`
|
|
17
|
+
);
|
|
18
|
+
}
|
|
19
|
+
throw e;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
module.exports = { requireOptional };
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const os = require('os');
|
|
4
|
+
const crypto = require('crypto');
|
|
5
|
+
const { pathToFileURL } = require('url');
|
|
6
|
+
|
|
7
|
+
const PICTURES_DIR = path.join(os.homedir(), '.config', 'mint', 'Pictures');
|
|
8
|
+
const INDEX_PATH = path.join(PICTURES_DIR, 'pictures.json');
|
|
9
|
+
|
|
10
|
+
const EXTENSIONS = {
|
|
11
|
+
'image/png': 'png',
|
|
12
|
+
'image/jpeg': 'jpg',
|
|
13
|
+
'image/jpg': 'jpg',
|
|
14
|
+
'image/webp': 'webp',
|
|
15
|
+
'image/gif': 'gif'
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
function ensurePicturesDir() {
|
|
19
|
+
fs.mkdirSync(PICTURES_DIR, { recursive: true });
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function readIndex() {
|
|
23
|
+
try {
|
|
24
|
+
if (!fs.existsSync(INDEX_PATH)) return [];
|
|
25
|
+
const parsed = JSON.parse(fs.readFileSync(INDEX_PATH, 'utf8'));
|
|
26
|
+
return Array.isArray(parsed) ? parsed : [];
|
|
27
|
+
} catch (error) {
|
|
28
|
+
console.error('[Pictures] Failed to read index:', error.message);
|
|
29
|
+
return [];
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function writeIndex(entries) {
|
|
34
|
+
ensurePicturesDir();
|
|
35
|
+
fs.writeFileSync(INDEX_PATH, JSON.stringify(entries, null, 2), 'utf8');
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function parseImageDataUri(dataUri) {
|
|
39
|
+
if (!dataUri || typeof dataUri !== 'string') return null;
|
|
40
|
+
const match = dataUri.match(/^data:(image\/[\w.+-]+);base64,([\s\S]+)$/);
|
|
41
|
+
if (!match) return null;
|
|
42
|
+
|
|
43
|
+
const mimeType = match[1].toLowerCase();
|
|
44
|
+
const extension = EXTENSIONS[mimeType] || 'png';
|
|
45
|
+
return {
|
|
46
|
+
mimeType,
|
|
47
|
+
extension,
|
|
48
|
+
buffer: Buffer.from(match[2], 'base64')
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function createFilename(extension) {
|
|
53
|
+
const stamp = new Date().toISOString()
|
|
54
|
+
.replace(/[-:]/g, '')
|
|
55
|
+
.replace(/\..+$/, '')
|
|
56
|
+
.replace('T', '-');
|
|
57
|
+
const id = crypto.randomBytes(4).toString('hex');
|
|
58
|
+
return `mint-${stamp}-${id}.${extension}`;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function saveChatImages(base64Image, metadata = {}) {
|
|
62
|
+
const images = Array.isArray(base64Image) ? base64Image : (base64Image ? [base64Image] : []);
|
|
63
|
+
const saved = [];
|
|
64
|
+
if (images.length === 0) return saved;
|
|
65
|
+
|
|
66
|
+
ensurePicturesDir();
|
|
67
|
+
const index = readIndex();
|
|
68
|
+
|
|
69
|
+
for (const item of images) {
|
|
70
|
+
const parsed = parseImageDataUri(item);
|
|
71
|
+
if (!parsed || parsed.buffer.length === 0) continue;
|
|
72
|
+
|
|
73
|
+
const filename = createFilename(parsed.extension);
|
|
74
|
+
const filePath = path.join(PICTURES_DIR, filename);
|
|
75
|
+
fs.writeFileSync(filePath, parsed.buffer);
|
|
76
|
+
|
|
77
|
+
const entry = {
|
|
78
|
+
id: path.basename(filename, path.extname(filename)),
|
|
79
|
+
filename,
|
|
80
|
+
path: filePath,
|
|
81
|
+
mimeType: parsed.mimeType,
|
|
82
|
+
createdAt: new Date().toISOString(),
|
|
83
|
+
source: metadata.source || 'chat',
|
|
84
|
+
message: String(metadata.message || '').slice(0, 240)
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
index.unshift(entry);
|
|
88
|
+
saved.push(entry);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
writeIndex(index);
|
|
92
|
+
return saved;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function listSavedPictures() {
|
|
96
|
+
ensurePicturesDir();
|
|
97
|
+
return readIndex()
|
|
98
|
+
.filter(entry => entry && entry.path && fs.existsSync(entry.path))
|
|
99
|
+
.map(entry => ({
|
|
100
|
+
...entry,
|
|
101
|
+
url: pathToFileURL(entry.path).href
|
|
102
|
+
}));
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
module.exports = {
|
|
106
|
+
PICTURES_DIR,
|
|
107
|
+
saveChatImages,
|
|
108
|
+
listSavedPictures
|
|
109
|
+
};
|
|
@@ -57,6 +57,12 @@ function addTask(description) {
|
|
|
57
57
|
createdAt: new Date().toISOString(),
|
|
58
58
|
updatedAt: new Date().toISOString(),
|
|
59
59
|
steps: [],
|
|
60
|
+
subtasks: [],
|
|
61
|
+
checkpoints: [],
|
|
62
|
+
artifacts: [],
|
|
63
|
+
retryCount: 0,
|
|
64
|
+
maxRetries: 1,
|
|
65
|
+
lastCheckpointAt: null,
|
|
60
66
|
result: null
|
|
61
67
|
};
|
|
62
68
|
tasks.push(newTask);
|
|
@@ -80,6 +86,120 @@ function updateTask(id, updates) {
|
|
|
80
86
|
return null;
|
|
81
87
|
}
|
|
82
88
|
|
|
89
|
+
function getTask(id) {
|
|
90
|
+
return readTasks().find(t => t.id === id) || null;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function normalizeTask(task) {
|
|
94
|
+
return {
|
|
95
|
+
...task,
|
|
96
|
+
steps: Array.isArray(task.steps) ? task.steps : [],
|
|
97
|
+
subtasks: Array.isArray(task.subtasks) ? task.subtasks : [],
|
|
98
|
+
checkpoints: Array.isArray(task.checkpoints) ? task.checkpoints : [],
|
|
99
|
+
artifacts: Array.isArray(task.artifacts) ? task.artifacts : [],
|
|
100
|
+
retryCount: Number.isFinite(task.retryCount) ? task.retryCount : 0,
|
|
101
|
+
maxRetries: Number.isFinite(task.maxRetries) ? task.maxRetries : 1
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function mutateTask(id, mutator) {
|
|
106
|
+
const tasks = readTasks();
|
|
107
|
+
const idx = tasks.findIndex(t => t.id === id);
|
|
108
|
+
if (idx === -1) return null;
|
|
109
|
+
const next = normalizeTask(tasks[idx]);
|
|
110
|
+
mutator(next);
|
|
111
|
+
next.updatedAt = new Date().toISOString();
|
|
112
|
+
tasks[idx] = next;
|
|
113
|
+
writeTasks(tasks);
|
|
114
|
+
return next;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function addSubtask(taskId, title, extra = {}) {
|
|
118
|
+
return mutateTask(taskId, task => {
|
|
119
|
+
task.subtasks.push({
|
|
120
|
+
id: `${taskId}-${task.subtasks.length + 1}`,
|
|
121
|
+
title,
|
|
122
|
+
status: extra.status || 'pending',
|
|
123
|
+
createdAt: new Date().toISOString(),
|
|
124
|
+
updatedAt: new Date().toISOString(),
|
|
125
|
+
...extra
|
|
126
|
+
});
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function updateSubtask(taskId, subtaskId, updates = {}) {
|
|
131
|
+
return mutateTask(taskId, task => {
|
|
132
|
+
const subtask = task.subtasks.find(item => item.id === subtaskId);
|
|
133
|
+
if (!subtask) return;
|
|
134
|
+
Object.assign(subtask, updates, { updatedAt: new Date().toISOString() });
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function addCheckpoint(taskId, checkpoint = {}) {
|
|
139
|
+
return mutateTask(taskId, task => {
|
|
140
|
+
const entry = {
|
|
141
|
+
id: `${taskId}-checkpoint-${task.checkpoints.length + 1}`,
|
|
142
|
+
time: new Date().toISOString(),
|
|
143
|
+
...checkpoint
|
|
144
|
+
};
|
|
145
|
+
task.checkpoints.push(entry);
|
|
146
|
+
task.lastCheckpointAt = entry.time;
|
|
147
|
+
task.steps.push(entry);
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function addArtifact(taskId, artifact = {}) {
|
|
152
|
+
return mutateTask(taskId, task => {
|
|
153
|
+
task.artifacts.push({
|
|
154
|
+
id: `${taskId}-artifact-${task.artifacts.length + 1}`,
|
|
155
|
+
time: new Date().toISOString(),
|
|
156
|
+
...artifact
|
|
157
|
+
});
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function failTaskWithRetry(id, errorMessage) {
|
|
162
|
+
return mutateTask(id, task => {
|
|
163
|
+
const retryCount = Number(task.retryCount) || 0;
|
|
164
|
+
const maxRetries = Number.isFinite(task.maxRetries) ? task.maxRetries : 1;
|
|
165
|
+
task.result = errorMessage;
|
|
166
|
+
task.retryCount = retryCount + 1;
|
|
167
|
+
task.status = task.retryCount <= maxRetries ? 'pending' : 'failed';
|
|
168
|
+
const checkpoint = {
|
|
169
|
+
id: `${id}-checkpoint-${task.checkpoints.length + 1}`,
|
|
170
|
+
time: new Date().toISOString(),
|
|
171
|
+
phase: task.status === 'pending' ? 'retry_scheduled' : 'failed',
|
|
172
|
+
message: errorMessage,
|
|
173
|
+
retryCount: task.retryCount,
|
|
174
|
+
maxRetries
|
|
175
|
+
};
|
|
176
|
+
task.checkpoints.push(checkpoint);
|
|
177
|
+
task.steps.push(checkpoint);
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function resumeRunningTasks() {
|
|
182
|
+
const resumed = [];
|
|
183
|
+
const tasks = readTasks().map(task => {
|
|
184
|
+
if (task.status !== 'running') return task;
|
|
185
|
+
const normalized = normalizeTask(task);
|
|
186
|
+
normalized.status = 'pending';
|
|
187
|
+
const checkpoint = {
|
|
188
|
+
id: `${normalized.id}-checkpoint-${normalized.checkpoints.length + 1}`,
|
|
189
|
+
time: new Date().toISOString(),
|
|
190
|
+
phase: 'resume_after_restart',
|
|
191
|
+
message: 'Task was running during shutdown and has been re-queued.'
|
|
192
|
+
};
|
|
193
|
+
normalized.checkpoints.push(checkpoint);
|
|
194
|
+
normalized.steps.push(checkpoint);
|
|
195
|
+
normalized.updatedAt = new Date().toISOString();
|
|
196
|
+
resumed.push(normalized);
|
|
197
|
+
return normalized;
|
|
198
|
+
});
|
|
199
|
+
writeTasks(tasks);
|
|
200
|
+
return resumed;
|
|
201
|
+
}
|
|
202
|
+
|
|
83
203
|
function clearCompletedTasks() {
|
|
84
204
|
const tasks = readTasks();
|
|
85
205
|
const activeTasks = tasks.filter(t => t.status === 'pending' || t.status === 'running');
|
|
@@ -88,8 +208,15 @@ function clearCompletedTasks() {
|
|
|
88
208
|
|
|
89
209
|
module.exports = {
|
|
90
210
|
addTask,
|
|
211
|
+
addArtifact,
|
|
212
|
+
addCheckpoint,
|
|
213
|
+
addSubtask,
|
|
214
|
+
failTaskWithRetry,
|
|
215
|
+
getTask,
|
|
91
216
|
getPendingTask,
|
|
217
|
+
resumeRunningTasks,
|
|
92
218
|
updateTask,
|
|
219
|
+
updateSubtask,
|
|
93
220
|
readTasks,
|
|
94
221
|
clearCompletedTasks
|
|
95
222
|
};
|
|
@@ -41,6 +41,19 @@ const TOOL_REGISTRY = Object.freeze({
|
|
|
41
41
|
important: true,
|
|
42
42
|
description: 'Run a non-destructive shell command after user approval.'
|
|
43
43
|
},
|
|
44
|
+
verify: {
|
|
45
|
+
permission: 'approval',
|
|
46
|
+
required: [],
|
|
47
|
+
codeAgentOnly: true,
|
|
48
|
+
important: true,
|
|
49
|
+
description: 'Run test/build/lint verification commands after user approval.'
|
|
50
|
+
},
|
|
51
|
+
plan: {
|
|
52
|
+
permission: 'approval',
|
|
53
|
+
required: ['plan'],
|
|
54
|
+
codeAgentOnly: true,
|
|
55
|
+
description: 'Present a multi-file edit plan before changing files.'
|
|
56
|
+
},
|
|
44
57
|
apply_patch: {
|
|
45
58
|
permission: 'approval',
|
|
46
59
|
required: ['patch'],
|