@pheem49/mint 1.6.1 → 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 +11 -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,255 @@
1
+ use std::collections::BTreeMap;
2
+
3
+ use base64::{Engine as _, engine::general_purpose::STANDARD};
4
+ use hmac::{Hmac, Mac};
5
+ use mint_core::{ChatRequest, load_config, orchestrate_chat};
6
+ use serde_json::{Value, json};
7
+ use sha2::Sha256;
8
+ use tokio::{
9
+ io::{AsyncReadExt, AsyncWriteExt},
10
+ net::{TcpListener, TcpStream},
11
+ };
12
+
13
+ type HmacSha256 = Hmac<Sha256>;
14
+
15
+ pub fn start_webhooks() {
16
+ tauri::async_runtime::spawn(async {
17
+ let _ = serve(3000, Service::Line).await;
18
+ });
19
+ tauri::async_runtime::spawn(async {
20
+ let _ = serve(3001, Service::Whatsapp).await;
21
+ });
22
+ }
23
+
24
+ #[derive(Clone, Copy)]
25
+ enum Service {
26
+ Line,
27
+ Whatsapp,
28
+ }
29
+
30
+ async fn serve(port: u16, service: Service) -> Result<(), String> {
31
+ let listener = TcpListener::bind(("127.0.0.1", port))
32
+ .await
33
+ .map_err(|error| error.to_string())?;
34
+ loop {
35
+ let (stream, _) = listener.accept().await.map_err(|error| error.to_string())?;
36
+ tauri::async_runtime::spawn(handle(stream, service));
37
+ }
38
+ }
39
+
40
+ async fn handle(mut stream: TcpStream, service: Service) {
41
+ let Ok(request) = read_request(&mut stream).await else {
42
+ return;
43
+ };
44
+ let response = match service {
45
+ Service::Line => handle_line(request).await,
46
+ Service::Whatsapp => handle_whatsapp(request).await,
47
+ };
48
+ let (status, body) = response.unwrap_or_else(|error| ("500 Internal Server Error", error));
49
+ let _ = stream.write_all(format!(
50
+ "HTTP/1.1 {status}\r\nContent-Type: text/plain\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{body}",
51
+ body.len()
52
+ ).as_bytes()).await;
53
+ }
54
+
55
+ async fn handle_line(request: HttpRequest) -> Result<(&'static str, String), String> {
56
+ if request.method != "POST" || request.path != "/callback" {
57
+ return Ok(("404 Not Found", "not found".into()));
58
+ }
59
+ let config = load_config().map_err(|error| error.to_string())?;
60
+ if !extra_bool(&config.extra, "enableLineBridge") {
61
+ return Ok(("503 Service Unavailable", "LINE bridge disabled".into()));
62
+ }
63
+ let secret = extra(&config.extra, "lineChannelSecret").ok_or("missing lineChannelSecret")?;
64
+ let signature = request
65
+ .headers
66
+ .get("x-line-signature")
67
+ .ok_or("missing LINE signature")?;
68
+ verify_signature(secret.as_bytes(), &request.body, signature, false)?;
69
+ let token = extra(&config.extra, "lineChannelAccessToken")
70
+ .ok_or("missing lineChannelAccessToken")?
71
+ .to_owned();
72
+ let payload: Value =
73
+ serde_json::from_slice(&request.body).map_err(|error| error.to_string())?;
74
+ for event in payload["events"].as_array().cloned().unwrap_or_default() {
75
+ let (Some(reply_token), Some(text)) = (
76
+ event["replyToken"].as_str(),
77
+ event["message"]["text"].as_str(),
78
+ ) else {
79
+ continue;
80
+ };
81
+ let answer = answer(text, "Reply concisely for a LINE chat.").await;
82
+ let _ = mint_core::HTTP_CLIENT.clone().post("https://api.line.me/v2/bot/message/reply").bearer_auth(&token)
83
+ .json(&json!({ "replyToken": reply_token, "messages": [{ "type": "text", "text": answer }] })).send().await;
84
+ }
85
+ Ok(("200 OK", "ok".into()))
86
+ }
87
+
88
+ async fn handle_whatsapp(request: HttpRequest) -> Result<(&'static str, String), String> {
89
+ let config = load_config().map_err(|error| error.to_string())?;
90
+ if request.method == "GET" {
91
+ let query = parse_query(&request.path);
92
+ let verify = extra(&config.extra, "whatsappVerifyToken").unwrap_or_default();
93
+ return if query
94
+ .get("hub.verify_token")
95
+ .is_some_and(|token| token == verify)
96
+ {
97
+ Ok((
98
+ "200 OK",
99
+ query.get("hub.challenge").cloned().unwrap_or_default(),
100
+ ))
101
+ } else {
102
+ Ok(("403 Forbidden", "verification failed".into()))
103
+ };
104
+ }
105
+ if request.method != "POST" {
106
+ return Ok(("404 Not Found", "not found".into()));
107
+ }
108
+ if !extra_bool(&config.extra, "enableWhatsappBridge") {
109
+ return Ok(("503 Service Unavailable", "WhatsApp bridge disabled".into()));
110
+ }
111
+ if let (Some(secret), Some(signature)) = (
112
+ extra(&config.extra, "whatsappAppSecret"),
113
+ request.headers.get("x-hub-signature-256"),
114
+ ) {
115
+ verify_signature(secret.as_bytes(), &request.body, signature, true)?;
116
+ }
117
+ let access_token = extra(&config.extra, "whatsappCloudAccessToken")
118
+ .ok_or("missing whatsappCloudAccessToken")?
119
+ .to_owned();
120
+ let phone_id = extra(&config.extra, "whatsappPhoneNumberId")
121
+ .ok_or("missing whatsappPhoneNumberId")?
122
+ .to_owned();
123
+ let payload: Value =
124
+ serde_json::from_slice(&request.body).map_err(|error| error.to_string())?;
125
+ for message in payload["entry"]
126
+ .as_array()
127
+ .into_iter()
128
+ .flatten()
129
+ .flat_map(|entry| entry["changes"].as_array().into_iter().flatten())
130
+ .flat_map(|change| change["value"]["messages"].as_array().into_iter().flatten())
131
+ {
132
+ let (Some(to), Some(text)) = (message["from"].as_str(), message["text"]["body"].as_str())
133
+ else {
134
+ continue;
135
+ };
136
+ let answer = answer(text, "Reply concisely for a WhatsApp chat.").await;
137
+ let _ = mint_core::HTTP_CLIENT.clone().post(format!("https://graph.facebook.com/v23.0/{phone_id}/messages")).bearer_auth(&access_token)
138
+ .json(&json!({ "messaging_product": "whatsapp", "to": to, "type": "text", "text": { "body": answer } })).send().await;
139
+ }
140
+ Ok(("200 OK", "ok".into()))
141
+ }
142
+
143
+ async fn answer(text: &str, system: &str) -> String {
144
+ let Ok(config) = load_config() else {
145
+ return "Mint config error".into();
146
+ };
147
+ orchestrate_chat(
148
+ &config,
149
+ &ChatRequest {
150
+ message: text.into(),
151
+ system_instruction: system.into(),
152
+ chat_id: None,
153
+ image_data_uri: None,
154
+ audio_data_uri: None,
155
+ document_attachment: None,
156
+ workspace_path: None,
157
+ },
158
+ )
159
+ .await
160
+ .map(|response| response.text)
161
+ .unwrap_or_else(|error| format!("Mint error: {error}"))
162
+ }
163
+
164
+ struct HttpRequest {
165
+ method: String,
166
+ path: String,
167
+ headers: BTreeMap<String, String>,
168
+ body: Vec<u8>,
169
+ }
170
+
171
+ async fn read_request(stream: &mut TcpStream) -> Result<HttpRequest, String> {
172
+ let mut bytes = Vec::new();
173
+ let mut buffer = [0_u8; 4096];
174
+ loop {
175
+ let read = stream
176
+ .read(&mut buffer)
177
+ .await
178
+ .map_err(|error| error.to_string())?;
179
+ if read == 0 {
180
+ break;
181
+ }
182
+ bytes.extend_from_slice(&buffer[..read]);
183
+ if let Some(split) = bytes.windows(4).position(|window| window == b"\r\n\r\n") {
184
+ let header = String::from_utf8_lossy(&bytes[..split]);
185
+ let mut lines = header.lines();
186
+ let mut first = lines.next().unwrap_or_default().split_whitespace();
187
+ let method = first.next().unwrap_or_default().to_owned();
188
+ let path = first.next().unwrap_or_default().to_owned();
189
+ let headers = lines
190
+ .filter_map(|line| line.split_once(':'))
191
+ .map(|(key, value)| (key.trim().to_ascii_lowercase(), value.trim().to_owned()))
192
+ .collect::<BTreeMap<_, _>>();
193
+ let length = headers
194
+ .get("content-length")
195
+ .and_then(|value| value.parse().ok())
196
+ .unwrap_or(0);
197
+ let start = split + 4;
198
+ while bytes.len() < start + length {
199
+ let read = stream
200
+ .read(&mut buffer)
201
+ .await
202
+ .map_err(|error| error.to_string())?;
203
+ if read == 0 {
204
+ break;
205
+ }
206
+ bytes.extend_from_slice(&buffer[..read]);
207
+ }
208
+ return Ok(HttpRequest {
209
+ method,
210
+ path,
211
+ headers,
212
+ body: bytes[start..start + length.min(bytes.len() - start)].to_vec(),
213
+ });
214
+ }
215
+ }
216
+ Err("invalid HTTP request".into())
217
+ }
218
+
219
+ fn verify_signature(secret: &[u8], body: &[u8], signature: &str, hex: bool) -> Result<(), String> {
220
+ let mut mac = HmacSha256::new_from_slice(secret).map_err(|error| error.to_string())?;
221
+ mac.update(body);
222
+ let expected = if hex {
223
+ hex_encode(&mac.finalize().into_bytes())
224
+ } else {
225
+ STANDARD.encode(mac.finalize().into_bytes())
226
+ };
227
+ (signature.trim_start_matches("sha256=") == expected)
228
+ .then_some(())
229
+ .ok_or_else(|| "webhook signature verification failed".into())
230
+ }
231
+
232
+ fn parse_query(path: &str) -> BTreeMap<String, String> {
233
+ path.split_once('?')
234
+ .map(|(_, query)| {
235
+ query
236
+ .split('&')
237
+ .filter_map(|item| item.split_once('='))
238
+ .map(|(key, value)| (key.to_owned(), value.to_owned()))
239
+ .collect()
240
+ })
241
+ .unwrap_or_default()
242
+ }
243
+
244
+ fn extra<'a>(extra: &'a BTreeMap<String, Value>, key: &str) -> Option<&'a str> {
245
+ extra
246
+ .get(key)
247
+ .and_then(Value::as_str)
248
+ .filter(|value| !value.is_empty())
249
+ }
250
+ fn extra_bool(extra: &BTreeMap<String, Value>, key: &str) -> bool {
251
+ extra.get(key).and_then(Value::as_bool).unwrap_or(false)
252
+ }
253
+ fn hex_encode(bytes: &[u8]) -> String {
254
+ bytes.iter().map(|byte| format!("{byte:02x}")).collect()
255
+ }
@@ -0,0 +1,70 @@
1
+ use std::{
2
+ collections::BTreeMap,
3
+ process::Command,
4
+ thread,
5
+ time::{Duration, Instant},
6
+ };
7
+
8
+ use mint_core::{load_config, load_workflows};
9
+ use serde_json::json;
10
+ use tauri::{AppHandle, Emitter, Manager};
11
+
12
+ pub fn start_monitor(app: AppHandle) {
13
+ thread::spawn(move || {
14
+ let mut last_triggered = BTreeMap::<String, Instant>::new();
15
+ loop {
16
+ thread::sleep(Duration::from_secs(15));
17
+ if load_config()
18
+ .ok()
19
+ .and_then(|config| config.extra.get("enableCustomWorkflows").cloned())
20
+ .and_then(|value| value.as_bool())
21
+ == Some(false)
22
+ {
23
+ continue;
24
+ }
25
+ let Ok(workflows) = load_workflows() else {
26
+ continue;
27
+ };
28
+ let Ok(output) = Command::new("ps").args(["-A", "-o", "comm="]).output() else {
29
+ continue;
30
+ };
31
+ let processes = String::from_utf8_lossy(&output.stdout)
32
+ .lines()
33
+ .map(|line| line.trim().to_ascii_lowercase())
34
+ .collect::<Vec<_>>();
35
+ for workflow in workflows {
36
+ let Some(id) = workflow["id"].as_str() else {
37
+ continue;
38
+ };
39
+ let Some(process_name) = workflow["trigger"]["processName"].as_str() else {
40
+ continue;
41
+ };
42
+ if !processes
43
+ .iter()
44
+ .any(|process| process == &process_name.to_ascii_lowercase())
45
+ {
46
+ continue;
47
+ }
48
+ if last_triggered
49
+ .get(id)
50
+ .is_some_and(|last| last.elapsed() < Duration::from_secs(60 * 60))
51
+ {
52
+ continue;
53
+ }
54
+ last_triggered.insert(id.into(), Instant::now());
55
+ if let Some(main) = app.get_webview_window("main") {
56
+ let payload = json!({
57
+ "message": workflow["action"]["message"]
58
+ .as_str()
59
+ .unwrap_or("Automation workflow triggered"),
60
+ "suggestions": [
61
+ { "label": "Yes, please", "action": workflow["action"] },
62
+ { "label": "Dismiss", "action": { "type": "none" } }
63
+ ]
64
+ });
65
+ let _ = main.emit("proactive-suggestion", payload);
66
+ }
67
+ }
68
+ }
69
+ });
70
+ }
@@ -0,0 +1,48 @@
1
+ {
2
+ "$schema": "https://schema.tauri.app/config/2",
3
+ "productName": "Mint",
4
+ "version": "1.6.2",
5
+ "identifier": "com.pheem49.mint",
6
+ "build": {
7
+ "beforeDevCommand": "npm run dev:desktop",
8
+ "beforeBuildCommand": "npm run build:desktop:ui",
9
+ "devUrl": "http://localhost:9000",
10
+ "frontendDist": "../out/renderer"
11
+ },
12
+ "app": {
13
+ "windows": [
14
+ {
15
+ "label": "main",
16
+ "title": "Mint Agent",
17
+ "width": 1440,
18
+ "height": 900,
19
+ "minWidth": 960,
20
+ "minHeight": 640,
21
+ "transparent": true,
22
+ "decorations": true
23
+ }
24
+ ],
25
+ "security": {
26
+ "csp": null,
27
+ "capabilities": ["main"],
28
+ "assetProtocol": {
29
+ "enable": true,
30
+ "scope": ["$HOME/.config/mint/**/*"]
31
+ }
32
+ }
33
+ },
34
+ "bundle": {
35
+ "active": true,
36
+ "createUpdaterArtifacts": false,
37
+ "targets": ["deb"],
38
+ "icon": [
39
+ "../assets/icon.png"
40
+ ]
41
+ },
42
+ "plugins": {
43
+ "updater": {
44
+ "pubkey": "",
45
+ "endpoints": []
46
+ }
47
+ }
48
+ }
package/tsconfig.json CHANGED
@@ -26,5 +26,5 @@
26
26
  "@shared/*": ["./src/*"]
27
27
  }
28
28
  },
29
- "include": ["src/renderer/src", "src/renderer/src/env.d.ts"]
29
+ "include": ["src/renderer/src", "src/renderer/src-web", "src/renderer/src/env.d.ts"]
30
30
  }