collavre 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (410) hide show
  1. checksums.yaml +7 -0
  2. data/README.md +221 -0
  3. data/Rakefile +8 -0
  4. data/app/assets/stylesheets/collavre/actiontext.css +577 -0
  5. data/app/assets/stylesheets/collavre/activity_logs.css +99 -0
  6. data/app/assets/stylesheets/collavre/comments_popup.css +692 -0
  7. data/app/assets/stylesheets/collavre/creatives.css +559 -0
  8. data/app/assets/stylesheets/collavre/dark_mode.css +118 -0
  9. data/app/assets/stylesheets/collavre/mention_menu.css +43 -0
  10. data/app/assets/stylesheets/collavre/popup.css +160 -0
  11. data/app/assets/stylesheets/collavre/print.css +37 -0
  12. data/app/assets/stylesheets/collavre/slide_view.css +79 -0
  13. data/app/assets/stylesheets/collavre/user_menu.css +34 -0
  14. data/app/channels/collavre/comments_presence_channel.rb +54 -0
  15. data/app/channels/collavre/slide_view_channel.rb +11 -0
  16. data/app/channels/collavre/topics_channel.rb +12 -0
  17. data/app/components/collavre/avatar_component.html.erb +15 -0
  18. data/app/components/collavre/avatar_component.rb +59 -0
  19. data/app/components/collavre/inbox/badge_component.html.erb +6 -0
  20. data/app/components/collavre/inbox/badge_component.rb +18 -0
  21. data/app/components/collavre/plans_timeline_component.html.erb +14 -0
  22. data/app/components/collavre/plans_timeline_component.rb +56 -0
  23. data/app/components/collavre/popup_menu_component.html.erb +6 -0
  24. data/app/components/collavre/popup_menu_component.rb +30 -0
  25. data/app/components/collavre/progress_filter_component.html.erb +5 -0
  26. data/app/components/collavre/progress_filter_component.rb +10 -0
  27. data/app/components/collavre/user_mention_menu_component.html.erb +3 -0
  28. data/app/components/collavre/user_mention_menu_component.rb +8 -0
  29. data/app/controllers/collavre/application_controller.rb +15 -0
  30. data/app/controllers/collavre/attachments_controller.rb +44 -0
  31. data/app/controllers/collavre/calendar_events_controller.rb +15 -0
  32. data/app/controllers/collavre/comment_read_pointers_controller.rb +80 -0
  33. data/app/controllers/collavre/comments/activity_logs_controller.rb +26 -0
  34. data/app/controllers/collavre/comments/reactions_controller.rb +82 -0
  35. data/app/controllers/collavre/comments_controller.rb +464 -0
  36. data/app/controllers/collavre/contacts_controller.rb +10 -0
  37. data/app/controllers/collavre/creative_expanded_states_controller.rb +27 -0
  38. data/app/controllers/collavre/creative_imports_controller.rb +24 -0
  39. data/app/controllers/collavre/creative_plans_controller.rb +69 -0
  40. data/app/controllers/collavre/creative_shares_controller.rb +79 -0
  41. data/app/controllers/collavre/creatives_controller.rb +535 -0
  42. data/app/controllers/collavre/devices_controller.rb +19 -0
  43. data/app/controllers/collavre/email_verifications_controller.rb +16 -0
  44. data/app/controllers/collavre/emails_controller.rb +11 -0
  45. data/app/controllers/collavre/github_auth_controller.rb +25 -0
  46. data/app/controllers/collavre/google_auth_controller.rb +43 -0
  47. data/app/controllers/collavre/inbox_items_controller.rb +64 -0
  48. data/app/controllers/collavre/invites_controller.rb +27 -0
  49. data/app/controllers/collavre/notion_auth_controller.rb +25 -0
  50. data/app/controllers/collavre/passwords_controller.rb +37 -0
  51. data/app/controllers/collavre/plans_controller.rb +110 -0
  52. data/app/controllers/collavre/sessions_controller.rb +57 -0
  53. data/app/controllers/collavre/topics_controller.rb +58 -0
  54. data/app/controllers/collavre/user_themes_controller.rb +58 -0
  55. data/app/controllers/collavre/users_controller.rb +390 -0
  56. data/app/helpers/collavre/application_helper.rb +4 -0
  57. data/app/helpers/collavre/comments_helper.rb +9 -0
  58. data/app/helpers/collavre/creatives_helper.rb +343 -0
  59. data/app/helpers/collavre/navigation_helper.rb +163 -0
  60. data/app/helpers/collavre/user_themes_helper.rb +4 -0
  61. data/app/javascript/collavre.js +26 -0
  62. data/app/javascript/components/InlineLexicalEditor.jsx +889 -0
  63. data/app/javascript/components/LinkPopup.jsx +112 -0
  64. data/app/javascript/components/creative_tree_row.js +503 -0
  65. data/app/javascript/components/plugins/attachment_cleanup_plugin.jsx +95 -0
  66. data/app/javascript/components/plugins/image_upload_plugin.jsx +162 -0
  67. data/app/javascript/controllers/click_target_controller.js +13 -0
  68. data/app/javascript/controllers/comment_controller.js +162 -0
  69. data/app/javascript/controllers/comments/__tests__/popup_controller.test.js +68 -0
  70. data/app/javascript/controllers/comments/form_controller.js +530 -0
  71. data/app/javascript/controllers/comments/list_controller.js +715 -0
  72. data/app/javascript/controllers/comments/mention_menu_controller.js +41 -0
  73. data/app/javascript/controllers/comments/popup_controller.js +385 -0
  74. data/app/javascript/controllers/comments/presence_controller.js +311 -0
  75. data/app/javascript/controllers/comments/topics_controller.js +338 -0
  76. data/app/javascript/controllers/common_popup_controller.js +55 -0
  77. data/app/javascript/controllers/creatives/drag_drop_controller.js +45 -0
  78. data/app/javascript/controllers/creatives/expansion_controller.js +222 -0
  79. data/app/javascript/controllers/creatives/import_controller.js +116 -0
  80. data/app/javascript/controllers/creatives/row_editor_controller.js +8 -0
  81. data/app/javascript/controllers/creatives/select_mode_controller.js +231 -0
  82. data/app/javascript/controllers/creatives/set_plan_modal_controller.js +107 -0
  83. data/app/javascript/controllers/creatives/tree_controller.js +218 -0
  84. data/app/javascript/controllers/index.js +79 -0
  85. data/app/javascript/controllers/link_creative_controller.js +91 -0
  86. data/app/javascript/controllers/popup_menu_controller.js +82 -0
  87. data/app/javascript/controllers/progress_filter_controller.js +35 -0
  88. data/app/javascript/controllers/reaction_picker_controller.js +107 -0
  89. data/app/javascript/controllers/share_invite_controller.js +15 -0
  90. data/app/javascript/controllers/share_user_search_controller.js +121 -0
  91. data/app/javascript/controllers/tabs_controller.js +43 -0
  92. data/app/javascript/creatives/drag_drop/dom.js +170 -0
  93. data/app/javascript/creatives/drag_drop/event_handlers.js +846 -0
  94. data/app/javascript/creatives/drag_drop/indicator.js +35 -0
  95. data/app/javascript/creatives/drag_drop/operations.js +116 -0
  96. data/app/javascript/creatives/drag_drop/state.js +31 -0
  97. data/app/javascript/creatives/tree_renderer.js +248 -0
  98. data/app/javascript/lib/api/__tests__/queue_manager.test.js +153 -0
  99. data/app/javascript/lib/api/creatives.js +79 -0
  100. data/app/javascript/lib/api/csrf_fetch.js +22 -0
  101. data/app/javascript/lib/api/drag_drop.js +31 -0
  102. data/app/javascript/lib/api/queue_manager.js +423 -0
  103. data/app/javascript/lib/apply_lexical_styles.js +15 -0
  104. data/app/javascript/lib/common_popup.js +195 -0
  105. data/app/javascript/lib/lexical/__tests__/action_text_attachment_node.test.jsx +91 -0
  106. data/app/javascript/lib/lexical/__tests__/attachment_payload.test.js +194 -0
  107. data/app/javascript/lib/lexical/action_text_attachment_node.js +459 -0
  108. data/app/javascript/lib/lexical/attachment_node.jsx +170 -0
  109. data/app/javascript/lib/lexical/attachment_payload.js +293 -0
  110. data/app/javascript/lib/lexical/dom_attachment_utils.js +66 -0
  111. data/app/javascript/lib/lexical/image_node.jsx +159 -0
  112. data/app/javascript/lib/lexical/style_attributes.js +40 -0
  113. data/app/javascript/lib/responsive_images.js +54 -0
  114. data/app/javascript/lib/turbo_stream_actions.js +33 -0
  115. data/app/javascript/lib/utils/markdown.js +23 -0
  116. data/app/javascript/modules/creative_guide.js +53 -0
  117. data/app/javascript/modules/creative_row_editor.js +1841 -0
  118. data/app/javascript/modules/creative_row_swipe.js +43 -0
  119. data/app/javascript/modules/creatives.js +15 -0
  120. data/app/javascript/modules/export_to_markdown.js +34 -0
  121. data/app/javascript/modules/inbox_panel.js +226 -0
  122. data/app/javascript/modules/lexical_inline_editor.jsx +133 -0
  123. data/app/javascript/modules/mention_menu.js +77 -0
  124. data/app/javascript/modules/plans_menu.js +39 -0
  125. data/app/javascript/modules/plans_timeline.js +397 -0
  126. data/app/javascript/modules/share_modal.js +73 -0
  127. data/app/javascript/modules/share_user_popup.js +77 -0
  128. data/app/javascript/modules/slide_view.js +163 -0
  129. data/app/javascript/services/cable.js +32 -0
  130. data/app/javascript/slide_view.js +2 -0
  131. data/app/javascript/utils/caret_position.js +42 -0
  132. data/app/javascript/utils/clipboard.js +40 -0
  133. data/app/jobs/collavre/ai_agent_job.rb +27 -0
  134. data/app/jobs/collavre/inbox_summary_job.rb +24 -0
  135. data/app/jobs/collavre/notion_export_job.rb +30 -0
  136. data/app/jobs/collavre/notion_sync_job.rb +48 -0
  137. data/app/jobs/collavre/permission_cache_cleanup_job.rb +36 -0
  138. data/app/jobs/collavre/permission_cache_job.rb +71 -0
  139. data/app/jobs/collavre/push_notification_job.rb +86 -0
  140. data/app/mailers/collavre/application_mailer.rb +17 -0
  141. data/app/mailers/collavre/creative_mailer.rb +9 -0
  142. data/app/mailers/collavre/email_verification_mailer.rb +20 -0
  143. data/app/mailers/collavre/inbox_mailer.rb +19 -0
  144. data/app/mailers/collavre/invitation_mailer.rb +16 -0
  145. data/app/mailers/collavre/passwords_mailer.rb +10 -0
  146. data/app/models/collavre/activity_log.rb +13 -0
  147. data/app/models/collavre/application_record.rb +5 -0
  148. data/app/models/collavre/calendar_event.rb +20 -0
  149. data/app/models/collavre/comment.rb +307 -0
  150. data/app/models/collavre/comment_presence_store.rb +30 -0
  151. data/app/models/collavre/comment_reaction.rb +11 -0
  152. data/app/models/collavre/comment_read_pointer.rb +26 -0
  153. data/app/models/collavre/contact.rb +23 -0
  154. data/app/models/collavre/creative.rb +413 -0
  155. data/app/models/collavre/creative_expanded_state.rb +11 -0
  156. data/app/models/collavre/creative_share.rb +122 -0
  157. data/app/models/collavre/creative_shares_cache.rb +18 -0
  158. data/app/models/collavre/current.rb +14 -0
  159. data/app/models/collavre/device.rb +13 -0
  160. data/app/models/collavre/email.rb +14 -0
  161. data/app/models/collavre/github_account.rb +10 -0
  162. data/app/models/collavre/github_repository_link.rb +19 -0
  163. data/app/models/collavre/inbox_item.rb +95 -0
  164. data/app/models/collavre/invitation.rb +22 -0
  165. data/app/models/collavre/label.rb +47 -0
  166. data/app/models/collavre/mcp_tool.rb +30 -0
  167. data/app/models/collavre/notion_account.rb +17 -0
  168. data/app/models/collavre/notion_block_link.rb +10 -0
  169. data/app/models/collavre/notion_page_link.rb +19 -0
  170. data/app/models/collavre/plan.rb +20 -0
  171. data/app/models/collavre/session.rb +24 -0
  172. data/app/models/collavre/system_setting.rb +144 -0
  173. data/app/models/collavre/tag.rb +10 -0
  174. data/app/models/collavre/task.rb +10 -0
  175. data/app/models/collavre/task_action.rb +10 -0
  176. data/app/models/collavre/topic.rb +12 -0
  177. data/app/models/collavre/user.rb +174 -0
  178. data/app/models/collavre/user_theme.rb +10 -0
  179. data/app/models/collavre/variation.rb +5 -0
  180. data/app/models/collavre/webauthn_credential.rb +11 -0
  181. data/app/services/collavre/ai_agent_service.rb +193 -0
  182. data/app/services/collavre/ai_client.rb +183 -0
  183. data/app/services/collavre/ai_system_prompt_renderer.rb +38 -0
  184. data/app/services/collavre/auto_theme_generator.rb +198 -0
  185. data/app/services/collavre/comment_link_formatter.rb +60 -0
  186. data/app/services/collavre/comments/action_executor.rb +262 -0
  187. data/app/services/collavre/comments/action_validator.rb +58 -0
  188. data/app/services/collavre/comments/calendar_command.rb +97 -0
  189. data/app/services/collavre/comments/command_processor.rb +37 -0
  190. data/app/services/collavre/comments/mcp_command.rb +109 -0
  191. data/app/services/collavre/comments/mcp_command_builder.rb +32 -0
  192. data/app/services/collavre/creatives/filter_pipeline.rb +196 -0
  193. data/app/services/collavre/creatives/filters/assignee_filter.rb +30 -0
  194. data/app/services/collavre/creatives/filters/base_filter.rb +24 -0
  195. data/app/services/collavre/creatives/filters/comment_filter.rb +21 -0
  196. data/app/services/collavre/creatives/filters/date_filter.rb +58 -0
  197. data/app/services/collavre/creatives/filters/progress_filter.rb +25 -0
  198. data/app/services/collavre/creatives/filters/search_filter.rb +28 -0
  199. data/app/services/collavre/creatives/filters/tag_filter.rb +16 -0
  200. data/app/services/collavre/creatives/importer.rb +47 -0
  201. data/app/services/collavre/creatives/index_query.rb +191 -0
  202. data/app/services/collavre/creatives/path_exporter.rb +131 -0
  203. data/app/services/collavre/creatives/permission_cache_builder.rb +194 -0
  204. data/app/services/collavre/creatives/permission_checker.rb +42 -0
  205. data/app/services/collavre/creatives/plan_tagger.rb +53 -0
  206. data/app/services/collavre/creatives/progress_service.rb +89 -0
  207. data/app/services/collavre/creatives/reorderer.rb +187 -0
  208. data/app/services/collavre/creatives/tree_builder.rb +231 -0
  209. data/app/services/collavre/creatives/tree_formatter.rb +36 -0
  210. data/app/services/collavre/gemini_parent_recommender.rb +77 -0
  211. data/app/services/collavre/github/client.rb +112 -0
  212. data/app/services/collavre/github/pull_request_analyzer.rb +280 -0
  213. data/app/services/collavre/github/pull_request_processor.rb +181 -0
  214. data/app/services/collavre/github/webhook_provisioner.rb +130 -0
  215. data/app/services/collavre/google_calendar_service.rb +149 -0
  216. data/app/services/collavre/link_preview_fetcher.rb +230 -0
  217. data/app/services/collavre/markdown_importer.rb +202 -0
  218. data/app/services/collavre/mcp_service.rb +217 -0
  219. data/app/services/collavre/notion_client.rb +231 -0
  220. data/app/services/collavre/notion_creative_exporter.rb +296 -0
  221. data/app/services/collavre/notion_service.rb +249 -0
  222. data/app/services/collavre/ppt_importer.rb +76 -0
  223. data/app/services/collavre/ruby_llm_interaction_logger.rb +34 -0
  224. data/app/services/collavre/system_events/context_builder.rb +41 -0
  225. data/app/services/collavre/system_events/dispatcher.rb +19 -0
  226. data/app/services/collavre/system_events/router.rb +72 -0
  227. data/app/services/collavre/tools/creative_retrieval_service.rb +138 -0
  228. data/app/views/admin/shared/_tabs.html.erb +4 -0
  229. data/app/views/collavre/comments/_activity_log_details.html.erb +23 -0
  230. data/app/views/collavre/comments/_comment.html.erb +147 -0
  231. data/app/views/collavre/comments/_comments_popup.html.erb +46 -0
  232. data/app/views/collavre/comments/_list.html.erb +10 -0
  233. data/app/views/collavre/comments/_presence_avatars.html.erb +8 -0
  234. data/app/views/collavre/comments/_reaction_picker.html.erb +15 -0
  235. data/app/views/collavre/comments/_read_receipts.html.erb +19 -0
  236. data/app/views/collavre/creatives/_add_button.html.erb +20 -0
  237. data/app/views/collavre/creatives/_delete_button.html.erb +12 -0
  238. data/app/views/collavre/creatives/_github_integration_modal.html.erb +77 -0
  239. data/app/views/collavre/creatives/_import_upload_zone.html.erb +10 -0
  240. data/app/views/collavre/creatives/_inline_edit_form.html.erb +89 -0
  241. data/app/views/collavre/creatives/_mobile_actions_menu.html.erb +24 -0
  242. data/app/views/collavre/creatives/_notion_integration_modal.html.erb +90 -0
  243. data/app/views/collavre/creatives/_set_plan_modal.html.erb +25 -0
  244. data/app/views/collavre/creatives/_share_button.html.erb +55 -0
  245. data/app/views/collavre/creatives/edit.html.erb +4 -0
  246. data/app/views/collavre/creatives/index.html.erb +192 -0
  247. data/app/views/collavre/creatives/new.html.erb +4 -0
  248. data/app/views/collavre/creatives/show.html.erb +29 -0
  249. data/app/views/collavre/creatives/slide_view.html.erb +20 -0
  250. data/app/views/collavre/email_verification_mailer/verify.html.erb +4 -0
  251. data/app/views/collavre/email_verification_mailer/verify.text.erb +2 -0
  252. data/app/views/collavre/emails/index.html.erb +19 -0
  253. data/app/views/collavre/emails/show.html.erb +5 -0
  254. data/app/views/collavre/inbox_items/_item.html.erb +18 -0
  255. data/app/views/collavre/inbox_items/_items.html.erb +3 -0
  256. data/app/views/collavre/inbox_items/_list.html.erb +7 -0
  257. data/app/views/collavre/inbox_items/index.html.erb +1 -0
  258. data/app/views/collavre/inbox_mailer/daily_summary.html.erb +11 -0
  259. data/app/views/collavre/inbox_mailer/daily_summary.text.erb +8 -0
  260. data/app/views/collavre/invitation_mailer/invite.html.erb +5 -0
  261. data/app/views/collavre/invitation_mailer/invite.text.erb +3 -0
  262. data/app/views/collavre/invites/show.html.erb +8 -0
  263. data/app/views/collavre/passwords/edit.html.erb +12 -0
  264. data/app/views/collavre/passwords/new.html.erb +17 -0
  265. data/app/views/collavre/passwords_mailer/reset.html.erb +4 -0
  266. data/app/views/collavre/passwords_mailer/reset.text.erb +2 -0
  267. data/app/views/collavre/sessions/new.html.erb +27 -0
  268. data/app/views/collavre/sessions/providers/_google.html.erb +7 -0
  269. data/app/views/collavre/sessions/providers/_passkey.html.erb +3 -0
  270. data/app/views/collavre/shared/_link_creative_modal.html.erb +6 -0
  271. data/app/views/collavre/shared/_navigation.html.erb +37 -0
  272. data/app/views/collavre/shared/navigation/_help_button.html.erb +1 -0
  273. data/app/views/collavre/shared/navigation/_inbox_button.html.erb +5 -0
  274. data/app/views/collavre/shared/navigation/_mobile_inbox_button.html.erb +3 -0
  275. data/app/views/collavre/shared/navigation/_mobile_plans_button.html.erb +1 -0
  276. data/app/views/collavre/shared/navigation/_panels.html.erb +18 -0
  277. data/app/views/collavre/shared/navigation/_plans_button.html.erb +3 -0
  278. data/app/views/collavre/shared/navigation/_search_form.html.erb +6 -0
  279. data/app/views/collavre/user_themes/index.html.erb +75 -0
  280. data/app/views/collavre/users/_app_header.html.erb +5 -0
  281. data/app/views/collavre/users/_contact_management.html.erb +77 -0
  282. data/app/views/collavre/users/_id_pwd_fields.html.erb +27 -0
  283. data/app/views/collavre/users/edit_ai.html.erb +79 -0
  284. data/app/views/collavre/users/edit_password.html.erb +27 -0
  285. data/app/views/collavre/users/index.html.erb +80 -0
  286. data/app/views/collavre/users/new.html.erb +33 -0
  287. data/app/views/collavre/users/new_ai.html.erb +87 -0
  288. data/app/views/collavre/users/passkeys.html.erb +43 -0
  289. data/app/views/collavre/users/show.html.erb +143 -0
  290. data/app/views/inbox/badge_component/_count.html.erb +2 -0
  291. data/app/views/layouts/collavre/slide.html.erb +29 -0
  292. data/config/locales/comments.en.yml +114 -0
  293. data/config/locales/comments.ko.yml +110 -0
  294. data/config/locales/contacts.en.yml +16 -0
  295. data/config/locales/contacts.ko.yml +16 -0
  296. data/config/locales/creatives.en.yml +183 -0
  297. data/config/locales/creatives.ko.yml +164 -0
  298. data/config/locales/invites.en.yml +19 -0
  299. data/config/locales/invites.ko.yml +17 -0
  300. data/config/locales/notifications.en.yml +8 -0
  301. data/config/locales/notifications.ko.yml +8 -0
  302. data/config/locales/plans.en.yml +12 -0
  303. data/config/locales/plans.ko.yml +19 -0
  304. data/config/locales/themes.en.yml +29 -0
  305. data/config/locales/themes.ko.yml +27 -0
  306. data/config/locales/users.en.yml +151 -0
  307. data/config/locales/users.ko.yml +146 -0
  308. data/config/routes.rb +92 -0
  309. data/db/migrate/20241201000000_create_notion_integrations.rb +29 -0
  310. data/db/migrate/20250128110017_create_creatives.rb +11 -0
  311. data/db/migrate/20250128120122_create_users.rb +11 -0
  312. data/db/migrate/20250128120123_create_sessions.rb +11 -0
  313. data/db/migrate/20250128123633_create_subscribers.rb +10 -0
  314. data/db/migrate/20250312000000_create_notion_block_links.rb +16 -0
  315. data/db/migrate/20250312010000_allow_multiple_notion_blocks_per_creative.rb +5 -0
  316. data/db/migrate/20250522115048_remove_name_from_creatives.rb +5 -0
  317. data/db/migrate/20250522190651_add_parent_id_to_creatives.rb +5 -0
  318. data/db/migrate/20250523133100_rename_inventory_count_to_progress.rb +13 -0
  319. data/db/migrate/20250523133101_add_sequence_to_creatives.rb +5 -0
  320. data/db/migrate/20250525205100_add_user_id_to_creatives.rb +10 -0
  321. data/db/migrate/20250527014217_create_creative_shares.rb +12 -0
  322. data/db/migrate/20250528142349_add_origin_id_to_creatives.rb +5 -0
  323. data/db/migrate/20250530060200_create_tags.rb +9 -0
  324. data/db/migrate/20250531105150_add_value_to_tags.rb +5 -0
  325. data/db/migrate/20250531140142_create_labels.rb +12 -0
  326. data/db/migrate/20250531140145_change_tag_to_label_reference.rb +6 -0
  327. data/db/migrate/20250601000000_create_comments.rb +10 -0
  328. data/db/migrate/20250601061830_drop_plans_and_variations.rb +6 -0
  329. data/db/migrate/20250604122600_add_owner_to_labels.rb +5 -0
  330. data/db/migrate/20250606000000_rename_email_address_to_email_in_users.rb +7 -0
  331. data/db/migrate/20250606150329_create_creative_hierarchies.rb +16 -0
  332. data/db/migrate/20250610142000_create_creative_expanded_states.rb +11 -0
  333. data/db/migrate/20250611030138_create_invitations.rb +15 -0
  334. data/db/migrate/20250611105524_create_inbox_items.rb +14 -0
  335. data/db/migrate/20250612150000_update_permissions.rb +43 -0
  336. data/db/migrate/20250612232913_add_email_verified_at_to_users.rb +5 -0
  337. data/db/migrate/20250616065905_add_avatar_to_users.rb +5 -0
  338. data/db/migrate/20250617092111_add_display_level_to_users.rb +5 -0
  339. data/db/migrate/20250620004558_create_emails.rb +13 -0
  340. data/db/migrate/20250621000000_create_comment_read_pointers.rb +11 -0
  341. data/db/migrate/20250622000000_add_completion_mark_to_users.rb +5 -0
  342. data/db/migrate/20250623000000_add_theme_to_users.rb +5 -0
  343. data/db/migrate/20250624000000_add_name_to_users.rb +18 -0
  344. data/db/migrate/20250714190000_create_devices.rb +17 -0
  345. data/db/migrate/20250715120000_add_notifications_enabled_to_users.rb +5 -0
  346. data/db/migrate/20250823000000_add_no_access_permission.rb +25 -0
  347. data/db/migrate/20250826000000_add_calendar_id_to_users.rb +5 -0
  348. data/db/migrate/20250827000000_add_google_oauth_tokens_to_users.rb +8 -0
  349. data/db/migrate/20250827061238_add_timezone_to_users.rb +5 -0
  350. data/db/migrate/20250828000000_create_calendar_events.rb +16 -0
  351. data/db/migrate/20250828060000_add_creative_to_calendar_events.rb +5 -0
  352. data/db/migrate/20250830141052_add_locale_to_users.rb +5 -0
  353. data/db/migrate/20250830141101_add_message_key_to_inbox_items.rb +6 -0
  354. data/db/migrate/20250902025423_remove_description_and_featured_image_from_creatives.rb +6 -0
  355. data/db/migrate/20250910000000_add_private_to_comments.rb +5 -0
  356. data/db/migrate/20250910105640_migrate_write_permissions_to_admin.rb +17 -0
  357. data/db/migrate/20250911084338_backfill_creative_in_comment_inbox_items.rb +27 -0
  358. data/db/migrate/20250923002959_deduplicate_device_fcm_tokens.rb +25 -0
  359. data/db/migrate/20250924000000_add_system_admin_to_users.rb +6 -0
  360. data/db/migrate/20250925000000_create_github_integrations.rb +26 -0
  361. data/db/migrate/20250927000000_add_webhook_secret_to_github_repository_links.rb +29 -0
  362. data/db/migrate/20250928000000_add_action_and_approver_to_comments.rb +6 -0
  363. data/db/migrate/20250928010000_add_action_execution_tracking_to_comments.rb +6 -0
  364. data/db/migrate/20250928105957_add_github_gemini_prompt_to_creatives.rb +5 -0
  365. data/db/migrate/20250929000000_add_comment_and_creative_refs_to_inbox_items.rb +6 -0
  366. data/db/migrate/20251001000001_create_contacts.rb +71 -0
  367. data/db/migrate/20251002000000_add_shared_by_to_creative_shares.rb +14 -0
  368. data/db/migrate/20251124120902_add_ai_fields_to_users.rb +7 -0
  369. data/db/migrate/20251124122218_add_created_by_id_to_users.rb +5 -0
  370. data/db/migrate/20251124124521_add_llm_api_key_to_users.rb +5 -0
  371. data/db/migrate/20251124130000_add_searchable_to_users.rb +6 -0
  372. data/db/migrate/20251125072705_migrate_linked_creative_children_to_origin.rb +28 -0
  373. data/db/migrate/20251126040752_add_description_to_creatives.rb +75 -0
  374. data/db/migrate/20251127000000_move_comments_from_linked_creatives_to_origins.rb +18 -0
  375. data/db/migrate/20251201073823_add_tools_to_users.rb +5 -0
  376. data/db/migrate/20251202062715_create_mcp_tools.rb +16 -0
  377. data/db/migrate/20251204125754_create_tasks_and_task_actions.rb +23 -0
  378. data/db/migrate/20251204125756_add_routing_expression_to_users.rb +5 -0
  379. data/db/migrate/20251204161133_set_default_routing_expression_for_ai_agents.rb +13 -0
  380. data/db/migrate/20251211033025_nullify_self_referencing_origins.rb +9 -0
  381. data/db/migrate/20251211080040_create_topics_and_add_topic_to_comments.rb +15 -0
  382. data/db/migrate/20251215143500_create_activity_logs.rb +16 -0
  383. data/db/migrate/20251222125727_create_webauthn_credentials.rb +14 -0
  384. data/db/migrate/20251222125839_add_webauthn_id_to_users.rb +5 -0
  385. data/db/migrate/20251223022315_create_comment_reactions.rb +13 -0
  386. data/db/migrate/20251223072625_create_user_themes.rb +11 -0
  387. data/db/migrate/20251230074456_add_creative_id_to_labels.rb +5 -0
  388. data/db/migrate/20251230113607_refactor_labels.rb +38 -0
  389. data/db/migrate/20251231010012_backfill_tags_for_labels.rb +15 -0
  390. data/db/migrate/20251231013234_drop_subscribers.rb +9 -0
  391. data/db/migrate/20260106090544_allow_null_user_id_in_creative_shares.rb +6 -0
  392. data/db/migrate/20260106160643_create_system_settings.rb +11 -0
  393. data/db/migrate/20260116000000_create_creative_shares_cache.rb +15 -0
  394. data/db/migrate/20260116000001_populate_creative_shares_cache.rb +23 -0
  395. data/db/migrate/20260119022933_make_source_share_id_nullable_in_creative_shares_caches.rb +7 -0
  396. data/db/migrate/20260119023446_populate_owner_cache_entries.rb +25 -0
  397. data/db/migrate/20260119100000_add_account_lockout_to_users.rb +6 -0
  398. data/db/migrate/20260119110000_add_last_active_at_to_sessions.rb +6 -0
  399. data/db/migrate/20260120045354_encrypt_oauth_tokens.rb +60 -0
  400. data/db/migrate/20260120162259_remove_fk_from_creative_shares_caches.rb +7 -0
  401. data/db/migrate/20260120163856_remove_timestamps_from_creative_shares_caches.rb +6 -0
  402. data/lib/collavre/configuration.rb +14 -0
  403. data/lib/collavre/engine.rb +77 -0
  404. data/lib/collavre/user_extensions.rb +29 -0
  405. data/lib/collavre/version.rb +3 -0
  406. data/lib/collavre.rb +26 -0
  407. data/lib/generators/collavre/install/install_generator.rb +105 -0
  408. data/lib/generators/collavre/install/templates/build.cjs.tt +100 -0
  409. data/lib/tasks/collavre_assets.rake +15 -0
  410. metadata +591 -0
@@ -0,0 +1,262 @@
1
+ module Collavre
2
+ require "json"
3
+
4
+ module Comments
5
+ class ActionExecutor
6
+ class ExecutionError < StandardError; end
7
+
8
+ def initialize(comment:, executor:)
9
+ @comment = comment
10
+ @executor = executor
11
+ end
12
+
13
+ def call
14
+ comment.with_lock do
15
+ prepare_for_execution!
16
+
17
+ execute_within_transaction!
18
+ end
19
+ rescue ExecutionError
20
+ raise
21
+ rescue StandardError, ScriptError => e
22
+ Rails.logger.error("Comment action execution failed: #{e.class} #{e.message}")
23
+ raise ExecutionError, I18n.t("collavre.comments.approve_execution_failed", message: e.message)
24
+ end
25
+
26
+ private
27
+
28
+ attr_reader :comment, :executor
29
+
30
+ def prepare_for_execution!
31
+ comment.reload
32
+
33
+ status = comment.approval_status(executor)
34
+ if status != :ok
35
+ error_key = case status
36
+ when :invalid_action_format then "collavre.comments.approve_invalid_format"
37
+ when :missing_action then "collavre.comments.approve_missing_action"
38
+ when :missing_approver then "collavre.comments.approve_missing_approver"
39
+ when :admin_required then "collavre.comments.approve_admin_required"
40
+ else "collavre.comments.approve_not_allowed"
41
+ end
42
+ raise ExecutionError, I18n.t(error_key)
43
+ end
44
+
45
+ if comment.action_executed_at.present?
46
+ raise ExecutionError, I18n.t("collavre.comments.approve_already_executed")
47
+ end
48
+ end
49
+
50
+ def execute_within_transaction!
51
+ ApplicationRecord.transaction do
52
+ execute_action!
53
+ mark_execution_completed!
54
+ end
55
+ end
56
+
57
+ def mark_execution_completed!
58
+ comment.action_executed_at = Time.current
59
+ comment.action_executed_by = executor
60
+ comment.approver = executor if comment.approver != executor
61
+ comment.save!
62
+ end
63
+
64
+ def execute_action!
65
+ ExecutionContext.new(comment).evaluate(comment.action)
66
+ rescue ExecutionContext::InvalidActionError => e
67
+ raise ExecutionError, e.message
68
+ end
69
+
70
+ class ExecutionContext
71
+ class InvalidActionError < StandardError; end
72
+
73
+ SUPPORTED_ACTIONS = {
74
+ "create_creative" => :create_creative,
75
+ "update_creative" => :update_creative,
76
+ "approve_tool" => :approve_tool
77
+ }.freeze
78
+
79
+ CREATIVE_ATTRIBUTES = %w[description progress].freeze
80
+
81
+ def initialize(comment)
82
+ @comment = comment
83
+ end
84
+
85
+ attr_reader :comment
86
+
87
+ def evaluate(code)
88
+ payload = parse_payload(code)
89
+
90
+ actions = Array(payload["actions"])
91
+ if actions.present?
92
+ Comment.transaction do
93
+ actions.each do |action_payload|
94
+ process_action(action_payload)
95
+ end
96
+ end
97
+ else
98
+ process_action(payload)
99
+ end
100
+ end
101
+
102
+ private
103
+
104
+ def parse_payload(code)
105
+ raise InvalidActionError, I18n.t("collavre.comments.approve_missing_action") if code.blank?
106
+
107
+ payload = JSON.parse(code)
108
+ unless payload.is_a?(Hash)
109
+ raise InvalidActionError, I18n.t("collavre.comments.approve_invalid_format")
110
+ end
111
+
112
+ deep_stringify_keys(payload)
113
+ rescue JSON::ParserError
114
+ raise InvalidActionError, I18n.t("collavre.comments.approve_invalid_format")
115
+ end
116
+
117
+ def deep_stringify_keys(value)
118
+ case value
119
+ when Hash
120
+ value.each_with_object({}) do |(key, inner_value), acc|
121
+ acc[key.to_s] = deep_stringify_keys(inner_value)
122
+ end
123
+ when Array
124
+ value.map { |item| deep_stringify_keys(item) }
125
+ else
126
+ value
127
+ end
128
+ end
129
+
130
+ def create_creative(payload)
131
+ attributes = extract_attributes(payload)
132
+ parent = parent_creative_for(payload)
133
+
134
+ new_creative = parent.children.build
135
+ new_creative.user = parent.user || comment.user || Current.user
136
+ assign_creative_attributes(new_creative, attributes)
137
+ new_creative.save!
138
+ rescue ActiveRecord::RecordInvalid => e
139
+ raise InvalidActionError, e.record.errors.full_messages.to_sentence
140
+ end
141
+
142
+ def update_creative(payload)
143
+ creative = find_target_creative(payload)
144
+ attributes = extract_attributes(payload)
145
+
146
+ assign_creative_attributes(creative, attributes)
147
+ creative.save!
148
+ rescue ActiveRecord::RecordInvalid => e
149
+ raise InvalidActionError, e.record.errors.full_messages.to_sentence
150
+ end
151
+
152
+ def approve_tool(payload)
153
+ tool_name = payload["tool_name"]
154
+ raise InvalidActionError, "Tool name is required" if tool_name.blank?
155
+
156
+ tool = McpTool.find_by(creative: comment.creative, name: tool_name)
157
+ raise InvalidActionError, "Tool '#{tool_name}' not found" unless tool
158
+
159
+ tool.approve!
160
+ end
161
+
162
+ def process_action(payload)
163
+ unless payload.is_a?(Hash)
164
+ raise InvalidActionError, I18n.t("collavre.comments.approve_invalid_format")
165
+ end
166
+
167
+ action = payload["action"] || payload["type"]
168
+ raise InvalidActionError, I18n.t("collavre.comments.approve_missing_action") if action.blank?
169
+
170
+ handler = SUPPORTED_ACTIONS[action]
171
+ unless handler
172
+ raise InvalidActionError, I18n.t("collavre.comments.approve_unsupported_action", action: action)
173
+ end
174
+
175
+ send(handler, payload)
176
+ end
177
+
178
+ def extract_attributes(payload)
179
+ attributes = payload["attributes"]
180
+ unless attributes.is_a?(Hash)
181
+ raise InvalidActionError, I18n.t("collavre.comments.approve_invalid_attributes")
182
+ end
183
+
184
+ sanitized = attributes.slice(*CREATIVE_ATTRIBUTES)
185
+ if sanitized.empty?
186
+ raise InvalidActionError, I18n.t("collavre.comments.approve_no_attributes")
187
+ end
188
+
189
+ deep_stringify_keys(validate_attribute_types(sanitized))
190
+ end
191
+
192
+ def validate_attribute_types(attributes)
193
+ attributes.each do |key, value|
194
+ case key.to_s
195
+ when "description"
196
+ unless value.is_a?(String)
197
+ raise InvalidActionError, I18n.t("collavre.comments.approve_invalid_description")
198
+ end
199
+ when "progress"
200
+ unless value.is_a?(Numeric)
201
+ raise InvalidActionError, I18n.t("collavre.comments.approve_invalid_progress")
202
+ end
203
+ end
204
+ end
205
+
206
+ attributes
207
+ end
208
+
209
+ def assign_creative_attributes(record, attributes)
210
+ assignable = attributes.except("description")
211
+ record.assign_attributes(assignable)
212
+ if attributes.key?("description")
213
+ record.description = attributes["description"]
214
+ end
215
+ end
216
+
217
+ def find_target_creative(payload)
218
+ creative_id = payload["creative_id"]
219
+ return comment.creative if creative_id.blank?
220
+
221
+ creative = find_creative_in_comment_tree(creative_id)
222
+ unless creative
223
+ raise InvalidActionError, I18n.t("collavre.comments.approve_invalid_creative")
224
+ end
225
+
226
+ creative
227
+ end
228
+
229
+ def parent_creative_for(payload)
230
+ parent_id = payload["parent_id"]
231
+ return comment.creative if parent_id.blank?
232
+
233
+ creative = find_creative_in_comment_tree(parent_id)
234
+ unless creative
235
+ raise InvalidActionError, I18n.t("collavre.comments.approve_invalid_creative")
236
+ end
237
+
238
+ creative
239
+ end
240
+
241
+ def find_creative_in_comment_tree(creative_id)
242
+ id = creative_id.to_i
243
+ return if id <= 0
244
+
245
+ creative = Creative.find_by(id: id)
246
+ return unless creative
247
+
248
+ return creative if allowed_creative_ids.include?(creative.id)
249
+
250
+ origin = creative.effective_origin
251
+ return origin if origin && allowed_creative_ids.include?(origin.id)
252
+
253
+ nil
254
+ end
255
+
256
+ def allowed_creative_ids
257
+ @allowed_creative_ids ||= comment.creative.self_and_descendants.pluck(:id)
258
+ end
259
+ end
260
+ end
261
+ end
262
+ end
@@ -0,0 +1,58 @@
1
+ module Collavre
2
+ module Comments
3
+ class ActionValidator
4
+ class ValidationError < StandardError; end
5
+
6
+ def initialize(comment:)
7
+ @comment = comment
8
+ end
9
+
10
+ def validate!(code)
11
+ context = ActionExecutor::ExecutionContext.new(comment)
12
+ payload = context.send(:parse_payload, code)
13
+ validate_payload(context, payload)
14
+ payload
15
+ rescue ActionExecutor::ExecutionContext::InvalidActionError => e
16
+ raise ValidationError, e.message
17
+ end
18
+
19
+ private
20
+
21
+ attr_reader :comment
22
+
23
+ def validate_payload(context, payload)
24
+ actions = payload["actions"]
25
+ if actions.present?
26
+ Array(actions).each do |action_payload|
27
+ validate_single_action(context, action_payload)
28
+ end
29
+ else
30
+ validate_single_action(context, payload)
31
+ end
32
+ end
33
+
34
+ def validate_single_action(context, payload)
35
+ unless payload.is_a?(Hash)
36
+ raise ValidationError, I18n.t("collavre.comments.approve_invalid_format")
37
+ end
38
+
39
+ action = payload["action"] || payload["type"]
40
+ raise ValidationError, I18n.t("collavre.comments.approve_missing_action") if action.blank?
41
+
42
+ handler = ActionExecutor::ExecutionContext::SUPPORTED_ACTIONS[action]
43
+ unless handler
44
+ raise ValidationError, I18n.t("collavre.comments.approve_unsupported_action", action: action)
45
+ end
46
+
47
+ case handler
48
+ when :create_creative
49
+ context.send(:extract_attributes, payload)
50
+ context.send(:parent_creative_for, payload)
51
+ when :update_creative
52
+ context.send(:extract_attributes, payload)
53
+ context.send(:find_target_creative, payload)
54
+ end
55
+ end
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,97 @@
1
+ module Collavre
2
+ module Comments
3
+ class CalendarCommand
4
+ def initialize(comment:, user:, url_helpers: Collavre::Engine.routes.url_helpers)
5
+ @comment = comment
6
+ @user = user
7
+ @creative = comment.creative.effective_origin
8
+ @url_helpers = url_helpers
9
+ end
10
+
11
+ def call
12
+ return unless calendar_command?
13
+
14
+ create_event
15
+ rescue StandardError => e
16
+ Rails.logger.error("Calendar command failed: #{e.message}")
17
+ e.message
18
+ end
19
+
20
+ private
21
+
22
+ attr_reader :comment, :user, :creative, :url_helpers
23
+
24
+ COMMAND_PATTERN = /\A\/(?:calendar|cal)\b/i.freeze
25
+
26
+ def calendar_command?
27
+ comment.content.to_s.strip.match?(COMMAND_PATTERN)
28
+ end
29
+
30
+ def parsed_args
31
+ @parsed_args ||= begin
32
+ args = command_body
33
+ args = args.sub(/\A(today)\b/i, Time.zone.today.to_s) if args.match?(/\Atoday\b/i)
34
+ match = args.match(/\A(?:(\d{4}-\d{2}-\d{2}))?(?:@(\d{2}:\d{2}))?(?:\s+(.*))?\z/)
35
+ return unless match
36
+ return unless match[1].present? || match[2].present?
37
+
38
+ {
39
+ date: match[1],
40
+ time: match[2],
41
+ memo: match[3]
42
+ }
43
+ end
44
+ end
45
+
46
+ def command_body
47
+ comment.content.to_s.strip.sub(/\A\S+/, "").strip
48
+ end
49
+
50
+ def create_event
51
+ data = parsed_args
52
+ return unless data
53
+
54
+ timezone = Time.zone
55
+ start_time, end_time = calculate_times(timezone, data[:date], data[:time])
56
+ summary = build_summary(data[:memo])
57
+ calendar_id = comment.user&.calendar_id.presence || "primary"
58
+
59
+ event = GoogleCalendarService.new(user: user).create_event(
60
+ calendar_id: calendar_id,
61
+ start_time: start_time,
62
+ end_time: end_time,
63
+ summary: summary,
64
+ description: event_description,
65
+ timezone: timezone.tzinfo.name,
66
+ all_day: data[:time].nil?,
67
+ creative: creative
68
+ )
69
+
70
+ I18n.t("collavre.comments.calendar_command.event_created", url: event.html_link)
71
+ end
72
+
73
+ def calculate_times(timezone, date_str, time_str)
74
+ if time_str
75
+ date_for_time = date_str.presence || timezone.today.to_s
76
+ start_time = timezone.parse("#{date_for_time} #{time_str}")
77
+ [ start_time, start_time ]
78
+ else
79
+ start_time = Date.parse(date_str.presence || timezone.today.to_s)
80
+ [ start_time, start_time ]
81
+ end
82
+ end
83
+
84
+ def build_summary(memo)
85
+ return memo if memo.present?
86
+
87
+ base_summary = creative.effective_description(false, false)
88
+ base_summary
89
+ end
90
+
91
+ def event_description
92
+ defaults = Rails.application.config.action_mailer.default_url_options || {}
93
+ url_helpers.creative_url(creative, defaults.merge(comment_id: comment.id))
94
+ end
95
+ end
96
+ end
97
+ end
@@ -0,0 +1,37 @@
1
+ module Collavre
2
+ module Comments
3
+ class CommandProcessor
4
+ def initialize(comment:, user:)
5
+ @comment = comment
6
+ @user = user
7
+ end
8
+
9
+ def call
10
+ command_handlers.each do |command|
11
+ result = command.call
12
+ return result if result.present?
13
+ end
14
+ nil
15
+ rescue StandardError => e
16
+ Rails.logger.error("Comment command processing failed: #{e.message}")
17
+ e.message
18
+ end
19
+
20
+ private
21
+
22
+ attr_reader :comment, :user
23
+
24
+ def command_handlers
25
+ static_commands + mcp_commands
26
+ end
27
+
28
+ def static_commands
29
+ [ Collavre::Comments::CalendarCommand.new(comment: comment, user: user) ]
30
+ end
31
+
32
+ def mcp_commands
33
+ Collavre::Comments::McpCommandBuilder.new(comment: comment, user: user).commands
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,109 @@
1
+ module Collavre
2
+ require "json"
3
+
4
+ module Comments
5
+ class McpCommand
6
+ def initialize(comment:, user:, tool:, meta_tool_service: default_meta_tool_service)
7
+ @comment = comment
8
+ @user = user
9
+ @tool = tool
10
+ @meta_tool_service = meta_tool_service
11
+ end
12
+
13
+ def call
14
+ return unless command_match?
15
+
16
+ arguments = parsed_arguments
17
+ return usage_message unless arguments
18
+
19
+ response = meta_tool_service.call(action: "run", tool_name: tool_name, arguments: arguments)
20
+ format_response(response)
21
+ rescue StandardError => e
22
+ Rails.logger.error("MCP command '#{tool_name}' failed: #{e.message}")
23
+ e.message
24
+ end
25
+
26
+ private
27
+
28
+ attr_reader :comment, :user, :tool, :meta_tool_service
29
+
30
+ def command_match?
31
+ comment.content.to_s.strip.match?(command_pattern)
32
+ end
33
+
34
+ def command_pattern
35
+ @command_pattern ||= /\A\/#{Regexp.escape(tool_name)}\b/i
36
+ end
37
+
38
+ def command_body
39
+ comment.content.to_s.strip.sub(command_pattern, "").strip
40
+ end
41
+
42
+ def parsed_arguments
43
+ body = command_body
44
+ return {} if body.blank?
45
+
46
+ JSON.parse(body)
47
+ rescue JSON::ParserError
48
+ key_value_arguments(body)
49
+ end
50
+
51
+ def key_value_arguments(body)
52
+ args = body.split(/\s+/).each_with_object({}) do |pair, result|
53
+ key, value = pair.split("=", 2)
54
+ next if key.blank? || value.nil?
55
+
56
+ result[key] = cast_value(value)
57
+ end
58
+
59
+ return args if args.present?
60
+
61
+ nil
62
+ end
63
+
64
+ def cast_value(value)
65
+ case value
66
+ when /\A\d+\z/ then value.to_i
67
+ when /\A\d+\.\d+\z/ then value.to_f
68
+ when /\Atrue\z/i then true
69
+ when /\Afalse\z/i then false
70
+ else
71
+ value
72
+ end
73
+ end
74
+
75
+ def usage_message
76
+ params = Array(tool[:params]).map { |param| param[:name] }.join(" ").presence
77
+ usage = [ tool_name, params ].compact.join(" ")
78
+ "Usage: /#{usage}"
79
+ end
80
+
81
+ def format_response(response)
82
+ # TODO: i18n text
83
+ return "Error running /#{tool_name}: #{response[:error]}" if response[:error].present?
84
+
85
+ result = response[:result]
86
+ content = case result
87
+ when Hash, Array
88
+ JSON.pretty_generate(result)
89
+ else
90
+ result.to_s
91
+ end
92
+
93
+ <<~HTML
94
+ <details><summary>#{tool_name} response</summary>
95
+ <pre><code>#{content}</code></pre>
96
+ </details>
97
+ HTML
98
+ end
99
+
100
+ def tool_name
101
+ tool[:name]
102
+ end
103
+
104
+ def default_meta_tool_service
105
+ ::Tools::MetaToolService.new
106
+ end
107
+ end
108
+ end
109
+ end
@@ -0,0 +1,32 @@
1
+ module Collavre
2
+ module Comments
3
+ class McpCommandBuilder
4
+ def initialize(comment:, user:, logger: Rails.logger)
5
+ @comment = comment
6
+ @user = user
7
+ @logger = logger
8
+ end
9
+
10
+ def commands
11
+ return [] unless defined?(RailsMcpEngine)
12
+
13
+ RailsMcpEngine::Engine.build_tools!
14
+ tools = meta_tool_service.call(action: "list", tool_name: nil, query: nil, arguments: nil)
15
+ Array(tools[:tools]).map do |tool|
16
+ Collavre::Comments::McpCommand.new(comment: comment, user: user, tool: tool, meta_tool_service: meta_tool_service)
17
+ end
18
+ rescue StandardError => e
19
+ logger.error("MCP command registration failed: #{e.message}")
20
+ []
21
+ end
22
+
23
+ private
24
+
25
+ attr_reader :comment, :user, :logger
26
+
27
+ def meta_tool_service
28
+ @meta_tool_service ||= ::Tools::MetaToolService.new
29
+ end
30
+ end
31
+ end
32
+ end