@pheem49/mint 1.8.2 → 1.10.0

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 (106) hide show
  1. package/Cargo.lock +55 -3
  2. package/Cargo.toml +2 -1
  3. package/Release_Note.md +297 -107
  4. package/bin/mint +0 -0
  5. package/crates/mint-cli/Cargo.toml +1 -0
  6. package/crates/mint-cli/src/actions.rs +233 -0
  7. package/crates/mint-cli/src/agent.rs +382 -127
  8. package/crates/mint-cli/src/gmail.rs +22 -2
  9. package/crates/mint-cli/src/image.rs +8 -8
  10. package/crates/mint-cli/src/interactive.rs +2888 -0
  11. package/crates/mint-cli/src/main.rs +391 -2506
  12. package/crates/mint-cli/src/markdown.rs +294 -0
  13. package/crates/mint-cli/src/onboard.rs +152 -109
  14. package/crates/mint-cli/src/setup.rs +143 -143
  15. package/crates/mint-cli/src/updater.rs +48 -6
  16. package/crates/mint-core/Cargo.toml +1 -0
  17. package/crates/mint-core/src/agent_loop.rs +1 -0
  18. package/crates/mint-core/src/api_server.rs +431 -65
  19. package/crates/mint-core/src/browser.rs +630 -102
  20. package/crates/mint-core/src/channels.rs +105 -6
  21. package/crates/mint-core/src/chat.rs +174 -11
  22. package/crates/mint-core/src/code_tools.rs +47 -31
  23. package/crates/mint-core/src/config.rs +302 -17
  24. package/crates/mint-core/src/image_gen.rs +299 -38
  25. package/crates/mint-core/src/lib.rs +10 -7
  26. package/crates/mint-core/src/mcp.rs +119 -6
  27. package/crates/mint-core/src/memory.rs +6 -1
  28. package/crates/mint-core/src/orchestration.rs +237 -96
  29. package/crates/mint-core/src/pictures.rs +67 -5
  30. package/crates/mint-core/src/semantic.rs +1 -1
  31. package/crates/mint-core/src/shell.rs +1 -1
  32. package/crates/mint-core/src/skills.rs +128 -25
  33. package/crates/mint-core/src/video_gen.rs +235 -0
  34. package/package.json +8 -2
  35. package/shared-modules/linux-audio/fluidsynth2-static.json +9 -3
  36. package/shared-modules/linux-audio/fluidsynth2.json +8 -2
  37. package/shared-modules/linux-audio/lilv.json +26 -19
  38. package/shared-modules/linux-audio/lv2.json +0 -1
  39. package/shared-modules/luajit/luajit.json +2 -2
  40. package/skills-lock.json +41 -0
  41. package/src/renderer/index-web.html +1 -1
  42. package/src/renderer/index.html +1 -1
  43. package/src/renderer/shared/components/AgentActivityTable.tsx +25 -6
  44. package/src/renderer/shared/constants/slashCommands.ts +27 -0
  45. package/src/renderer/shared/platform.ts +3 -0
  46. package/src/renderer/shared/types.ts +4 -0
  47. package/src/renderer/shared/utils/approval.tsx +86 -7
  48. package/src/renderer/shared/utils/fileIcons.ts +135 -0
  49. package/src/renderer/shared/utils/markdown.tsx +217 -88
  50. package/src/renderer/shared/utils/slashCommandProcessor.ts +239 -0
  51. package/src/renderer/src/components/ChatPanel.tsx +486 -69
  52. package/src/renderer/src/components/DashboardSidebar.tsx +11 -2
  53. package/src/renderer/src/components/ImageStudioPanel.tsx +87 -4
  54. package/src/renderer/src/components/MintDashboard.tsx +195 -7
  55. package/src/renderer/src/components/PicturesLibrary.tsx +156 -15
  56. package/src/renderer/src/components/Settings/AgentsTab.tsx +21 -2
  57. package/src/renderer/src/components/Settings/AutomationTab.tsx +316 -18
  58. package/src/renderer/src/components/Settings/GeneralTab.tsx +795 -413
  59. package/src/renderer/src/components/Settings/PluginsTab.tsx +304 -70
  60. package/src/renderer/src/components/Settings/ThemeTab.tsx +6 -1
  61. package/src/renderer/src/components/SettingsWindow.tsx +106 -3
  62. package/src/renderer/src/components/VeoStudioPanel.tsx +549 -0
  63. package/src/renderer/src/components/WorkspacePanel.tsx +7 -125
  64. package/src/renderer/src/css/image-studio.css +106 -0
  65. package/src/renderer/src/css/settings.css +158 -0
  66. package/src/renderer/src/css/styles.css +368 -1
  67. package/src/renderer/src/css/veo-studio.css +626 -0
  68. package/src/renderer/src/index.css +1 -0
  69. package/src/renderer/src/tauri.ts +144 -17
  70. package/src/renderer/src-web/components/ChatPanel.tsx +385 -93
  71. package/src/renderer/src-web/components/DashboardSidebar.tsx +16 -1
  72. package/src/renderer/src-web/components/ImageStudioPanel.tsx +87 -4
  73. package/src/renderer/src-web/components/MintDashboard.tsx +156 -5
  74. package/src/renderer/src-web/components/PicturesLibrary.tsx +155 -15
  75. package/src/renderer/src-web/components/Settings/AgentsTab.tsx +21 -2
  76. package/src/renderer/src-web/components/Settings/AutomationTab.tsx +316 -18
  77. package/src/renderer/src-web/components/Settings/GeneralTab.tsx +795 -411
  78. package/src/renderer/src-web/components/Settings/PluginsTab.tsx +304 -70
  79. package/src/renderer/src-web/components/Settings/ThemeTab.tsx +6 -1
  80. package/src/renderer/src-web/components/SettingsWindow.tsx +110 -3
  81. package/src/renderer/src-web/components/VeoStudioPanel.tsx +549 -0
  82. package/src/renderer/src-web/css/image-studio.css +106 -0
  83. package/src/renderer/src-web/css/settings.css +158 -0
  84. package/src/renderer/src-web/css/styles.css +369 -1
  85. package/src/renderer/src-web/css/veo-studio.css +650 -0
  86. package/src/renderer/src-web/index.css +1 -0
  87. package/src/renderer/src-web/tauri.ts +135 -16
  88. package/src-tauri/src/desktop.rs +18 -16
  89. package/src-tauri/src/discord_rpc.rs +1 -1
  90. package/src-tauri/src/lib.rs +155 -25
  91. package/src-tauri/src/webhooks.rs +45 -31
  92. package/src-tauri/src/workflows.rs +1 -0
  93. package/src-tauri/tauri.conf.json +1 -1
  94. package/temp_gridgeist/AGENTS.md +8 -0
  95. package/temp_gridgeist/CHANGELOG.md +130 -0
  96. package/temp_gridgeist/LICENSE +21 -0
  97. package/temp_gridgeist/README.md +323 -0
  98. package/temp_gridgeist/README.th.md +325 -0
  99. package/temp_gridgeist/skills/gridgeist/SKILL.md +88 -0
  100. package/temp_gridgeist/skills/gridgeist/agents/openai.yaml +9 -0
  101. package/temp_gridgeist/skills/gridgeist/assets/gridgeist-small.svg +10 -0
  102. package/temp_gridgeist/skills/gridgeist/assets/gridgeist.png +0 -0
  103. package/temp_gridgeist/skills/gridgeist/assets/gridgeist.svg +18 -0
  104. package/temp_gridgeist/skills/gridgeist/references/design-language.md +88 -0
  105. package/temp_gridgeist/skills/gridgeist/references/review-checklist.md +128 -0
  106. package/temp_gridgeist/skills/gridgeist/references/system-contract.md +109 -0
@@ -2,6 +2,7 @@ use std::{
2
2
  fs,
3
3
  io::Cursor,
4
4
  path::{Path, PathBuf},
5
+ process::Command,
5
6
  time::{SystemTime, UNIX_EPOCH},
6
7
  };
7
8
 
@@ -79,11 +80,10 @@ pub fn list_saved_pictures() -> Result<Vec<PictureEntry>, PictureError> {
79
80
  .thumbnail_path
80
81
  .as_ref()
81
82
  .is_none_or(|thumbnail_path| !thumbnail_path.exists())
83
+ && let Some(thumbnail_path) = create_thumbnail_from_file(&directory, &entry)
82
84
  {
83
- if let Some(thumbnail_path) = create_thumbnail_from_file(&directory, &entry) {
84
- entry.thumbnail_path = Some(thumbnail_path);
85
- changed = true;
86
- }
85
+ entry.thumbnail_path = Some(thumbnail_path);
86
+ changed = true;
87
87
  }
88
88
  entry.url = Some(file_url(&entry.path));
89
89
  entry.thumbnail_url = entry.thumbnail_path.as_deref().map(file_url);
@@ -132,6 +132,11 @@ pub fn parse_data_uri(raw: &str) -> Option<(String, &'static str, Vec<u8>)> {
132
132
  "image/jpeg" | "image/jpg" => "jpg",
133
133
  "image/webp" => "webp",
134
134
  "image/gif" => "gif",
135
+ "video/mp4" => "mp4",
136
+ "video/webm" => "webm",
137
+ "video/quicktime" => "mov",
138
+ "video/x-msvideo" => "avi",
139
+ "video/x-matroska" => "mkv",
135
140
  _ => return None,
136
141
  };
137
142
  Some((mime_type, extension, STANDARD.decode(encoded).ok()?))
@@ -167,7 +172,13 @@ fn save_chat_images_to_directory(
167
172
  let id = filename
168
173
  .trim_end_matches(&format!(".{extension}"))
169
174
  .to_owned();
170
- let thumbnail_path = create_thumbnail_from_bytes(directory, &id, &bytes);
175
+ let thumbnail_path = if mime_type.starts_with("image/") {
176
+ create_thumbnail_from_bytes(directory, &id, &bytes)
177
+ } else if mime_type.starts_with("video/") {
178
+ extract_video_thumbnail_from_bytes(directory, &id, &bytes)
179
+ } else {
180
+ None
181
+ };
171
182
  let entry = PictureEntry {
172
183
  id,
173
184
  filename,
@@ -222,6 +233,57 @@ fn create_thumbnail_from_bytes(directory: &Path, id: &str, bytes: &[u8]) -> Opti
222
233
  Some(path)
223
234
  }
224
235
 
236
+ fn extract_video_thumbnail_from_bytes(directory: &Path, id: &str, bytes: &[u8]) -> Option<PathBuf> {
237
+ // Write video to temporary file
238
+ let temp_video = std::env::temp_dir().join(format!("mint-video-{}.tmp", id));
239
+ if fs::write(&temp_video, bytes).is_err() {
240
+ return None;
241
+ }
242
+
243
+ let temp_frame = std::env::temp_dir().join(format!("mint-frame-{}.png", id));
244
+
245
+ let res = (|| -> Option<PathBuf> {
246
+ let temp_video_str = temp_video.to_str()?;
247
+ let temp_frame_str = temp_frame.to_str()?;
248
+
249
+ let output = Command::new("ffmpeg")
250
+ .args(&["-i", temp_video_str])
251
+ .args(&["-vf", "select=eq(n\\,0)"])
252
+ .args(&["-vframes", "1"])
253
+ .args(&["-y", temp_frame_str])
254
+ .stderr(std::process::Stdio::null())
255
+ .stdout(std::process::Stdio::null())
256
+ .output()
257
+ .ok()?;
258
+
259
+ if !output.status.success() {
260
+ return None;
261
+ }
262
+
263
+ // Load extracted frame and create thumbnail
264
+ let frame_bytes = fs::read(&temp_frame).ok()?;
265
+ let image = image::load_from_memory(&frame_bytes).ok()?;
266
+ let thumbnail = image.thumbnail(THUMBNAIL_MAX_SIZE, THUMBNAIL_MAX_SIZE);
267
+
268
+ let thumbnail_directory = thumbnails_directory(directory);
269
+ ensure_directory(&thumbnail_directory).ok()?;
270
+ let path = thumbnail_directory.join(format!("{id}.thumb.png"));
271
+
272
+ let mut encoded = Cursor::new(Vec::new());
273
+ thumbnail
274
+ .write_to(&mut encoded, image::ImageFormat::Png)
275
+ .ok()?;
276
+ fs::write(&path, encoded.into_inner()).ok()?;
277
+ Some(path)
278
+ })();
279
+
280
+ // Always clean up temp files to avoid leaking in /tmp
281
+ let _ = fs::remove_file(&temp_video);
282
+ let _ = fs::remove_file(&temp_frame);
283
+
284
+ res
285
+ }
286
+
225
287
  fn read_index(directory: &Path) -> Result<Vec<PictureEntry>, PictureError> {
226
288
  let path = directory.join("pictures.json");
227
289
  if !path.exists() {
@@ -169,7 +169,7 @@ fn chunk_text(content: &str) -> Vec<(usize, usize, String)> {
169
169
  while start < lines.len() {
170
170
  let mut end = start;
171
171
  let mut chars = 0;
172
- while end < lines.len() && (end == start || chars + lines[end].len() + 1 <= MAX_CHARS) {
172
+ while end < lines.len() && (end == start || chars + lines[end].len() < MAX_CHARS) {
173
173
  chars += lines[end].len() + 1;
174
174
  end += 1;
175
175
  }
@@ -121,7 +121,7 @@ fn run_in_sandbox(
121
121
  .arg(cwd)
122
122
  .args(["bash", "-lc", command])
123
123
  .output()?;
124
- return Ok(Some(output));
124
+ Ok(Some(output))
125
125
  }
126
126
 
127
127
  #[cfg(target_os = "macos")]
@@ -53,10 +53,38 @@ pub fn learn_skill(path: &Path) -> Result<LearnedSkill, SkillError> {
53
53
  Ok(MemoryStore::open_default()?.add_learned_skill(name, &path.to_string_lossy(), &content)?)
54
54
  }
55
55
 
56
- pub fn learned_skills_context(workspace_root: Option<&Path>) -> Result<String, SkillError> {
56
+ pub fn load_agent_rules_file(file_path: &Path, list: &mut Vec<LearnedSkill>) {
57
+ if file_path.is_file() {
58
+ if let Ok(content) = fs::read_to_string(file_path) {
59
+ let name = file_path
60
+ .file_name()
61
+ .and_then(|n| n.to_str())
62
+ .unwrap_or("AGENTS.md")
63
+ .to_string();
64
+ let description = parse_skill_description(&content)
65
+ .or_else(|| Some("Workspace instructions and rules".to_string()));
66
+ list.push(LearnedSkill {
67
+ id: 0,
68
+ name,
69
+ source_path: file_path.to_string_lossy().to_string(),
70
+ content,
71
+ created_at: String::new(),
72
+ description,
73
+ });
74
+ }
75
+ }
76
+ }
77
+
78
+ pub fn learned_skills_context(
79
+ workspace_root: Option<&Path>,
80
+ chat_id: Option<&str>,
81
+ ) -> Result<String, SkillError> {
57
82
  let mut skills = MemoryStore::open_default()?.learned_skills(20)?;
58
83
 
59
84
  if let Some(home) = dirs::home_dir() {
85
+ let global_agents_path = home.join(".gemini").join("config").join("AGENTS.md");
86
+ load_agent_rules_file(&global_agents_path, &mut skills);
87
+
60
88
  let global_skills_path = home.join(".config").join("mint").join("mint-skills");
61
89
  if !global_skills_path.exists() {
62
90
  let _ = std::fs::create_dir_all(&global_skills_path);
@@ -65,6 +93,12 @@ pub fn learned_skills_context(workspace_root: Option<&Path>) -> Result<String, S
65
93
  }
66
94
 
67
95
  if let Some(root) = workspace_root {
96
+ let workspace_agents_path1 = root.join(".agents").join("AGENTS.md");
97
+ load_agent_rules_file(&workspace_agents_path1, &mut skills);
98
+
99
+ let workspace_agents_path2 = root.join("AGENTS.md");
100
+ load_agent_rules_file(&workspace_agents_path2, &mut skills);
101
+
68
102
  let workspace_skills_path1 = root.join(".agents").join("skills");
69
103
  load_skills_from_dir(&workspace_skills_path1, &mut skills);
70
104
 
@@ -77,11 +111,53 @@ pub fn learned_skills_context(workspace_root: Option<&Path>) -> Result<String, S
77
111
  unique_skills.insert(skill.name.clone(), skill);
78
112
  }
79
113
 
80
- let value = unique_skills
81
- .into_values()
82
- .map(|skill| format!("Skill: {}\n{}", skill.name, skill.content))
83
- .collect::<Vec<_>>()
84
- .join("\n\n");
114
+ let mut has_history = false;
115
+ if let Some(cid) = chat_id {
116
+ if let Ok(memory) = MemoryStore::open_default() {
117
+ if let Ok(interactions) = memory.recent_interactions_for_chat(cid, 1) {
118
+ has_history = !interactions.is_empty();
119
+ }
120
+ }
121
+ }
122
+
123
+ let mut formatted_skills = Vec::new();
124
+ for skill in unique_skills.into_values() {
125
+ let mut is_workspace_skill = false;
126
+ let mut rel_path = String::new();
127
+ if let Some(root) = workspace_root {
128
+ let skill_path = Path::new(&skill.source_path);
129
+ if let Ok(canonical_skill) = skill_path.canonicalize() {
130
+ if let Ok(canonical_root) = root.canonicalize() {
131
+ if canonical_skill.starts_with(&canonical_root) {
132
+ if let Ok(relative) = canonical_skill.strip_prefix(&canonical_root) {
133
+ is_workspace_skill = true;
134
+ rel_path = relative.to_string_lossy().to_string();
135
+ }
136
+ }
137
+ }
138
+ }
139
+ }
140
+
141
+ if is_workspace_skill {
142
+ let desc = skill
143
+ .description
144
+ .as_deref()
145
+ .unwrap_or("No description provided");
146
+ let status_msg = if has_history {
147
+ "READ. You have already read this skill in a previous turn of this conversation. Do NOT call `read_file` to read it again unless you need to re-verify its contents."
148
+ } else {
149
+ "UNREAD. You MUST execute the `read_file` tool with the path above to read this skill before starting the task."
150
+ };
151
+ formatted_skills.push(format!(
152
+ "Skill: {}\nDescription: {}\nPath: {}\nStatus: {}",
153
+ skill.name, desc, rel_path, status_msg
154
+ ));
155
+ } else {
156
+ formatted_skills.push(format!("Skill: {}\n{}", skill.name, skill.content));
157
+ }
158
+ }
159
+
160
+ let value = formatted_skills.join("\n\n");
85
161
  if value.len() <= MAX_CONTEXT_BYTES {
86
162
  return Ok(value);
87
163
  }
@@ -92,6 +168,31 @@ pub fn learned_skills_context(workspace_root: Option<&Path>) -> Result<String, S
92
168
  Ok(format!("{}\n...<learned skills truncated>", &value[..end]))
93
169
  }
94
170
 
171
+ pub fn parse_skill_description(content: &str) -> Option<String> {
172
+ let content = content.trim_start();
173
+ if !content.starts_with("---") {
174
+ return None;
175
+ }
176
+ let rest = &content[3..];
177
+ let end_idx = rest.find("---")?;
178
+ let frontmatter = &rest[..end_idx];
179
+ for line in frontmatter.lines() {
180
+ let line = line.trim();
181
+ if let Some(desc) = line.strip_prefix("description:") {
182
+ let mut val = desc.trim();
183
+ if (val.starts_with('"') && val.ends_with('"'))
184
+ || (val.starts_with('\'') && val.ends_with('\''))
185
+ {
186
+ if val.len() >= 2 {
187
+ val = &val[1..val.len() - 1];
188
+ }
189
+ }
190
+ return Some(val.to_string());
191
+ }
192
+ }
193
+ None
194
+ }
195
+
95
196
  pub fn load_skills_from_dir(dir: &Path, list: &mut Vec<LearnedSkill>) {
96
197
  if !dir.is_dir() {
97
198
  return;
@@ -109,36 +210,38 @@ pub fn load_skills_from_dir(dir: &Path, list: &mut Vec<LearnedSkill>) {
109
210
  .and_then(|n| n.to_str())
110
211
  .unwrap_or("skill")
111
212
  .to_string();
213
+ let description = parse_skill_description(&content);
112
214
  list.push(LearnedSkill {
113
215
  id: 0,
114
216
  name,
115
217
  source_path: skill_file.to_string_lossy().to_string(),
116
218
  content,
117
219
  created_at: String::new(),
220
+ description,
118
221
  });
119
222
  }
120
223
  break;
121
224
  }
122
225
  }
123
- } else if path.is_file() {
124
- if let Some(ext) = path.extension().and_then(|e| e.to_str()) {
125
- if matches!(ext.to_ascii_lowercase().as_str(), "md" | "txt") {
126
- if let Ok(content) = fs::read_to_string(&path) {
127
- let name = path
128
- .file_stem()
129
- .and_then(|n| n.to_str())
130
- .unwrap_or("skill")
131
- .to_string();
132
- list.push(LearnedSkill {
133
- id: 0,
134
- name,
135
- source_path: path.to_string_lossy().to_string(),
136
- content,
137
- created_at: String::new(),
138
- });
139
- }
140
- }
141
- }
226
+ } else if path.is_file()
227
+ && let Some(ext) = path.extension().and_then(|e| e.to_str())
228
+ && matches!(ext.to_ascii_lowercase().as_str(), "md" | "txt")
229
+ && let Ok(content) = fs::read_to_string(&path)
230
+ {
231
+ let name = path
232
+ .file_stem()
233
+ .and_then(|n| n.to_str())
234
+ .unwrap_or("skill")
235
+ .to_string();
236
+ let description = parse_skill_description(&content);
237
+ list.push(LearnedSkill {
238
+ id: 0,
239
+ name,
240
+ source_path: path.to_string_lossy().to_string(),
241
+ content,
242
+ created_at: String::new(),
243
+ description,
244
+ });
142
245
  }
143
246
  }
144
247
  }
@@ -0,0 +1,235 @@
1
+ use base64::{Engine as _, engine::general_purpose::STANDARD};
2
+ use serde::{Deserialize, Serialize};
3
+ use serde_json::{Value, json};
4
+ use thiserror::Error;
5
+
6
+ use crate::{MintConfig, PictureEntry};
7
+
8
+ #[derive(Debug, Clone, Deserialize)]
9
+ #[serde(rename_all = "camelCase")]
10
+ pub struct VideoGenRequest {
11
+ pub prompt: String,
12
+ pub negative_prompt: Option<String>,
13
+ pub aspect_ratio: String,
14
+ pub duration: u32,
15
+ pub model: Option<String>,
16
+ pub provider: String,
17
+ }
18
+
19
+ #[derive(Debug, Clone, Serialize)]
20
+ #[serde(rename_all = "camelCase")]
21
+ pub struct VideoGenResponse {
22
+ pub videos: Vec<PictureEntry>,
23
+ pub model: String,
24
+ pub provider: String,
25
+ pub prompt: String,
26
+ }
27
+
28
+ #[derive(Debug, Error)]
29
+ pub enum VideoGenError {
30
+ #[error("missing Gemini API key — set GEMINI_API_KEY or configure api_key in config")]
31
+ MissingApiKey,
32
+ #[error("API request failed: {0}")]
33
+ Request(#[from] reqwest::Error),
34
+ #[error("unexpected response structure: {0}")]
35
+ UnexpectedResponse(String),
36
+ #[error("video generation timed out")]
37
+ Timeout,
38
+ #[error("failed to save generated video: {0}")]
39
+ SaveError(String),
40
+ #[error(
41
+ "Google Veo model endpoint returned 404 Not Found. Google Veo video generation requires an API Key or Google Cloud project with Veo Model access enabled: {0}"
42
+ )]
43
+ ModelNotFound(String),
44
+ }
45
+
46
+ pub async fn generate_video(
47
+ config: &MintConfig,
48
+ request: &VideoGenRequest,
49
+ ) -> Result<VideoGenResponse, VideoGenError> {
50
+ let api_key = if config.api_key.trim().is_empty() {
51
+ std::env::var("GEMINI_API_KEY").unwrap_or_default()
52
+ } else {
53
+ config.api_key.clone()
54
+ };
55
+ if api_key.trim().is_empty() {
56
+ return Err(VideoGenError::MissingApiKey);
57
+ }
58
+
59
+ let mut model_owned = request
60
+ .model
61
+ .as_deref()
62
+ .map(str::trim)
63
+ .filter(|m| !m.is_empty())
64
+ .map(str::to_owned)
65
+ .unwrap_or_else(|| {
66
+ config
67
+ .extra
68
+ .get("veoModel")
69
+ .and_then(|v| v.as_str())
70
+ .unwrap_or("veo-2.0-generate-001")
71
+ .to_string()
72
+ });
73
+
74
+ if model_owned == "veo-2.0-flash-exp" {
75
+ model_owned = "veo-2.0-generate-001".to_string();
76
+ }
77
+
78
+ let client = crate::HTTP_CLIENT.clone();
79
+ let mut url = format!(
80
+ "https://generativelanguage.googleapis.com/v1beta/models/{model_owned}:predictLongRunning"
81
+ );
82
+
83
+ let payload = json!({
84
+ "instances": [
85
+ {
86
+ "prompt": request.prompt
87
+ }
88
+ ],
89
+ "parameters": {
90
+ "aspectRatio": request.aspect_ratio,
91
+ "durationSeconds": request.duration,
92
+ "sampleCount": 1
93
+ }
94
+ });
95
+
96
+ let mut response_res = client
97
+ .post(&url)
98
+ .header("x-goog-api-key", &api_key)
99
+ .json(&payload)
100
+ .send()
101
+ .await?;
102
+
103
+ if response_res.status() == reqwest::StatusCode::NOT_FOUND && model_owned != "veo-2.0-flash-001"
104
+ {
105
+ model_owned = "veo-2.0-flash-001".to_string();
106
+ url = format!(
107
+ "https://generativelanguage.googleapis.com/v1beta/models/{model_owned}:predictLongRunning"
108
+ );
109
+ response_res = client
110
+ .post(&url)
111
+ .header("x-goog-api-key", &api_key)
112
+ .json(&payload)
113
+ .send()
114
+ .await?;
115
+ }
116
+
117
+ if !response_res.status().is_success() {
118
+ let status = response_res.status();
119
+ let err_text = response_res.text().await.unwrap_or_default();
120
+ if status == reqwest::StatusCode::NOT_FOUND {
121
+ return Err(VideoGenError::ModelNotFound(format!(
122
+ "404 for model {model_owned} on generativelanguage.googleapis.com API. ({err_text})"
123
+ )));
124
+ }
125
+ return Err(VideoGenError::UnexpectedResponse(format!(
126
+ "HTTP {status}: {err_text}"
127
+ )));
128
+ }
129
+
130
+ let res: Value = response_res.json().await?;
131
+
132
+ let operation_name = res
133
+ .get("name")
134
+ .and_then(|v| v.as_str())
135
+ .ok_or_else(|| VideoGenError::UnexpectedResponse("Missing operation name".to_string()))?;
136
+
137
+ let poll_url = format!("https://generativelanguage.googleapis.com/v1beta/{operation_name}");
138
+ let mut done = false;
139
+ let mut final_response = None;
140
+
141
+ // Poll every 5 seconds, up to 60 times (5 minutes max)
142
+ for _ in 0..60 {
143
+ tokio::time::sleep(tokio::time::Duration::from_secs(5)).await;
144
+ let poll_res: Value = client
145
+ .get(&poll_url)
146
+ .header("x-goog-api-key", &api_key)
147
+ .send()
148
+ .await?
149
+ .error_for_status()?
150
+ .json()
151
+ .await?;
152
+
153
+ if let Some(d) = poll_res.get("done").and_then(|v| v.as_bool()) {
154
+ if d {
155
+ done = true;
156
+ final_response = Some(poll_res);
157
+ break;
158
+ }
159
+ }
160
+ }
161
+
162
+ if !done {
163
+ return Err(VideoGenError::Timeout);
164
+ }
165
+
166
+ let op_res = final_response.ok_or(VideoGenError::Timeout)?;
167
+
168
+ // Check if error occurred in operation
169
+ if let Some(err) = op_res.get("error") {
170
+ let msg = err["message"].as_str().unwrap_or("unknown operation error");
171
+ return Err(VideoGenError::UnexpectedResponse(msg.to_string()));
172
+ }
173
+
174
+ let response_obj = op_res
175
+ .get("response")
176
+ .ok_or_else(|| VideoGenError::UnexpectedResponse("Missing response object".to_string()))?;
177
+
178
+ // Try to find generated samples or outputs
179
+ let generated_samples = response_obj
180
+ .get("generateVideoResponse")
181
+ .and_then(|gvr| gvr.get("generatedSamples"))
182
+ .or_else(|| {
183
+ response_obj
184
+ .get("outputs")
185
+ .and_then(|o| o.as_array()?.first()?.get("generatedVideos"))
186
+ })
187
+ .ok_or_else(|| {
188
+ VideoGenError::UnexpectedResponse(
189
+ "No generated samples or videos in response".to_string(),
190
+ )
191
+ })?;
192
+
193
+ let sample = generated_samples
194
+ .as_array()
195
+ .and_then(|arr| arr.first())
196
+ .ok_or_else(|| {
197
+ VideoGenError::UnexpectedResponse("Generated samples array is empty".to_string())
198
+ })?;
199
+
200
+ let video_uri = sample
201
+ .get("video")
202
+ .and_then(|v| v.get("uri"))
203
+ .and_then(|u| u.as_str())
204
+ .ok_or_else(|| VideoGenError::UnexpectedResponse("Missing video URI".to_string()))?;
205
+
206
+ // Download the video file bytes
207
+ let video_bytes = client
208
+ .get(video_uri)
209
+ .header("x-goog-api-key", &api_key)
210
+ .send()
211
+ .await?
212
+ .error_for_status()?
213
+ .bytes()
214
+ .await?
215
+ .to_vec();
216
+
217
+ // Convert to data URI for saving
218
+ let data_b64 = STANDARD.encode(&video_bytes);
219
+ let data_uri = format!("data:video/mp4;base64,{data_b64}");
220
+
221
+ // Save to the pictures directory and record index entry
222
+ let saved = crate::pictures::save_chat_images(
223
+ vec![data_uri],
224
+ Some("veo".to_string()),
225
+ Some(request.prompt.clone()),
226
+ )
227
+ .map_err(|e| VideoGenError::SaveError(e.to_string()))?;
228
+
229
+ Ok(VideoGenResponse {
230
+ videos: saved,
231
+ model: model_owned,
232
+ provider: "veo".to_string(),
233
+ prompt: request.prompt.clone(),
234
+ })
235
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pheem49/mint",
3
- "version": "1.8.2",
3
+ "version": "1.10.0",
4
4
  "description": "A native Tauri desktop AI assistant with a Rust backend and React UI.",
5
5
  "scripts": {
6
6
  "dev:web": "vite --config vite.config.web.ts",
@@ -56,6 +56,7 @@
56
56
  "mint": "src/bin/index.js"
57
57
  },
58
58
  "dependencies": {
59
+ "@pouyanafisi/gmail-mcp": "^2.0.0",
59
60
  "@tauri-apps/api": "^2.0.0",
60
61
  "@vitejs/plugin-react": "^6.0.1",
61
62
  "@xyflow/react": "^12.11.1",
@@ -71,6 +72,11 @@
71
72
  "@tauri-apps/cli": "^2.0.0"
72
73
  },
73
74
  "overrides": {
74
- "gh-pages": "^5.0.0"
75
+ "gh-pages": "^5.0.0",
76
+ "@modelcontextprotocol/sdk": "^1.24.0",
77
+ "nodemailer": "^9.0.0",
78
+ "uuid": "^11.1.1",
79
+ "brace-expansion": "^5.0.8",
80
+ "@hono/node-server": "^2.0.5"
75
81
  }
76
82
  }
@@ -22,9 +22,15 @@
22
22
  ],
23
23
  "sources": [
24
24
  {
25
- "type": "archive",
26
- "url": "https://github.com/FluidSynth/fluidsynth/archive/refs/tags/v2.4.6.tar.gz",
27
- "sha256": "a6be90fd4842b9e7246500597180af5cf213c11bfa3998a3236dd8ff47961ea8"
25
+ "type": "archive",
26
+ "url": "https://github.com/FluidSynth/fluidsynth/archive/v2.5.6.tar.gz",
27
+ "sha256": "0825f024c9cf7a18073739b83612d46542ecbfb349ae9147a1e9f08e2d524407"
28
+ },
29
+ {
30
+ "type": "git",
31
+ "url": "https://github.com/kthohr/gcem.git",
32
+ "commit": "012ae73c6d0a2cb09ffe86475f5c6fba3926e200",
33
+ "dest": "gcem"
28
34
  }
29
35
  ]
30
36
  }
@@ -12,8 +12,14 @@
12
12
  "sources": [
13
13
  {
14
14
  "type": "archive",
15
- "url": "https://github.com/FluidSynth/fluidsynth/archive/refs/tags/v2.4.6.tar.gz",
16
- "sha256": "a6be90fd4842b9e7246500597180af5cf213c11bfa3998a3236dd8ff47961ea8"
15
+ "url": "https://github.com/FluidSynth/fluidsynth/archive/v2.5.6.tar.gz",
16
+ "sha256": "0825f024c9cf7a18073739b83612d46542ecbfb349ae9147a1e9f08e2d524407"
17
+ },
18
+ {
19
+ "type": "git",
20
+ "url": "https://github.com/kthohr/gcem.git",
21
+ "commit": "012ae73c6d0a2cb09ffe86475f5c6fba3926e200",
22
+ "dest": "gcem"
17
23
  }
18
24
  ]
19
25
  }