@pheem49/mint 1.5.3 → 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 CHANGED
@@ -6,15 +6,27 @@
6
6
 
7
7
  ## 1. การติดตั้ง (Installation)
8
8
 
9
- คุณสามารถติดตั้ง Mint ได้สองวิธีหลักๆ ดังนี้:
9
+ คุณสามารถติดตั้ง Mint ได้ด้วยวิธีต่างๆ ดังนี้:
10
10
 
11
- ### วิธีที่ 1: ติดตั้งผ่าน NPM (แนะนำสำหรับผู้ใช้ทั่วไป)
12
- หากคุณต้องการใช้งานคำสั่ง `mint` ได้จากทุกที่ในเครื่อง:
11
+ ### วิธีที่ 1: ติดตั้งแบบด่วน (แนะนำ)
12
+ วิธีนี้จะติดตั้ง Mint CLI ให้อัตโนมัติในคำสั่งเดียว:
13
+ **สำหรับ macOS และ Linux:**
14
+ ```bash
15
+ curl -fsSL https://raw.githubusercontent.com/Pheem49/Mint/main/install.sh | bash
16
+ ```
17
+
18
+ **สำหรับ Windows (PowerShell หรือ CMD):**
19
+ ```powershell
20
+ powershell -Command "iwr -useb https://raw.githubusercontent.com/Pheem49/Mint/main/install.ps1 | iex"
21
+ ```
22
+
23
+ ### วิธีที่ 2: ติดตั้งผ่าน NPM
24
+ หากคุณต้องการใช้งานคำสั่ง `mint` และถนัดการใช้ NPM:
13
25
  ```bash
14
26
  npm install -g @pheem49/mint@latest
15
27
  ```
16
28
 
17
- ### วิธีที่ 2: ติดตั้งจาก Source Code (สำหรับนักพัฒนา)
29
+ ### วิธีที่ 3: ติดตั้งจาก Source Code (สำหรับนักพัฒนา)
18
30
  หากคุณต้องการแก้ไขโค้ดหรือลองฟีเจอร์ใหม่ๆ:
19
31
  1. Clone repository: `git clone https://github.com/Pheem49/Mint.git`
20
32
  2. เข้าไปที่โฟลเดอร์: `cd Mint`
package/README.md CHANGED
@@ -39,7 +39,23 @@ Mint is an AI assistant built to live in your desktop and terminal. It combines
39
39
 
40
40
  ## Installation & Setup
41
41
 
42
- ### Global Install
42
+ ### Quick Install (Recommended)
43
+
44
+ The easiest way to install Mint CLI is using our installation script:
45
+
46
+ **For macOS & Linux:**
47
+ ```bash
48
+ curl -fsSL https://raw.githubusercontent.com/Pheem49/Mint/main/install.sh | bash
49
+ ```
50
+
51
+ **For Windows (PowerShell or CMD):**
52
+ ```powershell
53
+ powershell -Command "iwr -useb https://raw.githubusercontent.com/Pheem49/Mint/main/install.ps1 | iex"
54
+ ```
55
+
56
+ ### NPM Install
57
+
58
+ If you prefer to install via NPM directly:
43
59
 
44
60
  ```bash
45
61
  npm install -g @pheem49/mint@latest
package/install.ps1 ADDED
@@ -0,0 +1,64 @@
1
+ <#
2
+ .SYNOPSIS
3
+ Mint CLI Installation Script for Windows
4
+ .DESCRIPTION
5
+ Installs the Mint CLI package globally via NPM.
6
+ #>
7
+
8
+ $ErrorActionPreference = "Stop"
9
+
10
+ Write-Host "Starting Mint CLI installation..." -ForegroundColor Cyan
11
+
12
+ # 1. Check for Node.js
13
+ try {
14
+ $nodeVersion = (node -v) 2>$null
15
+ if ([string]::IsNullOrWhiteSpace($nodeVersion)) {
16
+ Write-Host "[Error] Node.js is not found on your system!" -ForegroundColor Red
17
+ Write-Host "Please install Node.js (version 18 or higher) before using Mint CLI." -ForegroundColor Yellow
18
+ Write-Host "Download at: https://nodejs.org/"
19
+ exit 1
20
+ }
21
+ } catch {
22
+ Write-Host "[Error] Node.js is not found on your system!" -ForegroundColor Red
23
+ Write-Host "Please install Node.js (version 18 or higher) before using Mint CLI." -ForegroundColor Yellow
24
+ Write-Host "Download at: https://nodejs.org/"
25
+ exit 1
26
+ }
27
+
28
+ # 2. Check for NPM
29
+ try {
30
+ $npmVersion = (npm -v) 2>$null
31
+ if ([string]::IsNullOrWhiteSpace($npmVersion)) {
32
+ Write-Host "[Error] NPM is not found on your system!" -ForegroundColor Red
33
+ exit 1
34
+ }
35
+ } catch {
36
+ Write-Host "[Error] NPM is not found on your system!" -ForegroundColor Red
37
+ exit 1
38
+ }
39
+
40
+ # Check Node.js version (recommend 18+)
41
+ $majorVersion = [int]($nodeVersion.Substring(1).Split('.')[0])
42
+ if ($majorVersion -lt 18) {
43
+ Write-Host "[Warning] Your Node.js version is too old (current: $nodeVersion)" -ForegroundColor Yellow
44
+ Write-Host "It is recommended to update to version 18 or higher for full functionality." -ForegroundColor Yellow
45
+ }
46
+
47
+ # 3. Install Mint CLI via NPM
48
+ Write-Host "Downloading and installing @pheem49/mint..." -ForegroundColor Cyan
49
+
50
+ try {
51
+ # In Windows, we usually don't need sudo for global npm install if Node was installed for the user.
52
+ # We will just run the command normally.
53
+ npm install -g @pheem49/mint@latest
54
+ } catch {
55
+ Write-Host "[Error] Installation failed. Try running PowerShell as Administrator." -ForegroundColor Red
56
+ exit 1
57
+ }
58
+
59
+ Write-Host ""
60
+ Write-Host "Mint CLI installed successfully!" -ForegroundColor Green
61
+ Write-Host "- Run 'mint' to get started."
62
+ Write-Host "- Run 'mint onboard' to set up your API Keys."
63
+ Write-Host "----------------------------------------"
64
+ Write-Host "Happy coding!" -ForegroundColor Cyan
package/install.sh ADDED
@@ -0,0 +1,54 @@
1
+ #!/bin/bash
2
+ # Mint CLI Installation Script
3
+ # Usage: curl -fsSL <URL>/install.sh | bash
4
+
5
+ set -e
6
+
7
+ echo "Starting Mint CLI installation..."
8
+
9
+ # 1. Check for Node.js
10
+ if ! command -v node &> /dev/null
11
+ then
12
+ echo "[Error] Node.js is not found on your system!"
13
+ echo "Please install Node.js (version 18 or higher) before using Mint CLI."
14
+ echo "Download at: https://nodejs.org/"
15
+ exit 1
16
+ fi
17
+
18
+ # 2. Check for NPM
19
+ if ! command -v npm &> /dev/null
20
+ then
21
+ echo "[Error] NPM is not found on your system!"
22
+ exit 1
23
+ fi
24
+
25
+ # Check Node.js version (recommend 18+)
26
+ NODE_VERSION=$(node -v | cut -d 'v' -f 2 | cut -d '.' -f 1)
27
+ if [ "$NODE_VERSION" -lt 18 ]; then
28
+ echo "[Warning] Your Node.js version is too old (current: v${NODE_VERSION})"
29
+ echo "It is recommended to update to version 18 or higher for full functionality."
30
+ fi
31
+
32
+ # 3. Install Mint CLI via NPM
33
+ echo "Downloading and installing @pheem49/mint (may require Admin/Root privileges)..."
34
+
35
+ # Determine if we need sudo
36
+ if [ "$EUID" -ne 0 ] && command -v sudo &> /dev/null; then
37
+ # Check if npm global directory is writable by the current user (e.g., using NVM)
38
+ NPM_PREFIX=$(npm config get prefix 2>/dev/null || echo "/usr/local")
39
+ if [ -w "$NPM_PREFIX" ] || [ -w "$NPM_PREFIX/lib/node_modules" ]; then
40
+ npm install -g @pheem49/mint@latest
41
+ else
42
+ # Preserve user's PATH so sudo can find npm if it's installed via custom paths
43
+ sudo env "PATH=$PATH" npm install -g @pheem49/mint@latest
44
+ fi
45
+ else
46
+ npm install -g @pheem49/mint@latest
47
+ fi
48
+
49
+ echo ""
50
+ echo "Mint CLI installed successfully!"
51
+ echo "- Run 'mint' to get started."
52
+ echo "- Run 'mint onboard' to set up your API Keys."
53
+ echo "----------------------------------------"
54
+ echo "Happy coding!"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pheem49/mint",
3
- "version": "1.5.3",
3
+ "version": "1.5.4",
4
4
  "description": "A powerful Electron-based AI desktop assistant powered by Google Gemini, featuring screen vision, web automation, and proactive suggestions.",
5
5
  "main": "main.js",
6
6
  "scripts": {
@@ -46,7 +46,8 @@
46
46
  "ink-text-input": "^6.0.0",
47
47
  "mammoth": "^1.12.0",
48
48
  "pdf-parse": "^2.4.5",
49
- "react": "^19.2.5"
49
+ "react": "^19.2.5",
50
+ "read-excel-file": "^9.0.10"
50
51
  },
51
52
  "peerDependenciesOptional": {
52
53
  "puppeteer": ">=22.0.0",
package/preload.js CHANGED
@@ -25,12 +25,14 @@ contextBridge.exposeInMainWorld('api', {
25
25
  startVision: () => ipcRenderer.invoke('start-screen-capture'),
26
26
  onVisionReady: (callback) => ipcRenderer.on('vision-ready', (event, data) => callback(data)),
27
27
  captureSilentScreen: () => ipcRenderer.invoke('capture-silent-screen'),
28
+ getSmartContext: () => ipcRenderer.invoke('get-smart-context'),
28
29
  // Proactive Assistant
29
30
  onProactiveSuggestion: (callback) => ipcRenderer.on('proactive-suggestion', (event, data) => callback(data)),
30
31
  onProactiveNotification: (callback) => ipcRenderer.on('proactive-notification', (event, data) => callback(data)),
31
32
  toggleProactive: (isOn) => ipcRenderer.send('toggle-proactive', isOn),
32
33
  recordBehavior: (context) => ipcRenderer.send('record-behavior', context),
33
34
  executeProactiveAction: (action) => ipcRenderer.invoke('execute-proactive-action', action),
35
+ executeApprovedAction: (action) => ipcRenderer.invoke('execute-approved-action', action),
34
36
  onSpotlightToChat: (callback) => ipcRenderer.on('spotlight-to-chat', (_event, query) => callback(query)),
35
37
  notifyAiResponse: () => ipcRenderer.send('ai-notify'),
36
38
  clearAiNotifications: () => ipcRenderer.send('ai-notify-clear'),
@@ -47,6 +47,34 @@ function normalizeImageList(base64Image) {
47
47
  return Array.isArray(base64Image) ? base64Image.filter(Boolean) : [base64Image];
48
48
  }
49
49
 
50
+ const CHAT_MODE_ACTION_POLICY = `GOAL:
51
+ Your goal is to help the user with their queries. This Electron app is Chat Mode: use at most ONE simple action per user message, only when the latest message explicitly asks for that local action. If the user asks a question or asks you to provide text/commands, answer with action "none".
52
+
53
+ ACTION DISCIPLINE:
54
+ - Always return a single JSON object. Never return a JSON array or multiple actions.
55
+ - If the user asks "พิมพ์คำสั่งให้หน่อย", "บอกคำสั่ง", "ขอคำสั่ง", "what command", or "type the command", provide the command in "response" and set action "none". Do NOT use "type_text" or "key_tap".
56
+ - Use "type_text", "key_tap", "mouse_click", or "mouse_move" only when the user explicitly asks you to control the currently focused UI, not when they ask for a command to copy/type themselves.
57
+ - If the user asks to run terminal commands or code, Chat Mode should provide the command or tell them to use the Mint CLI agent. Do not type or press Enter on their behalf.
58
+ - Never say you opened, checked, inspected, or verified a file/folder unless the selected action actually does it and the app will execute that action.
59
+ - If the request needs workspace code inspection, edits, tests, or shell execution, tell the user to use the Mint CLI agent instead of pretending to inspect files.`;
60
+
61
+ const AGENT_MODE_ACTION_POLICY = `GOAL:
62
+ Your goal is to act as Mint's Desktop Agent Mode. You may use ONE concrete desktop action per response when it directly advances the user's latest request or a clear desktop task implied by Smart Context. Prefer useful action over explaining when the user asked Mint to do something.
63
+
64
+ ACTION DISCIPLINE:
65
+ - Always return a single JSON object. Never return a JSON array or multiple actions.
66
+ - Choose exactly one action when a desktop action is useful and the user's intent is clear; otherwise use action "none" and ask a concise follow-up.
67
+ - You may use safe desktop actions such as open_url, search, open_app, find_path, open_file, open_folder, create_folder, clipboard_write, learn_file, learn_folder, plugin, mcp_tool, web_automation, system_info, mouse_move, mouse_click, type_text, and key_tap when they match the request.
68
+ - Approval and dangerous actions are handled by Mint's UI. You may propose system_automation or delete_file only when the user clearly requested it; the app will ask for permission before running.
69
+ - For UI-control actions (mouse_click, mouse_move, type_text, key_tap), rely on Smart Context or the attached screenshot. If the target is ambiguous, ask before acting.
70
+ - If the user asks "พิมพ์คำสั่งให้หน่อย", "บอกคำสั่ง", "ขอคำสั่ง", "what command", or "type the command", provide the command in "response" and set action "none" unless they explicitly ask Mint to type it into the active UI.
71
+ - If the request needs workspace code inspection, edits, tests, or shell execution, tell the user to use the Mint CLI agent instead of pretending to inspect files or run commands from Chat UI.
72
+ - Never say you opened, checked, inspected, or verified something unless the selected action actually does it and the app will execute that action.`;
73
+
74
+ function buildActionModeInstruction(config = readConfig()) {
75
+ return config.assistantMode === 'agent' ? AGENT_MODE_ACTION_POLICY : CHAT_MODE_ACTION_POLICY;
76
+ }
77
+
50
78
  const systemInstruction = `You are "Mint" (มิ้นท์), a cute, cheerful, and highly helpful female Local AI Desktop Agent.
51
79
 
52
80
  PERSONALITY & TONE:
@@ -71,16 +99,7 @@ NATURAL CHAT FLOW:
71
99
  - The latest user message is authoritative. Do not continue or describe older tasks unless the latest message explicitly asks you to continue them.
72
100
  - For greetings, name-calls, acknowledgements, and backchannels such as "มิ้น", "มิ้นๆ", "อ๋อ", "โอเค", "ขอบคุณ", "hi", "hello", "ok", or "thanks", return action "none" and a short reply only.
73
101
 
74
- GOAL:
75
- Your goal is to help the user with their queries. This Electron app is Chat Mode, not Code Agent Mode: use at most ONE simple action per user message, only when the latest message explicitly asks for that local action. If the user asks a question or asks you to provide text/commands, answer with action "none".
76
-
77
- ACTION DISCIPLINE:
78
- - Always return a single JSON object. Never return a JSON array or multiple actions.
79
- - If the user asks "พิมพ์คำสั่งให้หน่อย", "บอกคำสั่ง", "ขอคำสั่ง", "what command", or "type the command", provide the command in "response" and set action "none". Do NOT use "type_text" or "key_tap".
80
- - Use "type_text", "key_tap", "mouse_click", or "mouse_move" only when the user explicitly asks you to control the currently focused UI, not when they ask for a command to copy/type themselves.
81
- - If the user asks to run terminal commands or code, Chat Mode should provide the command or tell them to use the CLI agent. Do not type or press Enter on their behalf.
82
- - Never say you opened, checked, inspected, or verified a file/folder unless the selected action actually does it and the app will execute that action.
83
- - If the request needs workspace code inspection, edits, tests, or shell execution, tell the user to use the Mint CLI agent instead of pretending to inspect files.
102
+ {{ACTION_MODE_INSTRUCTION}}
84
103
 
85
104
  CREATOR INFO:
86
105
  - The creator is Pheem49.
@@ -144,6 +163,7 @@ ${toolRegistry.buildToolPromptSection()}
144
163
  // Replaces 5 previously duplicated mcpPrompt blocks.
145
164
  // ─────────────────────────────────────────────────────────────────────────────
146
165
  function buildSystemPrompt() {
166
+ const config = readConfig();
147
167
  pluginManager.loadPlugins();
148
168
  const mcpTools = mcpManager.getAllTools();
149
169
 
@@ -171,7 +191,9 @@ function buildSystemPrompt() {
171
191
  workspaceSection = `\n\n[WORKSPACE DETECTED: ${ws.name}]\nPath: ${ws.path}\nProject Instructions: ${ws.instructions}\n`;
172
192
  }
173
193
 
174
- return systemInstruction + personaInstruction + workspaceSection + pluginManager.getPromptDescriptions() + mcpSection + userContext;
194
+ const modeInstruction = buildActionModeInstruction(config);
195
+ const baseInstruction = systemInstruction.replace('{{ACTION_MODE_INSTRUCTION}}', modeInstruction);
196
+ return baseInstruction + personaInstruction + workspaceSection + pluginManager.getPromptDescriptions() + mcpSection + userContext;
175
197
  }
176
198
 
177
199
  function buildMessageWithRelevantMemory(finalMessage) {
@@ -197,10 +219,15 @@ function stripRelevantMemoryBlock(text) {
197
219
  return input
198
220
  .replace(/\n?\[Relevant long-term memory for this user message\][\s\S]*?\[End relevant memory\]\n?/g, '\n')
199
221
  .replace(/^\s*\[Relevant long-term memory for this user message\][\s\S]*?\[End relevant memory\]\s*/g, '')
222
+ .replace(/\n?\[SMART_CONTEXT\][\s\S]*?\[\/SMART_CONTEXT\]\n?/g, '\n')
200
223
  .replace(/\n?\[LOCAL KNOWLEDGE BASE - USE THIS CONTEXT TO ANSWER\][\s\S]*/g, '')
201
224
  .trim();
202
225
  }
203
226
 
227
+ function hasSmartContextBlock(text) {
228
+ return /\[SMART_CONTEXT\][\s\S]*?\[\/SMART_CONTEXT\]/.test(String(text || ''));
229
+ }
230
+
204
231
  function cleanHistoryForStorage(history) {
205
232
  if (!Array.isArray(history)) return [];
206
233
  return history.map(msg => ({
@@ -467,13 +494,15 @@ async function handleChat(message, base64Image = null, base64Audio = null) {
467
494
  const config = readConfig();
468
495
  const images = normalizeImageList(base64Image);
469
496
  const previousHistory = readChatHistory();
497
+ const userVisibleMessage = stripRelevantMemoryBlock(message);
498
+ const containsSmartContext = hasSmartContextBlock(message);
470
499
 
471
500
  let finalMessage = message;
472
501
 
473
502
  // Inject Local RAG Context
474
- if (message && message.trim().length > 0 && shouldUseKnowledgeSearch(message)) {
503
+ if (userVisibleMessage && userVisibleMessage.trim().length > 0 && shouldUseKnowledgeSearch(userVisibleMessage)) {
475
504
  const { searchKnowledge } = require('./knowledge_base');
476
- const retrievedDocs = await searchKnowledge(message);
505
+ const retrievedDocs = await searchKnowledge(userVisibleMessage);
477
506
  if (retrievedDocs && retrievedDocs.length > 0) {
478
507
  let contextString = `\n\n[LOCAL KNOWLEDGE BASE - USE THIS CONTEXT TO ANSWER]\n`;
479
508
  retrievedDocs.forEach(doc => {
@@ -483,8 +512,8 @@ async function handleChat(message, base64Image = null, base64Audio = null) {
483
512
  }
484
513
  }
485
514
 
486
- if (finalMessage && images.length === 0 && !base64Audio) {
487
- const cached = memoryStore.getCachedResponse(finalMessage);
515
+ if (!containsSmartContext && userVisibleMessage && images.length === 0 && !base64Audio) {
516
+ const cached = memoryStore.getCachedResponse(userVisibleMessage);
488
517
  if (cached) return cached;
489
518
  }
490
519
 
@@ -507,15 +536,15 @@ async function handleChat(message, base64Image = null, base64Audio = null) {
507
536
  model: getProviderModel(provider, config),
508
537
  usage: client.getUsageSummary()
509
538
  };
510
- const parsedResult = parseChatProviderResponse(outputText, finalMessage, now);
539
+ const parsedResult = parseChatProviderResponse(outputText, userVisibleMessage || finalMessage, now);
511
540
  parsedResult.providerInfo = providerInfo;
512
- appendChatProviderHistory(previousHistory, finalMessage, outputText, providerInfo, now);
541
+ appendChatProviderHistory(previousHistory, userVisibleMessage || finalMessage, outputText, providerInfo, now);
513
542
 
514
- if (finalMessage && parsedResult.response) {
543
+ if ((userVisibleMessage || finalMessage) && parsedResult.response) {
515
544
  setImmediate(() => {
516
- memoryStore.recordInteraction(finalMessage, parsedResult.response);
517
- if (images.length === 0 && !base64Audio) {
518
- memoryStore.cacheResponse(finalMessage, parsedResult);
545
+ memoryStore.recordInteraction(userVisibleMessage || finalMessage, parsedResult.response);
546
+ if (!containsSmartContext && images.length === 0 && !base64Audio) {
547
+ memoryStore.cacheResponse(userVisibleMessage || finalMessage, parsedResult);
519
548
  }
520
549
  });
521
550
  }
@@ -761,6 +790,7 @@ module.exports = {
761
790
  refreshApiKeyFromConfig,
762
791
  _helpers: {
763
792
  getProviderAttemptOrder,
764
- normalizeParsedResult
793
+ normalizeParsedResult,
794
+ buildActionModeInstruction
765
795
  }
766
796
  };
@@ -18,6 +18,8 @@ const IGNORED_DIRECTORY_NAMES = new Set([
18
18
  'coverage'
19
19
  ]);
20
20
 
21
+ const COMMON_HOME_FOLDERS = ['Downloads', 'Desktop', 'Documents', 'Videos', 'Pictures', 'Music', 'vscode', 'Games'];
22
+
21
23
  function getSearchRoots() {
22
24
  return Array.from(new Set([
23
25
  process.cwd(),
@@ -34,8 +36,6 @@ function resolveSmartPath(target) {
34
36
  if (!target) return target;
35
37
 
36
38
  const home = os.homedir();
37
- const commonFolders = ['Downloads', 'Desktop', 'Documents', 'Videos', 'Pictures', 'Music', 'vscode', 'Games'];
38
-
39
39
  // 1. If it's already an absolute path and exists, use it
40
40
  if (path.isAbsolute(target) && fs.existsSync(target)) return target;
41
41
 
@@ -54,13 +54,13 @@ function resolveSmartPath(target) {
54
54
  // 4. Check if the target itself starts with a common folder (e.g., "Downloads/resume.pdf")
55
55
  const parts = target.split(/[/\\]/);
56
56
  const firstPart = parts[0];
57
- if (commonFolders.includes(firstPart)) {
57
+ if (COMMON_HOME_FOLDERS.includes(firstPart)) {
58
58
  const potentialPath = path.join(home, target);
59
59
  if (fs.existsSync(potentialPath)) return potentialPath;
60
60
  }
61
61
 
62
62
  // 5. Try searching the filename in all common folders
63
- for (const folder of commonFolders) {
63
+ for (const folder of COMMON_HOME_FOLDERS) {
64
64
  const potentialPath = path.join(home, folder, target);
65
65
  if (fs.existsSync(potentialPath)) return potentialPath;
66
66
  }
@@ -76,6 +76,18 @@ function findPath(target, options = {}) {
76
76
 
77
77
  const normalizedType = ['file', 'dir', 'any'].includes(options.type) ? options.type : 'any';
78
78
  const loweredQuery = target.trim().toLowerCase();
79
+ const directPath = resolveSmartPath(target.trim());
80
+ if (directPath && path.isAbsolute(directPath) && fs.existsSync(directPath)) {
81
+ const stat = fs.statSync(directPath);
82
+ const directType = stat.isDirectory() ? 'dir' : 'file';
83
+ if (normalizedType === 'any' || normalizedType === directType) {
84
+ return {
85
+ success: true,
86
+ matches: [{ path: directPath, type: directType }]
87
+ };
88
+ }
89
+ }
90
+
79
91
  const exactMatches = [];
80
92
  const partialMatches = [];
81
93
  const visited = new Set();
@@ -271,4 +283,4 @@ async function deleteFile(target) {
271
283
  }
272
284
  }
273
285
 
274
- module.exports = { createFolder, openFile, deleteFile, findPath };
286
+ module.exports = { createFolder, openFile, deleteFile, findPath, resolveSmartPath };
@@ -45,6 +45,7 @@ const DEFAULT_CONFIG = {
45
45
  apiKey: '',
46
46
  geminiModel: 'gemini-2.5-flash',
47
47
  language: 'th-TH',
48
+ assistantMode: 'chat',
48
49
  automationBrowser: 'chromium',
49
50
  proactiveInterval: 60, // seconds between screen captures
50
51
  proactiveCooldown: 120, // seconds minimum between actual suggestions
@@ -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,
@@ -36,6 +76,12 @@ function registerIpcHandlers({
36
76
 
37
77
  if (aiResponse.action && aiResponse.action.type !== 'none') {
38
78
  try {
79
+ const approval = buildApprovalRequest(aiResponse.action);
80
+ if (approval) {
81
+ aiResponse.approval = approval;
82
+ return aiResponse;
83
+ }
84
+
39
85
  const actionResult = await executeAction(aiResponse.action, { clipboard });
40
86
  if (actionResult && typeof actionResult === 'string') {
41
87
  aiResponse.response += `\n\n${actionResult}`;
@@ -53,6 +99,18 @@ function registerIpcHandlers({
53
99
  }
54
100
  });
55
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
+
56
114
  ipcMain.on('close-window', () => {
57
115
  const mainWindow = windowManager.getMainWindow();
58
116
  if (mainWindow) mainWindow.hide();
@@ -167,6 +225,30 @@ function registerIpcHandlers({
167
225
  }
168
226
  });
169
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
+
170
252
  ipcMain.on('spotlight-resize', (event, width, height) => {
171
253
  const spotlightWindow = windowManager.getSpotlightWindow();
172
254
  if (spotlightWindow) spotlightWindow.setSize(width, height);
@@ -207,6 +289,8 @@ function registerIpcHandlers({
207
289
  ipcMain.on('vision-cancel', () => screenCapture.cancel());
208
290
  ipcMain.handle('capture-silent-screen', () => screenCapture.captureSilentScreen());
209
291
 
292
+ ipcMain.handle('get-smart-context', () => getSmartContext({ clipboard }));
293
+
210
294
  ipcMain.on('toggle-proactive', (event, isOn) => {
211
295
  if (isOn) {
212
296
  proactiveLoop.start();
@@ -245,4 +329,4 @@ function registerIpcHandlers({
245
329
  });
246
330
  }
247
331
 
248
- module.exports = { registerIpcHandlers };
332
+ module.exports = { registerIpcHandlers, buildApprovalRequest, executeApprovedAction };