@pheem49/mint 1.5.0 → 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.
Files changed (78) hide show
  1. package/README.md +27 -1
  2. package/main.js +28 -14
  3. package/mint-cli-logic.js +3 -119
  4. package/mint-cli.js +497 -23
  5. package/models/Shiroko_Model/Shiroko/Shiroko_Core/72d86db84cfa9730b894c241fd24c0db.png +0 -0
  6. package/models/Shiroko_Model/Shiroko/Shiroko_Core/items_pinned_to_model.json +14 -0
  7. package/models/Shiroko_Model/Shiroko/Shiroko_Core//345/221/206/347/214/253.exp3.json +10 -0
  8. 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
  9. package/models/Shiroko_Model/Shiroko/Shiroko_Core//345/233/264/350/243/231.exp3.json +10 -0
  10. package/models/Shiroko_Model/Shiroko/Shiroko_Core//346/213/215/347/205/247.exp3.json +50 -0
  11. package/models/Shiroko_Model/Shiroko/Shiroko_Core//346/213/277/347/254/224.exp3.json +10 -0
  12. package/models/Shiroko_Model/Shiroko/Shiroko_Core//347/202/271/344/270/200/344/270/213.exp3.json +10 -0
  13. package/models/Shiroko_Model/Shiroko/Shiroko_Core//347/214/253/345/222/252/346/273/244/351/225/234.exp3.json +10 -0
  14. package/models/Shiroko_Model/Shiroko/Shiroko_Core//347/234/274/351/225/234.exp3.json +10 -0
  15. package/models/Shiroko_Model/Shiroko/Shiroko_Core//351/235/242/351/245/2740.4096/texture_00.png +0 -0
  16. package/models/Shiroko_Model/Shiroko/Shiroko_Core//351/235/242/351/245/2740.4096/texture_01.png +0 -0
  17. package/models/Shiroko_Model/Shiroko/Shiroko_Core//351/235/242/351/245/2740.4096/texture_02.png +0 -0
  18. package/models/Shiroko_Model/Shiroko/Shiroko_Core//351/235/242/351/245/2740.4096/texture_03.png +0 -0
  19. package/models/Shiroko_Model/Shiroko/Shiroko_Core//351/235/242/351/245/2740.cdi3.json +1498 -0
  20. package/models/Shiroko_Model/Shiroko/Shiroko_Core//351/235/242/351/245/2740.moc3 +0 -0
  21. package/models/Shiroko_Model/Shiroko/Shiroko_Core//351/235/242/351/245/2740.model3.json +47 -0
  22. package/models/Shiroko_Model/Shiroko/Shiroko_Core//351/235/242/351/245/2740.physics3.json +6658 -0
  23. package/models/Shiroko_Model/Shiroko/Shiroko_Core//351/235/242/351/245/2740.vtube.json +1299 -0
  24. 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
  25. package/package.json +26 -1
  26. package/src/AI_Brain/Gemini_API.js +147 -46
  27. package/src/AI_Brain/autonomous_brain.js +2 -1
  28. package/src/AI_Brain/memory_store.js +299 -3
  29. package/src/CLI/chat_router.js +18 -6
  30. package/src/CLI/chat_ui.js +396 -50
  31. package/src/CLI/code_agent.js +203 -14
  32. package/src/CLI/image_input.js +90 -0
  33. package/src/CLI/onboarding.js +72 -15
  34. package/src/CLI/updater.js +6 -4
  35. package/src/System/action_executor.js +59 -10
  36. package/src/System/config_manager.js +31 -1
  37. package/src/System/granular_automation.js +122 -53
  38. package/src/System/proactive_loop.js +19 -3
  39. package/src/System/safety_manager.js +108 -0
  40. package/src/System/sandbox_runner.js +182 -0
  41. package/src/System/system_automation.js +127 -81
  42. package/src/System/system_info.js +70 -0
  43. package/src/System/tool_registry.js +280 -0
  44. package/src/System/window_manager.js +4 -2
  45. package/src/UI/live2d_manager.js +368 -0
  46. package/src/UI/renderer.js +176 -18
  47. package/src/UI/styles.css +452 -31
  48. package/.codex +0 -0
  49. package/docs/assets/Agent_Mint.png +0 -0
  50. package/docs/assets/CLI_Screen.png +0 -0
  51. package/docs/assets/Settings.png +0 -0
  52. package/docs/assets/icon.png +0 -0
  53. package/docs/guide.html +0 -632
  54. package/docs/index.html +0 -133
  55. package/docs/style.css +0 -579
  56. package/index.html +0 -16
  57. package/src/UI/index.html +0 -126
  58. package/tech_news.txt +0 -3
  59. package/test_knowledge.txt +0 -3
  60. package/tests/action_executor_safety.test.js +0 -67
  61. package/tests/agent_orchestrator.test.js +0 -41
  62. package/tests/chat_router.test.js +0 -42
  63. package/tests/code_agent.test.js +0 -69
  64. package/tests/config_manager.test.js +0 -141
  65. package/tests/docker.test.js +0 -46
  66. package/tests/file_operations.test.js +0 -57
  67. package/tests/gmail.test.js +0 -135
  68. package/tests/gmail_auth.test.js +0 -129
  69. package/tests/google_calendar.test.js +0 -113
  70. package/tests/google_tts_urls.test.js +0 -24
  71. package/tests/memory_store.test.js +0 -185
  72. package/tests/notion.test.js +0 -121
  73. package/tests/provider_routing.test.js +0 -83
  74. package/tests/safety_manager.test.js +0 -40
  75. package/tests/spotify.test.js +0 -201
  76. package/tests/system_monitor.test.js +0 -37
  77. package/tests/updater.test.js +0 -32
  78. package/tests/workspace_manager.test.js +0 -56
@@ -1,19 +1,33 @@
1
- const { clipboard: electronClipboard } = require('electron');
1
+ let electronClipboard = null;
2
+ try {
3
+ ({ clipboard: electronClipboard } = require('electron'));
4
+ } catch (_) {
5
+ electronClipboard = {
6
+ writeText: () => {}
7
+ };
8
+ }
2
9
  const { openApp } = require('../Automation_Layer/open_app');
3
10
  const { openWebsite, openSearch } = require('../Automation_Layer/open_website');
4
11
  const { performWebAutomation } = require('../Automation_Layer/browser_automation');
5
12
  const { createFolder, openFile, deleteFile, findPath } = require('../Automation_Layer/file_operations');
6
13
  const { indexFile, indexFolder } = require('../AI_Brain/knowledge_base');
14
+ const { getSystemInfo, getWeather } = require('./system_info');
7
15
  const pluginManager = require('../Plugins/plugin_manager');
8
16
  const mcpManager = require('../Plugins/mcp_manager');
9
- const granularAutomation = require('./granular_automation');
10
17
  const SystemAutomation = require('./system_automation');
11
18
  const safetyManager = require('./safety_manager');
19
+ const toolRegistry = require('./tool_registry');
20
+ const os = require('os');
21
+ const path = require('path');
12
22
 
13
23
  async function executeAction(action, options = {}) {
14
- console.log("Executing action:", action);
24
+ if (process.env.MINT_DEBUG === '1') {
25
+ console.log("Executing action:", action);
26
+ }
27
+ toolRegistry.validateToolInput(action.type, action);
15
28
  const clipboard = options.clipboard || electronClipboard;
16
29
  const safety = safetyManager.assertActionAllowed(action, {
30
+ allowApproval: options.allowApproval === true,
17
31
  allowDangerous: options.allowDangerous === true
18
32
  });
19
33
  safetyManager.appendActionLog({
@@ -21,7 +35,7 @@ async function executeAction(action, options = {}) {
21
35
  action: action.type,
22
36
  target: action.target || action.path || '',
23
37
  tier: safety.tier,
24
- approved: options.allowDangerous === true || safety.tier !== safetyManager.TIERS.DANGEROUS
38
+ approved: options.allowApproval === true || options.allowDangerous === true || safety.tier === safetyManager.TIERS.SAFE
25
39
  });
26
40
 
27
41
  switch (action.type) {
@@ -37,17 +51,23 @@ async function executeAction(action, options = {}) {
37
51
  case 'web_automation':
38
52
  return await performWebAutomation(action.target);
39
53
  case 'create_folder':
54
+ safetyManager.assertPathCapability(action.target, 'write', {
55
+ defaultBase: path.join(os.homedir(), 'Desktop')
56
+ });
40
57
  createFolder(action.target);
41
58
  break;
42
59
  case 'open_file': {
60
+ safetyManager.assertPathCapability(action.target, 'read');
43
61
  const fileRes = await openFile(action.target);
44
62
  return fileRes || `Successfully opened file: ${action.target} ✅`;
45
63
  }
46
64
  case 'open_folder': {
65
+ safetyManager.assertPathCapability(action.target, 'read');
47
66
  const folderRes = await openFile(action.target);
48
67
  return folderRes || `Successfully opened folder: ${action.target} ✅`;
49
68
  }
50
69
  case 'delete_file':
70
+ safetyManager.assertPathCapability(action.target, 'write');
51
71
  await deleteFile(action.target);
52
72
  break;
53
73
  case 'find_path':
@@ -56,21 +76,33 @@ async function executeAction(action, options = {}) {
56
76
  clipboard.writeText(action.target);
57
77
  break;
58
78
  case 'learn_file':
79
+ safetyManager.assertPathCapability(action.target, 'read');
59
80
  return await indexFile(action.target);
60
81
  case 'learn_folder':
82
+ safetyManager.assertPathCapability(action.target, 'read');
61
83
  return await indexFolder(action.target);
84
+ case 'system_info':
85
+ return await handleSystemInfo(action.target);
62
86
  case 'mcp_tool': {
63
87
  const mcpResult = await mcpManager.callTool(action.server, action.target, action.args);
64
88
  return JSON.stringify(mcpResult.content);
65
89
  }
66
- case 'mouse_move':
90
+ case 'mouse_move': {
91
+ const granularAutomation = require('./granular_automation');
67
92
  return await granularAutomation.mouseMove(action.x, action.y);
68
- case 'mouse_click':
93
+ }
94
+ case 'mouse_click': {
95
+ const granularAutomation = require('./granular_automation');
69
96
  return await granularAutomation.mouseClick(action.x, action.y, action.button || 1);
70
- case 'type_text':
97
+ }
98
+ case 'type_text': {
99
+ const granularAutomation = require('./granular_automation');
71
100
  return await granularAutomation.typeText(action.target);
72
- case 'key_tap':
101
+ }
102
+ case 'key_tap': {
103
+ const granularAutomation = require('./granular_automation');
73
104
  return await granularAutomation.keyTap(action.target);
105
+ }
74
106
  case 'plugin':
75
107
  return await pluginManager.executePlugin(action.pluginName, action.target);
76
108
  case 'system_automation':
@@ -80,10 +112,27 @@ async function executeAction(action, options = {}) {
80
112
  }
81
113
  }
82
114
 
115
+ async function handleSystemInfo(target = '') {
116
+ const query = String(target || '').trim();
117
+ if (query) {
118
+ const weather = await getWeather(query);
119
+ return JSON.stringify({
120
+ type: 'weather',
121
+ target: query,
122
+ ...weather
123
+ });
124
+ }
125
+ return JSON.stringify({
126
+ type: 'system_info',
127
+ data: getSystemInfo()
128
+ });
129
+ }
130
+
83
131
  async function executeFindPath(action) {
84
132
  const result = findPath(action.target, {
85
133
  type: action.pathType,
86
- maxResults: 10
134
+ maxResults: 10,
135
+ roots: safetyManager.getAllowedRoots('read')
87
136
  });
88
137
  if (!result.success) {
89
138
  return result.message;
@@ -126,4 +175,4 @@ async function handleSystemAutomation(target) {
126
175
  }
127
176
  }
128
177
 
129
- module.exports = { executeAction, handleSystemAutomation };
178
+ module.exports = { executeAction, handleSystemAutomation, handleSystemInfo };
@@ -99,7 +99,37 @@ const DEFAULT_CONFIG = {
99
99
  enableAgentCollaboration: false,
100
100
  enableAutoUpdate: true,
101
101
  autoUpdateCheckIntervalHours: 24,
102
- lastUpdateCheckAt: ''
102
+ lastUpdateCheckAt: '',
103
+ safetyEnabled: true,
104
+ sandboxMode: 'prefer', // off | prefer | enforce
105
+ sandboxCommand: process.platform === 'darwin' ? 'sandbox-exec' : process.platform === 'linux' ? 'bwrap' : '',
106
+ allowedReadPaths: [
107
+ os.homedir(),
108
+ process.cwd(),
109
+ path.join(os.homedir(), 'Desktop'),
110
+ path.join(os.homedir(), 'Documents'),
111
+ path.join(os.homedir(), 'Downloads'),
112
+ path.join(os.homedir(), 'Pictures'),
113
+ path.join(os.homedir(), 'Music'),
114
+ path.join(os.homedir(), 'Videos')
115
+ ],
116
+ allowedWritePaths: [
117
+ os.homedir(),
118
+ process.cwd(),
119
+ path.join(os.homedir(), 'Desktop'),
120
+ path.join(os.homedir(), 'Documents'),
121
+ path.join(os.homedir(), 'Downloads'),
122
+ path.join(os.homedir(), 'Pictures'),
123
+ path.join(os.homedir(), 'Music'),
124
+ path.join(os.homedir(), 'Videos')
125
+ ],
126
+ blockedPaths: [
127
+ path.join(os.homedir(), '.ssh'),
128
+ path.join(os.homedir(), '.gnupg'),
129
+ path.join(os.homedir(), '.config', 'mint', 'mint-config.json'),
130
+ path.join(os.homedir(), '.mint', 'mint-config.json')
131
+ ],
132
+ blockedFileNames: ['.env', 'id_rsa', 'id_ed25519']
103
133
  };
104
134
 
105
135
 
@@ -1,88 +1,157 @@
1
- const { exec } = require('child_process');
1
+ const { execFile, spawnSync } = require('child_process');
2
2
  const { screen } = require('electron');
3
3
 
4
- /**
5
- * GranularAutomation handles low-level OS input via xdotool.
6
- * It uses a normalized coordinate system (0-1000).
7
- */
4
+ function commandExists(command) {
5
+ const lookup = process.platform === 'win32' ? 'where' : 'which';
6
+ const result = spawnSync(lookup, [command], { encoding: 'utf8', shell: false });
7
+ return result.status === 0;
8
+ }
9
+
10
+ function run(command, args = []) {
11
+ return new Promise((resolve, reject) => {
12
+ execFile(command, args, (err, stdout, stderr) => {
13
+ if (err) {
14
+ err.stderr = stderr;
15
+ reject(err);
16
+ return;
17
+ }
18
+ resolve(stdout);
19
+ });
20
+ });
21
+ }
22
+
23
+ function unsupported(feature) {
24
+ throw new Error(`${feature} is not supported on ${process.platform} by the current input automation provider.`);
25
+ }
26
+
27
+ function escapePowerShellSingleQuoted(value) {
28
+ return String(value || '').replace(/'/g, "''");
29
+ }
30
+
31
+ function keyToMacKey(key) {
32
+ const value = String(key || '').trim();
33
+ const map = {
34
+ Enter: 'return',
35
+ Return: 'return',
36
+ Escape: 'escape',
37
+ Esc: 'escape',
38
+ Space: 'space',
39
+ Backspace: 'delete',
40
+ Delete: 'forward delete',
41
+ Tab: 'tab'
42
+ };
43
+ return map[value] || value;
44
+ }
45
+
8
46
  class GranularAutomation {
9
47
  constructor() {
10
- this.screenWidth = 1920; // Default fallback
48
+ this.screenWidth = 1920;
11
49
  this.screenHeight = 1080;
12
50
  this.updateScreenSize();
13
51
  }
14
52
 
15
53
  updateScreenSize() {
16
54
  try {
17
- // In Electron main process, we can use the screen module
18
55
  const primaryDisplay = screen.getPrimaryDisplay();
19
56
  if (primaryDisplay && primaryDisplay.size) {
20
57
  this.screenWidth = primaryDisplay.size.width;
21
58
  this.screenHeight = primaryDisplay.size.height;
22
- console.log(`[Automation] Screen detected: ${this.screenWidth}x${this.screenHeight}`);
23
59
  }
24
- } catch (e) {
25
- // Fallback for CLI or cases where screen module is unavailable
26
- exec('xdpyinfo | grep dimensions', (err, stdout) => {
27
- if (!err && stdout) {
28
- const match = stdout.match(/(\d+)x(\d+) pixels/);
29
- if (match) {
30
- this.screenWidth = parseInt(match[1]);
31
- this.screenHeight = parseInt(match[2]);
32
- console.log(`[Automation] Screen detected via xdpyinfo: ${this.screenWidth}x${this.screenHeight}`);
33
- }
34
- }
35
- });
60
+ } catch (_) {
61
+ // Electron screen can be unavailable in CLI-only contexts.
36
62
  }
37
63
  }
38
64
 
39
65
  scaleX(x) {
40
- return Math.round((x / 1000) * this.screenWidth);
66
+ return Math.round((Number(x) / 1000) * this.screenWidth);
41
67
  }
42
68
 
43
69
  scaleY(y) {
44
- return Math.round((y / 1000) * this.screenHeight);
70
+ return Math.round((Number(y) / 1000) * this.screenHeight);
45
71
  }
46
72
 
47
- run(command) {
48
- return new Promise((resolve, reject) => {
49
- exec(command, (err, stdout, stderr) => {
50
- if (err) {
51
- console.error(`[Automation] xdotool error: ${stderr}`);
52
- reject(err);
53
- } else {
54
- resolve(stdout);
55
- }
56
- });
57
- });
73
+ provider() {
74
+ if (process.platform === 'darwin') return macProvider;
75
+ if (process.platform === 'win32') return windowsProvider;
76
+ return linuxProvider;
58
77
  }
59
78
 
60
- async mouseMove(x, y) {
61
- const sx = this.scaleX(x);
62
- const sy = this.scaleY(y);
63
- console.log(`[Automation] Moving mouse to ${sx}, ${sy}`);
64
- return this.run(`xdotool mousemove ${sx} ${sy}`);
79
+ mouseMove(x, y) {
80
+ return this.provider().mouseMove(this.scaleX(x), this.scaleY(y));
65
81
  }
66
82
 
67
- async mouseClick(x, y, button = 1) {
68
- const sx = this.scaleX(x);
69
- const sy = this.scaleY(y);
70
- console.log(`[Automation] Clicking ${button} at ${sx}, ${sy}`);
71
- // move first then click to be safe
72
- return this.run(`xdotool mousemove ${sx} ${sy} click ${button}`);
83
+ mouseClick(x, y, button = 1) {
84
+ return this.provider().mouseClick(this.scaleX(x), this.scaleY(y), button);
73
85
  }
74
86
 
75
- async typeText(text) {
76
- console.log(`[Automation] Typing: ${text}`);
77
- // Escape double quotes for shell
78
- const escaped = text.replace(/"/g, '\\"');
79
- return this.run(`xdotool type "${escaped}"`);
87
+ typeText(text) {
88
+ return this.provider().typeText(String(text || ''));
80
89
  }
81
90
 
82
- async keyTap(key) {
83
- console.log(`[Automation] Key tap: ${key}`);
84
- return this.run(`xdotool key "${key}"`);
91
+ keyTap(key) {
92
+ return this.provider().keyTap(String(key || ''));
85
93
  }
86
94
  }
87
95
 
88
- module.exports = new GranularAutomation();
96
+ const linuxProvider = {
97
+ mouseMove: (x, y) => run('xdotool', ['mousemove', String(x), String(y)]),
98
+ mouseClick: (x, y, button = 1) => run('xdotool', ['mousemove', String(x), String(y), 'click', String(button)]),
99
+ typeText: (text) => run('xdotool', ['type', text]),
100
+ keyTap: (key) => run('xdotool', ['key', key])
101
+ };
102
+
103
+ const macProvider = {
104
+ mouseMove(x, y) {
105
+ if (!commandExists('cliclick')) return unsupported('Mouse move');
106
+ return run('cliclick', [`m:${x},${y}`]);
107
+ },
108
+ mouseClick(x, y) {
109
+ if (!commandExists('cliclick')) return unsupported('Mouse click');
110
+ return run('cliclick', [`c:${x},${y}`]);
111
+ },
112
+ typeText(text) {
113
+ if (commandExists('cliclick')) return run('cliclick', [`t:${text}`]);
114
+ return run('osascript', ['-e', `tell application "System Events" to keystroke ${JSON.stringify(text)}`]);
115
+ },
116
+ keyTap(key) {
117
+ const macKey = keyToMacKey(key);
118
+ if (commandExists('cliclick')) return run('cliclick', [`kp:${macKey}`]);
119
+ if (macKey.length === 1) {
120
+ return run('osascript', ['-e', `tell application "System Events" to keystroke ${JSON.stringify(macKey)}`]);
121
+ }
122
+ return unsupported('Special key tap without cliclick');
123
+ }
124
+ };
125
+
126
+ const windowsProvider = {
127
+ mouseMove(x, y) {
128
+ const script = `[void][Reflection.Assembly]::LoadWithPartialName('System.Windows.Forms'); [System.Windows.Forms.Cursor]::Position = New-Object System.Drawing.Point(${x}, ${y})`;
129
+ return run('powershell.exe', ['-NoProfile', '-NonInteractive', '-Command', script]);
130
+ },
131
+ mouseClick(x, y, button = 1) {
132
+ const down = Number(button) === 2 ? '0x0008' : '0x0002';
133
+ const up = Number(button) === 2 ? '0x0010' : '0x0004';
134
+ const script = [
135
+ "Add-Type -MemberDefinition '[DllImport(\"user32.dll\")] public static extern bool SetCursorPos(int X,int Y); [DllImport(\"user32.dll\")] public static extern void mouse_event(int dwFlags,int dx,int dy,int dwData,int dwExtraInfo);' -Name NativeMouse -Namespace Mint;",
136
+ `[Mint.NativeMouse]::SetCursorPos(${x}, ${y}) | Out-Null;`,
137
+ `[Mint.NativeMouse]::mouse_event(${down},0,0,0,0);`,
138
+ `[Mint.NativeMouse]::mouse_event(${up},0,0,0,0);`
139
+ ].join(' ');
140
+ return run('powershell.exe', ['-NoProfile', '-NonInteractive', '-Command', script]);
141
+ },
142
+ typeText(text) {
143
+ const safe = escapePowerShellSingleQuoted(text);
144
+ const script = `Add-Type -AssemblyName System.Windows.Forms; [System.Windows.Forms.SendKeys]::SendWait('${safe}')`;
145
+ return run('powershell.exe', ['-NoProfile', '-NonInteractive', '-Command', script]);
146
+ },
147
+ keyTap(key) {
148
+ const safe = escapePowerShellSingleQuoted(key);
149
+ const script = `Add-Type -AssemblyName System.Windows.Forms; [System.Windows.Forms.SendKeys]::SendWait('{${safe}}')`;
150
+ return run('powershell.exe', ['-NoProfile', '-NonInteractive', '-Command', script]);
151
+ }
152
+ };
153
+
154
+ const instance = new GranularAutomation();
155
+ instance._providers = { linuxProvider, macProvider, windowsProvider, commandExists };
156
+
157
+ module.exports = instance;
@@ -1,10 +1,24 @@
1
1
  const { BrowserWindow, desktopCapturer, screen, powerMonitor } = require('electron');
2
2
  const path = require('path');
3
- const { analyzeAndSuggest } = require('../AI_Brain/proactive_engine');
4
- const { recordBehavior, getBehaviorSummary } = require('../AI_Brain/behavior_memory');
5
3
 
6
4
  const IDLE_THRESHOLD_SEC = 300;
7
5
 
6
+ let proactiveEngine = null;
7
+ function getProactiveEngine() {
8
+ if (!proactiveEngine) {
9
+ proactiveEngine = require('../AI_Brain/proactive_engine');
10
+ }
11
+ return proactiveEngine;
12
+ }
13
+
14
+ let behaviorMemory = null;
15
+ function getBehaviorMemory() {
16
+ if (!behaviorMemory) {
17
+ behaviorMemory = require('../AI_Brain/behavior_memory');
18
+ }
19
+ return behaviorMemory;
20
+ }
21
+
8
22
  function createProactiveLoop({ app, projectRoot, readConfig, getMainWindow }) {
9
23
  let proactiveGlowWindow = null;
10
24
  let proactiveIntervalHandle = null;
@@ -29,6 +43,8 @@ function createProactiveLoop({ app, projectRoot, readConfig, getMainWindow }) {
29
43
  if (!primarySource || !primarySource.thumbnail) return;
30
44
 
31
45
  const base64Image = primarySource.thumbnail.toJPEG(60).toString('base64');
46
+ const { analyzeAndSuggest } = getProactiveEngine();
47
+ const { recordBehavior, getBehaviorSummary } = getBehaviorMemory();
32
48
  const result = await analyzeAndSuggest(base64Image, getBehaviorSummary());
33
49
 
34
50
  if (result && result.message && Array.isArray(result.suggestions)) {
@@ -130,7 +146,7 @@ function createProactiveLoop({ app, projectRoot, readConfig, getMainWindow }) {
130
146
  stop,
131
147
  startIdleWatcher,
132
148
  isRunning: () => Boolean(proactiveIntervalHandle),
133
- recordBehavior
149
+ recordBehavior: (...args) => getBehaviorMemory().recordBehavior(...args)
134
150
  };
135
151
  }
136
152
 
@@ -1,6 +1,7 @@
1
1
  const fs = require('fs');
2
2
  const os = require('os');
3
3
  const path = require('path');
4
+ const { readConfig } = require('./config_manager');
4
5
 
5
6
  const TIERS = Object.freeze({
6
7
  SAFE: 'safe',
@@ -40,6 +41,7 @@ const SAFE_ACTIONS = new Set([
40
41
  'clipboard_write',
41
42
  'learn_file',
42
43
  'learn_folder',
44
+ 'system_info',
43
45
  'mcp_tool',
44
46
  'mouse_move',
45
47
  'mouse_click',
@@ -56,6 +58,103 @@ function normalizeCommand(command) {
56
58
  return String(command || '').replace(/\s+/g, ' ').trim();
57
59
  }
58
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
+
59
158
  function classifyShellCommand(command) {
60
159
  const normalized = normalizeCommand(command);
61
160
  if (!normalized) {
@@ -105,6 +204,7 @@ function classifyAction(action = {}) {
105
204
  function assertActionAllowed(action, options = {}) {
106
205
  const classification = classifyAction(action);
107
206
  const allowDangerous = options.allowDangerous === true;
207
+ const allowApproval = options.allowApproval === true;
108
208
 
109
209
  if (classification.tier === TIERS.BLOCKED) {
110
210
  throw new Error(`Blocked action (${classification.reason}): ${action.type}`);
@@ -114,6 +214,10 @@ function assertActionAllowed(action, options = {}) {
114
214
  throw new Error(`Dangerous action requires explicit permission (${classification.reason}): ${action.type}`);
115
215
  }
116
216
 
217
+ if (classification.tier === TIERS.APPROVAL && !allowApproval) {
218
+ throw new Error(`Action requires approval (${classification.reason}): ${action.type}`);
219
+ }
220
+
117
221
  return classification;
118
222
  }
119
223
 
@@ -156,10 +260,14 @@ function appendActionLog(entry, options = {}) {
156
260
 
157
261
  module.exports = {
158
262
  TIERS,
263
+ getPolicy,
264
+ getAllowedRoots,
159
265
  classifyShellCommand,
160
266
  assertShellCommandAllowed,
161
267
  classifyAction,
162
268
  assertActionAllowed,
269
+ assertPathCapability,
270
+ resolveCapabilityPath,
163
271
  resolveWithinRoot,
164
272
  appendActionLog
165
273
  };