@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,308 @@
1
+ use std::{
2
+ fs,
3
+ path::PathBuf,
4
+ time::{SystemTime, UNIX_EPOCH},
5
+ };
6
+
7
+ use serde::{Deserialize, Serialize};
8
+ use serde_json::Value;
9
+ use thiserror::Error;
10
+
11
+ #[derive(Debug, Error)]
12
+ pub enum TaskError {
13
+ #[error("unable to determine the user config directory")]
14
+ ConfigDirectoryUnavailable,
15
+ #[error("unable to create task directory {path}: {source}")]
16
+ CreateDirectory {
17
+ path: PathBuf,
18
+ source: std::io::Error,
19
+ },
20
+ #[error("unable to read tasks file {path}: {source}")]
21
+ Read {
22
+ path: PathBuf,
23
+ source: std::io::Error,
24
+ },
25
+ #[error("unable to parse tasks file {path}: {source}")]
26
+ Parse {
27
+ path: PathBuf,
28
+ source: serde_json::Error,
29
+ },
30
+ #[error("unable to serialize tasks: {0}")]
31
+ Serialize(serde_json::Error),
32
+ #[error("unable to write tasks 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(rename_all = "camelCase")]
41
+ pub struct Task {
42
+ pub id: String,
43
+ pub description: String,
44
+ pub status: String,
45
+ pub created_at: String,
46
+ pub updated_at: String,
47
+ #[serde(default)]
48
+ pub steps: Vec<Value>,
49
+ #[serde(default)]
50
+ pub subtasks: Vec<Value>,
51
+ #[serde(default)]
52
+ pub checkpoints: Vec<Value>,
53
+ #[serde(default)]
54
+ pub artifacts: Vec<Value>,
55
+ #[serde(default)]
56
+ pub retry_count: u32,
57
+ #[serde(default = "default_max_retries")]
58
+ pub max_retries: u32,
59
+ #[serde(default)]
60
+ pub last_checkpoint_at: Option<String>,
61
+ #[serde(default)]
62
+ pub result: Option<Value>,
63
+ #[serde(flatten)]
64
+ pub extra: serde_json::Map<String, Value>,
65
+ }
66
+
67
+ #[derive(Clone)]
68
+ pub struct TaskStore {
69
+ path: PathBuf,
70
+ }
71
+
72
+ impl TaskStore {
73
+ pub fn open_default() -> Result<Self, TaskError> {
74
+ Ok(Self::open(tasks_path()?))
75
+ }
76
+
77
+ pub fn open(path: impl Into<PathBuf>) -> Self {
78
+ Self { path: path.into() }
79
+ }
80
+
81
+ pub fn list(&self) -> Result<Vec<Task>, TaskError> {
82
+ if !self.path.exists() {
83
+ return Ok(Vec::new());
84
+ }
85
+ let raw = fs::read_to_string(&self.path).map_err(|source| TaskError::Read {
86
+ path: self.path.clone(),
87
+ source,
88
+ })?;
89
+ serde_json::from_str(&raw).map_err(|source| TaskError::Parse {
90
+ path: self.path.clone(),
91
+ source,
92
+ })
93
+ }
94
+
95
+ pub fn add(&self, description: impl Into<String>) -> Result<Task, TaskError> {
96
+ let now = timestamp();
97
+ let task = Task {
98
+ id: now.clone(),
99
+ description: description.into(),
100
+ status: "pending".into(),
101
+ created_at: now.clone(),
102
+ updated_at: now,
103
+ steps: Vec::new(),
104
+ subtasks: Vec::new(),
105
+ checkpoints: Vec::new(),
106
+ artifacts: Vec::new(),
107
+ retry_count: 0,
108
+ max_retries: default_max_retries(),
109
+ last_checkpoint_at: None,
110
+ result: None,
111
+ extra: serde_json::Map::new(),
112
+ };
113
+ let mut tasks = self.list()?;
114
+ tasks.push(task.clone());
115
+ self.write(&tasks)?;
116
+ Ok(task)
117
+ }
118
+
119
+ pub fn get(&self, id: &str) -> Result<Option<Task>, TaskError> {
120
+ Ok(self.list()?.into_iter().find(|task| task.id == id))
121
+ }
122
+
123
+ pub fn pending(&self) -> Result<Option<Task>, TaskError> {
124
+ Ok(self
125
+ .list()?
126
+ .into_iter()
127
+ .find(|task| task.status == "pending"))
128
+ }
129
+
130
+ pub fn update_status(
131
+ &self,
132
+ id: &str,
133
+ status: &str,
134
+ result: Option<Value>,
135
+ ) -> Result<Option<Task>, TaskError> {
136
+ self.mutate(id, |task| {
137
+ task.status = status.into();
138
+ if result.is_some() {
139
+ task.result = result;
140
+ }
141
+ })
142
+ }
143
+
144
+ pub fn add_checkpoint(
145
+ &self,
146
+ id: &str,
147
+ mut checkpoint: Value,
148
+ ) -> Result<Option<Task>, TaskError> {
149
+ self.mutate(id, |task| {
150
+ let time = timestamp();
151
+ let checkpoint_id = format!("{id}-checkpoint-{}", task.checkpoints.len() + 1);
152
+ if let Some(value) = checkpoint.as_object_mut() {
153
+ value.entry("id").or_insert(Value::String(checkpoint_id));
154
+ value.entry("time").or_insert(Value::String(time.clone()));
155
+ }
156
+ task.last_checkpoint_at = Some(time);
157
+ task.steps.push(checkpoint.clone());
158
+ task.checkpoints.push(checkpoint);
159
+ })
160
+ }
161
+
162
+ pub fn add_artifact(&self, id: &str, mut artifact: Value) -> Result<Option<Task>, TaskError> {
163
+ self.mutate(id, |task| {
164
+ if let Some(value) = artifact.as_object_mut() {
165
+ value.entry("id").or_insert(Value::String(format!(
166
+ "{id}-artifact-{}",
167
+ task.artifacts.len() + 1
168
+ )));
169
+ value
170
+ .entry("time")
171
+ .or_insert_with(|| Value::String(timestamp()));
172
+ }
173
+ task.artifacts.push(artifact);
174
+ })
175
+ }
176
+
177
+ pub fn fail_with_retry(&self, id: &str, message: &str) -> Result<Option<Task>, TaskError> {
178
+ self.mutate(id, |task| {
179
+ task.retry_count += 1;
180
+ task.status = if task.retry_count <= task.max_retries {
181
+ "pending"
182
+ } else {
183
+ "failed"
184
+ }
185
+ .into();
186
+ task.result = Some(Value::String(message.into()));
187
+ let checkpoint = serde_json::json!({
188
+ "id": format!("{id}-checkpoint-{}", task.checkpoints.len() + 1),
189
+ "time": timestamp(),
190
+ "phase": if task.status == "pending" { "retry_scheduled" } else { "failed" },
191
+ "message": message,
192
+ "retryCount": task.retry_count,
193
+ "maxRetries": task.max_retries,
194
+ });
195
+ task.steps.push(checkpoint.clone());
196
+ task.checkpoints.push(checkpoint);
197
+ })
198
+ }
199
+
200
+ pub fn resume_running(&self) -> Result<Vec<Task>, TaskError> {
201
+ let mut tasks = self.list()?;
202
+ let mut resumed = Vec::new();
203
+ for task in &mut tasks {
204
+ if task.status != "running" {
205
+ continue;
206
+ }
207
+ task.status = "pending".into();
208
+ task.updated_at = timestamp();
209
+ let checkpoint = serde_json::json!({
210
+ "id": format!("{}-checkpoint-{}", task.id, task.checkpoints.len() + 1),
211
+ "time": timestamp(),
212
+ "phase": "resume_after_restart",
213
+ "message": "Task was running during shutdown and has been re-queued.",
214
+ });
215
+ task.steps.push(checkpoint.clone());
216
+ task.checkpoints.push(checkpoint);
217
+ resumed.push(task.clone());
218
+ }
219
+ self.write(&tasks)?;
220
+ Ok(resumed)
221
+ }
222
+
223
+ pub fn clear_completed(&self) -> Result<usize, TaskError> {
224
+ let tasks = self.list()?;
225
+ let count = tasks.len();
226
+ let retained = tasks
227
+ .into_iter()
228
+ .filter(|task| matches!(task.status.as_str(), "pending" | "running"))
229
+ .collect::<Vec<_>>();
230
+ let removed = count.saturating_sub(retained.len());
231
+ self.write(&retained)?;
232
+ Ok(removed)
233
+ }
234
+
235
+ fn mutate(&self, id: &str, update: impl FnOnce(&mut Task)) -> Result<Option<Task>, TaskError> {
236
+ let mut tasks = self.list()?;
237
+ let Some(task) = tasks.iter_mut().find(|task| task.id == id) else {
238
+ return Ok(None);
239
+ };
240
+ update(task);
241
+ task.updated_at = timestamp();
242
+ let task = task.clone();
243
+ self.write(&tasks)?;
244
+ Ok(Some(task))
245
+ }
246
+
247
+ fn write(&self, tasks: &[Task]) -> Result<(), TaskError> {
248
+ if let Some(directory) = self.path.parent() {
249
+ fs::create_dir_all(directory).map_err(|source| TaskError::CreateDirectory {
250
+ path: directory.to_path_buf(),
251
+ source,
252
+ })?;
253
+ }
254
+ let raw = serde_json::to_string_pretty(tasks).map_err(TaskError::Serialize)?;
255
+ fs::write(&self.path, format!("{raw}\n")).map_err(|source| TaskError::Write {
256
+ path: self.path.clone(),
257
+ source,
258
+ })
259
+ }
260
+ }
261
+
262
+ pub fn tasks_path() -> Result<PathBuf, TaskError> {
263
+ dirs::config_dir()
264
+ .map(|directory| directory.join("mint").join("tasks.json"))
265
+ .ok_or(TaskError::ConfigDirectoryUnavailable)
266
+ }
267
+
268
+ fn timestamp() -> String {
269
+ SystemTime::now()
270
+ .duration_since(UNIX_EPOCH)
271
+ .unwrap_or_default()
272
+ .as_millis()
273
+ .to_string()
274
+ }
275
+
276
+ fn default_max_retries() -> u32 {
277
+ 1
278
+ }
279
+
280
+ #[cfg(test)]
281
+ mod tests {
282
+ use super::*;
283
+
284
+ fn test_path(name: &str) -> PathBuf {
285
+ std::env::temp_dir().join(format!("mint-task-{name}-{}.json", std::process::id()))
286
+ }
287
+
288
+ #[test]
289
+ fn clears_finished_tasks_but_keeps_active_tasks() {
290
+ let path = test_path("clear");
291
+ let store = TaskStore::open(&path);
292
+ store
293
+ .write(&[
294
+ Task {
295
+ status: "completed".into(),
296
+ ..store.add("done").unwrap()
297
+ },
298
+ Task {
299
+ status: "pending".into(),
300
+ ..store.add("todo").unwrap()
301
+ },
302
+ ])
303
+ .unwrap();
304
+ assert_eq!(store.clear_completed().unwrap(), 1);
305
+ assert_eq!(store.list().unwrap().len(), 1);
306
+ let _ = fs::remove_file(path);
307
+ }
308
+ }
@@ -0,0 +1,92 @@
1
+ use serde::Serialize;
2
+
3
+ const MAX_GOOGLE_TTS_CHARS: usize = 200;
4
+
5
+ #[derive(Debug, Clone, Serialize)]
6
+ #[serde(rename_all = "camelCase")]
7
+ pub struct TtsUrl {
8
+ pub short_text: String,
9
+ pub url: String,
10
+ }
11
+
12
+ pub fn google_tts_urls(text: &str, language: &str) -> Vec<TtsUrl> {
13
+ let chunks = split_text(text, MAX_GOOGLE_TTS_CHARS);
14
+ let total = chunks.len();
15
+ chunks
16
+ .into_iter()
17
+ .enumerate()
18
+ .map(|(index, chunk)| TtsUrl {
19
+ url: format!(
20
+ "https://translate.google.com/translate_tts?ie=UTF-8&q={}&tl={}&client=tw-ob&idx={index}&total={total}&textlen={}",
21
+ encode_component(&chunk),
22
+ encode_component(language),
23
+ chunk.chars().count()
24
+ ),
25
+ short_text: chunk,
26
+ })
27
+ .collect()
28
+ }
29
+
30
+ fn split_text(text: &str, max_length: usize) -> Vec<String> {
31
+ let mut remaining = text.split_whitespace().collect::<Vec<_>>().join(" ");
32
+ let mut chunks = Vec::new();
33
+ while remaining.chars().count() > max_length {
34
+ let char_boundary = remaining
35
+ .char_indices()
36
+ .nth(max_length)
37
+ .map(|(index, _)| index)
38
+ .unwrap_or(remaining.len());
39
+ let boundary = remaining
40
+ .char_indices()
41
+ .take_while(|(index, _)| *index <= char_boundary)
42
+ .filter(|(_, character)| matches!(character, '.' | '?' | '!' | ',' | ' '))
43
+ .map(|(index, _)| index)
44
+ .last()
45
+ .filter(|index| *index > 0)
46
+ .unwrap_or(char_boundary);
47
+ chunks.push(remaining[..boundary].trim().to_owned());
48
+ remaining = remaining[boundary..].trim().to_owned();
49
+ }
50
+ if !remaining.is_empty() {
51
+ chunks.push(remaining);
52
+ }
53
+ chunks
54
+ }
55
+
56
+ fn encode_component(value: &str) -> String {
57
+ value
58
+ .bytes()
59
+ .map(|byte| match byte {
60
+ b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' | b'-' | b'_' | b'.' | b'~' => {
61
+ (byte as char).to_string()
62
+ }
63
+ b' ' => "+".into(),
64
+ _ => format!("%{byte:02X}"),
65
+ })
66
+ .collect()
67
+ }
68
+
69
+ #[cfg(test)]
70
+ mod tests {
71
+ use super::*;
72
+
73
+ #[test]
74
+ fn splits_long_text_for_google_tts() {
75
+ let chunks = split_text(&"a ".repeat(150), MAX_GOOGLE_TTS_CHARS);
76
+ assert!(chunks.len() > 1);
77
+ assert!(
78
+ chunks
79
+ .iter()
80
+ .all(|chunk| chunk.len() <= MAX_GOOGLE_TTS_CHARS)
81
+ );
82
+ }
83
+
84
+ #[test]
85
+ fn encodes_tts_query() {
86
+ assert!(
87
+ google_tts_urls("hello mint", "en")[0]
88
+ .url
89
+ .contains("q=hello+mint")
90
+ );
91
+ }
92
+ }
@@ -0,0 +1,93 @@
1
+ use serde::Serialize;
2
+ use serde_json::Value;
3
+ use thiserror::Error;
4
+
5
+ #[derive(Debug, Error)]
6
+ pub enum WeatherError {
7
+ #[error("weather city is required")]
8
+ MissingCity,
9
+ #[error("weather request failed: {0}")]
10
+ Request(#[from] reqwest::Error),
11
+ #[error("weather location was not found: {0}")]
12
+ LocationNotFound(String),
13
+ #[error("weather response did not include {0}")]
14
+ MissingValue(String),
15
+ }
16
+
17
+ #[derive(Debug, Serialize)]
18
+ #[serde(rename_all = "camelCase")]
19
+ pub struct WeatherReport {
20
+ pub location: String,
21
+ pub data: String,
22
+ pub temperature_celsius: f64,
23
+ pub apparent_temperature_celsius: f64,
24
+ pub humidity_percent: f64,
25
+ pub wind_speed_kmh: f64,
26
+ pub weather_code: i64,
27
+ }
28
+
29
+ pub async fn weather(city: &str) -> Result<WeatherReport, WeatherError> {
30
+ let city = city.trim();
31
+ if city.is_empty() {
32
+ return Err(WeatherError::MissingCity);
33
+ }
34
+ let client = crate::HTTP_CLIENT.clone();
35
+ let geocode: Value = client
36
+ .get("https://geocoding-api.open-meteo.com/v1/search")
37
+ .query(&[("name", city), ("count", "1"), ("language", "en")])
38
+ .send()
39
+ .await?
40
+ .error_for_status()?
41
+ .json()
42
+ .await?;
43
+ let place = geocode["results"]
44
+ .as_array()
45
+ .and_then(|results| results.first())
46
+ .ok_or_else(|| WeatherError::LocationNotFound(city.into()))?;
47
+ let latitude = number(place, "latitude")?;
48
+ let longitude = number(place, "longitude")?;
49
+ let current: Value = client
50
+ .get("https://api.open-meteo.com/v1/forecast")
51
+ .query(&[
52
+ ("latitude", latitude.to_string()),
53
+ ("longitude", longitude.to_string()),
54
+ (
55
+ "current",
56
+ "temperature_2m,relative_humidity_2m,apparent_temperature,weather_code,wind_speed_10m".into(),
57
+ ),
58
+ ])
59
+ .send()
60
+ .await?
61
+ .error_for_status()?
62
+ .json()
63
+ .await?;
64
+ let current = &current["current"];
65
+ let temperature = number(current, "temperature_2m")?;
66
+ let apparent = number(current, "apparent_temperature")?;
67
+ let humidity = number(current, "relative_humidity_2m")?;
68
+ let wind = number(current, "wind_speed_10m")?;
69
+ let code = current["weather_code"].as_i64().unwrap_or_default();
70
+ let location = [place["name"].as_str(), place["country"].as_str()]
71
+ .into_iter()
72
+ .flatten()
73
+ .collect::<Vec<_>>()
74
+ .join(", ");
75
+ Ok(WeatherReport {
76
+ data: format!(
77
+ "{location}: {:.1} C, feels like {:.1} C, humidity {:.0}%, wind {:.1} km/h",
78
+ temperature, apparent, humidity, wind
79
+ ),
80
+ location,
81
+ temperature_celsius: temperature,
82
+ apparent_temperature_celsius: apparent,
83
+ humidity_percent: humidity,
84
+ wind_speed_kmh: wind,
85
+ weather_code: code,
86
+ })
87
+ }
88
+
89
+ fn number(value: &Value, key: &str) -> Result<f64, WeatherError> {
90
+ value[key]
91
+ .as_f64()
92
+ .ok_or_else(|| WeatherError::MissingValue(key.into()))
93
+ }
@@ -0,0 +1,200 @@
1
+ use serde::{Deserialize, Serialize};
2
+ use thiserror::Error;
3
+
4
+ use crate::MintConfig;
5
+
6
+ #[derive(Debug, Error)]
7
+ pub enum WebSearchError {
8
+ #[error("no web search API key configured (set googleSearchApiKey or braveSearchApiKey)")]
9
+ NoApiKey,
10
+ #[error("web search request failed: {0}")]
11
+ Request(String),
12
+ #[error("web search response was empty or unparseable")]
13
+ EmptyResponse,
14
+ }
15
+
16
+ fn sanitize_reqwest_error(err: reqwest::Error) -> WebSearchError {
17
+ let mut msg = err.to_string();
18
+ if let Some(pos) = msg.find("https://www.googleapis.com") {
19
+ let mut end_pos = msg.len();
20
+ for (idx, ch) in msg[pos..].char_indices() {
21
+ if ch == ' '
22
+ || ch == ')'
23
+ || ch == '"'
24
+ || ch == '\''
25
+ || ch == ']'
26
+ || ch == '}'
27
+ || ch == '>'
28
+ {
29
+ end_pos = pos + idx;
30
+ break;
31
+ }
32
+ }
33
+ msg.replace_range(pos..end_pos, "https://www.googleapis.com/customsearch/v1");
34
+ }
35
+ if let Some(pos) = msg.find("https://api.search.brave.com") {
36
+ let mut end_pos = msg.len();
37
+ for (idx, ch) in msg[pos..].char_indices() {
38
+ if ch == ' '
39
+ || ch == ')'
40
+ || ch == '"'
41
+ || ch == '\''
42
+ || ch == ']'
43
+ || ch == '}'
44
+ || ch == '>'
45
+ {
46
+ end_pos = pos + idx;
47
+ break;
48
+ }
49
+ }
50
+ msg.replace_range(
51
+ pos..end_pos,
52
+ "https://api.search.brave.com/res/v1/web/search",
53
+ );
54
+ }
55
+ WebSearchError::Request(msg)
56
+ }
57
+
58
+ #[derive(Debug, Clone, Serialize, Deserialize)]
59
+ pub struct SearchHit {
60
+ pub title: String,
61
+ pub url: String,
62
+ pub snippet: String,
63
+ }
64
+
65
+ /// Search the web using the first configured provider (Google → Brave).
66
+ /// Returns the search hits and the name of the provider used.
67
+ pub async fn search(
68
+ query: &str,
69
+ limit: usize,
70
+ config: &MintConfig,
71
+ ) -> Result<(Vec<SearchHit>, String), WebSearchError> {
72
+ let google_key = config
73
+ .extra
74
+ .get("googleSearchApiKey")
75
+ .and_then(|v| v.as_str())
76
+ .unwrap_or("")
77
+ .trim()
78
+ .to_owned();
79
+ let google_cx = config
80
+ .extra
81
+ .get("googleSearchCx")
82
+ .and_then(|v| v.as_str())
83
+ .unwrap_or("")
84
+ .trim()
85
+ .to_owned();
86
+ let brave_key = config
87
+ .extra
88
+ .get("braveSearchApiKey")
89
+ .and_then(|v| v.as_str())
90
+ .unwrap_or("")
91
+ .trim()
92
+ .to_owned();
93
+
94
+ let mut last_err = None;
95
+
96
+ if !google_key.is_empty() && !google_cx.is_empty() {
97
+ match google_search(query, limit, &google_key, &google_cx).await {
98
+ Ok(hits) => {
99
+ if !hits.is_empty() {
100
+ return Ok((hits, "Google".to_owned()));
101
+ }
102
+ }
103
+ Err(e) => {
104
+ last_err = Some(e);
105
+ }
106
+ }
107
+ }
108
+
109
+ if !brave_key.is_empty() {
110
+ match brave_search(query, limit, &brave_key).await {
111
+ Ok(hits) => {
112
+ return Ok((hits, "Brave".to_owned()));
113
+ }
114
+ Err(e) => {
115
+ last_err = Some(e);
116
+ }
117
+ }
118
+ }
119
+
120
+ Err(last_err.unwrap_or(WebSearchError::NoApiKey))
121
+ }
122
+
123
+ async fn google_search(
124
+ query: &str,
125
+ limit: usize,
126
+ api_key: &str,
127
+ cx: &str,
128
+ ) -> Result<Vec<SearchHit>, WebSearchError> {
129
+ let client = crate::HTTP_CLIENT.clone();
130
+ let num_str = limit.min(10).to_string();
131
+ let response: serde_json::Value = client
132
+ .get("https://www.googleapis.com/customsearch/v1")
133
+ .query(&[
134
+ ("key", api_key),
135
+ ("cx", cx),
136
+ ("q", query),
137
+ ("num", num_str.as_str()),
138
+ ])
139
+ .send()
140
+ .await
141
+ .map_err(sanitize_reqwest_error)?
142
+ .error_for_status()
143
+ .map_err(sanitize_reqwest_error)?
144
+ .json()
145
+ .await
146
+ .map_err(sanitize_reqwest_error)?;
147
+
148
+ let items = response["items"]
149
+ .as_array()
150
+ .ok_or(WebSearchError::EmptyResponse)?;
151
+
152
+ Ok(items
153
+ .iter()
154
+ .take(limit)
155
+ .filter_map(|item| {
156
+ Some(SearchHit {
157
+ title: item["title"].as_str()?.to_owned(),
158
+ url: item["link"].as_str()?.to_owned(),
159
+ snippet: item["snippet"].as_str().unwrap_or("").to_owned(),
160
+ })
161
+ })
162
+ .collect())
163
+ }
164
+
165
+ async fn brave_search(
166
+ query: &str,
167
+ limit: usize,
168
+ api_key: &str,
169
+ ) -> Result<Vec<SearchHit>, WebSearchError> {
170
+ let client = crate::HTTP_CLIENT.clone();
171
+ let response: serde_json::Value = client
172
+ .get("https://api.search.brave.com/res/v1/web/search")
173
+ .header("Accept", "application/json")
174
+ .header("X-Subscription-Token", api_key)
175
+ .query(&[("q", query), ("count", &limit.to_string())])
176
+ .send()
177
+ .await
178
+ .map_err(sanitize_reqwest_error)?
179
+ .error_for_status()
180
+ .map_err(sanitize_reqwest_error)?
181
+ .json()
182
+ .await
183
+ .map_err(sanitize_reqwest_error)?;
184
+
185
+ let results = response["web"]["results"]
186
+ .as_array()
187
+ .ok_or(WebSearchError::EmptyResponse)?;
188
+
189
+ Ok(results
190
+ .iter()
191
+ .take(limit)
192
+ .filter_map(|item| {
193
+ Some(SearchHit {
194
+ title: item["title"].as_str()?.to_owned(),
195
+ url: item["url"].as_str()?.to_owned(),
196
+ snippet: item["description"].as_str().unwrap_or("").to_owned(),
197
+ })
198
+ })
199
+ .collect())
200
+ }