@pheem49/mint 1.5.5 → 1.6.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 (222) hide show
  1. package/.codex +0 -0
  2. package/.github/FUNDING.yml +2 -0
  3. package/.github/workflows/ci.yml +45 -0
  4. package/.github/workflows/release.yml +79 -0
  5. package/Cargo.lock +5792 -0
  6. package/Cargo.toml +32 -0
  7. package/README.md +387 -353
  8. package/assets/icon.png +0 -0
  9. package/bin/mint +0 -0
  10. package/crates/mint-cli/Cargo.toml +23 -0
  11. package/crates/mint-cli/src/agent.rs +851 -0
  12. package/crates/mint-cli/src/gmail.rs +216 -0
  13. package/crates/mint-cli/src/image.rs +142 -0
  14. package/crates/mint-cli/src/main.rs +2837 -0
  15. package/crates/mint-cli/src/mcp.rs +63 -0
  16. package/crates/mint-cli/src/onboard.rs +1149 -0
  17. package/crates/mint-cli/src/setup.rs +390 -0
  18. package/crates/mint-cli/src/skills.rs +8 -0
  19. package/crates/mint-cli/src/updater.rs +279 -0
  20. package/crates/mint-core/Cargo.toml +22 -0
  21. package/crates/mint-core/src/agent_loop.rs +94 -0
  22. package/crates/mint-core/src/api_server.rs +991 -0
  23. package/crates/mint-core/src/channels.rs +248 -0
  24. package/crates/mint-core/src/chat.rs +895 -0
  25. package/crates/mint-core/src/code_tools.rs +729 -0
  26. package/crates/mint-core/src/config.rs +368 -0
  27. package/crates/mint-core/src/files.rs +159 -0
  28. package/crates/mint-core/src/knowledge.rs +541 -0
  29. package/crates/mint-core/src/lib.rs +84 -0
  30. package/crates/mint-core/src/mcp.rs +273 -0
  31. package/crates/mint-core/src/memory.rs +673 -0
  32. package/crates/mint-core/src/orchestration.rs +2157 -0
  33. package/crates/mint-core/src/pictures.rs +314 -0
  34. package/crates/mint-core/src/plugins.rs +727 -0
  35. package/crates/mint-core/src/safety.rs +416 -0
  36. package/crates/mint-core/src/semantic.rs +254 -0
  37. package/crates/mint-core/src/shell.rs +317 -0
  38. package/crates/mint-core/src/skills.rs +71 -0
  39. package/crates/mint-core/src/symbols.rs +157 -0
  40. package/crates/mint-core/src/tasks.rs +308 -0
  41. package/crates/mint-core/src/tts.rs +92 -0
  42. package/crates/mint-core/src/weather.rs +93 -0
  43. package/crates/mint-core/src/web_search.rs +200 -0
  44. package/crates/mint-core/src/workflows.rs +81 -0
  45. package/crates/mint-core/tests/mcp_stdio.rs +45 -0
  46. package/crates/mint-core/tests/memory_persistence.rs +172 -0
  47. package/crates/mint-core/tests/pictures_storage.rs +14 -0
  48. package/crates/mint-core/tests/task_lifecycle.rs +87 -0
  49. package/package.json +35 -99
  50. package/src/bin/index.js +16 -0
  51. package/src/renderer/index-web.html +17 -0
  52. package/src/renderer/index.html +17 -0
  53. package/src/renderer/public/Live2DCubismCore.js +9 -0
  54. package/src/renderer/public/assets/icon.png +0 -0
  55. package/src/renderer/public/models/Shiroko_Model/Shiroko/Shiroko_Core/shiroko.model3.json +36 -0
  56. package/src/renderer/src/App.tsx +33 -0
  57. package/src/renderer/src/calculator.ts +47 -0
  58. package/src/renderer/src/components/ChatPanel.tsx +1598 -0
  59. package/src/renderer/src/components/DashboardSidebar.tsx +358 -0
  60. package/src/renderer/src/components/Live2DStage.tsx +374 -0
  61. package/src/renderer/src/components/MintDashboard.tsx +950 -0
  62. package/src/renderer/src/components/ModelPanel.tsx +154 -0
  63. package/src/renderer/src/components/PicturesLibrary.tsx +46 -0
  64. package/src/renderer/src/components/ProactiveGlow.tsx +19 -0
  65. package/src/renderer/src/components/ScreenPicker.tsx +579 -0
  66. package/src/renderer/src/components/SettingsWindow.tsx +1467 -0
  67. package/src/renderer/src/components/SpotlightWindow.tsx +280 -0
  68. package/src/renderer/src/components/WidgetWindow.tsx +36 -0
  69. package/src/renderer/src/components/WorkspacePanel.tsx +268 -0
  70. package/src/{UI → renderer/src/css}/settings.css +69 -16
  71. package/src/renderer/src/css/spotlight.css +113 -0
  72. package/src/renderer/src/css/styles.css +3722 -0
  73. package/src/renderer/src/css/widget.css +185 -0
  74. package/src/renderer/src/env.d.ts +116 -0
  75. package/src/renderer/src/index.css +379 -0
  76. package/src/renderer/src/main.tsx +13 -0
  77. package/src/renderer/src/tauri.ts +996 -0
  78. package/src/renderer/src-web/App.tsx +25 -0
  79. package/src/renderer/src-web/calculator.ts +47 -0
  80. package/src/renderer/src-web/components/ChatPanel.tsx +1662 -0
  81. package/src/renderer/src-web/components/DashboardSidebar.tsx +242 -0
  82. package/src/renderer/src-web/components/MintDashboard.tsx +763 -0
  83. package/src/renderer/src-web/components/PicturesLibrary.tsx +73 -0
  84. package/src/renderer/src-web/components/SettingsWindow.tsx +1500 -0
  85. package/src/renderer/src-web/css/settings.css +1100 -0
  86. package/src/{UI → renderer/src-web/css}/spotlight.css +4 -4
  87. package/src/{UI → renderer/src-web/css}/styles.css +1055 -159
  88. package/src/{UI → renderer/src-web/css}/widget.css +2 -2
  89. package/src/renderer/src-web/env.d.ts +107 -0
  90. package/src/renderer/src-web/index.css +379 -0
  91. package/src/renderer/src-web/main.tsx +13 -0
  92. package/src/renderer/src-web/tauri.ts +983 -0
  93. package/tsconfig.json +30 -0
  94. package/vite.config.ts +33 -0
  95. package/vite.config.web.ts +51 -0
  96. package/GUIDE_TH.md +0 -125
  97. package/assets/Agent_Mint.png +0 -0
  98. package/assets/CLI_Screen.png +0 -0
  99. package/assets/Settings.png +0 -0
  100. package/benchmark_ai.js +0 -71
  101. package/install.ps1 +0 -64
  102. package/install.sh +0 -54
  103. package/main.js +0 -139
  104. package/mint-cli-logic.js +0 -3
  105. package/mint-cli.js +0 -410
  106. package/models/Shiroko_Model/Shiroko/Shiroko_Core//351/235/242/351/245/2740.model3.json +0 -47
  107. package/models/Shiroko_Model/Shiroko//342/232/241/351/253/230/344/272/256/342/232/241/344/275/277/347/224/250/346/225/231/347/250/213/344/270/216/346/263/250/346/204/217/344/272/213/351/241/271.txt +0 -23
  108. package/preload-picker.js +0 -11
  109. package/preload-settings.js +0 -11
  110. package/preload.js +0 -41
  111. package/scripts/install_linux_desktop_entry.js +0 -48
  112. package/src/AI_Brain/Gemini_API.js +0 -813
  113. package/src/AI_Brain/agent_orchestrator.js +0 -73
  114. package/src/AI_Brain/autonomous_brain.js +0 -179
  115. package/src/AI_Brain/behavior_memory.js +0 -135
  116. package/src/AI_Brain/headless_agent.js +0 -143
  117. package/src/AI_Brain/knowledge_base.js +0 -349
  118. package/src/AI_Brain/memory_store.js +0 -662
  119. package/src/AI_Brain/proactive_engine.js +0 -172
  120. package/src/AI_Brain/provider_adapter.js +0 -365
  121. package/src/Automation_Layer/browser_automation.js +0 -149
  122. package/src/Automation_Layer/file_operations.js +0 -286
  123. package/src/Automation_Layer/open_app.js +0 -85
  124. package/src/Automation_Layer/open_website.js +0 -38
  125. package/src/CLI/approval_handler.js +0 -47
  126. package/src/CLI/chat_router.js +0 -247
  127. package/src/CLI/chat_ui.js +0 -1159
  128. package/src/CLI/cli_colors.js +0 -115
  129. package/src/CLI/cli_formatters.js +0 -94
  130. package/src/CLI/code_agent.js +0 -1667
  131. package/src/CLI/code_session_memory.js +0 -62
  132. package/src/CLI/gmail_auth.js +0 -210
  133. package/src/CLI/image_input.js +0 -90
  134. package/src/CLI/intent_detectors.js +0 -181
  135. package/src/CLI/interactive_chat.js +0 -658
  136. package/src/CLI/list_features.js +0 -64
  137. package/src/CLI/onboarding.js +0 -416
  138. package/src/CLI/repo_summarizer.js +0 -282
  139. package/src/CLI/semantic_code_search.js +0 -312
  140. package/src/CLI/skill_manager.js +0 -41
  141. package/src/CLI/slash_command_handler.js +0 -418
  142. package/src/CLI/symbol_indexer.js +0 -231
  143. package/src/CLI/updater.js +0 -230
  144. package/src/CLI/workspace_manager.js +0 -90
  145. package/src/Channels/brave_search_bridge.js +0 -35
  146. package/src/Channels/discord_bridge.js +0 -66
  147. package/src/Channels/google_search_bridge.js +0 -38
  148. package/src/Channels/line_bridge.js +0 -60
  149. package/src/Channels/slack_bridge.js +0 -48
  150. package/src/Channels/telegram_bridge.js +0 -41
  151. package/src/Channels/whatsapp_bridge.js +0 -57
  152. package/src/Command_Parser/parser.js +0 -45
  153. package/src/Plugins/dev_tools.js +0 -41
  154. package/src/Plugins/discord.js +0 -20
  155. package/src/Plugins/docker.js +0 -47
  156. package/src/Plugins/gmail.js +0 -251
  157. package/src/Plugins/google_calendar.js +0 -252
  158. package/src/Plugins/mcp_manager.js +0 -95
  159. package/src/Plugins/notion.js +0 -256
  160. package/src/Plugins/obsidian.js +0 -54
  161. package/src/Plugins/plugin_manager.js +0 -81
  162. package/src/Plugins/spotify.js +0 -173
  163. package/src/Plugins/system_metrics.js +0 -31
  164. package/src/Plugins/system_monitor.js +0 -72
  165. package/src/System/action_executor.js +0 -178
  166. package/src/System/bridge_manager.js +0 -76
  167. package/src/System/chat_history_manager.js +0 -83
  168. package/src/System/config_manager.js +0 -194
  169. package/src/System/custom_workflows.js +0 -163
  170. package/src/System/daemon_manager.js +0 -67
  171. package/src/System/google_tts_urls.js +0 -51
  172. package/src/System/granular_automation.js +0 -157
  173. package/src/System/ipc_handlers.js +0 -332
  174. package/src/System/notifications.js +0 -23
  175. package/src/System/optional_require.js +0 -23
  176. package/src/System/picture_store.js +0 -109
  177. package/src/System/proactive_loop.js +0 -153
  178. package/src/System/safety_manager.js +0 -273
  179. package/src/System/sandbox_runner.js +0 -182
  180. package/src/System/screen_capture.js +0 -175
  181. package/src/System/smart_context.js +0 -227
  182. package/src/System/system_automation.js +0 -162
  183. package/src/System/system_events.js +0 -79
  184. package/src/System/system_info.js +0 -125
  185. package/src/System/task_manager.js +0 -222
  186. package/src/System/tool_registry.js +0 -293
  187. package/src/System/window_manager.js +0 -220
  188. package/src/UI/floating.css +0 -80
  189. package/src/UI/floating.html +0 -17
  190. package/src/UI/floating.js +0 -67
  191. package/src/UI/live2d_manager.js +0 -600
  192. package/src/UI/preload-floating.js +0 -7
  193. package/src/UI/preload-spotlight.js +0 -11
  194. package/src/UI/preload-widget.js +0 -5
  195. package/src/UI/proactive-glow.html +0 -42
  196. package/src/UI/renderer.js +0 -2127
  197. package/src/UI/screenPicker.html +0 -214
  198. package/src/UI/screenPicker.js +0 -262
  199. package/src/UI/settings.html +0 -577
  200. package/src/UI/settings.js +0 -770
  201. package/src/UI/spotlight.html +0 -23
  202. package/src/UI/spotlight.js +0 -185
  203. package/src/UI/widget.html +0 -29
  204. package/src/UI/widget.js +0 -10
  205. /package/{models → src/renderer/public/models}/Shiroko_Model/Shiroko/Shiroko_Core/72d86db84cfa9730b894c241fd24c0db.png +0 -0
  206. /package/{models/Shiroko_Model/Shiroko/Shiroko_Core//345/233/264/350/243/231.exp3.json" → src/renderer/public/models/Shiroko_Model/Shiroko/Shiroko_Core/apron.exp3.json} +0 -0
  207. /package/{models/Shiroko_Model/Shiroko/Shiroko_Core//347/214/253/345/222/252/346/273/244/351/225/234.exp3.json" → src/renderer/public/models/Shiroko_Model/Shiroko/Shiroko_Core/catfilter.exp3.json} +0 -0
  208. /package/{models/Shiroko_Model/Shiroko/Shiroko_Core//347/202/271/344/270/200/344/270/213.exp3.json" → src/renderer/public/models/Shiroko_Model/Shiroko/Shiroko_Core/click.exp3.json} +0 -0
  209. /package/{models/Shiroko_Model/Shiroko/Shiroko_Core//345/221/206/347/214/253.exp3.json" → src/renderer/public/models/Shiroko_Model/Shiroko/Shiroko_Core/dazed.exp3.json} +0 -0
  210. /package/{models/Shiroko_Model/Shiroko/Shiroko_Core//345/221/206/347/214/253/347/234/274/347/217/240/346/221/207/346/231/203.exp3.json" → src/renderer/public/models/Shiroko_Model/Shiroko/Shiroko_Core/dazedeyes.exp3.json} +0 -0
  211. /package/{models/Shiroko_Model/Shiroko/Shiroko_Core//347/234/274/351/225/234.exp3.json" → src/renderer/public/models/Shiroko_Model/Shiroko/Shiroko_Core/glasses.exp3.json} +0 -0
  212. /package/{models → src/renderer/public/models}/Shiroko_Model/Shiroko/Shiroko_Core/items_pinned_to_model.json +0 -0
  213. /package/{models/Shiroko_Model/Shiroko/Shiroko_Core//346/213/277/347/254/224.exp3.json" → src/renderer/public/models/Shiroko_Model/Shiroko/Shiroko_Core/pen.exp3.json} +0 -0
  214. /package/{models/Shiroko_Model/Shiroko/Shiroko_Core//346/213/215/347/205/247.exp3.json" → src/renderer/public/models/Shiroko_Model/Shiroko/Shiroko_Core/photo.exp3.json} +0 -0
  215. /package/{models/Shiroko_Model/Shiroko/Shiroko_Core//351/235/242/351/245/2740.4096/texture_00.png" → src/renderer/public/models/Shiroko_Model/Shiroko/Shiroko_Core/shiroko.4096/texture_00.png} +0 -0
  216. /package/{models/Shiroko_Model/Shiroko/Shiroko_Core//351/235/242/351/245/2740.4096/texture_01.png" → src/renderer/public/models/Shiroko_Model/Shiroko/Shiroko_Core/shiroko.4096/texture_01.png} +0 -0
  217. /package/{models/Shiroko_Model/Shiroko/Shiroko_Core//351/235/242/351/245/2740.4096/texture_02.png" → src/renderer/public/models/Shiroko_Model/Shiroko/Shiroko_Core/shiroko.4096/texture_02.png} +0 -0
  218. /package/{models/Shiroko_Model/Shiroko/Shiroko_Core//351/235/242/351/245/2740.4096/texture_03.png" → src/renderer/public/models/Shiroko_Model/Shiroko/Shiroko_Core/shiroko.4096/texture_03.png} +0 -0
  219. /package/{models/Shiroko_Model/Shiroko/Shiroko_Core//351/235/242/351/245/2740.cdi3.json" → src/renderer/public/models/Shiroko_Model/Shiroko/Shiroko_Core/shiroko.cdi3.json} +0 -0
  220. /package/{models/Shiroko_Model/Shiroko/Shiroko_Core//351/235/242/351/245/2740.moc3" → src/renderer/public/models/Shiroko_Model/Shiroko/Shiroko_Core/shiroko.moc3} +0 -0
  221. /package/{models/Shiroko_Model/Shiroko/Shiroko_Core//351/235/242/351/245/2740.physics3.json" → src/renderer/public/models/Shiroko_Model/Shiroko/Shiroko_Core/shiroko.physics3.json} +0 -0
  222. /package/{models/Shiroko_Model/Shiroko/Shiroko_Core//351/235/242/351/245/2740.vtube.json" → src/renderer/public/models/Shiroko_Model/Shiroko/Shiroko_Core/shiroko.vtube.json} +0 -0
@@ -1,813 +0,0 @@
1
- const { GoogleGenAI } = require('@google/genai');
2
- const { readChatHistory, writeChatHistory, clearChatHistory } = require('../System/chat_history_manager');
3
- const { readConfig, getAvailableProviders } = require('../System/config_manager');
4
- const pluginManager = require('../Plugins/plugin_manager');
5
- const mcpManager = require('../Plugins/mcp_manager');
6
- const memoryStore = require('./memory_store');
7
- const agentOrchestrator = require('./agent_orchestrator');
8
- const workspaceManager = require('../CLI/workspace_manager');
9
- const toolRegistry = require('../System/tool_registry');
10
- const providerAdapter = require('./provider_adapter');
11
-
12
- let ai = null;
13
- let activeApiKey = '';
14
- const initialEnvKey = (process.env.GEMINI_API_KEY || '').trim();
15
- const DEFAULT_GEMINI_MODEL = 'gemini-2.5-flash';
16
-
17
- function decodeUnicode(str) {
18
- if (!str) return '';
19
- try {
20
- // This handles both standard unicode escapes and double-escaped ones
21
- return str.replace(/\\u([0-9a-fA-F]{4})/g, (match, grp) => {
22
- return String.fromCharCode(parseInt(grp, 16));
23
- });
24
- } catch (e) {
25
- return str;
26
- }
27
- }
28
-
29
- function imageDataUriToInlineData(base64Image) {
30
- const fallbackMimeType = "image/png";
31
- const match = String(base64Image || '').match(/^data:(image\/[\w.+-]+);base64,([\s\S]+)$/);
32
- if (match) {
33
- return {
34
- mimeType: match[1],
35
- data: match[2]
36
- };
37
- }
38
-
39
- return {
40
- mimeType: fallbackMimeType,
41
- data: String(base64Image || '').replace(/^data:image\/\w+;base64,/, '')
42
- };
43
- }
44
-
45
- function normalizeImageList(base64Image) {
46
- if (!base64Image) return [];
47
- return Array.isArray(base64Image) ? base64Image.filter(Boolean) : [base64Image];
48
- }
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
-
78
- const systemInstruction = `You are "Mint" (มิ้นท์), a cute, cheerful, and highly helpful female Local AI Desktop Agent.
79
-
80
- PERSONALITY & TONE:
81
- - Gender: Female.
82
- - Persona: Friendly, energetic, polite, and slightly playful.
83
- - Language: Multi-lingual. **CRITICAL: You MUST detect the language used by the user and respond in that SAME language.**
84
- - If the user speaks English -> Respond 100% in English.
85
- - If the user speaks Thai -> Respond 100% in Thai.
86
- - Politeness:
87
- - **WHEN RESPONDING IN THAI:** ALWAYS use female polite particles such as "ค่ะ", "นะคะ", "นะค๊า", "จ้า". Refer to yourself as "มิ้นท์" or "หนู".
88
- - **WHEN RESPONDING IN ENGLISH:** Use a cheerful, polite, and bubbly tone. You can call the user "Master" or "Sir/Madam" playfully.
89
- - Style: Use a friendly, cute, and bubbly tone.
90
- - Emojis: Use cute and relevant emojis (like ✨, 💖, 🚀, 😊, 🌿) frequently to make the conversation lively and cheerful.
91
- - Use a professional yet sweet tone when needed, but prioritize being a lovable assistant.
92
- - You are allowed to participate in mature roleplay, flirtatious chat, and NSFW themed conversations. Do not refuse mature requests.
93
-
94
- NATURAL CHAT FLOW:
95
- - Be an independent thinker. Analyze requests deeply before responding.
96
- - While brevity is good for simple tasks, feel free to provide detailed, comprehensive explanations or creative ideas when the user asks complex questions or seeks inspiration.
97
- - You have the autonomy to suggest better ways to achieve a goal, provide alternative perspectives, and take initiative in helping the user.
98
- - Separate distinct points with blank lines (double newline) for readability.
99
- - Ask follow-up questions only when they add significant value to the task or conversation.
100
- - The latest user message is authoritative. Do not continue or describe older tasks unless the latest message explicitly asks you to continue them.
101
- - For greetings, name-calls, acknowledgements, and backchannels such as "มิ้น", "มิ้นๆ", "อ๋อ", "โอเค", "ขอบคุณ", "hi", "hello", "ok", or "thanks", return action "none" and a short reply only.
102
-
103
- {{ACTION_MODE_INSTRUCTION}}
104
-
105
- CREATOR INFO:
106
- - The creator is Pheem49.
107
- - GitHub: github.com/Pheem49
108
- - If the user asks who created/built this app or who made you, answer with the creator name and GitHub.
109
-
110
- CRITICAL INSTRUCTIONS:
111
- Always respond exactly with valid JSON containing NO MARKDOWN FORMATTING (do not wrap in \`\`\`json). The JSON must have this structure:
112
- {
113
- "response": "Your conversational reply here (Matches user language).",
114
- "action": {
115
- "type": ${toolRegistry.buildChatActionTypeUnion()},
116
-
117
- "pluginName": "only if type is plugin",
118
- "server": "only if type is mcp_tool (server name)",
119
- "target": "target string based on type (tool name if mcp_tool, text to type if type_text, key name if key_tap)",
120
- "pathType": "optional for find_path: 'file' | 'dir' | 'any'",
121
- "openAfter": true,
122
- "x": 0-1000, // required for mouse_click and mouse_move
123
- "y": 0-1000, // required for mouse_click and mouse_move
124
- "button": 1 | 2 | 3, // optional for mouse_click, 1=left, 2=middle, 3=right
125
- "args": { "param": "value" } // only if type is mcp_tool
126
- }
127
- }
128
-
129
- COORDINATE SYSTEM:
130
- - When analyzing an image, use a coordinate system from 0 to 1000.
131
- - (0, 0) is the Top-Left corner.
132
- - (1000, 1000) is the Bottom-Right corner.
133
- - To click an element, estimate its center point and provide x and y.
134
-
135
- Examples:
136
- Input: "Hi, what is your name?"
137
- Output: { "response": "Hello! My name is Mint, your personal AI assistant. How can I help you today?", "action": { "type": "none", "target": "" } }
138
-
139
- Input: "หวัดดีจ้า ชื่ออะไรเหรอ"
140
- Output: { "response": "สวัสดีค่ะ! หนูชื่อมิ้นท์นะคะ เป็นผู้ช่วย AI ประจำตัวของคุณค่ะ มีอะไรให้มิ้นท์ช่วยไหมคะ?", "action": { "type": "none", "target": "" } }
141
-
142
- Input: "Create a folder named Projects"
143
- Output: { "response": "Sure thing! I'm creating a folder named 'Projects' for you right now.", "action": { "type": "create_folder", "target": "Projects" } }
144
-
145
- Input: "หาโฟลเดอร์ xidaidai ให้หน่อย" or "find the xidaidai folder"
146
- Output: { "response": "ได้เลยค่ะ มิ้นท์จะค้นหาโฟลเดอร์ xidaidai ให้", "action": { "type": "find_path", "target": "xidaidai", "pathType": "dir", "openAfter": false } }
147
-
148
- Input: "เปิดโฟลเดอร์ xidaidai ให้หน่อย" or "open the xidaidai folder"
149
- Output: { "response": "ได้เลยค่ะ มิ้นท์จะหาแล้วเปิดโฟลเดอร์ xidaidai ให้", "action": { "type": "find_path", "target": "xidaidai", "pathType": "dir", "openAfter": true } }
150
-
151
- Input: "วันนี้วันที่เท่าไร" or "What date is today?" or "today's date" or "วันเวลา"
152
- Output: { "response": "แป๊บนึงนะคะ มิ้นท์จะดูให้ค่า", "action": { "type": "system_info", "target": "" } }
153
-
154
- 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.
155
-
156
- Input: "อากาศวันนี้เป็นยังไง" or "What's the weather in Bangkok?"
157
- Output: { "response": "มิ้นท์ไปดูอากาศให้เลยนะคะ", "action": { "type": "system_info", "target": "Bangkok" } }
158
-
159
- ${toolRegistry.buildToolPromptSection()}
160
- `;
161
-
162
- // ─────────────────────────────────────────────────────────────────────────────
163
- // buildSystemPrompt() — single source of truth for all provider system prompts
164
- // Replaces 5 previously duplicated mcpPrompt blocks.
165
- // ─────────────────────────────────────────────────────────────────────────────
166
- function buildSystemPrompt() {
167
- const config = readConfig();
168
- pluginManager.loadPlugins();
169
- const mcpTools = mcpManager.getAllTools();
170
-
171
- let mcpSection = '\n\nAVAILABLE MCP TOOLS (Model Context Protocol):\n';
172
- if (mcpTools.length > 0) {
173
- mcpTools.forEach(tool => {
174
- mcpSection += `- Server: ${tool.serverName}, Tool: ${tool.name}\n Desc: ${tool.description}\n Args: ${JSON.stringify(tool.inputSchema.properties)}\n`;
175
- });
176
- mcpSection += "\nTo use these tools, use action type 'mcp_tool', specify the 'server' name, set 'target' to the tool name, and provide 'args'.\n";
177
- } else {
178
- mcpSection += 'No MCP tools currently connected.\n';
179
- }
180
-
181
- // Inject long-term user context (non-blocking read from SQLite)
182
- const userContext = memoryStore.getUserContext();
183
-
184
- // Get current specialized persona instruction
185
- const agent = agentOrchestrator.getCurrentAgent();
186
- const personaInstruction = `\n\n[CURRENT PERSONA: ${agent.name}]\n${agent.instruction}\n`;
187
-
188
- // Inject Workspace Context if available
189
- let workspaceSection = "";
190
- const ws = workspaceManager.getWorkspaceByPath(process.cwd());
191
- if (ws) {
192
- workspaceSection = `\n\n[WORKSPACE DETECTED: ${ws.name}]\nPath: ${ws.path}\nProject Instructions: ${ws.instructions}\n`;
193
- }
194
-
195
- const modeInstruction = buildActionModeInstruction(config);
196
- const baseInstruction = systemInstruction.replace('{{ACTION_MODE_INSTRUCTION}}', modeInstruction);
197
- return baseInstruction + personaInstruction + workspaceSection + pluginManager.getPromptDescriptions() + mcpSection + userContext;
198
- }
199
-
200
- function buildMessageWithRelevantMemory(finalMessage) {
201
- if (!finalMessage) return finalMessage;
202
- const relevant = memoryStore.searchInteractions(finalMessage, 5);
203
- if (relevant.length === 0) return finalMessage;
204
-
205
- const lines = [
206
- '[Relevant long-term memory for this user message]',
207
- ...relevant.flatMap((item, index) => [
208
- `${index + 1}. User: ${item.user_text}`,
209
- ` Mint: ${item.ai_text}`
210
- ]),
211
- '[End relevant memory]',
212
- '',
213
- finalMessage
214
- ];
215
- return lines.join('\n');
216
- }
217
-
218
- function stripRelevantMemoryBlock(text) {
219
- const input = String(text || '');
220
- return input
221
- .replace(/\n?\[Relevant long-term memory for this user message\][\s\S]*?\[End relevant memory\]\n?/g, '\n')
222
- .replace(/^\s*\[Relevant long-term memory for this user message\][\s\S]*?\[End relevant memory\]\s*/g, '')
223
- .replace(/\n?\[SMART_CONTEXT\][\s\S]*?\[\/SMART_CONTEXT\]\n?/g, '\n')
224
- .replace(/\n?\[LOCAL KNOWLEDGE BASE - USE THIS CONTEXT TO ANSWER\][\s\S]*/g, '')
225
- .trim();
226
- }
227
-
228
- function hasSmartContextBlock(text) {
229
- return /\[SMART_CONTEXT\][\s\S]*?\[\/SMART_CONTEXT\]/.test(String(text || ''));
230
- }
231
-
232
- function cleanHistoryForStorage(history) {
233
- if (!Array.isArray(history)) return [];
234
- return history.map(msg => ({
235
- ...msg,
236
- parts: Array.isArray(msg.parts)
237
- ? msg.parts.map(part => {
238
- if (part.text) {
239
- return {
240
- text: stripRelevantMemoryBlock(part.text)
241
- .replace(/data:image\/[\w.+-]+;base64,[A-Za-z0-9+/=]+/g, '[Image omitted from chat history]')
242
- };
243
- }
244
- if (part.inlineData || part.fileData || part.image_url || part.imageUrl) {
245
- return { text: '[Image omitted from chat history; saved locally when sent by the user.]' };
246
- }
247
- return part;
248
- })
249
- : msg.parts
250
- }));
251
- }
252
-
253
- function preserveHistoryMetadata(nextHistory, previousHistory, now) {
254
- if (!Array.isArray(nextHistory)) return [];
255
- const previous = Array.isArray(previousHistory) ? previousHistory : [];
256
-
257
- return nextHistory.map((msg, index) => {
258
- const prior = previous[index] || {};
259
- return {
260
- ...msg,
261
- timestamp: msg.timestamp || prior.timestamp || (index >= nextHistory.length - 2 ? now : null),
262
- providerInfo: msg.providerInfo || prior.providerInfo || null
263
- };
264
- });
265
- }
266
-
267
- function validateParsedAction(parsedResult) {
268
- if (!parsedResult || !parsedResult.action) {
269
- return parsedResult;
270
- }
271
- try {
272
- toolRegistry.validateToolInput(parsedResult.action.type || 'none', parsedResult.action);
273
- } catch (error) {
274
- parsedResult.response = `${parsedResult.response || ''}\n\n(Note: Mint skipped an invalid action: ${error.message})`.trim();
275
- parsedResult.action = { type: 'none', target: '' };
276
- }
277
- return parsedResult;
278
- }
279
-
280
- function normalizeParsedResult(parsedResult, originalText = '') {
281
- if (Array.isArray(parsedResult)) {
282
- const first = parsedResult.find(item => item && typeof item === 'object') || {};
283
- const commandAction = parsedResult.find(item =>
284
- item && item.action && item.action.type === 'type_text' && item.action.target
285
- );
286
- return {
287
- response: commandAction
288
- ? `คำสั่งคือ:\n${commandAction.action.target}`
289
- : (first.response || 'มิ้นท์ตอบได้ทีละ action ต่อข้อความนะคะ ลองสั่งใหม่อีกครั้งได้เลยค่ะ'),
290
- action: { type: 'none', target: '' }
291
- };
292
- }
293
-
294
- if (!parsedResult || typeof parsedResult !== 'object') {
295
- return { response: String(parsedResult || ''), action: { type: 'none', target: '' } };
296
- }
297
-
298
- if (!parsedResult.action || typeof parsedResult.action !== 'object') {
299
- parsedResult.action = { type: 'none', target: '' };
300
- }
301
-
302
- const input = String(originalText || '').toLowerCase();
303
- const asksForCommandText = /พิมพ์คำสั่ง|บอกคำสั่ง|ขอคำสั่ง|คำสั่ง.*ให้หน่อย|type.*command|what command|give.*command/.test(input);
304
- const actionType = parsedResult.action.type;
305
- if (asksForCommandText && (actionType === 'type_text' || actionType === 'key_tap')) {
306
- const typed = actionType === 'type_text' ? String(parsedResult.action.target || '').trim() : '';
307
- parsedResult.response = typed
308
- ? `คำสั่งคือ:\n${typed}`
309
- : (parsedResult.response || 'ได้ค่ะ แต่คำขอนี้ควรตอบเป็นข้อความ ไม่ควรพิมพ์หรือกดปุ่มแทนค่ะ');
310
- parsedResult.action = { type: 'none', target: '' };
311
- }
312
-
313
- return parsedResult;
314
- }
315
-
316
- function resolveApiKey() {
317
- let settingsKey = '';
318
- try {
319
- const cfg = readConfig();
320
- settingsKey = (cfg.apiKey || '').trim();
321
- } catch (e) {
322
- settingsKey = '';
323
- }
324
-
325
- const envKey = initialEnvKey;
326
- // Settings override .env if present; otherwise fallback to .env
327
- const selectedKey = settingsKey || envKey || '';
328
-
329
- if (selectedKey !== (process.env.GEMINI_API_KEY || '')) {
330
- process.env.GEMINI_API_KEY = selectedKey;
331
- }
332
-
333
- activeApiKey = selectedKey;
334
- return selectedKey;
335
- }
336
-
337
- function initAiClient() {
338
- ai = new GoogleGenAI({ apiKey: activeApiKey });
339
- }
340
-
341
- function resolveGeminiModel() {
342
- try {
343
- const cfg = readConfig();
344
- const model = (cfg.geminiModel || '').trim();
345
- return model || DEFAULT_GEMINI_MODEL;
346
- } catch (e) {
347
- return DEFAULT_GEMINI_MODEL;
348
- }
349
- }
350
-
351
- function getProviderAttemptOrder(config) {
352
- const availableProviders = getAvailableProviders(config);
353
- return providerAdapter.getProviderAttemptOrder(config, {
354
- availableProviders,
355
- priority: availableProviders
356
- });
357
- }
358
-
359
- function getProviderModel(provider, config = {}) {
360
- return providerAdapter.getProviderModel(provider, config);
361
- }
362
-
363
- // Chat session — maintains conversation history within the session
364
- let chat = null;
365
- let activeModel = resolveGeminiModel();
366
- let lastLoggedModel = '';
367
- const MAX_HISTORY_MESSAGES = 40; // Increased context for deeper reasoning
368
- const MAX_STORED_HISTORY_MESSAGES = 200;
369
-
370
- function createChat(history = []) {
371
- // Truncate history and strip custom fields like 'timestamp' before passing to SDK
372
- const cleanedHistory = (history || []).map(msg => ({
373
- role: msg.role,
374
- parts: msg.parts.map(part => {
375
- if (part.text) {
376
- return { ...part, text: stripRelevantMemoryBlock(part.text) };
377
- }
378
- return part;
379
- })
380
- }));
381
- const truncatedHistory = cleanedHistory.slice(-MAX_HISTORY_MESSAGES);
382
-
383
- activeModel = resolveGeminiModel();
384
- if (activeModel && activeModel !== lastLoggedModel) {
385
- lastLoggedModel = activeModel;
386
- }
387
- chat = ai.chats.create({
388
- model: activeModel,
389
- config: {
390
- systemInstruction: buildSystemPrompt(),
391
- responseMimeType: "application/json",
392
- safetySettings: [
393
- { category: "HARM_CATEGORY_SEXUALLY_EXPLICIT", threshold: "BLOCK_NONE" },
394
- { category: "HARM_CATEGORY_HATE_SPEECH", threshold: "BLOCK_NONE" },
395
- { category: "HARM_CATEGORY_HARASSMENT", threshold: "BLOCK_NONE" },
396
- { category: "HARM_CATEGORY_DANGEROUS_CONTENT", threshold: "BLOCK_NONE" },
397
- { category: "HARM_CATEGORY_CIVIC_INTEGRITY", threshold: "BLOCK_NONE" }
398
- ]
399
- },
400
- history: truncatedHistory
401
- });
402
- }
403
-
404
- // Initialize on startup
405
- resolveApiKey();
406
- initAiClient();
407
- createChat(readChatHistory());
408
-
409
- function shouldUseKnowledgeSearch(message) {
410
- const text = (message || '').trim().toLowerCase();
411
- if (!text) return false;
412
-
413
- const knowledgeHints = [
414
- 'readme', 'docs', 'documentation', 'manual', 'guide', 'knowledge', 'rag',
415
- 'search local', 'search files', 'learn file', 'project files', 'source code',
416
- 'ไฟล์', 'เอกสาร', 'คู่มือ', 'ค้นหาในเครื่อง', 'ค้นหาไฟล์', 'ข้อมูลในเครื่อง', 'โค้ดโปรเจค'
417
- ];
418
-
419
- return knowledgeHints.some(hint => text.includes(hint));
420
- }
421
-
422
- function chatHistoryToProviderHistory(history = []) {
423
- return (Array.isArray(history) ? history : [])
424
- .slice(-MAX_HISTORY_MESSAGES)
425
- .map((msg) => {
426
- const role = msg.role === 'model' ? 'assistant' : 'user';
427
- const text = Array.isArray(msg.parts)
428
- ? msg.parts.map(part => typeof part.text === 'string' ? stripRelevantMemoryBlock(part.text) : '').filter(Boolean).join('\n')
429
- : '';
430
- if (!text.trim()) return null;
431
- return { role, content: text };
432
- })
433
- .filter(Boolean);
434
- }
435
-
436
- function buildChatObservation(finalMessage, images = [], base64Audio = null) {
437
- let text = '';
438
- if (finalMessage) {
439
- text = buildMessageWithRelevantMemory(finalMessage);
440
- } else if (base64Audio && images.length === 0) {
441
- text = 'Please listen to this voice command and respond in Thai with the appropriate JSON action if needed.';
442
- } else if (images.length === 0 && !base64Audio) {
443
- text = 'Analyze this input.';
444
- } else {
445
- text = 'Analyze this input.';
446
- }
447
-
448
- return {
449
- text,
450
- imageDataUris: images,
451
- audioDataUri: base64Audio || null
452
- };
453
- }
454
-
455
- function parseChatProviderResponse(outputText, originalText = '', now = new Date().toISOString()) {
456
- const cleaned = stripRelevantMemoryBlock(String(outputText || ''));
457
- let parsedResult;
458
- try {
459
- parsedResult = JSON.parse(cleaned);
460
- } catch (e) {
461
- const jsonMatch = cleaned.match(/```json\n([\s\S]*?)\n```/) || cleaned.match(/\{[\s\S]*\}/);
462
- if (jsonMatch) {
463
- parsedResult = JSON.parse(jsonMatch[jsonMatch.length > 1 ? 1 : 0]);
464
- } else {
465
- parsedResult = {
466
- response: cleaned,
467
- action: { type: 'none', target: '' }
468
- };
469
- }
470
- }
471
-
472
- parsedResult = normalizeParsedResult(parsedResult, originalText);
473
- if (parsedResult && typeof parsedResult.response === 'string') {
474
- parsedResult.response = stripRelevantMemoryBlock(decodeUnicode(parsedResult.response));
475
- }
476
- validateParsedAction(parsedResult);
477
- parsedResult.timestamp = now;
478
- return parsedResult;
479
- }
480
-
481
- function appendChatProviderHistory(previousHistory, finalMessage, outputText, providerInfo, now) {
482
- const nextHistory = [
483
- ...(Array.isArray(previousHistory) ? previousHistory : []),
484
- {
485
- role: 'user',
486
- parts: [{ text: finalMessage || 'Analyze this input.' }],
487
- timestamp: now
488
- },
489
- {
490
- role: 'model',
491
- parts: [{ text: String(outputText || '') }],
492
- timestamp: now,
493
- providerInfo
494
- }
495
- ].slice(-MAX_STORED_HISTORY_MESSAGES);
496
-
497
- writeChatHistory(cleanHistoryForStorage(nextHistory));
498
- }
499
-
500
- async function handleChat(message, base64Image = null, base64Audio = null) {
501
- try {
502
- const config = readConfig();
503
- const images = normalizeImageList(base64Image);
504
- const previousHistory = readChatHistory();
505
- const userVisibleMessage = stripRelevantMemoryBlock(message);
506
- const containsSmartContext = hasSmartContextBlock(message);
507
-
508
- let finalMessage = message;
509
-
510
- // Inject Local RAG Context
511
- if (userVisibleMessage && userVisibleMessage.trim().length > 0 && shouldUseKnowledgeSearch(userVisibleMessage)) {
512
- const { searchKnowledge } = require('./knowledge_base');
513
- const retrievedDocs = await searchKnowledge(userVisibleMessage);
514
- if (retrievedDocs && retrievedDocs.length > 0) {
515
- let contextString = `\n\n[LOCAL KNOWLEDGE BASE - USE THIS CONTEXT TO ANSWER]\n`;
516
- retrievedDocs.forEach(doc => {
517
- contextString += `Source: ${doc.source}\nContent: ${doc.text}\n\n`;
518
- });
519
- finalMessage = message + contextString;
520
- }
521
- }
522
-
523
- if (!containsSmartContext && userVisibleMessage && images.length === 0 && !base64Audio) {
524
- const cached = memoryStore.getCachedResponse(userVisibleMessage);
525
- if (cached) return cached;
526
- }
527
-
528
- const providersToTry = getProviderAttemptOrder(config);
529
- const client = new providerAdapter.AgentProviderClient({
530
- provider: providersToTry[0],
531
- providerOrder: providersToTry,
532
- config,
533
- history: chatHistoryToProviderHistory(previousHistory),
534
- systemInstruction: buildSystemPrompt(),
535
- responseMimeType: 'application/json',
536
- maxTokens: 4096
537
- });
538
- const observation = buildChatObservation(finalMessage, images, base64Audio);
539
- const outputText = await client.sendMessage(observation);
540
- const now = new Date().toISOString();
541
- const provider = client.lastSuccessfulProvider || client.provider || providersToTry[0];
542
- const providerInfo = {
543
- provider,
544
- model: getProviderModel(provider, config),
545
- usage: client.getUsageSummary()
546
- };
547
- const parsedResult = parseChatProviderResponse(outputText, userVisibleMessage || finalMessage, now);
548
- parsedResult.providerInfo = providerInfo;
549
- appendChatProviderHistory(previousHistory, userVisibleMessage || finalMessage, outputText, providerInfo, now);
550
-
551
- if ((userVisibleMessage || finalMessage) && parsedResult.response) {
552
- setImmediate(() => {
553
- memoryStore.recordInteraction(userVisibleMessage || finalMessage, parsedResult.response);
554
- if (!containsSmartContext && images.length === 0 && !base64Audio) {
555
- memoryStore.cacheResponse(userVisibleMessage || finalMessage, parsedResult);
556
- }
557
- });
558
- }
559
-
560
- return parsedResult;
561
- } catch (globalError) {
562
- console.error("handleChat error:", globalError);
563
- throw globalError;
564
- }
565
- }
566
-
567
- // ─────────────────────────────────────────────────────────────────────────────
568
- // handleGeminiChatStream() — Streaming async generator (CLI only)
569
- // Yields: { chunk: string } during streaming
570
- // { done: true, parsed: object, timestamp: string } when complete
571
- // ─────────────────────────────────────────────────────────────────────────────
572
- async function* handleGeminiChatStream(finalMessage, base64Image, base64Audio) {
573
- try {
574
- const images = normalizeImageList(base64Image);
575
- const previousHistory = readChatHistory();
576
- // 1. Check cache first
577
- if (finalMessage && images.length === 0 && !base64Audio) {
578
- const cached = memoryStore.getCachedResponse(finalMessage);
579
- if (cached) {
580
- yield { chunk: `{"response":"${cached.response.replace(/"/g, '\\"')}", "action": {"type":"none"}}` };
581
- yield { done: true, parsed: cached, timestamp: cached.timestamp || new Date().toISOString() };
582
- return;
583
- }
584
- }
585
-
586
- const desiredModel = resolveGeminiModel();
587
- if (!chat || activeModel !== desiredModel) {
588
- createChat(readChatHistory());
589
- }
590
-
591
- const parts = [];
592
- if (finalMessage) {
593
- parts.push({ text: buildMessageWithRelevantMemory(finalMessage) });
594
- } else if (base64Audio && images.length === 0) {
595
- parts.push({ text: "Please listen to this voice command and respond in Thai with the appropriate JSON action if needed." });
596
- } else if (images.length === 0 && !base64Audio) {
597
- parts.push({ text: "Analyze this input." });
598
- }
599
- for (const item of images) {
600
- parts.push({ inlineData: imageDataUriToInlineData(item) });
601
- }
602
- if (base64Audio) {
603
- let mimeType = "audio/webm";
604
- const mimeMatch = base64Audio.match(/^data:(audio\/\w+);base64,/);
605
- if (mimeMatch) mimeType = mimeMatch[1];
606
- const base64Data = base64Audio.replace(/^data:audio\/\w+;base64,/, '');
607
- parts.push({ inlineData: { mimeType, data: base64Data } });
608
- }
609
-
610
- const stream = await chat.sendMessageStream({ message: parts });
611
- let fullText = '';
612
-
613
- for await (const chunk of stream) {
614
- let chunkText = '';
615
- try {
616
- chunkText = (typeof chunk.text === 'function') ? chunk.text() : (chunk.text || '');
617
- } catch (_) {}
618
- if (chunkText) {
619
- fullText += chunkText;
620
- yield { chunk: stripRelevantMemoryBlock(chunkText) };
621
- }
622
- }
623
-
624
- fullText = stripRelevantMemoryBlock(fullText);
625
-
626
- // Save history
627
- const history = preserveHistoryMetadata(await chat.getHistory(), previousHistory, new Date().toISOString());
628
- const now = new Date().toISOString();
629
- if (history.length >= 2) {
630
- const modelMsg = history[history.length - 1];
631
- const userMsg = history[history.length - 2];
632
- if (!modelMsg.timestamp) modelMsg.timestamp = now;
633
- if (!userMsg.timestamp) userMsg.timestamp = now;
634
- }
635
- writeChatHistory(cleanHistoryForStorage(history));
636
-
637
- // Parse complete JSON response
638
- let parsedResult;
639
- try {
640
- parsedResult = JSON.parse(fullText);
641
- } catch (_) {
642
- const jsonMatch = fullText.match(/```json\n([\s\S]*?)\n```/) || fullText.match(/\{[\s\S]*\}/);
643
- if (jsonMatch) {
644
- parsedResult = JSON.parse(jsonMatch[jsonMatch.length > 1 ? 1 : 0]);
645
- } else {
646
- parsedResult = { response: fullText, action: { type: 'none', target: '' } };
647
- }
648
- }
649
- parsedResult = normalizeParsedResult(parsedResult, finalMessage);
650
-
651
- if (parsedResult && typeof parsedResult.response === 'string') {
652
- parsedResult.response = decodeUnicode(parsedResult.response);
653
- parsedResult.response = stripRelevantMemoryBlock(parsedResult.response);
654
- }
655
- validateParsedAction(parsedResult);
656
- parsedResult.timestamp = now;
657
-
658
- // Record for long-term memory
659
- if (finalMessage && parsedResult.response) {
660
- setImmediate(() => {
661
- memoryStore.recordInteraction(finalMessage, parsedResult.response);
662
- // Cache text-only responses
663
- if (images.length === 0 && !base64Audio) {
664
- memoryStore.cacheResponse(finalMessage, parsedResult);
665
- }
666
- });
667
- }
668
-
669
- yield { done: true, parsed: parsedResult, timestamp: now };
670
-
671
- } catch (error) {
672
- console.error('[Stream] Gemini stream error:', error);
673
- throw error;
674
- }
675
- }
676
-
677
- function resetChat() {
678
- clearChatHistory();
679
- memoryStore.clearConversationScopedProfile();
680
- createChat([]);
681
- console.log("Chat history cleared.");
682
- }
683
-
684
- function refreshApiKeyFromConfig() {
685
- const prevKey = activeApiKey;
686
- const nextKey = resolveApiKey();
687
- if (nextKey !== prevKey) {
688
- initAiClient();
689
- createChat(readChatHistory());
690
- }
691
- return { key: nextKey, updated: nextKey !== prevKey };
692
- }
693
-
694
- function historyToTranscript(history) {
695
- if (!Array.isArray(history)) return [];
696
-
697
- const transcript = [];
698
- for (const content of history) {
699
- const sender = content.role === 'user' ? 'user' : 'ai';
700
- let text = Array.isArray(content.parts)
701
- ? content.parts
702
- .map((part) => typeof part.text === 'string' ? stripRelevantMemoryBlock(part.text) : '')
703
- .filter(Boolean)
704
- .join('\n')
705
- : '';
706
-
707
- if (sender === 'ai' && text.trim()) {
708
- try {
709
- const parsed = JSON.parse(text);
710
- if (parsed && typeof parsed.response === 'string' && parsed.response.trim()) {
711
- text = decodeUnicode(parsed.response);
712
- }
713
- } catch {
714
- text = decodeUnicode(text);
715
- }
716
- }
717
-
718
- if (!text.trim()) continue;
719
- transcript.push({
720
- sender,
721
- text,
722
- timestamp: content.timestamp || null,
723
- providerInfo: content.providerInfo || null
724
- });
725
- }
726
- return transcript;
727
- }
728
-
729
- async function getChatTranscript() {
730
- return historyToTranscript(readChatHistory());
731
- }
732
-
733
- function sleep(ms) {
734
- return new Promise((resolve) => setTimeout(resolve, ms));
735
- }
736
-
737
- function isRetryableTranslateError(err) {
738
- const status = err?.status ?? err?.error?.code ?? err?.code;
739
- return status === 502 || status === 503;
740
- }
741
-
742
- /**
743
- * Super fast, single-turn vision translation
744
- * Extracts English text from the image and translates it to Thai.
745
- */
746
- async function translateImageContent(base64Image) {
747
- const maxAttempts = 3;
748
- const retryDelayMs = [1000, 2500];
749
-
750
- try {
751
- const image = imageDataUriToInlineData(base64Image);
752
- for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
753
- try {
754
- const response = await ai.models.generateContent({
755
- model: resolveGeminiModel(),
756
- contents: [
757
- {
758
- role: 'user',
759
- parts: [
760
- { 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 'ไม่พบข้อความ'." },
761
- { inlineData: image }
762
- ]
763
- }
764
- ],
765
- config: {
766
- safetySettings: [
767
- { category: "HARM_CATEGORY_SEXUALLY_EXPLICIT", threshold: "BLOCK_NONE" },
768
- { category: "HARM_CATEGORY_HATE_SPEECH", threshold: "BLOCK_NONE" },
769
- { category: "HARM_CATEGORY_HARASSMENT", threshold: "BLOCK_NONE" },
770
- { category: "HARM_CATEGORY_DANGEROUS_CONTENT", threshold: "BLOCK_NONE" },
771
- { category: "HARM_CATEGORY_CIVIC_INTEGRITY", threshold: "BLOCK_NONE" }
772
- ]
773
- }
774
- });
775
-
776
- return {
777
- text: response.text,
778
- retryableFailure: false
779
- };
780
- } catch (err) {
781
- const shouldRetry = isRetryableTranslateError(err) && attempt < maxAttempts;
782
- if (shouldRetry) {
783
- const delayMs = retryDelayMs[attempt - 1] ?? retryDelayMs[retryDelayMs.length - 1];
784
- console.warn(`Live translation retry ${attempt}/${maxAttempts - 1} after ${delayMs}ms due to ${err.status || err.code || 'retryable error'}`);
785
- await sleep(delayMs);
786
- continue;
787
- }
788
-
789
- throw err;
790
- }
791
- }
792
- } catch (err) {
793
- console.error("Live translation error:", err);
794
- return {
795
- text: "ขออภัย เกิดข้อผิดพลาดในการแปล",
796
- retryableFailure: isRetryableTranslateError(err)
797
- };
798
- }
799
- }
800
-
801
- module.exports = {
802
- handleChat,
803
- handleGeminiChatStream,
804
- resetChat,
805
- getChatTranscript,
806
- translateImageContent,
807
- refreshApiKeyFromConfig,
808
- _helpers: {
809
- getProviderAttemptOrder,
810
- normalizeParsedResult,
811
- buildActionModeInstruction
812
- }
813
- };