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,887 @@
1
+ use std::path::Path;
2
+
3
+ use async_trait::async_trait;
4
+ use command_group::{AsyncCommandGroup, AsyncGroupChild};
5
+ use tokio::process::Command;
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
+ fn create_watchkill_script(command: &str) -> String {
18
+ let claude_plan_stop_indicator = "Exit plan mode?";
19
+ format!(
20
+ r#"#!/usr/bin/env bash
21
+ set -euo pipefail
22
+
23
+ word="{}"
24
+ command="{}"
25
+
26
+ exit_code=0
27
+ while IFS= read -r line; do
28
+ printf '%s\n' "$line"
29
+ if [[ $line == *"$word"* ]]; then
30
+ exit 0
31
+ fi
32
+ done < <($command <&0 2>&1)
33
+
34
+ exit_code=${{PIPESTATUS[0]}}
35
+ exit "$exit_code"
36
+ "#,
37
+ claude_plan_stop_indicator, command
38
+ )
39
+ }
40
+
41
+ /// An executor that uses Claude CLI to process tasks
42
+ pub struct ClaudeExecutor {
43
+ executor_type: String,
44
+ command: String,
45
+ }
46
+
47
+ impl Default for ClaudeExecutor {
48
+ fn default() -> Self {
49
+ Self::new()
50
+ }
51
+ }
52
+
53
+ impl ClaudeExecutor {
54
+ /// Create a new ClaudeExecutor with default settings
55
+ pub fn new() -> Self {
56
+ Self {
57
+ executor_type: "Claude Code".to_string(),
58
+ command: "npx -y @anthropic-ai/claude-code@latest -p --dangerously-skip-permissions --verbose --output-format=stream-json".to_string(),
59
+ }
60
+ }
61
+
62
+ pub fn new_plan_mode() -> Self {
63
+ let command = "npx -y @anthropic-ai/claude-code@latest -p --permission-mode=plan --verbose --output-format=stream-json";
64
+ let script = create_watchkill_script(command);
65
+ Self {
66
+ executor_type: "ClaudePlan".to_string(),
67
+ command: script,
68
+ }
69
+ }
70
+
71
+ /// Create a new ClaudeExecutor with custom settings
72
+ pub fn with_command(executor_type: String, command: String) -> Self {
73
+ Self {
74
+ executor_type,
75
+ command,
76
+ }
77
+ }
78
+ }
79
+
80
+ #[async_trait]
81
+ impl Executor for ClaudeExecutor {
82
+ async fn spawn(
83
+ &self,
84
+ pool: &sqlx::SqlitePool,
85
+ task_id: Uuid,
86
+ worktree_path: &str,
87
+ ) -> Result<AsyncGroupChild, ExecutorError> {
88
+ // Get the task to fetch its description
89
+ let task = Task::find_by_id(pool, task_id)
90
+ .await?
91
+ .ok_or(ExecutorError::TaskNotFound)?;
92
+
93
+ let prompt = if let Some(task_description) = task.description {
94
+ format!(
95
+ r#"project_id: {}
96
+
97
+ Task title: {}
98
+ Task description: {}"#,
99
+ task.project_id, task.title, task_description
100
+ )
101
+ } else {
102
+ format!(
103
+ r#"project_id: {}
104
+
105
+ Task title: {}"#,
106
+ task.project_id, task.title
107
+ )
108
+ };
109
+
110
+ // Use shell command for cross-platform compatibility
111
+ let (shell_cmd, shell_arg) = get_shell_command();
112
+ // Pass prompt via stdin instead of command line to avoid shell escaping issues
113
+ let claude_command = &self.command;
114
+
115
+ let mut command = Command::new(shell_cmd);
116
+ command
117
+ .kill_on_drop(true)
118
+ .stdin(std::process::Stdio::piped())
119
+ .stdout(std::process::Stdio::piped())
120
+ .stderr(std::process::Stdio::piped())
121
+ .current_dir(worktree_path)
122
+ .arg(shell_arg)
123
+ .arg(claude_command)
124
+ .env("NODE_NO_WARNINGS", "1");
125
+
126
+ let mut child = command
127
+ .group_spawn() // Create new process group so we can kill entire tree
128
+ .map_err(|e| {
129
+ crate::executor::SpawnContext::from_command(&command, &self.executor_type)
130
+ .with_task(task_id, Some(task.title.clone()))
131
+ .with_context(format!("{} CLI execution for new task", self.executor_type))
132
+ .spawn_error(e)
133
+ })?;
134
+
135
+ // Write prompt to stdin safely
136
+ if let Some(mut stdin) = child.inner().stdin.take() {
137
+ use tokio::io::AsyncWriteExt;
138
+ tracing::debug!(
139
+ "Writing prompt to Claude stdin for task {}: {:?}",
140
+ task_id,
141
+ prompt
142
+ );
143
+ stdin.write_all(prompt.as_bytes()).await.map_err(|e| {
144
+ let context =
145
+ crate::executor::SpawnContext::from_command(&command, &self.executor_type)
146
+ .with_task(task_id, Some(task.title.clone()))
147
+ .with_context(format!(
148
+ "Failed to write prompt to {} CLI stdin",
149
+ self.executor_type
150
+ ));
151
+ ExecutorError::spawn_failed(e, context)
152
+ })?;
153
+ stdin.shutdown().await.map_err(|e| {
154
+ let context =
155
+ crate::executor::SpawnContext::from_command(&command, &self.executor_type)
156
+ .with_task(task_id, Some(task.title.clone()))
157
+ .with_context(format!("Failed to close {} CLI stdin", self.executor_type));
158
+ ExecutorError::spawn_failed(e, context)
159
+ })?;
160
+ }
161
+
162
+ Ok(child)
163
+ }
164
+
165
+ async fn spawn_followup(
166
+ &self,
167
+ _pool: &sqlx::SqlitePool,
168
+ _task_id: Uuid,
169
+ session_id: &str,
170
+ prompt: &str,
171
+ worktree_path: &str,
172
+ ) -> Result<AsyncGroupChild, ExecutorError> {
173
+ // Use shell command for cross-platform compatibility
174
+ let (shell_cmd, shell_arg) = get_shell_command();
175
+
176
+ // Determine the command based on whether this is plan mode or not
177
+ let claude_command = if self.executor_type == "ClaudePlan" {
178
+ let command = format!(
179
+ "npx -y @anthropic-ai/claude-code@latest -p --permission-mode=plan --verbose --output-format=stream-json --resume={}",
180
+ session_id
181
+ );
182
+ create_watchkill_script(&command)
183
+ } else {
184
+ format!("{} --resume={}", self.command, session_id)
185
+ };
186
+
187
+ let mut command = Command::new(shell_cmd);
188
+ command
189
+ .kill_on_drop(true)
190
+ .stdin(std::process::Stdio::piped())
191
+ .stdout(std::process::Stdio::piped())
192
+ .stderr(std::process::Stdio::piped())
193
+ .current_dir(worktree_path)
194
+ .arg(shell_arg)
195
+ .arg(&claude_command)
196
+ .env("NODE_NO_WARNINGS", "1");
197
+
198
+ let mut child = command.group_spawn().map_err(|e| {
199
+ crate::executor::SpawnContext::from_command(&command, &self.executor_type)
200
+ .with_context(format!(
201
+ "{} CLI followup execution for session {}",
202
+ self.executor_type, session_id
203
+ ))
204
+ .spawn_error(e)
205
+ })?;
206
+
207
+ // Write prompt to stdin safely
208
+ if let Some(mut stdin) = child.inner().stdin.take() {
209
+ use tokio::io::AsyncWriteExt;
210
+ tracing::debug!(
211
+ "Writing prompt to {} stdin for session {}: {:?}",
212
+ self.executor_type,
213
+ session_id,
214
+ prompt
215
+ );
216
+ stdin.write_all(prompt.as_bytes()).await.map_err(|e| {
217
+ let context =
218
+ crate::executor::SpawnContext::from_command(&command, &self.executor_type)
219
+ .with_context(format!(
220
+ "Failed to write prompt to {} CLI stdin for session {}",
221
+ self.executor_type, session_id
222
+ ));
223
+ ExecutorError::spawn_failed(e, context)
224
+ })?;
225
+ stdin.shutdown().await.map_err(|e| {
226
+ let context =
227
+ crate::executor::SpawnContext::from_command(&command, &self.executor_type)
228
+ .with_context(format!(
229
+ "Failed to close {} CLI stdin for session {}",
230
+ self.executor_type, session_id
231
+ ));
232
+ ExecutorError::spawn_failed(e, context)
233
+ })?;
234
+ }
235
+
236
+ Ok(child)
237
+ }
238
+
239
+ fn normalize_logs(
240
+ &self,
241
+ logs: &str,
242
+ worktree_path: &str,
243
+ ) -> Result<NormalizedConversation, String> {
244
+ use serde_json::Value;
245
+
246
+ let mut entries = Vec::new();
247
+ let mut session_id = None;
248
+
249
+ for line in logs.lines() {
250
+ let trimmed = line.trim();
251
+ if trimmed.is_empty() {
252
+ continue;
253
+ }
254
+
255
+ // Try to parse as JSON
256
+ let json: Value = match serde_json::from_str(trimmed) {
257
+ Ok(json) => json,
258
+ Err(_) => {
259
+ // If line isn't valid JSON, add it as raw text
260
+ entries.push(NormalizedEntry {
261
+ timestamp: None,
262
+ entry_type: NormalizedEntryType::SystemMessage,
263
+ content: format!("Raw output: {}", trimmed),
264
+ metadata: None,
265
+ });
266
+ continue;
267
+ }
268
+ };
269
+
270
+ // Extract session ID
271
+ if session_id.is_none() {
272
+ if let Some(sess_id) = json.get("session_id").and_then(|v| v.as_str()) {
273
+ session_id = Some(sess_id.to_string());
274
+ }
275
+ }
276
+
277
+ // Process different message types
278
+ let processed = if let Some(msg_type) = json.get("type").and_then(|t| t.as_str()) {
279
+ match msg_type {
280
+ "assistant" => {
281
+ if let Some(message) = json.get("message") {
282
+ if let Some(content) = message.get("content").and_then(|c| c.as_array())
283
+ {
284
+ for content_item in content {
285
+ if let Some(content_type) =
286
+ content_item.get("type").and_then(|t| t.as_str())
287
+ {
288
+ match content_type {
289
+ "text" => {
290
+ if let Some(text) = content_item
291
+ .get("text")
292
+ .and_then(|t| t.as_str())
293
+ {
294
+ entries.push(NormalizedEntry {
295
+ timestamp: None,
296
+ entry_type:
297
+ NormalizedEntryType::AssistantMessage,
298
+ content: text.to_string(),
299
+ metadata: Some(content_item.clone()),
300
+ });
301
+ }
302
+ }
303
+ "tool_use" => {
304
+ if let Some(tool_name) = content_item
305
+ .get("name")
306
+ .and_then(|n| n.as_str())
307
+ {
308
+ let input = content_item
309
+ .get("input")
310
+ .unwrap_or(&Value::Null);
311
+ let action_type = self.extract_action_type(
312
+ tool_name,
313
+ input,
314
+ worktree_path,
315
+ );
316
+ let content = self.generate_concise_content(
317
+ tool_name,
318
+ input,
319
+ &action_type,
320
+ worktree_path,
321
+ );
322
+
323
+ entries.push(NormalizedEntry {
324
+ timestamp: None,
325
+ entry_type: NormalizedEntryType::ToolUse {
326
+ tool_name: tool_name.to_string(),
327
+ action_type,
328
+ },
329
+ content,
330
+ metadata: Some(content_item.clone()),
331
+ });
332
+ }
333
+ }
334
+ _ => {}
335
+ }
336
+ }
337
+ }
338
+ }
339
+ }
340
+ true
341
+ }
342
+ "user" => {
343
+ if let Some(message) = json.get("message") {
344
+ if let Some(content) = message.get("content").and_then(|c| c.as_array())
345
+ {
346
+ for content_item in content {
347
+ if let Some(content_type) =
348
+ content_item.get("type").and_then(|t| t.as_str())
349
+ {
350
+ if content_type == "text" {
351
+ if let Some(text) =
352
+ content_item.get("text").and_then(|t| t.as_str())
353
+ {
354
+ entries.push(NormalizedEntry {
355
+ timestamp: None,
356
+ entry_type: NormalizedEntryType::UserMessage,
357
+ content: text.to_string(),
358
+ metadata: Some(content_item.clone()),
359
+ });
360
+ }
361
+ }
362
+ }
363
+ }
364
+ }
365
+ }
366
+ true
367
+ }
368
+ "system" => {
369
+ if let Some(subtype) = json.get("subtype").and_then(|s| s.as_str()) {
370
+ if subtype == "init" {
371
+ entries.push(NormalizedEntry {
372
+ timestamp: None,
373
+ entry_type: NormalizedEntryType::SystemMessage,
374
+ content: format!(
375
+ "System initialized with model: {}",
376
+ json.get("model")
377
+ .and_then(|m| m.as_str())
378
+ .unwrap_or("unknown")
379
+ ),
380
+ metadata: Some(json.clone()),
381
+ });
382
+ }
383
+ }
384
+ true
385
+ }
386
+ _ => false,
387
+ }
388
+ } else {
389
+ false
390
+ };
391
+
392
+ // If JSON didn't match expected patterns, add it as unrecognized JSON
393
+ // Skip JSON with type "result" as requested
394
+ if !processed {
395
+ if let Some(msg_type) = json.get("type").and_then(|t| t.as_str()) {
396
+ if msg_type == "result" {
397
+ // Skip result entries
398
+ continue;
399
+ }
400
+ }
401
+ entries.push(NormalizedEntry {
402
+ timestamp: None,
403
+ entry_type: NormalizedEntryType::SystemMessage,
404
+ content: format!("Unrecognized JSON: {}", trimmed),
405
+ metadata: Some(json),
406
+ });
407
+ }
408
+ }
409
+
410
+ Ok(NormalizedConversation {
411
+ entries,
412
+ session_id,
413
+ executor_type: self.executor_type.clone(),
414
+ prompt: None,
415
+ summary: None,
416
+ })
417
+ }
418
+ }
419
+
420
+ impl ClaudeExecutor {
421
+ /// Convert absolute paths to relative paths based on worktree path
422
+ fn make_path_relative(&self, path: &str, worktree_path: &str) -> String {
423
+ let path_obj = Path::new(path);
424
+ let worktree_path_obj = Path::new(worktree_path);
425
+
426
+ tracing::debug!("Making path relative: {} -> {}", path, worktree_path);
427
+
428
+ // If path is already relative, return as is
429
+ if path_obj.is_relative() {
430
+ return path.to_string();
431
+ }
432
+
433
+ // Try to make path relative to the worktree path
434
+ match path_obj.strip_prefix(worktree_path_obj) {
435
+ Ok(relative_path) => {
436
+ let result = relative_path.to_string_lossy().to_string();
437
+ tracing::debug!("Successfully made relative: '{}' -> '{}'", path, result);
438
+ result
439
+ }
440
+ Err(_) => {
441
+ // Handle symlinks by resolving canonical paths
442
+ let canonical_path = std::fs::canonicalize(path);
443
+ let canonical_worktree = std::fs::canonicalize(worktree_path);
444
+
445
+ match (canonical_path, canonical_worktree) {
446
+ (Ok(canon_path), Ok(canon_worktree)) => {
447
+ tracing::debug!(
448
+ "Trying canonical path resolution: '{}' -> '{}', '{}' -> '{}'",
449
+ path,
450
+ canon_path.display(),
451
+ worktree_path,
452
+ canon_worktree.display()
453
+ );
454
+
455
+ match canon_path.strip_prefix(&canon_worktree) {
456
+ Ok(relative_path) => {
457
+ let result = relative_path.to_string_lossy().to_string();
458
+ tracing::debug!(
459
+ "Successfully made relative with canonical paths: '{}' -> '{}'",
460
+ path,
461
+ result
462
+ );
463
+ result
464
+ }
465
+ Err(e) => {
466
+ tracing::warn!(
467
+ "Failed to make canonical path relative: '{}' relative to '{}', error: {}, returning original",
468
+ canon_path.display(),
469
+ canon_worktree.display(),
470
+ e
471
+ );
472
+ path.to_string()
473
+ }
474
+ }
475
+ }
476
+ _ => {
477
+ tracing::debug!(
478
+ "Could not canonicalize paths (paths may not exist): '{}', '{}', returning original",
479
+ path,
480
+ worktree_path
481
+ );
482
+ path.to_string()
483
+ }
484
+ }
485
+ }
486
+ }
487
+ }
488
+
489
+ fn generate_concise_content(
490
+ &self,
491
+ tool_name: &str,
492
+ input: &serde_json::Value,
493
+ action_type: &ActionType,
494
+ worktree_path: &str,
495
+ ) -> String {
496
+ match action_type {
497
+ ActionType::FileRead { path } => format!("`{}`", path),
498
+ ActionType::FileWrite { path } => format!("`{}`", path),
499
+ ActionType::CommandRun { command } => format!("`{}`", command),
500
+ ActionType::Search { query } => format!("`{}`", query),
501
+ ActionType::WebFetch { url } => format!("`{}`", url),
502
+ ActionType::TaskCreate { description } => description.clone(),
503
+ ActionType::PlanPresentation { plan } => plan.clone(),
504
+ ActionType::Other { description: _ } => {
505
+ // For other tools, try to extract key information or fall back to tool name
506
+ match tool_name.to_lowercase().as_str() {
507
+ "todoread" | "todowrite" => {
508
+ // Extract todo list from input to show actual todos
509
+ if let Some(todos) = input.get("todos").and_then(|t| t.as_array()) {
510
+ let mut todo_items = Vec::new();
511
+ for todo in todos {
512
+ if let Some(content) = todo.get("content").and_then(|c| c.as_str())
513
+ {
514
+ let status = todo
515
+ .get("status")
516
+ .and_then(|s| s.as_str())
517
+ .unwrap_or("pending");
518
+ let status_emoji = match status {
519
+ "completed" => "✅",
520
+ "in_progress" => "🔄",
521
+ "pending" | "todo" => "⏳",
522
+ _ => "📝",
523
+ };
524
+ let priority = todo
525
+ .get("priority")
526
+ .and_then(|p| p.as_str())
527
+ .unwrap_or("medium");
528
+ todo_items.push(format!(
529
+ "{} {} ({})",
530
+ status_emoji, content, priority
531
+ ));
532
+ }
533
+ }
534
+ if !todo_items.is_empty() {
535
+ format!("TODO List:\n{}", todo_items.join("\n"))
536
+ } else {
537
+ "Managing TODO list".to_string()
538
+ }
539
+ } else {
540
+ "Managing TODO list".to_string()
541
+ }
542
+ }
543
+ "ls" => {
544
+ if let Some(path) = input.get("path").and_then(|p| p.as_str()) {
545
+ let relative_path = self.make_path_relative(path, worktree_path);
546
+ if relative_path.is_empty() {
547
+ "List directory".to_string()
548
+ } else {
549
+ format!("List directory: `{}`", relative_path)
550
+ }
551
+ } else {
552
+ "List directory".to_string()
553
+ }
554
+ }
555
+ "glob" => {
556
+ let pattern = input.get("pattern").and_then(|p| p.as_str()).unwrap_or("*");
557
+ let path = input.get("path").and_then(|p| p.as_str());
558
+
559
+ if let Some(search_path) = path {
560
+ format!(
561
+ "Find files: `{}` in `{}`",
562
+ pattern,
563
+ self.make_path_relative(search_path, worktree_path)
564
+ )
565
+ } else {
566
+ format!("Find files: `{}`", pattern)
567
+ }
568
+ }
569
+ "codebase_search_agent" => {
570
+ if let Some(query) = input.get("query").and_then(|q| q.as_str()) {
571
+ format!("Search: {}", query)
572
+ } else {
573
+ "Codebase search".to_string()
574
+ }
575
+ }
576
+ _ => tool_name.to_string(),
577
+ }
578
+ }
579
+ }
580
+ }
581
+
582
+ fn extract_action_type(
583
+ &self,
584
+ tool_name: &str,
585
+ input: &serde_json::Value,
586
+ worktree_path: &str,
587
+ ) -> ActionType {
588
+ match tool_name.to_lowercase().as_str() {
589
+ "read" => {
590
+ if let Some(file_path) = input.get("file_path").and_then(|p| p.as_str()) {
591
+ ActionType::FileRead {
592
+ path: self.make_path_relative(file_path, worktree_path),
593
+ }
594
+ } else {
595
+ ActionType::Other {
596
+ description: "File read operation".to_string(),
597
+ }
598
+ }
599
+ }
600
+ "edit" | "write" | "multiedit" => {
601
+ if let Some(file_path) = input.get("file_path").and_then(|p| p.as_str()) {
602
+ ActionType::FileWrite {
603
+ path: self.make_path_relative(file_path, worktree_path),
604
+ }
605
+ } else if let Some(path) = input.get("path").and_then(|p| p.as_str()) {
606
+ ActionType::FileWrite {
607
+ path: self.make_path_relative(path, worktree_path),
608
+ }
609
+ } else {
610
+ ActionType::Other {
611
+ description: "File write operation".to_string(),
612
+ }
613
+ }
614
+ }
615
+ "bash" => {
616
+ if let Some(command) = input.get("command").and_then(|c| c.as_str()) {
617
+ ActionType::CommandRun {
618
+ command: command.to_string(),
619
+ }
620
+ } else {
621
+ ActionType::Other {
622
+ description: "Command execution".to_string(),
623
+ }
624
+ }
625
+ }
626
+ "grep" => {
627
+ if let Some(pattern) = input.get("pattern").and_then(|p| p.as_str()) {
628
+ ActionType::Search {
629
+ query: pattern.to_string(),
630
+ }
631
+ } else {
632
+ ActionType::Other {
633
+ description: "Search operation".to_string(),
634
+ }
635
+ }
636
+ }
637
+ "glob" => {
638
+ if let Some(pattern) = input.get("pattern").and_then(|p| p.as_str()) {
639
+ ActionType::Other {
640
+ description: format!("Find files: {}", pattern),
641
+ }
642
+ } else {
643
+ ActionType::Other {
644
+ description: "File pattern search".to_string(),
645
+ }
646
+ }
647
+ }
648
+ "webfetch" => {
649
+ if let Some(url) = input.get("url").and_then(|u| u.as_str()) {
650
+ ActionType::WebFetch {
651
+ url: url.to_string(),
652
+ }
653
+ } else {
654
+ ActionType::Other {
655
+ description: "Web fetch operation".to_string(),
656
+ }
657
+ }
658
+ }
659
+ "task" => {
660
+ if let Some(description) = input.get("description").and_then(|d| d.as_str()) {
661
+ ActionType::TaskCreate {
662
+ description: description.to_string(),
663
+ }
664
+ } else if let Some(prompt) = input.get("prompt").and_then(|p| p.as_str()) {
665
+ ActionType::TaskCreate {
666
+ description: prompt.to_string(),
667
+ }
668
+ } else {
669
+ ActionType::Other {
670
+ description: "Task creation".to_string(),
671
+ }
672
+ }
673
+ }
674
+ "exit_plan_mode" => {
675
+ if let Some(plan) = input.get("plan").and_then(|p| p.as_str()) {
676
+ ActionType::PlanPresentation {
677
+ plan: plan.to_string(),
678
+ }
679
+ } else {
680
+ ActionType::Other {
681
+ description: "Plan presentation".to_string(),
682
+ }
683
+ }
684
+ }
685
+ _ => ActionType::Other {
686
+ description: format!("Tool: {}", tool_name),
687
+ },
688
+ }
689
+ }
690
+ }
691
+
692
+ #[cfg(test)]
693
+ mod tests {
694
+ use super::*;
695
+
696
+ #[test]
697
+ fn test_normalize_logs_ignores_result_type() {
698
+ let executor = ClaudeExecutor::new();
699
+ let logs = r#"{"type":"system","subtype":"init","cwd":"/private/tmp","session_id":"e988eeea-3712-46a1-82d4-84fbfaa69114","tools":[],"model":"claude-sonnet-4-20250514"}
700
+ {"type":"assistant","message":{"id":"msg_123","type":"message","role":"assistant","model":"claude-sonnet-4-20250514","content":[{"type":"text","text":"Hello world"}],"stop_reason":null},"session_id":"e988eeea-3712-46a1-82d4-84fbfaa69114"}
701
+ {"type":"result","subtype":"success","is_error":false,"duration_ms":6059,"result":"Final result"}
702
+ {"type":"unknown","data":"some data"}"#;
703
+
704
+ let result = executor.normalize_logs(logs, "/tmp/test-worktree").unwrap();
705
+
706
+ // Should have system message, assistant message, and unknown message
707
+ // but NOT the result message
708
+ assert_eq!(result.entries.len(), 3);
709
+
710
+ // Check that no entry contains "result"
711
+ for entry in &result.entries {
712
+ assert!(!entry.content.contains("result"));
713
+ }
714
+
715
+ // Check that unknown JSON is still processed
716
+ assert!(result
717
+ .entries
718
+ .iter()
719
+ .any(|e| e.content.contains("Unrecognized JSON")));
720
+ }
721
+
722
+ #[test]
723
+ fn test_make_path_relative() {
724
+ let executor = ClaudeExecutor::new();
725
+
726
+ // Test with relative path (should remain unchanged)
727
+ assert_eq!(
728
+ executor.make_path_relative("src/main.rs", "/tmp/test-worktree"),
729
+ "src/main.rs"
730
+ );
731
+
732
+ // Test with absolute path (should become relative if possible)
733
+ let test_worktree = "/tmp/test-worktree";
734
+ let absolute_path = format!("{}/src/main.rs", test_worktree);
735
+ let result = executor.make_path_relative(&absolute_path, test_worktree);
736
+ assert_eq!(result, "src/main.rs");
737
+ }
738
+
739
+ #[test]
740
+ fn test_todo_tool_content_extraction() {
741
+ let executor = ClaudeExecutor::new();
742
+
743
+ // Test TodoWrite with actual todo list
744
+ let todo_input = serde_json::json!({
745
+ "todos": [
746
+ {
747
+ "id": "1",
748
+ "content": "Fix the navigation bug",
749
+ "status": "completed",
750
+ "priority": "high"
751
+ },
752
+ {
753
+ "id": "2",
754
+ "content": "Add user authentication",
755
+ "status": "in_progress",
756
+ "priority": "medium"
757
+ },
758
+ {
759
+ "id": "3",
760
+ "content": "Write documentation",
761
+ "status": "pending",
762
+ "priority": "low"
763
+ }
764
+ ]
765
+ });
766
+
767
+ let result = executor.generate_concise_content(
768
+ "TodoWrite",
769
+ &todo_input,
770
+ &ActionType::Other {
771
+ description: "Tool: TodoWrite".to_string(),
772
+ },
773
+ "/tmp/test-worktree",
774
+ );
775
+
776
+ assert!(result.contains("TODO List:"));
777
+ assert!(result.contains("✅ Fix the navigation bug (high)"));
778
+ assert!(result.contains("🔄 Add user authentication (medium)"));
779
+ assert!(result.contains("⏳ Write documentation (low)"));
780
+ }
781
+
782
+ #[test]
783
+ fn test_todo_tool_empty_list() {
784
+ let executor = ClaudeExecutor::new();
785
+
786
+ // Test TodoWrite with empty todo list
787
+ let empty_input = serde_json::json!({
788
+ "todos": []
789
+ });
790
+
791
+ let result = executor.generate_concise_content(
792
+ "TodoWrite",
793
+ &empty_input,
794
+ &ActionType::Other {
795
+ description: "Tool: TodoWrite".to_string(),
796
+ },
797
+ "/tmp/test-worktree",
798
+ );
799
+
800
+ assert_eq!(result, "Managing TODO list");
801
+ }
802
+
803
+ #[test]
804
+ fn test_todo_tool_no_todos_field() {
805
+ let executor = ClaudeExecutor::new();
806
+
807
+ // Test TodoWrite with no todos field
808
+ let no_todos_input = serde_json::json!({
809
+ "other_field": "value"
810
+ });
811
+
812
+ let result = executor.generate_concise_content(
813
+ "TodoWrite",
814
+ &no_todos_input,
815
+ &ActionType::Other {
816
+ description: "Tool: TodoWrite".to_string(),
817
+ },
818
+ "/tmp/test-worktree",
819
+ );
820
+
821
+ assert_eq!(result, "Managing TODO list");
822
+ }
823
+
824
+ #[test]
825
+ fn test_glob_tool_content_extraction() {
826
+ let executor = ClaudeExecutor::new();
827
+
828
+ // Test Glob with pattern and path
829
+ let glob_input = serde_json::json!({
830
+ "pattern": "**/*.ts",
831
+ "path": "/tmp/test-worktree/src"
832
+ });
833
+
834
+ let result = executor.generate_concise_content(
835
+ "Glob",
836
+ &glob_input,
837
+ &ActionType::Other {
838
+ description: "Find files: **/*.ts".to_string(),
839
+ },
840
+ "/tmp/test-worktree",
841
+ );
842
+
843
+ assert_eq!(result, "Find files: `**/*.ts` in `src`");
844
+ }
845
+
846
+ #[test]
847
+ fn test_glob_tool_pattern_only() {
848
+ let executor = ClaudeExecutor::new();
849
+
850
+ // Test Glob with pattern only
851
+ let glob_input = serde_json::json!({
852
+ "pattern": "*.js"
853
+ });
854
+
855
+ let result = executor.generate_concise_content(
856
+ "Glob",
857
+ &glob_input,
858
+ &ActionType::Other {
859
+ description: "Find files: *.js".to_string(),
860
+ },
861
+ "/tmp/test-worktree",
862
+ );
863
+
864
+ assert_eq!(result, "Find files: `*.js`");
865
+ }
866
+
867
+ #[test]
868
+ fn test_ls_tool_content_extraction() {
869
+ let executor = ClaudeExecutor::new();
870
+
871
+ // Test LS with path
872
+ let ls_input = serde_json::json!({
873
+ "path": "/tmp/test-worktree/components"
874
+ });
875
+
876
+ let result = executor.generate_concise_content(
877
+ "LS",
878
+ &ls_input,
879
+ &ActionType::Other {
880
+ description: "Tool: LS".to_string(),
881
+ },
882
+ "/tmp/test-worktree",
883
+ );
884
+
885
+ assert_eq!(result, "List directory: `components`");
886
+ }
887
+ }