clawmini 0.0.3 → 0.0.5

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 (333) hide show
  1. package/README.md +19 -0
  2. package/dist/adapter-discord/index.d.mts.map +1 -1
  3. package/dist/adapter-discord/index.mjs +398 -193
  4. package/dist/adapter-discord/index.mjs.map +1 -1
  5. package/dist/adapter-google-chat/index.d.mts +5 -0
  6. package/dist/adapter-google-chat/index.d.mts.map +1 -0
  7. package/dist/adapter-google-chat/index.mjs +1077 -0
  8. package/dist/adapter-google-chat/index.mjs.map +1 -0
  9. package/dist/cli/index.mjs +107 -14
  10. package/dist/cli/index.mjs.map +1 -1
  11. package/dist/cli/lite.mjs +175 -16
  12. package/dist/cli/lite.mjs.map +1 -1
  13. package/dist/cli/propose-policy.d.mts +1 -0
  14. package/dist/cli/propose-policy.mjs +7159 -0
  15. package/dist/cli/propose-policy.mjs.map +1 -0
  16. package/dist/daemon/index.d.mts.map +1 -1
  17. package/dist/daemon/index.mjs +1427 -513
  18. package/dist/daemon/index.mjs.map +1 -1
  19. package/dist/{lite-oSYSvaOr.mjs → lite-CBxOT1y5.mjs} +101 -24
  20. package/dist/lite-CBxOT1y5.mjs.map +1 -0
  21. package/dist/routing-D8rTxtaV.mjs +245 -0
  22. package/dist/routing-D8rTxtaV.mjs.map +1 -0
  23. package/dist/web/_app/immutable/assets/0.C-4eziNy.css +1 -0
  24. package/dist/web/_app/immutable/assets/4.Cc_xwLNl.css +1 -0
  25. package/dist/web/_app/immutable/chunks/B6YN0Nuq.js +1 -0
  26. package/dist/web/_app/immutable/chunks/{Dc-UOHw9.js → BmRlVmv6.js} +1 -1
  27. package/{web/.svelte-kit/output/client/_app/immutable/chunks/8YNcRyEk.js → dist/web/_app/immutable/chunks/C20lZMGz.js} +1 -1
  28. package/dist/web/_app/immutable/chunks/C9lbZ-kT.js +1 -0
  29. package/dist/web/_app/immutable/chunks/CK9JZLaG.js +2 -0
  30. package/dist/web/_app/immutable/chunks/CME08kGM.js +1 -0
  31. package/dist/web/_app/immutable/chunks/{BPy8HLo7.js → Ck-be5J2.js} +1 -1
  32. package/dist/web/_app/immutable/chunks/Ck3rYNON.js +1 -0
  33. package/dist/web/_app/immutable/chunks/DMtIqaiV.js +2 -0
  34. package/dist/web/_app/immutable/chunks/{B8yYFADm.js → DhD271EB.js} +1 -1
  35. package/dist/web/_app/immutable/chunks/{DcrmIfTj.js → DpuLqk8d.js} +1 -1
  36. package/dist/web/_app/immutable/chunks/{ZkLyk0mE.js → Drm9vgeP.js} +1 -1
  37. package/dist/web/_app/immutable/chunks/DsIToJCP.js +1 -0
  38. package/dist/web/_app/immutable/chunks/{CyNaE55B.js → Zeh-C-mx.js} +1 -1
  39. package/{web/.svelte-kit/output/client/_app/immutable/entry/app.DO5eYwVz.js → dist/web/_app/immutable/entry/app.BgB5VkRU.js} +2 -2
  40. package/dist/web/_app/immutable/entry/start.DuxJo6av.js +1 -0
  41. package/dist/web/_app/immutable/nodes/0.C9oFZP9h.js +1 -0
  42. package/dist/web/_app/immutable/nodes/1.BON2Wk6k.js +1 -0
  43. package/dist/web/_app/immutable/nodes/{2.CK3CLC0f.js → 2.BnwnD1Ki.js} +1 -1
  44. package/dist/web/_app/immutable/nodes/{3.ncP0xLO6.js → 3.CIs4tjjw.js} +1 -1
  45. package/dist/web/_app/immutable/nodes/4.DLarELN4.js +60 -0
  46. package/dist/web/_app/immutable/nodes/{5.BpJUN6QH.js → 5.CE_QKy_3.js} +1 -1
  47. package/dist/web/_app/version.json +1 -1
  48. package/dist/web/index.html +12 -12
  49. package/dist/{workspace-DjoNjhW0.mjs → workspace-BJmJBfKi.mjs} +103 -11
  50. package/dist/workspace-BJmJBfKi.mjs.map +1 -0
  51. package/docs/14_google_chat_adapter/development_log.md +40 -0
  52. package/docs/14_google_chat_adapter/notes.md +28 -0
  53. package/docs/14_google_chat_adapter/prd.md +35 -0
  54. package/docs/14_google_chat_adapter/questions.md +9 -0
  55. package/docs/14_google_chat_adapter/tickets.md +117 -0
  56. package/docs/15_sandbox_policies/tickets.md +33 -0
  57. package/docs/16_session_timeout/development_log.md +20 -0
  58. package/docs/16_session_timeout/notes.md +44 -0
  59. package/docs/16_session_timeout/prd.md +106 -0
  60. package/docs/16_session_timeout/questions.md +10 -0
  61. package/docs/16_session_timeout/tickets.md +64 -0
  62. package/docs/17_auto_approve_policy/development_log.md +29 -0
  63. package/docs/17_auto_approve_policy/notes.md +25 -0
  64. package/docs/17_auto_approve_policy/prd.md +34 -0
  65. package/docs/17_auto_approve_policy/questions.md +10 -0
  66. package/docs/17_auto_approve_policy/tickets.md +11 -0
  67. package/docs/18_clawmini_skills/development_log.md +36 -0
  68. package/docs/18_clawmini_skills/notes.md +8 -0
  69. package/docs/18_clawmini_skills/prd.md +45 -0
  70. package/docs/18_clawmini_skills/questions.md +10 -0
  71. package/docs/18_clawmini_skills/tickets.md +55 -0
  72. package/docs/19_subagents/development_log.md +69 -0
  73. package/docs/19_subagents/notes.md +18 -0
  74. package/docs/19_subagents/prd.md +156 -0
  75. package/docs/19_subagents/questions.md +13 -0
  76. package/docs/19_subagents/tickets.md +113 -0
  77. package/docs/20_chat_logs_cleanup/development_log.md +50 -0
  78. package/docs/20_chat_logs_cleanup/notes.md +43 -0
  79. package/docs/20_chat_logs_cleanup/prd.md +232 -0
  80. package/docs/20_chat_logs_cleanup/questions.md +2 -0
  81. package/docs/20_chat_logs_cleanup/tickets.md +98 -0
  82. package/docs/20_webui_markdown/development_log.md +36 -0
  83. package/docs/20_webui_markdown/notes.md +23 -0
  84. package/docs/20_webui_markdown/prd.md +49 -0
  85. package/docs/20_webui_markdown/questions.md +10 -0
  86. package/docs/20_webui_markdown/tickets.md +55 -0
  87. package/docs/21_adapter_filtering/development_log.md +29 -0
  88. package/docs/21_adapter_filtering/notes.md +25 -0
  89. package/docs/21_adapter_filtering/prd.md +44 -0
  90. package/docs/21_adapter_filtering/questions.md +12 -0
  91. package/docs/21_adapter_filtering/tickets.md +38 -0
  92. package/docs/21_built_in_routers/development_log.md +17 -0
  93. package/docs/21_built_in_routers/notes.md +27 -0
  94. package/docs/21_built_in_routers/prd.md +34 -0
  95. package/docs/21_built_in_routers/questions.md +4 -0
  96. package/docs/21_built_in_routers/tickets.md +25 -0
  97. package/docs/21_fancy_policies/development_log.md +38 -0
  98. package/docs/21_fancy_policies/notes.md +27 -0
  99. package/docs/21_fancy_policies/prd.md +58 -0
  100. package/docs/21_fancy_policies/questions.md +6 -0
  101. package/docs/21_fancy_policies/tickets.md +48 -0
  102. package/docs/22_adapter_multi_chat/development_log.md +76 -0
  103. package/docs/22_adapter_multi_chat/notes.md +42 -0
  104. package/docs/22_adapter_multi_chat/prd.md +76 -0
  105. package/docs/22_adapter_multi_chat/questions.md +16 -0
  106. package/docs/22_adapter_multi_chat/tickets.md +164 -0
  107. package/docs/23_custom_token_env/development_log.md +31 -0
  108. package/docs/23_custom_token_env/notes.md +16 -0
  109. package/docs/23_custom_token_env/prd.md +42 -0
  110. package/docs/23_custom_token_env/questions.md +8 -0
  111. package/docs/23_custom_token_env/tickets.md +54 -0
  112. package/docs/guides/discord_adapter_setup.md +15 -2
  113. package/docs/guides/google_chat_adapter_setup.md +145 -0
  114. package/napkin.md +5 -0
  115. package/package.json +7 -2
  116. package/src/adapter-discord/config.test.ts +27 -8
  117. package/src/adapter-discord/config.ts +6 -8
  118. package/src/adapter-discord/forwarder.test.ts +307 -114
  119. package/src/adapter-discord/forwarder.ts +260 -75
  120. package/src/adapter-discord/index.test.ts +278 -0
  121. package/src/adapter-discord/index.ts +160 -30
  122. package/src/adapter-discord/interactions.test.ts +96 -0
  123. package/src/adapter-discord/interactions.ts +156 -0
  124. package/src/adapter-discord/state.test.ts +9 -8
  125. package/src/adapter-discord/state.ts +51 -8
  126. package/src/adapter-google-chat/auth.test.ts +87 -0
  127. package/src/adapter-google-chat/auth.ts +132 -0
  128. package/src/adapter-google-chat/cards.ts +71 -0
  129. package/src/adapter-google-chat/client.test.ts +561 -0
  130. package/src/adapter-google-chat/client.ts +430 -0
  131. package/src/adapter-google-chat/config.test.ts +187 -0
  132. package/src/adapter-google-chat/config.ts +82 -0
  133. package/src/adapter-google-chat/cron.test.ts +143 -0
  134. package/src/adapter-google-chat/cron.ts +81 -0
  135. package/src/adapter-google-chat/forwarder.test.ts +537 -0
  136. package/src/adapter-google-chat/forwarder.ts +349 -0
  137. package/src/adapter-google-chat/index.test.ts +62 -0
  138. package/src/adapter-google-chat/index.ts +61 -0
  139. package/src/adapter-google-chat/state.test.ts +96 -0
  140. package/src/adapter-google-chat/state.ts +85 -0
  141. package/src/adapter-google-chat/subscriptions.ts +124 -0
  142. package/src/adapter-google-chat/upload.ts +88 -0
  143. package/src/adapter-google-chat/utils.test.ts +111 -0
  144. package/src/adapter-google-chat/utils.ts +133 -0
  145. package/src/cli/commands/init.ts +0 -7
  146. package/src/cli/commands/messages.ts +18 -3
  147. package/src/cli/commands/policies.ts +70 -0
  148. package/src/cli/commands/skills.ts +71 -0
  149. package/src/cli/commands/web-api/chats.ts +5 -1
  150. package/src/cli/e2e/basic.test.ts +1 -1
  151. package/src/cli/e2e/cron.test.ts +1 -1
  152. package/src/cli/e2e/daemon.test.ts +132 -4
  153. package/src/cli/e2e/export-lite-func.test.ts +54 -31
  154. package/src/cli/e2e/fallbacks.test.ts +8 -6
  155. package/src/cli/e2e/init.test.ts +7 -0
  156. package/src/cli/e2e/messages.test.ts +90 -55
  157. package/src/cli/e2e/propose-policy.test.ts +203 -0
  158. package/src/cli/e2e/requests.test.ts +15 -0
  159. package/src/cli/e2e/session-timeout.test.ts +192 -0
  160. package/src/cli/e2e/skills.test.ts +55 -0
  161. package/src/cli/e2e/slash-new.test.ts +93 -0
  162. package/src/cli/e2e/subagents.test.ts +106 -0
  163. package/src/cli/index.ts +4 -0
  164. package/src/cli/lite.ts +51 -11
  165. package/src/cli/propose-policy.ts +91 -0
  166. package/src/cli/subagent-commands.ts +215 -0
  167. package/src/daemon/agent/agent-context.ts +89 -0
  168. package/src/daemon/agent/agent-extractors.ts +68 -0
  169. package/src/daemon/agent/agent-runner.ts +153 -0
  170. package/src/daemon/agent/agent-session.ts +261 -0
  171. package/src/daemon/agent/chat-logger.test.ts +158 -0
  172. package/src/daemon/agent/chat-logger.ts +188 -0
  173. package/src/daemon/agent/task-scheduler.test.ts +202 -0
  174. package/src/daemon/agent/task-scheduler.ts +276 -0
  175. package/src/daemon/agent/types.ts +84 -0
  176. package/src/daemon/agent/utils.ts +7 -0
  177. package/src/daemon/api/agent-router.ts +166 -18
  178. package/src/daemon/api/index.test.ts +50 -18
  179. package/src/daemon/api/policy-request.test.ts +39 -2
  180. package/src/daemon/api/subagent-router.test.ts +108 -0
  181. package/src/daemon/api/subagent-router.ts +296 -0
  182. package/src/daemon/api/subagent-utils.test.ts +56 -0
  183. package/src/daemon/api/subagent-utils.ts +130 -0
  184. package/src/daemon/api/user-router.ts +30 -13
  185. package/src/daemon/auth.ts +1 -0
  186. package/src/daemon/chats.ts +6 -0
  187. package/src/daemon/cron.test.ts +66 -1
  188. package/src/daemon/cron.ts +35 -8
  189. package/src/daemon/index.ts +23 -0
  190. package/src/daemon/message-agent.test.ts +11 -25
  191. package/src/daemon/message-extraction.test.ts +10 -27
  192. package/src/daemon/message-fallbacks.test.ts +13 -35
  193. package/src/daemon/message-interruption.test.ts +70 -53
  194. package/src/daemon/message-jobs.test.ts +138 -0
  195. package/src/daemon/message-queue.test.ts +30 -43
  196. package/src/daemon/message-router.test.ts +12 -11
  197. package/src/daemon/message-session.test.ts +41 -28
  198. package/src/daemon/message-typing.test.ts +19 -6
  199. package/src/daemon/message.ts +103 -515
  200. package/src/daemon/policy-request-service.ts +8 -3
  201. package/src/daemon/policy-utils.ts +19 -1
  202. package/src/daemon/queue.ts +16 -0
  203. package/src/daemon/request-store.test.ts +4 -0
  204. package/src/daemon/routers/session-timeout.test.ts +122 -0
  205. package/src/daemon/routers/session-timeout.ts +71 -0
  206. package/src/daemon/routers/slash-new.ts +3 -1
  207. package/src/daemon/routers/slash-policies.test.ts +26 -13
  208. package/src/daemon/routers/slash-policies.ts +39 -29
  209. package/src/daemon/routers/types.ts +8 -0
  210. package/src/daemon/routers.ts +64 -2
  211. package/src/daemon/utils/spawn.ts +6 -8
  212. package/src/shared/adapters/commands.test.ts +155 -0
  213. package/src/shared/adapters/commands.ts +125 -0
  214. package/src/shared/adapters/filtering.test.ts +111 -0
  215. package/src/shared/adapters/filtering.ts +57 -0
  216. package/src/shared/adapters/routing.test.ts +144 -0
  217. package/src/shared/adapters/routing.ts +109 -0
  218. package/src/shared/agent-utils.ts +10 -0
  219. package/src/shared/chats.test.ts +145 -3
  220. package/src/shared/chats.ts +215 -18
  221. package/src/shared/config.ts +67 -15
  222. package/src/shared/lite.ts +22 -18
  223. package/src/shared/policies.ts +7 -0
  224. package/src/shared/workspace.test.ts +45 -1
  225. package/src/shared/workspace.ts +119 -6
  226. package/templates/debug/settings.json +5 -2
  227. package/templates/environments/cladding/env.json +2 -2
  228. package/templates/gemini/.gemini/hooks/check-subagents.mjs +23 -0
  229. package/templates/gemini/.gemini/hooks/clawmini-logging.sh +17 -0
  230. package/templates/gemini/.gemini/hooks/insert-pending.sh +9 -0
  231. package/templates/gemini/.gemini/settings.json +50 -0
  232. package/templates/gemini/settings.json +22 -8
  233. package/templates/gemini-claw/.gemini/base-system.md +100 -0
  234. package/templates/gemini-claw/.gemini/hooks/check-subagents.mjs +23 -0
  235. package/templates/gemini-claw/.gemini/hooks/clawmini-logging.sh +1 -1
  236. package/templates/gemini-claw/.gemini/settings.json +13 -0
  237. package/templates/gemini-claw/.gemini/subagent-system.md +7 -0
  238. package/templates/gemini-claw/.gemini/system.md +3 -99
  239. package/templates/gemini-claw/settings.json +27 -22
  240. package/templates/skills/clawmini-requests/SKILL.md +92 -0
  241. package/templates/skills/clawmini-subagents/SKILL.md +79 -0
  242. package/templates/skills/skill-creator/SKILL.md +60 -0
  243. package/tsdown.config.ts +10 -1
  244. package/web/.svelte-kit/generated/server/internal.js +2 -1
  245. package/web/.svelte-kit/non-ambient.d.ts +2 -0
  246. package/web/.svelte-kit/output/client/.vite/manifest.json +141 -138
  247. package/web/.svelte-kit/output/client/_app/immutable/assets/0.C-4eziNy.css +1 -0
  248. package/web/.svelte-kit/output/client/_app/immutable/assets/4.Cc_xwLNl.css +1 -0
  249. package/web/.svelte-kit/output/client/_app/immutable/chunks/B6YN0Nuq.js +1 -0
  250. package/web/.svelte-kit/output/client/_app/immutable/chunks/{Dc-UOHw9.js → BmRlVmv6.js} +1 -1
  251. package/{dist/web/_app/immutable/chunks/8YNcRyEk.js → web/.svelte-kit/output/client/_app/immutable/chunks/C20lZMGz.js} +1 -1
  252. package/web/.svelte-kit/output/client/_app/immutable/chunks/C9lbZ-kT.js +1 -0
  253. package/web/.svelte-kit/output/client/_app/immutable/chunks/CK9JZLaG.js +2 -0
  254. package/web/.svelte-kit/output/client/_app/immutable/chunks/CME08kGM.js +1 -0
  255. package/web/.svelte-kit/output/client/_app/immutable/chunks/{BPy8HLo7.js → Ck-be5J2.js} +1 -1
  256. package/web/.svelte-kit/output/client/_app/immutable/chunks/Ck3rYNON.js +1 -0
  257. package/web/.svelte-kit/output/client/_app/immutable/chunks/DMtIqaiV.js +2 -0
  258. package/web/.svelte-kit/output/client/_app/immutable/chunks/{B8yYFADm.js → DhD271EB.js} +1 -1
  259. package/web/.svelte-kit/output/client/_app/immutable/chunks/{DcrmIfTj.js → DpuLqk8d.js} +1 -1
  260. package/web/.svelte-kit/output/client/_app/immutable/chunks/{ZkLyk0mE.js → Drm9vgeP.js} +1 -1
  261. package/web/.svelte-kit/output/client/_app/immutable/chunks/DsIToJCP.js +1 -0
  262. package/web/.svelte-kit/output/client/_app/immutable/chunks/{CyNaE55B.js → Zeh-C-mx.js} +1 -1
  263. package/{dist/web/_app/immutable/entry/app.DO5eYwVz.js → web/.svelte-kit/output/client/_app/immutable/entry/app.BgB5VkRU.js} +2 -2
  264. package/web/.svelte-kit/output/client/_app/immutable/entry/start.DuxJo6av.js +1 -0
  265. package/web/.svelte-kit/output/client/_app/immutable/nodes/0.C9oFZP9h.js +1 -0
  266. package/web/.svelte-kit/output/client/_app/immutable/nodes/1.BON2Wk6k.js +1 -0
  267. package/web/.svelte-kit/output/client/_app/immutable/nodes/{2.CK3CLC0f.js → 2.BnwnD1Ki.js} +1 -1
  268. package/web/.svelte-kit/output/client/_app/immutable/nodes/{3.ncP0xLO6.js → 3.CIs4tjjw.js} +1 -1
  269. package/web/.svelte-kit/output/client/_app/immutable/nodes/4.DLarELN4.js +60 -0
  270. package/web/.svelte-kit/output/client/_app/immutable/nodes/{5.BpJUN6QH.js → 5.CE_QKy_3.js} +1 -1
  271. package/web/.svelte-kit/output/client/_app/version.json +1 -1
  272. package/web/.svelte-kit/output/server/.vite/manifest.json +12 -3
  273. package/web/.svelte-kit/output/server/_app/immutable/assets/_layout.C-4eziNy.css +1 -0
  274. package/web/.svelte-kit/output/server/_app/immutable/assets/_page.Cc_xwLNl.css +1 -0
  275. package/web/.svelte-kit/output/server/chunks/app-state.svelte.js +5 -0
  276. package/web/.svelte-kit/output/server/chunks/bot.js +4 -4
  277. package/web/.svelte-kit/output/server/chunks/client.js +2 -1
  278. package/web/.svelte-kit/output/server/chunks/exports.js +0 -1
  279. package/web/.svelte-kit/output/server/chunks/internal.js +2 -1
  280. package/web/.svelte-kit/output/server/chunks/root.js +482 -392
  281. package/web/.svelte-kit/output/server/entries/pages/_layout.svelte.js +57 -7
  282. package/web/.svelte-kit/output/server/entries/pages/chats/_id_/_page.svelte.js +234 -9
  283. package/web/.svelte-kit/output/server/index.js +82 -10
  284. package/web/.svelte-kit/output/server/manifest-full.js +1 -1
  285. package/web/.svelte-kit/output/server/manifest.js +1 -1
  286. package/web/.svelte-kit/output/server/nodes/0.js +2 -2
  287. package/web/.svelte-kit/output/server/nodes/1.js +1 -1
  288. package/web/.svelte-kit/output/server/nodes/2.js +1 -1
  289. package/web/.svelte-kit/output/server/nodes/3.js +1 -1
  290. package/web/.svelte-kit/output/server/nodes/4.js +2 -2
  291. package/web/.svelte-kit/output/server/nodes/5.js +1 -1
  292. package/web/.svelte-kit/types/src/routes/$types.d.ts +1 -2
  293. package/web/.svelte-kit/types/src/routes/agents/$types.d.ts +1 -2
  294. package/web/.svelte-kit/types/src/routes/chats/[id]/$types.d.ts +1 -2
  295. package/web/.svelte-kit/types/src/routes/chats/[id]/settings/$types.d.ts +1 -2
  296. package/web/package.json +8 -0
  297. package/web/src/lib/app-state.svelte.ts +5 -1
  298. package/web/src/lib/components/app/markdown-renderer.svelte +56 -0
  299. package/web/src/lib/components/app/markdown-renderer.svelte.spec.ts +44 -0
  300. package/web/src/lib/components/app/message-content.svelte +16 -0
  301. package/web/src/lib/types.ts +67 -3
  302. package/web/src/routes/+layout.svelte +31 -1
  303. package/web/src/routes/chats/[id]/+page.svelte +167 -18
  304. package/web/src/routes/chats/[id]/page.svelte.spec.ts +58 -7
  305. package/dist/lite-oSYSvaOr.mjs.map +0 -1
  306. package/dist/web/_app/immutable/assets/0.GI4C4dpV.css +0 -1
  307. package/dist/web/_app/immutable/chunks/B5abRDXp.js +0 -1
  308. package/dist/web/_app/immutable/chunks/Bi0jeV7Q.js +0 -1
  309. package/dist/web/_app/immutable/chunks/BmUXQ3wy.js +0 -2
  310. package/dist/web/_app/immutable/chunks/C3k55nDF.js +0 -1
  311. package/dist/web/_app/immutable/chunks/CpaGRn9L.js +0 -1
  312. package/dist/web/_app/immutable/chunks/DG5RZBw-.js +0 -2
  313. package/dist/web/_app/immutable/chunks/DQoygso7.js +0 -1
  314. package/dist/web/_app/immutable/entry/start.D48mVn1m.js +0 -1
  315. package/dist/web/_app/immutable/nodes/0.B-0CcADM.js +0 -1
  316. package/dist/web/_app/immutable/nodes/1.FixKgvRO.js +0 -1
  317. package/dist/web/_app/immutable/nodes/4.CQYJEgv8.js +0 -1
  318. package/dist/workspace-DjoNjhW0.mjs.map +0 -1
  319. package/src/daemon/message-verbosity.test.ts +0 -127
  320. package/web/.svelte-kit/output/client/_app/immutable/assets/0.GI4C4dpV.css +0 -1
  321. package/web/.svelte-kit/output/client/_app/immutable/chunks/B5abRDXp.js +0 -1
  322. package/web/.svelte-kit/output/client/_app/immutable/chunks/Bi0jeV7Q.js +0 -1
  323. package/web/.svelte-kit/output/client/_app/immutable/chunks/BmUXQ3wy.js +0 -2
  324. package/web/.svelte-kit/output/client/_app/immutable/chunks/C3k55nDF.js +0 -1
  325. package/web/.svelte-kit/output/client/_app/immutable/chunks/CpaGRn9L.js +0 -1
  326. package/web/.svelte-kit/output/client/_app/immutable/chunks/DG5RZBw-.js +0 -2
  327. package/web/.svelte-kit/output/client/_app/immutable/chunks/DQoygso7.js +0 -1
  328. package/web/.svelte-kit/output/client/_app/immutable/entry/start.D48mVn1m.js +0 -1
  329. package/web/.svelte-kit/output/client/_app/immutable/nodes/0.B-0CcADM.js +0 -1
  330. package/web/.svelte-kit/output/client/_app/immutable/nodes/1.FixKgvRO.js +0 -1
  331. package/web/.svelte-kit/output/client/_app/immutable/nodes/4.CQYJEgv8.js +0 -1
  332. package/web/.svelte-kit/output/server/_app/immutable/assets/_layout.GI4C4dpV.css +0 -1
  333. /package/templates/{gemini-claw/.gemini/skills → skills}/clawmini-jobs/SKILL.md +0 -0
@@ -12,6 +12,13 @@ const { mockClientInstance } = vi.hoisted(() => ({
12
12
  },
13
13
  }));
14
14
 
15
+ vi.mock('./state.js', () => ({
16
+ readDiscordState: vi
17
+ .fn()
18
+ .mockResolvedValue({ channelChatMap: { 'channel-123': { chatId: 'default' } } }),
19
+ updateDiscordState: vi.fn().mockResolvedValue(undefined),
20
+ }));
21
+
15
22
  vi.mock('discord.js', () => {
16
23
  return {
17
24
  Client: class {
@@ -22,6 +29,24 @@ vi.mock('discord.js', () => {
22
29
  Events: {
23
30
  ClientReady: 'ready',
24
31
  MessageCreate: 'messageCreate',
32
+ InteractionCreate: 'interactionCreate',
33
+ },
34
+ ActionRowBuilder: class {
35
+ addComponents = vi.fn().mockReturnThis();
36
+ },
37
+ ModalBuilder: class {
38
+ setCustomId = vi.fn().mockReturnThis();
39
+ setTitle = vi.fn().mockReturnThis();
40
+ addComponents = vi.fn().mockReturnThis();
41
+ },
42
+ TextInputBuilder: class {
43
+ setCustomId = vi.fn().mockReturnThis();
44
+ setLabel = vi.fn().mockReturnThis();
45
+ setStyle = vi.fn().mockReturnThis();
46
+ setRequired = vi.fn().mockReturnThis();
47
+ },
48
+ TextInputStyle: {
49
+ Paragraph: 2,
25
50
  },
26
51
  GatewayIntentBits: {
27
52
  Guilds: 1,
@@ -38,6 +63,7 @@ vi.mock('./config.js', () => ({
38
63
  readDiscordConfig: vi.fn(),
39
64
  initDiscordConfig: vi.fn(),
40
65
  isAuthorized: vi.fn(),
66
+ getDiscordConfigPath: vi.fn(),
41
67
  }));
42
68
 
43
69
  vi.mock('./client.js', () => ({
@@ -59,6 +85,8 @@ describe('Discord Adapter Entry Point', () => {
59
85
  botToken: 'test-token',
60
86
  authorizedUserId: 'user-123',
61
87
  chatId: 'default',
88
+ maxAttachmentSizeMB: 25,
89
+ requireMention: false,
62
90
  });
63
91
 
64
92
  // Reset the mock implementation to return the instance
@@ -101,6 +129,8 @@ describe('Discord Adapter Entry Point', () => {
101
129
  author: { id: 'user-123', tag: 'user#1234' },
102
130
  content: 'Hello daemon!',
103
131
  guild: null,
132
+ channelId: 'channel-123',
133
+ reply: vi.fn(),
104
134
  attachments: new Map(),
105
135
  };
106
136
 
@@ -153,6 +183,8 @@ describe('Discord Adapter Entry Point', () => {
153
183
  author: { id: 'user-123', tag: 'user#1234' } as unknown as import('discord.js').User,
154
184
  content: 'message 1',
155
185
  guild: null,
186
+ channelId: 'channel-123',
187
+ reply: vi.fn(),
156
188
  attachments: new Map(),
157
189
  } as unknown as import('discord.js').Message);
158
190
  await messageHandler({
@@ -160,6 +192,8 @@ describe('Discord Adapter Entry Point', () => {
160
192
  author: { id: 'user-123', tag: 'user#1234' } as unknown as import('discord.js').User,
161
193
  content: 'message 2',
162
194
  guild: null,
195
+ channelId: 'channel-123',
196
+ reply: vi.fn(),
163
197
  attachments: new Map(),
164
198
  } as unknown as import('discord.js').Message);
165
199
  }
@@ -209,6 +243,8 @@ describe('Discord Adapter Entry Point', () => {
209
243
  author: { id: 'user-evil', tag: 'evil#666' },
210
244
  content: 'Hack the daemon!',
211
245
  guild: null,
246
+ channelId: 'channel-123',
247
+ reply: vi.fn(),
212
248
  };
213
249
 
214
250
  const { isAuthorized } = await import('./config.js');
@@ -221,6 +257,123 @@ describe('Discord Adapter Entry Point', () => {
221
257
  expect(mockTrpc.sendMessage.mutate).not.toHaveBeenCalled();
222
258
  });
223
259
 
260
+ it('should process non-DM (guild) messages if requireMention is false', async () => {
261
+ let messageHandler: ((message: import('discord.js').Message) => Promise<void>) | undefined;
262
+ vi.mocked(mockClientInstance.on).mockImplementation(
263
+ (event: string, cb: (...args: unknown[]) => void) => {
264
+ if (event === 'messageCreate') {
265
+ messageHandler = cb as unknown as (
266
+ message: import('discord.js').Message
267
+ ) => Promise<void>;
268
+ }
269
+ return mockClientInstance as unknown as import('discord.js').Client;
270
+ }
271
+ );
272
+
273
+ const { main } = await import('./index.js');
274
+ await main();
275
+
276
+ const mockMessage = {
277
+ author: { id: 'user-123', tag: 'user#1234' },
278
+ content: 'Hack the daemon!',
279
+ guild: { id: 'guild-123' },
280
+ channelId: 'channel-123',
281
+ reply: vi.fn(),
282
+ attachments: new Map(),
283
+ };
284
+
285
+ const { isAuthorized } = await import('./config.js');
286
+ vi.mocked(isAuthorized).mockReturnValue(true);
287
+
288
+ if (messageHandler) {
289
+ await messageHandler(mockMessage as unknown as import('discord.js').Message);
290
+ }
291
+
292
+ expect(mockTrpc.sendMessage.mutate).toHaveBeenCalled();
293
+ });
294
+
295
+ it('should NOT process non-DM (guild) messages if channel requireMention is true and not mentioned', async () => {
296
+ let messageHandler: ((message: import('discord.js').Message) => Promise<void>) | undefined;
297
+ vi.mocked(mockClientInstance.on).mockImplementation(
298
+ (event: string, cb: (...args: unknown[]) => void) => {
299
+ if (event === 'messageCreate') {
300
+ messageHandler = cb as unknown as (
301
+ message: import('discord.js').Message
302
+ ) => Promise<void>;
303
+ }
304
+ return mockClientInstance as unknown as import('discord.js').Client;
305
+ }
306
+ );
307
+
308
+ const { main } = await import('./index.js');
309
+ await main();
310
+
311
+ const mockMessage = {
312
+ author: { id: 'user-123', tag: 'user#1234' },
313
+ content: 'Hack the daemon!',
314
+ guild: { id: 'guild-123' },
315
+ channelId: 'channel-123',
316
+ reply: vi.fn(),
317
+ mentions: { has: vi.fn().mockReturnValue(false) },
318
+ attachments: new Map(),
319
+ };
320
+
321
+ const { isAuthorized } = await import('./config.js');
322
+ vi.mocked(isAuthorized).mockReturnValue(true);
323
+
324
+ const { readDiscordState } = await import('./state.js');
325
+ vi.mocked(readDiscordState).mockResolvedValue({
326
+ channelChatMap: { 'channel-123': { chatId: 'default', requireMention: true } },
327
+ });
328
+
329
+ if (messageHandler) {
330
+ await messageHandler(mockMessage as unknown as import('discord.js').Message);
331
+ }
332
+
333
+ expect(mockTrpc.sendMessage.mutate).not.toHaveBeenCalled();
334
+ });
335
+
336
+ it('should process non-DM (guild) messages if channel requireMention is true and IS mentioned', async () => {
337
+ let messageHandler: ((message: import('discord.js').Message) => Promise<void>) | undefined;
338
+ vi.mocked(mockClientInstance.on).mockImplementation(
339
+ (event: string, cb: (...args: unknown[]) => void) => {
340
+ if (event === 'messageCreate') {
341
+ messageHandler = cb as unknown as (
342
+ message: import('discord.js').Message
343
+ ) => Promise<void>;
344
+ }
345
+ return mockClientInstance as unknown as import('discord.js').Client;
346
+ }
347
+ );
348
+
349
+ const { main } = await import('./index.js');
350
+ await main();
351
+
352
+ const mockMessage = {
353
+ author: { id: 'user-123', tag: 'user#1234' },
354
+ content: 'Hack the daemon!',
355
+ guild: { id: 'guild-123' },
356
+ channelId: 'channel-123',
357
+ reply: vi.fn(),
358
+ mentions: { has: vi.fn().mockReturnValue(true) },
359
+ attachments: new Map(),
360
+ };
361
+
362
+ const { isAuthorized } = await import('./config.js');
363
+ vi.mocked(isAuthorized).mockReturnValue(true);
364
+
365
+ const { readDiscordState } = await import('./state.js');
366
+ vi.mocked(readDiscordState).mockResolvedValue({
367
+ channelChatMap: { 'channel-123': { chatId: 'default', requireMention: true } },
368
+ });
369
+
370
+ if (messageHandler) {
371
+ await messageHandler(mockMessage as unknown as import('discord.js').Message);
372
+ }
373
+
374
+ expect(mockTrpc.sendMessage.mutate).toHaveBeenCalled();
375
+ });
376
+
224
377
  it('should download attachments and forward their paths', async () => {
225
378
  vi.useFakeTimers();
226
379
  let messageHandler: ((message: import('discord.js').Message) => Promise<void>) | undefined;
@@ -259,6 +412,8 @@ describe('Discord Adapter Entry Point', () => {
259
412
  author: { id: 'user-123', tag: 'user#1234' } as unknown as import('discord.js').User,
260
413
  content: 'Check out this file',
261
414
  guild: null,
415
+ channelId: 'channel-123',
416
+ reply: vi.fn(),
262
417
  attachments,
263
418
  } as unknown as import('discord.js').Message);
264
419
  }
@@ -319,6 +474,7 @@ describe('Discord Adapter Entry Point', () => {
319
474
  author: { id: 'user-123', tag: 'user#1234' } as unknown as import('discord.js').User,
320
475
  content: 'Check out this huge file',
321
476
  guild: null,
477
+ channelId: 'channel-123',
322
478
  attachments,
323
479
  reply: replyMock,
324
480
  } as unknown as import('discord.js').Message);
@@ -375,6 +531,8 @@ describe('Discord Adapter Entry Point', () => {
375
531
  content: "Yes, I'm in!",
376
532
  guild: null,
377
533
  attachments: new Map(),
534
+ channelId: 'channel-123',
535
+ reply: vi.fn(),
378
536
  reference: { messageId: '12345' },
379
537
  fetchReference: vi.fn().mockResolvedValue(mockReferencedMessage),
380
538
  } as unknown as import('discord.js').Message);
@@ -396,4 +554,124 @@ describe('Discord Adapter Entry Point', () => {
396
554
  });
397
555
  vi.useRealTimers();
398
556
  });
557
+
558
+ describe('Interaction Handling', () => {
559
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
560
+ let interactionHandler: ((interaction: any) => Promise<void>) | undefined;
561
+
562
+ beforeEach(async () => {
563
+ vi.mocked(mockClientInstance.on).mockImplementation(
564
+ (event: string, cb: (...args: unknown[]) => void) => {
565
+ if (event === 'interactionCreate') {
566
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
567
+ interactionHandler = cb as any;
568
+ }
569
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
570
+ return mockClientInstance as any;
571
+ }
572
+ );
573
+ const { main } = await import('./index.js');
574
+ await main();
575
+ const { isAuthorized } = await import('./config.js');
576
+ vi.mocked(isAuthorized).mockReturnValue(true);
577
+ });
578
+
579
+ it('should ignore non-button and non-modal interactions', async () => {
580
+ const mockInteraction = {
581
+ isButton: () => false,
582
+ isModalSubmit: () => false,
583
+ };
584
+ if (interactionHandler) await interactionHandler(mockInteraction);
585
+ expect(mockTrpc.sendMessage.mutate).not.toHaveBeenCalled();
586
+ });
587
+
588
+ it('should ignore unauthorized interactions', async () => {
589
+ const { isAuthorized } = await import('./config.js');
590
+ vi.mocked(isAuthorized).mockReturnValue(false);
591
+ const mockInteraction = {
592
+ isButton: () => true,
593
+ isModalSubmit: () => false,
594
+ isRepliable: () => true,
595
+ user: { id: 'unauth' },
596
+ reply: vi.fn(),
597
+ };
598
+ if (interactionHandler) await interactionHandler(mockInteraction);
599
+ expect(mockInteraction.reply).toHaveBeenCalledWith({
600
+ content: 'You are not authorized to perform this action.',
601
+ ephemeral: true,
602
+ });
603
+ expect(mockTrpc.sendMessage.mutate).not.toHaveBeenCalled();
604
+ });
605
+
606
+ it('should handle approve button interaction', async () => {
607
+ const mockInteraction = {
608
+ isButton: () => true,
609
+ isModalSubmit: () => false,
610
+ user: { id: 'user-123' },
611
+ customId: 'approve_123',
612
+ update: vi.fn(),
613
+ followUp: vi.fn(),
614
+ };
615
+ if (interactionHandler) await interactionHandler(mockInteraction);
616
+ expect(mockInteraction.update).toHaveBeenCalledWith({ components: [] });
617
+ expect(mockInteraction.followUp).toHaveBeenCalledWith({
618
+ content: 'Approving policy 123...',
619
+ ephemeral: true,
620
+ });
621
+ expect(mockTrpc.sendMessage.mutate).toHaveBeenCalledWith({
622
+ type: 'send-message',
623
+ client: 'cli',
624
+ data: {
625
+ message: '/approve 123',
626
+ chatId: 'default',
627
+ adapter: 'discord',
628
+ noWait: true,
629
+ },
630
+ });
631
+ });
632
+
633
+ it('should handle reject button interaction by showing a modal', async () => {
634
+ const mockInteraction = {
635
+ isButton: () => true,
636
+ isModalSubmit: () => false,
637
+ user: { id: 'user-123' },
638
+ customId: 'reject_123',
639
+ showModal: vi.fn(),
640
+ };
641
+ if (interactionHandler) await interactionHandler(mockInteraction);
642
+ expect(mockInteraction.showModal).toHaveBeenCalled();
643
+ });
644
+
645
+ it('should handle reject modal submit interaction', async () => {
646
+ const mockInteraction = {
647
+ isButton: () => false,
648
+ isModalSubmit: () => true,
649
+ isFromMessage: () => true,
650
+ user: { id: 'user-123' },
651
+ customId: 'modal_reject_123',
652
+ fields: {
653
+ getTextInputValue: vi.fn().mockReturnValue('bad policy'),
654
+ },
655
+ reply: vi.fn(),
656
+ update: vi.fn(),
657
+ followUp: vi.fn(),
658
+ };
659
+ if (interactionHandler) await interactionHandler(mockInteraction);
660
+ expect(mockInteraction.update).toHaveBeenCalledWith({ components: [] });
661
+ expect(mockInteraction.followUp).toHaveBeenCalledWith({
662
+ content: 'Rejecting policy 123...',
663
+ ephemeral: true,
664
+ });
665
+ expect(mockTrpc.sendMessage.mutate).toHaveBeenCalledWith({
666
+ type: 'send-message',
667
+ client: 'cli',
668
+ data: {
669
+ message: '/reject 123 bad policy',
670
+ chatId: 'default',
671
+ adapter: 'discord',
672
+ noWait: true,
673
+ },
674
+ });
675
+ });
676
+ });
399
677
  });
@@ -2,11 +2,16 @@
2
2
 
3
3
  import { Client, Events, GatewayIntentBits, Partials } from 'discord.js';
4
4
  import { readDiscordConfig, isAuthorized, initDiscordConfig } from './config.js';
5
+ import { readDiscordState, updateDiscordState } from './state.js';
6
+ import { handleDiscordInteraction } from './interactions.js';
5
7
  import { getTRPCClient } from './client.js';
6
8
  import { startDaemonToDiscordForwarder } from './forwarder.js';
7
9
  import { getClawminiDir } from '../shared/workspace.js';
10
+ import { handleAdapterCommand, type CommandTrpcClient } from '../shared/adapters/commands.js';
11
+ import { formatMessage, type FilteringConfig } from '../shared/adapters/filtering.js';
8
12
  import fs from 'node:fs/promises';
9
13
  import path from 'node:path';
14
+ import { handleRoutingCommand, type RoutingTrpcClient } from '../shared/adapters/routing.js';
10
15
 
11
16
  export async function main() {
12
17
  const args = process.argv.slice(2);
@@ -29,27 +34,69 @@ export async function main() {
29
34
  const trpc = getTRPCClient();
30
35
 
31
36
  const client = new Client({
32
- intents: [GatewayIntentBits.DirectMessages, GatewayIntentBits.MessageContent],
37
+ intents: [
38
+ GatewayIntentBits.DirectMessages,
39
+ GatewayIntentBits.MessageContent,
40
+ GatewayIntentBits.Guilds,
41
+ GatewayIntentBits.GuildMessages,
42
+ ],
33
43
  partials: [Partials.Channel],
34
44
  });
35
45
 
46
+ const state = await readDiscordState();
47
+ const filteringConfig: FilteringConfig = { filters: state.filters };
48
+
36
49
  client.once(Events.ClientReady, (readyClient) => {
37
50
  console.log(`Ready! Logged in as ${readyClient.user.tag}`);
38
51
 
39
52
  // Start forwarding from daemon to Discord
40
- startDaemonToDiscordForwarder(readyClient, trpc, config.authorizedUserId, config.chatId).catch(
41
- (error) => {
42
- console.error('Error in daemon-to-discord forwarder:', error);
43
- }
44
- );
53
+ startDaemonToDiscordForwarder(readyClient, trpc, config.authorizedUserId, {
54
+ chatId: config.chatId,
55
+ config: filteringConfig,
56
+ }).catch((error) => {
57
+ console.error('Error in daemon-to-discord forwarder:', error);
58
+ });
45
59
  });
46
60
 
47
61
  client.on(Events.MessageCreate, async (message) => {
48
62
  // Ignore messages from the bot itself
49
63
  if (message.author.id === client.user?.id) return;
64
+ if (message.author.bot) return;
65
+
66
+ const externalContextId = message.channelId;
67
+ const currentState = await readDiscordState();
68
+ const mappedChatId = currentState.channelChatMap?.[externalContextId]?.chatId;
69
+ const isRoutingCommand =
70
+ message.content.startsWith('/chat') || message.content.startsWith('/agent');
71
+
72
+ // Enforce requireMention config for guild messages
73
+ if (message.guild) {
74
+ const channelConfig = currentState.channelChatMap?.[externalContextId];
75
+ const requiresMention =
76
+ channelConfig?.requireMention !== undefined
77
+ ? channelConfig.requireMention
78
+ : config.requireMention;
79
+
80
+ if (requiresMention) {
81
+ const isMentioned = message.mentions.has(client.user!.id);
82
+ let isReplyToBot = false;
50
83
 
51
- // Only handle DM messages
52
- if (message.guild) return;
84
+ if (message.reference && message.reference.messageId) {
85
+ try {
86
+ const referencedMessage = await message.channel.messages.fetch(
87
+ message.reference.messageId
88
+ );
89
+ isReplyToBot = referencedMessage.author.id === client.user!.id;
90
+ } catch (err) {
91
+ console.error('Failed to fetch referenced message for mention check:', err);
92
+ }
93
+ }
94
+
95
+ if (!isMentioned && !isReplyToBot) {
96
+ return;
97
+ }
98
+ }
99
+ }
53
100
 
54
101
  // Check if the user is authorized
55
102
  if (!isAuthorized(message.author.id, config.authorizedUserId)) {
@@ -61,6 +108,102 @@ export async function main() {
61
108
 
62
109
  console.log(`Received message from ${message.author.tag}: ${message.content}`);
63
110
 
111
+ if (isRoutingCommand) {
112
+ const stringChatMap = Object.fromEntries(
113
+ Object.entries(currentState.channelChatMap || {}).map(([k, v]) => [k, v.chatId || ''])
114
+ );
115
+ const routingResult = await handleRoutingCommand(
116
+ message.content,
117
+ externalContextId,
118
+ stringChatMap,
119
+ 'discord',
120
+ trpc as unknown as RoutingTrpcClient
121
+ );
122
+
123
+ if (routingResult) {
124
+ if (routingResult.type === 'mapped') {
125
+ await updateDiscordState((latestState) => ({
126
+ channelChatMap: {
127
+ ...(latestState.channelChatMap || {}),
128
+ [externalContextId]: {
129
+ ...(latestState.channelChatMap?.[externalContextId] || {}),
130
+ chatId: routingResult.newChatId,
131
+ },
132
+ },
133
+ }));
134
+ }
135
+ await message.reply(routingResult.text);
136
+ return;
137
+ }
138
+ }
139
+
140
+ let targetChatId = mappedChatId;
141
+
142
+ if (!targetChatId && !isRoutingCommand) {
143
+ const isFirstEverMessage =
144
+ !currentState.channelChatMap ||
145
+ Object.values(currentState.channelChatMap).every((entry) => !entry.chatId);
146
+
147
+ if (isFirstEverMessage) {
148
+ targetChatId = config.chatId || 'default';
149
+ console.log(
150
+ `First contact detected. Automatically mapping channel ${externalContextId} to chat ${targetChatId}.`
151
+ );
152
+ await updateDiscordState((latestState) => ({
153
+ channelChatMap: {
154
+ ...(latestState.channelChatMap || {}),
155
+ [externalContextId]: {
156
+ ...(latestState.channelChatMap?.[externalContextId] || {}),
157
+ chatId: targetChatId as string,
158
+ },
159
+ },
160
+ }));
161
+ } else {
162
+ const isDirectMessage = !message.guild;
163
+ const isMentioned = message.mentions.has(client.user!.id);
164
+ const isSlashCommand = message.content.startsWith('/');
165
+ if (isDirectMessage || isMentioned || isSlashCommand) {
166
+ console.log(`Unmapped channel ${externalContextId}, sending first contact warning.`);
167
+ await message.reply(
168
+ 'This channel/space is not currently mapped to a daemon chat. Please use `/chat [chat-id]` or `/agent [agent-id]` to map it.'
169
+ );
170
+ } else {
171
+ console.log(
172
+ `Unmapped channel ${externalContextId}, silently ignoring background message.`
173
+ );
174
+ }
175
+ return;
176
+ }
177
+ }
178
+
179
+ // Fallback typing safeguard
180
+ if (!targetChatId) targetChatId = config.chatId || 'default';
181
+
182
+ const commandResult = await handleAdapterCommand(
183
+ message.content,
184
+ filteringConfig,
185
+ trpc as unknown as CommandTrpcClient,
186
+ targetChatId
187
+ );
188
+
189
+ if (commandResult) {
190
+ if (commandResult.type === 'text') {
191
+ if (commandResult.newConfig) {
192
+ filteringConfig.filters = commandResult.newConfig.filters;
193
+ await updateDiscordState({ filters: filteringConfig.filters });
194
+ }
195
+ await message.reply(commandResult.text);
196
+ } else if (commandResult.type === 'debug') {
197
+ const formatted =
198
+ commandResult.messages.length === 0
199
+ ? 'No ignored background messages found.'
200
+ : `**Debug Output (${commandResult.messages.length} ignored messages):**\n\n` +
201
+ commandResult.messages.map((msg) => formatMessage(msg)).join('\n\n---\n\n');
202
+ await message.reply(formatted);
203
+ }
204
+ return;
205
+ }
206
+
64
207
  const downloadedFiles: string[] = [];
65
208
  if (message.attachments.size > 0) {
66
209
  const tmpDir = path.join(getClawminiDir(process.cwd()), 'tmp', 'discord');
@@ -121,7 +264,7 @@ export async function main() {
121
264
  client: 'cli',
122
265
  data: {
123
266
  message: finalContent,
124
- chatId: config.chatId,
267
+ chatId: targetChatId,
125
268
  files: downloadedFiles.length > 0 ? downloadedFiles : undefined,
126
269
  adapter: 'discord',
127
270
  noWait: true,
@@ -133,6 +276,10 @@ export async function main() {
133
276
  }
134
277
  });
135
278
 
279
+ client.on(Events.InteractionCreate, async (interaction) => {
280
+ await handleDiscordInteraction(interaction, config, trpc as unknown as CommandTrpcClient);
281
+ });
282
+
136
283
  try {
137
284
  await client.login(config.botToken);
138
285
  } catch (error) {
@@ -141,24 +288,7 @@ export async function main() {
141
288
  }
142
289
  }
143
290
 
144
- import { fileURLToPath } from 'node:url';
145
-
146
- const isMainModule = (() => {
147
- try {
148
- if (typeof process === 'undefined' || !process.argv || process.argv.length < 2) return false;
149
- const argv1 = process.argv[1];
150
- if (!argv1) return false;
151
- const p1 = path.resolve(argv1);
152
- const p2 = path.resolve(fileURLToPath(import.meta.url));
153
- return p1 === p2;
154
- } catch {
155
- return false;
156
- }
157
- })();
158
-
159
- if (isMainModule) {
160
- main().catch((error) => {
161
- console.error('Unhandled error in Discord Adapter:', error);
162
- process.exit(1);
163
- });
164
- }
291
+ main().catch((error) => {
292
+ console.error('Unhandled error in Discord Adapter:', error);
293
+ process.exit(1);
294
+ });
@@ -0,0 +1,96 @@
1
+ import { describe, it, expect, vi, beforeEach } from 'vitest';
2
+ import { handleDiscordInteraction } from './interactions.js';
3
+ import { readDiscordState } from './state.js';
4
+
5
+ vi.mock('./state.js', () => ({
6
+ readDiscordState: vi.fn(),
7
+ }));
8
+
9
+ describe('handleDiscordInteraction', () => {
10
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
11
+ let mockTrpc: any;
12
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
13
+ let mockInteraction: any;
14
+
15
+ beforeEach(() => {
16
+ vi.clearAllMocks();
17
+ mockTrpc = {
18
+ sendMessage: {
19
+ mutate: vi.fn().mockResolvedValue({}),
20
+ },
21
+ };
22
+ mockInteraction = {
23
+ isButton: vi.fn().mockReturnValue(true),
24
+ isModalSubmit: vi.fn().mockReturnValue(false),
25
+ user: { id: 'user-1' },
26
+ customId: '',
27
+ channelId: 'channel-1',
28
+ update: vi.fn().mockResolvedValue({}),
29
+ followUp: vi.fn().mockResolvedValue({}),
30
+ showModal: vi.fn().mockResolvedValue({}),
31
+ };
32
+ vi.mocked(readDiscordState).mockResolvedValue({});
33
+ });
34
+
35
+ const config = {
36
+ authorizedUserId: 'user-1',
37
+ botToken: 'token',
38
+ clientId: 'client',
39
+ chatId: 'default',
40
+ maxAttachmentSizeMB: 10,
41
+ requireMention: false,
42
+ };
43
+
44
+ it('routes approve to explicit chat if provided', async () => {
45
+ mockInteraction.customId = 'approve|policy-1|explicit-chat';
46
+ await handleDiscordInteraction(mockInteraction, config, mockTrpc);
47
+
48
+ expect(mockTrpc.sendMessage.mutate).toHaveBeenCalledWith(
49
+ expect.objectContaining({
50
+ data: expect.objectContaining({
51
+ chatId: 'explicit-chat',
52
+ }),
53
+ })
54
+ );
55
+ });
56
+
57
+ it('routes approve to channel mapped chat if explicit not provided', async () => {
58
+ mockInteraction.customId = 'approve_policy-1';
59
+ vi.mocked(readDiscordState).mockResolvedValue({
60
+ channelChatMap: { 'channel-1': { chatId: 'mapped-chat' } },
61
+ });
62
+ await handleDiscordInteraction(mockInteraction, config, mockTrpc);
63
+
64
+ expect(mockTrpc.sendMessage.mutate).toHaveBeenCalledWith(
65
+ expect.objectContaining({
66
+ data: expect.objectContaining({
67
+ chatId: 'mapped-chat',
68
+ }),
69
+ })
70
+ );
71
+ });
72
+
73
+ it('routes modal_reject to mapped chat if explicit chat ID is empty string', async () => {
74
+ mockInteraction.isButton.mockReturnValue(false);
75
+ mockInteraction.isModalSubmit.mockReturnValue(true);
76
+ mockInteraction.isFromMessage = vi.fn().mockReturnValue(true);
77
+ mockInteraction.fields = {
78
+ getTextInputValue: vi.fn().mockReturnValue('nope'),
79
+ };
80
+ mockInteraction.customId = 'modal_reject|policy-1|';
81
+
82
+ vi.mocked(readDiscordState).mockResolvedValue({
83
+ channelChatMap: { 'channel-1': { chatId: 'mapped-chat' } },
84
+ });
85
+
86
+ await handleDiscordInteraction(mockInteraction, config, mockTrpc);
87
+
88
+ expect(mockTrpc.sendMessage.mutate).toHaveBeenCalledWith(
89
+ expect.objectContaining({
90
+ data: expect.objectContaining({
91
+ chatId: 'mapped-chat',
92
+ }),
93
+ })
94
+ );
95
+ });
96
+ });