@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
@@ -0,0 +1,991 @@
1
+ use std::{
2
+ net::SocketAddr,
3
+ path::PathBuf,
4
+ process::{Command, Stdio},
5
+ };
6
+
7
+ use serde::Deserialize;
8
+ use serde_json::{Value, json};
9
+ use tokio::io::{AsyncReadExt, AsyncWriteExt};
10
+ use tokio::net::TcpListener;
11
+
12
+ use crate::{
13
+ AgentProgress, ApprovalOutcome, ChatRequest, ChatResponse, DEFAULT_CONVERSATION_ID,
14
+ MemoryStore, MintConfig, config_path, create_folder, find_paths, list_saved_pictures,
15
+ load_config, orchestrate_agent_loop, orchestrate_chat_stream_with_fallback,
16
+ orchestrate_chat_with_fallback, save_chat_images, save_config, weather,
17
+ };
18
+
19
+ const MAX_API_REQUEST_BYTES: usize = 32 * 1024 * 1024;
20
+
21
+ pub async fn start_api_server(port: u16) -> Result<(), std::io::Error> {
22
+ let addr = SocketAddr::from(([0, 0, 0, 0], port));
23
+ let listener = TcpListener::bind(addr).await?;
24
+ println!("\x1b[36m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\x1b[0m");
25
+ println!(
26
+ "\x1b[32m Mint Local API Server running at http://{}\x1b[0m",
27
+ addr
28
+ );
29
+ println!("\x1b[36m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\x1b[0m");
30
+
31
+ // Start background messaging bridges (Telegram, Discord, Slack)
32
+ crate::start_channels();
33
+
34
+ loop {
35
+ let (mut socket, _) = match listener.accept().await {
36
+ Ok(val) => val,
37
+ Err(_) => continue,
38
+ };
39
+
40
+ tokio::spawn(async move {
41
+ let mut request_bytes = Vec::with_capacity(8192);
42
+ let mut chunk = [0_u8; 8192];
43
+ let mut expected_len: Option<usize> = None;
44
+
45
+ loop {
46
+ let n = match socket.read(&mut chunk).await {
47
+ Ok(n) if n > 0 => n,
48
+ _ => break,
49
+ };
50
+ request_bytes.extend_from_slice(&chunk[..n]);
51
+ if request_bytes.len() > MAX_API_REQUEST_BYTES {
52
+ send_json_response(
53
+ socket,
54
+ "413 Payload Too Large",
55
+ "{\"provider\":\"error\",\"model\":\"error\",\"text\":\"Request is too large. Try a smaller image or fewer images.\"}",
56
+ )
57
+ .await;
58
+ return;
59
+ }
60
+
61
+ let headers_str = String::from_utf8_lossy(&request_bytes);
62
+ if expected_len.is_none() && headers_str.contains("\r\n\r\n") {
63
+ expected_len = headers_str
64
+ .to_lowercase()
65
+ .find("content-length:")
66
+ .and_then(|content_length_pos| {
67
+ let sub = &headers_str[content_length_pos..];
68
+ let line_end = sub.find("\r\n")?;
69
+ sub["content-length:".len()..line_end]
70
+ .trim()
71
+ .parse::<usize>()
72
+ .ok()
73
+ })
74
+ .and_then(|content_len| {
75
+ let header_len = headers_str.find("\r\n\r\n")? + 4;
76
+ Some(header_len + content_len)
77
+ });
78
+ }
79
+
80
+ if let Some(total_len) = expected_len {
81
+ if request_bytes.len() >= total_len {
82
+ break;
83
+ }
84
+ } else if headers_str.contains("\r\n\r\n") {
85
+ break;
86
+ }
87
+ }
88
+
89
+ if request_bytes.is_empty() {
90
+ return;
91
+ }
92
+
93
+ let request_str = String::from_utf8_lossy(&request_bytes);
94
+ let lines: Vec<&str> = request_str.split("\r\n").collect();
95
+ if lines.is_empty() {
96
+ return;
97
+ }
98
+
99
+ let req_line: Vec<&str> = lines[0].split_whitespace().collect();
100
+ if req_line.len() < 2 {
101
+ return;
102
+ }
103
+
104
+ let method = req_line[0];
105
+ let path = req_line[1];
106
+
107
+ let header_end = match request_str.find("\r\n\r\n") {
108
+ Some(idx) => idx,
109
+ None => return,
110
+ };
111
+ let body = &request_str[header_end + 4..];
112
+
113
+ if method == "OPTIONS" {
114
+ let response = "HTTP/1.1 200 OK\r\n\
115
+ Access-Control-Allow-Origin: *\r\n\
116
+ Access-Control-Allow-Headers: Content-Type\r\n\
117
+ Access-Control-Allow-Methods: GET, POST, OPTIONS\r\n\
118
+ Content-Length: 0\r\n\
119
+ Connection: close\r\n\r\n";
120
+ let _ = socket.write_all(response.as_bytes()).await;
121
+ let _ = socket.flush().await;
122
+ return;
123
+ }
124
+
125
+ let (route, query) = path.split_once('?').unwrap_or((path, ""));
126
+
127
+ match (method, route) {
128
+ ("GET", "/api/status") => {
129
+ let config = load_config().unwrap_or_default();
130
+ let path_str = config_path()
131
+ .map(|p| p.display().to_string())
132
+ .unwrap_or_default();
133
+ let active = config.ai_provider.clone();
134
+ let available: Vec<String> = config
135
+ .available_providers()
136
+ .into_iter()
137
+ .map(|s| s.to_string())
138
+ .collect();
139
+ let status_json = serde_json::json!({
140
+ "backend": "rust-api-server",
141
+ "configPath": path_str,
142
+ "activeProvider": active,
143
+ "availableProviders": available,
144
+ "integrations": {},
145
+ "localIp": get_local_ip()
146
+ });
147
+ send_json_response(socket, "200 OK", &status_json.to_string()).await;
148
+ }
149
+ ("GET", "/api/system-info") => {
150
+ send_json_response(socket, "200 OK", &system_info().to_string()).await;
151
+ }
152
+ ("GET", "/api/smart-context") => {
153
+ send_json_response(socket, "200 OK", &smart_context().to_string()).await;
154
+ }
155
+ ("GET", "/api/interactions") => {
156
+ let limit = query_param(query, "limit")
157
+ .and_then(|value| value.parse::<usize>().ok())
158
+ .unwrap_or(50)
159
+ .min(200);
160
+ if let Ok(memory) = MemoryStore::open_default() {
161
+ let chat_id = query_param(query, "chatId")
162
+ .unwrap_or_else(|| DEFAULT_CONVERSATION_ID.to_owned());
163
+ let list = memory
164
+ .recent_interactions_for_chat(&chat_id, limit)
165
+ .unwrap_or_default();
166
+ if let Ok(json_str) = serde_json::to_string(&list) {
167
+ send_json_response(socket, "200 OK", &json_str).await;
168
+ return;
169
+ }
170
+ }
171
+ send_json_response(socket, "500 Internal Server Error", "[]").await;
172
+ }
173
+ ("GET", "/api/chat-sessions") => {
174
+ if let Ok(memory) = MemoryStore::open_default() {
175
+ let list = memory.list_chat_sessions().unwrap_or_default();
176
+ if let Ok(json_str) = serde_json::to_string(&list) {
177
+ send_json_response(socket, "200 OK", &json_str).await;
178
+ return;
179
+ }
180
+ }
181
+ send_json_response(socket, "500 Internal Server Error", "[]").await;
182
+ }
183
+ ("POST", "/api/chat-sessions/delete") => {
184
+ let chat_id = query_param(query, "chatId").unwrap_or_default();
185
+ if let Ok(memory) = MemoryStore::open_default() {
186
+ let deleted = memory.delete_chat_session(&chat_id).unwrap_or(0);
187
+ let response = serde_json::json!({ "status": "ok", "deleted": deleted });
188
+ send_json_response(socket, "200 OK", &response.to_string()).await;
189
+ } else {
190
+ send_json_response(
191
+ socket,
192
+ "500 Internal Server Error",
193
+ "{\"status\":\"error\",\"deleted\":0}",
194
+ )
195
+ .await;
196
+ }
197
+ }
198
+ ("POST", "/api/chat-sessions/rename") => {
199
+ #[derive(Deserialize)]
200
+ #[serde(rename_all = "camelCase")]
201
+ struct RenameRequest {
202
+ chat_id: String,
203
+ new_title: String,
204
+ }
205
+
206
+ if let Ok(req) = serde_json::from_str::<RenameRequest>(body) {
207
+ if let Ok(memory) = MemoryStore::open_default() {
208
+ let updated = memory
209
+ .rename_chat_session(&req.chat_id, &req.new_title)
210
+ .unwrap_or(0);
211
+ let response =
212
+ serde_json::json!({ "status": "ok", "updated": updated });
213
+ send_json_response(socket, "200 OK", &response.to_string()).await;
214
+ return;
215
+ }
216
+ }
217
+ send_json_response(
218
+ socket,
219
+ "500 Internal Server Error",
220
+ "{\"status\":\"error\",\"updated\":0}",
221
+ )
222
+ .await;
223
+ }
224
+ ("GET", "/api/profile") => {
225
+ let key = query_param(query, "key").unwrap_or_default();
226
+ if let Ok(memory) = MemoryStore::open_default() {
227
+ let value = memory.get_profile(&key).unwrap_or(None).unwrap_or_default();
228
+ send_json_response(
229
+ socket,
230
+ "200 OK",
231
+ &serde_json::json!({ "value": value }).to_string(),
232
+ )
233
+ .await;
234
+ return;
235
+ }
236
+ send_json_response(socket, "500 Internal Server Error", "{\"value\":\"\"}")
237
+ .await;
238
+ }
239
+ ("POST", "/api/profile") => {
240
+ #[derive(Deserialize)]
241
+ struct ProfileRequest {
242
+ key: String,
243
+ value: String,
244
+ }
245
+ if let Ok(req) = serde_json::from_str::<ProfileRequest>(body) {
246
+ if let Ok(memory) = MemoryStore::open_default() {
247
+ let _ = memory.set_profile(&req.key, &req.value);
248
+ send_json_response(socket, "200 OK", "{\"status\":\"ok\"}").await;
249
+ return;
250
+ }
251
+ }
252
+ send_json_response(
253
+ socket,
254
+ "500 Internal Server Error",
255
+ "{\"status\":\"error\"}",
256
+ )
257
+ .await;
258
+ }
259
+ ("POST", "/api/interactions/clear") => {
260
+ if let Ok(memory) = MemoryStore::open_default() {
261
+ let chat_id = query_param(query, "chatId")
262
+ .unwrap_or_else(|| DEFAULT_CONVERSATION_ID.to_owned());
263
+ let _ = memory.clear_interactions_for_chat(&chat_id);
264
+ send_json_response(socket, "200 OK", "{\"status\":\"ok\"}").await;
265
+ } else {
266
+ send_json_response(
267
+ socket,
268
+ "500 Internal Server Error",
269
+ "{\"status\":\"error\"}",
270
+ )
271
+ .await;
272
+ }
273
+ }
274
+ ("GET", "/api/pictures") => match list_saved_pictures() {
275
+ Ok(mut pictures) => {
276
+ for picture in &mut pictures {
277
+ picture.url = Some(format!("/api/pictures/{}", picture.filename));
278
+ }
279
+ if let Ok(json_str) = serde_json::to_string(&pictures) {
280
+ send_json_response(socket, "200 OK", &json_str).await;
281
+ } else {
282
+ send_json_response(socket, "500 Internal Server Error", "[]").await;
283
+ }
284
+ }
285
+ Err(_) => send_json_response(socket, "500 Internal Server Error", "[]").await,
286
+ },
287
+ ("GET", route) if route.starts_with("/api/pictures/") => {
288
+ let filename = percent_decode(route.trim_start_matches("/api/pictures/"));
289
+ match picture_bytes(&filename) {
290
+ Ok((mime_type, bytes)) => {
291
+ send_binary_response(socket, "200 OK", &mime_type, &bytes).await
292
+ }
293
+ Err(_) => {
294
+ send_json_response(
295
+ socket,
296
+ "404 Not Found",
297
+ "{\"error\":\"picture not found\"}",
298
+ )
299
+ .await
300
+ }
301
+ }
302
+ }
303
+ ("GET", "/api/config") => {
304
+ let config = load_config().unwrap_or_default();
305
+ if let Ok(json_str) = serde_json::to_string(&config) {
306
+ send_json_response(socket, "200 OK", &json_str).await;
307
+ } else {
308
+ send_json_response(socket, "500 Internal Server Error", "{}").await;
309
+ }
310
+ }
311
+ ("POST", "/api/config") => {
312
+ if let Ok(new_config) = serde_json::from_str::<MintConfig>(body) {
313
+ if save_config(&new_config).is_ok() {
314
+ send_json_response(socket, "200 OK", "{\"status\":\"ok\"}").await;
315
+ return;
316
+ }
317
+ }
318
+ send_json_response(
319
+ socket,
320
+ "400 Bad Request",
321
+ "{\"status\":\"invalid config json\"}",
322
+ )
323
+ .await;
324
+ }
325
+ ("GET", "/api/weather") => {
326
+ let city = query_param(query, "city").unwrap_or_default();
327
+ match weather(&city).await {
328
+ Ok(report) => {
329
+ if let Ok(json_str) = serde_json::to_string(&report) {
330
+ send_json_response(socket, "200 OK", &json_str).await;
331
+ } else {
332
+ send_json_response(socket, "500 Internal Server Error", "{}").await;
333
+ }
334
+ }
335
+ Err(error) => {
336
+ let err_json = json!({ "error": error.to_string() });
337
+ send_json_response(
338
+ socket,
339
+ "500 Internal Server Error",
340
+ &err_json.to_string(),
341
+ )
342
+ .await;
343
+ }
344
+ }
345
+ }
346
+ ("POST", "/api/action") => {
347
+ if let Ok(action) = serde_json::from_str::<ApiAction>(body) {
348
+ let config = load_config().unwrap_or_default();
349
+ match execute_api_action(&config, action) {
350
+ Ok(value) => {
351
+ send_json_response(socket, "200 OK", &value.to_string()).await
352
+ }
353
+ Err(error) => {
354
+ let err_json = json!({ "success": false, "message": error });
355
+ send_json_response(
356
+ socket,
357
+ "400 Bad Request",
358
+ &err_json.to_string(),
359
+ )
360
+ .await;
361
+ }
362
+ }
363
+ } else {
364
+ send_json_response(
365
+ socket,
366
+ "400 Bad Request",
367
+ "{\"success\":false,\"message\":\"invalid action body\"}",
368
+ )
369
+ .await;
370
+ }
371
+ }
372
+ ("POST", "/api/chat") => {
373
+ #[derive(Deserialize)]
374
+ #[serde(rename_all = "camelCase")]
375
+ struct ApiChatRequest {
376
+ message: String,
377
+ system_instruction: Option<String>,
378
+ chat_id: Option<String>,
379
+ image_data_uri: Option<String>,
380
+ audio_data_uri: Option<String>,
381
+ }
382
+
383
+ if let Ok(req) = serde_json::from_str::<ApiChatRequest>(body) {
384
+ let config = load_config().unwrap_or_default();
385
+ let mut chat_req = ChatRequest {
386
+ message: req.message,
387
+ system_instruction: req.system_instruction.unwrap_or_default(),
388
+ chat_id: req.chat_id,
389
+ image_data_uri: req.image_data_uri,
390
+ audio_data_uri: req.audio_data_uri,
391
+ document_attachment: None,
392
+ workspace_path: None,
393
+ };
394
+ let sent_image = chat_req.image_data_uri.clone();
395
+ let sent_message = chat_req.message.clone();
396
+
397
+ let response = if let Some(clean_message) =
398
+ chat_req.message.strip_prefix("/chat ").map(str::to_owned)
399
+ {
400
+ chat_req.message = clean_message;
401
+ if chat_req.system_instruction.trim().is_empty() {
402
+ chat_req.system_instruction = default_chat_system_instruction();
403
+ }
404
+ orchestrate_chat_with_fallback(&config, &chat_req)
405
+ .await
406
+ .map(|(response, _)| response)
407
+ .map_err(|error| error.to_string())
408
+ } else {
409
+ run_web_agent_loop(&config, &chat_req).await
410
+ };
411
+
412
+ match response {
413
+ Ok(resp) => {
414
+ if let Some(image) = sent_image {
415
+ let _ = save_chat_images(
416
+ image
417
+ .split_whitespace()
418
+ .map(str::to_owned)
419
+ .collect::<Vec<_>>(),
420
+ Some("web".into()),
421
+ Some(sent_message),
422
+ );
423
+ }
424
+ if let Ok(json_str) = serde_json::to_string(&resp) {
425
+ send_json_response(socket, "200 OK", &json_str).await;
426
+ return;
427
+ }
428
+ }
429
+ Err(e) => {
430
+ eprintln!("API Chat error: {:?}", e);
431
+ let err_json = serde_json::json!({
432
+ "provider": "error",
433
+ "model": "error",
434
+ "text": format!("Error orchestrating chat: {e}")
435
+ });
436
+ send_json_response(
437
+ socket,
438
+ "500 Internal Server Error",
439
+ &err_json.to_string(),
440
+ )
441
+ .await;
442
+ return;
443
+ }
444
+ }
445
+ }
446
+ send_json_response(
447
+ socket,
448
+ "400 Bad Request",
449
+ "{\"status\":\"invalid chat request body\"}",
450
+ )
451
+ .await;
452
+ }
453
+ ("POST", "/api/chat-stream") => {
454
+ #[derive(Deserialize)]
455
+ #[serde(rename_all = "camelCase")]
456
+ struct ApiChatRequest {
457
+ message: String,
458
+ system_instruction: Option<String>,
459
+ chat_id: Option<String>,
460
+ image_data_uri: Option<String>,
461
+ audio_data_uri: Option<String>,
462
+ }
463
+
464
+ if let Ok(req) = serde_json::from_str::<ApiChatRequest>(body) {
465
+ let config = load_config().unwrap_or_default();
466
+ let mut chat_req = ChatRequest {
467
+ message: req.message,
468
+ system_instruction: req.system_instruction.unwrap_or_default(),
469
+ chat_id: req.chat_id,
470
+ image_data_uri: req.image_data_uri,
471
+ audio_data_uri: req.audio_data_uri,
472
+ document_attachment: None,
473
+ workspace_path: None,
474
+ };
475
+ let sent_image = chat_req.image_data_uri.clone();
476
+ let sent_message = chat_req.message.clone();
477
+
478
+ let is_chat = if let Some(clean_message) =
479
+ chat_req.message.strip_prefix("/chat ").map(str::to_owned)
480
+ {
481
+ chat_req.message = clean_message;
482
+ if chat_req.system_instruction.trim().is_empty() {
483
+ chat_req.system_instruction = default_chat_system_instruction();
484
+ }
485
+ true
486
+ } else {
487
+ false
488
+ };
489
+
490
+ let headers = "HTTP/1.1 200 OK\r\n\
491
+ Access-Control-Allow-Origin: *\r\n\
492
+ Access-Control-Allow-Headers: Content-Type\r\n\
493
+ Content-Type: application/x-ndjson\r\n\
494
+ Cache-Control: no-cache\r\n\
495
+ Connection: close\r\n\r\n";
496
+ if socket.write_all(headers.as_bytes()).await.is_ok() {
497
+ let _ = socket.flush().await;
498
+
499
+ let (tx, mut rx) = tokio::sync::mpsc::unbounded_channel::<String>();
500
+
501
+ {
502
+ let tx_progress = tx.clone();
503
+ let progress_cb = move |progress: AgentProgress| {
504
+ if let Ok(json_val) =
505
+ serde_json::to_string(&serde_json::json!({
506
+ "type": "progress",
507
+ "progress": progress
508
+ }))
509
+ {
510
+ let _ = tx_progress.send(format!("{}\n", json_val));
511
+ }
512
+ };
513
+
514
+ let tx_chunk = tx.clone();
515
+ let on_chunk = move |chunk: String| {
516
+ if let Ok(json_val) =
517
+ serde_json::to_string(&serde_json::json!({
518
+ "type": "chunk",
519
+ "chunk": chunk
520
+ }))
521
+ {
522
+ let _ = tx_chunk.send(format!("{}\n", json_val));
523
+ }
524
+ };
525
+
526
+ if is_chat {
527
+ let tx_chunk_inner = tx.clone();
528
+ let config_clone = config.clone();
529
+ let chat_req_clone = chat_req.clone();
530
+ let tx_done = tx.clone();
531
+ tokio::spawn(async move {
532
+ let result = orchestrate_chat_stream_with_fallback(
533
+ &config_clone,
534
+ &chat_req_clone,
535
+ move |chunk| {
536
+ if let Ok(json_val) =
537
+ serde_json::to_string(&serde_json::json!({
538
+ "type": "chunk",
539
+ "chunk": chunk
540
+ }))
541
+ {
542
+ let _ = tx_chunk_inner
543
+ .send(format!("{}\n", json_val));
544
+ }
545
+ },
546
+ )
547
+ .await;
548
+
549
+ match result {
550
+ Ok((response, _)) => {
551
+ if let Ok(json_val) =
552
+ serde_json::to_string(&serde_json::json!({
553
+ "type": "done",
554
+ "response": response
555
+ }))
556
+ {
557
+ let _ = tx_done.send(format!("{}\n", json_val));
558
+ }
559
+ }
560
+ Err(e) => {
561
+ eprintln!("API Chat Stream error: {:?}", e);
562
+ let err_json = serde_json::json!({
563
+ "type": "done",
564
+ "response": {
565
+ "provider": "error",
566
+ "model": "error",
567
+ "text": format!("Error orchestrating chat: {e}")
568
+ }
569
+ });
570
+ let _ = tx_done
571
+ .send(format!("{}\n", err_json.to_string()));
572
+ }
573
+ }
574
+ });
575
+ } else {
576
+ let root = std::env::current_dir().unwrap_or_default();
577
+ let fast_mode = config
578
+ .extra
579
+ .get("enableFastMode")
580
+ .and_then(Value::as_bool)
581
+ .unwrap_or(false);
582
+
583
+ let tx_done = tx.clone();
584
+ let config_clone = config.clone();
585
+ let chat_id = chat_req.chat_id.clone();
586
+ let message = chat_req.message.clone();
587
+ let image_data_uri = chat_req.image_data_uri.clone();
588
+
589
+ tokio::spawn(async move {
590
+ let result = orchestrate_agent_loop(
591
+ &config_clone,
592
+ &message,
593
+ &root,
594
+ image_data_uri,
595
+ chat_id.as_deref(),
596
+ fast_mode,
597
+ |_| Ok(ApprovalOutcome::Denied),
598
+ progress_cb,
599
+ on_chunk,
600
+ )
601
+ .await;
602
+
603
+ match result {
604
+ Ok(res) => {
605
+ let response = ChatResponse {
606
+ provider: res.provider,
607
+ model: res.model,
608
+ text: res.summary,
609
+ fallback_provider: res.fallback,
610
+ };
611
+ if let Ok(json_val) =
612
+ serde_json::to_string(&serde_json::json!({
613
+ "type": "done",
614
+ "response": response
615
+ }))
616
+ {
617
+ let _ = tx_done.send(format!("{}\n", json_val));
618
+ }
619
+ }
620
+ Err(e) => {
621
+ let err_json = serde_json::json!({
622
+ "type": "done",
623
+ "response": {
624
+ "provider": "error",
625
+ "model": "error",
626
+ "text": format!("Error orchestrating agent: {e}")
627
+ }
628
+ });
629
+ let _ = tx_done
630
+ .send(format!("{}\n", err_json.to_string()));
631
+ }
632
+ }
633
+ });
634
+ }
635
+ }
636
+
637
+ drop(tx);
638
+
639
+ while let Some(line) = rx.recv().await {
640
+ if socket.write_all(line.as_bytes()).await.is_err() {
641
+ break;
642
+ }
643
+ let _ = socket.flush().await;
644
+ }
645
+
646
+ if let Some(image) = sent_image {
647
+ let _ = save_chat_images(
648
+ image
649
+ .split_whitespace()
650
+ .map(str::to_owned)
651
+ .collect::<Vec<_>>(),
652
+ Some("web".into()),
653
+ Some(sent_message),
654
+ );
655
+ }
656
+ }
657
+ return;
658
+ }
659
+ send_json_response(
660
+ socket,
661
+ "400 Bad Request",
662
+ "{\"status\":\"invalid chat request body\"}",
663
+ )
664
+ .await;
665
+ }
666
+ _ => {
667
+ send_json_response(socket, "404 Not Found", "{\"error\":\"Not Found\"}").await;
668
+ }
669
+ }
670
+ });
671
+ }
672
+ }
673
+
674
+ async fn run_web_agent_loop(
675
+ config: &MintConfig,
676
+ request: &ChatRequest,
677
+ ) -> Result<ChatResponse, String> {
678
+ let root = std::env::current_dir().map_err(|error| error.to_string())?;
679
+ let fast_mode = config
680
+ .extra
681
+ .get("enableFastMode")
682
+ .and_then(Value::as_bool)
683
+ .unwrap_or(false);
684
+ let result = orchestrate_agent_loop(
685
+ config,
686
+ &request.message,
687
+ &root,
688
+ request.image_data_uri.clone(),
689
+ request.chat_id.as_deref(),
690
+ fast_mode,
691
+ |_| Ok(ApprovalOutcome::Denied),
692
+ |_| {},
693
+ |_| {},
694
+ )
695
+ .await
696
+ .map_err(|error| error.to_string())?;
697
+
698
+ Ok(ChatResponse {
699
+ provider: result.provider,
700
+ model: result.model,
701
+ text: result.summary,
702
+ fallback_provider: result.fallback,
703
+ })
704
+ }
705
+
706
+ fn default_chat_system_instruction() -> String {
707
+ "You are Mint, a warm and helpful Thai assistant. Speak naturally and politely. \
708
+ If the user writes Thai, answer in Thai and refer to yourself as มิ้น. \
709
+ You may discuss mature personal topics at a non-explicit level, including adult relationships, sex education, intimacy, emotions, and feelings. \
710
+ Keep the tone respectful and supportive, avoid graphic sexual detail, and do not engage with sexual content involving minors, coercion, exploitation, or sexual violence."
711
+ .into()
712
+ }
713
+
714
+ #[derive(Debug, Deserialize)]
715
+ #[serde(rename_all = "camelCase")]
716
+ struct ApiAction {
717
+ #[serde(rename = "type")]
718
+ kind: String,
719
+ #[serde(default)]
720
+ target: String,
721
+ #[serde(default)]
722
+ args: Value,
723
+ }
724
+
725
+ fn execute_api_action(config: &MintConfig, action: ApiAction) -> Result<Value, String> {
726
+ match action.kind.as_str() {
727
+ "none" => Ok(success_json("no action requested")),
728
+ "system_info" => Ok(success_json(&system_info().to_string())),
729
+ "open_url" => {
730
+ if !(action.target.starts_with("https://")
731
+ || action.target.starts_with("http://")
732
+ || action.target.starts_with("file://"))
733
+ {
734
+ return Err("only http, https, and file URLs may be opened".into());
735
+ }
736
+ spawn_detached("xdg-open", &[&action.target])?;
737
+ Ok(success_json("opened URL"))
738
+ }
739
+ "search" => {
740
+ let query = action.target.trim();
741
+ if query.is_empty() {
742
+ return Err("search query is required".into());
743
+ }
744
+ let url = format!("https://www.google.com/search?q={}", encode_query(query));
745
+ spawn_detached("xdg-open", &[&url])?;
746
+ Ok(success_json("opened web search"))
747
+ }
748
+ "open_app" => {
749
+ let app = action.target.trim();
750
+ if app.is_empty()
751
+ || !app
752
+ .chars()
753
+ .all(|char| char.is_ascii_alphanumeric() || matches!(char, '-' | '_' | '.'))
754
+ {
755
+ return Err("application name contains unsupported characters".into());
756
+ }
757
+ spawn_detached(app, &[])?;
758
+ Ok(success_json("opened application"))
759
+ }
760
+ "find_path" => {
761
+ let roots = action.args["roots"]
762
+ .as_array()
763
+ .map(|roots| {
764
+ roots
765
+ .iter()
766
+ .filter_map(Value::as_str)
767
+ .map(PathBuf::from)
768
+ .collect::<Vec<_>>()
769
+ })
770
+ .filter(|roots| !roots.is_empty())
771
+ .unwrap_or_else(default_search_roots);
772
+ let limit = action.args["limit"].as_u64().unwrap_or(20).min(100) as usize;
773
+ serde_json::to_value(find_paths(&action.target, &roots, limit, config))
774
+ .map(|matches| json!({ "success": true, "message": matches.to_string(), "matches": matches }))
775
+ .map_err(|error| error.to_string())
776
+ }
777
+ "create_folder" => create_folder(std::path::Path::new(&action.target), config)
778
+ .map(|path| success_json(&format!("created {}", path.display())))
779
+ .map_err(|error| error.to_string()),
780
+ other => Err(format!("local API action '{other}' is not supported")),
781
+ }
782
+ }
783
+
784
+ pub fn get_local_ip() -> Option<String> {
785
+ use std::net::UdpSocket;
786
+ let socket = UdpSocket::bind("0.0.0.0:0").ok()?;
787
+ socket.connect("8.8.8.8:80").ok()?;
788
+ socket.local_addr().ok().map(|addr| addr.ip().to_string())
789
+ }
790
+
791
+ fn system_info() -> Value {
792
+ json!({
793
+ "backend": "rust-api-server",
794
+ "os": std::env::consts::OS,
795
+ "arch": std::env::consts::ARCH,
796
+ "family": std::env::consts::FAMILY,
797
+ "host": hostname(),
798
+ "localIp": get_local_ip(),
799
+ "currentDir": std::env::current_dir().ok().map(|path| path.display().to_string()),
800
+ "configPath": config_path().ok().map(|path| path.display().to_string()),
801
+ })
802
+ }
803
+
804
+ fn smart_context() -> Value {
805
+ let active_window = active_window();
806
+ let current_app = active_window.as_ref().map(|window| {
807
+ json!({
808
+ "name": window["appName"],
809
+ "processName": window["processName"],
810
+ "pid": window["pid"]
811
+ })
812
+ });
813
+ json!({
814
+ "capturedAt": unix_timestamp().to_string(),
815
+ "platform": std::env::consts::OS,
816
+ "host": hostname(),
817
+ "activeWindow": active_window,
818
+ "currentApp": current_app,
819
+ "browser": Value::Null,
820
+ "selectedText": selected_text(),
821
+ })
822
+ }
823
+
824
+ fn active_window() -> Option<Value> {
825
+ let id = command_output("xdotool", &["getactivewindow"])?;
826
+ let title = command_output("xdotool", &["getwindowname", &id]).unwrap_or_default();
827
+ let pid = command_output("xdotool", &["getwindowpid", &id]).unwrap_or_default();
828
+ let process_name = command_output("ps", &["-p", &pid, "-o", "comm="]).unwrap_or_default();
829
+ Some(json!({
830
+ "id": id,
831
+ "title": title,
832
+ "appName": process_name,
833
+ "processName": process_name,
834
+ "pid": pid.parse::<u32>().ok(),
835
+ "platform": std::env::consts::OS
836
+ }))
837
+ }
838
+
839
+ fn selected_text() -> String {
840
+ [
841
+ ("wl-paste", vec!["--primary", "--no-newline"]),
842
+ ("xclip", vec!["-selection", "primary", "-out"]),
843
+ ("xsel", vec!["--primary", "--output"]),
844
+ ]
845
+ .into_iter()
846
+ .find_map(|(program, args)| command_output(program, &args))
847
+ .unwrap_or_default()
848
+ .chars()
849
+ .take(2000)
850
+ .collect()
851
+ }
852
+
853
+ fn picture_bytes(filename: &str) -> Result<(String, Vec<u8>), String> {
854
+ if filename.contains('/') || filename.contains('\\') || filename.contains("..") {
855
+ return Err("invalid picture path".into());
856
+ }
857
+ let picture = list_saved_pictures()
858
+ .map_err(|error| error.to_string())?
859
+ .into_iter()
860
+ .find(|entry| entry.filename == filename)
861
+ .ok_or_else(|| "picture not found".to_string())?;
862
+ let bytes = std::fs::read(&picture.path).map_err(|error| error.to_string())?;
863
+ Ok((picture.mime_type, bytes))
864
+ }
865
+
866
+ fn query_param(query: &str, key: &str) -> Option<String> {
867
+ query.split('&').find_map(|pair| {
868
+ let (name, value) = pair.split_once('=')?;
869
+ (percent_decode(name) == key).then(|| percent_decode(value))
870
+ })
871
+ }
872
+
873
+ fn percent_decode(raw: &str) -> String {
874
+ let bytes = raw.as_bytes();
875
+ let mut output = Vec::with_capacity(bytes.len());
876
+ let mut index = 0;
877
+ while index < bytes.len() {
878
+ if bytes[index] == b'%' && index + 2 < bytes.len() {
879
+ if let Ok(hex) = u8::from_str_radix(&raw[index + 1..index + 3], 16) {
880
+ output.push(hex);
881
+ index += 3;
882
+ continue;
883
+ }
884
+ }
885
+ output.push(if bytes[index] == b'+' {
886
+ b' '
887
+ } else {
888
+ bytes[index]
889
+ });
890
+ index += 1;
891
+ }
892
+ String::from_utf8_lossy(&output).into_owned()
893
+ }
894
+
895
+ fn default_search_roots() -> Vec<PathBuf> {
896
+ let mut roots = vec![std::env::current_dir().unwrap_or_else(|_| PathBuf::from("."))];
897
+ if let Some(home) = dirs::home_dir() {
898
+ roots.push(home);
899
+ }
900
+ roots
901
+ }
902
+
903
+ fn success_json(message: &str) -> Value {
904
+ json!({ "success": true, "message": message })
905
+ }
906
+
907
+ fn spawn_detached(program: &str, args: &[&str]) -> Result<(), String> {
908
+ Command::new(program)
909
+ .args(args)
910
+ .stdin(Stdio::null())
911
+ .stdout(Stdio::null())
912
+ .stderr(Stdio::null())
913
+ .spawn()
914
+ .map(|_| ())
915
+ .map_err(|error| format!("unable to start '{program}': {error}"))
916
+ }
917
+
918
+ fn command_output(program: &str, args: &[&str]) -> Option<String> {
919
+ Command::new(program)
920
+ .args(args)
921
+ .output()
922
+ .ok()
923
+ .filter(|output| output.status.success())
924
+ .map(|output| String::from_utf8_lossy(&output.stdout).trim().to_owned())
925
+ .filter(|output| !output.is_empty())
926
+ }
927
+
928
+ fn hostname() -> String {
929
+ command_output("hostname", &[]).unwrap_or_else(|| "unknown".into())
930
+ }
931
+
932
+ fn unix_timestamp() -> u64 {
933
+ std::time::SystemTime::now()
934
+ .duration_since(std::time::UNIX_EPOCH)
935
+ .map(|duration| duration.as_secs())
936
+ .unwrap_or_default()
937
+ }
938
+
939
+ fn encode_query(query: &str) -> String {
940
+ query
941
+ .bytes()
942
+ .flat_map(|byte| match byte {
943
+ b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' | b'-' | b'_' | b'.' | b'~' => {
944
+ vec![byte as char]
945
+ }
946
+ b' ' => vec!['+'],
947
+ _ => format!("%{byte:02X}").chars().collect(),
948
+ })
949
+ .collect()
950
+ }
951
+
952
+ async fn send_json_response(mut socket: tokio::net::TcpStream, status: &str, body_json: &str) {
953
+ let response = format!(
954
+ "HTTP/1.1 {}\r\n\
955
+ Access-Control-Allow-Origin: *\r\n\
956
+ Access-Control-Allow-Headers: Content-Type\r\n\
957
+ Access-Control-Allow-Methods: GET, POST, OPTIONS\r\n\
958
+ Content-Type: application/json\r\n\
959
+ Content-Length: {}\r\n\
960
+ Connection: close\r\n\r\n\
961
+ {}",
962
+ status,
963
+ body_json.len(),
964
+ body_json
965
+ );
966
+ let _ = socket.write_all(response.as_bytes()).await;
967
+ let _ = socket.flush().await;
968
+ }
969
+
970
+ async fn send_binary_response(
971
+ mut socket: tokio::net::TcpStream,
972
+ status: &str,
973
+ content_type: &str,
974
+ body: &[u8],
975
+ ) {
976
+ let response = format!(
977
+ "HTTP/1.1 {}\r\n\
978
+ Access-Control-Allow-Origin: *\r\n\
979
+ Access-Control-Allow-Headers: Content-Type\r\n\
980
+ Access-Control-Allow-Methods: GET, POST, OPTIONS\r\n\
981
+ Content-Type: {}\r\n\
982
+ Content-Length: {}\r\n\
983
+ Connection: close\r\n\r\n",
984
+ status,
985
+ content_type,
986
+ body.len()
987
+ );
988
+ let _ = socket.write_all(response.as_bytes()).await;
989
+ let _ = socket.write_all(body).await;
990
+ let _ = socket.flush().await;
991
+ }