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,697 @@
1
+ use std::path::Path;
2
+
3
+ use async_trait::async_trait;
4
+ use command_group::{AsyncCommandGroup, AsyncGroupChild};
5
+ use serde::{Deserialize, Serialize};
6
+ use uuid::Uuid;
7
+
8
+ use crate::{
9
+ executor::{
10
+ ActionType, Executor, ExecutorError, NormalizedConversation, NormalizedEntry,
11
+ NormalizedEntryType,
12
+ },
13
+ models::task::Task,
14
+ utils::shell::get_shell_command,
15
+ };
16
+
17
+ /// An executor that uses Amp to process tasks
18
+ pub struct AmpExecutor;
19
+
20
+ #[derive(Deserialize, Serialize, Debug, Clone, PartialEq, Eq)]
21
+ #[serde(tag = "type")]
22
+ pub enum AmpJson {
23
+ #[serde(rename = "messages")]
24
+ Messages {
25
+ messages: Vec<(usize, AmpMessage)>,
26
+ #[serde(rename = "toolResults")]
27
+ tool_results: Vec<serde_json::Value>,
28
+ },
29
+ #[serde(rename = "initial")]
30
+ Initial {
31
+ #[serde(rename = "threadID")]
32
+ thread_id: Option<String>,
33
+ },
34
+ #[serde(rename = "token-usage")]
35
+ TokenUsage(serde_json::Value),
36
+ #[serde(rename = "state")]
37
+ State { state: String },
38
+ #[serde(rename = "shutdown")]
39
+ Shutdown,
40
+ #[serde(rename = "tool-status")]
41
+ ToolStatus(serde_json::Value),
42
+ }
43
+
44
+ #[derive(Deserialize, Serialize, Debug, Clone, PartialEq, Eq)]
45
+ pub struct AmpMessage {
46
+ pub role: String,
47
+ pub content: Vec<AmpContentItem>,
48
+ pub state: Option<serde_json::Value>,
49
+ pub meta: Option<AmpMeta>,
50
+ }
51
+
52
+ #[derive(Deserialize, Serialize, Debug, Clone, PartialEq, Eq)]
53
+ pub struct AmpMeta {
54
+ #[serde(rename = "sentAt")]
55
+ pub sent_at: u64,
56
+ }
57
+
58
+ #[derive(Deserialize, Serialize, Debug, Clone, PartialEq, Eq)]
59
+ #[serde(tag = "type")]
60
+ pub enum AmpContentItem {
61
+ #[serde(rename = "text")]
62
+ Text { text: String },
63
+ #[serde(rename = "thinking")]
64
+ Thinking { thinking: String },
65
+ #[serde(rename = "tool_use")]
66
+ ToolUse {
67
+ id: String,
68
+ name: String,
69
+ input: serde_json::Value,
70
+ },
71
+ #[serde(rename = "tool_result")]
72
+ ToolResult {
73
+ #[serde(rename = "toolUseID")]
74
+ tool_use_id: String,
75
+ run: serde_json::Value,
76
+ },
77
+ }
78
+
79
+ impl AmpJson {
80
+ pub fn should_process(&self) -> bool {
81
+ matches!(self, AmpJson::Messages { .. })
82
+ }
83
+
84
+ pub fn extract_session_id(&self) -> Option<String> {
85
+ match self {
86
+ AmpJson::Initial { thread_id } => thread_id.clone(),
87
+ _ => None,
88
+ }
89
+ }
90
+
91
+ pub fn has_streaming_content(&self) -> bool {
92
+ match self {
93
+ AmpJson::Messages { messages, .. } => messages.iter().any(|(_index, message)| {
94
+ if let Some(state) = &message.state {
95
+ if let Some(state_type) = state.get("type").and_then(|t| t.as_str()) {
96
+ state_type == "streaming"
97
+ } else {
98
+ false
99
+ }
100
+ } else {
101
+ false
102
+ }
103
+ }),
104
+ _ => false,
105
+ }
106
+ }
107
+
108
+ pub fn to_normalized_entries(
109
+ &self,
110
+ executor: &AmpExecutor,
111
+ worktree_path: &str,
112
+ ) -> Vec<NormalizedEntry> {
113
+ match self {
114
+ AmpJson::Messages { messages, .. } => {
115
+ if self.has_streaming_content() {
116
+ return vec![];
117
+ }
118
+
119
+ let mut entries = Vec::new();
120
+ for (_index, message) in messages {
121
+ let role = &message.role;
122
+ for content_item in &message.content {
123
+ if let Some(entry) =
124
+ content_item.to_normalized_entry(role, message, executor, worktree_path)
125
+ {
126
+ entries.push(entry);
127
+ }
128
+ }
129
+ }
130
+ entries
131
+ }
132
+ _ => vec![],
133
+ }
134
+ }
135
+ }
136
+
137
+ impl AmpContentItem {
138
+ pub fn to_normalized_entry(
139
+ &self,
140
+ role: &str,
141
+ message: &AmpMessage,
142
+ executor: &AmpExecutor,
143
+ worktree_path: &str,
144
+ ) -> Option<NormalizedEntry> {
145
+ use serde_json::Value;
146
+
147
+ let timestamp = message.meta.as_ref().map(|meta| meta.sent_at.to_string());
148
+
149
+ match self {
150
+ AmpContentItem::Text { text } => {
151
+ let entry_type = match role {
152
+ "user" => NormalizedEntryType::UserMessage,
153
+ "assistant" => NormalizedEntryType::AssistantMessage,
154
+ _ => return None,
155
+ };
156
+ Some(NormalizedEntry {
157
+ timestamp,
158
+ entry_type,
159
+ content: text.clone(),
160
+ metadata: Some(serde_json::to_value(self).unwrap_or(Value::Null)),
161
+ })
162
+ }
163
+ AmpContentItem::Thinking { thinking } => Some(NormalizedEntry {
164
+ timestamp,
165
+ entry_type: NormalizedEntryType::Thinking,
166
+ content: thinking.clone(),
167
+ metadata: Some(serde_json::to_value(self).unwrap_or(Value::Null)),
168
+ }),
169
+ AmpContentItem::ToolUse { name, input, .. } => {
170
+ let action_type = executor.extract_action_type(name, input, worktree_path);
171
+ let content =
172
+ executor.generate_concise_content(name, input, &action_type, worktree_path);
173
+
174
+ Some(NormalizedEntry {
175
+ timestamp,
176
+ entry_type: NormalizedEntryType::ToolUse {
177
+ tool_name: name.clone(),
178
+ action_type,
179
+ },
180
+ content,
181
+ metadata: Some(serde_json::to_value(self).unwrap_or(Value::Null)),
182
+ })
183
+ }
184
+ AmpContentItem::ToolResult { .. } => None,
185
+ }
186
+ }
187
+ }
188
+
189
+ #[async_trait]
190
+ impl Executor for AmpExecutor {
191
+ async fn spawn(
192
+ &self,
193
+ pool: &sqlx::SqlitePool,
194
+ task_id: Uuid,
195
+ worktree_path: &str,
196
+ ) -> Result<AsyncGroupChild, ExecutorError> {
197
+ // Get the task to fetch its description
198
+ let task = Task::find_by_id(pool, task_id)
199
+ .await?
200
+ .ok_or(ExecutorError::TaskNotFound)?;
201
+
202
+ use std::process::Stdio;
203
+
204
+ use tokio::{io::AsyncWriteExt, process::Command};
205
+
206
+ let prompt = if let Some(task_description) = task.description {
207
+ format!(
208
+ r#"project_id: {}
209
+
210
+ Task title: {}
211
+ Task description: {}"#,
212
+ task.project_id, task.title, task_description
213
+ )
214
+ } else {
215
+ format!(
216
+ r#"project_id: {}
217
+
218
+ Task title: {}"#,
219
+ task.project_id, task.title
220
+ )
221
+ };
222
+
223
+ // Use shell command for cross-platform compatibility
224
+ let (shell_cmd, shell_arg) = get_shell_command();
225
+ // --format=jsonl is deprecated in latest versions of Amp CLI
226
+ let amp_command = "npx @sourcegraph/amp@0.0.1752148945-gd8844f --format=jsonl";
227
+
228
+ let mut command = Command::new(shell_cmd);
229
+ command
230
+ .kill_on_drop(true)
231
+ .stdin(Stdio::piped()) // <-- open a pipe
232
+ .stdout(Stdio::piped())
233
+ .stderr(Stdio::piped())
234
+ .current_dir(worktree_path)
235
+ .arg(shell_arg)
236
+ .arg(amp_command);
237
+
238
+ let mut child = command
239
+ .group_spawn() // Create new process group so we can kill entire tree
240
+ .map_err(|e| {
241
+ crate::executor::SpawnContext::from_command(&command, "Amp")
242
+ .with_task(task_id, Some(task.title.clone()))
243
+ .with_context("Amp CLI execution for new task")
244
+ .spawn_error(e)
245
+ })?;
246
+
247
+ // feed the prompt in, then close the pipe so `amp` sees EOF
248
+ if let Some(mut stdin) = child.inner().stdin.take() {
249
+ stdin.write_all(prompt.as_bytes()).await.unwrap();
250
+ stdin.shutdown().await.unwrap(); // or `drop(stdin);`
251
+ }
252
+
253
+ Ok(child)
254
+ }
255
+
256
+ async fn spawn_followup(
257
+ &self,
258
+ _pool: &sqlx::SqlitePool,
259
+ _task_id: Uuid,
260
+ session_id: &str,
261
+ prompt: &str,
262
+ worktree_path: &str,
263
+ ) -> Result<AsyncGroupChild, ExecutorError> {
264
+ use std::process::Stdio;
265
+
266
+ use tokio::{io::AsyncWriteExt, process::Command};
267
+
268
+ // Use shell command for cross-platform compatibility
269
+ let (shell_cmd, shell_arg) = get_shell_command();
270
+ let amp_command = format!(
271
+ "npx @sourcegraph/amp@0.0.1752148945-gd8844f threads continue {} --format=jsonl",
272
+ session_id
273
+ );
274
+
275
+ let mut command = Command::new(shell_cmd);
276
+ command
277
+ .kill_on_drop(true)
278
+ .stdin(Stdio::piped())
279
+ .stdout(Stdio::piped())
280
+ .stderr(Stdio::piped())
281
+ .current_dir(worktree_path)
282
+ .arg(shell_arg)
283
+ .arg(&amp_command);
284
+
285
+ let mut child = command.group_spawn().map_err(|e| {
286
+ crate::executor::SpawnContext::from_command(&command, "Amp")
287
+ .with_context(format!(
288
+ "Amp CLI followup execution for thread {}",
289
+ session_id
290
+ ))
291
+ .spawn_error(e)
292
+ })?;
293
+
294
+ // Feed the prompt in, then close the pipe so amp sees EOF
295
+ if let Some(mut stdin) = child.inner().stdin.take() {
296
+ stdin.write_all(prompt.as_bytes()).await.map_err(|e| {
297
+ crate::executor::SpawnContext::from_command(&command, "Amp")
298
+ .with_context(format!(
299
+ "Failed to write prompt to Amp CLI stdin for thread {}",
300
+ session_id
301
+ ))
302
+ .spawn_error(e)
303
+ })?;
304
+ stdin.shutdown().await.map_err(|e| {
305
+ crate::executor::SpawnContext::from_command(&command, "Amp")
306
+ .with_context(format!(
307
+ "Failed to close Amp CLI stdin for thread {}",
308
+ session_id
309
+ ))
310
+ .spawn_error(e)
311
+ })?;
312
+ }
313
+
314
+ Ok(child)
315
+ }
316
+
317
+ fn normalize_logs(
318
+ &self,
319
+ logs: &str,
320
+ worktree_path: &str,
321
+ ) -> Result<NormalizedConversation, String> {
322
+ let mut entries = Vec::new();
323
+ let mut session_id = None;
324
+
325
+ for line in logs.lines() {
326
+ let trimmed = line.trim();
327
+ if trimmed.is_empty() {
328
+ continue;
329
+ }
330
+
331
+ // Try to parse as AmpMessage
332
+ let amp_message: AmpJson = match serde_json::from_str(trimmed) {
333
+ Ok(msg) => msg,
334
+ Err(_) => {
335
+ // If line isn't valid JSON, add it as raw text
336
+ entries.push(NormalizedEntry {
337
+ timestamp: None,
338
+ entry_type: NormalizedEntryType::SystemMessage,
339
+ content: format!("Raw output: {}", trimmed),
340
+ metadata: None,
341
+ });
342
+ continue;
343
+ }
344
+ };
345
+
346
+ // Extract session ID if available
347
+ if session_id.is_none() {
348
+ if let Some(id) = amp_message.extract_session_id() {
349
+ session_id = Some(id);
350
+ }
351
+ }
352
+
353
+ // Process the message if it's a type we care about
354
+ if amp_message.should_process() {
355
+ let new_entries = amp_message.to_normalized_entries(self, worktree_path);
356
+ entries.extend(new_entries);
357
+ }
358
+ }
359
+
360
+ Ok(NormalizedConversation {
361
+ entries,
362
+ session_id,
363
+ executor_type: "amp".to_string(),
364
+ prompt: None,
365
+ summary: None,
366
+ })
367
+ }
368
+ }
369
+
370
+ impl AmpExecutor {
371
+ /// Convert absolute paths to relative paths based on worktree path
372
+ fn make_path_relative(&self, path: &str, worktree_path: &str) -> String {
373
+ let path_obj = Path::new(path);
374
+ let worktree_obj = Path::new(worktree_path);
375
+
376
+ // If path is already relative, return as is
377
+ if path_obj.is_relative() {
378
+ return path.to_string();
379
+ }
380
+
381
+ // Try to make path relative to worktree path
382
+ if let Ok(relative_path) = path_obj.strip_prefix(worktree_obj) {
383
+ return relative_path.to_string_lossy().to_string();
384
+ }
385
+
386
+ // If we can't make it relative, return the original path
387
+ path.to_string()
388
+ }
389
+
390
+ fn generate_concise_content(
391
+ &self,
392
+ tool_name: &str,
393
+ input: &serde_json::Value,
394
+ action_type: &ActionType,
395
+ worktree_path: &str,
396
+ ) -> String {
397
+ match action_type {
398
+ ActionType::FileRead { path } => format!("`{}`", path),
399
+ ActionType::FileWrite { path } => format!("`{}`", path),
400
+ ActionType::CommandRun { command } => format!("`{}`", command),
401
+ ActionType::Search { query } => format!("`{}`", query),
402
+ ActionType::WebFetch { url } => format!("`{}`", url),
403
+ ActionType::PlanPresentation { plan } => format!("Plan Presentation: `{}`", plan),
404
+ ActionType::TaskCreate { description } => description.clone(),
405
+ ActionType::Other { description: _ } => {
406
+ // For other tools, try to extract key information or fall back to tool name
407
+ match tool_name.to_lowercase().as_str() {
408
+ "todowrite" | "todoread" | "todo_write" | "todo_read" => {
409
+ if let Some(todos) = input.get("todos").and_then(|t| t.as_array()) {
410
+ let mut todo_items = Vec::new();
411
+ for todo in todos {
412
+ if let (Some(content), Some(status)) = (
413
+ todo.get("content").and_then(|c| c.as_str()),
414
+ todo.get("status").and_then(|s| s.as_str()),
415
+ ) {
416
+ let emoji = match status {
417
+ "completed" => "✅",
418
+ "in_progress" | "in-progress" => "🔄",
419
+ "pending" | "todo" => "⏳",
420
+ _ => "📝",
421
+ };
422
+ let priority = todo
423
+ .get("priority")
424
+ .and_then(|p| p.as_str())
425
+ .unwrap_or("medium");
426
+ todo_items
427
+ .push(format!("{} {} ({})", emoji, content, priority));
428
+ }
429
+ }
430
+ if !todo_items.is_empty() {
431
+ format!("TODO List:\n{}", todo_items.join("\n"))
432
+ } else {
433
+ "Managing TODO list".to_string()
434
+ }
435
+ } else {
436
+ "Managing TODO list".to_string()
437
+ }
438
+ }
439
+ "ls" => {
440
+ if let Some(path) = input.get("path").and_then(|p| p.as_str()) {
441
+ let relative_path = self.make_path_relative(path, worktree_path);
442
+ if relative_path.is_empty() {
443
+ "List directory".to_string()
444
+ } else {
445
+ format!("List directory: `{}`", relative_path)
446
+ }
447
+ } else {
448
+ "List directory".to_string()
449
+ }
450
+ }
451
+ "glob" => {
452
+ let pattern = input.get("pattern").and_then(|p| p.as_str()).unwrap_or("*");
453
+ let path = input.get("path").and_then(|p| p.as_str());
454
+
455
+ if let Some(path) = path {
456
+ let relative_path = self.make_path_relative(path, worktree_path);
457
+ format!("Find files: `{}` in `{}`", pattern, relative_path)
458
+ } else {
459
+ format!("Find files: `{}`", pattern)
460
+ }
461
+ }
462
+ "grep" => {
463
+ let pattern = input.get("pattern").and_then(|p| p.as_str()).unwrap_or("");
464
+ let include = input.get("include").and_then(|i| i.as_str());
465
+ let path = input.get("path").and_then(|p| p.as_str());
466
+
467
+ let mut parts = vec![format!("Search: `{}`", pattern)];
468
+ if let Some(include) = include {
469
+ parts.push(format!("in `{}`", include));
470
+ }
471
+ if let Some(path) = path {
472
+ let relative_path = self.make_path_relative(path, worktree_path);
473
+ parts.push(format!("at `{}`", relative_path));
474
+ }
475
+ parts.join(" ")
476
+ }
477
+ "read" => {
478
+ if let Some(file_path) = input.get("file_path").and_then(|p| p.as_str()) {
479
+ let relative_path = self.make_path_relative(file_path, worktree_path);
480
+ format!("Read file: `{}`", relative_path)
481
+ } else {
482
+ "Read file".to_string()
483
+ }
484
+ }
485
+ "write" => {
486
+ if let Some(file_path) = input.get("file_path").and_then(|p| p.as_str()) {
487
+ let relative_path = self.make_path_relative(file_path, worktree_path);
488
+ format!("Write file: `{}`", relative_path)
489
+ } else {
490
+ "Write file".to_string()
491
+ }
492
+ }
493
+ "edit" => {
494
+ if let Some(file_path) = input.get("file_path").and_then(|p| p.as_str()) {
495
+ let relative_path = self.make_path_relative(file_path, worktree_path);
496
+ format!("Edit file: `{}`", relative_path)
497
+ } else {
498
+ "Edit file".to_string()
499
+ }
500
+ }
501
+ "multiedit" => {
502
+ if let Some(file_path) = input.get("file_path").and_then(|p| p.as_str()) {
503
+ let relative_path = self.make_path_relative(file_path, worktree_path);
504
+ format!("Multi-edit file: `{}`", relative_path)
505
+ } else {
506
+ "Multi-edit file".to_string()
507
+ }
508
+ }
509
+ "bash" => {
510
+ if let Some(command) = input.get("command").and_then(|c| c.as_str()) {
511
+ format!("Run command: `{}`", command)
512
+ } else {
513
+ "Run command".to_string()
514
+ }
515
+ }
516
+ "webfetch" => {
517
+ if let Some(url) = input.get("url").and_then(|u| u.as_str()) {
518
+ format!("Fetch URL: `{}`", url)
519
+ } else {
520
+ "Fetch URL".to_string()
521
+ }
522
+ }
523
+ "task" => {
524
+ if let Some(description) = input.get("description").and_then(|d| d.as_str())
525
+ {
526
+ format!("Task: {}", description)
527
+ } else if let Some(prompt) = input.get("prompt").and_then(|p| p.as_str()) {
528
+ format!("Task: {}", prompt)
529
+ } else {
530
+ "Task".to_string()
531
+ }
532
+ }
533
+ _ => tool_name.to_string(),
534
+ }
535
+ }
536
+ }
537
+ }
538
+
539
+ fn extract_action_type(
540
+ &self,
541
+ tool_name: &str,
542
+ input: &serde_json::Value,
543
+ worktree_path: &str,
544
+ ) -> ActionType {
545
+ match tool_name.to_lowercase().as_str() {
546
+ "read_file" | "read" => {
547
+ if let Some(path) = input.get("path").and_then(|p| p.as_str()) {
548
+ ActionType::FileRead {
549
+ path: self.make_path_relative(path, worktree_path),
550
+ }
551
+ } else if let Some(file_path) = input.get("file_path").and_then(|p| p.as_str()) {
552
+ ActionType::FileRead {
553
+ path: self.make_path_relative(file_path, worktree_path),
554
+ }
555
+ } else {
556
+ ActionType::Other {
557
+ description: "File read operation".to_string(),
558
+ }
559
+ }
560
+ }
561
+ "edit_file" | "write" | "create_file" | "edit" | "multiedit" => {
562
+ if let Some(path) = input.get("path").and_then(|p| p.as_str()) {
563
+ ActionType::FileWrite {
564
+ path: self.make_path_relative(path, worktree_path),
565
+ }
566
+ } else if let Some(file_path) = input.get("file_path").and_then(|p| p.as_str()) {
567
+ ActionType::FileWrite {
568
+ path: self.make_path_relative(file_path, worktree_path),
569
+ }
570
+ } else {
571
+ ActionType::Other {
572
+ description: "File write operation".to_string(),
573
+ }
574
+ }
575
+ }
576
+ "bash" | "run_command" => {
577
+ if let Some(cmd) = input.get("cmd").and_then(|c| c.as_str()) {
578
+ ActionType::CommandRun {
579
+ command: cmd.to_string(),
580
+ }
581
+ } else if let Some(command) = input.get("command").and_then(|c| c.as_str()) {
582
+ ActionType::CommandRun {
583
+ command: command.to_string(),
584
+ }
585
+ } else {
586
+ ActionType::Other {
587
+ description: "Command execution".to_string(),
588
+ }
589
+ }
590
+ }
591
+ "grep" | "search" => {
592
+ if let Some(pattern) = input.get("pattern").and_then(|p| p.as_str()) {
593
+ ActionType::Search {
594
+ query: pattern.to_string(),
595
+ }
596
+ } else if let Some(query) = input.get("query").and_then(|q| q.as_str()) {
597
+ ActionType::Search {
598
+ query: query.to_string(),
599
+ }
600
+ } else {
601
+ ActionType::Other {
602
+ description: "Search operation".to_string(),
603
+ }
604
+ }
605
+ }
606
+ "web_fetch" | "webfetch" => {
607
+ if let Some(url) = input.get("url").and_then(|u| u.as_str()) {
608
+ ActionType::WebFetch {
609
+ url: url.to_string(),
610
+ }
611
+ } else {
612
+ ActionType::Other {
613
+ description: "Web fetch operation".to_string(),
614
+ }
615
+ }
616
+ }
617
+ "task" => {
618
+ if let Some(description) = input.get("description").and_then(|d| d.as_str()) {
619
+ ActionType::TaskCreate {
620
+ description: description.to_string(),
621
+ }
622
+ } else if let Some(prompt) = input.get("prompt").and_then(|p| p.as_str()) {
623
+ ActionType::TaskCreate {
624
+ description: prompt.to_string(),
625
+ }
626
+ } else {
627
+ ActionType::Other {
628
+ description: "Task creation".to_string(),
629
+ }
630
+ }
631
+ }
632
+ "glob" => ActionType::Other {
633
+ description: "File pattern search".to_string(),
634
+ },
635
+ "ls" => ActionType::Other {
636
+ description: "List directory".to_string(),
637
+ },
638
+ "todowrite" | "todoread" | "todo_write" | "todo_read" => ActionType::Other {
639
+ description: "Manage TODO list".to_string(),
640
+ },
641
+ _ => ActionType::Other {
642
+ description: format!("Tool: {}", tool_name),
643
+ },
644
+ }
645
+ }
646
+ }
647
+
648
+ #[cfg(test)]
649
+ mod tests {
650
+ use super::*;
651
+
652
+ #[test]
653
+ fn test_filter_streaming_messages() {
654
+ // Test logs that simulate the actual normalize_logs behavior
655
+ let amp_executor = AmpExecutor;
656
+ let logs = r#"{"type":"messages","messages":[[7,{"role":"assistant","content":[{"type":"text","text":"Created all three files: test1.txt, test2.txt, and test3.txt"}],"state":{"type":"streaming"}}]],"toolResults":[]}
657
+ {"type":"messages","messages":[[7,{"role":"assistant","content":[{"type":"text","text":"Created all three files: test1.txt, test2.txt, and test3.txt, each with a line of text."}],"state":{"type":"streaming"}}]],"toolResults":[]}
658
+ {"type":"messages","messages":[[7,{"role":"assistant","content":[{"type":"text","text":"Created all three files: test1.txt, test2.txt, and test3.txt, each with a line of text."}],"state":{"type":"complete","stopReason":"end_turn"}}]],"toolResults":[]}"#;
659
+
660
+ let result = amp_executor.normalize_logs(logs, "/tmp/test");
661
+ assert!(result.is_ok());
662
+
663
+ let conversation = result.unwrap();
664
+
665
+ // Should only have 1 assistant message (the complete one)
666
+ let assistant_messages: Vec<_> = conversation
667
+ .entries
668
+ .iter()
669
+ .filter(|e| matches!(e.entry_type, NormalizedEntryType::AssistantMessage))
670
+ .collect();
671
+
672
+ assert_eq!(assistant_messages.len(), 1);
673
+ assert_eq!(assistant_messages[0].content, "Created all three files: test1.txt, test2.txt, and test3.txt, each with a line of text.");
674
+ }
675
+
676
+ #[test]
677
+ fn test_filter_preserves_messages_without_state() {
678
+ // Test that messages without state metadata are preserved (for compatibility)
679
+ let amp_executor = AmpExecutor;
680
+ let logs = r#"{"type":"messages","messages":[[1,{"role":"assistant","content":[{"type":"text","text":"Regular message"}]}]],"toolResults":[]}"#;
681
+
682
+ let result = amp_executor.normalize_logs(logs, "/tmp/test");
683
+ assert!(result.is_ok());
684
+
685
+ let conversation = result.unwrap();
686
+
687
+ // Should have 1 assistant message
688
+ let assistant_messages: Vec<_> = conversation
689
+ .entries
690
+ .iter()
691
+ .filter(|e| matches!(e.entry_type, NormalizedEntryType::AssistantMessage))
692
+ .collect();
693
+
694
+ assert_eq!(assistant_messages.len(), 1);
695
+ assert_eq!(assistant_messages[0].content, "Regular message");
696
+ }
697
+ }