@pheem49/mint 1.3.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 +174 -126
- package/main.js +21 -1
- package/mint-cli-logic.js +21 -1
- package/mint-cli.js +287 -45
- package/package.json +13 -2
- package/src/AI_Brain/Gemini_API.js +331 -64
- package/src/AI_Brain/agent_orchestrator.js +73 -0
- package/src/AI_Brain/autonomous_brain.js +2 -0
- package/src/AI_Brain/memory_store.js +318 -0
- 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 +57 -9
- package/src/CLI/chat_ui.js +117 -11
- package/src/CLI/code_agent.js +249 -36
- package/src/CLI/onboarding.js +53 -6
- package/src/CLI/workspace_manager.js +90 -0
- package/src/Plugins/docker.js +12 -10
- package/src/Plugins/spotify.js +168 -40
- package/src/Plugins/system_monitor.js +72 -0
- package/src/System/config_manager.js +35 -2
- package/src/System/custom_workflows.js +9 -2
- package/src/System/notifications.js +23 -0
- package/src/UI/settings.html +143 -65
- package/src/UI/settings.js +155 -41
- package/tests/agent_orchestrator.test.js +41 -0
- package/tests/chat_router.test.js +42 -0
- package/tests/code_agent.test.js +69 -0
- package/tests/config_manager.test.js +141 -0
- package/tests/docker.test.js +46 -0
- package/tests/file_operations.test.js +57 -0
- package/tests/memory_store.test.js +185 -0
- package/tests/provider_routing.test.js +67 -0
- package/tests/spotify.test.js +201 -0
- package/tests/system_monitor.test.js +37 -0
- package/tests/workspace_manager.test.js +56 -0
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mint Long-Term Memory Store
|
|
3
|
+
* ---------------------------
|
|
4
|
+
* Persists user preferences, session summaries, and usage patterns
|
|
5
|
+
* across all Mint sessions using SQLite (same DB as knowledge_base).
|
|
6
|
+
*
|
|
7
|
+
* Auto-injects a "User Context" block into the system prompt so Mint
|
|
8
|
+
* remembers who it's talking to even after restart.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
const fs = require('fs');
|
|
12
|
+
const path = require('path');
|
|
13
|
+
const os = require('os');
|
|
14
|
+
const crypto = require('crypto');
|
|
15
|
+
const { readConfig } = require('../System/config_manager');
|
|
16
|
+
|
|
17
|
+
// ── Electron-safe app path ──────────────────────────────────────────────────
|
|
18
|
+
let app;
|
|
19
|
+
try {
|
|
20
|
+
const electron = require('electron');
|
|
21
|
+
app = electron.app;
|
|
22
|
+
} catch (_) {
|
|
23
|
+
app = null;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function getDbPath() {
|
|
27
|
+
const fileName = 'mint-knowledge.sqlite'; // shared DB with knowledge_base
|
|
28
|
+
if (app && app.getPath) {
|
|
29
|
+
return path.join(app.getPath('userData'), fileName);
|
|
30
|
+
}
|
|
31
|
+
const mintDir = path.join(os.homedir(), '.mint');
|
|
32
|
+
if (!fs.existsSync(mintDir)) fs.mkdirSync(mintDir, { recursive: true });
|
|
33
|
+
return path.join(mintDir, fileName);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// ── Lazy DatabaseSync init ─────────────────────────────────────────────────
|
|
37
|
+
let DatabaseSync = null;
|
|
38
|
+
function getDatabaseSync() {
|
|
39
|
+
if (!DatabaseSync) ({ DatabaseSync } = require('node:sqlite'));
|
|
40
|
+
return DatabaseSync;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
let dbInstance = null;
|
|
44
|
+
function getDb() {
|
|
45
|
+
if (dbInstance) return dbInstance;
|
|
46
|
+
const Database = getDatabaseSync();
|
|
47
|
+
dbInstance = new Database(getDbPath());
|
|
48
|
+
|
|
49
|
+
dbInstance.exec(`
|
|
50
|
+
-- User profile: arbitrary key-value pairs
|
|
51
|
+
CREATE TABLE IF NOT EXISTS user_profile (
|
|
52
|
+
key TEXT PRIMARY KEY,
|
|
53
|
+
value TEXT,
|
|
54
|
+
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
|
55
|
+
);
|
|
56
|
+
|
|
57
|
+
-- Condensed summaries of past sessions
|
|
58
|
+
CREATE TABLE IF NOT EXISTS session_memories (
|
|
59
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
60
|
+
summary TEXT NOT NULL,
|
|
61
|
+
tags TEXT DEFAULT '',
|
|
62
|
+
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
-- Frequently used topics / commands
|
|
66
|
+
CREATE TABLE IF NOT EXISTS usage_patterns (
|
|
67
|
+
pattern TEXT PRIMARY KEY,
|
|
68
|
+
count INTEGER DEFAULT 1,
|
|
69
|
+
last_used DATETIME DEFAULT CURRENT_TIMESTAMP
|
|
70
|
+
);
|
|
71
|
+
|
|
72
|
+
-- Response Cache: For repetitive exact queries
|
|
73
|
+
CREATE TABLE IF NOT EXISTS response_cache (
|
|
74
|
+
query_hash TEXT PRIMARY KEY,
|
|
75
|
+
response TEXT NOT NULL,
|
|
76
|
+
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
|
77
|
+
);
|
|
78
|
+
`);
|
|
79
|
+
|
|
80
|
+
return dbInstance;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// ── Profile helpers ────────────────────────────────────────────────────────
|
|
84
|
+
function setProfile(key, value) {
|
|
85
|
+
try {
|
|
86
|
+
const db = getDb();
|
|
87
|
+
db.prepare(`
|
|
88
|
+
INSERT INTO user_profile (key, value, updated_at)
|
|
89
|
+
VALUES (?, ?, CURRENT_TIMESTAMP)
|
|
90
|
+
ON CONFLICT(key) DO UPDATE SET value = excluded.value, updated_at = CURRENT_TIMESTAMP
|
|
91
|
+
`).run(key, String(value));
|
|
92
|
+
} catch (err) {
|
|
93
|
+
console.error('[Memory] setProfile error:', err.message);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function getProfile(key, defaultValue = null) {
|
|
98
|
+
try {
|
|
99
|
+
const row = getDb().prepare('SELECT value FROM user_profile WHERE key = ?').get(key);
|
|
100
|
+
return row ? row.value : defaultValue;
|
|
101
|
+
} catch (_) {
|
|
102
|
+
return defaultValue;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function getAllProfile() {
|
|
107
|
+
try {
|
|
108
|
+
const rows = getDb().prepare('SELECT key, value FROM user_profile').all();
|
|
109
|
+
return Object.fromEntries(rows.map(r => [r.key, r.value]));
|
|
110
|
+
} catch (_) {
|
|
111
|
+
return {};
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// ── Session memory helpers ─────────────────────────────────────────────────
|
|
116
|
+
const MAX_SESSION_MEMORIES = 20; // keep last N summaries
|
|
117
|
+
|
|
118
|
+
function addSessionMemory(summary, tags = []) {
|
|
119
|
+
try {
|
|
120
|
+
const db = getDb();
|
|
121
|
+
db.prepare('INSERT INTO session_memories (summary, tags) VALUES (?, ?)').run(
|
|
122
|
+
summary.slice(0, 800), // cap length
|
|
123
|
+
tags.join(',')
|
|
124
|
+
);
|
|
125
|
+
// Prune oldest beyond limit
|
|
126
|
+
db.exec(`
|
|
127
|
+
DELETE FROM session_memories WHERE id NOT IN (
|
|
128
|
+
SELECT id FROM session_memories ORDER BY id DESC LIMIT ${MAX_SESSION_MEMORIES}
|
|
129
|
+
)
|
|
130
|
+
`);
|
|
131
|
+
} catch (err) {
|
|
132
|
+
console.error('[Memory] addSessionMemory error:', err.message);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function getRecentMemories(limit = 5) {
|
|
137
|
+
try {
|
|
138
|
+
return getDb()
|
|
139
|
+
.prepare('SELECT summary, tags, created_at FROM session_memories ORDER BY id DESC LIMIT ?')
|
|
140
|
+
.all(limit);
|
|
141
|
+
} catch (_) {
|
|
142
|
+
return [];
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// ── Usage pattern helpers ──────────────────────────────────────────────────
|
|
147
|
+
function recordPattern(pattern) {
|
|
148
|
+
try {
|
|
149
|
+
const db = getDb();
|
|
150
|
+
db.prepare(`
|
|
151
|
+
INSERT INTO usage_patterns (pattern, count, last_used)
|
|
152
|
+
VALUES (?, 1, CURRENT_TIMESTAMP)
|
|
153
|
+
ON CONFLICT(pattern) DO UPDATE
|
|
154
|
+
SET count = count + 1, last_used = CURRENT_TIMESTAMP
|
|
155
|
+
`).run(pattern.slice(0, 120));
|
|
156
|
+
} catch (_) {}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function getTopPatterns(limit = 8) {
|
|
160
|
+
try {
|
|
161
|
+
return getDb()
|
|
162
|
+
.prepare('SELECT pattern, count FROM usage_patterns ORDER BY count DESC, last_used DESC LIMIT ?')
|
|
163
|
+
.all(limit);
|
|
164
|
+
} catch (_) {
|
|
165
|
+
return [];
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// ── Simple keyword extractor (no external deps) ────────────────────────────
|
|
170
|
+
const STOP_WORDS = new Set([
|
|
171
|
+
'ที่', 'ให้', 'และ', 'ของ', 'กับ', 'ใน', 'บน', 'เป็น', 'อยู่', 'มี', 'ได้', 'the', 'a', 'an',
|
|
172
|
+
'is', 'are', 'was', 'were', 'it', 'in', 'on', 'at', 'for', 'to', 'of', 'with', 'and', 'or',
|
|
173
|
+
'this', 'that', 'i', 'you', 'me', 'my', 'your', 'can', 'do', 'be', 'will', 'please', 'how',
|
|
174
|
+
'what', 'which', 'when', 'where', 'why', 'help', 'want', 'need', 'make', 'create', 'get', 'run'
|
|
175
|
+
]);
|
|
176
|
+
|
|
177
|
+
function extractKeywords(text) {
|
|
178
|
+
return text
|
|
179
|
+
.toLowerCase()
|
|
180
|
+
.replace(/[^\w\u0E00-\u0E7F\s]/g, ' ')
|
|
181
|
+
.split(/\s+/)
|
|
182
|
+
.filter(w => w.length > 2 && !STOP_WORDS.has(w))
|
|
183
|
+
.slice(0, 6);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// ── Main public API ────────────────────────────────────────────────────────
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Called after every successful chat turn.
|
|
190
|
+
* Extracts patterns & infers preferences — runs async, non-blocking.
|
|
191
|
+
*/
|
|
192
|
+
function recordInteraction(userMessage, aiResponseText) {
|
|
193
|
+
try {
|
|
194
|
+
if (!userMessage || !aiResponseText) return;
|
|
195
|
+
|
|
196
|
+
// Extract keywords as usage patterns
|
|
197
|
+
const keywords = extractKeywords(userMessage);
|
|
198
|
+
keywords.forEach(kw => recordPattern(kw));
|
|
199
|
+
|
|
200
|
+
// Detect preferred language
|
|
201
|
+
const thaiRatio = (userMessage.match(/[\u0E00-\u0E7F]/g) || []).length / userMessage.length;
|
|
202
|
+
if (thaiRatio > 0.3) setProfile('preferred_language', 'thai');
|
|
203
|
+
else setProfile('preferred_language', 'english');
|
|
204
|
+
|
|
205
|
+
// Detect coding intent (update project activity)
|
|
206
|
+
const codingKeywords = ['code', 'fix', 'debug', 'function', 'class', 'import', 'script',
|
|
207
|
+
'แก้', 'เขียน', 'โค้ด', 'สคริปต์', 'ฟังก์ชัน'];
|
|
208
|
+
if (codingKeywords.some(k => userMessage.toLowerCase().includes(k))) {
|
|
209
|
+
const cwd = process.cwd();
|
|
210
|
+
if (cwd !== os.homedir()) {
|
|
211
|
+
setProfile('last_active_project', path.basename(cwd));
|
|
212
|
+
setProfile('last_active_project_path', cwd);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// Update interaction counter
|
|
217
|
+
const count = parseInt(getProfile('total_interactions', '0'), 10);
|
|
218
|
+
setProfile('total_interactions', String(count + 1));
|
|
219
|
+
setProfile('last_seen', new Date().toISOString());
|
|
220
|
+
} catch (err) {
|
|
221
|
+
console.error('[Memory] recordInteraction error:', err.message);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Saves a condensed summary of a completed conversation.
|
|
227
|
+
* Call this when user clears history or after N turns.
|
|
228
|
+
*/
|
|
229
|
+
function saveSessionSummary(summary, tags = []) {
|
|
230
|
+
if (!summary || summary.trim().length < 10) return;
|
|
231
|
+
addSessionMemory(summary.trim(), tags);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Returns a formatted context string to inject into the AI system prompt.
|
|
236
|
+
* Lightweight — no async calls.
|
|
237
|
+
*/
|
|
238
|
+
function getUserContext() {
|
|
239
|
+
try {
|
|
240
|
+
const profile = getAllProfile();
|
|
241
|
+
const patterns = getTopPatterns(6);
|
|
242
|
+
const memories = getRecentMemories(3);
|
|
243
|
+
|
|
244
|
+
const lines = ['\n\n[LONG-TERM USER CONTEXT — use this to personalize responses]'];
|
|
245
|
+
|
|
246
|
+
// Profile info
|
|
247
|
+
if (Object.keys(profile).length > 0) {
|
|
248
|
+
if (profile.preferred_language)
|
|
249
|
+
lines.push(`• Preferred language: ${profile.preferred_language}`);
|
|
250
|
+
if (profile.last_active_project)
|
|
251
|
+
lines.push(`• Last active project: ${profile.last_active_project} (${profile.last_active_project_path || ''})`);
|
|
252
|
+
if (profile.total_interactions)
|
|
253
|
+
lines.push(`• Total interactions with Mint: ${profile.total_interactions}`);
|
|
254
|
+
if (profile.last_seen) {
|
|
255
|
+
const d = new Date(profile.last_seen);
|
|
256
|
+
lines.push(`• Last session: ${d.toLocaleDateString('th-TH')} ${d.toLocaleTimeString('th-TH', { hour: '2-digit', minute: '2-digit' })}`);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// Usage patterns
|
|
261
|
+
if (patterns.length > 0) {
|
|
262
|
+
const topTopics = patterns.map(p => p.pattern).join(', ');
|
|
263
|
+
lines.push(`• Frequent topics/tools: ${topTopics}`);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// Past session memories
|
|
267
|
+
if (memories.length > 0) {
|
|
268
|
+
lines.push('\nRecent session summaries:');
|
|
269
|
+
memories.forEach((m, i) => lines.push(` ${i + 1}. ${m.summary}`));
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
if (lines.length === 1) return ''; // nothing to add
|
|
273
|
+
lines.push('[END USER CONTEXT]\n');
|
|
274
|
+
return lines.join('\n');
|
|
275
|
+
} catch (err) {
|
|
276
|
+
console.error('[Memory] getUserContext error:', err.message);
|
|
277
|
+
return '';
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// ── Response Cache helpers ────────────────────────────────────────────────
|
|
282
|
+
function getCachedResponse(query) {
|
|
283
|
+
try {
|
|
284
|
+
const hash = crypto.createHash('md5').update(query.trim().toLowerCase()).digest('hex');
|
|
285
|
+
const row = getDb().prepare('SELECT response, created_at FROM response_cache WHERE query_hash = ?').get(hash);
|
|
286
|
+
if (row) {
|
|
287
|
+
// Optional: check TTL (e.g., 24 hours)
|
|
288
|
+
const age = Date.now() - new Date(row.created_at).getTime();
|
|
289
|
+
if (age < 24 * 60 * 60 * 1000) {
|
|
290
|
+
return JSON.parse(row.response);
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
} catch (_) {}
|
|
294
|
+
return null;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
function cacheResponse(query, responseObj) {
|
|
298
|
+
try {
|
|
299
|
+
const hash = crypto.createHash('md5').update(query.trim().toLowerCase()).digest('hex');
|
|
300
|
+
getDb().prepare(`
|
|
301
|
+
INSERT INTO response_cache (query_hash, response, created_at)
|
|
302
|
+
VALUES (?, ?, CURRENT_TIMESTAMP)
|
|
303
|
+
ON CONFLICT(query_hash) DO UPDATE SET response = excluded.response, created_at = CURRENT_TIMESTAMP
|
|
304
|
+
`).run(hash, JSON.stringify(responseObj));
|
|
305
|
+
} catch (_) {}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
module.exports = {
|
|
309
|
+
recordInteraction,
|
|
310
|
+
saveSessionSummary,
|
|
311
|
+
getUserContext,
|
|
312
|
+
setProfile,
|
|
313
|
+
getProfile,
|
|
314
|
+
getTopPatterns,
|
|
315
|
+
getRecentMemories,
|
|
316
|
+
getCachedResponse,
|
|
317
|
+
cacheResponse
|
|
318
|
+
};
|
|
@@ -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
|
}
|