@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,368 @@
1
+ use std::{
2
+ collections::BTreeMap,
3
+ fs,
4
+ path::{Path, PathBuf},
5
+ };
6
+
7
+ use serde::{Deserialize, Serialize};
8
+ use serde_json::Value;
9
+ use thiserror::Error;
10
+
11
+ #[derive(Debug, Error)]
12
+ pub enum ConfigError {
13
+ #[error("unable to determine the user config directory")]
14
+ ConfigDirectoryUnavailable,
15
+ #[error("unable to create config directory {path}: {source}")]
16
+ CreateDirectory {
17
+ path: PathBuf,
18
+ source: std::io::Error,
19
+ },
20
+ #[error("unable to read config file {path}: {source}")]
21
+ Read {
22
+ path: PathBuf,
23
+ source: std::io::Error,
24
+ },
25
+ #[error("unable to parse config file {path}: {source}")]
26
+ Parse {
27
+ path: PathBuf,
28
+ source: serde_json::Error,
29
+ },
30
+ #[error("unable to serialize config: {0}")]
31
+ Serialize(serde_json::Error),
32
+ #[error("unable to write config file {path}: {source}")]
33
+ Write {
34
+ path: PathBuf,
35
+ source: std::io::Error,
36
+ },
37
+ }
38
+
39
+ #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
40
+ #[serde(default, rename_all = "camelCase")]
41
+ pub struct MintConfig {
42
+ pub theme: String,
43
+ pub accent_color: String,
44
+ pub language: String,
45
+ pub assistant_mode: String,
46
+ pub ai_provider: String,
47
+ pub api_key: String,
48
+ pub gemini_model: String,
49
+ pub anthropic_api_key: String,
50
+ pub anthropic_model: String,
51
+ pub openai_api_key: String,
52
+ pub openai_model: String,
53
+ pub openrouter_api_key: String,
54
+ pub openrouter_model: String,
55
+ pub deepseek_api_key: String,
56
+ pub deepseek_model: String,
57
+ pub hf_api_key: String,
58
+ pub hf_model: String,
59
+ pub local_api_base_url: String,
60
+ pub local_model_name: String,
61
+ pub ollama_host: String,
62
+ pub ollama_model: String,
63
+ pub show_desktop_widget: bool,
64
+ pub safety_enabled: bool,
65
+ pub sandbox_mode: String,
66
+ pub sandbox_command: String,
67
+ pub allowed_read_paths: Vec<PathBuf>,
68
+ pub allowed_write_paths: Vec<PathBuf>,
69
+ pub blocked_paths: Vec<PathBuf>,
70
+ pub blocked_file_names: Vec<String>,
71
+ pub disabled_tools: Vec<String>,
72
+ #[serde(flatten)]
73
+ pub extra: BTreeMap<String, Value>,
74
+ }
75
+
76
+ impl Default for MintConfig {
77
+ fn default() -> Self {
78
+ let home = dirs::home_dir().unwrap_or_else(|| PathBuf::from("."));
79
+ let current_directory = std::env::current_dir().unwrap_or_else(|_| home.clone());
80
+ let allowed_paths = vec![
81
+ home.clone(),
82
+ current_directory,
83
+ home.join("Desktop"),
84
+ home.join("Documents"),
85
+ home.join("Downloads"),
86
+ home.join("Pictures"),
87
+ home.join("Music"),
88
+ home.join("Videos"),
89
+ ];
90
+ Self {
91
+ theme: "dark".into(),
92
+ accent_color: "#8b5cf6".into(),
93
+ language: "th-TH".into(),
94
+ assistant_mode: "chat".into(),
95
+ ai_provider: "gemini".into(),
96
+ api_key: String::new(),
97
+ gemini_model: "gemini-2.5-flash".into(),
98
+ anthropic_api_key: String::new(),
99
+ anthropic_model: "claude-3-5-sonnet-latest".into(),
100
+ openai_api_key: String::new(),
101
+ openai_model: "gpt-4o".into(),
102
+ openrouter_api_key: String::new(),
103
+ openrouter_model: "openai/gpt-4o-mini".into(),
104
+ deepseek_api_key: String::new(),
105
+ deepseek_model: "deepseek-v4-flash".into(),
106
+ hf_api_key: String::new(),
107
+ hf_model: "meta-llama/Meta-Llama-3-8B-Instruct".into(),
108
+ local_api_base_url: String::new(),
109
+ local_model_name: "local-model".into(),
110
+ ollama_host: String::new(),
111
+ ollama_model: "llama3:latest".into(),
112
+ show_desktop_widget: true,
113
+ safety_enabled: true,
114
+ sandbox_mode: "prefer".into(),
115
+ sandbox_command: default_sandbox_command().into(),
116
+ allowed_read_paths: allowed_paths.clone(),
117
+ allowed_write_paths: allowed_paths,
118
+ blocked_paths: vec![
119
+ home.join(".ssh"),
120
+ home.join(".gnupg"),
121
+ home.join(".config/mint/mint-config.json"),
122
+ home.join(".mint/mint-config.json"),
123
+ ],
124
+ blocked_file_names: vec![".env".into(), "id_rsa".into(), "id_ed25519".into()],
125
+ disabled_tools: Vec::new(),
126
+ extra: runtime_extra_defaults(),
127
+ }
128
+ }
129
+ }
130
+
131
+ impl MintConfig {
132
+ pub fn available_providers(&self) -> Vec<&'static str> {
133
+ let mut providers = Vec::new();
134
+ if has_value(&self.anthropic_api_key) {
135
+ providers.push("anthropic");
136
+ }
137
+ if has_value(&self.openai_api_key) {
138
+ providers.push("openai");
139
+ }
140
+ if has_value(&self.openrouter_api_key) {
141
+ providers.push("openrouter");
142
+ }
143
+ if has_value(&self.deepseek_api_key) {
144
+ providers.push("deepseek");
145
+ }
146
+ if has_value(&self.api_key) {
147
+ providers.push("gemini");
148
+ }
149
+ if has_value(&self.hf_api_key) {
150
+ providers.push("huggingface");
151
+ }
152
+ if has_value(&self.local_api_base_url) {
153
+ providers.push("local_openai");
154
+ }
155
+ providers.push("ollama");
156
+ providers
157
+ }
158
+ }
159
+
160
+ pub fn config_path() -> Result<PathBuf, ConfigError> {
161
+ dirs::config_dir()
162
+ .map(|directory| directory.join("mint").join("mint-config.json"))
163
+ .ok_or(ConfigError::ConfigDirectoryUnavailable)
164
+ }
165
+
166
+ pub fn load_config() -> Result<MintConfig, ConfigError> {
167
+ load_config_from(&config_path()?)
168
+ }
169
+
170
+ pub fn initialize_config() -> Result<MintConfig, ConfigError> {
171
+ let config = load_config()?;
172
+ save_config(&config)?;
173
+ Ok(config)
174
+ }
175
+
176
+ pub fn save_config(config: &MintConfig) -> Result<(), ConfigError> {
177
+ save_config_to(&config_path()?, config)
178
+ }
179
+
180
+ pub fn set_config_value(key: &str, value: Value) -> Result<MintConfig, ConfigError> {
181
+ let mut raw = serde_json::to_value(load_config()?).map_err(ConfigError::Serialize)?;
182
+ raw.as_object_mut()
183
+ .expect("MintConfig always serializes to an object")
184
+ .insert(key.to_owned(), value);
185
+ let config = serde_json::from_value(raw).map_err(ConfigError::Serialize)?;
186
+ save_config(&config)?;
187
+ Ok(config)
188
+ }
189
+
190
+ fn load_config_from(path: &Path) -> Result<MintConfig, ConfigError> {
191
+ if !path.exists() {
192
+ let config = MintConfig::default();
193
+ save_config_to(path, &config)?;
194
+ return Ok(config);
195
+ }
196
+
197
+ let raw = fs::read_to_string(path).map_err(|source| ConfigError::Read {
198
+ path: path.to_path_buf(),
199
+ source,
200
+ })?;
201
+ let mut config: MintConfig =
202
+ serde_json::from_str(&raw).map_err(|source| ConfigError::Parse {
203
+ path: path.to_path_buf(),
204
+ source,
205
+ })?;
206
+ for (key, value) in runtime_extra_defaults() {
207
+ config.extra.entry(key).or_insert(value);
208
+ }
209
+ Ok(config)
210
+ }
211
+
212
+ fn save_config_to(path: &Path, config: &MintConfig) -> Result<(), ConfigError> {
213
+ if let Some(directory) = path.parent() {
214
+ fs::create_dir_all(directory).map_err(|source| ConfigError::CreateDirectory {
215
+ path: directory.to_path_buf(),
216
+ source,
217
+ })?;
218
+ }
219
+ let raw = serde_json::to_string_pretty(config).map_err(ConfigError::Serialize)?;
220
+ fs::write(path, format!("{raw}\n")).map_err(|source| ConfigError::Write {
221
+ path: path.to_path_buf(),
222
+ source,
223
+ })
224
+ }
225
+
226
+ fn has_value(value: &str) -> bool {
227
+ let value = value.trim();
228
+ !value.is_empty() && !value.starts_with("your_") && !value.contains("key_here")
229
+ }
230
+
231
+ fn default_sandbox_command() -> &'static str {
232
+ if cfg!(target_os = "macos") {
233
+ "sandbox-exec"
234
+ } else if cfg!(target_os = "linux") {
235
+ "bwrap"
236
+ } else {
237
+ ""
238
+ }
239
+ }
240
+
241
+ fn runtime_extra_defaults() -> BTreeMap<String, Value> {
242
+ serde_json::from_value(serde_json::json!({
243
+ "automationBrowser": "chromium",
244
+ "browserDebugUrl": "http://127.0.0.1:9222/json/list",
245
+ "browserExtensionContextUrl": "http://127.0.0.1:3212/context",
246
+ "proactiveInterval": 60,
247
+ "proactiveCooldown": 120,
248
+ "enableHeadlessTaskQueue": false,
249
+ "enableAutoUpdate": false,
250
+ "updaterEndpoint": "",
251
+ "updaterPublicKey": "",
252
+ "enableVoiceReply": true,
253
+ "enableCustomWorkflows": true,
254
+ "ttsProvider": "google",
255
+ "ttsVolume": 1.0,
256
+ "ttsSpeed": 1.0,
257
+ "ttsPitch": 1.0,
258
+ "pluginCalendarEnabled": false,
259
+ "pluginGmailEnabled": false,
260
+ "pluginNotionEnabled": false,
261
+ "telegramBotToken": "",
262
+ "enableTelegramBridge": false,
263
+ "discordBotToken": "",
264
+ "discordApplicationId": "",
265
+ "enableDiscordBridge": false,
266
+ "slackBotToken": "",
267
+ "slackAppToken": "",
268
+ "enableSlackBridge": false,
269
+ "lineChannelAccessToken": "",
270
+ "lineChannelSecret": "",
271
+ "enableLineBridge": false,
272
+ "lineWebhookPort": 3000,
273
+ "whatsappCloudAccessToken": "",
274
+ "whatsappPhoneNumberId": "",
275
+ "whatsappVerifyToken": "",
276
+ "whatsappAppSecret": "",
277
+ "enableWhatsappBridge": false,
278
+ "googleSearchApiKey": "",
279
+ "googleSearchCx": "",
280
+ "braveSearchApiKey": "",
281
+ "googleCalendarClientId": "",
282
+ "googleCalendarClientSecret": "",
283
+ "googleCalendarRefreshToken": "",
284
+ "googleCalendarId": "primary",
285
+ "gmailClientId": "",
286
+ "gmailClientSecret": "",
287
+ "gmailRefreshToken": "",
288
+ "gmailUserId": "me",
289
+ "notionApiKey": "",
290
+ "notionDatabaseId": "",
291
+ "notionPageId": "",
292
+ "notionTitleProperty": "Name",
293
+ "allowedShellModes": ["readOnly", "test", "mutating", "network"],
294
+ "allowedNativePlugins": ["dev_tools", "system_metrics"],
295
+ "allowedMcpTools": {},
296
+ "mcpServers": {}
297
+ }))
298
+ .expect("runtime config defaults must be a JSON object")
299
+ }
300
+
301
+ #[cfg(test)]
302
+ mod tests {
303
+ use super::*;
304
+
305
+ #[test]
306
+ fn default_config_always_exposes_ollama() {
307
+ assert_eq!(MintConfig::default().available_providers(), vec!["ollama"]);
308
+ }
309
+
310
+ #[test]
311
+ fn configured_providers_are_reported_before_ollama() {
312
+ let config = MintConfig {
313
+ api_key: "gemini-secret".into(),
314
+ openai_api_key: "openai-secret".into(),
315
+ openrouter_api_key: "openrouter-secret".into(),
316
+ deepseek_api_key: "deepseek-secret".into(),
317
+ local_api_base_url: "http://localhost:1234/v1".into(),
318
+ ..MintConfig::default()
319
+ };
320
+ assert_eq!(
321
+ config.available_providers(),
322
+ vec![
323
+ "openai",
324
+ "openrouter",
325
+ "deepseek",
326
+ "gemini",
327
+ "local_openai",
328
+ "ollama"
329
+ ]
330
+ );
331
+ }
332
+
333
+ #[test]
334
+ fn config_round_trips_through_json() {
335
+ let expected = MintConfig {
336
+ ai_provider: "ollama".into(),
337
+ ..MintConfig::default()
338
+ };
339
+ let json = serde_json::to_string(&expected).unwrap();
340
+ let actual: MintConfig = serde_json::from_str(&json).unwrap();
341
+ assert_eq!(actual, expected);
342
+ }
343
+
344
+ #[test]
345
+ fn config_preserves_fields_that_have_not_migrated_yet() {
346
+ let config: MintConfig =
347
+ serde_json::from_str(r#"{"aiProvider":"gemini","pluginGmailEnabled":true}"#).unwrap();
348
+ let json = serde_json::to_value(config).unwrap();
349
+ assert_eq!(json["pluginGmailEnabled"], true);
350
+ }
351
+
352
+ #[test]
353
+ fn default_config_includes_native_runtime_flags() {
354
+ let config = MintConfig::default();
355
+ assert_eq!(config.extra["enableHeadlessTaskQueue"], false);
356
+ assert_eq!(config.extra["ttsProvider"], "google");
357
+ assert_eq!(config.extra["lineWebhookPort"], 3000);
358
+ assert_eq!(
359
+ config.extra["allowedShellModes"],
360
+ serde_json::json!(["readOnly", "test", "mutating", "network"])
361
+ );
362
+ assert_eq!(
363
+ config.extra["allowedNativePlugins"],
364
+ serde_json::json!(["dev_tools", "system_metrics"])
365
+ );
366
+ assert_eq!(config.extra["allowedMcpTools"], serde_json::json!({}));
367
+ }
368
+ }
@@ -0,0 +1,159 @@
1
+ use std::{
2
+ fs,
3
+ path::{Path, PathBuf},
4
+ };
5
+
6
+ use serde::Serialize;
7
+ use thiserror::Error;
8
+
9
+ use crate::{Capability, MintConfig, SafetyError, assert_path_capability};
10
+
11
+ const IGNORED_DIRECTORIES: &[&str] = &[
12
+ ".git",
13
+ "node_modules",
14
+ ".cache",
15
+ "dist",
16
+ "build",
17
+ "coverage",
18
+ "target",
19
+ ];
20
+
21
+ #[derive(Debug, Error)]
22
+ pub enum FileOperationError {
23
+ #[error(transparent)]
24
+ Safety(#[from] SafetyError),
25
+ #[error("unable to create directory {path}: {source}")]
26
+ CreateDirectory {
27
+ path: PathBuf,
28
+ source: std::io::Error,
29
+ },
30
+ }
31
+
32
+ #[derive(Debug, Clone, Serialize, PartialEq, Eq)]
33
+ #[serde(rename_all = "lowercase")]
34
+ pub enum PathKind {
35
+ File,
36
+ Directory,
37
+ }
38
+
39
+ #[derive(Debug, Clone, Serialize, PartialEq, Eq)]
40
+ pub struct PathMatch {
41
+ pub path: PathBuf,
42
+ pub kind: PathKind,
43
+ }
44
+
45
+ pub fn create_folder(target: &Path, config: &MintConfig) -> Result<PathBuf, FileOperationError> {
46
+ let target = if target.is_absolute() || target.components().count() > 1 {
47
+ target.to_path_buf()
48
+ } else {
49
+ dirs::home_dir()
50
+ .unwrap_or_else(|| PathBuf::from("."))
51
+ .join("Desktop")
52
+ .join(target)
53
+ };
54
+ let target = assert_path_capability(&target, Capability::Write, config)?;
55
+ fs::create_dir_all(&target).map_err(|source| FileOperationError::CreateDirectory {
56
+ path: target.clone(),
57
+ source,
58
+ })?;
59
+ Ok(target)
60
+ }
61
+
62
+ pub fn find_paths(
63
+ query: &str,
64
+ roots: &[PathBuf],
65
+ limit: usize,
66
+ config: &MintConfig,
67
+ ) -> Vec<PathMatch> {
68
+ let query = query.trim().to_lowercase();
69
+ if query.is_empty() || limit == 0 {
70
+ return Vec::new();
71
+ }
72
+ let mut exact = Vec::new();
73
+ let mut partial = Vec::new();
74
+ for root in roots {
75
+ let Ok(root) = assert_path_capability(root, Capability::Read, config) else {
76
+ continue;
77
+ };
78
+ visit(&root, &query, limit, config, &mut exact, &mut partial);
79
+ if exact.len() >= limit || partial.len() >= limit {
80
+ break;
81
+ }
82
+ }
83
+ let mut matches = if exact.is_empty() { partial } else { exact };
84
+ matches.sort_by(|left, right| left.path.cmp(&right.path));
85
+ matches.truncate(limit);
86
+ matches
87
+ }
88
+
89
+ fn visit(
90
+ directory: &Path,
91
+ query: &str,
92
+ limit: usize,
93
+ config: &MintConfig,
94
+ exact: &mut Vec<PathMatch>,
95
+ partial: &mut Vec<PathMatch>,
96
+ ) {
97
+ let Ok(entries) = fs::read_dir(directory) else {
98
+ return;
99
+ };
100
+ for entry in entries.flatten() {
101
+ let path = entry.path();
102
+ let name = entry.file_name().to_string_lossy().to_string();
103
+ let Ok(file_type) = entry.file_type() else {
104
+ continue;
105
+ };
106
+ if file_type.is_symlink() {
107
+ continue;
108
+ }
109
+ if file_type.is_dir() && IGNORED_DIRECTORIES.contains(&name.as_str()) {
110
+ continue;
111
+ }
112
+ if assert_path_capability(&path, Capability::Read, config).is_err() {
113
+ continue;
114
+ }
115
+ let kind = if file_type.is_dir() {
116
+ PathKind::Directory
117
+ } else {
118
+ PathKind::File
119
+ };
120
+ let lower_name = name.to_lowercase();
121
+ if lower_name == query {
122
+ exact.push(PathMatch {
123
+ path: path.clone(),
124
+ kind: kind.clone(),
125
+ });
126
+ } else if lower_name.contains(query) {
127
+ partial.push(PathMatch {
128
+ path: path.clone(),
129
+ kind: kind.clone(),
130
+ });
131
+ }
132
+ if exact.len() >= limit || partial.len() >= limit {
133
+ return;
134
+ }
135
+ if file_type.is_dir() {
136
+ visit(&path, query, limit, config, exact, partial);
137
+ }
138
+ }
139
+ }
140
+
141
+ #[cfg(test)]
142
+ mod tests {
143
+ use super::*;
144
+
145
+ #[test]
146
+ fn finds_files_inside_allowed_root() {
147
+ let root = std::env::temp_dir().join(format!("mint-find-{}", std::process::id()));
148
+ fs::create_dir_all(root.join("nested")).unwrap();
149
+ fs::write(root.join("nested/report.txt"), "ok").unwrap();
150
+ let config = MintConfig {
151
+ allowed_read_paths: vec![root.clone()],
152
+ blocked_paths: vec![],
153
+ ..MintConfig::default()
154
+ };
155
+ let matches = find_paths("report", std::slice::from_ref(&root), 5, &config);
156
+ assert_eq!(matches.len(), 1);
157
+ let _ = fs::remove_dir_all(root);
158
+ }
159
+ }