collavre 0.20.3 → 0.22.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 (163) hide show
  1. checksums.yaml +4 -4
  2. data/app/assets/stylesheets/collavre/actiontext.css +92 -2
  3. data/app/assets/stylesheets/collavre/code_highlight.css +144 -26
  4. data/app/assets/stylesheets/collavre/comments_popup.css +133 -2
  5. data/app/assets/stylesheets/collavre/landing.css +507 -0
  6. data/app/assets/stylesheets/collavre/popup.css +148 -0
  7. data/app/channels/collavre/agent_channel.rb +205 -0
  8. data/app/channels/collavre/comments_presence_channel.rb +7 -0
  9. data/app/controllers/collavre/admin/integrations_controller.rb +93 -0
  10. data/app/controllers/collavre/admin/settings_controller.rb +22 -17
  11. data/app/controllers/collavre/api/v1/agents_controller.rb +777 -0
  12. data/app/controllers/collavre/api/v1/base_controller.rb +46 -0
  13. data/app/controllers/collavre/application_controller.rb +27 -0
  14. data/app/controllers/collavre/attachments_controller.rb +30 -2
  15. data/app/controllers/collavre/channels_controller.rb +23 -0
  16. data/app/controllers/collavre/comments_controller.rb +1 -1
  17. data/app/controllers/collavre/creatives/attachments_controller.rb +79 -0
  18. data/app/controllers/collavre/creatives_controller.rb +141 -7
  19. data/app/controllers/collavre/landing_controller.rb +8 -0
  20. data/app/controllers/collavre/public_assets_controller.rb +24 -0
  21. data/app/controllers/collavre/tasks_controller.rb +12 -4
  22. data/app/controllers/collavre/topics_controller.rb +36 -30
  23. data/app/controllers/concerns/collavre/comments/approval_actions.rb +57 -14
  24. data/app/helpers/collavre/comments_helper.rb +7 -0
  25. data/app/helpers/collavre/public_assets_helper.rb +14 -0
  26. data/app/javascript/components/InlineLexicalEditor.jsx +42 -59
  27. data/app/javascript/components/plugins/attachment_cleanup_plugin.jsx +3 -0
  28. data/app/javascript/components/plugins/image_upload_plugin.jsx +12 -0
  29. data/app/javascript/controllers/__tests__/link_creative_controller.test.js +447 -0
  30. data/app/javascript/controllers/comment_controller.js +15 -1
  31. data/app/javascript/controllers/comments/__tests__/presence_controller.test.js +108 -0
  32. data/app/javascript/controllers/comments/form_controller.js +4 -0
  33. data/app/javascript/controllers/comments/list_controller.js +27 -9
  34. data/app/javascript/controllers/comments/popup_controller.js +9 -0
  35. data/app/javascript/controllers/comments/presence_controller.js +137 -4
  36. data/app/javascript/controllers/comments/topics_controller.js +15 -0
  37. data/app/javascript/controllers/creatives/__tests__/sync_controller.test.js +89 -0
  38. data/app/javascript/controllers/creatives/__tests__/tree_controller.test.js +120 -0
  39. data/app/javascript/controllers/creatives/sync_controller.js +30 -9
  40. data/app/javascript/controllers/creatives/tree_controller.js +23 -0
  41. data/app/javascript/controllers/index.js +4 -1
  42. data/app/javascript/controllers/landing_video_controller.js +53 -0
  43. data/app/javascript/controllers/link_creative_controller.js +451 -29
  44. data/app/javascript/creatives/tree_renderer.js +6 -0
  45. data/app/javascript/lib/api/__tests__/queue_manager.test.js +27 -0
  46. data/app/javascript/lib/api/creatives.js +13 -0
  47. data/app/javascript/lib/api/queue_manager.js +17 -5
  48. data/app/javascript/lib/lexical/__tests__/color_import.test.js +318 -0
  49. data/app/javascript/lib/lexical/__tests__/minimize_html.test.js +259 -0
  50. data/app/javascript/lib/lexical/color_import.js +186 -0
  51. data/app/javascript/lib/lexical/minimize_html.js +182 -0
  52. data/app/javascript/lib/lexical/video_node.jsx +96 -0
  53. data/app/javascript/modules/__tests__/command_args_form.test.js +103 -0
  54. data/app/javascript/modules/__tests__/html_content_empty.test.js +41 -0
  55. data/app/javascript/modules/__tests__/markdown_source_reconcile.test.js +70 -0
  56. data/app/javascript/modules/command_args_form.js +22 -4
  57. data/app/javascript/modules/command_menu.js +27 -0
  58. data/app/javascript/modules/creative_row_editor.js +227 -17
  59. data/app/javascript/modules/html_content_empty.js +12 -0
  60. data/app/javascript/modules/markdown_source_reconcile.js +53 -0
  61. data/app/jobs/collavre/ai_agent_job.rb +89 -3
  62. data/app/jobs/collavre/cancel_offline_delegated_tasks_job.rb +134 -0
  63. data/app/jobs/collavre/claude_channel_presence_job.rb +91 -0
  64. data/app/jobs/collavre/drop_trigger_job.rb +37 -8
  65. data/app/mailers/collavre/application_mailer.rb +1 -1
  66. data/app/models/collavre/agent_subscription.rb +52 -0
  67. data/app/models/collavre/channel/injected_message.rb +5 -0
  68. data/app/models/collavre/channel.rb +87 -0
  69. data/app/models/collavre/comment/claude_channel_permission.rb +145 -0
  70. data/app/models/collavre/comment.rb +70 -5
  71. data/app/models/collavre/creative/describable.rb +202 -3
  72. data/app/models/collavre/creative.rb +2 -0
  73. data/app/models/collavre/creative_share.rb +1 -0
  74. data/app/models/collavre/integration_setting.rb +35 -0
  75. data/app/models/collavre/preview_channel.rb +93 -0
  76. data/app/models/collavre/system_setting.rb +13 -2
  77. data/app/models/collavre/task.rb +34 -5
  78. data/app/models/collavre/topic.rb +8 -25
  79. data/app/models/collavre/user.rb +4 -0
  80. data/app/models/concerns/collavre/ai_agent_resolvable.rb +12 -3
  81. data/app/services/collavre/agent_session_abort.rb +28 -0
  82. data/app/services/collavre/ai_agent/claude_channel_adapter.rb +79 -0
  83. data/app/services/collavre/ai_agent/response_finalizer.rb +4 -2
  84. data/app/services/collavre/ai_agent_service.rb +68 -49
  85. data/app/services/collavre/ai_client.rb +3 -3
  86. data/app/services/collavre/attachment_backfill.rb +26 -0
  87. data/app/services/collavre/channel_attacher.rb +58 -0
  88. data/app/services/collavre/comments/mcp_command.rb +31 -1
  89. data/app/services/collavre/creatives/breadcrumb_resolver.rb +91 -0
  90. data/app/services/collavre/creatives/filter_pipeline.rb +26 -42
  91. data/app/services/collavre/creatives/filters/search_filter.rb +12 -2
  92. data/app/services/collavre/creatives/index_query.rb +110 -8
  93. data/app/services/collavre/creatives/permission_filter.rb +50 -0
  94. data/app/services/collavre/creatives/reveal_path_resolver.rb +118 -0
  95. data/app/services/collavre/creatives/tree_builder.rb +7 -3
  96. data/app/services/collavre/crons/recurring_task_arguments.rb +28 -0
  97. data/app/services/collavre/google_calendar_service.rb +4 -2
  98. data/app/services/collavre/markdown_converter.rb +130 -15
  99. data/app/services/collavre/markdown_importer.rb +7 -2
  100. data/app/services/collavre/orchestration/policy_resolver.rb +11 -1
  101. data/app/services/collavre/orchestration/stuck_detector.rb +22 -2
  102. data/app/services/collavre/tools/creative_attach_files_service.rb +62 -0
  103. data/app/services/collavre/tools/creative_list_attachments_service.rb +42 -0
  104. data/app/services/collavre/tools/creative_remove_attachment_service.rb +37 -0
  105. data/app/services/collavre/tools/cron_list_service.rb +1 -14
  106. data/app/services/collavre/tools/permission_denied_error.rb +9 -0
  107. data/app/services/collavre/tools/preview_attach_service.rb +128 -0
  108. data/app/services/collavre/tools/preview_detach_service.rb +61 -0
  109. data/app/services/collavre/tools/topic_authorizer.rb +24 -0
  110. data/app/services/collavre/topic_branch_service.rb +34 -26
  111. data/app/services/collavre/topics/orphaned_cron_notifier.rb +68 -0
  112. data/app/views/admin/shared/_tabs.html.erb +1 -0
  113. data/app/views/collavre/admin/integrations/_category.html.erb +22 -0
  114. data/app/views/collavre/admin/integrations/_setting_row.html.erb +70 -0
  115. data/app/views/collavre/admin/integrations/index.html.erb +42 -0
  116. data/app/views/collavre/admin/settings/_system_tab.html.erb +8 -0
  117. data/app/views/collavre/comments/_channel_chips.html.erb +33 -0
  118. data/app/views/collavre/comments/_comment.html.erb +16 -2
  119. data/app/views/collavre/comments/_comments_popup.html.erb +4 -1
  120. data/app/views/collavre/creatives/_inline_edit_form.html.erb +19 -0
  121. data/app/views/collavre/creatives/index.html.erb +10 -2
  122. data/app/views/collavre/landing/show.html.erb +130 -0
  123. data/app/views/collavre/shared/_link_creative_modal.html.erb +6 -2
  124. data/app/views/layouts/collavre/landing.html.erb +33 -0
  125. data/config/locales/admin.en.yml +4 -2
  126. data/config/locales/admin.ko.yml +4 -2
  127. data/config/locales/channels.en.yml +13 -0
  128. data/config/locales/channels.ko.yml +13 -0
  129. data/config/locales/claude_channel.en.yml +16 -0
  130. data/config/locales/claude_channel.ko.yml +16 -0
  131. data/config/locales/comments.en.yml +5 -0
  132. data/config/locales/comments.ko.yml +5 -0
  133. data/config/locales/creatives.en.yml +11 -0
  134. data/config/locales/creatives.ko.yml +10 -0
  135. data/config/locales/integrations.en.yml +55 -0
  136. data/config/locales/integrations.ko.yml +55 -0
  137. data/config/locales/landing.en.yml +51 -0
  138. data/config/locales/landing.ko.yml +51 -0
  139. data/config/routes.rb +30 -0
  140. data/db/migrate/20260526000000_create_channels.rb +42 -0
  141. data/db/migrate/20260527000000_add_dismissed_at_to_channels.rb +6 -0
  142. data/db/migrate/20260527000100_backfill_dismissed_at_for_legacy_detached_channels.rb +28 -0
  143. data/db/migrate/20260528000000_add_preview_channel_unique_index.rb +31 -0
  144. data/db/migrate/20260529000000_add_primary_agent_id_to_topics.rb +40 -0
  145. data/db/migrate/20260529100000_create_integration_settings.rb +15 -0
  146. data/db/migrate/20260609000000_drop_action_text_rich_texts.rb +20 -0
  147. data/db/migrate/20260609005000_add_session_id_to_topics.rb +16 -0
  148. data/db/migrate/20260609010000_create_agent_subscriptions.rb +19 -0
  149. data/db/migrate/20260609020000_add_last_seen_at_to_agent_subscriptions.rb +23 -0
  150. data/db/migrate/20260609030000_add_session_id_to_agent_subscriptions.rb +17 -0
  151. data/db/migrate/20260609190659_backfill_creative_files_into_description.rb +24 -0
  152. data/db/seeds.rb +19 -0
  153. data/lib/collavre/aws_credentials.rb +75 -0
  154. data/lib/collavre/engine.rb +50 -0
  155. data/lib/collavre/integration_settings/key_definition.rb +35 -0
  156. data/lib/collavre/integration_settings/registry.rb +60 -0
  157. data/lib/collavre/integration_settings/resolver.rb +71 -0
  158. data/lib/collavre/integration_settings.rb +46 -0
  159. data/lib/collavre/ses_settings_interceptor.rb +72 -0
  160. data/lib/collavre/version.rb +1 -1
  161. data/lib/collavre.rb +3 -0
  162. metadata +82 -2
  163. data/app/services/collavre/openclaw_abort_service.rb +0 -45
@@ -0,0 +1,55 @@
1
+ en:
2
+ collavre:
3
+ admin:
4
+ integrations:
5
+ title: "Integrations"
6
+ subtitle: "Configure external integration secrets. Database values take precedence over ENV; ENV is the seed/fallback."
7
+ tab_label: "Integrations"
8
+ empty_state: "No integrations registered yet."
9
+ sensitive: "(sensitive)"
10
+ restart_required: "restart required"
11
+ placeholder_leave_blank: "Leave blank to keep current"
12
+ env_var_label: "ENV"
13
+ confirm_reset: "Delete the DB value and fall back to ENV?"
14
+ headers:
15
+ key: "Key"
16
+ value: "Value"
17
+ actions: "Actions"
18
+ source:
19
+ db: "DB"
20
+ env: "ENV"
21
+ default: "default"
22
+ unknown: "unknown"
23
+ category:
24
+ slack: "Slack"
25
+ google_oauth: "Google OAuth"
26
+ github_oauth: "GitHub OAuth"
27
+ notion_oauth: "Notion OAuth"
28
+ github: "GitHub Integration"
29
+ aws_s3: "AWS S3"
30
+ aws_ses: "AWS SES"
31
+ firebase: "Firebase / FCM"
32
+ gemini: "Gemini"
33
+ llm: "LLM API Keys"
34
+ openclaw: "OpenClaw Gateway"
35
+ mail: "Mail / Host"
36
+ misc: "Miscellaneous"
37
+ actions:
38
+ save: "Save changes"
39
+ reset: "Reset to ENV"
40
+ flash:
41
+ updated: "Integration settings saved."
42
+ restart_required: "Some changed keys require a server restart to take effect."
43
+ reset_done: "Reset %{key} to ENV/default."
44
+ descriptions:
45
+ firebase_project_id: "Firebase project ID (e.g. my-app-12345). Required for both server push and browser SDK. Find it at Firebase Console → Project Settings → General."
46
+ firebase_service_account_json: "Paste the full JSON body of a Google Cloud service account key. Recommended path for server FCM auth — set this and you're done. Generate at Firebase Console → Project Settings → Service Accounts → Generate New Private Key. Takes precedence over WIF."
47
+ fcm_wif_audience: "Optional WIF override (AWS only). Used only when the JSON key above is empty. Leave blank to derive it in production from Sender ID using the default aws-pool/aws-provider. Override for non-default pools: //iam.googleapis.com/projects/{PROJECT_NUMBER}/locations/global/workloadIdentityPools/{POOL_ID}/providers/{PROVIDER_ID}."
48
+ fcm_wif_credential_source: "Optional WIF override: AWS credential source JSON (IMDS endpoints). Leave blank to use the default AWS IMDS endpoints. Override example: {\"environment_id\":\"aws1\",\"region_url\":\"http://169.254.169.254/latest/meta-data/placement/availability-zone\",\"url\":\"http://169.254.169.254/latest/meta-data/iam/security-credentials\",\"regional_cred_verification_url\":\"https://sts.{region}.amazonaws.com?Action=GetCallerIdentity&Version=2011-06-15\"}"
49
+ fcm_wif_service_account_email: "GCP service account email to impersonate via WIF (e.g. firebase-sender@PROJECT.iam.gserviceaccount.com). Used only in WIF mode. Reads the legacy FIREBASE_SERVICE_ACCOUNT env var as fallback."
50
+ fcm_sender_id: "Web-push only: messagingSenderId for the Firebase JS SDK (project number). Required for browser push subscriptions. Find at Firebase Console → Project Settings → Cloud Messaging → Sender ID."
51
+ fcm_vapid_key: "Web-push only: VAPID public key for browser push subscriptions. Generate at Firebase Console → Project Settings → Cloud Messaging → Web configuration → Web Push certificates."
52
+ fcm_server_key: "Legacy FCM server key (deprecated by Google). Only set this if you have legacy clients using the old HTTP v0 API."
53
+ firebase_api_key: "Web-push only: Firebase JS SDK apiKey. Required for browser Firebase initialization (web push, Auth, Firestore). Find at Firebase Console → Project Settings → General → Web API Key."
54
+ firebase_auth_domain: "Web-push only: Firebase Auth redirect domain (usually {project}.firebaseapp.com). Required only if you use Firebase Auth in the browser."
55
+ firebase_app_id: "Web-push only: Firebase web app ID (e.g. 1:1234:web:abcd...). Required for browser Firebase initialization. Find at Firebase Console → Project Settings → General → Your apps."
@@ -0,0 +1,55 @@
1
+ ko:
2
+ collavre:
3
+ admin:
4
+ integrations:
5
+ title: "통합"
6
+ subtitle: "외부 통합 비밀값을 설정합니다. DB 값이 ENV보다 우선하며, ENV는 시드/폴백입니다."
7
+ tab_label: "통합"
8
+ empty_state: "등록된 통합이 없습니다."
9
+ sensitive: "(민감)"
10
+ restart_required: "재시작 필요"
11
+ placeholder_leave_blank: "비워두면 현재 값 유지"
12
+ env_var_label: "환경변수"
13
+ confirm_reset: "DB 값을 삭제하고 ENV로 폴백할까요?"
14
+ headers:
15
+ key: "키"
16
+ value: "값"
17
+ actions: "작업"
18
+ source:
19
+ db: "DB"
20
+ env: "ENV"
21
+ default: "기본값"
22
+ unknown: "알 수 없음"
23
+ category:
24
+ slack: "Slack"
25
+ google_oauth: "Google OAuth"
26
+ github_oauth: "GitHub OAuth"
27
+ notion_oauth: "Notion OAuth"
28
+ github: "GitHub 연동"
29
+ aws_s3: "AWS S3"
30
+ aws_ses: "AWS SES"
31
+ firebase: "Firebase / FCM"
32
+ gemini: "Gemini"
33
+ llm: "LLM API 키"
34
+ openclaw: "OpenClaw 게이트웨이"
35
+ mail: "메일 / 호스트"
36
+ misc: "기타"
37
+ actions:
38
+ save: "변경사항 저장"
39
+ reset: "ENV로 초기화"
40
+ flash:
41
+ updated: "통합 설정이 저장되었습니다."
42
+ restart_required: "변경된 일부 키는 서버 재시작 후에 적용됩니다."
43
+ reset_done: "%{key}을(를) ENV/기본값으로 초기화했습니다."
44
+ descriptions:
45
+ firebase_project_id: "Firebase 프로젝트 ID (예: my-app-12345). 서버 푸시와 브라우저 SDK 모두에서 필수. Firebase Console → 프로젝트 설정 → 일반에서 확인."
46
+ firebase_service_account_json: "Google Cloud 서비스 계정 키 JSON 본문 전체를 붙여넣으세요. 서버 FCM 인증의 권장 경로 — 이 값만 설정하면 끝납니다. Firebase Console → 프로젝트 설정 → 서비스 계정 → 새 비공개 키 생성. WIF 보다 우선합니다."
47
+ fcm_wif_audience: "선택적 WIF 오버라이드 (AWS 전용). 위 JSON 키가 비어있을 때만 사용됩니다. 비워두면 production 에서 Sender ID 로부터 기본 aws-pool/aws-provider 를 사용해 자동 생성됩니다. 비표준 풀 오버라이드: //iam.googleapis.com/projects/{PROJECT_NUMBER}/locations/global/workloadIdentityPools/{POOL_ID}/providers/{PROVIDER_ID}."
48
+ fcm_wif_credential_source: "선택적 WIF 오버라이드: AWS credential source JSON (IMDS 엔드포인트). 비워두면 기본 AWS IMDS 엔드포인트를 사용합니다. 오버라이드 예시: {\"environment_id\":\"aws1\",\"region_url\":\"http://169.254.169.254/latest/meta-data/placement/availability-zone\",\"url\":\"http://169.254.169.254/latest/meta-data/iam/security-credentials\",\"regional_cred_verification_url\":\"https://sts.{region}.amazonaws.com?Action=GetCallerIdentity&Version=2011-06-15\"}"
49
+ fcm_wif_service_account_email: "WIF 로 임퍼소네이션할 GCP 서비스 계정 이메일 (예: firebase-sender@PROJECT.iam.gserviceaccount.com). WIF 모드에서만 사용됩니다. 레거시 FIREBASE_SERVICE_ACCOUNT 환경변수를 폴백으로 읽습니다."
50
+ fcm_sender_id: "웹 푸시 전용: Firebase JS SDK 의 messagingSenderId (프로젝트 번호). 브라우저 푸시 구독에 필수. Firebase Console → 프로젝트 설정 → 클라우드 메시징 → 발신자 ID 에서 확인."
51
+ fcm_vapid_key: "웹 푸시 전용: 브라우저 푸시 구독용 VAPID 공개키. Firebase Console → 프로젝트 설정 → 클라우드 메시징 → 웹 구성 → 웹 푸시 인증서에서 생성."
52
+ fcm_server_key: "Legacy FCM 서버 키 (Google 에서 deprecated). 구버전 HTTP v0 API 를 쓰는 클라이언트가 있을 때만 설정하세요."
53
+ firebase_api_key: "웹 푸시 전용: Firebase JS SDK apiKey. 브라우저 Firebase 초기화 (웹 푸시, Auth, Firestore) 에 필수. Firebase Console → 프로젝트 설정 → 일반 → 웹 API 키에서 확인."
54
+ firebase_auth_domain: "웹 푸시 전용: Firebase Auth 리다이렉트 도메인 (보통 {project}.firebaseapp.com). 브라우저에서 Firebase Auth 를 쓸 때만 필요."
55
+ firebase_app_id: "웹 푸시 전용: Firebase 웹 앱 ID (예: 1:1234:web:abcd...). 브라우저 Firebase 초기화에 필수. Firebase Console → 프로젝트 설정 → 일반 → 내 앱에서 확인."
@@ -0,0 +1,51 @@
1
+ en:
2
+ collavre:
3
+ landing:
4
+ meta:
5
+ title: "Where Teams Think Together"
6
+ description: "The collaborative workspace where hierarchical creatives, AI agents, and real-time chat come together. Organize, collaborate, and ship — with AI as your teammate."
7
+ hero:
8
+ headline_html: "A platform for both AI agents and people."
9
+ subline: "Achieve sustainable productivity gains."
10
+ learn_more: "Learn More"
11
+ demo:
12
+ title: "See it in action"
13
+ placeholder: "Product demo video goes here"
14
+ problem:
15
+ title: "Your tools are failing you"
16
+ notion:
17
+ title: "Notion"
18
+ desc: "Docs become graveyards. Nobody edits someone else's page. Search replaces structure."
19
+ jira:
20
+ title: "Jira"
21
+ desc: "Great for tasks, terrible for knowledge. Results never sync back to the wiki. Information overload."
22
+ slack:
23
+ title: "Slack"
24
+ desc: "Conversations vanish in the stream. Flat channel lists explode. Context is lost daily."
25
+ features:
26
+ title: "Everything in one living tree"
27
+ subtitle: "Creatives are hierarchical blocks that organize your work, track progress, and carry context — all at once."
28
+ tree:
29
+ title: "Hierarchical Creatives"
30
+ desc: "Organize work as a tree. Drag, link, and reuse blocks across projects. Progress rolls up automatically."
31
+ ai:
32
+ title: "AI Agents as Teammates"
33
+ desc: "Add AI agents to any conversation. They review, collaborate, and respond to @mentions — just like a colleague."
34
+ context:
35
+ title: "Context System"
36
+ desc: "Reduce cognitive load for people, and inject the knowledge and skills AI needs."
37
+ chat:
38
+ title: "Real-time Chat & Topics"
39
+ desc: "Every creative has a chat. Organize conversations with topics, move messages between creatives, and keep discussions focused."
40
+ integrations:
41
+ title: "Integrations"
42
+ desc: "GitHub PRs auto-analyzed, Slack messages synced, Google Calendar events linked, MCP tools built-in."
43
+ progress:
44
+ title: "Live Progress"
45
+ desc: "Track completion from leaf to root. Filter by tags, plans, or status. See your project's real health at a glance."
46
+ cta:
47
+ title: "Ready to collaborate smarter?"
48
+ subtitle: "Start organizing your work with AI-powered context."
49
+ start: "Get Started"
50
+ footer:
51
+ rights: "All rights reserved."
@@ -0,0 +1,51 @@
1
+ ko:
2
+ collavre:
3
+ landing:
4
+ meta:
5
+ title: "팀이 함께 생각하는 곳"
6
+ description: "계층형 크리에이티브, AI 에이전트, 실시간 채팅이 하나로. 체계적으로 정리하고, 협업하고, AI와 함께 완성하세요."
7
+ hero:
8
+ headline_html: "AI 에이전트와 사람 모두의 플랫폼."
9
+ subline: "지속 가능한 생산성 향상을 이루세요."
10
+ learn_more: "알아보기"
11
+ demo:
12
+ title: "직접 확인하세요"
13
+ placeholder: "실제 작동 화면이 들어갈 자리입니다"
14
+ problem:
15
+ title: "기존 도구의 한계"
16
+ notion:
17
+ title: "Notion"
18
+ desc: "문서는 무덤이 됩니다. 남의 페이지를 고치기 꺼려하고, 검색이 구조를 대체합니다."
19
+ jira:
20
+ title: "Jira"
21
+ desc: "작업 관리는 좋지만 지식 관리는 안 됩니다. 결과물이 위키로 돌아오지 않습니다."
22
+ slack:
23
+ title: "Slack"
24
+ desc: "대화가 흘러가 버립니다. 채널은 폭발적으로 늘어나고, 맥락은 매일 사라집니다."
25
+ features:
26
+ title: "하나의 살아있는 트리로"
27
+ subtitle: "크리에이티브는 작업을 정리하고, 진행률을 추적하며, 맥락을 전달하는 계층형 블록입니다."
28
+ tree:
29
+ title: "계층형 크리에이티브"
30
+ desc: "작업을 트리로 구성하세요. 드래그, 링크, 재사용이 자유롭고 진행률은 자동 계산됩니다."
31
+ ai:
32
+ title: "AI 에이전트 팀원"
33
+ desc: "AI 에이전트를 대화에 추가하세요. 리뷰, 협업, @멘션 응답 — 동료처럼 일합니다."
34
+ context:
35
+ title: "컨텍스트 시스템"
36
+ desc: "사람의 인지 부하를 줄이고, AI에게 필요한 지식과 Skill을 주입합니다."
37
+ chat:
38
+ title: "실시간 채팅 & 토픽"
39
+ desc: "모든 크리에이티브에 채팅이 있습니다. 토픽으로 대화를 정리하고, 메시지를 다른 크리에이티브로 옮기세요."
40
+ integrations:
41
+ title: "외부 연동"
42
+ desc: "GitHub PR 자동 분석, Slack 동기화, Google Calendar 연동, MCP 도구 내장."
43
+ progress:
44
+ title: "실시간 진행률"
45
+ desc: "리프부터 루트까지 완료율 추적. 태그, 계획, 상태별 필터. 프로젝트 건강도를 한눈에."
46
+ cta:
47
+ title: "더 똑똑하게 협업할 준비 되셨나요?"
48
+ subtitle: "AI 기반 컨텍스트로 작업을 정리하세요."
49
+ start: "시작하기"
50
+ footer:
51
+ rights: "모든 권리 보유."
data/config/routes.rb CHANGED
@@ -1,4 +1,7 @@
1
1
  Collavre::Engine.routes.draw do
2
+ # Landing page
3
+ get "landing", to: "landing#show"
4
+
2
5
  # Authentication routes
3
6
  resource :session, only: [ :new, :create, :destroy ]
4
7
  resources :passwords, param: :token, only: [ :new, :create, :edit, :update ]
@@ -28,8 +31,13 @@ Collavre::Engine.routes.draw do
28
31
  match "/auth/google_oauth2/callback", to: "google_auth#callback", via: [ :get, :post ]
29
32
 
30
33
  delete "/attachments/:signed_id", to: "attachments#destroy", as: :attachment
34
+ get "/public-assets/blobs/:signed_id/*filename",
35
+ to: "public_assets#show",
36
+ as: :public_asset,
37
+ format: false
31
38
 
32
39
  resources :calendar_events, only: [ :destroy ]
40
+ resources :channels, only: [ :destroy ]
33
41
  resources :contacts, only: [ :destroy ]
34
42
  resources :devices, only: [ :create ]
35
43
 
@@ -50,6 +58,7 @@ Collavre::Engine.routes.draw do
50
58
  end
51
59
  end
52
60
  resources :creatives do
61
+ resources :attachments, only: [ :create ], module: :creatives
53
62
  resources :creative_shares, only: [ :index, :create, :update, :destroy ]
54
63
  resources :invitations, only: [ :update, :destroy ], controller: "creative_invitations"
55
64
  resources :topics, only: [ :index, :create, :update, :destroy ] do
@@ -58,6 +67,7 @@ Collavre::Engine.routes.draw do
58
67
  get :next_name
59
68
  end
60
69
  member do
70
+ get :channel_chips
61
71
  patch :move
62
72
  patch :archive
63
73
  patch :unarchive
@@ -68,6 +78,7 @@ Collavre::Engine.routes.draw do
68
78
  member do
69
79
  post :convert
70
80
  post :approve
81
+ post :deny
71
82
  patch :update_action
72
83
  delete :reactions, to: "comments/reactions#destroy"
73
84
  get :download_images
@@ -127,6 +138,16 @@ Collavre::Engine.routes.draw do
127
138
  patch "/creatives/:creative_id/user_creative_preferences/update_last_topic", to: "user_creative_preferences#update_last_topic", as: :update_last_topic
128
139
  post "/comment_read_pointers/update", to: "comment_read_pointers#update"
129
140
 
141
+ # Agent API (Claude Channel MCP plugin)
142
+ namespace :api do
143
+ namespace :v1 do
144
+ post "agent/register", to: "agents#register"
145
+ post "agent/reply", to: "agents#reply"
146
+ post "agent/notify", to: "agents#notify"
147
+ delete "agent/:id", to: "agents#destroy"
148
+ end
149
+ end
150
+
130
151
  # Admin settings & orchestration
131
152
  scope "/admin", as: :admin do
132
153
  get "/", to: "admin/settings#index", as: :settings
@@ -134,5 +155,14 @@ Collavre::Engine.routes.draw do
134
155
  get "/uiux", to: "admin/settings#uiux", as: :uiux
135
156
  patch "/uiux", to: "admin/settings#update_uiux"
136
157
  resource :orchestration, only: [ :show, :update ], controller: "admin/orchestration"
158
+
159
+ resources :integrations, only: [ :index ], param: :key, controller: "admin/integrations" do
160
+ collection do
161
+ patch :bulk_update
162
+ end
163
+ member do
164
+ delete :reset
165
+ end
166
+ end
137
167
  end
138
168
  end
@@ -0,0 +1,42 @@
1
+ class CreateChannels < ActiveRecord::Migration[8.1]
2
+ def up
3
+ postgres = connection.adapter_name == "PostgreSQL"
4
+
5
+ create_table :channels do |t|
6
+ t.string :type, null: false
7
+ t.references :topic, null: false, foreign_key: { to_table: :topics }, index: true
8
+ if postgres
9
+ t.jsonb :config, null: false, default: {}
10
+ else
11
+ t.json :config, null: false, default: {}
12
+ end
13
+ t.integer :state, null: false, default: 0
14
+ t.string :latest_label
15
+ t.string :latest_link
16
+ t.datetime :last_event_at
17
+ t.timestamps
18
+ end
19
+
20
+ if postgres
21
+ add_index :channels, :config, using: :gin
22
+ add_index :channels, :type
23
+ # Concurrent webhook safety: a single PR may emit pull_request.opened twice
24
+ # back-to-back (or auto-attach can race with pr_monitor). Without this
25
+ # constraint, the dispatch loop would inject every event N times.
26
+ execute <<~SQL
27
+ CREATE UNIQUE INDEX index_channels_on_type_topic_repo_pr
28
+ ON channels (type, topic_id, (config->>'repo_full_name'), (config->>'pr_number'))
29
+ SQL
30
+ else
31
+ add_index :channels, :type
32
+ execute <<~SQL
33
+ CREATE UNIQUE INDEX index_channels_on_type_topic_repo_pr
34
+ ON channels (type, topic_id, json_extract(config, '$.repo_full_name'), json_extract(config, '$.pr_number'))
35
+ SQL
36
+ end
37
+ end
38
+
39
+ def down
40
+ drop_table :channels
41
+ end
42
+ end
@@ -0,0 +1,6 @@
1
+ class AddDismissedAtToChannels < ActiveRecord::Migration[8.1]
2
+ def change
3
+ add_column :channels, :dismissed_at, :datetime
4
+ add_index :channels, :dismissed_at
5
+ end
6
+ end
@@ -0,0 +1,28 @@
1
+ class BackfillDismissedAtForLegacyDetachedChannels < ActiveRecord::Migration[8.1]
2
+ # Chips are now rendered via `not_dismissed` (dismissed_at IS NULL) instead
3
+ # of `state = active`. Without a backfill, any channel that was already
4
+ # detached pre-upgrade — user clicked X under the old flow, or the PR auto-
5
+ # detached on close — would reappear as a chip with the default-open badge
6
+ # the first time the new view runs. Mark legacy detached rows as dismissed
7
+ # using their last-touched timestamp as a best-effort dismissed_at.
8
+ #
9
+ # Idempotent: the WHERE clause skips rows that already have dismissed_at.
10
+ # Scoped to GithubPrChannel because the dismiss-on-detach UI ships only for
11
+ # PR chips. Future Channel STI subtypes opt in by adding their own backfill
12
+ # or by being created post-upgrade.
13
+ def up
14
+ execute <<~SQL.squish
15
+ UPDATE channels
16
+ SET dismissed_at = COALESCE(updated_at, CURRENT_TIMESTAMP)
17
+ WHERE state = 1
18
+ AND dismissed_at IS NULL
19
+ AND type = 'CollavreGithub::GithubPrChannel'
20
+ SQL
21
+ end
22
+
23
+ def down
24
+ # No-op: we can't distinguish backfilled rows from rows the user actually
25
+ # dismissed at the exact same timestamp, and leaving dismissed_at populated
26
+ # on rollback is harmless.
27
+ end
28
+ end
@@ -0,0 +1,31 @@
1
+ class AddPreviewChannelUniqueIndex < ActiveRecord::Migration[8.1]
2
+ def up
3
+ postgres = connection.adapter_name == "PostgreSQL"
4
+
5
+ # Mirror the PR-channel uniqueness guarantee for preview channels: a topic
6
+ # has at most one PreviewChannel per worktree_id. Without this, a racing
7
+ # preview_attach (e.g. concurrent CI + skill calls) could land two rows
8
+ # for the same worktree and the chip would render twice. `type` is
9
+ # included so the partial index does not collide with other channel
10
+ # subtypes that happen to store a `worktree_id` in their config.
11
+ if postgres
12
+ execute <<~SQL
13
+ CREATE UNIQUE INDEX index_channels_on_topic_preview_worktree
14
+ ON channels (topic_id, (config->>'worktree_id'))
15
+ WHERE type = 'Collavre::PreviewChannel'
16
+ SQL
17
+ else
18
+ execute <<~SQL
19
+ CREATE UNIQUE INDEX index_channels_on_topic_preview_worktree
20
+ ON channels (topic_id, json_extract(config, '$.worktree_id'))
21
+ WHERE type = 'Collavre::PreviewChannel'
22
+ SQL
23
+ end
24
+ end
25
+
26
+ def down
27
+ if connection.index_exists?(:channels, nil, name: "index_channels_on_topic_preview_worktree")
28
+ execute "DROP INDEX index_channels_on_topic_preview_worktree"
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ class AddPrimaryAgentIdToTopics < ActiveRecord::Migration[8.0]
4
+ def up
5
+ add_column :topics, :primary_agent_id, :integer
6
+ add_index :topics, :primary_agent_id
7
+
8
+ # Migrate existing data from orchestrator_policies using ActiveRecord (adapter-agnostic)
9
+ Collavre::OrchestratorPolicy
10
+ .where(policy_type: "arbitration", scope_type: "Topic", enabled: true)
11
+ .find_each do |policy|
12
+ agent_id = policy.config&.dig("primary_agent_id")
13
+ next unless agent_id.present?
14
+
15
+ Collavre::Topic.where(id: policy.scope_id).update_all(primary_agent_id: agent_id)
16
+ end
17
+
18
+ # Remove migrated topic-scoped arbitration policies
19
+ Collavre::OrchestratorPolicy
20
+ .where(scope_type: "Topic", policy_type: "arbitration")
21
+ .delete_all
22
+ end
23
+
24
+ def down
25
+ # Re-create orchestrator_policies from topics with primary_agent_id
26
+ Collavre::Topic.where.not(primary_agent_id: nil).find_each do |topic|
27
+ Collavre::OrchestratorPolicy.create!(
28
+ policy_type: "arbitration",
29
+ scope_type: "Topic",
30
+ scope_id: topic.id,
31
+ config: { "strategy" => "primary_first", "primary_agent_id" => topic.primary_agent_id },
32
+ priority: 10,
33
+ enabled: true
34
+ )
35
+ end
36
+
37
+ remove_index :topics, :primary_agent_id
38
+ remove_column :topics, :primary_agent_id
39
+ end
40
+ end
@@ -0,0 +1,15 @@
1
+ class CreateIntegrationSettings < ActiveRecord::Migration[8.1]
2
+ def change
3
+ create_table :integration_settings do |t|
4
+ t.string :key, null: false
5
+ t.text :value
6
+ t.string :category, null: false
7
+ t.boolean :sensitive, null: false, default: true
8
+
9
+ t.timestamps
10
+ end
11
+
12
+ add_index :integration_settings, :key, unique: true
13
+ add_index :integration_settings, :category
14
+ end
15
+ end
@@ -0,0 +1,20 @@
1
+ # Drops the dead ActionText table. Creatives moved their rich text to the
2
+ # `creatives.description` column (see 20251126040752_add_description_to_creatives),
3
+ # and no model declares `has_rich_text` anymore, so this table is unused.
4
+ class DropActionTextRichTexts < ActiveRecord::Migration[8.1]
5
+ def up
6
+ drop_table :action_text_rich_texts
7
+ end
8
+
9
+ def down
10
+ create_table :action_text_rich_texts do |t|
11
+ t.string :name, null: false
12
+ t.text :body
13
+ t.references :record, null: false, polymorphic: true, index: false
14
+
15
+ t.timestamps
16
+
17
+ t.index [ :record_type, :record_id, :name ], name: "index_action_text_rich_texts_uniqueness", unique: true
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Claude Channel Agent/Session split: a Session (one Claude Code session) maps
4
+ # to one Topic, identified by a stable session_id (derived per cwd by the
5
+ # plugin, stable across --resume). The Agent (ai_user) is now shared across a
6
+ # human's sessions, so the topic can no longer be located by the per-agent name
7
+ # alone — multiple session topics share one primary_agent. session_id is that
8
+ # per-session key.
9
+ class AddSessionIdToTopics < ActiveRecord::Migration[8.0]
10
+ def change
11
+ add_column :topics, :session_id, :string
12
+ # Look up a session topic by (primary_agent_id, session_id) on re-register.
13
+ add_index :topics, [ :primary_agent_id, :session_id ],
14
+ name: "index_topics_on_primary_agent_and_session"
15
+ end
16
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Presence rows for Claude Channel session subscriptions. One shared agent can
4
+ # now have many concurrent live sessions; routing_expression must stay active
5
+ # while ANY of them is subscribed. A single routing_subscription_token column
6
+ # (last-write-wins) cannot represent N subscribers — one session's unsubscribe
7
+ # would clear routing for a still-live sibling. Each live subscription gets a
8
+ # row here; routing turns off only when the last row is removed.
9
+ class CreateAgentSubscriptions < ActiveRecord::Migration[8.0]
10
+ def change
11
+ create_table :agent_subscriptions do |t|
12
+ t.integer :agent_id, null: false
13
+ t.string :token, null: false
14
+ t.timestamps
15
+ end
16
+ add_index :agent_subscriptions, :agent_id
17
+ add_index :agent_subscriptions, :token, unique: true
18
+ end
19
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Liveness heartbeat column for Claude Channel presence rows. Rows are only
4
+ # deleted by AgentChannel#unsubscribed, so a Puma/ActionCable crash or deploy
5
+ # orphans a row — and presence-gated routing would then stay active forever,
6
+ # dispatching into a dead stream. last_seen_at is refreshed by the channel's
7
+ # periodic heartbeat; a row whose last_seen_at is older than the staleness
8
+ # window is treated as dead (ignored by the live scope, reaped opportunistically).
9
+ class AddLastSeenAtToAgentSubscriptions < ActiveRecord::Migration[8.0]
10
+ def up
11
+ add_column :agent_subscriptions, :last_seen_at, :datetime
12
+ # Backfill existing rows from created_at so a row written before this
13
+ # migration is considered live until its next heartbeat (or stale-reap).
14
+ execute "UPDATE agent_subscriptions SET last_seen_at = created_at WHERE last_seen_at IS NULL"
15
+ change_column_null :agent_subscriptions, :last_seen_at, false
16
+ add_index :agent_subscriptions, :last_seen_at
17
+ end
18
+
19
+ def down
20
+ remove_index :agent_subscriptions, :last_seen_at
21
+ remove_column :agent_subscriptions, :last_seen_at
22
+ end
23
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ # A shared agent fans out to many concurrent sessions, each with its own
4
+ # presence row. The HTTP unregister path (DELETE /api/v1/agent/:id) needs to
5
+ # drop ONLY the exiting session's row before deciding whether a sibling is
6
+ # still live — otherwise the plugin's close-WS-then-DELETE ordering lets a
7
+ # session's own still-live row masquerade as a sibling and skip the
8
+ # last-session teardown. The WS token is server-minted and unknown to the HTTP
9
+ # client; session_id (stable across --resume, sent by the plugin) is the
10
+ # identity the DELETE can correlate to. Nullable: topic-stream/legacy
11
+ # subscribers have no session_id.
12
+ class AddSessionIdToAgentSubscriptions < ActiveRecord::Migration[8.0]
13
+ def change
14
+ add_column :agent_subscriptions, :session_id, :string
15
+ add_index :agent_subscriptions, [ :agent_id, :session_id ]
16
+ end
17
+ end
@@ -0,0 +1,24 @@
1
+ class BackfillCreativeFilesIntoDescription < ActiveRecord::Migration[8.1]
2
+ disable_ddl_transaction!
3
+
4
+ # Description HTML is now the source of truth for creative.files (see
5
+ # Collavre::Creative::Describable#reconcile_description_attachments). Existing
6
+ # creatives may have blobs attached to creative.files that are NOT referenced
7
+ # in their description (legacy MCP/console attaches). Embed those orphan blobs
8
+ # into the description so the first reconcile-driven save does not detach them.
9
+ #
10
+ # Idempotent: re-running embeds nothing already referenced. Non-destructive:
11
+ # only ADDS embed nodes; the down migration is a no-op.
12
+ def up
13
+ Collavre::Creative.reset_column_information
14
+ Collavre::Creative.where.not(description: nil).find_each(batch_size: 200) do |creative|
15
+ next unless creative.files.attached?
16
+
17
+ Collavre::AttachmentBackfill.embed_orphans!(creative)
18
+ end
19
+ end
20
+
21
+ def down
22
+ # No-op: embedded media nodes are non-destructive and idempotent.
23
+ end
24
+ end
data/db/seeds.rb ADDED
@@ -0,0 +1,19 @@
1
+ module Collavre
2
+ module ChannelBotSeed
3
+ def self.call
4
+ email = Channel::BOT_EMAIL
5
+ name = Channel::BOT_NAME
6
+ user = User.find_or_initialize_by(email: email)
7
+ user.name = name
8
+ user.password = SecureRandom.hex(32) if user.new_record?
9
+ user.email_verified_at ||= Time.current
10
+ user.searchable = false if user.respond_to?(:searchable=)
11
+ user.llm_vendor = nil
12
+ user.save!
13
+ Rails.logger.info "[Collavre] Channel bot user ensured: #{email}"
14
+ user
15
+ end
16
+ end
17
+ end
18
+
19
+ Collavre::ChannelBotSeed.call