@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.
Files changed (52) hide show
  1. package/GUIDE_TH.md +7 -7
  2. package/README.md +140 -66
  3. package/assets/Agent_Mint.png +0 -0
  4. package/assets/Settings.png +0 -0
  5. package/main.js +12 -0
  6. package/mint-cli.js +148 -921
  7. package/models/Shiroko_Model/Shiroko/Shiroko_Core//345/221/206/347/214/253.exp3.json +31 -1
  8. package/models/Shiroko_Model/Shiroko/Shiroko_Core//347/202/271/344/270/200/344/270/213.exp3.json +6 -1
  9. package/package.json +20 -21
  10. package/preload.js +2 -0
  11. package/scripts/install_linux_desktop_entry.js +48 -0
  12. package/src/AI_Brain/Gemini_API.js +194 -491
  13. package/src/AI_Brain/autonomous_brain.js +46 -19
  14. package/src/AI_Brain/headless_agent.js +21 -2
  15. package/src/AI_Brain/proactive_engine.js +12 -2
  16. package/src/AI_Brain/provider_adapter.js +358 -0
  17. package/src/Automation_Layer/browser_automation.js +26 -24
  18. package/src/CLI/approval_handler.js +47 -0
  19. package/src/CLI/chat_router.js +7 -0
  20. package/src/CLI/chat_ui.js +586 -80
  21. package/src/CLI/cli_colors.js +115 -0
  22. package/src/CLI/cli_formatters.js +94 -0
  23. package/src/CLI/code_agent.js +825 -283
  24. package/src/CLI/intent_detectors.js +181 -0
  25. package/src/CLI/interactive_chat.js +641 -0
  26. package/src/CLI/list_features.js +3 -0
  27. package/src/CLI/repo_summarizer.js +282 -0
  28. package/src/CLI/semantic_code_search.js +312 -0
  29. package/src/CLI/skill_manager.js +41 -0
  30. package/src/CLI/slash_command_handler.js +418 -0
  31. package/src/CLI/symbol_indexer.js +231 -0
  32. package/src/CLI/updater.js +21 -1
  33. package/src/Channels/discord_bridge.js +11 -13
  34. package/src/Channels/line_bridge.js +10 -10
  35. package/src/Channels/slack_bridge.js +7 -12
  36. package/src/Channels/telegram_bridge.js +6 -14
  37. package/src/Channels/whatsapp_bridge.js +11 -9
  38. package/src/System/chat_history_manager.js +20 -12
  39. package/src/System/config_manager.js +4 -1
  40. package/src/System/ipc_handlers.js +10 -0
  41. package/src/System/optional_require.js +23 -0
  42. package/src/System/picture_store.js +109 -0
  43. package/src/System/task_manager.js +127 -0
  44. package/src/System/tool_registry.js +13 -0
  45. package/src/System/window_manager.js +16 -8
  46. package/src/UI/live2d_manager.js +246 -14
  47. package/src/UI/renderer.js +620 -45
  48. package/src/UI/settings.css +738 -439
  49. package/src/UI/settings.html +487 -432
  50. package/src/UI/settings.js +44 -10
  51. package/src/UI/styles.css +1403 -106
  52. package/privacy.txt +0 -1
@@ -1,9 +1,15 @@
1
- const { Client, GatewayIntentBits, Partials } = require('discord.js');
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.replace(`<@!${this.client.user.id}>`, '').replace(`<@${this.client.user.id}>`, '').trim();
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
- const line = require('@line/bot-sdk');
2
- const express = require('express');
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 = credentials.port || 3000;
12
- this.client = new line.messagingApi.MessagingApiClient({
14
+ this.port = credentials.port || 3000;
15
+ this.client = new this._line.messagingApi.MessagingApiClient({
13
16
  channelAccessToken: credentials.accessToken
14
17
  });
15
- this.app = express();
18
+ this.app = this._express();
16
19
  }
17
20
 
18
21
  async connect() {
19
- this.app.post('/callback', line.middleware(this.config), (req, res) => {
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
- const { App } = require('@slack/bolt');
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
- const { Telegraf } = require('telegraf');
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
- const { Client, LocalAuth } = require('whatsapp-web.js');
2
- const qrcode = require('qrcode-terminal');
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
- qrcode.generate(qr, { small: true });
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 Electron userData or old ~/.mint to ~/.config/mint
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 legacyPath = path.join(MINT_DIR, 'mint-chat-history.json');
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
- if (electronUserData && fs.existsSync(electronUserData)) {
28
- try {
29
- fs.copyFileSync(electronUserData, CHAT_HISTORY_PATH);
30
- console.log('[History] Migrated chat history from Electron userData');
31
- } catch (e) { console.error('[History] Migration from Electron failed:', e); }
32
- } else if (fs.existsSync(legacyPath)) {
33
- try {
34
- fs.copyFileSync(legacyPath, CHAT_HISTORY_PATH);
35
- console.log('[History] Migrated chat history from ~/.mint');
36
- } catch (e) { console.error('[History] Migration from ~/.mint failed:', e); }
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'],