@pheem49/mint 1.5.2 → 1.5.4

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 (40) hide show
  1. package/GUIDE_TH.md +23 -11
  2. package/README.md +148 -66
  3. package/assets/Agent_Mint.png +0 -0
  4. package/assets/Settings.png +0 -0
  5. package/install.ps1 +64 -0
  6. package/install.sh +54 -0
  7. package/main.js +12 -0
  8. package/package.json +5 -3
  9. package/preload.js +4 -0
  10. package/scripts/install_linux_desktop_entry.js +48 -0
  11. package/src/AI_Brain/Gemini_API.js +231 -498
  12. package/src/AI_Brain/autonomous_brain.js +46 -19
  13. package/src/AI_Brain/headless_agent.js +21 -2
  14. package/src/AI_Brain/provider_adapter.js +358 -0
  15. package/src/Automation_Layer/file_operations.js +17 -5
  16. package/src/CLI/approval_handler.js +5 -0
  17. package/src/CLI/chat_router.js +7 -0
  18. package/src/CLI/chat_ui.js +397 -76
  19. package/src/CLI/cli_colors.js +86 -3
  20. package/src/CLI/cli_formatters.js +6 -1
  21. package/src/CLI/code_agent.js +706 -273
  22. package/src/CLI/interactive_chat.js +311 -149
  23. package/src/CLI/slash_command_handler.js +2 -2
  24. package/src/CLI/updater.js +21 -1
  25. package/src/System/config_manager.js +5 -1
  26. package/src/System/ipc_handlers.js +95 -1
  27. package/src/System/picture_store.js +109 -0
  28. package/src/System/smart_context.js +227 -0
  29. package/src/System/task_manager.js +127 -0
  30. package/src/System/tool_registry.js +13 -0
  31. package/src/System/window_manager.js +16 -8
  32. package/src/UI/live2d_manager.js +42 -8
  33. package/src/UI/preload-spotlight.js +1 -0
  34. package/src/UI/renderer.js +837 -63
  35. package/src/UI/settings.css +160 -96
  36. package/src/UI/settings.html +9 -0
  37. package/src/UI/settings.js +35 -2
  38. package/src/UI/spotlight.js +13 -9
  39. package/src/UI/styles.css +1592 -165
  40. package/privacy.txt +0 -1
@@ -39,9 +39,13 @@ 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',
48
+ assistantMode: 'chat',
45
49
  automationBrowser: 'chromium',
46
50
  proactiveInterval: 60, // seconds between screen captures
47
51
  proactiveCooldown: 120, // seconds minimum between actual suggestions
@@ -187,4 +191,4 @@ function isPlaceholder(val) {
187
191
  return !val || val.startsWith('your_') || val.includes('key_here') || val.trim() === '';
188
192
  }
189
193
 
190
- module.exports = { readConfig, writeConfig, getAvailableProviders, isPlaceholder, CONFIG_PATH };
194
+ module.exports = { readConfig, writeConfig, getAvailableProviders, isPlaceholder, CONFIG_PATH, CONFIG_DIR };
@@ -1,3 +1,43 @@
1
+ const safetyManager = require('./safety_manager');
2
+ const { getSmartContext } = require('./smart_context');
3
+
4
+ function buildApprovalRequest(action) {
5
+ const classification = safetyManager.classifyAction(action);
6
+ if (
7
+ classification.tier !== safetyManager.TIERS.APPROVAL &&
8
+ classification.tier !== safetyManager.TIERS.DANGEROUS
9
+ ) {
10
+ return null;
11
+ }
12
+
13
+ return {
14
+ required: true,
15
+ tier: classification.tier,
16
+ reason: classification.reason,
17
+ action
18
+ };
19
+ }
20
+
21
+ async function executeApprovedAction(executeAction, action, clipboard) {
22
+ const classification = safetyManager.classifyAction(action);
23
+ const options = {
24
+ clipboard,
25
+ source: 'user_approved_action',
26
+ allowApproval: classification.tier === safetyManager.TIERS.APPROVAL,
27
+ allowDangerous: classification.tier === safetyManager.TIERS.DANGEROUS
28
+ };
29
+ const result = await executeAction(action, options);
30
+ return {
31
+ success: true,
32
+ action,
33
+ tier: classification.tier,
34
+ result,
35
+ message: result && typeof result === 'string'
36
+ ? result
37
+ : 'Action completed.'
38
+ };
39
+ }
40
+
1
41
  function registerIpcHandlers({
2
42
  app,
3
43
  ipcMain,
@@ -17,6 +57,8 @@ function registerIpcHandlers({
17
57
  getWeather,
18
58
  readConfig,
19
59
  writeConfig,
60
+ saveChatImages,
61
+ listSavedPictures,
20
62
  parseCommand,
21
63
  executeAction,
22
64
  getGoogleTtsUrls,
@@ -25,11 +67,21 @@ function registerIpcHandlers({
25
67
 
26
68
  ipcMain.handle('chat-message', async (event, message, base64Image = null, base64Audio = null) => {
27
69
  try {
70
+ if (base64Image && saveChatImages) {
71
+ saveChatImages(base64Image, { source: 'chat', message });
72
+ }
73
+
28
74
  const rawResponse = await handleChat(message, base64Image, base64Audio);
29
75
  const aiResponse = parseCommand(rawResponse);
30
76
 
31
77
  if (aiResponse.action && aiResponse.action.type !== 'none') {
32
78
  try {
79
+ const approval = buildApprovalRequest(aiResponse.action);
80
+ if (approval) {
81
+ aiResponse.approval = approval;
82
+ return aiResponse;
83
+ }
84
+
33
85
  const actionResult = await executeAction(aiResponse.action, { clipboard });
34
86
  if (actionResult && typeof actionResult === 'string') {
35
87
  aiResponse.response += `\n\n${actionResult}`;
@@ -47,6 +99,18 @@ function registerIpcHandlers({
47
99
  }
48
100
  });
49
101
 
102
+ ipcMain.handle('execute-approved-action', async (event, action) => {
103
+ try {
104
+ if (!action || action.type === 'none') {
105
+ return { success: false, message: 'No action to execute.' };
106
+ }
107
+ return await executeApprovedAction(executeAction, action, clipboard);
108
+ } catch (err) {
109
+ console.error('[ApprovedAction] Error:', err);
110
+ return { success: false, message: err.message || 'Action failed.' };
111
+ }
112
+ });
113
+
50
114
  ipcMain.on('close-window', () => {
51
115
  const mainWindow = windowManager.getMainWindow();
52
116
  if (mainWindow) mainWindow.hide();
@@ -79,6 +143,10 @@ function registerIpcHandlers({
79
143
 
80
144
  ipcMain.handle('get-chat-history', () => getChatTranscript());
81
145
 
146
+ ipcMain.handle('list-saved-pictures', () => {
147
+ return listSavedPictures ? listSavedPictures() : [];
148
+ });
149
+
82
150
  ipcMain.handle('open-settings', () => {
83
151
  windowManager.createSettingsWindow();
84
152
  });
@@ -157,6 +225,30 @@ function registerIpcHandlers({
157
225
  }
158
226
  });
159
227
 
228
+ ipcMain.handle('spotlight-action', async (event, action) => {
229
+ const spotlightWindow = windowManager.getSpotlightWindow();
230
+ if (spotlightWindow) spotlightWindow.hide();
231
+
232
+ if (!action || action.type === 'none') {
233
+ return { success: false, message: 'No Spotlight action to execute.' };
234
+ }
235
+
236
+ try {
237
+ const result = await executeAction(action, {
238
+ clipboard,
239
+ source: 'spotlight'
240
+ });
241
+ return {
242
+ success: true,
243
+ action,
244
+ message: result && typeof result === 'string' ? result : 'Spotlight action completed.'
245
+ };
246
+ } catch (err) {
247
+ console.error('[SpotlightAction] Error:', err);
248
+ return { success: false, message: err.message || 'Spotlight action failed.' };
249
+ }
250
+ });
251
+
160
252
  ipcMain.on('spotlight-resize', (event, width, height) => {
161
253
  const spotlightWindow = windowManager.getSpotlightWindow();
162
254
  if (spotlightWindow) spotlightWindow.setSize(width, height);
@@ -197,6 +289,8 @@ function registerIpcHandlers({
197
289
  ipcMain.on('vision-cancel', () => screenCapture.cancel());
198
290
  ipcMain.handle('capture-silent-screen', () => screenCapture.captureSilentScreen());
199
291
 
292
+ ipcMain.handle('get-smart-context', () => getSmartContext({ clipboard }));
293
+
200
294
  ipcMain.on('toggle-proactive', (event, isOn) => {
201
295
  if (isOn) {
202
296
  proactiveLoop.start();
@@ -235,4 +329,4 @@ function registerIpcHandlers({
235
329
  });
236
330
  }
237
331
 
238
- module.exports = { registerIpcHandlers };
332
+ module.exports = { registerIpcHandlers, buildApprovalRequest, executeApprovedAction };
@@ -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
+ };
@@ -0,0 +1,227 @@
1
+ const { execFile } = require('child_process');
2
+ const os = require('os');
3
+
4
+ const MAX_TEXT_LENGTH = 2000;
5
+ const BROWSER_NAMES = [
6
+ 'chrome',
7
+ 'chromium',
8
+ 'brave',
9
+ 'firefox',
10
+ 'edge',
11
+ 'safari',
12
+ 'opera',
13
+ 'vivaldi'
14
+ ];
15
+
16
+ function run(command, args = [], options = {}) {
17
+ return new Promise((resolve) => {
18
+ execFile(command, args, { timeout: options.timeout || 1200 }, (error, stdout) => {
19
+ if (error) {
20
+ resolve(null);
21
+ return;
22
+ }
23
+ resolve(String(stdout || '').trim() || null);
24
+ });
25
+ });
26
+ }
27
+
28
+ function truncateText(value, maxLength = MAX_TEXT_LENGTH) {
29
+ const text = String(value || '').replace(/\0/g, '').trim();
30
+ if (text.length <= maxLength) return text;
31
+ return `${text.slice(0, maxLength)}\n[truncated ${text.length - maxLength} chars]`;
32
+ }
33
+
34
+ function normalizeProcessName(value) {
35
+ return String(value || '').trim().replace(/\.exe$/i, '');
36
+ }
37
+
38
+ function isBrowserProcess(name = '') {
39
+ const normalized = normalizeProcessName(name).toLowerCase();
40
+ return BROWSER_NAMES.some(browser => normalized.includes(browser));
41
+ }
42
+
43
+ async function getLinuxActiveWindow() {
44
+ const windowId = await run('xdotool', ['getactivewindow']);
45
+ if (!windowId) return null;
46
+
47
+ const [title, pid] = await Promise.all([
48
+ run('xdotool', ['getwindowname', windowId]),
49
+ run('xdotool', ['getwindowpid', windowId])
50
+ ]);
51
+
52
+ let processName = '';
53
+ if (pid) {
54
+ processName = await run('ps', ['-p', pid, '-o', 'comm=']) || '';
55
+ }
56
+
57
+ return {
58
+ id: windowId,
59
+ title: title || '',
60
+ appName: normalizeProcessName(processName),
61
+ processName: normalizeProcessName(processName),
62
+ pid: pid ? Number(pid) : null,
63
+ platform: 'linux'
64
+ };
65
+ }
66
+
67
+ async function getMacActiveWindow() {
68
+ const script = [
69
+ 'tell application "System Events"',
70
+ 'set frontApp to first application process whose frontmost is true',
71
+ 'set appName to name of frontApp',
72
+ 'set windowTitle to ""',
73
+ 'try',
74
+ 'set windowTitle to name of front window of frontApp',
75
+ 'end try',
76
+ 'return appName & linefeed & windowTitle',
77
+ 'end tell'
78
+ ].join('\n');
79
+ const output = await run('osascript', ['-e', script]);
80
+ if (!output) return null;
81
+ const [appName = '', title = ''] = output.split(/\r?\n/);
82
+ return {
83
+ title,
84
+ appName,
85
+ processName: appName,
86
+ pid: null,
87
+ platform: 'darwin'
88
+ };
89
+ }
90
+
91
+ async function getWindowsActiveWindow() {
92
+ const script = [
93
+ 'Add-Type @\'',
94
+ 'using System;',
95
+ 'using System.Runtime.InteropServices;',
96
+ 'using System.Text;',
97
+ 'public class Win {',
98
+ '[DllImport("user32.dll")] public static extern IntPtr GetForegroundWindow();',
99
+ '[DllImport("user32.dll")] public static extern int GetWindowText(IntPtr hWnd, StringBuilder text, int count);',
100
+ '[DllImport("user32.dll")] public static extern uint GetWindowThreadProcessId(IntPtr hWnd, out uint pid);',
101
+ '}',
102
+ '\'@',
103
+ '$hwnd = [Win]::GetForegroundWindow()',
104
+ '$builder = New-Object System.Text.StringBuilder 1024',
105
+ '[void][Win]::GetWindowText($hwnd, $builder, $builder.Capacity)',
106
+ '$pid = 0',
107
+ '[void][Win]::GetWindowThreadProcessId($hwnd, [ref]$pid)',
108
+ '$proc = Get-Process -Id $pid -ErrorAction SilentlyContinue',
109
+ '[PSCustomObject]@{ title = $builder.ToString(); appName = $proc.ProcessName; pid = $pid } | ConvertTo-Json -Compress'
110
+ ].join('\n');
111
+ const output = await run('powershell.exe', ['-NoProfile', '-NonInteractive', '-Command', script], { timeout: 1800 });
112
+ if (!output) return null;
113
+ try {
114
+ const parsed = JSON.parse(output);
115
+ return {
116
+ title: parsed.title || '',
117
+ appName: normalizeProcessName(parsed.appName),
118
+ processName: normalizeProcessName(parsed.appName),
119
+ pid: parsed.pid ? Number(parsed.pid) : null,
120
+ platform: 'win32'
121
+ };
122
+ } catch (_) {
123
+ return null;
124
+ }
125
+ }
126
+
127
+ async function getActiveWindowContext(platform = process.platform) {
128
+ try {
129
+ if (platform === 'darwin') return await getMacActiveWindow();
130
+ if (platform === 'win32') return await getWindowsActiveWindow();
131
+ return await getLinuxActiveWindow();
132
+ } catch (_) {
133
+ return null;
134
+ }
135
+ }
136
+
137
+ async function getLinuxSelectedText() {
138
+ const attempts = [
139
+ ['wl-paste', ['--primary', '--no-newline']],
140
+ ['xclip', ['-selection', 'primary', '-out']],
141
+ ['xsel', ['--primary', '--output']]
142
+ ];
143
+
144
+ for (const [command, args] of attempts) {
145
+ const text = await run(command, args);
146
+ if (text) return truncateText(text);
147
+ }
148
+ return '';
149
+ }
150
+
151
+ async function getSelectedText(platform = process.platform) {
152
+ if (platform === 'linux') return getLinuxSelectedText();
153
+ return '';
154
+ }
155
+
156
+ async function getMacBrowserContext(appName) {
157
+ const normalized = String(appName || '').toLowerCase();
158
+ let script = '';
159
+ if (normalized.includes('safari')) {
160
+ script = 'tell application "Safari" to return name of front document & linefeed & URL of front document';
161
+ } else if (normalized.includes('chrome') || normalized.includes('chromium') || normalized.includes('brave') || normalized.includes('edge')) {
162
+ script = `tell application "${appName}" to return title of active tab of front window & linefeed & URL of active tab of front window`;
163
+ }
164
+ if (!script) return null;
165
+ const output = await run('osascript', ['-e', script], { timeout: 1500 });
166
+ if (!output) return null;
167
+ const [title = '', url = ''] = output.split(/\r?\n/);
168
+ return { title, url };
169
+ }
170
+
171
+ async function getBrowserContext(activeWindow, platform = process.platform) {
172
+ if (!activeWindow || !isBrowserProcess(activeWindow.appName || activeWindow.processName)) {
173
+ return null;
174
+ }
175
+ if (platform === 'darwin') {
176
+ const browser = await getMacBrowserContext(activeWindow.appName);
177
+ if (browser) return browser;
178
+ }
179
+ return {
180
+ title: activeWindow.title || '',
181
+ url: '',
182
+ urlUnavailableReason: 'Browser URL is not available from the current OS context without browser integration.'
183
+ };
184
+ }
185
+
186
+ async function getSmartContext(options = {}) {
187
+ const platform = options.platform || process.platform;
188
+ const clipboard = options.clipboard || null;
189
+ const [activeWindow, selectedText] = await Promise.all([
190
+ getActiveWindowContext(platform),
191
+ getSelectedText(platform)
192
+ ]);
193
+
194
+ let clipboardText = '';
195
+ try {
196
+ clipboardText = clipboard && typeof clipboard.readText === 'function'
197
+ ? truncateText(clipboard.readText())
198
+ : '';
199
+ } catch (_) {
200
+ clipboardText = '';
201
+ }
202
+
203
+ const browser = await getBrowserContext(activeWindow, platform);
204
+ return {
205
+ capturedAt: new Date().toISOString(),
206
+ platform,
207
+ host: os.hostname(),
208
+ activeWindow,
209
+ currentApp: activeWindow ? {
210
+ name: activeWindow.appName || activeWindow.processName || '',
211
+ processName: activeWindow.processName || activeWindow.appName || '',
212
+ pid: activeWindow.pid || null
213
+ } : null,
214
+ browser,
215
+ selectedText,
216
+ clipboardText
217
+ };
218
+ }
219
+
220
+ module.exports = {
221
+ getSmartContext,
222
+ getActiveWindowContext,
223
+ getBrowserContext,
224
+ getSelectedText,
225
+ truncateText,
226
+ isBrowserProcess
227
+ };
@@ -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'],