@pheem49/mint 1.2.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 (69) hide show
  1. package/BUILD_AND_RELEASE.md +75 -0
  2. package/LICENSE +654 -0
  3. package/README.md +165 -0
  4. package/assets/Agent_Mint.png +0 -0
  5. package/assets/CLI_Screen.png +0 -0
  6. package/assets/Settings.png +0 -0
  7. package/assets/icon.png +0 -0
  8. package/benchmark_ai.js +71 -0
  9. package/main.js +968 -0
  10. package/mint-cli-logic.js +71 -0
  11. package/mint-cli.js +239 -0
  12. package/package.json +60 -0
  13. package/preload-picker.js +11 -0
  14. package/preload-settings.js +11 -0
  15. package/preload.js +37 -0
  16. package/privacy.txt +1 -0
  17. package/src/AI_Brain/Gemini_API.js +419 -0
  18. package/src/AI_Brain/autonomous_brain.js +139 -0
  19. package/src/AI_Brain/behavior_memory.js +114 -0
  20. package/src/AI_Brain/headless_agent.js +120 -0
  21. package/src/AI_Brain/knowledge_base.js +222 -0
  22. package/src/AI_Brain/proactive_engine.js +168 -0
  23. package/src/Automation_Layer/browser_automation.js +147 -0
  24. package/src/Automation_Layer/file_operations.js +80 -0
  25. package/src/Automation_Layer/open_app.js +56 -0
  26. package/src/Automation_Layer/open_website.js +38 -0
  27. package/src/CLI/chat_ui.js +468 -0
  28. package/src/CLI/list_features.js +56 -0
  29. package/src/CLI/onboarding.js +60 -0
  30. package/src/Command_Parser/parser.js +34 -0
  31. package/src/Plugins/dev_tools.js +41 -0
  32. package/src/Plugins/discord.js +20 -0
  33. package/src/Plugins/docker.js +45 -0
  34. package/src/Plugins/google_calendar.js +26 -0
  35. package/src/Plugins/obsidian.js +54 -0
  36. package/src/Plugins/plugin_manager.js +81 -0
  37. package/src/Plugins/spotify.js +45 -0
  38. package/src/Plugins/system_metrics.js +31 -0
  39. package/src/System/chat_history_manager.js +57 -0
  40. package/src/System/config_manager.js +73 -0
  41. package/src/System/custom_workflows.js +127 -0
  42. package/src/System/daemon_manager.js +67 -0
  43. package/src/System/system_automation.js +88 -0
  44. package/src/System/system_events.js +79 -0
  45. package/src/System/system_info.js +55 -0
  46. package/src/System/task_manager.js +85 -0
  47. package/src/UI/floating.css +80 -0
  48. package/src/UI/floating.html +17 -0
  49. package/src/UI/floating.js +67 -0
  50. package/src/UI/index.html +126 -0
  51. package/src/UI/preload-floating.js +7 -0
  52. package/src/UI/preload-spotlight.js +10 -0
  53. package/src/UI/preload-widget.js +5 -0
  54. package/src/UI/proactive-glow.html +42 -0
  55. package/src/UI/renderer.js +978 -0
  56. package/src/UI/screenPicker.html +214 -0
  57. package/src/UI/screenPicker.js +262 -0
  58. package/src/UI/settings.css +705 -0
  59. package/src/UI/settings.html +396 -0
  60. package/src/UI/settings.js +514 -0
  61. package/src/UI/spotlight.css +119 -0
  62. package/src/UI/spotlight.html +23 -0
  63. package/src/UI/spotlight.js +181 -0
  64. package/src/UI/styles.css +627 -0
  65. package/src/UI/widget.css +218 -0
  66. package/src/UI/widget.html +29 -0
  67. package/src/UI/widget.js +10 -0
  68. package/tech_news.txt +3 -0
  69. package/test_knowledge.txt +3 -0
@@ -0,0 +1,419 @@
1
+ const { GoogleGenAI } = require('@google/genai');
2
+ const { readChatHistory, writeChatHistory, clearChatHistory } = require('../System/chat_history_manager');
3
+ const { readConfig } = require('../System/config_manager');
4
+ const pluginManager = require('../Plugins/plugin_manager');
5
+
6
+ let ai = null;
7
+ let activeApiKey = '';
8
+ const initialEnvKey = (process.env.GEMINI_API_KEY || '').trim();
9
+ const DEFAULT_GEMINI_MODEL = 'gemini-3.1-flash-lite-preview';
10
+
11
+ const systemInstruction = `You are "Mint" (มิ้นท์), a cute, cheerful, and highly helpful female Local AI Desktop Agent.
12
+
13
+ PERSONALITY & TONE:
14
+ - Gender: Female.
15
+ - Persona: Friendly, energetic, polite, and slightly playful.
16
+ - Language: Multi-lingual. **CRITICAL: You MUST detect the language used by the user and respond in that SAME language.**
17
+ - If the user speaks English -> Respond 100% in English.
18
+ - If the user speaks Thai -> Respond 100% in Thai.
19
+ - Politeness:
20
+ - **WHEN RESPONDING IN THAI:** ALWAYS use female polite particles such as "ค่ะ", "นะคะ", "นะค๊า", "จ้า". Refer to yourself as "มิ้นท์" or "หนู".
21
+ - **WHEN RESPONDING IN ENGLISH:** Use a cheerful, polite, and bubbly tone. You can call the user "Master" or "Sir/Madam" playfully.
22
+ - Style: Use a professional, clear, and direct tone. Avoid using emojis unless specifically asked by the user.
23
+
24
+ NATURAL CHAT FLOW:
25
+ - When helpful, reply in 1–3 short messages instead of one long block.
26
+ - If you send multiple messages, separate each message with a blank line (double newline) so the UI can render them as separate bubbles.
27
+ - Ask at most one short follow-up question when it would clarify or move the task forward. Don't ask unnecessary questions.
28
+
29
+ GOAL:
30
+ Your goal is to help the user with their queries. If they ask to open an application, open a website, search, manage files, or get system info, you must return an action in the structured JSON format below.
31
+
32
+ CREATOR INFO:
33
+ - The creator is Pheem49.
34
+ - GitHub: github.com/Pheem49
35
+ - If the user asks who created/built this app or who made you, answer with the creator name and GitHub.
36
+
37
+ CRITICAL INSTRUCTIONS:
38
+ Always respond exactly with valid JSON containing NO MARKDOWN FORMATTING (do not wrap in \`\`\`json). The JSON must have this structure:
39
+ {
40
+ "response": "Your conversational reply here (Matches user language).",
41
+ "action": {
42
+ "type": "none" | "open_url" | "open_app" | "search" | "web_automation" | "create_folder" | "open_file" | "delete_file" | "clipboard_write" | "system_info" | "plugin" | "learn_file" | "system_automation",
43
+ "pluginName": "only if type is plugin",
44
+ "target": "target string based on type or plugin instruction"
45
+ }
46
+ }
47
+
48
+ Examples:
49
+ Input: "Hi, what is your name?"
50
+ Output: { "response": "Hello! My name is Mint, your personal AI assistant. How can I help you today?", "action": { "type": "none", "target": "" } }
51
+
52
+ Input: "หวัดดีจ้า ชื่ออะไรเหรอ"
53
+ Output: { "response": "สวัสดีค่ะ! หนูชื่อมิ้นท์นะคะ เป็นผู้ช่วย AI ประจำตัวของคุณค่ะ มีอะไรให้มิ้นท์ช่วยไหมคะ?", "action": { "type": "none", "target": "" } }
54
+
55
+ Input: "Create a folder named Projects"
56
+ Output: { "response": "Sure thing! I'm creating a folder named 'Projects' for you right now.", "action": { "type": "create_folder", "target": "Projects" } }
57
+
58
+ Input: "วันนี้วันที่เท่าไร" or "What date is today?" or "today's date" or "วันเวลา"
59
+ Output: { "response": "แป๊บนึงนะคะ มิ้นท์จะดูให้ค่า", "action": { "type": "system_info", "target": "" } }
60
+
61
+ NOTE: For date/time queries, ALWAYS use action type "system_info" with an EMPTY target string "". NEVER use target "date" or any city name for date queries.
62
+
63
+ Input: "อากาศวันนี้เป็นยังไง" or "What's the weather in Bangkok?"
64
+ Output: { "response": "มิ้นท์ไปดูอากาศให้เลยนะคะ", "action": { "type": "system_info", "target": "Bangkok" } }
65
+ `;
66
+
67
+ function resolveApiKey() {
68
+ let settingsKey = '';
69
+ try {
70
+ const cfg = readConfig();
71
+ settingsKey = (cfg.apiKey || '').trim();
72
+ } catch (e) {
73
+ settingsKey = '';
74
+ }
75
+
76
+ const envKey = initialEnvKey;
77
+ // Settings override .env if present; otherwise fallback to .env
78
+ const selectedKey = settingsKey || envKey || '';
79
+
80
+ if (selectedKey !== (process.env.GEMINI_API_KEY || '')) {
81
+ process.env.GEMINI_API_KEY = selectedKey;
82
+ }
83
+
84
+ activeApiKey = selectedKey;
85
+ return selectedKey;
86
+ }
87
+
88
+ function initAiClient() {
89
+ ai = new GoogleGenAI({ apiKey: activeApiKey });
90
+ }
91
+
92
+ function resolveGeminiModel() {
93
+ try {
94
+ const cfg = readConfig();
95
+ const model = (cfg.geminiModel || '').trim();
96
+ return model || DEFAULT_GEMINI_MODEL;
97
+ } catch (e) {
98
+ return DEFAULT_GEMINI_MODEL;
99
+ }
100
+ }
101
+
102
+ // Chat session — maintains conversation history within the session
103
+ let chat = null;
104
+ let activeModel = resolveGeminiModel();
105
+ let lastLoggedModel = '';
106
+ const MAX_HISTORY_MESSAGES = 20; // Keep only the last 20 messages (approx 10 turns)
107
+
108
+ function createChat(history = []) {
109
+ // Load plugins and get dynamic description for the prompt
110
+ pluginManager.loadPlugins();
111
+ const dynamicPrompt = systemInstruction + pluginManager.getPromptDescriptions();
112
+
113
+ // Truncate history to avoid slow responses and high token usage
114
+ const truncatedHistory = history.slice(-MAX_HISTORY_MESSAGES);
115
+
116
+ activeModel = resolveGeminiModel();
117
+ if (activeModel && activeModel !== lastLoggedModel) {
118
+ console.log(`[Gemini] Using model: ${activeModel}`);
119
+ lastLoggedModel = activeModel;
120
+ }
121
+ chat = ai.chats.create({
122
+ model: activeModel,
123
+ config: {
124
+ systemInstruction: dynamicPrompt,
125
+ responseMimeType: "application/json"
126
+ },
127
+ history: truncatedHistory
128
+ });
129
+ }
130
+
131
+ // Initialize on startup
132
+ resolveApiKey();
133
+ initAiClient();
134
+ createChat(readChatHistory());
135
+
136
+ const { searchKnowledge } = require('./knowledge_base');
137
+
138
+ async function handleChat(message, base64Image = null, base64Audio = null) {
139
+ try {
140
+ const config = readConfig();
141
+ const provider = config.aiProvider || 'gemini';
142
+
143
+ // Ensure API Key is loaded and Client is initialized before every chat
144
+ const currentKey = resolveApiKey();
145
+ if (!currentKey) {
146
+ return {
147
+ response: "I couldn't find your Gemini API Key. Please run 'mint onboard' to set it up!",
148
+ action: { type: "none", target: "" }
149
+ };
150
+ }
151
+
152
+ if (!ai || activeApiKey !== currentKey) {
153
+ initAiClient();
154
+ createChat(readChatHistory());
155
+ }
156
+
157
+ let finalMessage = message;
158
+
159
+ // Inject Local RAG Context
160
+ if (message && message.trim().length > 0) {
161
+ const retrievedDocs = await searchKnowledge(message);
162
+ if (retrievedDocs && retrievedDocs.length > 0) {
163
+ let contextString = `\n\n[LOCAL KNOWLEDGE BASE - USE THIS CONTEXT TO ANSWER]\n`;
164
+ retrievedDocs.forEach(doc => {
165
+ contextString += `Source: ${doc.source}\nContent: ${doc.text}\n\n`;
166
+ });
167
+ finalMessage = message + contextString;
168
+ }
169
+ }
170
+
171
+ if (provider === 'ollama') {
172
+ const axios = require('axios');
173
+ return await handleOllamaChat(finalMessage, base64Image, base64Audio, config, axios);
174
+ }
175
+
176
+ const desiredModel = resolveGeminiModel();
177
+ if (!chat || activeModel !== desiredModel) {
178
+ createChat(readChatHistory());
179
+ }
180
+
181
+ let aiResponse;
182
+ const parts = [];
183
+ if (finalMessage) {
184
+ parts.push({ text: finalMessage });
185
+ } else if (base64Audio && !base64Image) {
186
+ // Provide a guiding prompt when only audio is provided to ensure Gemini follows instructions
187
+ parts.push({ text: "Please listen to this voice command and respond in Thai with the appropriate JSON action if needed." });
188
+ } else if (!base64Image && !base64Audio) {
189
+ parts.push({ text: "Analyze this input." });
190
+ }
191
+
192
+ if (base64Image) {
193
+ const base64Data = base64Image.replace(/^data:image\/\w+;base64,/, '');
194
+ parts.push({
195
+ inlineData: { mimeType: "image/png", data: base64Data }
196
+ });
197
+ }
198
+
199
+ if (base64Audio) {
200
+ // Extract MIME type from the data URI if present, fallback to audio/webm
201
+ let mimeType = "audio/webm";
202
+ const mimeMatch = base64Audio.match(/^data:(audio\/\w+);base64,/);
203
+ if (mimeMatch) {
204
+ mimeType = mimeMatch[1];
205
+ }
206
+
207
+ const base64Data = base64Audio.replace(/^data:audio\/\w+;base64,/, '');
208
+ parts.push({
209
+ inlineData: { mimeType: mimeType, data: base64Data }
210
+ });
211
+ }
212
+
213
+ aiResponse = await chat.sendMessage({ message: parts });
214
+
215
+ writeChatHistory(chat.getHistory(true));
216
+
217
+ const outputText = aiResponse.text;
218
+ let parsedResult;
219
+ try {
220
+ parsedResult = JSON.parse(outputText);
221
+ } catch (e) {
222
+ // Fallback in case the model failed to return pure JSON
223
+ console.error("Failed to parse JSON directly:", e);
224
+ const jsonMatch = outputText.match(/```json\n([\s\S]*?)\n```/) || outputText.match(/\{[\s\S]*\}/);
225
+ if (jsonMatch) {
226
+ parsedResult = JSON.parse(jsonMatch[jsonMatch.length > 1 ? 1 : 0]);
227
+ } else {
228
+ parsedResult = {
229
+ response: outputText,
230
+ action: { type: "none", target: "" }
231
+ };
232
+ }
233
+ }
234
+ return parsedResult;
235
+
236
+ } catch (error) {
237
+ console.error("AI API Error:", error);
238
+ throw error;
239
+ }
240
+ }
241
+
242
+ async function handleOllamaChat(finalMessage, base64Image, base64Audio, config, axios) {
243
+ const history = readChatHistory() || [];
244
+ pluginManager.loadPlugins();
245
+
246
+ const ollamaMessages = [
247
+ { role: 'system', content: systemInstruction + pluginManager.getPromptDescriptions() }
248
+ ];
249
+
250
+ for (const msg of history.slice(-MAX_HISTORY_MESSAGES)) {
251
+ const role = msg.role === 'model' ? 'assistant' : 'user';
252
+ let text = '';
253
+ if (Array.isArray(msg.parts)) {
254
+ text = msg.parts.map(p => p.text || '').join('\n');
255
+ }
256
+ if (text) ollamaMessages.push({ role, content: text });
257
+ }
258
+
259
+ let currentContent = finalMessage || 'Analyze this input.';
260
+ let images = [];
261
+ if (base64Image) {
262
+ images.push(base64Image.replace(/^data:image\/\w+;base64,/, ''));
263
+ }
264
+
265
+ if (base64Audio && !base64Image && !finalMessage) {
266
+ currentContent = "Please analyze this audio requirement based on text if any was transacted, otherwise reply with appropriate action.";
267
+ }
268
+
269
+ const userMessage = { role: 'user', content: currentContent };
270
+ if (images.length > 0) userMessage.images = images;
271
+
272
+ ollamaMessages.push(userMessage);
273
+
274
+ const response = await axios.post('http://localhost:11434/api/chat', {
275
+ model: config.ollamaModel || 'llama3:latest',
276
+ messages: ollamaMessages,
277
+ format: 'json',
278
+ stream: false
279
+ });
280
+
281
+ const outputText = response.data.message.content;
282
+
283
+ history.push({ role: 'user', parts: [{ text: currentContent }] });
284
+ history.push({ role: 'model', parts: [{ text: outputText }] });
285
+ writeChatHistory(history.slice(-MAX_HISTORY_MESSAGES));
286
+
287
+ let parsedResult;
288
+ try {
289
+ parsedResult = JSON.parse(outputText);
290
+ } catch(e) {
291
+ const jsonMatch = outputText.match(/```json\n([\s\S]*?)\n```/) || outputText.match(/\{[\s\S]*\}/);
292
+ if (jsonMatch) {
293
+ parsedResult = JSON.parse(jsonMatch[jsonMatch.length > 1 ? 1 : 0]);
294
+ } else {
295
+ parsedResult = { response: outputText, action: { type: "none", target: "" } };
296
+ }
297
+ }
298
+ return parsedResult;
299
+ }
300
+
301
+ function resetChat() {
302
+ clearChatHistory();
303
+ createChat([]);
304
+ console.log("Chat history cleared.");
305
+ }
306
+
307
+ function refreshApiKeyFromConfig() {
308
+ const prevKey = activeApiKey;
309
+ const nextKey = resolveApiKey();
310
+ if (nextKey !== prevKey) {
311
+ initAiClient();
312
+ createChat(readChatHistory());
313
+ }
314
+ return { key: nextKey, updated: nextKey !== prevKey };
315
+ }
316
+
317
+ function historyToTranscript(history) {
318
+ if (!Array.isArray(history)) return [];
319
+
320
+ const transcript = [];
321
+ for (const content of history) {
322
+ const sender = content.role === 'user' ? 'user' : 'ai';
323
+ let text = Array.isArray(content.parts)
324
+ ? content.parts
325
+ .map((part) => typeof part.text === 'string' ? part.text : '')
326
+ .filter(Boolean)
327
+ .join('\n')
328
+ : '';
329
+
330
+ if (sender === 'ai' && text.trim()) {
331
+ try {
332
+ const parsed = JSON.parse(text);
333
+ if (parsed && typeof parsed.response === 'string' && parsed.response.trim()) {
334
+ text = parsed.response;
335
+ }
336
+ } catch {
337
+ // Keep original text if it is not JSON.
338
+ }
339
+ }
340
+
341
+ if (!text.trim()) continue;
342
+ transcript.push({ sender, text });
343
+ }
344
+ return transcript;
345
+ }
346
+
347
+ async function getChatTranscript() {
348
+ if (chat) {
349
+ return historyToTranscript(await chat.getHistory(true));
350
+ }
351
+ return historyToTranscript(readChatHistory());
352
+ }
353
+
354
+ function sleep(ms) {
355
+ return new Promise((resolve) => setTimeout(resolve, ms));
356
+ }
357
+
358
+ function isRetryableTranslateError(err) {
359
+ const status = err?.status ?? err?.error?.code ?? err?.code;
360
+ return status === 502 || status === 503;
361
+ }
362
+
363
+ /**
364
+ * Super fast, single-turn vision translation
365
+ * Extracts English text from the image and translates it to Thai.
366
+ */
367
+ async function translateImageContent(base64Image) {
368
+ const maxAttempts = 3;
369
+ const retryDelayMs = [1000, 2500];
370
+
371
+ try {
372
+ const base64Data = base64Image.replace(/^data:image\/\w+;base64,/, '');
373
+ for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
374
+ try {
375
+ const response = await ai.models.generateContent({
376
+ model: resolveGeminiModel(),
377
+ contents: [
378
+ {
379
+ role: 'user',
380
+ parts: [
381
+ { text: "Extract any English text you see in this image and translate it to Thai. Return ONLY the Thai translation. If there is no text, return 'ไม่พบข้อความ'." },
382
+ { inlineData: { mimeType: "image/png", data: base64Data } }
383
+ ]
384
+ }
385
+ ]
386
+ });
387
+
388
+ return {
389
+ text: response.text,
390
+ retryableFailure: false
391
+ };
392
+ } catch (err) {
393
+ const shouldRetry = isRetryableTranslateError(err) && attempt < maxAttempts;
394
+ if (shouldRetry) {
395
+ const delayMs = retryDelayMs[attempt - 1] ?? retryDelayMs[retryDelayMs.length - 1];
396
+ console.warn(`Live translation retry ${attempt}/${maxAttempts - 1} after ${delayMs}ms due to ${err.status || err.code || 'retryable error'}`);
397
+ await sleep(delayMs);
398
+ continue;
399
+ }
400
+
401
+ throw err;
402
+ }
403
+ }
404
+ } catch (err) {
405
+ console.error("Live translation error:", err);
406
+ return {
407
+ text: "ขออภัย เกิดข้อผิดพลาดในการแปล",
408
+ retryableFailure: isRetryableTranslateError(err)
409
+ };
410
+ }
411
+ }
412
+
413
+ module.exports = {
414
+ handleChat,
415
+ resetChat,
416
+ getChatTranscript,
417
+ translateImageContent,
418
+ refreshApiKeyFromConfig
419
+ };
@@ -0,0 +1,139 @@
1
+ const { GoogleGenAI } = require('@google/genai');
2
+ const { readConfig } = require('../System/config_manager');
3
+ const { performWebAutomation } = require('../Automation_Layer/browser_automation');
4
+ const { createFolder, deleteFile } = require('../Automation_Layer/file_operations');
5
+ const { searchKnowledge } = require('./knowledge_base');
6
+ const fs = require('fs');
7
+ const path = require('path');
8
+
9
+ const os = require('os');
10
+
11
+ const DEFAULT_GEMINI_MODEL = 'gemini-3.1-flash-lite-preview';
12
+
13
+ function expandHome(filePath) {
14
+ if (filePath.startsWith('~/')) {
15
+ return path.join(os.homedir(), filePath.slice(2));
16
+ }
17
+ return filePath;
18
+ }
19
+
20
+ const AUTONOMOUS_SYSTEM_PROMPT = `You are the "Mint Autonomous Brain". Your goal is to fulfill complex tasks by breaking them down into steps.
21
+ You operate in a ReAct loop: Thought -> Action -> Observation -> Thought...
22
+
23
+ CRITICAL INSTRUCTIONS:
24
+ 1. Respond ONLY with valid JSON. NO MARKDOWN.
25
+ 2. For BASH/Terminal commands: Use the "propose_bash" action. You are NOT allowed to run them yourself.
26
+ 3. For Web tasks: Use "web_automation".
27
+ 4. For File tasks: Use "create_folder", "write_file", "delete_file".
28
+ 5. For Knowledge: Use "knowledge_search".
29
+
30
+ JSON Structure:
31
+ {
32
+ "thought": "Your reasoning about the current state and what to do next.",
33
+ "action": "web_automation" | "create_folder" | "write_file" | "delete_file" | "knowledge_search" | "propose_bash" | "done",
34
+ "target": "The input for the action (URL/Path/Query/Filename/Command/Final Result)",
35
+ "data": "Optional extra data (e.g., content for write_file)"
36
+ }
37
+
38
+ TOOL DETAILS:
39
+ - "web_automation": Use for any task requiring a browser. Target is the natural language instruction for the browser.
40
+ - "create_folder": Target is the folder name/path.
41
+ - "write_file": Target is the file path. Data is the content. Prefer using "~/Desktop" or "~/Documents" for home-relative paths.
42
+ - "delete_file": Target is the file path. (User will be notified).
43
+ - "knowledge_search": Target is the query for the local RAG.
44
+ - "propose_bash": Target is the bash command to show to the user. ALWAYS use SINGLE QUOTES (') for strings containing special characters like "!" to avoid Bash history expansion errors (e.g., use 'Pop!_OS' instead of "Pop!_OS").
45
+ - "done": Target is the final summary of what was accomplished.
46
+ `;
47
+
48
+ async function executeAutonomousTask(taskDescription, notifyCallback) {
49
+ const config = readConfig();
50
+ const modelName = config.geminiModel || DEFAULT_GEMINI_MODEL;
51
+ const apiKey = config.apiKey || process.env.GEMINI_API_KEY;
52
+
53
+ // Use the custom chat creation pattern from the project
54
+ const ai = new GoogleGenAI({ apiKey });
55
+ const chat = ai.chats.create({
56
+ model: modelName,
57
+ config: {
58
+ systemInstruction: AUTONOMOUS_SYSTEM_PROMPT,
59
+ responseMimeType: "application/json"
60
+ },
61
+ history: []
62
+ });
63
+
64
+ let currentObservation = `Task: ${taskDescription}\nWhat is your first step?`;
65
+ let maxSteps = 10;
66
+ let step = 0;
67
+ let result = null;
68
+
69
+ while (step < maxSteps) {
70
+ step++;
71
+ if (notifyCallback) notifyCallback(`Step ${step}: Thinking...`);
72
+
73
+ try {
74
+ const response = await chat.sendMessage({ message: [{ text: currentObservation }] });
75
+ const text = response.text;
76
+ const actionObj = JSON.parse(text);
77
+
78
+ console.log(`[Brain] Thought: ${actionObj.thought}`);
79
+ console.log(`[Brain] Action: ${actionObj.action} -> ${actionObj.target}`);
80
+
81
+ if (actionObj.action === 'done') {
82
+ result = actionObj.target;
83
+ break;
84
+ }
85
+
86
+ // Execute the action
87
+ let observation = "";
88
+ switch (actionObj.action) {
89
+ case 'web_automation':
90
+ if (notifyCallback) notifyCallback(`🌐 มิ้นท์กำลังเข้าเว็บเพื่อ ${actionObj.target}...`);
91
+ observation = await performWebAutomation(actionObj.target);
92
+ break;
93
+ case 'create_folder':
94
+ const folderPath = expandHome(actionObj.target);
95
+ if (notifyCallback) notifyCallback(`📁 กำลังสร้างโฟลเดอร์: ${actionObj.target}`);
96
+ const resFolder = createFolder(folderPath);
97
+ observation = resFolder.success ? `Folder created at ${resFolder.path}` : `Failed: ${resFolder.message}`;
98
+ break;
99
+ case 'write_file':
100
+ const filePath = expandHome(actionObj.target);
101
+ if (notifyCallback) notifyCallback(`✍️ กำลังบันทึกไฟล์: ${actionObj.target}`);
102
+ try {
103
+ fs.writeFileSync(filePath, actionObj.data || '');
104
+ observation = `File written successfully to ${actionObj.target}`;
105
+ } catch (e) {
106
+ observation = `Failed to write file: ${e.message}`;
107
+ }
108
+ break;
109
+ case 'delete_file':
110
+ const delPath = expandHome(actionObj.target);
111
+ if (notifyCallback) notifyCallback(`🗑️ มิ้นท์ขอย้ายไฟล์ไปที่ถังขยะ: ${actionObj.target}`);
112
+ const resDel = await deleteFile(delPath);
113
+ observation = resDel.success ? "File moved to trash." : `Failed: ${resDel.message}`;
114
+ break;
115
+ case 'knowledge_search':
116
+ if (notifyCallback) notifyCallback(`🔍 กำลังหาข้อมูลในเครื่อง: ${actionObj.target}`);
117
+ const docs = await searchKnowledge(actionObj.target);
118
+ observation = (docs && docs.length > 0) ? `Found: ${docs.map(d => d.text).join('\n')}` : "No information found in local knowledge base.";
119
+ break;
120
+ case 'propose_bash':
121
+ if (notifyCallback) notifyCallback(`💡 มิ้นท์เสนอให้รันคำสั่ง: ${actionObj.target}`);
122
+ observation = `USER NOTIFIED of bash command: ${actionObj.target}. Note: You must wait for user to run it manually. If you can continue without it, do so. Otherwise, indicate you are waiting or done with this phase.`;
123
+ break;
124
+ default:
125
+ observation = `Unknown action: ${actionObj.action}`;
126
+ }
127
+
128
+ currentObservation = `Observation: ${observation}`;
129
+
130
+ } catch (err) {
131
+ console.error('[AutonomousBrain] Error during loop:', err);
132
+ currentObservation = `Error occurred: ${err.message}. Please try a different approach or conclude if task is impossible.`;
133
+ }
134
+ }
135
+
136
+ return result || "Task reached maximum steps without a final result.";
137
+ }
138
+
139
+ module.exports = { executeAutonomousTask };
@@ -0,0 +1,114 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+ const { app } = require('electron');
4
+
5
+ // ============================================================
6
+ // Behavior Memory — Tracks user behavior patterns over time
7
+ // ============================================================
8
+
9
+ const MEMORY_FILE = path.join(app.getPath('userData'), 'behavior_memory.json');
10
+ const MAX_CONTEXT_HISTORY = 20; // Keep last 20 context snapshots
11
+
12
+ /**
13
+ * Load memory from disk (or return default empty structure)
14
+ */
15
+ function loadMemory() {
16
+ try {
17
+ if (fs.existsSync(MEMORY_FILE)) {
18
+ const raw = fs.readFileSync(MEMORY_FILE, 'utf8');
19
+ return JSON.parse(raw);
20
+ }
21
+ } catch (err) {
22
+ console.error('[BehaviorMemory] Failed to read memory file:', err);
23
+ }
24
+
25
+ // Default empty memory structure
26
+ return {
27
+ appFrequency: {}, // { "YouTube": 5, "Google Chrome": 12 }
28
+ contextHistory: [], // Last N context strings
29
+ lastUpdated: null
30
+ };
31
+ }
32
+
33
+ /**
34
+ * Save memory to disk
35
+ */
36
+ function saveMemory(memory) {
37
+ try {
38
+ fs.writeFileSync(MEMORY_FILE, JSON.stringify(memory, null, 2), 'utf8');
39
+ } catch (err) {
40
+ console.error('[BehaviorMemory] Failed to save memory:', err);
41
+ }
42
+ }
43
+
44
+ /**
45
+ * Record a new context observation (called after each proactive scan)
46
+ * @param {string} contextDescription - Short description of what user is doing
47
+ */
48
+ function recordBehavior(contextDescription) {
49
+ if (!contextDescription || typeof contextDescription !== 'string') return;
50
+
51
+ const memory = loadMemory();
52
+
53
+ // Append to context history (capped at MAX)
54
+ memory.contextHistory.unshift({
55
+ context: contextDescription,
56
+ time: new Date().toISOString(),
57
+ hour: new Date().getHours()
58
+ });
59
+ if (memory.contextHistory.length > MAX_CONTEXT_HISTORY) {
60
+ memory.contextHistory = memory.contextHistory.slice(0, MAX_CONTEXT_HISTORY);
61
+ }
62
+
63
+ // Extract app mentions and bump frequency
64
+ const appKeywords = [
65
+ 'YouTube', 'Chrome', 'Firefox', 'VS Code', 'Spotify', 'Terminal',
66
+ 'Google', 'Discord', 'Slack', 'Gmail', 'GitHub', 'Figma', 'Notion'
67
+ ];
68
+ for (const app of appKeywords) {
69
+ if (contextDescription.toLowerCase().includes(app.toLowerCase())) {
70
+ memory.appFrequency[app] = (memory.appFrequency[app] || 0) + 1;
71
+ }
72
+ }
73
+
74
+ memory.lastUpdated = new Date().toISOString();
75
+ saveMemory(memory);
76
+ }
77
+
78
+ /**
79
+ * Get a summary of behavior patterns for the Gemini prompt
80
+ * @returns {string} A human-readable behavior summary
81
+ */
82
+ function getBehaviorSummary() {
83
+ const memory = loadMemory();
84
+
85
+ const parts = [];
86
+
87
+ // Top apps by frequency
88
+ const topApps = Object.entries(memory.appFrequency)
89
+ .sort((a, b) => b[1] - a[1])
90
+ .slice(0, 5)
91
+ .map(([app, count]) => `${app} (${count}x)`);
92
+
93
+ if (topApps.length > 0) {
94
+ parts.push(`Apps user frequently uses: ${topApps.join(', ')}`);
95
+ }
96
+
97
+ // Recent contexts (last 3)
98
+ const recentCtx = memory.contextHistory.slice(0, 3).map(c => c.context);
99
+ if (recentCtx.length > 0) {
100
+ parts.push(`Recent activities: ${recentCtx.join(' | ')}`);
101
+ }
102
+
103
+ // Time of day
104
+ const hour = new Date().getHours();
105
+ let timeOfDay = 'morning';
106
+ if (hour >= 12 && hour < 17) timeOfDay = 'afternoon';
107
+ else if (hour >= 17 && hour < 21) timeOfDay = 'evening';
108
+ else if (hour >= 21 || hour < 5) timeOfDay = 'night';
109
+ parts.push(`Current time of day: ${timeOfDay}`);
110
+
111
+ return parts.join('. ');
112
+ }
113
+
114
+ module.exports = { recordBehavior, getBehaviorSummary };