@pheem49/mint 1.4.2 → 1.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/GUIDE_TH.md +113 -0
- package/README.md +267 -78
- package/assets/CLI_Screen.png +0 -0
- package/main.js +76 -890
- package/mint-cli-logic.js +3 -107
- package/mint-cli.js +594 -29
- package/models/Shiroko_Model/Shiroko/Shiroko_Core/72d86db84cfa9730b894c241fd24c0db.png +0 -0
- package/models/Shiroko_Model/Shiroko/Shiroko_Core/items_pinned_to_model.json +14 -0
- package/models/Shiroko_Model/Shiroko/Shiroko_Core//345/221/206/347/214/253.exp3.json +10 -0
- package/models/Shiroko_Model/Shiroko/Shiroko_Core//345/221/206/347/214/253/347/234/274/347/217/240/346/221/207/346/231/203.exp3.json +15 -0
- package/models/Shiroko_Model/Shiroko/Shiroko_Core//345/233/264/350/243/231.exp3.json +10 -0
- package/models/Shiroko_Model/Shiroko/Shiroko_Core//346/213/215/347/205/247.exp3.json +50 -0
- package/models/Shiroko_Model/Shiroko/Shiroko_Core//346/213/277/347/254/224.exp3.json +10 -0
- package/models/Shiroko_Model/Shiroko/Shiroko_Core//347/202/271/344/270/200/344/270/213.exp3.json +10 -0
- package/models/Shiroko_Model/Shiroko/Shiroko_Core//347/214/253/345/222/252/346/273/244/351/225/234.exp3.json +10 -0
- package/models/Shiroko_Model/Shiroko/Shiroko_Core//347/234/274/351/225/234.exp3.json +10 -0
- package/models/Shiroko_Model/Shiroko/Shiroko_Core//351/235/242/351/245/2740.4096/texture_00.png +0 -0
- package/models/Shiroko_Model/Shiroko/Shiroko_Core//351/235/242/351/245/2740.4096/texture_01.png +0 -0
- package/models/Shiroko_Model/Shiroko/Shiroko_Core//351/235/242/351/245/2740.4096/texture_02.png +0 -0
- package/models/Shiroko_Model/Shiroko/Shiroko_Core//351/235/242/351/245/2740.4096/texture_03.png +0 -0
- package/models/Shiroko_Model/Shiroko/Shiroko_Core//351/235/242/351/245/2740.cdi3.json +1498 -0
- package/models/Shiroko_Model/Shiroko/Shiroko_Core//351/235/242/351/245/2740.moc3 +0 -0
- package/models/Shiroko_Model/Shiroko/Shiroko_Core//351/235/242/351/245/2740.model3.json +47 -0
- package/models/Shiroko_Model/Shiroko/Shiroko_Core//351/235/242/351/245/2740.physics3.json +6658 -0
- package/models/Shiroko_Model/Shiroko/Shiroko_Core//351/235/242/351/245/2740.vtube.json +1299 -0
- package/models/Shiroko_Model/Shiroko//342/232/241/351/253/230/344/272/256/342/232/241/344/275/277/347/224/250/346/225/231/347/250/213/344/270/216/346/263/250/346/204/217/344/272/213/351/241/271.txt +23 -0
- package/package.json +37 -4
- package/src/AI_Brain/Gemini_API.js +223 -65
- package/src/AI_Brain/autonomous_brain.js +11 -0
- package/src/AI_Brain/behavior_memory.js +26 -5
- package/src/AI_Brain/headless_agent.js +4 -0
- package/src/AI_Brain/knowledge_base.js +61 -8
- package/src/AI_Brain/memory_store.js +354 -10
- package/src/Automation_Layer/file_operations.js +1 -1
- package/src/CLI/chat_router.js +20 -7
- package/src/CLI/chat_ui.js +596 -825
- package/src/CLI/code_agent.js +347 -56
- package/src/CLI/gmail_auth.js +210 -0
- package/src/CLI/image_input.js +90 -0
- package/src/CLI/list_features.js +2 -0
- package/src/CLI/onboarding.js +364 -55
- package/src/CLI/updater.js +210 -0
- package/src/Channels/brave_search_bridge.js +35 -0
- package/src/Channels/discord_bridge.js +68 -0
- package/src/Channels/google_search_bridge.js +38 -0
- package/src/Channels/line_bridge.js +60 -0
- package/src/Channels/slack_bridge.js +53 -0
- package/src/Channels/telegram_bridge.js +49 -0
- package/src/Channels/whatsapp_bridge.js +55 -0
- package/src/Command_Parser/parser.js +12 -1
- package/src/Plugins/gmail.js +251 -0
- package/src/Plugins/google_calendar.js +245 -19
- package/src/Plugins/notion.js +256 -0
- package/src/System/action_executor.js +178 -0
- package/src/System/bridge_manager.js +76 -0
- package/src/System/chat_history_manager.js +23 -5
- package/src/System/config_manager.js +71 -7
- package/src/System/custom_workflows.js +31 -2
- package/src/System/google_tts_urls.js +51 -0
- package/src/System/granular_automation.js +122 -53
- package/src/System/ipc_handlers.js +238 -0
- package/src/System/proactive_loop.js +153 -0
- package/src/System/safety_manager.js +273 -0
- package/src/System/sandbox_runner.js +182 -0
- package/src/System/screen_capture.js +175 -0
- package/src/System/system_automation.js +127 -81
- package/src/System/system_info.js +70 -0
- package/src/System/task_manager.js +15 -5
- package/src/System/tool_registry.js +280 -0
- package/src/System/window_manager.js +212 -0
- package/src/UI/live2d_manager.js +368 -0
- package/src/UI/renderer.js +208 -24
- package/src/UI/settings.html +24 -0
- package/src/UI/settings.js +14 -4
- package/src/UI/styles.css +466 -32
- package/.codex +0 -0
- package/docs/assets/Agent_Mint.png +0 -0
- package/docs/assets/CLI_Screen.png +0 -0
- package/docs/assets/Settings.png +0 -0
- package/docs/assets/icon.png +0 -0
- package/docs/index.html +0 -132
- package/docs/style.css +0 -579
- package/index.html +0 -16
- package/src/UI/index.html +0 -126
- package/tech_news.txt +0 -3
- package/test_knowledge.txt +0 -3
- package/tests/agent_orchestrator.test.js +0 -41
- package/tests/chat_router.test.js +0 -42
- package/tests/code_agent.test.js +0 -69
- package/tests/config_manager.test.js +0 -141
- package/tests/docker.test.js +0 -46
- package/tests/file_operations.test.js +0 -57
- package/tests/memory_store.test.js +0 -185
- package/tests/provider_routing.test.js +0 -67
- package/tests/spotify.test.js +0 -201
- package/tests/system_monitor.test.js +0 -37
- package/tests/workspace_manager.test.js +0 -56
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const os = require('os');
|
|
3
|
+
const path = require('path');
|
|
4
|
+
const { readConfig } = require('./config_manager');
|
|
5
|
+
|
|
6
|
+
const TIERS = Object.freeze({
|
|
7
|
+
SAFE: 'safe',
|
|
8
|
+
APPROVAL: 'approval',
|
|
9
|
+
DANGEROUS: 'dangerous',
|
|
10
|
+
BLOCKED: 'blocked'
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
const BLOCKED_COMMAND_PATTERNS = [
|
|
14
|
+
{ pattern: /\brm\s+(-[^\s]*r[^\s]*f|-rf|-fr)\b/, reason: 'recursive force delete' },
|
|
15
|
+
{ pattern: /\bgit\s+reset\s+--hard\b/, reason: 'destructive git reset' },
|
|
16
|
+
{ pattern: /\bgit\s+checkout\s+--\b/, reason: 'destructive git checkout path restore' },
|
|
17
|
+
{ pattern: /\bgit\s+clean\b.*\s-[^\s]*f/, reason: 'destructive git clean' },
|
|
18
|
+
{ pattern: /\bmkfs(?:\.\w+)?\b/, reason: 'filesystem formatting' },
|
|
19
|
+
{ pattern: /\bdd\s+.*\bof=\/dev\//, reason: 'raw disk write' },
|
|
20
|
+
{ pattern: />\s*\/dev\/(?:sd|nvme|hd|mapper)/, reason: 'write redirection to block device' },
|
|
21
|
+
{ pattern: /\b(shutdown|reboot|poweroff|halt)\b/, reason: 'system power command' },
|
|
22
|
+
{ pattern: /\bsudo\b/, reason: 'privilege escalation' },
|
|
23
|
+
{ pattern: /\bchmod\s+-R\s+777\b/, reason: 'unsafe recursive permissions' },
|
|
24
|
+
{ pattern: /\bchown\s+-R\b/, reason: 'unsafe recursive ownership change' },
|
|
25
|
+
{ pattern: /\bcurl\b.*\|\s*(sh|bash|zsh)\b/, reason: 'remote script piping' },
|
|
26
|
+
{ pattern: /\bwget\b.*\|\s*(sh|bash|zsh)\b/, reason: 'remote script piping' }
|
|
27
|
+
];
|
|
28
|
+
|
|
29
|
+
const DANGEROUS_ACTIONS = new Set([
|
|
30
|
+
'delete_file',
|
|
31
|
+
'system_automation'
|
|
32
|
+
]);
|
|
33
|
+
|
|
34
|
+
const SAFE_ACTIONS = new Set([
|
|
35
|
+
'open_url',
|
|
36
|
+
'search',
|
|
37
|
+
'open_app',
|
|
38
|
+
'open_file',
|
|
39
|
+
'open_folder',
|
|
40
|
+
'find_path',
|
|
41
|
+
'clipboard_write',
|
|
42
|
+
'learn_file',
|
|
43
|
+
'learn_folder',
|
|
44
|
+
'system_info',
|
|
45
|
+
'mcp_tool',
|
|
46
|
+
'mouse_move',
|
|
47
|
+
'mouse_click',
|
|
48
|
+
'type_text',
|
|
49
|
+
'key_tap',
|
|
50
|
+
'plugin',
|
|
51
|
+
'web_automation',
|
|
52
|
+
'create_folder'
|
|
53
|
+
]);
|
|
54
|
+
|
|
55
|
+
const DANGEROUS_SYSTEM_COMMANDS = new Set(['shutdown', 'restart', 'reboot', 'poweroff', 'sleep']);
|
|
56
|
+
|
|
57
|
+
function normalizeCommand(command) {
|
|
58
|
+
return String(command || '').replace(/\s+/g, ' ').trim();
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function expandHome(targetPath) {
|
|
62
|
+
const value = String(targetPath || '');
|
|
63
|
+
if (value === '~') return os.homedir();
|
|
64
|
+
if (value.startsWith('~/')) return path.join(os.homedir(), value.slice(2));
|
|
65
|
+
return value;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function normalizeRootList(paths) {
|
|
69
|
+
return (Array.isArray(paths) ? paths : [])
|
|
70
|
+
.filter(Boolean)
|
|
71
|
+
.map((entry) => path.resolve(expandHome(entry)));
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function getPolicy(config = readConfig()) {
|
|
75
|
+
const enabled = config.safetyEnabled !== false;
|
|
76
|
+
const fallbackRead = [
|
|
77
|
+
os.homedir(),
|
|
78
|
+
process.cwd(),
|
|
79
|
+
path.join(os.homedir(), 'Desktop'),
|
|
80
|
+
path.join(os.homedir(), 'Documents'),
|
|
81
|
+
path.join(os.homedir(), 'Downloads'),
|
|
82
|
+
path.join(os.homedir(), 'Pictures'),
|
|
83
|
+
path.join(os.homedir(), 'Music'),
|
|
84
|
+
path.join(os.homedir(), 'Videos')
|
|
85
|
+
];
|
|
86
|
+
const fallbackWrite = [
|
|
87
|
+
os.homedir(),
|
|
88
|
+
process.cwd(),
|
|
89
|
+
path.join(os.homedir(), 'Desktop'),
|
|
90
|
+
path.join(os.homedir(), 'Documents'),
|
|
91
|
+
path.join(os.homedir(), 'Downloads'),
|
|
92
|
+
path.join(os.homedir(), 'Pictures'),
|
|
93
|
+
path.join(os.homedir(), 'Music'),
|
|
94
|
+
path.join(os.homedir(), 'Videos')
|
|
95
|
+
];
|
|
96
|
+
|
|
97
|
+
return {
|
|
98
|
+
enabled,
|
|
99
|
+
sandboxMode: ['off', 'prefer', 'enforce'].includes(config.sandboxMode) ? config.sandboxMode : 'prefer',
|
|
100
|
+
sandboxCommand: config.sandboxCommand || (process.platform === 'darwin' ? 'sandbox-exec' : process.platform === 'linux' ? 'bwrap' : ''),
|
|
101
|
+
allowedReadPaths: normalizeRootList(config.allowedReadPaths && config.allowedReadPaths.length ? config.allowedReadPaths : fallbackRead),
|
|
102
|
+
allowedWritePaths: normalizeRootList(config.allowedWritePaths && config.allowedWritePaths.length ? config.allowedWritePaths : fallbackWrite),
|
|
103
|
+
blockedPaths: normalizeRootList(config.blockedPaths || []),
|
|
104
|
+
blockedFileNames: new Set(Array.isArray(config.blockedFileNames) ? config.blockedFileNames : ['.env', 'id_rsa', 'id_ed25519'])
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function isPathWithin(root, targetPath) {
|
|
109
|
+
const relative = path.relative(root, targetPath);
|
|
110
|
+
return relative === '' || (!relative.startsWith('..') && !path.isAbsolute(relative));
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function resolveCapabilityPath(targetPath, options = {}) {
|
|
114
|
+
if (!targetPath) throw new Error('Target path is required.');
|
|
115
|
+
|
|
116
|
+
const expanded = expandHome(targetPath);
|
|
117
|
+
if (path.isAbsolute(expanded)) return path.resolve(expanded);
|
|
118
|
+
|
|
119
|
+
const firstPart = String(expanded).split(/[/\\]/)[0];
|
|
120
|
+
const commonHomeFolders = new Set(['Desktop', 'Documents', 'Downloads', 'Pictures', 'Music', 'Videos']);
|
|
121
|
+
if (commonHomeFolders.has(firstPart)) {
|
|
122
|
+
return path.resolve(os.homedir(), expanded);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const base = options.defaultBase || process.cwd();
|
|
126
|
+
return path.resolve(base, expanded);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function assertPathCapability(targetPath, capability = 'read', options = {}) {
|
|
130
|
+
const policy = getPolicy(options.config);
|
|
131
|
+
const resolved = resolveCapabilityPath(targetPath, options);
|
|
132
|
+
|
|
133
|
+
if (!policy.enabled) return resolved;
|
|
134
|
+
|
|
135
|
+
if (policy.blockedFileNames.has(path.basename(resolved))) {
|
|
136
|
+
throw new Error(`Blocked ${capability} access to sensitive file name: ${path.basename(resolved)}`);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const blockedRoot = policy.blockedPaths.find((root) => isPathWithin(root, resolved));
|
|
140
|
+
if (blockedRoot) {
|
|
141
|
+
throw new Error(`Blocked ${capability} access to protected path: ${resolved}`);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const allowedRoots = capability === 'write' ? policy.allowedWritePaths : policy.allowedReadPaths;
|
|
145
|
+
const allowed = allowedRoots.some((root) => isPathWithin(root, resolved));
|
|
146
|
+
if (!allowed) {
|
|
147
|
+
throw new Error(`Path ${capability} denied by capability policy: ${resolved}`);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
return resolved;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function getAllowedRoots(capability = 'read', options = {}) {
|
|
154
|
+
const policy = getPolicy(options.config);
|
|
155
|
+
return capability === 'write' ? policy.allowedWritePaths : policy.allowedReadPaths;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function classifyShellCommand(command) {
|
|
159
|
+
const normalized = normalizeCommand(command);
|
|
160
|
+
if (!normalized) {
|
|
161
|
+
return { tier: TIERS.BLOCKED, reason: 'empty shell command' };
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
for (const rule of BLOCKED_COMMAND_PATTERNS) {
|
|
165
|
+
if (rule.pattern.test(normalized)) {
|
|
166
|
+
return { tier: TIERS.BLOCKED, reason: rule.reason };
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
return { tier: TIERS.APPROVAL, reason: 'shell command requires approval' };
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function assertShellCommandAllowed(command) {
|
|
174
|
+
const result = classifyShellCommand(command);
|
|
175
|
+
if (result.tier === TIERS.BLOCKED) {
|
|
176
|
+
throw new Error(`Blocked unsafe command (${result.reason}): ${command}`);
|
|
177
|
+
}
|
|
178
|
+
return result;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function classifyAction(action = {}) {
|
|
182
|
+
const type = action.type || 'none';
|
|
183
|
+
if (type === 'none') return { tier: TIERS.SAFE, reason: 'no-op action' };
|
|
184
|
+
|
|
185
|
+
if (type === 'system_automation') {
|
|
186
|
+
const command = String(action.target || '').split(':')[0];
|
|
187
|
+
if (DANGEROUS_SYSTEM_COMMANDS.has(command)) {
|
|
188
|
+
return { tier: TIERS.DANGEROUS, reason: `system automation command '${command}'` };
|
|
189
|
+
}
|
|
190
|
+
return { tier: TIERS.APPROVAL, reason: 'system automation requires approval' };
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
if (DANGEROUS_ACTIONS.has(type)) {
|
|
194
|
+
return { tier: TIERS.DANGEROUS, reason: `${type} can affect user data or system state` };
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
if (SAFE_ACTIONS.has(type)) {
|
|
198
|
+
return { tier: TIERS.SAFE, reason: 'allowed action' };
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
return { tier: TIERS.APPROVAL, reason: 'unknown action requires approval' };
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function assertActionAllowed(action, options = {}) {
|
|
205
|
+
const classification = classifyAction(action);
|
|
206
|
+
const allowDangerous = options.allowDangerous === true;
|
|
207
|
+
const allowApproval = options.allowApproval === true;
|
|
208
|
+
|
|
209
|
+
if (classification.tier === TIERS.BLOCKED) {
|
|
210
|
+
throw new Error(`Blocked action (${classification.reason}): ${action.type}`);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
if (classification.tier === TIERS.DANGEROUS && !allowDangerous) {
|
|
214
|
+
throw new Error(`Dangerous action requires explicit permission (${classification.reason}): ${action.type}`);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
if (classification.tier === TIERS.APPROVAL && !allowApproval) {
|
|
218
|
+
throw new Error(`Action requires approval (${classification.reason}): ${action.type}`);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
return classification;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function resolveWithinRoot(root, targetPath) {
|
|
225
|
+
if (!root) throw new Error('Root path is required.');
|
|
226
|
+
if (!targetPath) throw new Error('Target path is required.');
|
|
227
|
+
|
|
228
|
+
const resolvedRoot = path.resolve(root);
|
|
229
|
+
const expandedTarget = String(targetPath).startsWith('~/')
|
|
230
|
+
? path.join(os.homedir(), String(targetPath).slice(2))
|
|
231
|
+
: targetPath;
|
|
232
|
+
const resolvedTarget = path.resolve(resolvedRoot, expandedTarget);
|
|
233
|
+
const relative = path.relative(resolvedRoot, resolvedTarget);
|
|
234
|
+
|
|
235
|
+
if (relative.startsWith('..') || path.isAbsolute(relative)) {
|
|
236
|
+
throw new Error(`Path is outside allowed root: ${targetPath}`);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
return resolvedTarget;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function appendActionLog(entry, options = {}) {
|
|
243
|
+
const logPath = options.logPath || path.join(os.homedir(), '.config', 'mint', 'action-log.jsonl');
|
|
244
|
+
const payload = {
|
|
245
|
+
time: new Date().toISOString(),
|
|
246
|
+
...entry
|
|
247
|
+
};
|
|
248
|
+
|
|
249
|
+
try {
|
|
250
|
+
fs.mkdirSync(path.dirname(logPath), { recursive: true });
|
|
251
|
+
fs.appendFileSync(logPath, `${JSON.stringify(payload)}\n`, 'utf8');
|
|
252
|
+
} catch (error) {
|
|
253
|
+
if (process.env.MINT_DEBUG === '1') {
|
|
254
|
+
console.error('[Safety] Failed to append action log:', error.message);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
return payload;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
module.exports = {
|
|
262
|
+
TIERS,
|
|
263
|
+
getPolicy,
|
|
264
|
+
getAllowedRoots,
|
|
265
|
+
classifyShellCommand,
|
|
266
|
+
assertShellCommandAllowed,
|
|
267
|
+
classifyAction,
|
|
268
|
+
assertActionAllowed,
|
|
269
|
+
assertPathCapability,
|
|
270
|
+
resolveCapabilityPath,
|
|
271
|
+
resolveWithinRoot,
|
|
272
|
+
appendActionLog
|
|
273
|
+
};
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
const { execFile, spawnSync } = require('child_process');
|
|
2
|
+
const fs = require('fs');
|
|
3
|
+
const os = require('os');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const safetyManager = require('./safety_manager');
|
|
6
|
+
|
|
7
|
+
function commandExists(command) {
|
|
8
|
+
const lookup = process.platform === 'win32' ? 'where' : 'which';
|
|
9
|
+
const result = spawnSync(lookup, [command], { encoding: 'utf8', shell: false });
|
|
10
|
+
return result.status === 0;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function uniqueExistingRoots(roots) {
|
|
14
|
+
return Array.from(new Set((roots || [])
|
|
15
|
+
.filter(Boolean)
|
|
16
|
+
.map((root) => path.resolve(root))
|
|
17
|
+
.filter((root) => fs.existsSync(root))));
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function buildBubblewrapArgs(command, options = {}) {
|
|
21
|
+
const cwd = path.resolve(options.cwd || process.cwd());
|
|
22
|
+
const readRoots = uniqueExistingRoots(safetyManager.getAllowedRoots('read'));
|
|
23
|
+
const writeRoots = uniqueExistingRoots(safetyManager.getAllowedRoots('write'));
|
|
24
|
+
const writableSet = new Set(writeRoots);
|
|
25
|
+
const bindRoots = uniqueExistingRoots([cwd, ...readRoots, ...writeRoots]);
|
|
26
|
+
|
|
27
|
+
const args = [
|
|
28
|
+
'--die-with-parent',
|
|
29
|
+
'--proc', '/proc',
|
|
30
|
+
'--dev', '/dev',
|
|
31
|
+
'--tmpfs', '/tmp',
|
|
32
|
+
'--ro-bind', '/usr', '/usr',
|
|
33
|
+
'--ro-bind', '/bin', '/bin',
|
|
34
|
+
'--ro-bind', '/etc', '/etc'
|
|
35
|
+
];
|
|
36
|
+
|
|
37
|
+
for (const libPath of ['/lib', '/lib64']) {
|
|
38
|
+
if (fs.existsSync(libPath)) args.push('--ro-bind', libPath, libPath);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const parentDirs = new Set();
|
|
42
|
+
for (const root of bindRoots) {
|
|
43
|
+
let current = path.dirname(root);
|
|
44
|
+
while (current && current !== path.dirname(current)) {
|
|
45
|
+
parentDirs.add(current);
|
|
46
|
+
current = path.dirname(current);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
for (const dir of Array.from(parentDirs).sort((a, b) => a.length - b.length)) {
|
|
50
|
+
if (!['/usr', '/bin', '/etc', '/lib', '/lib64'].includes(dir)) {
|
|
51
|
+
args.push('--dir', dir);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
for (const root of bindRoots) {
|
|
56
|
+
const flag = writableSet.has(root) || root === cwd ? '--bind' : '--ro-bind';
|
|
57
|
+
args.push(flag, root, root);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
args.push('--chdir', cwd, 'bash', '-lc', command);
|
|
61
|
+
return args;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function escapeSandboxProfileString(value) {
|
|
65
|
+
return String(value || '').replace(/\\/g, '\\\\').replace(/"/g, '\\"');
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function buildMacSandboxProfile(options = {}) {
|
|
69
|
+
const cwd = path.resolve(options.cwd || process.cwd());
|
|
70
|
+
const readRoots = uniqueExistingRoots(safetyManager.getAllowedRoots('read'));
|
|
71
|
+
const writeRoots = uniqueExistingRoots(safetyManager.getAllowedRoots('write'));
|
|
72
|
+
const allowedRead = uniqueExistingRoots([
|
|
73
|
+
cwd,
|
|
74
|
+
'/bin',
|
|
75
|
+
'/sbin',
|
|
76
|
+
'/usr',
|
|
77
|
+
'/System',
|
|
78
|
+
'/Library',
|
|
79
|
+
...readRoots,
|
|
80
|
+
...writeRoots
|
|
81
|
+
]);
|
|
82
|
+
const allowedWrite = uniqueExistingRoots([cwd, ...writeRoots]);
|
|
83
|
+
|
|
84
|
+
const readRules = allowedRead.map((root) => ` (subpath "${escapeSandboxProfileString(root)}")`).join('\n');
|
|
85
|
+
const writeRules = allowedWrite.map((root) => ` (subpath "${escapeSandboxProfileString(root)}")`).join('\n');
|
|
86
|
+
|
|
87
|
+
return [
|
|
88
|
+
'(version 1)',
|
|
89
|
+
'(deny default)',
|
|
90
|
+
'(allow process*)',
|
|
91
|
+
'(allow sysctl-read)',
|
|
92
|
+
'(allow signal (target self))',
|
|
93
|
+
'(allow file-read-metadata)',
|
|
94
|
+
'(allow file-read*',
|
|
95
|
+
readRules,
|
|
96
|
+
')',
|
|
97
|
+
'(allow file-write*',
|
|
98
|
+
writeRules,
|
|
99
|
+
` (subpath "${escapeSandboxProfileString(os.tmpdir())}")`,
|
|
100
|
+
')'
|
|
101
|
+
].join('\n');
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function getShellInvocation(command) {
|
|
105
|
+
if (process.platform === 'win32') {
|
|
106
|
+
return {
|
|
107
|
+
command: 'powershell.exe',
|
|
108
|
+
args: ['-NoLogo', '-NoProfile', '-NonInteractive', '-ExecutionPolicy', 'Bypass', '-Command', command]
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
return {
|
|
112
|
+
command: 'bash',
|
|
113
|
+
args: ['-lc', command]
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function execFilePromise(command, args, options = {}) {
|
|
118
|
+
return new Promise((resolve, reject) => {
|
|
119
|
+
execFile(command, args, options, (error, stdout, stderr) => {
|
|
120
|
+
if (error) {
|
|
121
|
+
error.stdout = stdout;
|
|
122
|
+
error.stderr = stderr;
|
|
123
|
+
reject(error);
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
resolve({ stdout, stderr });
|
|
127
|
+
});
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
async function runShell(command, options = {}) {
|
|
132
|
+
const policy = safetyManager.getPolicy();
|
|
133
|
+
safetyManager.assertShellCommandAllowed(command);
|
|
134
|
+
|
|
135
|
+
const cwd = path.resolve(options.cwd || process.cwd());
|
|
136
|
+
const execOptions = {
|
|
137
|
+
cwd,
|
|
138
|
+
maxBuffer: options.maxBuffer || 1024 * 1024 * 4,
|
|
139
|
+
env: options.env || process.env
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
if (!policy.enabled || policy.sandboxMode === 'off') {
|
|
143
|
+
const shell = getShellInvocation(command);
|
|
144
|
+
return execFilePromise(shell.command, shell.args, execOptions);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const sandboxCommand = policy.sandboxCommand || 'bwrap';
|
|
148
|
+
if (process.platform === 'linux' && commandExists(sandboxCommand)) {
|
|
149
|
+
return execFilePromise(sandboxCommand, buildBubblewrapArgs(command, { cwd }), execOptions);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
if (process.platform === 'darwin' && commandExists('sandbox-exec')) {
|
|
153
|
+
return execFilePromise('sandbox-exec', ['-p', buildMacSandboxProfile({ cwd }), 'bash', '-lc', command], execOptions);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
if (policy.sandboxMode === 'enforce') {
|
|
157
|
+
const hint = process.platform === 'darwin'
|
|
158
|
+
? "macOS sandbox-exec is not available."
|
|
159
|
+
: process.platform === 'win32'
|
|
160
|
+
? 'Windows sandbox provider is not configured. Use WSL/containers or set sandboxMode to prefer.'
|
|
161
|
+
: `Sandbox command '${sandboxCommand}' is not available.`;
|
|
162
|
+
throw new Error(`Sandbox is enforced but no sandbox provider could run. ${hint}`);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
safetyManager.appendActionLog({
|
|
166
|
+
source: options.source || 'sandbox_runner',
|
|
167
|
+
action: 'sandbox_fallback',
|
|
168
|
+
sandboxCommand,
|
|
169
|
+
platform: process.platform,
|
|
170
|
+
cwd
|
|
171
|
+
});
|
|
172
|
+
const shell = getShellInvocation(command);
|
|
173
|
+
return execFilePromise(shell.command, shell.args, execOptions);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
module.exports = {
|
|
177
|
+
runShell,
|
|
178
|
+
buildBubblewrapArgs,
|
|
179
|
+
buildMacSandboxProfile,
|
|
180
|
+
getShellInvocation,
|
|
181
|
+
commandExists
|
|
182
|
+
};
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
const { BrowserWindow, desktopCapturer, screen } = require('electron');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
|
|
4
|
+
const TRANSLATE_REFRESH_MS = 3000;
|
|
5
|
+
const TRANSLATE_FAILURE_COOLDOWN_MS = 15000;
|
|
6
|
+
|
|
7
|
+
function createScreenCaptureController({ projectRoot, translateImageContent, getMainWindow }) {
|
|
8
|
+
let screenPickerWindow = null;
|
|
9
|
+
let translateIntervalHandle = null;
|
|
10
|
+
let isTranslateRequestInFlight = false;
|
|
11
|
+
let translateCooldownUntil = 0;
|
|
12
|
+
|
|
13
|
+
async function startScreenCapture() {
|
|
14
|
+
if (screenPickerWindow) return;
|
|
15
|
+
|
|
16
|
+
try {
|
|
17
|
+
const primaryDisplay = screen.getPrimaryDisplay();
|
|
18
|
+
const { width, height } = primaryDisplay.size;
|
|
19
|
+
const sources = await desktopCapturer.getSources({
|
|
20
|
+
types: ['screen'],
|
|
21
|
+
thumbnailSize: { width, height }
|
|
22
|
+
});
|
|
23
|
+
const primarySource = sources[0];
|
|
24
|
+
|
|
25
|
+
screenPickerWindow = new BrowserWindow({
|
|
26
|
+
width,
|
|
27
|
+
height,
|
|
28
|
+
x: primaryDisplay.bounds.x,
|
|
29
|
+
y: primaryDisplay.bounds.y,
|
|
30
|
+
fullscreen: true,
|
|
31
|
+
transparent: true,
|
|
32
|
+
frame: false,
|
|
33
|
+
alwaysOnTop: true,
|
|
34
|
+
skipTaskbar: true,
|
|
35
|
+
webPreferences: {
|
|
36
|
+
preload: path.join(projectRoot, 'preload-picker.js'),
|
|
37
|
+
nodeIntegration: false,
|
|
38
|
+
contextIsolation: true
|
|
39
|
+
}
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
await screenPickerWindow.loadFile(path.join(projectRoot, 'src/UI/screenPicker.html'));
|
|
43
|
+
|
|
44
|
+
if (primarySource && primarySource.thumbnail) {
|
|
45
|
+
screenPickerWindow.webContents.send('screenshot-data', primarySource.thumbnail.toDataURL());
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
screenPickerWindow.on('closed', () => { screenPickerWindow = null; });
|
|
49
|
+
} catch (err) {
|
|
50
|
+
console.error("Error starting screen capture:", err);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function handleSelection(base64Image) {
|
|
55
|
+
if (screenPickerWindow) screenPickerWindow.close();
|
|
56
|
+
|
|
57
|
+
const mainWindow = getMainWindow();
|
|
58
|
+
if (mainWindow) {
|
|
59
|
+
mainWindow.webContents.send('vision-ready', base64Image);
|
|
60
|
+
mainWindow.show();
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function startLiveTranslate(rect) {
|
|
65
|
+
if (!screenPickerWindow) return;
|
|
66
|
+
|
|
67
|
+
screenPickerWindow.setIgnoreMouseEvents(true, { forward: true });
|
|
68
|
+
isTranslateRequestInFlight = false;
|
|
69
|
+
translateCooldownUntil = 0;
|
|
70
|
+
|
|
71
|
+
stopLiveTranslate(false);
|
|
72
|
+
captureAndTranslate(rect);
|
|
73
|
+
translateIntervalHandle = setInterval(() => {
|
|
74
|
+
captureAndTranslate(rect);
|
|
75
|
+
}, TRANSLATE_REFRESH_MS);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function stopLiveTranslate(resetMouseEvents = true) {
|
|
79
|
+
if (translateIntervalHandle) {
|
|
80
|
+
clearInterval(translateIntervalHandle);
|
|
81
|
+
translateIntervalHandle = null;
|
|
82
|
+
}
|
|
83
|
+
if (resetMouseEvents && screenPickerWindow && !screenPickerWindow.isDestroyed()) {
|
|
84
|
+
screenPickerWindow.setIgnoreMouseEvents(false);
|
|
85
|
+
}
|
|
86
|
+
isTranslateRequestInFlight = false;
|
|
87
|
+
translateCooldownUntil = 0;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function setOverlayInteractable(isInteractable) {
|
|
91
|
+
if (!screenPickerWindow || screenPickerWindow.isDestroyed()) return;
|
|
92
|
+
screenPickerWindow.setIgnoreMouseEvents(!isInteractable, { forward: true });
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
async function captureAndTranslate(rect) {
|
|
96
|
+
if (!screenPickerWindow || screenPickerWindow.isDestroyed()) return;
|
|
97
|
+
if (isTranslateRequestInFlight) return;
|
|
98
|
+
if (Date.now() < translateCooldownUntil) return;
|
|
99
|
+
|
|
100
|
+
try {
|
|
101
|
+
isTranslateRequestInFlight = true;
|
|
102
|
+
const primaryDisplay = screen.getPrimaryDisplay();
|
|
103
|
+
const sources = await desktopCapturer.getSources({
|
|
104
|
+
types: ['screen'],
|
|
105
|
+
thumbnailSize: {
|
|
106
|
+
width: primaryDisplay.size.width,
|
|
107
|
+
height: primaryDisplay.size.height
|
|
108
|
+
}
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
if (sources.length > 0) {
|
|
112
|
+
const croppedImage = sources[0].thumbnail.crop({
|
|
113
|
+
x: Math.round(rect.x),
|
|
114
|
+
y: Math.round(rect.y),
|
|
115
|
+
width: Math.round(rect.width),
|
|
116
|
+
height: Math.round(rect.height)
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
const base64Crop = croppedImage.toJPEG(70).toString('base64');
|
|
120
|
+
const translationResult = await translateImageContent(`data:image/jpeg;base64,${base64Crop}`);
|
|
121
|
+
if (translationResult.retryableFailure) {
|
|
122
|
+
translateCooldownUntil = Date.now() + TRANSLATE_FAILURE_COOLDOWN_MS;
|
|
123
|
+
console.warn(`Live translation cooldown active for ${TRANSLATE_FAILURE_COOLDOWN_MS / 1000}s after retryable API failure.`);
|
|
124
|
+
} else {
|
|
125
|
+
translateCooldownUntil = 0;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
if (screenPickerWindow && !screenPickerWindow.isDestroyed()) {
|
|
129
|
+
screenPickerWindow.webContents.send('vision-translate-result', translationResult.text);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
} catch (err) {
|
|
133
|
+
console.error("Continuous translation loop failed:", err);
|
|
134
|
+
} finally {
|
|
135
|
+
isTranslateRequestInFlight = false;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function cancel() {
|
|
140
|
+
stopLiveTranslate(false);
|
|
141
|
+
if (screenPickerWindow) screenPickerWindow.close();
|
|
142
|
+
|
|
143
|
+
const mainWindow = getMainWindow();
|
|
144
|
+
if (mainWindow) mainWindow.show();
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
async function captureSilentScreen() {
|
|
148
|
+
try {
|
|
149
|
+
const primaryDisplay = screen.getPrimaryDisplay();
|
|
150
|
+
const { width, height } = primaryDisplay.size;
|
|
151
|
+
const sources = await desktopCapturer.getSources({
|
|
152
|
+
types: ['screen'],
|
|
153
|
+
thumbnailSize: { width, height }
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
const primarySource = sources[0];
|
|
157
|
+
return primarySource && primarySource.thumbnail ? primarySource.thumbnail.toDataURL() : null;
|
|
158
|
+
} catch (err) {
|
|
159
|
+
console.error("Error silently capturing screen:", err);
|
|
160
|
+
return null;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
return {
|
|
165
|
+
startScreenCapture,
|
|
166
|
+
handleSelection,
|
|
167
|
+
startLiveTranslate,
|
|
168
|
+
stopLiveTranslate,
|
|
169
|
+
setOverlayInteractable,
|
|
170
|
+
cancel,
|
|
171
|
+
captureSilentScreen
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
module.exports = { createScreenCaptureController };
|