clawmini 0.0.1

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 (469) hide show
  1. package/.gemini/settings.json +46 -0
  2. package/.prettierrc +7 -0
  3. package/GEMINI.md +11 -0
  4. package/README.md +137 -0
  5. package/dist/adapter-discord/index.d.mts +5 -0
  6. package/dist/adapter-discord/index.d.mts.map +1 -0
  7. package/dist/adapter-discord/index.mjs +456 -0
  8. package/dist/adapter-discord/index.mjs.map +1 -0
  9. package/dist/chats-DKgTeU7i.mjs +91 -0
  10. package/dist/chats-DKgTeU7i.mjs.map +1 -0
  11. package/dist/chats-Zd_HXDHx.mjs +29 -0
  12. package/dist/chats-Zd_HXDHx.mjs.map +1 -0
  13. package/dist/cli/index.d.mts +1 -0
  14. package/dist/cli/index.mjs +850 -0
  15. package/dist/cli/index.mjs.map +1 -0
  16. package/dist/cli/lite.d.mts +1 -0
  17. package/dist/cli/lite.mjs +4434 -0
  18. package/dist/cli/lite.mjs.map +1 -0
  19. package/dist/daemon/index.d.mts +5 -0
  20. package/dist/daemon/index.d.mts.map +1 -0
  21. package/dist/daemon/index.mjs +1222 -0
  22. package/dist/daemon/index.mjs.map +1 -0
  23. package/dist/fetch-BjZVyU3Z.mjs +37 -0
  24. package/dist/fetch-BjZVyU3Z.mjs.map +1 -0
  25. package/dist/fs-B5wW0oaH.mjs +14 -0
  26. package/dist/fs-B5wW0oaH.mjs.map +1 -0
  27. package/dist/lite-Dl7WXyaH.mjs +80 -0
  28. package/dist/lite-Dl7WXyaH.mjs.map +1 -0
  29. package/dist/rolldown-runtime-95iHPtFO.mjs +18 -0
  30. package/dist/web/_app/env.js +1 -0
  31. package/dist/web/_app/immutable/assets/0.GI4C4dpV.css +1 -0
  32. package/dist/web/_app/immutable/chunks/B5abRDXp.js +1 -0
  33. package/dist/web/_app/immutable/chunks/B8yYFADm.js +1 -0
  34. package/dist/web/_app/immutable/chunks/BPy8HLo7.js +5 -0
  35. package/dist/web/_app/immutable/chunks/Bi0jeV7Q.js +1 -0
  36. package/dist/web/_app/immutable/chunks/BmUXQ3wy.js +2 -0
  37. package/dist/web/_app/immutable/chunks/C3k55nDF.js +1 -0
  38. package/dist/web/_app/immutable/chunks/COekwvP2.js +1 -0
  39. package/dist/web/_app/immutable/chunks/CSvS_NwK.js +1 -0
  40. package/dist/web/_app/immutable/chunks/CpaGRn9L.js +1 -0
  41. package/dist/web/_app/immutable/chunks/CyNaE55B.js +1 -0
  42. package/dist/web/_app/immutable/chunks/DG5RZBw-.js +2 -0
  43. package/dist/web/_app/immutable/chunks/Dc-UOHw9.js +1 -0
  44. package/dist/web/_app/immutable/chunks/DcrmIfTj.js +1 -0
  45. package/dist/web/_app/immutable/chunks/ZkLyk0mE.js +1 -0
  46. package/dist/web/_app/immutable/entry/app.B-vZe7PN.js +2 -0
  47. package/dist/web/_app/immutable/entry/start.oP1AgKhs.js +1 -0
  48. package/dist/web/_app/immutable/nodes/0.B5WFN0zw.js +1 -0
  49. package/dist/web/_app/immutable/nodes/1.D1wtJb2k.js +1 -0
  50. package/dist/web/_app/immutable/nodes/2.CK3CLC0f.js +1 -0
  51. package/dist/web/_app/immutable/nodes/3.BB5wCoBf.js +4 -0
  52. package/dist/web/_app/immutable/nodes/4.Dr2jvAXK.js +1 -0
  53. package/dist/web/_app/immutable/nodes/5.BJl7oM3b.js +1 -0
  54. package/dist/web/_app/version.json +1 -0
  55. package/dist/web/index.html +37 -0
  56. package/dist/web/robots.txt +3 -0
  57. package/dist/workspace-CSgfo_2J.mjs +383 -0
  58. package/dist/workspace-CSgfo_2J.mjs.map +1 -0
  59. package/docs/01_chats/development_log.md +36 -0
  60. package/docs/01_chats/notes.md +27 -0
  61. package/docs/01_chats/prd.md +47 -0
  62. package/docs/01_chats/questions.md +19 -0
  63. package/docs/01_chats/tickets.md +67 -0
  64. package/docs/02_sessions/development_log.md +79 -0
  65. package/docs/02_sessions/notes.md +40 -0
  66. package/docs/02_sessions/prd.md +75 -0
  67. package/docs/02_sessions/questions.md +7 -0
  68. package/docs/02_sessions/tickets.md +68 -0
  69. package/docs/03_web_interface/development_log.md +60 -0
  70. package/docs/03_web_interface/notes.md +29 -0
  71. package/docs/03_web_interface/prd.md +42 -0
  72. package/docs/03_web_interface/questions.md +8 -0
  73. package/docs/03_web_interface/tickets.md +59 -0
  74. package/docs/04_agents/development_log.md +54 -0
  75. package/docs/04_agents/notes.md +45 -0
  76. package/docs/04_agents/prd.md +47 -0
  77. package/docs/04_agents/questions.md +13 -0
  78. package/docs/04_agents/tickets.md +107 -0
  79. package/docs/05_routers/development_log.md +13 -0
  80. package/docs/05_routers/notes.md +40 -0
  81. package/docs/05_routers/prd.md +55 -0
  82. package/docs/05_routers/questions.md +21 -0
  83. package/docs/05_routers/tickets.md +109 -0
  84. package/docs/06_agent_templates/development_log.md +38 -0
  85. package/docs/06_agent_templates/notes.md +25 -0
  86. package/docs/06_agent_templates/prd.md +34 -0
  87. package/docs/06_agent_templates/questions.md +11 -0
  88. package/docs/06_agent_templates/tickets.md +49 -0
  89. package/docs/06_cron/development_log.md +51 -0
  90. package/docs/06_cron/notes.md +14 -0
  91. package/docs/06_cron/prd.md +92 -0
  92. package/docs/06_cron/questions.md +15 -0
  93. package/docs/06_cron/tickets.md +75 -0
  94. package/docs/07_web_chat_ux/development_log.md +30 -0
  95. package/docs/07_web_chat_ux/notes.md +25 -0
  96. package/docs/07_web_chat_ux/prd.md +46 -0
  97. package/docs/07_web_chat_ux/questions.md +7 -0
  98. package/docs/07_web_chat_ux/tickets.md +48 -0
  99. package/docs/08_agent_api/development_log.md +52 -0
  100. package/docs/08_agent_api/notes.md +31 -0
  101. package/docs/08_agent_api/prd.md +56 -0
  102. package/docs/08_agent_api/questions.md +14 -0
  103. package/docs/08_agent_api/tickets.md +104 -0
  104. package/docs/09_agent_fallbacks/development_log.md +52 -0
  105. package/docs/09_agent_fallbacks/notes.md +40 -0
  106. package/docs/09_agent_fallbacks/prd.md +55 -0
  107. package/docs/09_agent_fallbacks/questions.md +10 -0
  108. package/docs/09_agent_fallbacks/tickets.md +88 -0
  109. package/docs/09_discord_adapter/development_log.md +95 -0
  110. package/docs/09_discord_adapter/notes.md +18 -0
  111. package/docs/09_discord_adapter/prd.md +57 -0
  112. package/docs/09_discord_adapter/questions.md +16 -0
  113. package/docs/09_discord_adapter/tickets.md +116 -0
  114. package/docs/10_file_attachments/development_log.md +55 -0
  115. package/docs/10_file_attachments/notes.md +59 -0
  116. package/docs/10_file_attachments/prd.md +73 -0
  117. package/docs/10_file_attachments/questions.md +15 -0
  118. package/docs/10_file_attachments/tickets.md +88 -0
  119. package/docs/11_message_verbosity/development_log.md +43 -0
  120. package/docs/11_message_verbosity/notes.md +26 -0
  121. package/docs/11_message_verbosity/prd.md +44 -0
  122. package/docs/11_message_verbosity/questions.md +8 -0
  123. package/docs/11_message_verbosity/tickets.md +33 -0
  124. package/docs/12_environments/development_log.md +43 -0
  125. package/docs/12_environments/notes.md +45 -0
  126. package/docs/12_environments/prd.md +113 -0
  127. package/docs/12_environments/questions.md +17 -0
  128. package/docs/12_environments/tickets.md +87 -0
  129. package/docs/12_setup_flow_improvements/development_log.md +40 -0
  130. package/docs/12_setup_flow_improvements/notes.md +34 -0
  131. package/docs/12_setup_flow_improvements/prd.md +35 -0
  132. package/docs/12_setup_flow_improvements/questions.md +8 -0
  133. package/docs/12_setup_flow_improvements/tickets.md +122 -0
  134. package/docs/13_discord_typing_indicators/development_log.md +38 -0
  135. package/docs/13_discord_typing_indicators/notes.md +18 -0
  136. package/docs/13_discord_typing_indicators/prd.md +41 -0
  137. package/docs/13_discord_typing_indicators/questions.md +6 -0
  138. package/docs/13_discord_typing_indicators/tickets.md +60 -0
  139. package/docs/14_interruptions/development_log.md +50 -0
  140. package/docs/14_interruptions/notes.md +38 -0
  141. package/docs/14_interruptions/prd.md +46 -0
  142. package/docs/14_interruptions/questions.md +12 -0
  143. package/docs/14_interruptions/tickets.md +69 -0
  144. package/docs/15_sandbox_policies/development_log.md +95 -0
  145. package/docs/15_sandbox_policies/notes.md +33 -0
  146. package/docs/15_sandbox_policies/prd.md +163 -0
  147. package/docs/15_sandbox_policies/questions.md +10 -0
  148. package/docs/15_sandbox_policies/tickets.md +196 -0
  149. package/docs/CHECKS.md +9 -0
  150. package/docs/guides/discord_adapter_setup.md +69 -0
  151. package/docs/guides/sandbox_policies.md +76 -0
  152. package/eslint.config.js +47 -0
  153. package/napkin.md +21 -0
  154. package/package.json +50 -0
  155. package/scripts/create_worktree.sh +49 -0
  156. package/scripts/get_pr_comments.sh +36 -0
  157. package/src/adapter-discord/client.test.ts +65 -0
  158. package/src/adapter-discord/client.ts +41 -0
  159. package/src/adapter-discord/config.test.ts +156 -0
  160. package/src/adapter-discord/config.ts +61 -0
  161. package/src/adapter-discord/forwarder.test.ts +493 -0
  162. package/src/adapter-discord/forwarder.ts +246 -0
  163. package/src/adapter-discord/index.test.ts +399 -0
  164. package/src/adapter-discord/index.ts +147 -0
  165. package/src/adapter-discord/state.test.ts +65 -0
  166. package/src/adapter-discord/state.ts +44 -0
  167. package/src/cli/client.ts +46 -0
  168. package/src/cli/commands/agents.ts +138 -0
  169. package/src/cli/commands/chats.ts +79 -0
  170. package/src/cli/commands/down.ts +32 -0
  171. package/src/cli/commands/environments.ts +39 -0
  172. package/src/cli/commands/export-lite.ts +62 -0
  173. package/src/cli/commands/init.ts +79 -0
  174. package/src/cli/commands/jobs.ts +141 -0
  175. package/src/cli/commands/messages.ts +103 -0
  176. package/src/cli/commands/up.ts +26 -0
  177. package/src/cli/commands/web-api/agents.ts +138 -0
  178. package/src/cli/commands/web-api/chats.ts +213 -0
  179. package/src/cli/commands/web-api/utils.ts +27 -0
  180. package/src/cli/commands/web.ts +105 -0
  181. package/src/cli/e2e/adapter-discord.test.ts +76 -0
  182. package/src/cli/e2e/agents.test.ts +140 -0
  183. package/src/cli/e2e/basic.test.ts +43 -0
  184. package/src/cli/e2e/cron.test.ts +132 -0
  185. package/src/cli/e2e/daemon.test.ts +293 -0
  186. package/src/cli/e2e/environments.test.ts +66 -0
  187. package/src/cli/e2e/export-lite-func.test.ts +155 -0
  188. package/src/cli/e2e/export-lite.test.ts +51 -0
  189. package/src/cli/e2e/fallbacks.test.ts +169 -0
  190. package/src/cli/e2e/global-setup.ts +15 -0
  191. package/src/cli/e2e/init.test.ts +70 -0
  192. package/src/cli/e2e/messages.test.ts +294 -0
  193. package/src/cli/e2e/requests.test.ts +165 -0
  194. package/src/cli/e2e/utils.ts +66 -0
  195. package/src/cli/index.test.ts +7 -0
  196. package/src/cli/index.ts +29 -0
  197. package/src/cli/lite.ts +247 -0
  198. package/src/cli/utils.ts +4 -0
  199. package/src/daemon/auth.test.ts +50 -0
  200. package/src/daemon/auth.ts +69 -0
  201. package/src/daemon/chats.ts +26 -0
  202. package/src/daemon/cron.test.ts +28 -0
  203. package/src/daemon/cron.ts +159 -0
  204. package/src/daemon/events.ts +15 -0
  205. package/src/daemon/index.ts +212 -0
  206. package/src/daemon/message-agent.test.ts +132 -0
  207. package/src/daemon/message-extraction.test.ts +166 -0
  208. package/src/daemon/message-fallbacks.test.ts +313 -0
  209. package/src/daemon/message-interruption.test.ts +125 -0
  210. package/src/daemon/message-queue.test.ts +143 -0
  211. package/src/daemon/message-router.test.ts +106 -0
  212. package/src/daemon/message-session.test.ts +127 -0
  213. package/src/daemon/message-test-utils.ts +41 -0
  214. package/src/daemon/message-typing.test.ts +93 -0
  215. package/src/daemon/message-verbosity.test.ts +127 -0
  216. package/src/daemon/message.ts +600 -0
  217. package/src/daemon/observation.test.ts +118 -0
  218. package/src/daemon/policy-request-service.test.ts +87 -0
  219. package/src/daemon/policy-request-service.ts +62 -0
  220. package/src/daemon/policy-utils.test.ts +138 -0
  221. package/src/daemon/policy-utils.ts +152 -0
  222. package/src/daemon/queue.test.ts +89 -0
  223. package/src/daemon/queue.ts +87 -0
  224. package/src/daemon/request-store.test.ts +103 -0
  225. package/src/daemon/request-store.ts +96 -0
  226. package/src/daemon/router-policy-request.test.ts +99 -0
  227. package/src/daemon/router.test.ts +380 -0
  228. package/src/daemon/router.ts +510 -0
  229. package/src/daemon/routers/slash-command.test.ts +145 -0
  230. package/src/daemon/routers/slash-command.ts +58 -0
  231. package/src/daemon/routers/slash-interrupt.test.ts +30 -0
  232. package/src/daemon/routers/slash-interrupt.ts +7 -0
  233. package/src/daemon/routers/slash-new.test.ts +59 -0
  234. package/src/daemon/routers/slash-new.ts +14 -0
  235. package/src/daemon/routers/slash-policies.test.ts +167 -0
  236. package/src/daemon/routers/slash-policies.ts +131 -0
  237. package/src/daemon/routers/slash-stop.test.ts +30 -0
  238. package/src/daemon/routers/slash-stop.ts +3 -0
  239. package/src/daemon/routers/types.ts +10 -0
  240. package/src/daemon/routers/utils.ts +22 -0
  241. package/src/daemon/routers.test.ts +141 -0
  242. package/src/daemon/routers.ts +115 -0
  243. package/src/daemon/utils/spawn.ts +61 -0
  244. package/src/shared/agent-utils.ts +30 -0
  245. package/src/shared/chats.test.ts +112 -0
  246. package/src/shared/chats.ts +164 -0
  247. package/src/shared/config.test.ts +90 -0
  248. package/src/shared/config.ts +100 -0
  249. package/src/shared/event-source.ts +121 -0
  250. package/src/shared/fetch.ts +45 -0
  251. package/src/shared/lite.ts +129 -0
  252. package/src/shared/policies.ts +24 -0
  253. package/src/shared/utils/env.ts +27 -0
  254. package/src/shared/utils/fs.ts +13 -0
  255. package/src/shared/workspace.test.ts +345 -0
  256. package/src/shared/workspace.ts +500 -0
  257. package/templates/environments/cladding/env.json +7 -0
  258. package/templates/environments/macos/env.json +8 -0
  259. package/templates/environments/macos/sandbox.sb +21 -0
  260. package/templates/environments/macos-proxy/allowlist.txt +1 -0
  261. package/templates/environments/macos-proxy/env.json +14 -0
  262. package/templates/environments/macos-proxy/proxy.mjs +86 -0
  263. package/templates/environments/macos-proxy/sandbox.sb +34 -0
  264. package/templates/gemini/settings.json +11 -0
  265. package/templates/gemini-claw/.gemini/hooks/clawmini-logging.sh +17 -0
  266. package/templates/gemini-claw/.gemini/settings.json +24 -0
  267. package/templates/gemini-claw/.gemini/skills/clawmini-jobs/SKILL.md +40 -0
  268. package/templates/gemini-claw/.gemini/system.md +98 -0
  269. package/templates/gemini-claw/BOOTSTRAP.md +54 -0
  270. package/templates/gemini-claw/GEMINI.md +107 -0
  271. package/templates/gemini-claw/HEARTBEAT.md +3 -0
  272. package/templates/gemini-claw/MEMORY.md +2 -0
  273. package/templates/gemini-claw/SOUL.md +42 -0
  274. package/templates/gemini-claw/TOOLS.md +38 -0
  275. package/templates/gemini-claw/USER.md +15 -0
  276. package/templates/gemini-claw/memory/.gitkeep +0 -0
  277. package/templates/gemini-claw/settings.json +24 -0
  278. package/templates/opencode/settings.json +11 -0
  279. package/tsconfig.json +42 -0
  280. package/tsdown.config.ts +19 -0
  281. package/vitest.config.ts +9 -0
  282. package/web/.svelte-kit/ambient.d.ts +382 -0
  283. package/web/.svelte-kit/generated/client/app.js +35 -0
  284. package/web/.svelte-kit/generated/client/matchers.js +1 -0
  285. package/web/.svelte-kit/generated/client/nodes/0.js +3 -0
  286. package/web/.svelte-kit/generated/client/nodes/1.js +1 -0
  287. package/web/.svelte-kit/generated/client/nodes/2.js +1 -0
  288. package/web/.svelte-kit/generated/client/nodes/3.js +1 -0
  289. package/web/.svelte-kit/generated/client/nodes/4.js +3 -0
  290. package/web/.svelte-kit/generated/client/nodes/5.js +3 -0
  291. package/web/.svelte-kit/generated/client-optimized/app.js +35 -0
  292. package/web/.svelte-kit/generated/client-optimized/matchers.js +1 -0
  293. package/web/.svelte-kit/generated/client-optimized/nodes/0.js +3 -0
  294. package/web/.svelte-kit/generated/client-optimized/nodes/1.js +1 -0
  295. package/web/.svelte-kit/generated/client-optimized/nodes/2.js +1 -0
  296. package/web/.svelte-kit/generated/client-optimized/nodes/3.js +1 -0
  297. package/web/.svelte-kit/generated/client-optimized/nodes/4.js +3 -0
  298. package/web/.svelte-kit/generated/client-optimized/nodes/5.js +3 -0
  299. package/web/.svelte-kit/generated/root.js +3 -0
  300. package/web/.svelte-kit/generated/root.svelte +68 -0
  301. package/web/.svelte-kit/generated/server/internal.js +53 -0
  302. package/web/.svelte-kit/non-ambient.d.ts +46 -0
  303. package/web/.svelte-kit/output/client/.vite/manifest.json +251 -0
  304. package/web/.svelte-kit/output/client/_app/immutable/assets/0.GI4C4dpV.css +1 -0
  305. package/web/.svelte-kit/output/client/_app/immutable/chunks/B5abRDXp.js +1 -0
  306. package/web/.svelte-kit/output/client/_app/immutable/chunks/B8yYFADm.js +1 -0
  307. package/web/.svelte-kit/output/client/_app/immutable/chunks/BPy8HLo7.js +5 -0
  308. package/web/.svelte-kit/output/client/_app/immutable/chunks/Bi0jeV7Q.js +1 -0
  309. package/web/.svelte-kit/output/client/_app/immutable/chunks/BmUXQ3wy.js +2 -0
  310. package/web/.svelte-kit/output/client/_app/immutable/chunks/C3k55nDF.js +1 -0
  311. package/web/.svelte-kit/output/client/_app/immutable/chunks/COekwvP2.js +1 -0
  312. package/web/.svelte-kit/output/client/_app/immutable/chunks/CSvS_NwK.js +1 -0
  313. package/web/.svelte-kit/output/client/_app/immutable/chunks/CpaGRn9L.js +1 -0
  314. package/web/.svelte-kit/output/client/_app/immutable/chunks/CyNaE55B.js +1 -0
  315. package/web/.svelte-kit/output/client/_app/immutable/chunks/DG5RZBw-.js +2 -0
  316. package/web/.svelte-kit/output/client/_app/immutable/chunks/Dc-UOHw9.js +1 -0
  317. package/web/.svelte-kit/output/client/_app/immutable/chunks/DcrmIfTj.js +1 -0
  318. package/web/.svelte-kit/output/client/_app/immutable/chunks/ZkLyk0mE.js +1 -0
  319. package/web/.svelte-kit/output/client/_app/immutable/entry/app.B-vZe7PN.js +2 -0
  320. package/web/.svelte-kit/output/client/_app/immutable/entry/start.oP1AgKhs.js +1 -0
  321. package/web/.svelte-kit/output/client/_app/immutable/nodes/0.B5WFN0zw.js +1 -0
  322. package/web/.svelte-kit/output/client/_app/immutable/nodes/1.D1wtJb2k.js +1 -0
  323. package/web/.svelte-kit/output/client/_app/immutable/nodes/2.CK3CLC0f.js +1 -0
  324. package/web/.svelte-kit/output/client/_app/immutable/nodes/3.BB5wCoBf.js +4 -0
  325. package/web/.svelte-kit/output/client/_app/immutable/nodes/4.Dr2jvAXK.js +1 -0
  326. package/web/.svelte-kit/output/client/_app/immutable/nodes/5.BJl7oM3b.js +1 -0
  327. package/web/.svelte-kit/output/client/_app/version.json +1 -0
  328. package/web/.svelte-kit/output/client/robots.txt +3 -0
  329. package/web/.svelte-kit/output/prerendered/dependencies/_app/env.js +1 -0
  330. package/web/.svelte-kit/output/server/.vite/manifest.json +215 -0
  331. package/web/.svelte-kit/output/server/_app/immutable/assets/_layout.GI4C4dpV.css +1 -0
  332. package/web/.svelte-kit/output/server/chunks/Icon.js +153 -0
  333. package/web/.svelte-kit/output/server/chunks/bot.js +2753 -0
  334. package/web/.svelte-kit/output/server/chunks/client.js +47 -0
  335. package/web/.svelte-kit/output/server/chunks/environment.js +34 -0
  336. package/web/.svelte-kit/output/server/chunks/exports.js +231 -0
  337. package/web/.svelte-kit/output/server/chunks/false.js +4 -0
  338. package/web/.svelte-kit/output/server/chunks/index-server.js +20 -0
  339. package/web/.svelte-kit/output/server/chunks/index.js +24 -0
  340. package/web/.svelte-kit/output/server/chunks/internal.js +133 -0
  341. package/web/.svelte-kit/output/server/chunks/plus.js +81 -0
  342. package/web/.svelte-kit/output/server/chunks/root.js +4076 -0
  343. package/web/.svelte-kit/output/server/chunks/shared.js +789 -0
  344. package/web/.svelte-kit/output/server/chunks/utils.js +43 -0
  345. package/web/.svelte-kit/output/server/entries/fallbacks/error.svelte.js +11 -0
  346. package/web/.svelte-kit/output/server/entries/pages/_layout.svelte.js +3944 -0
  347. package/web/.svelte-kit/output/server/entries/pages/_layout.ts.js +28 -0
  348. package/web/.svelte-kit/output/server/entries/pages/_page.svelte.js +7 -0
  349. package/web/.svelte-kit/output/server/entries/pages/agents/_page.svelte.js +379 -0
  350. package/web/.svelte-kit/output/server/entries/pages/chats/_id_/_page.svelte.js +292 -0
  351. package/web/.svelte-kit/output/server/entries/pages/chats/_id_/_page.ts.js +17 -0
  352. package/web/.svelte-kit/output/server/entries/pages/chats/_id_/settings/_page.svelte.js +259 -0
  353. package/web/.svelte-kit/output/server/entries/pages/chats/_id_/settings/_page.ts.js +17 -0
  354. package/web/.svelte-kit/output/server/index.js +3748 -0
  355. package/web/.svelte-kit/output/server/internal.js +14 -0
  356. package/web/.svelte-kit/output/server/manifest-full.js +63 -0
  357. package/web/.svelte-kit/output/server/manifest.js +63 -0
  358. package/web/.svelte-kit/output/server/nodes/0.js +13 -0
  359. package/web/.svelte-kit/output/server/nodes/1.js +8 -0
  360. package/web/.svelte-kit/output/server/nodes/2.js +8 -0
  361. package/web/.svelte-kit/output/server/nodes/3.js +8 -0
  362. package/web/.svelte-kit/output/server/nodes/4.js +13 -0
  363. package/web/.svelte-kit/output/server/nodes/5.js +13 -0
  364. package/web/.svelte-kit/output/server/remote-entry.js +557 -0
  365. package/web/.svelte-kit/tsconfig.json +67 -0
  366. package/web/.svelte-kit/types/route_meta_data.json +17 -0
  367. package/web/.svelte-kit/types/src/routes/$types.d.ts +26 -0
  368. package/web/.svelte-kit/types/src/routes/agents/$types.d.ts +18 -0
  369. package/web/.svelte-kit/types/src/routes/chats/[id]/$types.d.ts +21 -0
  370. package/web/.svelte-kit/types/src/routes/chats/[id]/proxy+page.ts +20 -0
  371. package/web/.svelte-kit/types/src/routes/chats/[id]/settings/$types.d.ts +21 -0
  372. package/web/.svelte-kit/types/src/routes/chats/[id]/settings/proxy+page.ts +19 -0
  373. package/web/.svelte-kit/types/src/routes/proxy+layout.ts +35 -0
  374. package/web/README.md +42 -0
  375. package/web/components.json +16 -0
  376. package/web/package.json +41 -0
  377. package/web/src/app.css +121 -0
  378. package/web/src/app.d.ts +13 -0
  379. package/web/src/app.html +11 -0
  380. package/web/src/demo.spec.ts +7 -0
  381. package/web/src/lib/app-state.svelte.ts +3 -0
  382. package/web/src/lib/assets/favicon.svg +1 -0
  383. package/web/src/lib/components/app/app-sidebar-test-wrapper.svelte +10 -0
  384. package/web/src/lib/components/app/app-sidebar.svelte +171 -0
  385. package/web/src/lib/components/app/app-sidebar.svelte.spec.ts +13 -0
  386. package/web/src/lib/components/ui/button/button.svelte +82 -0
  387. package/web/src/lib/components/ui/button/index.ts +17 -0
  388. package/web/src/lib/components/ui/dialog/dialog-close.svelte +7 -0
  389. package/web/src/lib/components/ui/dialog/dialog-content.svelte +45 -0
  390. package/web/src/lib/components/ui/dialog/dialog-description.svelte +17 -0
  391. package/web/src/lib/components/ui/dialog/dialog-footer.svelte +20 -0
  392. package/web/src/lib/components/ui/dialog/dialog-header.svelte +20 -0
  393. package/web/src/lib/components/ui/dialog/dialog-overlay.svelte +20 -0
  394. package/web/src/lib/components/ui/dialog/dialog-portal.svelte +7 -0
  395. package/web/src/lib/components/ui/dialog/dialog-title.svelte +17 -0
  396. package/web/src/lib/components/ui/dialog/dialog-trigger.svelte +7 -0
  397. package/web/src/lib/components/ui/dialog/dialog.svelte +7 -0
  398. package/web/src/lib/components/ui/dialog/index.ts +34 -0
  399. package/web/src/lib/components/ui/input/index.ts +7 -0
  400. package/web/src/lib/components/ui/input/input.svelte +52 -0
  401. package/web/src/lib/components/ui/separator/index.ts +7 -0
  402. package/web/src/lib/components/ui/separator/separator.svelte +21 -0
  403. package/web/src/lib/components/ui/sheet/index.ts +34 -0
  404. package/web/src/lib/components/ui/sheet/sheet-close.svelte +7 -0
  405. package/web/src/lib/components/ui/sheet/sheet-content.svelte +60 -0
  406. package/web/src/lib/components/ui/sheet/sheet-description.svelte +17 -0
  407. package/web/src/lib/components/ui/sheet/sheet-footer.svelte +20 -0
  408. package/web/src/lib/components/ui/sheet/sheet-header.svelte +20 -0
  409. package/web/src/lib/components/ui/sheet/sheet-overlay.svelte +20 -0
  410. package/web/src/lib/components/ui/sheet/sheet-portal.svelte +7 -0
  411. package/web/src/lib/components/ui/sheet/sheet-title.svelte +17 -0
  412. package/web/src/lib/components/ui/sheet/sheet-trigger.svelte +7 -0
  413. package/web/src/lib/components/ui/sheet/sheet.svelte +7 -0
  414. package/web/src/lib/components/ui/sidebar/constants.ts +6 -0
  415. package/web/src/lib/components/ui/sidebar/context.svelte.ts +79 -0
  416. package/web/src/lib/components/ui/sidebar/index.ts +75 -0
  417. package/web/src/lib/components/ui/sidebar/sidebar-content.svelte +24 -0
  418. package/web/src/lib/components/ui/sidebar/sidebar-footer.svelte +21 -0
  419. package/web/src/lib/components/ui/sidebar/sidebar-group-action.svelte +36 -0
  420. package/web/src/lib/components/ui/sidebar/sidebar-group-content.svelte +21 -0
  421. package/web/src/lib/components/ui/sidebar/sidebar-group-label.svelte +34 -0
  422. package/web/src/lib/components/ui/sidebar/sidebar-group.svelte +21 -0
  423. package/web/src/lib/components/ui/sidebar/sidebar-header.svelte +21 -0
  424. package/web/src/lib/components/ui/sidebar/sidebar-input.svelte +21 -0
  425. package/web/src/lib/components/ui/sidebar/sidebar-inset.svelte +24 -0
  426. package/web/src/lib/components/ui/sidebar/sidebar-menu-action.svelte +43 -0
  427. package/web/src/lib/components/ui/sidebar/sidebar-menu-badge.svelte +29 -0
  428. package/web/src/lib/components/ui/sidebar/sidebar-menu-button.svelte +103 -0
  429. package/web/src/lib/components/ui/sidebar/sidebar-menu-item.svelte +21 -0
  430. package/web/src/lib/components/ui/sidebar/sidebar-menu-skeleton.svelte +36 -0
  431. package/web/src/lib/components/ui/sidebar/sidebar-menu-sub-button.svelte +43 -0
  432. package/web/src/lib/components/ui/sidebar/sidebar-menu-sub-item.svelte +21 -0
  433. package/web/src/lib/components/ui/sidebar/sidebar-menu-sub.svelte +25 -0
  434. package/web/src/lib/components/ui/sidebar/sidebar-menu.svelte +21 -0
  435. package/web/src/lib/components/ui/sidebar/sidebar-provider.svelte +53 -0
  436. package/web/src/lib/components/ui/sidebar/sidebar-rail.svelte +36 -0
  437. package/web/src/lib/components/ui/sidebar/sidebar-separator.svelte +19 -0
  438. package/web/src/lib/components/ui/sidebar/sidebar-trigger.svelte +35 -0
  439. package/web/src/lib/components/ui/sidebar/sidebar.svelte +104 -0
  440. package/web/src/lib/components/ui/skeleton/index.ts +7 -0
  441. package/web/src/lib/components/ui/skeleton/skeleton.svelte +17 -0
  442. package/web/src/lib/components/ui/switch/index.ts +7 -0
  443. package/web/src/lib/components/ui/switch/switch.svelte +29 -0
  444. package/web/src/lib/components/ui/textarea/index.ts +7 -0
  445. package/web/src/lib/components/ui/textarea/textarea.svelte +23 -0
  446. package/web/src/lib/components/ui/tooltip/index.ts +19 -0
  447. package/web/src/lib/components/ui/tooltip/tooltip-content.svelte +52 -0
  448. package/web/src/lib/components/ui/tooltip/tooltip-portal.svelte +7 -0
  449. package/web/src/lib/components/ui/tooltip/tooltip-provider.svelte +7 -0
  450. package/web/src/lib/components/ui/tooltip/tooltip-trigger.svelte +7 -0
  451. package/web/src/lib/components/ui/tooltip/tooltip.svelte +7 -0
  452. package/web/src/lib/hooks/is-mobile.svelte.ts +9 -0
  453. package/web/src/lib/index.ts +1 -0
  454. package/web/src/lib/types.ts +23 -0
  455. package/web/src/lib/utils.ts +13 -0
  456. package/web/src/routes/+layout.svelte +67 -0
  457. package/web/src/routes/+layout.ts +34 -0
  458. package/web/src/routes/+page.svelte +7 -0
  459. package/web/src/routes/agents/+page.svelte +206 -0
  460. package/web/src/routes/chats/[id]/+page.svelte +406 -0
  461. package/web/src/routes/chats/[id]/+page.ts +19 -0
  462. package/web/src/routes/chats/[id]/page.svelte.spec.ts +102 -0
  463. package/web/src/routes/chats/[id]/settings/+page.svelte +165 -0
  464. package/web/src/routes/chats/[id]/settings/+page.ts +18 -0
  465. package/web/src/routes/page.svelte.spec.ts +13 -0
  466. package/web/static/robots.txt +3 -0
  467. package/web/svelte.config.js +21 -0
  468. package/web/tsconfig.json +20 -0
  469. package/web/vite.config.ts +41 -0
@@ -0,0 +1,246 @@
1
+ import type { Client, MessageCreateOptions } from 'discord.js';
2
+ import path from 'node:path';
3
+ import type { getTRPCClient } from './client.js';
4
+ import { readDiscordState, writeDiscordState } from './state.js';
5
+ import type { ChatMessage, CommandLogMessage } from '../shared/chats.js';
6
+ import { getWorkspaceRoot } from '../shared/workspace.js';
7
+
8
+ export async function startDaemonToDiscordForwarder(
9
+ client: Client,
10
+ trpc: ReturnType<typeof getTRPCClient>,
11
+ discordUserId: string,
12
+ chatId: string = 'default',
13
+ signal?: AbortSignal
14
+ ) {
15
+ const state = await readDiscordState();
16
+ let lastMessageId = state.lastSyncedMessageId;
17
+
18
+ // 1. If we don't have a lastMessageId, get the most recent one from the daemon
19
+ // to avoid sending the entire chat history on first run.
20
+ if (!lastMessageId) {
21
+ try {
22
+ const messages = await trpc.getMessages.query({ chatId, limit: 1 });
23
+ if (Array.isArray(messages) && messages.length > 0) {
24
+ const lastMsg = messages[messages.length - 1];
25
+ if (lastMsg) {
26
+ lastMessageId = lastMsg.id;
27
+ await writeDiscordState({ lastSyncedMessageId: lastMessageId });
28
+ }
29
+ }
30
+ } catch (error) {
31
+ if (signal?.aborted) return;
32
+ console.error('Failed to fetch initial messages from daemon:', error);
33
+ }
34
+ }
35
+
36
+ console.log(
37
+ `Starting daemon-to-discord forwarder for chat ${chatId}, lastMessageId: ${lastMessageId}`
38
+ );
39
+
40
+ let retryDelay = 1000;
41
+ const maxRetryDelay = 30000;
42
+
43
+ // 2. Start the observation loop using tRPC subscription
44
+ return new Promise<void>((resolve) => {
45
+ let subscription: { unsubscribe: () => void } | null = null;
46
+ let messageQueue = Promise.resolve();
47
+
48
+ const connect = () => {
49
+ if (signal?.aborted) {
50
+ resolve();
51
+ return;
52
+ }
53
+
54
+ subscription = trpc.waitForMessages.subscribe(
55
+ { chatId, lastMessageId },
56
+ {
57
+ onData: (messages) => {
58
+ retryDelay = 1000; // Reset retry delay on successful data
59
+
60
+ if (!Array.isArray(messages) || messages.length === 0) {
61
+ return;
62
+ }
63
+
64
+ // Queue processing to ensure sequential execution
65
+ messageQueue = messageQueue.then(async () => {
66
+ for (const rawMessage of messages) {
67
+ if (signal?.aborted) break;
68
+
69
+ const message = rawMessage as ChatMessage;
70
+
71
+ // Only forward logs (agent responses, system messages)
72
+ if (message.role === 'log') {
73
+ const logMessage = message as CommandLogMessage;
74
+
75
+ if (logMessage.level === 'verbose') {
76
+ lastMessageId = logMessage.id;
77
+ await writeDiscordState({ lastSyncedMessageId: lastMessageId }).catch(
78
+ console.error
79
+ );
80
+ continue;
81
+ }
82
+
83
+ const hasContent = !!logMessage.content?.trim();
84
+ const hasFiles = Array.isArray(logMessage.files) && logMessage.files.length > 0;
85
+
86
+ // The daemon stores logMessage.files as paths relative to the WORKSPACE directory
87
+ // (the directory containing .clawmini). We must resolve these against the current
88
+ // workspace root so discord.js can successfully locate and read the files.
89
+ let absoluteFiles: string[] = [];
90
+ if (hasFiles) {
91
+ const workspaceRoot = getWorkspaceRoot(process.cwd());
92
+ absoluteFiles = logMessage.files!.map((f) => path.resolve(workspaceRoot, f));
93
+ }
94
+
95
+ if (!hasContent && !hasFiles) {
96
+ lastMessageId = logMessage.id;
97
+ await writeDiscordState({ lastSyncedMessageId: lastMessageId }).catch(
98
+ console.error
99
+ );
100
+ continue;
101
+ }
102
+
103
+ try {
104
+ const user = await client.users.fetch(discordUserId);
105
+ const dm = await user.createDM();
106
+
107
+ // Discord has a 2000 character limit for messages.
108
+ if (hasContent && logMessage.content.length > 2000) {
109
+ const chunks = chunkString(logMessage.content, 2000);
110
+ for (let i = 0; i < chunks.length; i++) {
111
+ if (signal?.aborted) break;
112
+ const chunkOptions: MessageCreateOptions = { content: chunks[i] as string };
113
+ if (i === chunks.length - 1 && hasFiles) {
114
+ chunkOptions.files = absoluteFiles;
115
+ }
116
+ await dm.send(chunkOptions);
117
+ }
118
+ } else {
119
+ const options: MessageCreateOptions = {};
120
+ if (hasContent) {
121
+ options.content = logMessage.content;
122
+ }
123
+ if (hasFiles) {
124
+ options.files = absoluteFiles;
125
+ }
126
+ await dm.send(options);
127
+ }
128
+ } catch (error) {
129
+ console.error(
130
+ `Failed to send message to Discord user ${discordUserId}:`,
131
+ error
132
+ );
133
+ // We don't advance lastMessageId if sending failed
134
+ break;
135
+ }
136
+ }
137
+
138
+ lastMessageId = message.id;
139
+ await writeDiscordState({ lastSyncedMessageId: lastMessageId }).catch(
140
+ console.error
141
+ );
142
+ }
143
+ });
144
+ },
145
+ onError: (error) => {
146
+ console.error(
147
+ `Error in daemon-to-discord forwarder subscription. Retrying in ${retryDelay}ms.`,
148
+ error
149
+ );
150
+ subscription?.unsubscribe();
151
+ subscription = null;
152
+
153
+ if (signal?.aborted) {
154
+ resolve();
155
+ return;
156
+ }
157
+
158
+ setTimeout(() => {
159
+ retryDelay = Math.min(retryDelay * 2, maxRetryDelay);
160
+ connect();
161
+ }, retryDelay);
162
+ },
163
+ onComplete: () => {
164
+ subscription = null;
165
+ if (!signal?.aborted) {
166
+ setTimeout(() => connect(), retryDelay);
167
+ } else {
168
+ resolve();
169
+ }
170
+ },
171
+ }
172
+ );
173
+ };
174
+
175
+ let typingSubscription: { unsubscribe: () => void } | null = null;
176
+ let typingRetryDelay = 1000;
177
+
178
+ const connectTyping = () => {
179
+ if (signal?.aborted) {
180
+ return;
181
+ }
182
+
183
+ typingSubscription = trpc.waitForTyping.subscribe(
184
+ { chatId },
185
+ {
186
+ onData: async (event) => {
187
+ typingRetryDelay = 1000; // Reset retry delay on successful data
188
+ if (!event) return;
189
+
190
+ try {
191
+ const user = await client.users.fetch(discordUserId);
192
+ const dm = await user.createDM();
193
+ await dm.sendTyping();
194
+ } catch (error) {
195
+ console.error(
196
+ `Failed to send typing indicator to Discord user ${discordUserId}:`,
197
+ error
198
+ );
199
+ }
200
+ },
201
+ onError: (error) => {
202
+ console.error(
203
+ `Error in daemon-to-discord typing forwarder subscription. Retrying in ${typingRetryDelay}ms.`,
204
+ error
205
+ );
206
+ typingSubscription?.unsubscribe();
207
+ typingSubscription = null;
208
+
209
+ if (signal?.aborted) {
210
+ return;
211
+ }
212
+
213
+ setTimeout(() => {
214
+ typingRetryDelay = Math.min(typingRetryDelay * 2, maxRetryDelay);
215
+ connectTyping();
216
+ }, typingRetryDelay);
217
+ },
218
+ onComplete: () => {
219
+ typingSubscription = null;
220
+ if (!signal?.aborted) {
221
+ setTimeout(() => connectTyping(), typingRetryDelay);
222
+ }
223
+ },
224
+ }
225
+ );
226
+ };
227
+
228
+ connect();
229
+ connectTyping();
230
+
231
+ signal?.addEventListener('abort', () => {
232
+ subscription?.unsubscribe();
233
+ typingSubscription?.unsubscribe();
234
+ resolve();
235
+ });
236
+ });
237
+ }
238
+
239
+ function chunkString(str: string, size: number): string[] {
240
+ const chunks: string[] = [];
241
+ const chars = Array.from(str);
242
+ for (let i = 0; i < chars.length; i += size) {
243
+ chunks.push(chars.slice(i, i + size).join(''));
244
+ }
245
+ return chunks;
246
+ }
@@ -0,0 +1,399 @@
1
+ import { describe, it, expect, vi, beforeEach } from 'vitest';
2
+ import { readDiscordConfig } from './config.js';
3
+ import { getTRPCClient } from './client.js';
4
+
5
+ // Mock the modules
6
+ const { mockClientInstance } = vi.hoisted(() => ({
7
+ mockClientInstance: {
8
+ once: vi.fn(),
9
+ on: vi.fn(),
10
+ login: vi.fn().mockResolvedValue('token'),
11
+ user: { id: 'bot-id', tag: 'bot#1234' },
12
+ },
13
+ }));
14
+
15
+ vi.mock('discord.js', () => {
16
+ return {
17
+ Client: class {
18
+ constructor() {
19
+ return mockClientInstance;
20
+ }
21
+ },
22
+ Events: {
23
+ ClientReady: 'ready',
24
+ MessageCreate: 'messageCreate',
25
+ },
26
+ GatewayIntentBits: {
27
+ Guilds: 1,
28
+ DirectMessages: 2,
29
+ MessageContent: 3,
30
+ },
31
+ Partials: {
32
+ Channel: 1,
33
+ },
34
+ };
35
+ });
36
+
37
+ vi.mock('./config.js', () => ({
38
+ readDiscordConfig: vi.fn(),
39
+ initDiscordConfig: vi.fn(),
40
+ isAuthorized: vi.fn(),
41
+ }));
42
+
43
+ vi.mock('./client.js', () => ({
44
+ getTRPCClient: vi.fn(),
45
+ }));
46
+
47
+ describe('Discord Adapter Entry Point', () => {
48
+ let mockTrpc: ReturnType<typeof import('./client.js').getTRPCClient>;
49
+
50
+ beforeEach(() => {
51
+ vi.clearAllMocks();
52
+ mockTrpc = {
53
+ sendMessage: {
54
+ mutate: vi.fn().mockResolvedValue({ success: true }),
55
+ },
56
+ } as unknown as ReturnType<typeof import('./client.js').getTRPCClient>;
57
+ vi.mocked(getTRPCClient).mockReturnValue(mockTrpc);
58
+ vi.mocked(readDiscordConfig).mockResolvedValue({
59
+ botToken: 'test-token',
60
+ authorizedUserId: 'user-123',
61
+ chatId: 'default',
62
+ });
63
+
64
+ // Reset the mock implementation to return the instance
65
+ vi.mocked(mockClientInstance.on).mockReturnValue(mockClientInstance);
66
+ vi.mocked(mockClientInstance.once).mockReturnValue(mockClientInstance);
67
+ });
68
+
69
+ it('should initialize Discord config and exit if init argument is provided', async () => {
70
+ process.argv = ['node', 'index.js', 'init'];
71
+ const { initDiscordConfig } = await import('./config.js');
72
+ const { main } = await import('./index.js');
73
+ await main();
74
+
75
+ expect(initDiscordConfig).toHaveBeenCalled();
76
+ expect(vi.mocked(mockClientInstance.login)).not.toHaveBeenCalled();
77
+ process.argv = []; // reset
78
+ });
79
+
80
+ it('should initialize Discord client and forward authorized DM messages', async () => {
81
+ vi.useFakeTimers();
82
+ let messageHandler: ((message: import('discord.js').Message) => Promise<void>) | undefined;
83
+ vi.mocked(mockClientInstance.on).mockImplementation(
84
+ (event: string, cb: (...args: unknown[]) => void) => {
85
+ if (event === 'messageCreate') {
86
+ messageHandler = cb as unknown as (
87
+ message: import('discord.js').Message
88
+ ) => Promise<void>;
89
+ }
90
+ return mockClientInstance as unknown as import('discord.js').Client;
91
+ }
92
+ );
93
+
94
+ const { main } = await import('./index.js');
95
+ await main();
96
+
97
+ expect(vi.mocked(mockClientInstance.login)).toHaveBeenCalledWith('test-token');
98
+ expect(messageHandler).toBeDefined();
99
+
100
+ const mockMessage = {
101
+ author: { id: 'user-123', tag: 'user#1234' },
102
+ content: 'Hello daemon!',
103
+ guild: null,
104
+ attachments: new Map(),
105
+ };
106
+
107
+ const { isAuthorized } = await import('./config.js');
108
+ vi.mocked(isAuthorized).mockReturnValue(true);
109
+
110
+ if (messageHandler) {
111
+ await messageHandler(mockMessage as unknown as import('discord.js').Message);
112
+ }
113
+
114
+ // Fast-forward time for debouncer
115
+ await vi.runAllTimersAsync();
116
+
117
+ expect(mockTrpc.sendMessage.mutate).toHaveBeenCalledWith({
118
+ type: 'send-message',
119
+ client: 'cli',
120
+ data: {
121
+ message: 'Hello daemon!',
122
+ chatId: 'default',
123
+ files: undefined,
124
+ adapter: 'discord',
125
+ noWait: true,
126
+ },
127
+ });
128
+ vi.useRealTimers();
129
+ });
130
+
131
+ it('should ignore duplicate network events based on Discord message ID', async () => {
132
+ let messageHandler: ((message: import('discord.js').Message) => Promise<void>) | undefined;
133
+ vi.mocked(mockClientInstance.on).mockImplementation(
134
+ (event: string, cb: (...args: unknown[]) => void) => {
135
+ if (event === 'messageCreate') {
136
+ messageHandler = cb as unknown as (
137
+ message: import('discord.js').Message
138
+ ) => Promise<void>;
139
+ }
140
+ return mockClientInstance as unknown as import('discord.js').Client;
141
+ }
142
+ );
143
+
144
+ const { main } = await import('./index.js');
145
+ await main();
146
+
147
+ const { isAuthorized } = await import('./config.js');
148
+ vi.mocked(isAuthorized).mockReturnValue(true);
149
+
150
+ if (messageHandler) {
151
+ await messageHandler({
152
+ id: 'msg-1',
153
+ author: { id: 'user-123', tag: 'user#1234' } as unknown as import('discord.js').User,
154
+ content: 'message 1',
155
+ guild: null,
156
+ attachments: new Map(),
157
+ } as unknown as import('discord.js').Message);
158
+ await messageHandler({
159
+ id: 'msg-2',
160
+ author: { id: 'user-123', tag: 'user#1234' } as unknown as import('discord.js').User,
161
+ content: 'message 2',
162
+ guild: null,
163
+ attachments: new Map(),
164
+ } as unknown as import('discord.js').Message);
165
+ }
166
+
167
+ expect(mockTrpc.sendMessage.mutate).toHaveBeenCalledTimes(2);
168
+ expect(mockTrpc.sendMessage.mutate).toHaveBeenNthCalledWith(1, {
169
+ type: 'send-message',
170
+ client: 'cli',
171
+ data: {
172
+ message: 'message 1',
173
+ chatId: 'default',
174
+ files: undefined,
175
+ adapter: 'discord',
176
+ noWait: true,
177
+ },
178
+ });
179
+ expect(mockTrpc.sendMessage.mutate).toHaveBeenNthCalledWith(2, {
180
+ type: 'send-message',
181
+ client: 'cli',
182
+ data: {
183
+ message: 'message 2',
184
+ chatId: 'default',
185
+ files: undefined,
186
+ adapter: 'discord',
187
+ noWait: true,
188
+ },
189
+ });
190
+ });
191
+
192
+ it('should ignore unauthorized messages', async () => {
193
+ let messageHandler: ((message: import('discord.js').Message) => Promise<void>) | undefined;
194
+ vi.mocked(mockClientInstance.on).mockImplementation(
195
+ (event: string, cb: (...args: unknown[]) => void) => {
196
+ if (event === 'messageCreate') {
197
+ messageHandler = cb as unknown as (
198
+ message: import('discord.js').Message
199
+ ) => Promise<void>;
200
+ }
201
+ return mockClientInstance as unknown as import('discord.js').Client;
202
+ }
203
+ );
204
+
205
+ const { main } = await import('./index.js');
206
+ await main();
207
+
208
+ const mockMessage = {
209
+ author: { id: 'user-evil', tag: 'evil#666' },
210
+ content: 'Hack the daemon!',
211
+ guild: null,
212
+ };
213
+
214
+ const { isAuthorized } = await import('./config.js');
215
+ vi.mocked(isAuthorized).mockReturnValue(false);
216
+
217
+ if (messageHandler) {
218
+ await messageHandler(mockMessage as unknown as import('discord.js').Message);
219
+ }
220
+
221
+ expect(mockTrpc.sendMessage.mutate).not.toHaveBeenCalled();
222
+ });
223
+
224
+ it('should download attachments and forward their paths', async () => {
225
+ vi.useFakeTimers();
226
+ let messageHandler: ((message: import('discord.js').Message) => Promise<void>) | undefined;
227
+ vi.mocked(mockClientInstance.on).mockImplementation(
228
+ (event: string, cb: (...args: unknown[]) => void) => {
229
+ if (event === 'messageCreate') {
230
+ messageHandler = cb as unknown as (
231
+ message: import('discord.js').Message
232
+ ) => Promise<void>;
233
+ }
234
+ return mockClientInstance as unknown as import('discord.js').Client;
235
+ }
236
+ );
237
+
238
+ const { main } = await import('./index.js');
239
+ await main();
240
+
241
+ const { isAuthorized } = await import('./config.js');
242
+ vi.mocked(isAuthorized).mockReturnValue(true);
243
+
244
+ const attachments = new Map();
245
+ attachments.set('1', { name: 'test.txt', url: 'http://example.com/test.txt', size: 100 });
246
+
247
+ const fsPromises = await import('node:fs/promises');
248
+ vi.spyOn(fsPromises.default, 'mkdir').mockResolvedValue(undefined);
249
+ vi.spyOn(fsPromises.default, 'writeFile').mockResolvedValue(undefined);
250
+
251
+ const mockResponse = {
252
+ ok: true,
253
+ arrayBuffer: vi.fn().mockResolvedValue(new ArrayBuffer(8)),
254
+ } as unknown as Response;
255
+ global.fetch = vi.fn().mockResolvedValue(mockResponse);
256
+
257
+ if (messageHandler) {
258
+ await messageHandler({
259
+ author: { id: 'user-123', tag: 'user#1234' } as unknown as import('discord.js').User,
260
+ content: 'Check out this file',
261
+ guild: null,
262
+ attachments,
263
+ } as unknown as import('discord.js').Message);
264
+ }
265
+
266
+ // Fast-forward time for debouncer
267
+ await vi.runAllTimersAsync();
268
+
269
+ expect(global.fetch).toHaveBeenCalledWith('http://example.com/test.txt');
270
+ expect(fsPromises.default.writeFile).toHaveBeenCalled();
271
+
272
+ expect(mockTrpc.sendMessage.mutate).toHaveBeenCalledWith({
273
+ type: 'send-message',
274
+ client: 'cli',
275
+ data: expect.objectContaining({
276
+ message: 'Check out this file',
277
+ chatId: 'default',
278
+ files: expect.arrayContaining([expect.stringContaining('test.txt')]),
279
+ }),
280
+ });
281
+
282
+ vi.useRealTimers();
283
+ });
284
+
285
+ it('should ignore attachments that exceed the size limit', async () => {
286
+ vi.useFakeTimers();
287
+ let messageHandler: ((message: import('discord.js').Message) => Promise<void>) | undefined;
288
+ vi.mocked(mockClientInstance.on).mockImplementation(
289
+ (event: string, cb: (...args: unknown[]) => void) => {
290
+ if (event === 'messageCreate') {
291
+ messageHandler = cb as unknown as (
292
+ message: import('discord.js').Message
293
+ ) => Promise<void>;
294
+ }
295
+ return mockClientInstance as unknown as import('discord.js').Client;
296
+ }
297
+ );
298
+
299
+ const { main } = await import('./index.js');
300
+ await main();
301
+
302
+ const { isAuthorized } = await import('./config.js');
303
+ vi.mocked(isAuthorized).mockReturnValue(true);
304
+
305
+ const attachments = new Map();
306
+ // 26MB is over the 25MB default
307
+ attachments.set('1', {
308
+ name: 'huge.txt',
309
+ url: 'http://example.com/huge.txt',
310
+ size: 26 * 1024 * 1024 + 1,
311
+ });
312
+
313
+ global.fetch = vi.fn();
314
+
315
+ const replyMock = vi.fn();
316
+
317
+ if (messageHandler) {
318
+ await messageHandler({
319
+ author: { id: 'user-123', tag: 'user#1234' } as unknown as import('discord.js').User,
320
+ content: 'Check out this huge file',
321
+ guild: null,
322
+ attachments,
323
+ reply: replyMock,
324
+ } as unknown as import('discord.js').Message);
325
+ }
326
+
327
+ // Fast-forward time for debouncer
328
+ await vi.runAllTimersAsync();
329
+
330
+ expect(global.fetch).not.toHaveBeenCalled();
331
+ expect(replyMock).toHaveBeenCalledWith(expect.stringContaining('exceeds the size limit'));
332
+
333
+ expect(mockTrpc.sendMessage.mutate).toHaveBeenCalledWith({
334
+ type: 'send-message',
335
+ client: 'cli',
336
+ data: {
337
+ message: 'Check out this huge file',
338
+ chatId: 'default',
339
+ files: undefined, // no files should be attached
340
+ adapter: 'discord',
341
+ noWait: true,
342
+ },
343
+ });
344
+
345
+ vi.useRealTimers();
346
+ });
347
+
348
+ it('should format message with blockquote when it is a reply', async () => {
349
+ vi.useFakeTimers();
350
+ let messageHandler: ((message: import('discord.js').Message) => Promise<void>) | undefined;
351
+ vi.mocked(mockClientInstance.on).mockImplementation(
352
+ (event: string, cb: (...args: unknown[]) => void) => {
353
+ if (event === 'messageCreate') {
354
+ messageHandler = cb as unknown as (
355
+ message: import('discord.js').Message
356
+ ) => Promise<void>;
357
+ }
358
+ return mockClientInstance as unknown as import('discord.js').Client;
359
+ }
360
+ );
361
+
362
+ const { main } = await import('./index.js');
363
+ await main();
364
+
365
+ const { isAuthorized } = await import('./config.js');
366
+ vi.mocked(isAuthorized).mockReturnValue(true);
367
+
368
+ const mockReferencedMessage = {
369
+ content: 'Would anyone like to get dinner Sunday?\nOr maybe lunch?',
370
+ };
371
+
372
+ if (messageHandler) {
373
+ await messageHandler({
374
+ author: { id: 'user-123', tag: 'user#1234' } as unknown as import('discord.js').User,
375
+ content: "Yes, I'm in!",
376
+ guild: null,
377
+ attachments: new Map(),
378
+ reference: { messageId: '12345' },
379
+ fetchReference: vi.fn().mockResolvedValue(mockReferencedMessage),
380
+ } as unknown as import('discord.js').Message);
381
+ }
382
+
383
+ // Fast-forward time for debouncer
384
+ await vi.runAllTimersAsync();
385
+
386
+ expect(mockTrpc.sendMessage.mutate).toHaveBeenCalledWith({
387
+ type: 'send-message',
388
+ client: 'cli',
389
+ data: {
390
+ message: "> Would anyone like to get dinner Sunday?\n> Or maybe lunch?\nYes, I'm in!",
391
+ chatId: 'default',
392
+ files: undefined,
393
+ adapter: 'discord',
394
+ noWait: true,
395
+ },
396
+ });
397
+ vi.useRealTimers();
398
+ });
399
+ });