@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.
- package/GUIDE_TH.md +23 -11
- package/README.md +148 -66
- package/assets/Agent_Mint.png +0 -0
- package/assets/Settings.png +0 -0
- package/install.ps1 +64 -0
- package/install.sh +54 -0
- package/main.js +12 -0
- package/package.json +5 -3
- package/preload.js +4 -0
- package/scripts/install_linux_desktop_entry.js +48 -0
- package/src/AI_Brain/Gemini_API.js +231 -498
- package/src/AI_Brain/autonomous_brain.js +46 -19
- package/src/AI_Brain/headless_agent.js +21 -2
- package/src/AI_Brain/provider_adapter.js +358 -0
- package/src/Automation_Layer/file_operations.js +17 -5
- package/src/CLI/approval_handler.js +5 -0
- package/src/CLI/chat_router.js +7 -0
- package/src/CLI/chat_ui.js +397 -76
- package/src/CLI/cli_colors.js +86 -3
- package/src/CLI/cli_formatters.js +6 -1
- package/src/CLI/code_agent.js +706 -273
- package/src/CLI/interactive_chat.js +311 -149
- package/src/CLI/slash_command_handler.js +2 -2
- package/src/CLI/updater.js +21 -1
- package/src/System/config_manager.js +5 -1
- package/src/System/ipc_handlers.js +95 -1
- package/src/System/picture_store.js +109 -0
- package/src/System/smart_context.js +227 -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 +42 -8
- package/src/UI/preload-spotlight.js +1 -0
- package/src/UI/renderer.js +837 -63
- package/src/UI/settings.css +160 -96
- package/src/UI/settings.html +9 -0
- package/src/UI/settings.js +35 -2
- package/src/UI/spotlight.js +13 -9
- package/src/UI/styles.css +1592 -165
- 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'],
|