automagik-forge 0.1.11 → 0.1.13

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 (301) hide show
  1. package/.cargo/config.toml +13 -0
  2. package/.claude/commands/commit.md +376 -0
  3. package/.claude/commands/prompt.md +871 -0
  4. package/.env.example +20 -0
  5. package/.github/actions/setup-node/action.yml +29 -0
  6. package/.github/images/automagik-logo.png +0 -0
  7. package/.github/workflows/pre-release.yml +470 -0
  8. package/.github/workflows/publish.yml +145 -0
  9. package/.github/workflows/test.yml +63 -0
  10. package/.mcp.json +57 -0
  11. package/AGENT.md +40 -0
  12. package/CLAUDE.md +40 -0
  13. package/CODE-OF-CONDUCT.md +89 -0
  14. package/Cargo.toml +19 -0
  15. package/Dockerfile +43 -0
  16. package/LICENSE +201 -0
  17. package/Makefile +97 -0
  18. package/README.md +447 -143
  19. package/backend/.sqlx/query-01b7e2bac1261d8be3d03c03df3e5220590da6c31c77f161074fc62752d63881.json +12 -0
  20. package/backend/.sqlx/query-03f2b02ba6dc5ea2b3cf6b1004caea0ad6bcc10ebd63f441d321a389f026e263.json +12 -0
  21. package/backend/.sqlx/query-0923b77d137a29fc54d399a873ff15fc4af894490bc65a4d344a7575cb0d8643.json +12 -0
  22. package/backend/.sqlx/query-0f808bcdb63c5f180836e448dd64c435c51758b2fc54a52ce9e67495b1ab200e.json +68 -0
  23. package/backend/.sqlx/query-1268afe9ca849daa6722e3df7ca8e9e61f0d37052e782bb5452ab8e1018d9b63.json +12 -0
  24. package/backend/.sqlx/query-1b082630a9622f8667ee7a9aba2c2d3176019a68c6bb83d33008594821415a57.json +12 -0
  25. package/backend/.sqlx/query-1c7b06ba1e112abf6b945a2ff08a0b40ec23f3738c2e7399f067b558cf8d490e.json +12 -0
  26. package/backend/.sqlx/query-1f619f01f46859a64ded531dd0ef61abacfe62e758abe7030a6aa745140b95ca.json +104 -0
  27. package/backend/.sqlx/query-1fca1ce14b4b20205364cd1f1f45ebe1d2e30cd745e59e189d56487b5639dfbb.json +12 -0
  28. package/backend/.sqlx/query-212828320e8d871ab9d83705a040b23bcf0393dc7252177fc539a74657f578ef.json +32 -0
  29. package/backend/.sqlx/query-290ce5c152be8d36e58ff42570f9157beb07ab9e77a03ec6fc30b4f56f9b8f6b.json +56 -0
  30. package/backend/.sqlx/query-2b471d2c2e8ffbe0cd42d2a91b814c0d79f9d09200f147e3cea33ba4ce673c8a.json +68 -0
  31. package/backend/.sqlx/query-354a48c705bb9bb2048c1b7f10fcb714e23f9db82b7a4ea6932486197b2ede6a.json +92 -0
  32. package/backend/.sqlx/query-36c9e3dd10648e94b949db5c91a774ecb1e10a899ef95da74066eccedca4d8b2.json +12 -0
  33. package/backend/.sqlx/query-36e4ba7bbd81b402d5a20b6005755eafbb174c8dda442081823406ac32809a94.json +56 -0
  34. package/backend/.sqlx/query-3a5b3c98a55ca183ab20c74708e3d7e579dda37972c059e7515c4ceee4bd8dd3.json +62 -0
  35. package/backend/.sqlx/query-3d0a1cabf2a52e9d90cdfd29c509ca89aeb448d0c1d2446c65cd43db40735e86.json +62 -0
  36. package/backend/.sqlx/query-3d6bd16fbce59efe30b7f67ea342e0e4ea6d1432389c02468ad79f1f742d4031.json +56 -0
  37. package/backend/.sqlx/query-4049ca413b285a05aca6b25385e9c8185575f01e9069e4e8581aa45d713f612f.json +32 -0
  38. package/backend/.sqlx/query-412bacd3477d86369082e90f52240407abce436cb81292d42b2dbe1e5c18eea1.json +104 -0
  39. package/backend/.sqlx/query-417a8b1ff4e51de82aea0159a3b97932224dc325b23476cb84153d690227fd8b.json +62 -0
  40. package/backend/.sqlx/query-461cc1b0bb6fd909afc9dd2246e8526b3771cfbb0b22ae4b5d17b51af587b9e2.json +56 -0
  41. package/backend/.sqlx/query-58408c7a8cdeeda0bef359f1f9bd91299a339dc2b191462fc58c9736a56d5227.json +92 -0
  42. package/backend/.sqlx/query-5a886026d75d515c01f347cc203c8d99dd04c61dc468e2e4c5aa548436d13834.json +62 -0
  43. package/backend/.sqlx/query-5b902137b11022d2e1a5c4f6a9c83fec1a856c6a710aff831abd2382ede76b43.json +12 -0
  44. package/backend/.sqlx/query-5ed1238e52e59bb5f76c0f153fd99a14093f7ce2585bf9843585608f17ec575b.json +104 -0
  45. package/backend/.sqlx/query-6e8b860b14decfc2227dc57213f38442943d3fbef5c8418fd6b634c6e0f5e2ea.json +104 -0
  46. package/backend/.sqlx/query-6ec414276994c4ccb2433eaa5b1b342168557d17ddf5a52dac84cb1b59b9de8f.json +68 -0
  47. package/backend/.sqlx/query-6ecfa16d0cf825aacf233544b5baf151e9adfdca26c226ad71020d291fd802d5.json +62 -0
  48. package/backend/.sqlx/query-72509d252c39fce77520aa816cb2acbc1fb35dc2605e7be893610599b2427f2e.json +62 -0
  49. package/backend/.sqlx/query-75239b2da188f749707d77f3c1544332ca70db3d6d6743b2601dc0d167536437.json +62 -0
  50. package/backend/.sqlx/query-83d10e29f8478aff33434f9ac67068e013b888b953a2657e2bb72a6f619d04f2.json +50 -0
  51. package/backend/.sqlx/query-8610803360ea18b9b9d078a6981ea56abfbfe84e6354fc1d5ae4c622e01410ed.json +68 -0
  52. package/backend/.sqlx/query-86d03eb70eef39c59296416867f2ee66c9f7cd8b7f961fbda2f89fc0a1c442c2.json +12 -0
  53. package/backend/.sqlx/query-87d0feb5a6b442bad9c60068ea7569599cc6fc91a0e2692ecb42e93b03201b9d.json +68 -0
  54. package/backend/.sqlx/query-8a67b3b3337248f06a57bdf8a908f7ef23177431eaed82dc08c94c3e5944340e.json +12 -0
  55. package/backend/.sqlx/query-8f01ebd64bdcde6a090479f14810d73ba23020e76fd70854ac57f2da251702c3.json +12 -0
  56. package/backend/.sqlx/query-90fd607fcb2dca72239ff25e618e21e174b195991eaa33722cbf5f76da84cfab.json +62 -0
  57. package/backend/.sqlx/query-92e8bdbcd80c5ff3db7a35cf79492048803ef305cbdef0d0a1fe5dc881ca8c71.json +104 -0
  58. package/backend/.sqlx/query-93a1605f90e9672dad29b472b6ad85fa9a55ea3ffa5abcb8724b09d61be254ca.json +20 -0
  59. package/backend/.sqlx/query-9472c8fb477958167f5fae40b85ac44252468c5226b2cdd7770f027332eed6d7.json +104 -0
  60. package/backend/.sqlx/query-96036c4f9e0f48bdc5a4a4588f0c5f288ac7aaa5425cac40fc33f337e1a351f2.json +56 -0
  61. package/backend/.sqlx/query-9edb2c01e91fd0f0fe7b56e988c7ae0393150f50be3f419a981e035c0121dfc7.json +104 -0
  62. package/backend/.sqlx/query-a157cf00616f703bfba21927f1eb1c9eec2a81c02da15f66efdba0b6c375de1b.json +26 -0
  63. package/backend/.sqlx/query-a31fff84f3b8e532fd1160447d89d700f06ae08821fee00c9a5b60492b05259c.json +62 -0
  64. package/backend/.sqlx/query-a5ba908419fb3e456bdd2daca41ba06cc3212ffffb8520fc7dbbcc8b60ada314.json +12 -0
  65. package/backend/.sqlx/query-a6d2961718dbc3b1a925e549f49a159c561bef58c105529275f274b27e2eba5b.json +104 -0
  66. package/backend/.sqlx/query-a9e93d5b09b29faf66e387e4d7596a792d81e75c4d3726e83c2963e8d7c9b56f.json +104 -0
  67. package/backend/.sqlx/query-ac5247c8d7fb86e4650c4b0eb9420031614c831b7b085083bac20c1af314c538.json +12 -0
  68. package/backend/.sqlx/query-afef9467be74c411c4cb119a8b2b1aea53049877dfc30cc60b486134ba4b4c9f.json +68 -0
  69. package/backend/.sqlx/query-b2b2c6b4d0b1a347b5c4cb63c3a46a265d4db53be9554989a814b069d0af82f2.json +62 -0
  70. package/backend/.sqlx/query-c50d2ff0b12e5bcc81e371089ee2d007e233e7db93aefba4fef08e7aa68f5ab7.json +20 -0
  71. package/backend/.sqlx/query-c614e6056b244ca07f1b9d44e7edc9d5819225c6f8d9e077070c6e518a17f50b.json +12 -0
  72. package/backend/.sqlx/query-c67259be8bf4ee0cfd32167b2aa3b7fe9192809181a8171bf1c2d6df731967ae.json +12 -0
  73. package/backend/.sqlx/query-d2d0a1b985ebbca6a2b3e882a221a219f3199890fa640afc946ef1a792d6d8de.json +12 -0
  74. package/backend/.sqlx/query-d30aa5786757f32bf2b9c5fe51a45e506c71c28c5994e430d9b0546adb15ffa2.json +20 -0
  75. package/backend/.sqlx/query-d3b9ea1de1576af71b312924ce7f4ea8ae5dbe2ac138ea3b4470f2d5cd734846.json +12 -0
  76. package/backend/.sqlx/query-ed8456646fa69ddd412441955f06ff22bfb790f29466450735e0b8bb1bc4ec94.json +12 -0
  77. package/backend/Cargo.toml +71 -0
  78. package/backend/build.rs +32 -0
  79. package/backend/migrations/20250617183714_init.sql +44 -0
  80. package/backend/migrations/20250620212427_execution_processes.sql +25 -0
  81. package/backend/migrations/20250620214100_remove_stdout_stderr_from_task_attempts.sql +28 -0
  82. package/backend/migrations/20250621120000_relate_activities_to_execution_processes.sql +23 -0
  83. package/backend/migrations/20250623120000_executor_sessions.sql +17 -0
  84. package/backend/migrations/20250623130000_add_executor_type_to_execution_processes.sql +4 -0
  85. package/backend/migrations/20250625000000_add_dev_script_to_projects.sql +4 -0
  86. package/backend/migrations/20250701000000_add_branch_to_task_attempts.sql +2 -0
  87. package/backend/migrations/20250701000001_add_pr_tracking_to_task_attempts.sql +5 -0
  88. package/backend/migrations/20250701120000_add_assistant_message_to_executor_sessions.sql +2 -0
  89. package/backend/migrations/20250708000000_add_base_branch_to_task_attempts.sql +2 -0
  90. package/backend/migrations/20250709000000_add_worktree_deleted_flag.sql +2 -0
  91. package/backend/migrations/20250710000000_add_setup_completion.sql +3 -0
  92. package/backend/migrations/20250715154859_add_task_templates.sql +25 -0
  93. package/backend/migrations/20250716143725_add_default_templates.sql +174 -0
  94. package/backend/migrations/20250716161432_update_executor_names_to_kebab_case.sql +20 -0
  95. package/backend/migrations/20250716170000_add_parent_task_to_tasks.sql +7 -0
  96. package/backend/migrations/20250717000000_drop_task_attempt_activities.sql +9 -0
  97. package/backend/migrations/20250719000000_add_cleanup_script_to_projects.sql +2 -0
  98. package/backend/migrations/20250720000000_add_cleanupscript_to_process_type_constraint.sql +25 -0
  99. package/backend/migrations/20250723000000_add_wish_to_tasks.sql +7 -0
  100. package/backend/migrations/20250724000000_remove_unique_wish_constraint.sql +5 -0
  101. package/backend/scripts/toast-notification.ps1 +23 -0
  102. package/backend/sounds/abstract-sound1.wav +0 -0
  103. package/backend/sounds/abstract-sound2.wav +0 -0
  104. package/backend/sounds/abstract-sound3.wav +0 -0
  105. package/backend/sounds/abstract-sound4.wav +0 -0
  106. package/backend/sounds/cow-mooing.wav +0 -0
  107. package/backend/sounds/phone-vibration.wav +0 -0
  108. package/backend/sounds/rooster.wav +0 -0
  109. package/backend/src/app_state.rs +218 -0
  110. package/backend/src/bin/generate_types.rs +189 -0
  111. package/backend/src/bin/mcp_task_server.rs +191 -0
  112. package/backend/src/execution_monitor.rs +1193 -0
  113. package/backend/src/executor.rs +1053 -0
  114. package/backend/src/executors/amp.rs +697 -0
  115. package/backend/src/executors/ccr.rs +91 -0
  116. package/backend/src/executors/charm_opencode.rs +113 -0
  117. package/backend/src/executors/claude.rs +887 -0
  118. package/backend/src/executors/cleanup_script.rs +124 -0
  119. package/backend/src/executors/dev_server.rs +53 -0
  120. package/backend/src/executors/echo.rs +79 -0
  121. package/backend/src/executors/gemini/config.rs +67 -0
  122. package/backend/src/executors/gemini/streaming.rs +363 -0
  123. package/backend/src/executors/gemini.rs +765 -0
  124. package/backend/src/executors/mod.rs +23 -0
  125. package/backend/src/executors/opencode_ai.rs +113 -0
  126. package/backend/src/executors/setup_script.rs +130 -0
  127. package/backend/src/executors/sst_opencode/filter.rs +184 -0
  128. package/backend/src/executors/sst_opencode/tools.rs +139 -0
  129. package/backend/src/executors/sst_opencode.rs +756 -0
  130. package/backend/src/lib.rs +45 -0
  131. package/backend/src/main.rs +324 -0
  132. package/backend/src/mcp/mod.rs +1 -0
  133. package/backend/src/mcp/task_server.rs +850 -0
  134. package/backend/src/middleware/mod.rs +3 -0
  135. package/backend/src/middleware/model_loaders.rs +242 -0
  136. package/backend/src/models/api_response.rs +36 -0
  137. package/backend/src/models/config.rs +375 -0
  138. package/backend/src/models/execution_process.rs +430 -0
  139. package/backend/src/models/executor_session.rs +225 -0
  140. package/backend/src/models/mod.rs +12 -0
  141. package/backend/src/models/project.rs +356 -0
  142. package/backend/src/models/task.rs +345 -0
  143. package/backend/src/models/task_attempt.rs +1214 -0
  144. package/backend/src/models/task_template.rs +146 -0
  145. package/backend/src/openapi.rs +93 -0
  146. package/backend/src/routes/auth.rs +297 -0
  147. package/backend/src/routes/config.rs +385 -0
  148. package/backend/src/routes/filesystem.rs +228 -0
  149. package/backend/src/routes/health.rs +16 -0
  150. package/backend/src/routes/mod.rs +9 -0
  151. package/backend/src/routes/projects.rs +562 -0
  152. package/backend/src/routes/stream.rs +244 -0
  153. package/backend/src/routes/task_attempts.rs +1172 -0
  154. package/backend/src/routes/task_templates.rs +229 -0
  155. package/backend/src/routes/tasks.rs +353 -0
  156. package/backend/src/services/analytics.rs +216 -0
  157. package/backend/src/services/git_service.rs +1321 -0
  158. package/backend/src/services/github_service.rs +307 -0
  159. package/backend/src/services/mod.rs +13 -0
  160. package/backend/src/services/notification_service.rs +263 -0
  161. package/backend/src/services/pr_monitor.rs +214 -0
  162. package/backend/src/services/process_service.rs +940 -0
  163. package/backend/src/utils/path.rs +96 -0
  164. package/backend/src/utils/shell.rs +19 -0
  165. package/backend/src/utils/text.rs +24 -0
  166. package/backend/src/utils/worktree_manager.rs +578 -0
  167. package/backend/src/utils.rs +125 -0
  168. package/backend/test.db +0 -0
  169. package/build-npm-package.sh +61 -0
  170. package/dev_assets_seed/config.json +19 -0
  171. package/frontend/.eslintrc.json +25 -0
  172. package/frontend/.prettierrc.json +8 -0
  173. package/frontend/components.json +17 -0
  174. package/frontend/index.html +19 -0
  175. package/frontend/package-lock.json +7321 -0
  176. package/frontend/package.json +61 -0
  177. package/frontend/postcss.config.js +6 -0
  178. package/frontend/public/android-chrome-192x192.png +0 -0
  179. package/frontend/public/android-chrome-512x512.png +0 -0
  180. package/frontend/public/apple-touch-icon.png +0 -0
  181. package/frontend/public/automagik-forge-logo-dark.svg +3 -0
  182. package/frontend/public/automagik-forge-logo.svg +3 -0
  183. package/frontend/public/automagik-forge-screenshot-overview.png +0 -0
  184. package/frontend/public/favicon-16x16.png +0 -0
  185. package/frontend/public/favicon-32x32.png +0 -0
  186. package/frontend/public/favicon.ico +0 -0
  187. package/frontend/public/site.webmanifest +1 -0
  188. package/frontend/public/viba-kanban-favicon.png +0 -0
  189. package/frontend/src/App.tsx +157 -0
  190. package/frontend/src/components/DisclaimerDialog.tsx +106 -0
  191. package/frontend/src/components/GitHubLoginDialog.tsx +314 -0
  192. package/frontend/src/components/OnboardingDialog.tsx +185 -0
  193. package/frontend/src/components/PrivacyOptInDialog.tsx +130 -0
  194. package/frontend/src/components/ProvidePatDialog.tsx +98 -0
  195. package/frontend/src/components/TaskTemplateManager.tsx +336 -0
  196. package/frontend/src/components/config-provider.tsx +119 -0
  197. package/frontend/src/components/context/TaskDetailsContextProvider.tsx +470 -0
  198. package/frontend/src/components/context/taskDetailsContext.ts +125 -0
  199. package/frontend/src/components/keyboard-shortcuts-demo.tsx +35 -0
  200. package/frontend/src/components/layout/navbar.tsx +86 -0
  201. package/frontend/src/components/logo.tsx +44 -0
  202. package/frontend/src/components/projects/ProjectCard.tsx +155 -0
  203. package/frontend/src/components/projects/project-detail.tsx +251 -0
  204. package/frontend/src/components/projects/project-form-fields.tsx +238 -0
  205. package/frontend/src/components/projects/project-form.tsx +301 -0
  206. package/frontend/src/components/projects/project-list.tsx +200 -0
  207. package/frontend/src/components/projects/projects-page.tsx +20 -0
  208. package/frontend/src/components/tasks/BranchSelector.tsx +169 -0
  209. package/frontend/src/components/tasks/DeleteFileConfirmationDialog.tsx +94 -0
  210. package/frontend/src/components/tasks/EditorSelectionDialog.tsx +119 -0
  211. package/frontend/src/components/tasks/TaskCard.tsx +154 -0
  212. package/frontend/src/components/tasks/TaskDetails/CollapsibleToolbar.tsx +33 -0
  213. package/frontend/src/components/tasks/TaskDetails/DiffCard.tsx +109 -0
  214. package/frontend/src/components/tasks/TaskDetails/DiffChunkSection.tsx +135 -0
  215. package/frontend/src/components/tasks/TaskDetails/DiffFile.tsx +296 -0
  216. package/frontend/src/components/tasks/TaskDetails/DiffTab.tsx +32 -0
  217. package/frontend/src/components/tasks/TaskDetails/DisplayConversationEntry.tsx +392 -0
  218. package/frontend/src/components/tasks/TaskDetails/LogsTab/Conversation.tsx +256 -0
  219. package/frontend/src/components/tasks/TaskDetails/LogsTab/ConversationEntry.tsx +56 -0
  220. package/frontend/src/components/tasks/TaskDetails/LogsTab/NormalizedConversationViewer.tsx +92 -0
  221. package/frontend/src/components/tasks/TaskDetails/LogsTab/Prompt.tsx +22 -0
  222. package/frontend/src/components/tasks/TaskDetails/LogsTab/SetupScriptRunning.tsx +49 -0
  223. package/frontend/src/components/tasks/TaskDetails/LogsTab.tsx +186 -0
  224. package/frontend/src/components/tasks/TaskDetails/ProcessesTab.tsx +288 -0
  225. package/frontend/src/components/tasks/TaskDetails/RelatedTasksTab.tsx +216 -0
  226. package/frontend/src/components/tasks/TaskDetails/TabNavigation.tsx +93 -0
  227. package/frontend/src/components/tasks/TaskDetailsHeader.tsx +169 -0
  228. package/frontend/src/components/tasks/TaskDetailsPanel.tsx +126 -0
  229. package/frontend/src/components/tasks/TaskDetailsToolbar.tsx +302 -0
  230. package/frontend/src/components/tasks/TaskFollowUpSection.tsx +130 -0
  231. package/frontend/src/components/tasks/TaskFormDialog.tsx +400 -0
  232. package/frontend/src/components/tasks/TaskKanbanBoard.tsx +180 -0
  233. package/frontend/src/components/tasks/Toolbar/CreateAttempt.tsx +259 -0
  234. package/frontend/src/components/tasks/Toolbar/CreatePRDialog.tsx +243 -0
  235. package/frontend/src/components/tasks/Toolbar/CurrentAttempt.tsx +899 -0
  236. package/frontend/src/components/tasks/index.ts +2 -0
  237. package/frontend/src/components/theme-provider.tsx +82 -0
  238. package/frontend/src/components/theme-toggle.tsx +36 -0
  239. package/frontend/src/components/ui/alert.tsx +59 -0
  240. package/frontend/src/components/ui/auto-expanding-textarea.tsx +70 -0
  241. package/frontend/src/components/ui/badge.tsx +36 -0
  242. package/frontend/src/components/ui/button.tsx +56 -0
  243. package/frontend/src/components/ui/card.tsx +86 -0
  244. package/frontend/src/components/ui/checkbox.tsx +44 -0
  245. package/frontend/src/components/ui/chip.tsx +25 -0
  246. package/frontend/src/components/ui/dialog.tsx +124 -0
  247. package/frontend/src/components/ui/dropdown-menu.tsx +198 -0
  248. package/frontend/src/components/ui/file-search-textarea.tsx +292 -0
  249. package/frontend/src/components/ui/folder-picker.tsx +279 -0
  250. package/frontend/src/components/ui/input.tsx +25 -0
  251. package/frontend/src/components/ui/label.tsx +24 -0
  252. package/frontend/src/components/ui/loader.tsx +26 -0
  253. package/frontend/src/components/ui/markdown-renderer.tsx +75 -0
  254. package/frontend/src/components/ui/select.tsx +160 -0
  255. package/frontend/src/components/ui/separator.tsx +31 -0
  256. package/frontend/src/components/ui/shadcn-io/kanban/index.tsx +185 -0
  257. package/frontend/src/components/ui/table.tsx +117 -0
  258. package/frontend/src/components/ui/tabs.tsx +53 -0
  259. package/frontend/src/components/ui/textarea.tsx +22 -0
  260. package/frontend/src/components/ui/tooltip.tsx +28 -0
  261. package/frontend/src/hooks/useNormalizedConversation.ts +440 -0
  262. package/frontend/src/index.css +225 -0
  263. package/frontend/src/lib/api.ts +630 -0
  264. package/frontend/src/lib/keyboard-shortcuts.ts +266 -0
  265. package/frontend/src/lib/responsive-config.ts +70 -0
  266. package/frontend/src/lib/types.ts +39 -0
  267. package/frontend/src/lib/utils.ts +10 -0
  268. package/frontend/src/main.tsx +50 -0
  269. package/frontend/src/pages/McpServers.tsx +418 -0
  270. package/frontend/src/pages/Settings.tsx +610 -0
  271. package/frontend/src/pages/project-tasks.tsx +575 -0
  272. package/frontend/src/pages/projects.tsx +18 -0
  273. package/frontend/src/vite-env.d.ts +1 -0
  274. package/frontend/tailwind.config.js +125 -0
  275. package/frontend/tsconfig.json +26 -0
  276. package/frontend/tsconfig.node.json +10 -0
  277. package/frontend/vite.config.ts +33 -0
  278. package/npx-cli/README.md +159 -0
  279. package/npx-cli/automagik-forge-0.0.55.tgz +0 -0
  280. package/npx-cli/automagik-forge-0.1.0.tgz +0 -0
  281. package/{dist/linux-x64/automagik-forge.zip → npx-cli/automagik-forge-0.1.10.tgz} +0 -0
  282. package/npx-cli/package.json +17 -0
  283. package/npx-cli/vibe-kanban-0.0.55.tgz +0 -0
  284. package/package.json +23 -13
  285. package/pnpm-workspace.yaml +2 -0
  286. package/rust-toolchain.toml +11 -0
  287. package/rustfmt.toml +3 -0
  288. package/scripts/load-env.js +43 -0
  289. package/scripts/mcp_test.js +374 -0
  290. package/scripts/prepare-db.js +45 -0
  291. package/scripts/setup-dev-environment.js +274 -0
  292. package/scripts/start-mcp-sse.js +70 -0
  293. package/scripts/test-debug.js +32 -0
  294. package/scripts/test-mcp-sse.js +138 -0
  295. package/scripts/test-simple.js +44 -0
  296. package/scripts/test-wish-final.js +179 -0
  297. package/scripts/test-wish-system.js +221 -0
  298. package/shared/types.ts +182 -0
  299. package/test-npm-package.sh +42 -0
  300. package/dist/linux-x64/automagik-forge-mcp.zip +0 -0
  301. /package/{bin → npx-cli/bin}/cli.js +0 -0
@@ -0,0 +1,3 @@
1
+ pub mod model_loaders;
2
+
3
+ pub use model_loaders::*;
@@ -0,0 +1,242 @@
1
+ use axum::{
2
+ extract::{Path, State},
3
+ http::StatusCode,
4
+ middleware::Next,
5
+ response::Response,
6
+ };
7
+ use uuid::Uuid;
8
+
9
+ use crate::{
10
+ app_state::AppState,
11
+ models::{
12
+ execution_process::ExecutionProcess, project::Project, task::Task,
13
+ task_attempt::TaskAttempt, task_template::TaskTemplate,
14
+ },
15
+ };
16
+
17
+ /// Middleware that loads and injects a Project based on the project_id path parameter
18
+ pub async fn load_project_middleware(
19
+ State(app_state): State<AppState>,
20
+ Path(project_id): Path<Uuid>,
21
+ request: axum::extract::Request,
22
+ next: Next,
23
+ ) -> Result<Response, StatusCode> {
24
+ // Load the project from the database
25
+ let project = match Project::find_by_id(&app_state.db_pool, project_id).await {
26
+ Ok(Some(project)) => project,
27
+ Ok(None) => {
28
+ tracing::warn!("Project {} not found", project_id);
29
+ return Err(StatusCode::NOT_FOUND);
30
+ }
31
+ Err(e) => {
32
+ tracing::error!("Failed to fetch project {}: {}", project_id, e);
33
+ return Err(StatusCode::INTERNAL_SERVER_ERROR);
34
+ }
35
+ };
36
+
37
+ // Insert the project as an extension
38
+ let mut request = request;
39
+ request.extensions_mut().insert(project);
40
+
41
+ // Continue with the next middleware/handler
42
+ Ok(next.run(request).await)
43
+ }
44
+
45
+ /// Middleware that loads and injects both Project and Task based on project_id and task_id path parameters
46
+ pub async fn load_task_middleware(
47
+ State(app_state): State<AppState>,
48
+ Path((project_id, task_id)): Path<(Uuid, Uuid)>,
49
+ request: axum::extract::Request,
50
+ next: Next,
51
+ ) -> Result<Response, StatusCode> {
52
+ // Load the project first
53
+ let project = match Project::find_by_id(&app_state.db_pool, project_id).await {
54
+ Ok(Some(project)) => project,
55
+ Ok(None) => {
56
+ tracing::warn!("Project {} not found", project_id);
57
+ return Err(StatusCode::NOT_FOUND);
58
+ }
59
+ Err(e) => {
60
+ tracing::error!("Failed to fetch project {}: {}", project_id, e);
61
+ return Err(StatusCode::INTERNAL_SERVER_ERROR);
62
+ }
63
+ };
64
+
65
+ // Load the task and validate it belongs to the project
66
+ let task = match Task::find_by_id_and_project_id(&app_state.db_pool, task_id, project_id).await
67
+ {
68
+ Ok(Some(task)) => task,
69
+ Ok(None) => {
70
+ tracing::warn!("Task {} not found in project {}", task_id, project_id);
71
+ return Err(StatusCode::NOT_FOUND);
72
+ }
73
+ Err(e) => {
74
+ tracing::error!(
75
+ "Failed to fetch task {} in project {}: {}",
76
+ task_id,
77
+ project_id,
78
+ e
79
+ );
80
+ return Err(StatusCode::INTERNAL_SERVER_ERROR);
81
+ }
82
+ };
83
+
84
+ // Insert both models as extensions
85
+ let mut request = request;
86
+ request.extensions_mut().insert(project);
87
+ request.extensions_mut().insert(task);
88
+
89
+ // Continue with the next middleware/handler
90
+ Ok(next.run(request).await)
91
+ }
92
+
93
+ /// Middleware that loads and injects Project, Task, and TaskAttempt based on project_id, task_id, and attempt_id path parameters
94
+ pub async fn load_task_attempt_middleware(
95
+ State(app_state): State<AppState>,
96
+ Path((project_id, task_id, attempt_id)): Path<(Uuid, Uuid, Uuid)>,
97
+ request: axum::extract::Request,
98
+ next: Next,
99
+ ) -> Result<Response, StatusCode> {
100
+ // Load the full context in one call using the existing method
101
+ let context = match TaskAttempt::load_context(
102
+ &app_state.db_pool,
103
+ attempt_id,
104
+ task_id,
105
+ project_id,
106
+ )
107
+ .await
108
+ {
109
+ Ok(context) => context,
110
+ Err(e) => {
111
+ tracing::error!(
112
+ "Failed to load context for attempt {} in task {} in project {}: {}",
113
+ attempt_id,
114
+ task_id,
115
+ project_id,
116
+ e
117
+ );
118
+ return Err(StatusCode::NOT_FOUND);
119
+ }
120
+ };
121
+
122
+ // Insert all models as extensions
123
+ let mut request = request;
124
+ request.extensions_mut().insert(context.project);
125
+ request.extensions_mut().insert(context.task);
126
+ request.extensions_mut().insert(context.task_attempt);
127
+
128
+ // Continue with the next middleware/handler
129
+ Ok(next.run(request).await)
130
+ }
131
+
132
+ /// Simple middleware that loads and injects ExecutionProcess based on the process_id path parameter
133
+ /// without any additional validation
134
+ pub async fn load_execution_process_simple_middleware(
135
+ State(app_state): State<AppState>,
136
+ Path(process_id): Path<Uuid>,
137
+ mut request: axum::extract::Request,
138
+ next: Next,
139
+ ) -> Result<Response, StatusCode> {
140
+ // Load the execution process from the database
141
+ let execution_process = match ExecutionProcess::find_by_id(&app_state.db_pool, process_id).await
142
+ {
143
+ Ok(Some(process)) => process,
144
+ Ok(None) => {
145
+ tracing::warn!("ExecutionProcess {} not found", process_id);
146
+ return Err(StatusCode::NOT_FOUND);
147
+ }
148
+ Err(e) => {
149
+ tracing::error!("Failed to fetch execution process {}: {}", process_id, e);
150
+ return Err(StatusCode::INTERNAL_SERVER_ERROR);
151
+ }
152
+ };
153
+
154
+ // Inject the execution process into the request
155
+ request.extensions_mut().insert(execution_process);
156
+
157
+ // Continue to the next middleware/handler
158
+ Ok(next.run(request).await)
159
+ }
160
+
161
+ /// Middleware that loads and injects Project, Task, TaskAttempt, and ExecutionProcess
162
+ /// based on the path parameters: project_id, task_id, attempt_id, process_id
163
+ pub async fn load_execution_process_with_context_middleware(
164
+ State(app_state): State<AppState>,
165
+ Path((project_id, task_id, attempt_id, process_id)): Path<(Uuid, Uuid, Uuid, Uuid)>,
166
+ request: axum::extract::Request,
167
+ next: Next,
168
+ ) -> Result<Response, StatusCode> {
169
+ // Load the task attempt context first
170
+ let context = match TaskAttempt::load_context(
171
+ &app_state.db_pool,
172
+ attempt_id,
173
+ task_id,
174
+ project_id,
175
+ )
176
+ .await
177
+ {
178
+ Ok(context) => context,
179
+ Err(e) => {
180
+ tracing::error!(
181
+ "Failed to load context for attempt {} in task {} in project {}: {}",
182
+ attempt_id,
183
+ task_id,
184
+ project_id,
185
+ e
186
+ );
187
+ return Err(StatusCode::NOT_FOUND);
188
+ }
189
+ };
190
+
191
+ // Load the execution process
192
+ let execution_process = match ExecutionProcess::find_by_id(&app_state.db_pool, process_id).await
193
+ {
194
+ Ok(Some(process)) => process,
195
+ Ok(None) => {
196
+ tracing::warn!("ExecutionProcess {} not found", process_id);
197
+ return Err(StatusCode::NOT_FOUND);
198
+ }
199
+ Err(e) => {
200
+ tracing::error!("Failed to fetch execution process {}: {}", process_id, e);
201
+ return Err(StatusCode::INTERNAL_SERVER_ERROR);
202
+ }
203
+ };
204
+
205
+ // Insert all models as extensions
206
+ let mut request = request;
207
+ request.extensions_mut().insert(context.project);
208
+ request.extensions_mut().insert(context.task);
209
+ request.extensions_mut().insert(context.task_attempt);
210
+ request.extensions_mut().insert(execution_process);
211
+
212
+ // Continue with the next middleware/handler
213
+ Ok(next.run(request).await)
214
+ }
215
+
216
+ /// Middleware that loads and injects TaskTemplate based on the template_id path parameter
217
+ pub async fn load_task_template_middleware(
218
+ State(app_state): State<AppState>,
219
+ Path(template_id): Path<Uuid>,
220
+ request: axum::extract::Request,
221
+ next: Next,
222
+ ) -> Result<Response, StatusCode> {
223
+ // Load the task template from the database
224
+ let task_template = match TaskTemplate::find_by_id(&app_state.db_pool, template_id).await {
225
+ Ok(Some(template)) => template,
226
+ Ok(None) => {
227
+ tracing::warn!("TaskTemplate {} not found", template_id);
228
+ return Err(StatusCode::NOT_FOUND);
229
+ }
230
+ Err(e) => {
231
+ tracing::error!("Failed to fetch task template {}: {}", template_id, e);
232
+ return Err(StatusCode::INTERNAL_SERVER_ERROR);
233
+ }
234
+ };
235
+
236
+ // Insert the task template as an extension
237
+ let mut request = request;
238
+ request.extensions_mut().insert(task_template);
239
+
240
+ // Continue with the next middleware/handler
241
+ Ok(next.run(request).await)
242
+ }
@@ -0,0 +1,36 @@
1
+ mod response {
2
+ use serde::Serialize;
3
+ use ts_rs::TS;
4
+ use utoipa::ToSchema;
5
+
6
+ #[derive(Debug, Serialize, TS, ToSchema)]
7
+ #[ts(export)]
8
+ pub struct ApiResponse<T> {
9
+ success: bool,
10
+ data: Option<T>,
11
+ message: Option<String>,
12
+ }
13
+
14
+ impl<T> ApiResponse<T> {
15
+ /// Creates a successful response, with `data` and no message.
16
+ pub fn success(data: T) -> Self {
17
+ ApiResponse {
18
+ success: true,
19
+ data: Some(data),
20
+ message: None,
21
+ }
22
+ }
23
+
24
+ /// Creates an error response, with `message` and no data.
25
+ pub fn error(message: &str) -> Self {
26
+ ApiResponse {
27
+ success: false,
28
+ data: None,
29
+ message: Some(message.to_string()),
30
+ }
31
+ }
32
+ }
33
+ }
34
+
35
+ // Re-export the type, but its fields remain private
36
+ pub use response::ApiResponse;
@@ -0,0 +1,375 @@
1
+ use std::path::PathBuf;
2
+
3
+ use serde::{Deserialize, Serialize};
4
+ use ts_rs::TS;
5
+ use utoipa::ToSchema;
6
+
7
+ use crate::executor::ExecutorConfig;
8
+
9
+ #[derive(Debug, Clone, Serialize, Deserialize, TS, ToSchema)]
10
+ #[ts(export)]
11
+ pub struct Config {
12
+ pub theme: ThemeMode,
13
+ pub executor: ExecutorConfig,
14
+ pub disclaimer_acknowledged: bool,
15
+ pub onboarding_acknowledged: bool,
16
+ pub github_login_acknowledged: bool,
17
+ pub telemetry_acknowledged: bool,
18
+ pub sound_alerts: bool,
19
+ pub sound_file: SoundFile,
20
+ pub push_notifications: bool,
21
+ pub editor: EditorConfig,
22
+ pub github: GitHubConfig,
23
+ pub analytics_enabled: Option<bool>,
24
+ }
25
+
26
+ #[derive(Debug, Clone, Serialize, Deserialize, TS, ToSchema)]
27
+ #[ts(export)]
28
+ #[serde(rename_all = "lowercase")]
29
+ pub enum ThemeMode {
30
+ Light,
31
+ Dark,
32
+ System,
33
+ Purple,
34
+ Green,
35
+ Blue,
36
+ Orange,
37
+ Red,
38
+ }
39
+
40
+ #[derive(Debug, Clone, Serialize, Deserialize, TS, ToSchema)]
41
+ #[ts(export)]
42
+ pub struct EditorConfig {
43
+ pub editor_type: EditorType,
44
+ pub custom_command: Option<String>,
45
+ }
46
+
47
+ #[derive(Debug, Clone, Serialize, Deserialize, TS, ToSchema)]
48
+ #[ts(export)]
49
+ pub struct GitHubConfig {
50
+ pub pat: Option<String>,
51
+ pub token: Option<String>,
52
+ pub username: Option<String>,
53
+ pub primary_email: Option<String>,
54
+ pub default_pr_base: Option<String>,
55
+ }
56
+
57
+ #[derive(Debug, Clone, Serialize, Deserialize, TS, ToSchema)]
58
+ #[ts(export)]
59
+ #[serde(rename_all = "lowercase")]
60
+ pub enum EditorType {
61
+ VSCode,
62
+ Cursor,
63
+ Windsurf,
64
+ IntelliJ,
65
+ Zed,
66
+ Custom,
67
+ }
68
+
69
+ #[derive(Debug, Clone, Serialize, Deserialize, TS, ToSchema)]
70
+ #[ts(export)]
71
+ #[serde(rename_all = "kebab-case")]
72
+ pub enum SoundFile {
73
+ AbstractSound1,
74
+ AbstractSound2,
75
+ AbstractSound3,
76
+ AbstractSound4,
77
+ CowMooing,
78
+ PhoneVibration,
79
+ Rooster,
80
+ }
81
+
82
+ // Constants for frontend
83
+ #[derive(Debug, Clone, Serialize, Deserialize, TS, ToSchema)]
84
+ #[ts(export)]
85
+ pub struct EditorConstants {
86
+ pub editor_types: Vec<EditorType>,
87
+ pub editor_labels: Vec<String>,
88
+ }
89
+
90
+ #[derive(Debug, Clone, Serialize, Deserialize, TS, ToSchema)]
91
+ #[ts(export)]
92
+ pub struct SoundConstants {
93
+ pub sound_files: Vec<SoundFile>,
94
+ pub sound_labels: Vec<String>,
95
+ }
96
+
97
+ impl EditorConstants {
98
+ pub fn new() -> Self {
99
+ Self {
100
+ editor_types: vec![
101
+ EditorType::VSCode,
102
+ EditorType::Cursor,
103
+ EditorType::Windsurf,
104
+ EditorType::IntelliJ,
105
+ EditorType::Zed,
106
+ EditorType::Custom,
107
+ ],
108
+ editor_labels: vec![
109
+ "VS Code".to_string(),
110
+ "Cursor".to_string(),
111
+ "Windsurf".to_string(),
112
+ "IntelliJ IDEA".to_string(),
113
+ "Zed".to_string(),
114
+ "Custom".to_string(),
115
+ ],
116
+ }
117
+ }
118
+ }
119
+
120
+ impl Default for EditorConstants {
121
+ fn default() -> Self {
122
+ Self::new()
123
+ }
124
+ }
125
+
126
+ impl SoundConstants {
127
+ pub fn new() -> Self {
128
+ Self {
129
+ sound_files: vec![
130
+ SoundFile::AbstractSound1,
131
+ SoundFile::AbstractSound2,
132
+ SoundFile::AbstractSound3,
133
+ SoundFile::AbstractSound4,
134
+ SoundFile::CowMooing,
135
+ SoundFile::PhoneVibration,
136
+ SoundFile::Rooster,
137
+ ],
138
+ sound_labels: vec![
139
+ "Gentle Chime".to_string(),
140
+ "Soft Bell".to_string(),
141
+ "Digital Tone".to_string(),
142
+ "Subtle Alert".to_string(),
143
+ "Cow Mooing".to_string(),
144
+ "Phone Vibration".to_string(),
145
+ "Rooster Call".to_string(),
146
+ ],
147
+ }
148
+ }
149
+ }
150
+
151
+ impl Default for SoundConstants {
152
+ fn default() -> Self {
153
+ Self::new()
154
+ }
155
+ }
156
+
157
+ impl Default for Config {
158
+ fn default() -> Self {
159
+ Self {
160
+ theme: ThemeMode::System,
161
+ executor: ExecutorConfig::Claude,
162
+ disclaimer_acknowledged: false,
163
+ onboarding_acknowledged: false,
164
+ github_login_acknowledged: false,
165
+ telemetry_acknowledged: false,
166
+ sound_alerts: true,
167
+ sound_file: SoundFile::AbstractSound4,
168
+ push_notifications: true,
169
+ editor: EditorConfig::default(),
170
+ github: GitHubConfig::default(),
171
+ analytics_enabled: None,
172
+ }
173
+ }
174
+ }
175
+
176
+ impl Default for EditorConfig {
177
+ fn default() -> Self {
178
+ Self {
179
+ editor_type: EditorType::VSCode,
180
+ custom_command: None,
181
+ }
182
+ }
183
+ }
184
+
185
+ impl Default for GitHubConfig {
186
+ fn default() -> Self {
187
+ Self {
188
+ pat: None,
189
+ token: None,
190
+ username: None,
191
+ primary_email: None,
192
+ default_pr_base: Some("main".to_string()),
193
+ }
194
+ }
195
+ }
196
+
197
+ impl EditorConfig {
198
+ pub fn get_command(&self) -> Vec<String> {
199
+ match &self.editor_type {
200
+ EditorType::VSCode => vec!["code".to_string()],
201
+ EditorType::Cursor => vec!["cursor".to_string()],
202
+ EditorType::Windsurf => vec!["windsurf".to_string()],
203
+ EditorType::IntelliJ => vec!["idea".to_string()],
204
+ EditorType::Zed => vec!["zed".to_string()],
205
+ EditorType::Custom => {
206
+ if let Some(custom) = &self.custom_command {
207
+ custom.split_whitespace().map(|s| s.to_string()).collect()
208
+ } else {
209
+ vec!["code".to_string()] // fallback to VSCode
210
+ }
211
+ }
212
+ }
213
+ }
214
+ }
215
+
216
+ impl SoundFile {
217
+ pub fn to_filename(&self) -> &'static str {
218
+ match self {
219
+ SoundFile::AbstractSound1 => "abstract-sound1.wav",
220
+ SoundFile::AbstractSound2 => "abstract-sound2.wav",
221
+ SoundFile::AbstractSound3 => "abstract-sound3.wav",
222
+ SoundFile::AbstractSound4 => "abstract-sound4.wav",
223
+ SoundFile::CowMooing => "cow-mooing.wav",
224
+ SoundFile::PhoneVibration => "phone-vibration.wav",
225
+ SoundFile::Rooster => "rooster.wav",
226
+ }
227
+ }
228
+
229
+ /// Get or create a cached sound file with the embedded sound data
230
+ pub async fn get_path(&self) -> Result<PathBuf, Box<dyn std::error::Error + Send + Sync>> {
231
+ use std::io::Write;
232
+
233
+ let filename = self.to_filename();
234
+ let cache_dir = crate::utils::cache_dir();
235
+ let cached_path = cache_dir.join(format!("sound-{}", filename));
236
+
237
+ // Check if cached file already exists and is valid
238
+ if cached_path.exists() {
239
+ // Verify file has content (basic validation)
240
+ if let Ok(metadata) = std::fs::metadata(&cached_path) {
241
+ if metadata.len() > 0 {
242
+ return Ok(cached_path);
243
+ }
244
+ }
245
+ }
246
+
247
+ // File doesn't exist or is invalid, create it
248
+ let sound_data = crate::SoundAssets::get(filename)
249
+ .ok_or_else(|| format!("Embedded sound file not found: {}", filename))?
250
+ .data;
251
+
252
+ // Ensure cache directory exists
253
+ std::fs::create_dir_all(&cache_dir)
254
+ .map_err(|e| format!("Failed to create cache directory: {}", e))?;
255
+
256
+ let mut file = std::fs::File::create(&cached_path)
257
+ .map_err(|e| format!("Failed to create cached sound file: {}", e))?;
258
+
259
+ file.write_all(&sound_data)
260
+ .map_err(|e| format!("Failed to write sound data to cached file: {}", e))?;
261
+
262
+ drop(file); // Ensure file is closed
263
+
264
+ Ok(cached_path)
265
+ }
266
+ }
267
+
268
+ impl Config {
269
+ pub fn load(config_path: &PathBuf) -> anyhow::Result<Self> {
270
+ if config_path.exists() {
271
+ let content = std::fs::read_to_string(config_path)?;
272
+
273
+ // Try to deserialize as is first
274
+ match serde_json::from_str::<Config>(&content) {
275
+ Ok(mut config) => {
276
+ if config.analytics_enabled.is_none() {
277
+ config.analytics_enabled = Some(true);
278
+ }
279
+
280
+ // Always save back to ensure new fields are written to disk
281
+ config.save(config_path)?;
282
+ Ok(config)
283
+ }
284
+ Err(_) => {
285
+ // If full deserialization fails, try to merge with defaults
286
+ match Self::load_with_defaults(&content, config_path) {
287
+ Ok(config) => Ok(config),
288
+ Err(_) => {
289
+ // Even partial loading failed - backup the corrupted file
290
+ if let Err(e) = Self::backup_corrupted_config(config_path) {
291
+ tracing::error!("Failed to backup corrupted config: {}", e);
292
+ }
293
+
294
+ // Remove corrupted file and create a default config
295
+ if let Err(e) = std::fs::remove_file(config_path) {
296
+ tracing::error!("Failed to remove corrupted config file: {}", e);
297
+ }
298
+
299
+ // Create and save default config
300
+ let config = Config::default();
301
+ config.save(config_path)?;
302
+ Ok(config)
303
+ }
304
+ }
305
+ }
306
+ }
307
+ } else {
308
+ let config = Config::default();
309
+ config.save(config_path)?;
310
+ Ok(config)
311
+ }
312
+ }
313
+
314
+ fn load_with_defaults(content: &str, config_path: &PathBuf) -> anyhow::Result<Self> {
315
+ // Parse as generic JSON value
316
+ let existing_value: serde_json::Value = serde_json::from_str(content)?;
317
+
318
+ // Get default config as JSON value
319
+ let default_config = Config::default();
320
+ let default_value = serde_json::to_value(&default_config)?;
321
+
322
+ // Merge existing config with defaults
323
+ let merged_value = Self::merge_json_values(default_value, existing_value);
324
+
325
+ // Deserialize merged value back to Config
326
+ let config: Config = serde_json::from_value(merged_value)?;
327
+
328
+ // Save the updated config with any missing defaults
329
+ config.save(config_path)?;
330
+
331
+ Ok(config)
332
+ }
333
+
334
+ fn merge_json_values(
335
+ mut base: serde_json::Value,
336
+ overlay: serde_json::Value,
337
+ ) -> serde_json::Value {
338
+ match (&mut base, overlay) {
339
+ (serde_json::Value::Object(base_map), serde_json::Value::Object(overlay_map)) => {
340
+ for (key, value) in overlay_map {
341
+ base_map
342
+ .entry(key)
343
+ .and_modify(|base_value| {
344
+ *base_value =
345
+ Self::merge_json_values(base_value.clone(), value.clone());
346
+ })
347
+ .or_insert(value);
348
+ }
349
+ base
350
+ }
351
+ (_, overlay) => overlay, // Use overlay value for non-objects
352
+ }
353
+ }
354
+
355
+ /// Create a backup of the corrupted config file
356
+ fn backup_corrupted_config(config_path: &PathBuf) -> anyhow::Result<()> {
357
+ let timestamp = chrono::Utc::now().format("%Y%m%d_%H%M%S");
358
+ let backup_filename = format!("config_backup_{}.json", timestamp);
359
+
360
+ let backup_path = config_path
361
+ .parent()
362
+ .unwrap_or_else(|| std::path::Path::new("."))
363
+ .join(backup_filename);
364
+
365
+ std::fs::copy(config_path, &backup_path)?;
366
+ tracing::info!("Corrupted config backed up to: {}", backup_path.display());
367
+ Ok(())
368
+ }
369
+
370
+ pub fn save(&self, config_path: &PathBuf) -> anyhow::Result<()> {
371
+ let content = serde_json::to_string_pretty(self)?;
372
+ std::fs::write(config_path, content)?;
373
+ Ok(())
374
+ }
375
+ }