@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,729 @@
1
+ use std::{
2
+ collections::BTreeMap,
3
+ fs,
4
+ path::{Path, PathBuf},
5
+ };
6
+
7
+ use serde::{Deserialize, Serialize};
8
+ use sha2::{Digest, Sha256};
9
+ use thiserror::Error;
10
+
11
+ use crate::{Capability, MintConfig, SafetyError, assert_path_capability};
12
+
13
+ const IGNORED_DIRECTORIES: &[&str] = &[
14
+ ".git",
15
+ ".cache",
16
+ "build",
17
+ "dist",
18
+ "node_modules",
19
+ "out",
20
+ "target",
21
+ ];
22
+
23
+ #[derive(Debug, Error)]
24
+ pub enum CodeInspectionError {
25
+ #[error(transparent)]
26
+ Safety(#[from] SafetyError),
27
+ #[error("unable to read {path}: {source}")]
28
+ Read {
29
+ path: PathBuf,
30
+ source: std::io::Error,
31
+ },
32
+ #[error("path is not a file: {0}")]
33
+ NotAFile(PathBuf),
34
+ #[error("unable to write {path}: {source}")]
35
+ Write {
36
+ path: PathBuf,
37
+ source: std::io::Error,
38
+ },
39
+ #[error("edit request must contain at least one file")]
40
+ EmptyEditRequest,
41
+ #[error("approval token does not match the proposed edit")]
42
+ InvalidApprovalToken,
43
+ #[error("file changed after approval proposal: {0}")]
44
+ StaleProposal(PathBuf),
45
+ #[error("patch hunk {index} old text was not found in {path}")]
46
+ PatchTextNotFound { path: PathBuf, index: usize },
47
+ #[error("edit path escapes workspace root: {0}")]
48
+ OutsideWorkspace(PathBuf),
49
+ }
50
+
51
+ #[derive(Debug, Clone, Serialize, PartialEq, Eq)]
52
+ #[serde(rename_all = "camelCase")]
53
+ pub struct CodeFile {
54
+ pub path: PathBuf,
55
+ pub size: u64,
56
+ }
57
+
58
+ #[derive(Debug, Clone, Serialize, PartialEq, Eq)]
59
+ #[serde(rename_all = "camelCase")]
60
+ pub struct CodeSearchHit {
61
+ pub path: PathBuf,
62
+ pub line: usize,
63
+ pub text: String,
64
+ }
65
+
66
+ #[derive(Debug, Clone, Serialize, PartialEq, Eq)]
67
+ #[serde(rename_all = "camelCase")]
68
+ pub struct RepositorySummary {
69
+ pub root: PathBuf,
70
+ pub file_count: usize,
71
+ pub total_bytes: u64,
72
+ pub extensions: BTreeMap<String, usize>,
73
+ }
74
+
75
+ #[derive(Debug, Clone, Serialize, PartialEq, Eq)]
76
+ #[serde(rename_all = "camelCase")]
77
+ pub struct CodePlan {
78
+ pub task: String,
79
+ pub root: PathBuf,
80
+ pub inspect_files: Vec<PathBuf>,
81
+ pub steps: Vec<String>,
82
+ }
83
+
84
+ #[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
85
+ #[serde(rename_all = "camelCase")]
86
+ pub struct CodeEdit {
87
+ pub path: PathBuf,
88
+ pub content: String,
89
+ }
90
+
91
+ #[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
92
+ #[serde(rename_all = "camelCase")]
93
+ pub struct CodePatchHunk {
94
+ pub old_text: String,
95
+ pub new_text: String,
96
+ }
97
+
98
+ #[derive(Debug, Clone, Serialize, PartialEq, Eq)]
99
+ #[serde(rename_all = "camelCase")]
100
+ pub struct CodeEditPreview {
101
+ pub path: PathBuf,
102
+ pub existed: bool,
103
+ pub before_sha256: String,
104
+ pub after_sha256: String,
105
+ pub diff: String,
106
+ }
107
+
108
+ #[derive(Debug, Clone, Serialize, PartialEq, Eq)]
109
+ #[serde(rename_all = "camelCase")]
110
+ pub struct CodeEditProposal {
111
+ pub approval_required: bool,
112
+ pub approval_token: String,
113
+ pub edits: Vec<CodeEditPreview>,
114
+ }
115
+
116
+ #[derive(Debug, Clone, Serialize, PartialEq, Eq)]
117
+ #[serde(rename_all = "camelCase")]
118
+ pub struct AppliedCodeEdit {
119
+ pub path: PathBuf,
120
+ pub created: bool,
121
+ pub bytes_written: usize,
122
+ }
123
+
124
+ pub fn list_code_files(
125
+ root: &Path,
126
+ limit: usize,
127
+ config: &MintConfig,
128
+ ) -> Result<Vec<CodeFile>, CodeInspectionError> {
129
+ let root = assert_path_capability(root, Capability::Read, config)?;
130
+ let mut files = Vec::new();
131
+ collect_files(&root, &mut files, limit.max(1), true)?;
132
+ Ok(files)
133
+ }
134
+
135
+ pub fn read_code_file(
136
+ path: &Path,
137
+ start_line: usize,
138
+ end_line: usize,
139
+ config: &MintConfig,
140
+ ) -> Result<String, CodeInspectionError> {
141
+ let path = assert_path_capability(path, Capability::Read, config)?;
142
+ if !path.is_file() {
143
+ return Err(CodeInspectionError::NotAFile(path));
144
+ }
145
+ let raw = fs::read_to_string(&path).map_err(|source| CodeInspectionError::Read {
146
+ path: path.clone(),
147
+ source,
148
+ })?;
149
+ let first = start_line.max(1);
150
+ let last = end_line.max(first);
151
+ Ok(raw
152
+ .lines()
153
+ .enumerate()
154
+ .filter(|(index, _)| {
155
+ let line = index + 1;
156
+ line >= first && line <= last
157
+ })
158
+ .map(|(index, line)| format!("{:>6} | {line}", index + 1))
159
+ .collect::<Vec<_>>()
160
+ .join("\n"))
161
+ }
162
+
163
+ pub fn search_code(
164
+ root: &Path,
165
+ query: &str,
166
+ limit: usize,
167
+ config: &MintConfig,
168
+ ) -> Result<Vec<CodeSearchHit>, CodeInspectionError> {
169
+ let files = list_code_files(root, usize::MAX, config)?;
170
+ let mut hits = Vec::new();
171
+ if query.trim().is_empty() {
172
+ return Ok(hits);
173
+ }
174
+ let escaped = regex::escape(query);
175
+ let re = match regex::RegexBuilder::new(&escaped)
176
+ .case_insensitive(true)
177
+ .build()
178
+ {
179
+ Ok(re) => re,
180
+ Err(_) => return Ok(hits),
181
+ };
182
+ for file in files {
183
+ let Ok(raw) = fs::read_to_string(&file.path) else {
184
+ continue;
185
+ };
186
+ if !re.is_match(&raw) {
187
+ continue;
188
+ }
189
+ for (index, line) in raw.lines().enumerate() {
190
+ if re.is_match(line) {
191
+ hits.push(CodeSearchHit {
192
+ path: file.path.clone(),
193
+ line: index + 1,
194
+ text: line.trim().to_owned(),
195
+ });
196
+ if hits.len() >= limit.max(1) {
197
+ return Ok(hits);
198
+ }
199
+ }
200
+ }
201
+ }
202
+ Ok(hits)
203
+ }
204
+
205
+ pub fn repository_summary(
206
+ root: &Path,
207
+ config: &MintConfig,
208
+ ) -> Result<RepositorySummary, CodeInspectionError> {
209
+ let root = assert_path_capability(root, Capability::Read, config)?;
210
+ let files = list_code_files(&root, usize::MAX, config)?;
211
+ let mut extensions = BTreeMap::new();
212
+ for file in &files {
213
+ let extension = file
214
+ .path
215
+ .extension()
216
+ .and_then(|value| value.to_str())
217
+ .unwrap_or("<none>")
218
+ .to_lowercase();
219
+ *extensions.entry(extension).or_insert(0) += 1;
220
+ }
221
+ Ok(RepositorySummary {
222
+ root,
223
+ file_count: files.len(),
224
+ total_bytes: files.iter().map(|file| file.size).sum(),
225
+ extensions,
226
+ })
227
+ }
228
+
229
+ pub fn inspect_code_plan(
230
+ task: impl Into<String>,
231
+ root: &Path,
232
+ inspect_files: Vec<PathBuf>,
233
+ config: &MintConfig,
234
+ ) -> Result<CodePlan, CodeInspectionError> {
235
+ let root = assert_path_capability(root, Capability::Read, config)?;
236
+ let inspect_files = inspect_files
237
+ .into_iter()
238
+ .map(|path| workspace_path(&root, &path, Capability::Read, config))
239
+ .collect::<Result<Vec<_>, _>>()?;
240
+ Ok(CodePlan {
241
+ task: task.into(),
242
+ root,
243
+ inspect_files,
244
+ steps: vec![
245
+ "Inspect repository summary and relevant files".into(),
246
+ "Search for affected symbols and behavior contracts".into(),
247
+ "Propose scoped edits and verification commands".into(),
248
+ "Require explicit approval before shell execution or file writes".into(),
249
+ ],
250
+ })
251
+ }
252
+
253
+ pub fn build_code_patch(
254
+ root: &Path,
255
+ path: PathBuf,
256
+ hunks: &[CodePatchHunk],
257
+ config: &MintConfig,
258
+ ) -> Result<CodeEdit, CodeInspectionError> {
259
+ let root = assert_path_capability(root, Capability::Write, config)?;
260
+ let path = workspace_path(&root, &path, Capability::Write, config)?;
261
+ let mut content = read_existing_content(&path)?;
262
+ for (index, hunk) in hunks.iter().enumerate() {
263
+ if !content.contains(&hunk.old_text) {
264
+ return Err(CodeInspectionError::PatchTextNotFound {
265
+ path,
266
+ index: index + 1,
267
+ });
268
+ }
269
+ content = content.replacen(&hunk.old_text, &hunk.new_text, 1);
270
+ }
271
+ Ok(CodeEdit { path, content })
272
+ }
273
+
274
+ pub fn propose_code_edits(
275
+ root: &Path,
276
+ edits: &[CodeEdit],
277
+ config: &MintConfig,
278
+ ) -> Result<CodeEditProposal, CodeInspectionError> {
279
+ if edits.is_empty() {
280
+ return Err(CodeInspectionError::EmptyEditRequest);
281
+ }
282
+ let root = assert_path_capability(root, Capability::Write, config)?;
283
+ let previews = prepare_edits(&root, edits, config)?;
284
+ Ok(CodeEditProposal {
285
+ approval_required: true,
286
+ approval_token: approval_token(&root, &previews),
287
+ edits: previews,
288
+ })
289
+ }
290
+
291
+ pub fn apply_code_edits(
292
+ root: &Path,
293
+ edits: &[CodeEdit],
294
+ approval_token_value: &str,
295
+ config: &MintConfig,
296
+ ) -> Result<Vec<AppliedCodeEdit>, CodeInspectionError> {
297
+ let proposal = propose_code_edits(root, edits, config)?;
298
+ if proposal.approval_token != approval_token_value {
299
+ return Err(CodeInspectionError::InvalidApprovalToken);
300
+ }
301
+ let root = assert_path_capability(root, Capability::Write, config)?;
302
+ let prepared = prepare_edits(&root, edits, config)?;
303
+ for preview in &prepared {
304
+ let current = read_optional_content(&preview.path)?;
305
+ if sha256(&current) != preview.before_sha256 {
306
+ return Err(CodeInspectionError::StaleProposal(preview.path.clone()));
307
+ }
308
+ }
309
+ let mut applied = Vec::new();
310
+ for (edit, preview) in edits.iter().zip(prepared) {
311
+ if let Some(parent) = preview.path.parent() {
312
+ fs::create_dir_all(parent).map_err(|source| CodeInspectionError::Write {
313
+ path: parent.to_path_buf(),
314
+ source,
315
+ })?;
316
+ }
317
+ fs::write(&preview.path, &edit.content).map_err(|source| CodeInspectionError::Write {
318
+ path: preview.path.clone(),
319
+ source,
320
+ })?;
321
+ applied.push(AppliedCodeEdit {
322
+ path: preview.path,
323
+ created: !preview.existed,
324
+ bytes_written: edit.content.len(),
325
+ });
326
+ }
327
+ Ok(applied)
328
+ }
329
+
330
+ fn prepare_edits(
331
+ root: &Path,
332
+ edits: &[CodeEdit],
333
+ config: &MintConfig,
334
+ ) -> Result<Vec<CodeEditPreview>, CodeInspectionError> {
335
+ edits
336
+ .iter()
337
+ .map(|edit| {
338
+ let path = workspace_path(root, &edit.path, Capability::Write, config)?;
339
+ let existed = path.exists();
340
+ let previous = read_optional_content(&path)?;
341
+ Ok(CodeEditPreview {
342
+ path: path.clone(),
343
+ existed,
344
+ before_sha256: sha256(&previous),
345
+ after_sha256: sha256(&edit.content),
346
+ diff: full_file_diff(&path, &previous, &edit.content),
347
+ })
348
+ })
349
+ .collect()
350
+ }
351
+
352
+ fn workspace_path(
353
+ root: &Path,
354
+ path: &Path,
355
+ capability: Capability,
356
+ config: &MintConfig,
357
+ ) -> Result<PathBuf, CodeInspectionError> {
358
+ let path = assert_path_capability(&root.join(path), capability, config)?;
359
+ if !path.starts_with(root) {
360
+ return Err(CodeInspectionError::OutsideWorkspace(path));
361
+ }
362
+ Ok(path)
363
+ }
364
+
365
+ fn read_existing_content(path: &Path) -> Result<String, CodeInspectionError> {
366
+ if !path.is_file() {
367
+ return Err(CodeInspectionError::NotAFile(path.to_path_buf()));
368
+ }
369
+ fs::read_to_string(path).map_err(|source| CodeInspectionError::Read {
370
+ path: path.to_path_buf(),
371
+ source,
372
+ })
373
+ }
374
+
375
+ fn read_optional_content(path: &Path) -> Result<String, CodeInspectionError> {
376
+ if path.exists() {
377
+ read_existing_content(path)
378
+ } else {
379
+ Ok(String::new())
380
+ }
381
+ }
382
+
383
+ fn approval_token(root: &Path, edits: &[CodeEditPreview]) -> String {
384
+ let mut hasher = Sha256::new();
385
+ hasher.update(b"mint-code-edit-approval-v1\0");
386
+ hasher.update(root.to_string_lossy().as_bytes());
387
+ for edit in edits {
388
+ hasher.update(b"\0");
389
+ hasher.update(edit.path.to_string_lossy().as_bytes());
390
+ hasher.update(b"\0");
391
+ hasher.update(edit.before_sha256.as_bytes());
392
+ hasher.update(b"\0");
393
+ hasher.update(edit.after_sha256.as_bytes());
394
+ }
395
+ format!("{:x}", hasher.finalize())
396
+ }
397
+
398
+ fn sha256(content: &str) -> String {
399
+ format!("{:x}", Sha256::digest(content.as_bytes()))
400
+ }
401
+
402
+ fn full_file_diff(path: &Path, previous: &str, next: &str) -> String {
403
+ let label = path.display();
404
+ let mut lines = vec![
405
+ format!("--- a/{label}"),
406
+ format!("+++ b/{label}"),
407
+ format!(
408
+ "@@ -1,{} +1,{} @@",
409
+ previous.lines().count(),
410
+ next.lines().count()
411
+ ),
412
+ ];
413
+ lines.extend(previous.lines().map(|line| format!("-{line}")));
414
+ lines.extend(next.lines().map(|line| format!("+{line}")));
415
+ lines.join("\n")
416
+ }
417
+
418
+ fn collect_files(
419
+ directory: &Path,
420
+ files: &mut Vec<CodeFile>,
421
+ limit: usize,
422
+ is_root: bool,
423
+ ) -> Result<(), CodeInspectionError> {
424
+ if files.len() >= limit || (!is_root && is_ignored_directory(directory)) {
425
+ return Ok(());
426
+ }
427
+ let entries = fs::read_dir(directory).map_err(|source| CodeInspectionError::Read {
428
+ path: directory.to_path_buf(),
429
+ source,
430
+ })?;
431
+ for entry in entries {
432
+ if files.len() >= limit {
433
+ break;
434
+ }
435
+ let entry = entry.map_err(|source| CodeInspectionError::Read {
436
+ path: directory.to_path_buf(),
437
+ source,
438
+ })?;
439
+ let path = entry.path();
440
+ let file_type = entry
441
+ .file_type()
442
+ .map_err(|source| CodeInspectionError::Read {
443
+ path: path.clone(),
444
+ source,
445
+ })?;
446
+ if file_type.is_dir() {
447
+ collect_files(&path, files, limit, false)?;
448
+ } else if file_type.is_file() {
449
+ let size = entry
450
+ .metadata()
451
+ .map_err(|source| CodeInspectionError::Read {
452
+ path: path.clone(),
453
+ source,
454
+ })?
455
+ .len();
456
+ files.push(CodeFile { path, size });
457
+ }
458
+ }
459
+ Ok(())
460
+ }
461
+
462
+ fn is_ignored_directory(path: &Path) -> bool {
463
+ path.file_name()
464
+ .and_then(|name| name.to_str())
465
+ .is_some_and(|name| IGNORED_DIRECTORIES.contains(&name))
466
+ }
467
+
468
+ pub fn parse_github_url(url: &str) -> Option<(String, String)> {
469
+ let cleaned = url
470
+ .trim()
471
+ .trim_start_matches("https://")
472
+ .trim_start_matches("http://")
473
+ .trim_start_matches("www.")
474
+ .trim_start_matches("github.com/");
475
+
476
+ let parts: Vec<&str> = cleaned.split('/').collect();
477
+ if parts.len() >= 2 {
478
+ let owner = parts[0].to_string();
479
+ let mut repo = parts[1].to_string();
480
+ if repo.ends_with(".git") {
481
+ repo = repo[..repo.len() - 4].to_string();
482
+ }
483
+ Some((owner, repo))
484
+ } else {
485
+ None
486
+ }
487
+ }
488
+
489
+ pub async fn fetch_github_repo_summary(owner: &str, repo: &str) -> Result<String, String> {
490
+ let client = reqwest::Client::builder()
491
+ .user_agent("mint-core")
492
+ .build()
493
+ .map_err(|e| e.to_string())?;
494
+
495
+ // 1. Fetch Repository Info
496
+ let repo_url = format!("https://api.github.com/repos/{}/{}", owner, repo);
497
+ let repo_resp = client
498
+ .get(&repo_url)
499
+ .send()
500
+ .await
501
+ .map_err(|e| e.to_string())?;
502
+ if !repo_resp.status().is_success() {
503
+ return Err(format!(
504
+ "Failed to fetch repository metadata: {}",
505
+ repo_resp.status()
506
+ ));
507
+ }
508
+ let repo_info: serde_json::Value = repo_resp.json().await.map_err(|e| e.to_string())?;
509
+
510
+ let description = repo_info["description"]
511
+ .as_str()
512
+ .unwrap_or("No description provided.");
513
+ let language = repo_info["language"].as_str().unwrap_or("Unknown");
514
+ let stars = repo_info["stargazers_count"].as_u64().unwrap_or(0);
515
+ let forks = repo_info["forks_count"].as_u64().unwrap_or(0);
516
+
517
+ let mut topics_list = Vec::new();
518
+ if let Some(topics) = repo_info["topics"].as_array() {
519
+ for t in topics {
520
+ if let Some(t_str) = t.as_str() {
521
+ topics_list.push(t_str.to_string());
522
+ }
523
+ }
524
+ }
525
+ let topics_str = if topics_list.is_empty() {
526
+ "None".to_string()
527
+ } else {
528
+ topics_list.join(", ")
529
+ };
530
+
531
+ // 2. Fetch Directory contents (top level)
532
+ let contents_url = format!("https://api.github.com/repos/{}/{}/contents", owner, repo);
533
+ let contents_resp = client
534
+ .get(&contents_url)
535
+ .send()
536
+ .await
537
+ .map_err(|e| e.to_string())?;
538
+ let mut file_tree = String::from("Unavailable");
539
+ if contents_resp.status().is_success() {
540
+ if let Ok(contents_info) = contents_resp.json::<serde_json::Value>().await {
541
+ if let Some(arr) = contents_info.as_array() {
542
+ let mut files = Vec::new();
543
+ for item in arr {
544
+ let name = item["name"].as_str().unwrap_or("");
545
+ let r#type = item["type"].as_str().unwrap_or("");
546
+ files.push(format!("- {} ({})", name, r#type));
547
+ }
548
+ file_tree = files.join("\n");
549
+ }
550
+ }
551
+ }
552
+
553
+ // 3. Fetch README.md
554
+ let readme_url = format!("https://api.github.com/repos/{}/{}/readme", owner, repo);
555
+ let readme_resp = client
556
+ .get(&readme_url)
557
+ .send()
558
+ .await
559
+ .map_err(|e| e.to_string())?;
560
+ let mut readme_text = String::from("No README available.");
561
+ if readme_resp.status().is_success() {
562
+ if let Ok(readme_info) = readme_resp.json::<serde_json::Value>().await {
563
+ if let Some(content_b64) = readme_info["content"].as_str() {
564
+ let cleaned_b64 = content_b64.replace('\n', "").replace('\r', "");
565
+ use base64::{Engine as _, engine::general_purpose::STANDARD};
566
+ if let Ok(decoded_bytes) = STANDARD.decode(cleaned_b64) {
567
+ readme_text = String::from_utf8_lossy(&decoded_bytes).to_string();
568
+ }
569
+ }
570
+ }
571
+ }
572
+
573
+ let summary = format!(
574
+ "Repository: {}/{}\nDescription: {}\nPrimary Language: {}\nStars: {}\nForks: {}\nTopics: {}\n\nTop-level File Directory:\n{}\n\nREADME.md:\n{}",
575
+ owner, repo, description, language, stars, forks, topics_str, file_tree, readme_text
576
+ );
577
+
578
+ Ok(summary)
579
+ }
580
+
581
+ #[cfg(test)]
582
+ mod tests {
583
+ use super::*;
584
+
585
+ fn config_for(root: &Path) -> MintConfig {
586
+ MintConfig {
587
+ allowed_read_paths: vec![root.to_path_buf()],
588
+ allowed_write_paths: vec![root.to_path_buf()],
589
+ blocked_paths: vec![],
590
+ ..MintConfig::default()
591
+ }
592
+ }
593
+
594
+ #[test]
595
+ fn searches_text_files_and_skips_build_directories() {
596
+ let root = std::env::temp_dir().join("mint-code-tools-search");
597
+ let _ = fs::remove_dir_all(&root);
598
+ fs::create_dir_all(root.join("target")).unwrap();
599
+ fs::write(root.join("main.rs"), "fn mint_tool() {}\n").unwrap();
600
+ fs::write(root.join("target/generated.rs"), "mint_tool\n").unwrap();
601
+ let hits = search_code(&root, "mint_tool", 10, &config_for(&root)).unwrap();
602
+ assert_eq!(hits.len(), 1);
603
+ assert_eq!(hits[0].line, 1);
604
+ let _ = fs::remove_dir_all(root);
605
+ }
606
+
607
+ #[test]
608
+ fn lists_explicitly_requested_build_directory() {
609
+ let root = std::env::temp_dir().join("mint-code-tools-explicit-out");
610
+ let _ = fs::remove_dir_all(&root);
611
+ fs::create_dir_all(root.join("out")).unwrap();
612
+ fs::write(root.join("out/index.html"), "<div>built</div>\n").unwrap();
613
+ fs::write(root.join("main.rs"), "fn main() {}\n").unwrap();
614
+
615
+ let config = config_for(&root);
616
+ let repo_files = list_code_files(&root, 10, &config).unwrap();
617
+ assert_eq!(repo_files.len(), 1);
618
+ assert_eq!(repo_files[0].path, root.join("main.rs"));
619
+
620
+ let out_files = list_code_files(&root.join("out"), 10, &config).unwrap();
621
+ assert_eq!(out_files.len(), 1);
622
+ assert_eq!(out_files[0].path, root.join("out/index.html"));
623
+
624
+ let _ = fs::remove_dir_all(root);
625
+ }
626
+
627
+ #[test]
628
+ fn blocks_plan_files_outside_allowed_root() {
629
+ let root = std::env::temp_dir().join("mint-code-tools-plan");
630
+ fs::create_dir_all(&root).unwrap();
631
+ let result = inspect_code_plan(
632
+ "test",
633
+ &root,
634
+ vec![PathBuf::from("../../etc/passwd")],
635
+ &config_for(&root),
636
+ );
637
+ assert!(matches!(result, Err(CodeInspectionError::Safety(_))));
638
+ let _ = fs::remove_dir_all(root);
639
+ }
640
+
641
+ #[test]
642
+ fn requires_matching_approval_token_before_writing() {
643
+ let root = std::env::temp_dir().join("mint-code-tools-approval");
644
+ let _ = fs::remove_dir_all(&root);
645
+ fs::create_dir_all(&root).unwrap();
646
+ let edit = CodeEdit {
647
+ path: PathBuf::from("note.txt"),
648
+ content: "approved\n".into(),
649
+ };
650
+ let config = config_for(&root);
651
+ assert!(matches!(
652
+ apply_code_edits(&root, std::slice::from_ref(&edit), "wrong", &config),
653
+ Err(CodeInspectionError::InvalidApprovalToken)
654
+ ));
655
+ assert!(!root.join("note.txt").exists());
656
+ let proposal = propose_code_edits(&root, std::slice::from_ref(&edit), &config).unwrap();
657
+ apply_code_edits(&root, &[edit], &proposal.approval_token, &config).unwrap();
658
+ assert_eq!(
659
+ fs::read_to_string(root.join("note.txt")).unwrap(),
660
+ "approved\n"
661
+ );
662
+ let _ = fs::remove_dir_all(root);
663
+ }
664
+
665
+ #[test]
666
+ fn rejects_approved_edit_after_source_changes() {
667
+ let root = std::env::temp_dir().join("mint-code-tools-stale");
668
+ let _ = fs::remove_dir_all(&root);
669
+ fs::create_dir_all(&root).unwrap();
670
+ fs::write(root.join("note.txt"), "before\n").unwrap();
671
+ let edit = CodeEdit {
672
+ path: PathBuf::from("note.txt"),
673
+ content: "after\n".into(),
674
+ };
675
+ let config = config_for(&root);
676
+ let proposal = propose_code_edits(&root, std::slice::from_ref(&edit), &config).unwrap();
677
+ fs::write(root.join("note.txt"), "changed elsewhere\n").unwrap();
678
+ assert!(matches!(
679
+ apply_code_edits(&root, &[edit], &proposal.approval_token, &config),
680
+ Err(CodeInspectionError::InvalidApprovalToken)
681
+ | Err(CodeInspectionError::StaleProposal(_))
682
+ ));
683
+ let _ = fs::remove_dir_all(root);
684
+ }
685
+
686
+ #[test]
687
+ fn patch_replaces_exact_text_once() {
688
+ let root = std::env::temp_dir().join("mint-code-tools-patch");
689
+ let _ = fs::remove_dir_all(&root);
690
+ fs::create_dir_all(&root).unwrap();
691
+ fs::write(root.join("note.txt"), "one one\n").unwrap();
692
+ let edit = build_code_patch(
693
+ &root,
694
+ PathBuf::from("note.txt"),
695
+ &[CodePatchHunk {
696
+ old_text: "one".into(),
697
+ new_text: "two".into(),
698
+ }],
699
+ &config_for(&root),
700
+ )
701
+ .unwrap();
702
+ assert_eq!(edit.content, "two one\n");
703
+ let _ = fs::remove_dir_all(root);
704
+ }
705
+
706
+ #[test]
707
+ fn blocks_edit_paths_outside_workspace_even_when_policy_allows_them() {
708
+ let root = std::env::temp_dir().join("mint-code-tools-workspace");
709
+ fs::create_dir_all(&root).unwrap();
710
+ let config = MintConfig {
711
+ allowed_write_paths: vec![std::env::temp_dir()],
712
+ blocked_paths: vec![],
713
+ ..MintConfig::default()
714
+ };
715
+ let result = propose_code_edits(
716
+ &root,
717
+ &[CodeEdit {
718
+ path: PathBuf::from("../outside.txt"),
719
+ content: "blocked".into(),
720
+ }],
721
+ &config,
722
+ );
723
+ assert!(matches!(
724
+ result,
725
+ Err(CodeInspectionError::OutsideWorkspace(_))
726
+ ));
727
+ let _ = fs::remove_dir_all(root);
728
+ }
729
+ }