@pheem49/mint 1.4.0 → 1.4.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/.codex +0 -0
- package/README.md +171 -127
- package/main.js +21 -1
- package/mint-cli-logic.js +21 -1
- package/mint-cli.js +89 -22
- package/package.json +1 -1
- package/src/AI_Brain/Gemini_API.js +38 -24
- package/src/AI_Brain/proactive_engine.js +2 -8
- package/src/Automation_Layer/file_operations.js +123 -4
- package/src/Automation_Layer/open_app.js +72 -43
- package/src/Automation_Layer/open_website.js +3 -3
- package/src/CLI/chat_router.js +51 -18
- package/src/CLI/chat_ui.js +34 -10
- package/src/CLI/code_agent.js +113 -13
- package/src/CLI/workspace_manager.js +15 -6
- package/src/Plugins/docker.js +12 -10
- package/src/System/custom_workflows.js +9 -2
- package/tests/chat_router.test.js +42 -0
- package/tests/code_agent.test.js +69 -0
- package/tests/docker.test.js +46 -0
- package/tests/file_operations.test.js +57 -0
- package/tests/provider_routing.test.js +67 -0
- package/tests/workspace_manager.test.js +15 -0
|
@@ -58,11 +58,13 @@ Always respond exactly with valid JSON containing NO MARKDOWN FORMATTING (do not
|
|
|
58
58
|
{
|
|
59
59
|
"response": "Your conversational reply here (Matches user language).",
|
|
60
60
|
"action": {
|
|
61
|
-
"type": "none" | "open_url" | "open_app" | "search" | "web_automation" | "create_folder" | "open_file" | "open_folder" | "delete_file" | "clipboard_write" | "system_info" | "plugin" | "learn_file" | "learn_folder" | "system_automation" | "mcp_tool" | "mouse_click" | "mouse_move" | "type_text" | "key_tap",
|
|
61
|
+
"type": "none" | "open_url" | "open_app" | "search" | "web_automation" | "create_folder" | "open_file" | "open_folder" | "find_path" | "delete_file" | "clipboard_write" | "system_info" | "plugin" | "learn_file" | "learn_folder" | "system_automation" | "mcp_tool" | "mouse_click" | "mouse_move" | "type_text" | "key_tap",
|
|
62
62
|
|
|
63
63
|
"pluginName": "only if type is plugin",
|
|
64
64
|
"server": "only if type is mcp_tool (server name)",
|
|
65
65
|
"target": "target string based on type (tool name if mcp_tool, text to type if type_text, key name if key_tap)",
|
|
66
|
+
"pathType": "optional for find_path: 'file' | 'dir' | 'any'",
|
|
67
|
+
"openAfter": true,
|
|
66
68
|
"x": 0-1000, // required for mouse_click and mouse_move
|
|
67
69
|
"y": 0-1000, // required for mouse_click and mouse_move
|
|
68
70
|
"button": 1 | 2 | 3, // optional for mouse_click, 1=left, 2=middle, 3=right
|
|
@@ -86,6 +88,12 @@ Output: { "response": "สวัสดีค่ะ! หนูชื่อมิ
|
|
|
86
88
|
Input: "Create a folder named Projects"
|
|
87
89
|
Output: { "response": "Sure thing! I'm creating a folder named 'Projects' for you right now.", "action": { "type": "create_folder", "target": "Projects" } }
|
|
88
90
|
|
|
91
|
+
Input: "หาโฟลเดอร์ xidaidai ให้หน่อย" or "find the xidaidai folder"
|
|
92
|
+
Output: { "response": "ได้เลยค่ะ มิ้นท์จะค้นหาโฟลเดอร์ xidaidai ให้", "action": { "type": "find_path", "target": "xidaidai", "pathType": "dir", "openAfter": false } }
|
|
93
|
+
|
|
94
|
+
Input: "เปิดโฟลเดอร์ xidaidai ให้หน่อย" or "open the xidaidai folder"
|
|
95
|
+
Output: { "response": "ได้เลยค่ะ มิ้นท์จะหาแล้วเปิดโฟลเดอร์ xidaidai ให้", "action": { "type": "find_path", "target": "xidaidai", "pathType": "dir", "openAfter": true } }
|
|
96
|
+
|
|
89
97
|
Input: "วันนี้วันที่เท่าไร" or "What date is today?" or "today's date" or "วันเวลา"
|
|
90
98
|
Output: { "response": "แป๊บนึงนะคะ มิ้นท์จะดูให้ค่า", "action": { "type": "system_info", "target": "" } }
|
|
91
99
|
|
|
@@ -165,6 +173,13 @@ function resolveGeminiModel() {
|
|
|
165
173
|
}
|
|
166
174
|
}
|
|
167
175
|
|
|
176
|
+
function getProviderAttemptOrder(config) {
|
|
177
|
+
const provider = config.aiProvider || 'gemini';
|
|
178
|
+
const availableProviders = getAvailableProviders(config);
|
|
179
|
+
const alternates = availableProviders.filter(p => p !== provider);
|
|
180
|
+
return [provider, ...alternates];
|
|
181
|
+
}
|
|
182
|
+
|
|
168
183
|
// Chat session — maintains conversation history within the session
|
|
169
184
|
let chat = null;
|
|
170
185
|
let activeModel = resolveGeminiModel();
|
|
@@ -214,21 +229,6 @@ function shouldUseKnowledgeSearch(message) {
|
|
|
214
229
|
async function handleChat(message, base64Image = null, base64Audio = null) {
|
|
215
230
|
try {
|
|
216
231
|
const config = readConfig();
|
|
217
|
-
const provider = config.aiProvider || 'gemini';
|
|
218
|
-
|
|
219
|
-
// Ensure API Key is loaded and Client is initialized before every chat
|
|
220
|
-
const currentKey = resolveApiKey();
|
|
221
|
-
if (!currentKey) {
|
|
222
|
-
return {
|
|
223
|
-
response: "I couldn't find your Gemini API Key. Please run 'mint onboard' to set it up!",
|
|
224
|
-
action: { type: "none", target: "" }
|
|
225
|
-
};
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
if (!ai || activeApiKey !== currentKey) {
|
|
229
|
-
initAiClient();
|
|
230
|
-
createChat(readChatHistory());
|
|
231
|
-
}
|
|
232
232
|
|
|
233
233
|
let finalMessage = message;
|
|
234
234
|
|
|
@@ -245,13 +245,7 @@ async function handleChat(message, base64Image = null, base64Audio = null) {
|
|
|
245
245
|
}
|
|
246
246
|
}
|
|
247
247
|
|
|
248
|
-
const
|
|
249
|
-
const availableProviders = getAvailableProviders(config);
|
|
250
|
-
|
|
251
|
-
// Ensure the requested provider is prioritized. If not available, fallback to the first available.
|
|
252
|
-
let providersToTry = [provider];
|
|
253
|
-
const alternates = availableProviders.filter(p => p !== provider);
|
|
254
|
-
providersToTry = providersToTry.concat(alternates);
|
|
248
|
+
const providersToTry = getProviderAttemptOrder(config);
|
|
255
249
|
|
|
256
250
|
for (let i = 0; i < providersToTry.length; i++) {
|
|
257
251
|
const currentProv = providersToTry[i];
|
|
@@ -272,6 +266,23 @@ async function handleChat(message, base64Image = null, base64Audio = null) {
|
|
|
272
266
|
return await handleHuggingFaceChat(finalMessage, base64Image, config);
|
|
273
267
|
}
|
|
274
268
|
|
|
269
|
+
const currentKey = resolveApiKey();
|
|
270
|
+
if (!currentKey) {
|
|
271
|
+
if (i === providersToTry.length - 1) {
|
|
272
|
+
return {
|
|
273
|
+
response: "I couldn't find your Gemini API Key. Please run 'mint onboard' to set it up!",
|
|
274
|
+
action: { type: "none", target: "" }
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
console.warn("[Fallback System] Gemini API key missing. Skipping Gemini provider.");
|
|
278
|
+
continue;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
if (!ai || activeApiKey !== currentKey) {
|
|
282
|
+
initAiClient();
|
|
283
|
+
createChat(readChatHistory());
|
|
284
|
+
}
|
|
285
|
+
|
|
275
286
|
return await handleGeminiChat(finalMessage, base64Image, base64Audio);
|
|
276
287
|
} catch (error) {
|
|
277
288
|
console.error(`[Fallback System] Provider '${currentProv}' failed:`, error.message);
|
|
@@ -887,5 +898,8 @@ module.exports = {
|
|
|
887
898
|
resetChat,
|
|
888
899
|
getChatTranscript,
|
|
889
900
|
translateImageContent,
|
|
890
|
-
refreshApiKeyFromConfig
|
|
901
|
+
refreshApiKeyFromConfig,
|
|
902
|
+
_helpers: {
|
|
903
|
+
getProviderAttemptOrder
|
|
904
|
+
}
|
|
891
905
|
};
|
|
@@ -1,7 +1,4 @@
|
|
|
1
1
|
const { GoogleGenAI } = require('@google/genai');
|
|
2
|
-
const path = require('path');
|
|
3
|
-
const fs = require('fs');
|
|
4
|
-
const { app } = require('electron');
|
|
5
2
|
const { readConfig } = require('../System/config_manager');
|
|
6
3
|
|
|
7
4
|
// ============================================================
|
|
@@ -76,11 +73,8 @@ function resolveGeminiModel() {
|
|
|
76
73
|
|
|
77
74
|
function getMinSuggestionIntervalMs() {
|
|
78
75
|
try {
|
|
79
|
-
const
|
|
80
|
-
|
|
81
|
-
const cfg = JSON.parse(fs.readFileSync(CONFIG_PATH, 'utf8'));
|
|
82
|
-
return (cfg.proactiveCooldown || 120) * 1000;
|
|
83
|
-
}
|
|
76
|
+
const cfg = readConfig();
|
|
77
|
+
return (cfg.proactiveCooldown || 120) * 1000;
|
|
84
78
|
} catch {
|
|
85
79
|
// ignore
|
|
86
80
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
const {
|
|
1
|
+
const { execFile } = require('child_process');
|
|
2
2
|
let shell;
|
|
3
3
|
try {
|
|
4
4
|
shell = require('electron').shell;
|
|
@@ -9,6 +9,22 @@ const fs = require('fs');
|
|
|
9
9
|
const path = require('path');
|
|
10
10
|
const os = require('os');
|
|
11
11
|
|
|
12
|
+
const IGNORED_DIRECTORY_NAMES = new Set([
|
|
13
|
+
'.git',
|
|
14
|
+
'node_modules',
|
|
15
|
+
'.cache',
|
|
16
|
+
'dist',
|
|
17
|
+
'build',
|
|
18
|
+
'coverage'
|
|
19
|
+
]);
|
|
20
|
+
|
|
21
|
+
function getSearchRoots() {
|
|
22
|
+
return Array.from(new Set([
|
|
23
|
+
process.cwd(),
|
|
24
|
+
os.homedir()
|
|
25
|
+
]));
|
|
26
|
+
}
|
|
27
|
+
|
|
12
28
|
/**
|
|
13
29
|
* Smartly resolves a path.
|
|
14
30
|
* If a path starts with '/' but doesn't exist at root, checks if it exists relative to home.
|
|
@@ -53,6 +69,109 @@ function resolveSmartPath(target) {
|
|
|
53
69
|
return target;
|
|
54
70
|
}
|
|
55
71
|
|
|
72
|
+
function findPath(target, options = {}) {
|
|
73
|
+
if (!target || !target.trim()) {
|
|
74
|
+
return { success: false, message: 'No search query provided.', matches: [] };
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const normalizedType = ['file', 'dir', 'any'].includes(options.type) ? options.type : 'any';
|
|
78
|
+
const loweredQuery = target.trim().toLowerCase();
|
|
79
|
+
const exactMatches = [];
|
|
80
|
+
const partialMatches = [];
|
|
81
|
+
const visited = new Set();
|
|
82
|
+
const maxResults = options.maxResults || 20;
|
|
83
|
+
const searchRoots = Array.isArray(options.roots) && options.roots.length > 0
|
|
84
|
+
? options.roots
|
|
85
|
+
: getSearchRoots();
|
|
86
|
+
|
|
87
|
+
function buildMatch(entryPath, entryType, rootPath, exactNameMatch) {
|
|
88
|
+
const relativeToCwd = path.relative(process.cwd(), entryPath);
|
|
89
|
+
const pathDepth = entryPath.split(path.sep).length;
|
|
90
|
+
return {
|
|
91
|
+
path: entryPath,
|
|
92
|
+
type: entryType,
|
|
93
|
+
exactNameMatch,
|
|
94
|
+
inCurrentWorkspace: !relativeToCwd.startsWith('..') && !path.isAbsolute(relativeToCwd),
|
|
95
|
+
pathDepth,
|
|
96
|
+
rootPath
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function sortMatches(matches) {
|
|
101
|
+
return matches.sort((a, b) => {
|
|
102
|
+
if (a.exactNameMatch !== b.exactNameMatch) return a.exactNameMatch ? -1 : 1;
|
|
103
|
+
if (a.inCurrentWorkspace !== b.inCurrentWorkspace) return a.inCurrentWorkspace ? -1 : 1;
|
|
104
|
+
if (a.pathDepth !== b.pathDepth) return a.pathDepth - b.pathDepth;
|
|
105
|
+
return a.path.localeCompare(b.path);
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function visit(currentPath, rootPath) {
|
|
110
|
+
let entries = [];
|
|
111
|
+
try {
|
|
112
|
+
entries = fs.readdirSync(currentPath, { withFileTypes: true });
|
|
113
|
+
} catch (_) {
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
for (const entry of entries) {
|
|
118
|
+
const absoluteEntryPath = path.join(currentPath, entry.name);
|
|
119
|
+
if (visited.has(absoluteEntryPath)) continue;
|
|
120
|
+
visited.add(absoluteEntryPath);
|
|
121
|
+
|
|
122
|
+
const entryType = entry.isDirectory() ? 'dir' : 'file';
|
|
123
|
+
if (entry.isDirectory() && IGNORED_DIRECTORY_NAMES.has(entry.name)) {
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
126
|
+
const relativePath = path.relative(rootPath, absoluteEntryPath);
|
|
127
|
+
const searchablePath = relativePath || entry.name;
|
|
128
|
+
const matchesType = normalizedType === 'any' || normalizedType === entryType;
|
|
129
|
+
const lowerEntryName = entry.name.toLowerCase();
|
|
130
|
+
const exactNameMatch = lowerEntryName === loweredQuery;
|
|
131
|
+
const partialMatch = lowerEntryName.includes(loweredQuery) || searchablePath.toLowerCase().includes(loweredQuery);
|
|
132
|
+
|
|
133
|
+
if (matchesType && partialMatch) {
|
|
134
|
+
const match = buildMatch(absoluteEntryPath, entryType, rootPath, exactNameMatch);
|
|
135
|
+
if (exactNameMatch) {
|
|
136
|
+
exactMatches.push(match);
|
|
137
|
+
if (exactMatches.length >= maxResults) return;
|
|
138
|
+
} else if (exactMatches.length === 0) {
|
|
139
|
+
partialMatches.push(match);
|
|
140
|
+
if (partialMatches.length >= maxResults) return;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
if (entry.isDirectory() && exactMatches.length < maxResults && partialMatches.length < maxResults) {
|
|
145
|
+
visit(absoluteEntryPath, rootPath);
|
|
146
|
+
if (exactMatches.length >= maxResults || partialMatches.length >= maxResults) return;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
for (const rootPath of searchRoots) {
|
|
152
|
+
if (!fs.existsSync(rootPath)) continue;
|
|
153
|
+
visit(rootPath, rootPath);
|
|
154
|
+
if (exactMatches.length >= maxResults || partialMatches.length >= maxResults) break;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const matches = exactMatches.length > 0
|
|
158
|
+
? sortMatches(exactMatches).slice(0, maxResults)
|
|
159
|
+
: sortMatches(partialMatches).slice(0, maxResults);
|
|
160
|
+
|
|
161
|
+
if (matches.length === 0) {
|
|
162
|
+
return {
|
|
163
|
+
success: false,
|
|
164
|
+
message: `ไม่พบ${normalizedType === 'dir' ? 'โฟลเดอร์' : normalizedType === 'file' ? 'ไฟล์' : 'ไฟล์หรือโฟลเดอร์'}ที่ตรงกับ "${target}" ค่ะ`,
|
|
165
|
+
matches: []
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
return {
|
|
170
|
+
success: true,
|
|
171
|
+
matches: matches.map(({ path: matchPath, type }) => ({ path: matchPath, type }))
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
|
|
56
175
|
/**
|
|
57
176
|
* สร้างโฟลเดอร์ใหม่
|
|
58
177
|
* target: ชื่อโฟลเดอร์ หรือ absolute path
|
|
@@ -99,7 +218,7 @@ async function openFile(target) {
|
|
|
99
218
|
}
|
|
100
219
|
} else {
|
|
101
220
|
return new Promise((resolve) => {
|
|
102
|
-
|
|
221
|
+
execFile('xdg-open', [resolvedPath], (err) => {
|
|
103
222
|
if (err) {
|
|
104
223
|
console.error("Failed to open path via xdg-open:", err);
|
|
105
224
|
resolve(`ไม่สามารถเปิดไฟล์ได้ค่ะ: ${err.message}`);
|
|
@@ -129,7 +248,7 @@ async function deleteFile(target) {
|
|
|
129
248
|
}
|
|
130
249
|
} else {
|
|
131
250
|
return new Promise((resolve) => {
|
|
132
|
-
|
|
251
|
+
execFile('gio', ['trash', resolvedPath], (err) => {
|
|
133
252
|
if (err) {
|
|
134
253
|
console.error("Failed to trash item via gio trash:", err);
|
|
135
254
|
resolve({ success: false, message: err.message });
|
|
@@ -141,4 +260,4 @@ async function deleteFile(target) {
|
|
|
141
260
|
}
|
|
142
261
|
}
|
|
143
262
|
|
|
144
|
-
module.exports = { createFolder, openFile, deleteFile };
|
|
263
|
+
module.exports = { createFolder, openFile, deleteFile, findPath };
|
|
@@ -1,56 +1,85 @@
|
|
|
1
|
-
const {
|
|
1
|
+
const { execFile } = require('child_process');
|
|
2
2
|
|
|
3
|
-
function
|
|
3
|
+
function execFilePromise(command, args) {
|
|
4
|
+
return new Promise((resolve, reject) => {
|
|
5
|
+
execFile(command, args, (error) => {
|
|
6
|
+
if (error) {
|
|
7
|
+
reject(error);
|
|
8
|
+
return;
|
|
9
|
+
}
|
|
10
|
+
resolve();
|
|
11
|
+
});
|
|
12
|
+
});
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
async function tryCommands(commands) {
|
|
16
|
+
let lastError = null;
|
|
17
|
+
for (const { command, args } of commands) {
|
|
18
|
+
try {
|
|
19
|
+
await execFilePromise(command, args);
|
|
20
|
+
return true;
|
|
21
|
+
} catch (error) {
|
|
22
|
+
lastError = error;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
if (lastError) {
|
|
27
|
+
console.error(`exec error: ${lastError}`);
|
|
28
|
+
}
|
|
29
|
+
return false;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
async function openApp(target) {
|
|
4
33
|
if (!target) return;
|
|
5
34
|
|
|
6
|
-
let cmd = '';
|
|
7
35
|
if (process.platform === 'win32') {
|
|
8
|
-
cmd
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
} else {
|
|
16
|
-
const tLower = target.toLowerCase();
|
|
17
|
-
const tCapitalized = target.charAt(0).toUpperCase() + target.slice(1).toLowerCase();
|
|
18
|
-
|
|
19
|
-
// Try common linux patterns: gtk-launch, exact name, lowercase, flatpak
|
|
36
|
+
await execFilePromise('cmd.exe', ['/c', 'start', '', target]).catch((error) => {
|
|
37
|
+
console.error(`exec error: ${error}`);
|
|
38
|
+
});
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (process.platform === 'darwin') {
|
|
20
43
|
if (!target.includes('/')) {
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
`gtk-launch com.${tLower}app.${tCapitalized}`,
|
|
26
|
-
`gtk-launch com.${tLower}.${tCapitalized}`,
|
|
27
|
-
target,
|
|
28
|
-
tLower,
|
|
29
|
-
`flatpak run ${target}`, // In case target is already ID
|
|
30
|
-
`flatpak run com.${tLower}app.${tCapitalized}`,
|
|
31
|
-
`flatpak run com.${tLower}.${tCapitalized}`,
|
|
32
|
-
`flatpak run com.${tLower}.Browser`,
|
|
33
|
-
`flatpak run com.${tLower}.${target}`,
|
|
34
|
-
`flatpak run com.valvesoftware.Steam`,
|
|
35
|
-
`flatpak run net.lutris.Lutris`,
|
|
36
|
-
`snap run ${tLower}`
|
|
37
|
-
];
|
|
38
|
-
cmd = patterns.join(' || ');
|
|
44
|
+
await tryCommands([
|
|
45
|
+
{ command: 'open', args: ['-X', '-a', target] },
|
|
46
|
+
{ command: 'open', args: ['-a', target] }
|
|
47
|
+
]);
|
|
39
48
|
} else {
|
|
40
|
-
|
|
49
|
+
await execFilePromise('open', [target]).catch((error) => {
|
|
50
|
+
console.error(`exec error: ${error}`);
|
|
51
|
+
});
|
|
41
52
|
}
|
|
53
|
+
return;
|
|
42
54
|
}
|
|
43
55
|
|
|
44
|
-
|
|
45
|
-
|
|
56
|
+
const tLower = target.toLowerCase();
|
|
57
|
+
const tCapitalized = target.charAt(0).toUpperCase() + target.slice(1).toLowerCase();
|
|
58
|
+
|
|
59
|
+
if (target.includes('/')) {
|
|
60
|
+
await execFilePromise('xdg-open', [target]).catch((error) => {
|
|
46
61
|
console.error(`exec error: ${error}`);
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
}
|
|
53
|
-
|
|
62
|
+
});
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
await tryCommands([
|
|
67
|
+
{ command: 'gtk-launch', args: [target] },
|
|
68
|
+
{ command: 'gtk-launch', args: [tLower] },
|
|
69
|
+
{ command: 'gtk-launch', args: [tCapitalized] },
|
|
70
|
+
{ command: 'gtk-launch', args: [`com.${tLower}app.${tCapitalized}`] },
|
|
71
|
+
{ command: 'gtk-launch', args: [`com.${tLower}.${tCapitalized}`] },
|
|
72
|
+
{ command: target, args: [] },
|
|
73
|
+
{ command: tLower, args: [] },
|
|
74
|
+
{ command: 'flatpak', args: ['run', target] },
|
|
75
|
+
{ command: 'flatpak', args: ['run', `com.${tLower}app.${tCapitalized}`] },
|
|
76
|
+
{ command: 'flatpak', args: ['run', `com.${tLower}.${tCapitalized}`] },
|
|
77
|
+
{ command: 'flatpak', args: ['run', `com.${tLower}.Browser`] },
|
|
78
|
+
{ command: 'flatpak', args: ['run', `com.${tLower}.${target}`] },
|
|
79
|
+
{ command: 'flatpak', args: ['run', 'com.valvesoftware.Steam'] },
|
|
80
|
+
{ command: 'flatpak', args: ['run', 'net.lutris.Lutris'] },
|
|
81
|
+
{ command: 'snap', args: ['run', tLower] }
|
|
82
|
+
]);
|
|
54
83
|
}
|
|
55
84
|
|
|
56
85
|
module.exports = { openApp };
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
const {
|
|
1
|
+
const { execFile } = require('child_process');
|
|
2
2
|
|
|
3
3
|
let shell;
|
|
4
4
|
try {
|
|
@@ -17,7 +17,7 @@ function openWebsite(targetUrl) {
|
|
|
17
17
|
shell.openExternal(url);
|
|
18
18
|
} else {
|
|
19
19
|
// Fallback for Node.js (Linux focus)
|
|
20
|
-
|
|
20
|
+
execFile('xdg-open', [url], (err) => {
|
|
21
21
|
if (err) console.error("Failed to open URL via xdg_open:", err);
|
|
22
22
|
});
|
|
23
23
|
}
|
|
@@ -29,7 +29,7 @@ function openSearch(query) {
|
|
|
29
29
|
if (shell) {
|
|
30
30
|
shell.openExternal(url);
|
|
31
31
|
} else {
|
|
32
|
-
|
|
32
|
+
execFile('xdg-open', [url], (err) => {
|
|
33
33
|
if (err) console.error("Failed to open search via xdg-open:", err);
|
|
34
34
|
});
|
|
35
35
|
}
|
package/src/CLI/chat_router.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
const fs = require('fs');
|
|
2
2
|
const path = require('path');
|
|
3
3
|
const { GoogleGenAI } = require('@google/genai');
|
|
4
|
-
const { executeCodeTask } = require('./code_agent');
|
|
4
|
+
const { executeCodeTask, _helpers: codeAgentHelpers } = require('./code_agent');
|
|
5
5
|
const { readConfig, getAvailableProviders } = require('../System/config_manager');
|
|
6
6
|
|
|
7
7
|
const DEFAULT_GEMINI_MODEL = 'gemini-2.5-flash';
|
|
@@ -26,7 +26,19 @@ Return JSON only:
|
|
|
26
26
|
}
|
|
27
27
|
|
|
28
28
|
Choose "code" when the user is asking to inspect, edit, review, debug, explain, refactor, verify, or otherwise operate on the current project/workspace/codebase/files.
|
|
29
|
-
Choose "chat" for general conversation, factual Q&A,
|
|
29
|
+
Choose "chat" for general conversation, factual Q&A, small/simple requests, non-code assistant tasks, or direct file-system actions like finding/opening a folder or file by name.
|
|
30
|
+
Only choose "code" for substantial coding work that likely needs multiple steps, workspace inspection, edits, verification, or project-wide reasoning.`;
|
|
31
|
+
|
|
32
|
+
function isDirectFilesystemActionRequest(text) {
|
|
33
|
+
const input = (text || '').trim().toLowerCase();
|
|
34
|
+
if (!input) return false;
|
|
35
|
+
|
|
36
|
+
const filesystemActionPattern = /(open|find|locate|search for|look for|หา|ค้นหา|เปิด)/;
|
|
37
|
+
const filesystemTargetPattern = /(folder|directory|dir|file|โฟลเดอร์|ไฟล์|ไดเรกทอรี)/;
|
|
38
|
+
const codeOperationPattern = /(inspect|review|refactor|debug|implement|edit|change|fix|explain|analyze|สำรวจ|รีวิว|รีแฟกเตอร์|แก้|อธิบาย|วิเคราะห์)/;
|
|
39
|
+
|
|
40
|
+
return filesystemActionPattern.test(input) && filesystemTargetPattern.test(input) && !codeOperationPattern.test(input);
|
|
41
|
+
}
|
|
30
42
|
|
|
31
43
|
function workspaceLooksLikeCodebase(workspaceRoot) {
|
|
32
44
|
const markers = [
|
|
@@ -45,13 +57,27 @@ function detectCodeIntentHeuristic(text, workspaceRoot = process.cwd()) {
|
|
|
45
57
|
const input = (text || '').trim().toLowerCase();
|
|
46
58
|
if (!input) return false;
|
|
47
59
|
if (input.startsWith('/code ')) return true;
|
|
60
|
+
if (isDirectFilesystemActionRequest(input)) return false;
|
|
61
|
+
|
|
62
|
+
return isLargeCodeTaskRequest(input, workspaceRoot);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function isLargeCodeTaskRequest(text, workspaceRoot = process.cwd()) {
|
|
66
|
+
const input = (text || '').trim().toLowerCase();
|
|
67
|
+
if (!input) return false;
|
|
68
|
+
if (!workspaceLooksLikeCodebase(workspaceRoot)) return false;
|
|
48
69
|
|
|
49
70
|
const hasCodeKeyword = CODE_KEYWORDS.some(keyword => input.includes(keyword));
|
|
50
71
|
const hasThaiCodeKeyword = THAI_CODE_KEYWORDS.some(keyword => input.includes(keyword));
|
|
51
72
|
const referencesProject = /โปรเจคนี้|โปรเจ็กต์นี้|this project|this repo|this repository|codebase|workspace/.test(input);
|
|
52
73
|
const asksForAction = /สำรวจ|ดู|แก้|เพิ่ม|ลบ|ปรับ|ตรวจ|วิเคราะห์|implement|inspect|explore|fix|update|change|refactor|review|explain|debug/.test(input);
|
|
74
|
+
const strongTaskSignal = /failing tests?|run tests?|verify|verification|bug|issue|error|refactor|implement|feature|patch|edit|modify|analyze the project|แก้บั๊ก|รันเทสต์|ทดสอบ|ตรวจสอบ|ยืนยันผล|รีแฟกเตอร์|เพิ่มฟีเจอร์|แก้โค้ด|วิเคราะห์โปรเจค/.test(input);
|
|
75
|
+
const multiStepSignal = /and|then|พร้อม|แล้ว|จากนั้น|ทั้ง|ทั่วทั้ง|ทั้งโปรเจค|project-wide|entire project|whole project/.test(input);
|
|
53
76
|
|
|
54
|
-
|
|
77
|
+
if (referencesProject && strongTaskSignal) return true;
|
|
78
|
+
if ((hasCodeKeyword || hasThaiCodeKeyword) && asksForAction && strongTaskSignal) return true;
|
|
79
|
+
if ((hasCodeKeyword || hasThaiCodeKeyword) && multiStepSignal && asksForAction) return true;
|
|
80
|
+
return false;
|
|
55
81
|
}
|
|
56
82
|
|
|
57
83
|
function getRouterClient() {
|
|
@@ -71,7 +97,7 @@ function summarizeWorkspace(workspaceRoot) {
|
|
|
71
97
|
.join(', ') || '(no obvious code markers)';
|
|
72
98
|
}
|
|
73
99
|
|
|
74
|
-
async function detectCodeIntent(text, workspaceRoot = process.cwd()) {
|
|
100
|
+
async function detectCodeIntent(text, workspaceRoot = process.cwd(), history = []) {
|
|
75
101
|
const input = (text || '').trim();
|
|
76
102
|
if (!input) {
|
|
77
103
|
return { route: 'chat', reason: 'Empty input.' };
|
|
@@ -81,6 +107,10 @@ async function detectCodeIntent(text, workspaceRoot = process.cwd()) {
|
|
|
81
107
|
return { route: 'code', reason: 'Explicit /code command.' };
|
|
82
108
|
}
|
|
83
109
|
|
|
110
|
+
if (isDirectFilesystemActionRequest(input)) {
|
|
111
|
+
return { route: 'chat', reason: 'Direct file-system action request.' };
|
|
112
|
+
}
|
|
113
|
+
|
|
84
114
|
const heuristicRoute = detectCodeIntentHeuristic(input, workspaceRoot);
|
|
85
115
|
const routerClient = getRouterClient();
|
|
86
116
|
if (!routerClient) {
|
|
@@ -103,7 +133,8 @@ async function detectCodeIntent(text, workspaceRoot = process.cwd()) {
|
|
|
103
133
|
text: [
|
|
104
134
|
`Workspace: ${workspaceRoot}`,
|
|
105
135
|
`Workspace markers: ${summarizeWorkspace(workspaceRoot)}`,
|
|
106
|
-
`
|
|
136
|
+
`Context (Last 5 turns): ${history.slice(-10).map(m => `${m.sender}: ${m.text}`).join('\n')}`,
|
|
137
|
+
`Current Message: ${input}`
|
|
107
138
|
].join('\n')
|
|
108
139
|
}]
|
|
109
140
|
}]
|
|
@@ -112,6 +143,12 @@ async function detectCodeIntent(text, workspaceRoot = process.cwd()) {
|
|
|
112
143
|
const textOutput = typeof response.text === 'function' ? response.text() : response.text;
|
|
113
144
|
const parsed = JSON.parse(textOutput);
|
|
114
145
|
const route = parsed.route === 'code' ? 'code' : 'chat';
|
|
146
|
+
if (route === 'code' && !isLargeCodeTaskRequest(input, workspaceRoot)) {
|
|
147
|
+
return {
|
|
148
|
+
route: 'chat',
|
|
149
|
+
reason: 'Request looks small enough for normal chat.'
|
|
150
|
+
};
|
|
151
|
+
}
|
|
115
152
|
return {
|
|
116
153
|
route,
|
|
117
154
|
reason: parsed.reason || (route === 'code' ? 'Model classified as code.' : 'Model classified as chat.')
|
|
@@ -126,21 +163,11 @@ async function detectCodeIntent(text, workspaceRoot = process.cwd()) {
|
|
|
126
163
|
|
|
127
164
|
async function runChatRoutedTask(input, context) {
|
|
128
165
|
const text = input.startsWith('/code ') ? input.slice('/code '.length).trim() : input;
|
|
129
|
-
const { appendMessage, setThinking, requestApproval, setMode } = context;
|
|
166
|
+
const { appendMessage, setThinking, requestApproval, setMode, history } = context;
|
|
130
167
|
|
|
131
168
|
const config = readConfig();
|
|
132
169
|
const availableProviders = getAvailableProviders(config);
|
|
133
|
-
|
|
134
|
-
// Smart Routing Priority for Code Tasks
|
|
135
|
-
let preferredProvider = config.aiProvider || 'gemini';
|
|
136
|
-
|
|
137
|
-
// If preferred isn't actually available, try best available
|
|
138
|
-
if (!availableProviders.includes(preferredProvider)) {
|
|
139
|
-
if (availableProviders.includes('anthropic')) preferredProvider = 'anthropic';
|
|
140
|
-
else if (availableProviders.includes('openai')) preferredProvider = 'openai';
|
|
141
|
-
else if (availableProviders.includes('gemini')) preferredProvider = 'gemini';
|
|
142
|
-
else preferredProvider = availableProviders[0] || 'gemini';
|
|
143
|
-
}
|
|
170
|
+
const preferredProvider = codeAgentHelpers.selectSupportedCodeProvider(config, availableProviders);
|
|
144
171
|
|
|
145
172
|
appendMessage('system', `Routing this request to Code Mode for workspace: ${process.cwd()} using [${preferredProvider}]`);
|
|
146
173
|
if (setMode) setMode('Code');
|
|
@@ -157,6 +184,7 @@ async function runChatRoutedTask(input, context) {
|
|
|
157
184
|
cwd: process.cwd(),
|
|
158
185
|
requestApproval,
|
|
159
186
|
provider: preferredProvider,
|
|
187
|
+
history: history,
|
|
160
188
|
onProgress: (message) => appendMessage('system', `[Code] ${message}`)
|
|
161
189
|
});
|
|
162
190
|
clearInterval(timer);
|
|
@@ -177,5 +205,10 @@ async function runChatRoutedTask(input, context) {
|
|
|
177
205
|
|
|
178
206
|
module.exports = {
|
|
179
207
|
detectCodeIntent,
|
|
180
|
-
runChatRoutedTask
|
|
208
|
+
runChatRoutedTask,
|
|
209
|
+
_helpers: {
|
|
210
|
+
detectCodeIntentHeuristic,
|
|
211
|
+
isDirectFilesystemActionRequest,
|
|
212
|
+
isLargeCodeTaskRequest
|
|
213
|
+
}
|
|
181
214
|
};
|