@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.
- package/.github/workflows/ci.yml +14 -0
- package/Cargo.lock +3 -3
- package/Cargo.toml +1 -1
- package/bin/mint +0 -0
- package/package.json +3 -3
- package/src/renderer/index-web.html +1 -1
- package/src/renderer/index.html +1 -1
- package/src/renderer/src/components/DashboardSidebar.tsx +2 -2
- package/src/renderer/src/components/MintDashboard.tsx +1 -1
- package/src/renderer/src-web/components/ChatPanel.tsx +1 -1
- package/src/renderer/src-web/components/DashboardSidebar.tsx +2 -2
- package/src/renderer/src-web/components/MintDashboard.tsx +1 -1
- package/src-tauri/Cargo.toml +29 -0
- package/src-tauri/build.rs +3 -0
- package/src-tauri/capabilities/main.json +7 -0
- package/src-tauri/gen/schemas/acl-manifests.json +1 -0
- package/src-tauri/gen/schemas/capabilities.json +1 -0
- package/src-tauri/gen/schemas/desktop-schema.json +2412 -0
- package/src-tauri/gen/schemas/linux-schema.json +2412 -0
- package/src-tauri/src/browser.rs +141 -0
- package/src-tauri/src/desktop.rs +392 -0
- package/src-tauri/src/discord_rpc.rs +99 -0
- package/src-tauri/src/events.rs +70 -0
- package/src-tauri/src/headless.rs +222 -0
- package/src-tauri/src/integrations.rs +126 -0
- package/src-tauri/src/lib.rs +1033 -0
- package/src-tauri/src/main.rs +3 -0
- package/src-tauri/src/plugins.rs +16 -0
- package/src-tauri/src/proactive.rs +254 -0
- package/src-tauri/src/system.rs +250 -0
- package/src-tauri/src/updater.rs +148 -0
- package/src-tauri/src/webhooks.rs +255 -0
- package/src-tauri/src/workflows.rs +70 -0
- package/src-tauri/tauri.conf.json +48 -0
- 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
|
+
}
|