@pheem49/mint 1.6.2 → 1.6.3

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 (35) hide show
  1. package/.github/workflows/ci.yml +14 -0
  2. package/Cargo.lock +3 -3
  3. package/Cargo.toml +1 -1
  4. package/bin/mint +0 -0
  5. package/package.json +3 -3
  6. package/src/renderer/index-web.html +1 -1
  7. package/src/renderer/index.html +1 -1
  8. package/src/renderer/src/components/DashboardSidebar.tsx +2 -2
  9. package/src/renderer/src/components/MintDashboard.tsx +1 -1
  10. package/src/renderer/src-web/components/ChatPanel.tsx +1 -1
  11. package/src/renderer/src-web/components/DashboardSidebar.tsx +2 -2
  12. package/src/renderer/src-web/components/MintDashboard.tsx +1 -1
  13. package/src-tauri/Cargo.toml +29 -0
  14. package/src-tauri/build.rs +3 -0
  15. package/src-tauri/capabilities/main.json +7 -0
  16. package/src-tauri/gen/schemas/acl-manifests.json +1 -0
  17. package/src-tauri/gen/schemas/capabilities.json +1 -0
  18. package/src-tauri/gen/schemas/desktop-schema.json +2412 -0
  19. package/src-tauri/gen/schemas/linux-schema.json +2412 -0
  20. package/src-tauri/src/browser.rs +141 -0
  21. package/src-tauri/src/desktop.rs +392 -0
  22. package/src-tauri/src/discord_rpc.rs +99 -0
  23. package/src-tauri/src/events.rs +70 -0
  24. package/src-tauri/src/headless.rs +222 -0
  25. package/src-tauri/src/integrations.rs +126 -0
  26. package/src-tauri/src/lib.rs +1033 -0
  27. package/src-tauri/src/main.rs +3 -0
  28. package/src-tauri/src/plugins.rs +16 -0
  29. package/src-tauri/src/proactive.rs +254 -0
  30. package/src-tauri/src/system.rs +250 -0
  31. package/src-tauri/src/updater.rs +148 -0
  32. package/src-tauri/src/webhooks.rs +255 -0
  33. package/src-tauri/src/workflows.rs +70 -0
  34. package/src-tauri/tauri.conf.json +48 -0
  35. package/tsconfig.json +1 -1
@@ -0,0 +1,222 @@
1
+ use std::{path::Path, time::Duration};
2
+
3
+ use mint_core::{
4
+ CodeEdit, KnowledgeStore, MintConfig, Task, TaskStore, load_config, parse_agent_json,
5
+ propose_code_edits, run_agent_loop,
6
+ };
7
+ use serde::Deserialize;
8
+ use serde_json::{Value, json};
9
+ use tauri::{AppHandle, Emitter, Manager};
10
+
11
+ use crate::browser::{click, navigate, read_page_text};
12
+
13
+ const MAX_STEPS: usize = 20;
14
+ const SYSTEM_PROMPT: &str = r#"You are Mint's native background task agent. Return only JSON:
15
+ {"thought":"short progress note","action":"done|propose_folder|propose_write_file|open_url|browser_read|browser_click|knowledge_search|propose_bash","target":"path, URL, selector, query, command, or final result","data":"optional file content"}
16
+ Use only one action per response. Background tasks never mutate the filesystem or execute shell commands. Use propose_folder, propose_write_file, and propose_bash so Mint can record a proposal for explicit user approval."#;
17
+
18
+ #[derive(Debug, Deserialize)]
19
+ struct AgentAction {
20
+ #[serde(default)]
21
+ thought: String,
22
+ action: String,
23
+ #[serde(default)]
24
+ target: String,
25
+ #[serde(default)]
26
+ data: String,
27
+ }
28
+
29
+ pub fn start_headless_queue(app: AppHandle) {
30
+ tauri::async_runtime::spawn(async move {
31
+ if let Ok(store) = TaskStore::open_default() {
32
+ let _ = store.resume_running();
33
+ }
34
+ loop {
35
+ tokio::time::sleep(Duration::from_secs(15)).await;
36
+ let Ok(config) = load_config() else {
37
+ continue;
38
+ };
39
+ if config
40
+ .extra
41
+ .get("enableHeadlessTaskQueue")
42
+ .and_then(Value::as_bool)
43
+ != Some(true)
44
+ {
45
+ continue;
46
+ }
47
+ let _ = run_next_task(&app).await;
48
+ }
49
+ });
50
+ }
51
+
52
+ pub async fn run_next_task(app: &AppHandle) -> Result<Option<Task>, String> {
53
+ let store = TaskStore::open_default().map_err(|error| error.to_string())?;
54
+ let Some(task) = store.pending().map_err(|error| error.to_string())? else {
55
+ return Ok(None);
56
+ };
57
+ store
58
+ .update_status(&task.id, "running", None)
59
+ .map_err(|error| error.to_string())?;
60
+ checkpoint(&store, &task.id, "started", &task.description)?;
61
+ emit(
62
+ app,
63
+ &format!("Started queued task: {}", task.description),
64
+ "info",
65
+ );
66
+
67
+ match execute_task(&store, &task).await {
68
+ Ok(result) => {
69
+ store
70
+ .add_artifact(
71
+ &task.id,
72
+ json!({ "type": "final_result", "content": result }),
73
+ )
74
+ .map_err(|error| error.to_string())?;
75
+ let completed = store
76
+ .update_status(&task.id, "completed", Some(Value::String(result.clone())))
77
+ .map_err(|error| error.to_string())?;
78
+ emit(app, &format!("Queued task completed: {result}"), "info");
79
+ Ok(completed)
80
+ }
81
+ Err(error) => {
82
+ let failed = store
83
+ .fail_with_retry(&task.id, &error)
84
+ .map_err(|store_error| store_error.to_string())?;
85
+ emit(app, &format!("Queued task failed: {error}"), "warning");
86
+ Ok(failed)
87
+ }
88
+ }
89
+ }
90
+
91
+ async fn execute_task(store: &TaskStore, task: &Task) -> Result<String, String> {
92
+ let config = load_config().map_err(|error| error.to_string())?;
93
+ let observer_store = store.clone();
94
+ let observer_task = task.clone();
95
+ let executor_store = store.clone();
96
+ let executor_task = task.clone();
97
+ let executor_config = config.clone();
98
+ run_agent_loop(
99
+ &config,
100
+ SYSTEM_PROMPT,
101
+ format!("Task: {}\nChoose the first action.", task.description),
102
+ MAX_STEPS,
103
+ |raw| parse_agent_json(raw).map_err(|error| error.to_string()),
104
+ |action: &AgentAction| (action.action == "done").then(|| action.target.clone()),
105
+ move |step, action| {
106
+ let store = executor_store.clone();
107
+ let task = executor_task.clone();
108
+ let config = executor_config.clone();
109
+ Box::pin(async move {
110
+ let observation = execute_action(&config, &store, &task, step, &action).await?;
111
+ checkpoint(&store, &task.id, "observation", &observation)?;
112
+ Ok(observation)
113
+ })
114
+ },
115
+ move |step, action| {
116
+ observer_store
117
+ .add_checkpoint(
118
+ &observer_task.id,
119
+ json!({
120
+ "phase": "native_agent_step",
121
+ "step": step,
122
+ "thought": action.thought,
123
+ "action": action.action,
124
+ "target": action.target,
125
+ }),
126
+ )
127
+ .map(|_| ())
128
+ .map_err(|error| error.to_string())
129
+ },
130
+ )
131
+ .await
132
+ .map_err(|error| error.to_string())
133
+ }
134
+
135
+ async fn execute_action(
136
+ config: &MintConfig,
137
+ store: &TaskStore,
138
+ task: &Task,
139
+ step: usize,
140
+ action: &AgentAction,
141
+ ) -> Result<String, String> {
142
+ match action.action.as_str() {
143
+ "propose_folder" => {
144
+ store
145
+ .add_artifact(
146
+ &task.id,
147
+ json!({ "type": "folder_proposal", "path": action.target, "description": format!("Proposed by native task step {step}; explicit approval required") }),
148
+ )
149
+ .map_err(|error| error.to_string())?;
150
+ Ok(format!(
151
+ "folder proposal recorded but not applied: {}",
152
+ action.target
153
+ ))
154
+ }
155
+ "propose_write_file" => {
156
+ let root = std::env::current_dir().map_err(|error| error.to_string())?;
157
+ let proposal = propose_code_edits(
158
+ &root,
159
+ &[CodeEdit {
160
+ path: Path::new(&action.target).to_path_buf(),
161
+ content: action.data.clone(),
162
+ }],
163
+ config,
164
+ )
165
+ .map_err(|error| error.to_string())?;
166
+ store
167
+ .add_artifact(
168
+ &task.id,
169
+ json!({ "type": "file_edit_proposal", "proposal": proposal, "description": format!("Proposed by native task step {step}; explicit approval required") }),
170
+ )
171
+ .map_err(|error| error.to_string())?;
172
+ Ok(format!(
173
+ "file edit proposal recorded but not applied: {}",
174
+ action.target
175
+ ))
176
+ }
177
+ "open_url" => navigate(config, &action.target).await,
178
+ "browser_read" => read_page_text(config).await,
179
+ "browser_click" => click(config, &action.target).await,
180
+ "knowledge_search" => {
181
+ let hits = KnowledgeStore::open_default()
182
+ .map_err(|error| error.to_string())?
183
+ .search(&action.target, 5)
184
+ .map_err(|error| error.to_string())?;
185
+ serde_json::to_string(&hits).map_err(|error| error.to_string())
186
+ }
187
+ "propose_bash" => Ok(format!(
188
+ "command proposal recorded but not executed: {}",
189
+ action.target
190
+ )),
191
+ other => Err(format!("unsupported native background action '{other}'")),
192
+ }
193
+ }
194
+
195
+ fn checkpoint(store: &TaskStore, id: &str, phase: &str, message: &str) -> Result<(), String> {
196
+ store
197
+ .add_checkpoint(id, json!({ "phase": phase, "message": message }))
198
+ .map(|_| ())
199
+ .map_err(|error| error.to_string())
200
+ }
201
+
202
+ fn emit(app: &AppHandle, message: &str, kind: &str) {
203
+ if let Some(main) = app.get_webview_window("main") {
204
+ let _ = main.emit(
205
+ "proactive-notification",
206
+ json!({ "message": message, "type": kind }),
207
+ );
208
+ }
209
+ }
210
+
211
+ #[cfg(test)]
212
+ mod tests {
213
+ use super::*;
214
+
215
+ #[test]
216
+ fn parses_json_wrapped_in_provider_text() {
217
+ let action: AgentAction =
218
+ parse_agent_json("```json\n{\"action\":\"done\",\"target\":\"ok\"}\n```").unwrap();
219
+ assert_eq!(action.action, "done");
220
+ assert_eq!(action.target, "ok");
221
+ }
222
+ }
@@ -0,0 +1,126 @@
1
+ use mint_core::MintConfig;
2
+ use serde::Serialize;
3
+ use serde_json::{Value, json};
4
+
5
+ #[derive(Debug, Serialize)]
6
+ #[serde(rename_all = "camelCase")]
7
+ pub struct PluginInfo {
8
+ pub name: &'static str,
9
+ pub description: &'static str,
10
+ pub migrated: bool,
11
+ pub configured: bool,
12
+ }
13
+
14
+ pub fn list_plugins(config: &MintConfig) -> Vec<PluginInfo> {
15
+ let mut plugins = vec![
16
+ PluginInfo {
17
+ name: "desktop-actions",
18
+ description: "Allowlisted URL and desktop application launcher",
19
+ migrated: true,
20
+ configured: true,
21
+ },
22
+ PluginInfo {
23
+ name: "mcp-stdio",
24
+ description: "Configured MCP server bridge over stdio JSON-RPC",
25
+ migrated: true,
26
+ configured: config.extra.get("mcpServers").is_some_and(|servers| {
27
+ servers
28
+ .as_object()
29
+ .is_some_and(|servers| !servers.is_empty())
30
+ }),
31
+ },
32
+ PluginInfo {
33
+ name: "gmail",
34
+ description: "Gmail OAuth search bridge",
35
+ migrated: true,
36
+ configured: has_values(
37
+ config,
38
+ &["gmailClientId", "gmailClientSecret", "gmailRefreshToken"],
39
+ ),
40
+ },
41
+ PluginInfo {
42
+ name: "google_calendar",
43
+ description: "Google Calendar OAuth list and browser-open bridge",
44
+ migrated: true,
45
+ configured: has_values(
46
+ config,
47
+ &[
48
+ "googleCalendarClientId",
49
+ "googleCalendarClientSecret",
50
+ "googleCalendarRefreshToken",
51
+ ],
52
+ ),
53
+ },
54
+ PluginInfo {
55
+ name: "notion",
56
+ description: "Notion page creation bridge",
57
+ migrated: true,
58
+ configured: has_values(config, &["notionApiKey", "notionDatabaseId"]),
59
+ },
60
+ PluginInfo {
61
+ name: "discord",
62
+ description: "Discord Rich Presence over native desktop IPC",
63
+ migrated: true,
64
+ configured: has_values(config, &["discordApplicationId"]),
65
+ },
66
+ ];
67
+ plugins.extend(
68
+ mint_core::native_plugins()
69
+ .into_iter()
70
+ .map(|plugin| PluginInfo {
71
+ name: plugin.name,
72
+ description: plugin.description,
73
+ migrated: true,
74
+ configured: true,
75
+ }),
76
+ );
77
+ plugins
78
+ }
79
+
80
+ pub fn channel_inventory(config: &MintConfig) -> Value {
81
+ json!([
82
+ channel(
83
+ config,
84
+ "telegram",
85
+ &["telegramBotToken"],
86
+ "native-long-poll"
87
+ ),
88
+ channel(config, "discord", &["discordBotToken"], "native-gateway"),
89
+ channel(
90
+ config,
91
+ "slack",
92
+ &["slackBotToken", "slackAppToken"],
93
+ "native-socket-mode"
94
+ ),
95
+ channel(
96
+ config,
97
+ "line",
98
+ &["lineChannelAccessToken", "lineChannelSecret"],
99
+ "native-webhook-127.0.0.1:3000"
100
+ ),
101
+ channel(
102
+ config,
103
+ "whatsapp",
104
+ &[
105
+ "whatsappCloudAccessToken",
106
+ "whatsappPhoneNumberId",
107
+ "whatsappVerifyToken"
108
+ ],
109
+ "native-cloud-api-webhook-127.0.0.1:3001"
110
+ )
111
+ ])
112
+ }
113
+
114
+ fn channel(config: &MintConfig, name: &str, keys: &[&str], runtime: &str) -> Value {
115
+ json!({ "name": name, "configured": has_values(config, keys), "runtime": runtime })
116
+ }
117
+
118
+ fn has_values(config: &MintConfig, keys: &[&str]) -> bool {
119
+ keys.iter().all(|key| {
120
+ config
121
+ .extra
122
+ .get(*key)
123
+ .and_then(Value::as_str)
124
+ .is_some_and(|value| !value.trim().is_empty())
125
+ })
126
+ }