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
@@ -0,0 +1,430 @@
1
+ /* eslint-disable max-lines */
2
+ import { PubSub, Message } from '@google-cloud/pubsub';
3
+ import { createTRPCClient, httpLink, splitLink, httpSubscriptionLink } from '@trpc/client';
4
+ import fs from 'node:fs';
5
+ import fsPromises from 'node:fs/promises';
6
+ import path from 'node:path';
7
+ import crypto from 'node:crypto';
8
+
9
+ import type { UserRouter as AppRouter } from '../daemon/api/index.js';
10
+ import { getSocketPath, getClawminiDir } from '../shared/workspace.js';
11
+ import { createUnixSocketFetch } from '../shared/fetch.js';
12
+ import { createUnixSocketEventSource } from '../shared/event-source.js';
13
+ import type { GoogleChatConfig } from './config.js';
14
+ import { isAuthorized, updateGoogleChatConfig } from './config.js';
15
+ import { readGoogleChatState, updateGoogleChatState } from './state.js';
16
+ import { downloadAttachment } from './utils.js';
17
+ import { handleAdapterCommand, type CommandTrpcClient } from '../shared/adapters/commands.js';
18
+ import { formatMessage, type FilteringConfig } from '../shared/adapters/filtering.js';
19
+ import { google } from 'googleapis';
20
+ import { getAuthClient } from './auth.js';
21
+ import { handleRoutingCommand, type RoutingTrpcClient } from '../shared/adapters/routing.js';
22
+
23
+ import { handleAddedToSpace, handleRemovedFromSpace } from './subscriptions.js';
24
+ import { handleCardClicked } from './cards.js';
25
+
26
+ export function getTRPCClient(options: { socketPath?: string } = {}) {
27
+ const socketPath = options.socketPath ?? getSocketPath();
28
+
29
+ if (!fs.existsSync(socketPath)) {
30
+ throw new Error(`Daemon not running. Socket not found at ${socketPath}`);
31
+ }
32
+
33
+ const customFetch = createUnixSocketFetch(socketPath);
34
+ const CustomEventSource = createUnixSocketEventSource(socketPath);
35
+
36
+ return createTRPCClient<AppRouter>({
37
+ links: [
38
+ splitLink({
39
+ condition(op) {
40
+ return op.type === 'subscription';
41
+ },
42
+ true: httpSubscriptionLink({
43
+ url: 'http://localhost',
44
+ EventSource: CustomEventSource,
45
+ }),
46
+ false: httpLink({
47
+ url: 'http://localhost',
48
+ fetch: customFetch,
49
+ }),
50
+ }),
51
+ ],
52
+ });
53
+ }
54
+
55
+ export function startGoogleChatIngestion(
56
+ config: GoogleChatConfig,
57
+ trpc: ReturnType<typeof getTRPCClient>,
58
+ filteringConfig: FilteringConfig
59
+ ) {
60
+ const pubsub = new PubSub({ projectId: config.projectId });
61
+ const subscription = pubsub.subscription(config.subscriptionName);
62
+
63
+ const seenMessageIds = new Map<string, number>();
64
+
65
+ // Periodically clean up deduplication cache every 5 minutes
66
+ setInterval(
67
+ () => {
68
+ const now = Date.now();
69
+ for (const [id, ts] of seenMessageIds.entries()) {
70
+ if (now - ts > 10 * 60 * 1000) {
71
+ seenMessageIds.delete(id);
72
+ }
73
+ }
74
+ },
75
+ 5 * 60 * 1000
76
+ ).unref();
77
+
78
+ subscription.on('message', async (message: Message) => {
79
+ const downloadedFiles: string[] = [];
80
+ try {
81
+ const dataString = message.data.toString('utf8');
82
+ const parsedData = JSON.parse(dataString);
83
+
84
+ const isWorkspaceEvent =
85
+ message.attributes &&
86
+ message.attributes['ce-type'] === 'google.workspace.chat.message.v1.created';
87
+
88
+ const eventType = isWorkspaceEvent ? 'MESSAGE' : parsedData.type;
89
+
90
+ const eventMessage = isWorkspaceEvent ? parsedData.message || parsedData : parsedData.message;
91
+ const email =
92
+ (isWorkspaceEvent
93
+ ? eventMessage?.sender?.email
94
+ : parsedData.user?.email || eventMessage?.sender?.email) || '';
95
+ const senderName = eventMessage?.sender?.name || parsedData.user?.name || '';
96
+
97
+ const space = isWorkspaceEvent
98
+ ? eventMessage?.space
99
+ : parsedData.space || eventMessage?.space;
100
+ const senderType = eventMessage?.sender?.type || '';
101
+ const messageId = eventMessage?.name || '';
102
+ const text = (eventMessage?.argumentText || eventMessage?.text || '').trim();
103
+
104
+ if (senderType === 'BOT') return void message.ack();
105
+
106
+ if (messageId) {
107
+ if (seenMessageIds.has(messageId)) return void message.ack();
108
+ seenMessageIds.set(messageId, Date.now());
109
+ }
110
+
111
+ // Only handle MESSAGE, CARD_CLICKED, ADDED_TO_SPACE, and REMOVED_FROM_SPACE events
112
+ if (
113
+ eventType !== 'MESSAGE' &&
114
+ eventType !== 'CARD_CLICKED' &&
115
+ eventType !== 'ADDED_TO_SPACE' &&
116
+ eventType !== 'REMOVED_FROM_SPACE'
117
+ ) {
118
+ message.ack();
119
+ return;
120
+ }
121
+
122
+ let isUserAuthorized = false;
123
+ let authorizedByEmail = false;
124
+
125
+ if (email && isAuthorized(email, config.authorizedUsers)) {
126
+ isUserAuthorized = true;
127
+ authorizedByEmail = true;
128
+ } else if (senderName && isAuthorized(senderName, config.authorizedUsers)) {
129
+ isUserAuthorized = true;
130
+ }
131
+
132
+ if (!isUserAuthorized) {
133
+ console.log(`Unauthorized or missing identifier: email=${email}, name=${senderName}`);
134
+ console.log('DEBUG missing identifier parsedData:', JSON.stringify(parsedData, null, 2));
135
+ message.ack();
136
+ return;
137
+ }
138
+
139
+ // Automatically authorize user IDs if associated an authorized email
140
+ if (authorizedByEmail && senderName && !isAuthorized(senderName, config.authorizedUsers)) {
141
+ console.log(
142
+ `Automatically authorizing user ID ${senderName} based on authorized email ${email}`
143
+ );
144
+ config.authorizedUsers.push(senderName);
145
+ updateGoogleChatConfig(config).catch((err) =>
146
+ console.error('Failed to update config with new user ID:', err)
147
+ );
148
+ }
149
+
150
+ const identifier = email || senderName;
151
+
152
+ const spaceName = space?.name;
153
+
154
+ if (!spaceName) {
155
+ console.log('Ignoring message: Could not determine space name.');
156
+ message.ack();
157
+ return;
158
+ }
159
+
160
+ const currentState = await readGoogleChatState();
161
+
162
+ const externalContextId = spaceName;
163
+ const mappedChatId = currentState.channelChatMap?.[externalContextId]?.chatId;
164
+ const isRoutingCommand = text.startsWith('/chat') || text.startsWith('/agent');
165
+
166
+ if (eventType === 'ADDED_TO_SPACE') {
167
+ await handleAddedToSpace(
168
+ spaceName as string,
169
+ externalContextId,
170
+ space?.type,
171
+ mappedChatId,
172
+ mappedChatId,
173
+ config
174
+ );
175
+ if (!text) {
176
+ message.ack();
177
+ return;
178
+ }
179
+ }
180
+
181
+ if (eventType === 'REMOVED_FROM_SPACE') {
182
+ await handleRemovedFromSpace(externalContextId, currentState, config);
183
+ message.ack();
184
+ return;
185
+ }
186
+
187
+ if (isRoutingCommand) {
188
+ const stringChatMap = Object.fromEntries(
189
+ Object.entries(currentState.channelChatMap || {}).map(([k, v]) => [k, v.chatId || ''])
190
+ );
191
+ const routingResult = await handleRoutingCommand(
192
+ text,
193
+ externalContextId,
194
+ stringChatMap,
195
+ 'google-chat',
196
+ trpc as unknown as RoutingTrpcClient
197
+ );
198
+
199
+ if (routingResult) {
200
+ if (routingResult.type === 'mapped') {
201
+ await updateGoogleChatState((latestState) => ({
202
+ channelChatMap: {
203
+ ...(latestState.channelChatMap || {}),
204
+ [externalContextId]: {
205
+ ...(latestState.channelChatMap?.[externalContextId] || {}),
206
+ chatId: routingResult.newChatId,
207
+ },
208
+ },
209
+ }));
210
+ }
211
+
212
+ try {
213
+ const authClient = await getAuthClient();
214
+ const chatApi = google.chat({ version: 'v1', auth: authClient });
215
+ await chatApi.spaces.messages.create({
216
+ parent: externalContextId,
217
+ requestBody: { text: routingResult.text },
218
+ });
219
+ } catch (err) {
220
+ console.error('Failed to send routing command reply:', err);
221
+ }
222
+
223
+ message.ack();
224
+ return;
225
+ }
226
+ }
227
+
228
+ let targetChatId = mappedChatId;
229
+
230
+ if (!targetChatId && !isRoutingCommand) {
231
+ const isFirstEverMessage =
232
+ !currentState.channelChatMap ||
233
+ Object.values(currentState.channelChatMap).every((entry) => !entry.chatId);
234
+
235
+ if (isFirstEverMessage) {
236
+ targetChatId = config.chatId || 'default';
237
+ console.log(
238
+ `First contact detected. Automatically mapping space ${externalContextId} to chat ${targetChatId}.`
239
+ );
240
+ await updateGoogleChatState((latestState) => ({
241
+ channelChatMap: {
242
+ ...(latestState.channelChatMap || {}),
243
+ [externalContextId]: {
244
+ ...(latestState.channelChatMap?.[externalContextId] || {}),
245
+ chatId: targetChatId as string,
246
+ },
247
+ },
248
+ }));
249
+ } else {
250
+ const isDirectMessage =
251
+ space?.type === 'DIRECT_MESSAGE' || space?.singleUserBotDm === true;
252
+ const isMentioned =
253
+ Array.isArray(eventMessage?.annotations) &&
254
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
255
+ eventMessage.annotations.some((a: any) => a.type === 'USER_MENTION');
256
+ const isSlashCommand = text.startsWith('/');
257
+ if (isDirectMessage || isMentioned || isSlashCommand) {
258
+ console.log(`Unmapped space ${externalContextId}, sending first contact warning.`);
259
+ try {
260
+ const authClient = await getAuthClient();
261
+ const chatApi = google.chat({ version: 'v1', auth: authClient });
262
+ await chatApi.spaces.messages.create({
263
+ parent: externalContextId,
264
+ requestBody: {
265
+ text: 'This channel/space is not currently mapped to a daemon chat. Please use `/chat [chat-id]` or `/agent [agent-id]` to map it.',
266
+ },
267
+ });
268
+ } catch (err) {
269
+ console.error('Failed to send first contact warning:', err);
270
+ }
271
+ } else {
272
+ console.log(
273
+ `Unmapped space ${externalContextId}, silently ignoring background message.`
274
+ );
275
+ }
276
+ message.ack();
277
+ return;
278
+ }
279
+ }
280
+
281
+ // Fallback typing safeguard
282
+ if (!targetChatId) targetChatId = config.chatId || 'default';
283
+
284
+ const isDirectMessage = space?.type === 'DIRECT_MESSAGE' || space?.singleUserBotDm === true;
285
+ if (!isDirectMessage && eventType === 'MESSAGE') {
286
+ const channelConfig = currentState.channelChatMap?.[externalContextId];
287
+ const requiresMention =
288
+ channelConfig?.requireMention !== undefined
289
+ ? channelConfig.requireMention
290
+ : config.requireMention;
291
+
292
+ if (requiresMention && !isRoutingCommand) {
293
+ const isMentioned =
294
+ Array.isArray(eventMessage?.annotations) &&
295
+ eventMessage.annotations.some(
296
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
297
+ (a: any) => a.type === 'USER_MENTION' && a.userMention?.user?.type === 'BOT'
298
+ );
299
+
300
+ let isReplyToBot = false;
301
+ if (eventMessage?.threadReply && eventMessage.thread?.name) {
302
+ try {
303
+ const authClient = await getAuthClient();
304
+ const chatApi = google.chat({ version: 'v1', auth: authClient });
305
+ const response = await chatApi.spaces.messages.list({
306
+ parent: externalContextId,
307
+ filter: `thread.name="${eventMessage.thread.name}"`,
308
+ });
309
+ isReplyToBot =
310
+ response.data.messages?.some(
311
+ (m) =>
312
+ m.sender?.type === 'BOT' ||
313
+ m.annotations?.some(
314
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
315
+ (a: any) => a.type === 'USER_MENTION' && a.userMention?.user?.type === 'BOT'
316
+ )
317
+ ) ?? false;
318
+ } catch (err) {
319
+ console.error('Failed to fetch thread messages for mention check:', err);
320
+ }
321
+ }
322
+
323
+ // If requireMention is true and it's not a DM, ignore if not mentioned and not a thread reply to the bot.
324
+ if (!isMentioned && !isReplyToBot) {
325
+ message.ack();
326
+ return;
327
+ }
328
+ }
329
+ }
330
+
331
+ if (eventType === 'CARD_CLICKED') {
332
+ await handleCardClicked(
333
+ parsedData,
334
+ targetChatId as string,
335
+ trpc as unknown as RoutingTrpcClient
336
+ );
337
+ message.ack();
338
+ return;
339
+ }
340
+
341
+ const commandResult = await handleAdapterCommand(
342
+ text,
343
+ filteringConfig,
344
+ trpc as unknown as CommandTrpcClient,
345
+ targetChatId
346
+ );
347
+
348
+ if (commandResult) {
349
+ let resultText = '';
350
+ if (commandResult.type === 'text') {
351
+ if (commandResult.newConfig) {
352
+ filteringConfig.filters = commandResult.newConfig.filters;
353
+ await updateGoogleChatState({ filters: filteringConfig.filters });
354
+ }
355
+ resultText = commandResult.text;
356
+ } else if (commandResult.type === 'debug') {
357
+ resultText =
358
+ commandResult.messages.length === 0
359
+ ? 'No ignored background messages found.'
360
+ : `**Debug Output (${commandResult.messages.length} ignored messages):**\n\n` +
361
+ commandResult.messages.map((msg) => formatMessage(msg)).join('\n\n---\n\n');
362
+ }
363
+
364
+ const authClient = await getAuthClient();
365
+ const chatApi = google.chat({ version: 'v1', auth: authClient });
366
+ await chatApi.spaces.messages.create({
367
+ parent: spaceName as string,
368
+ requestBody: { text: resultText },
369
+ });
370
+ message.ack();
371
+ return;
372
+ }
373
+ const attachments = eventMessage?.attachment || [];
374
+
375
+ if (attachments.length > 0) {
376
+ const tmpDir = path.join(getClawminiDir(process.cwd()), 'tmp', 'google-chat');
377
+ await fsPromises.mkdir(tmpDir, { recursive: true });
378
+
379
+ for (const att of attachments) {
380
+ const resourceName = att.attachmentDataRef?.resourceName;
381
+ if (resourceName) {
382
+ try {
383
+ const buffer = await downloadAttachment(resourceName, config.maxAttachmentSizeMB);
384
+ const uniqueName = `${crypto.randomUUID()}-${att.contentName || 'attachment'}`;
385
+ const filePath = path.join(tmpDir, uniqueName);
386
+ await fsPromises.writeFile(filePath, buffer);
387
+ downloadedFiles.push(filePath);
388
+ } catch (err) {
389
+ console.error(`Error downloading attachment:`, err);
390
+ }
391
+ }
392
+ }
393
+ }
394
+
395
+ await trpc.sendMessage.mutate({
396
+ type: 'send-message',
397
+ client: 'cli',
398
+ data: {
399
+ message: text,
400
+ chatId: targetChatId,
401
+ files: downloadedFiles.length > 0 ? downloadedFiles : undefined,
402
+ adapter: 'google-chat',
403
+ noWait: true,
404
+ },
405
+ });
406
+
407
+ console.log(`Forwarded message from ${identifier} to daemon.`);
408
+ message.ack();
409
+ } catch (error) {
410
+ console.error('Error processing Pub/Sub message:', error);
411
+ for (const file of downloadedFiles) {
412
+ try {
413
+ await fsPromises.unlink(file);
414
+ } catch (unlinkErr) {
415
+ console.error(`Failed to delete downloaded file ${file} after error:`, unlinkErr);
416
+ }
417
+ }
418
+ // Add a brief artificial delay before nacking to avoid tight retry loops
419
+ await new Promise((resolve) => setTimeout(resolve, 2000));
420
+ // Nack the message so it can be retried if it's a transient failure
421
+ message.nack();
422
+ }
423
+ });
424
+
425
+ subscription.on('error', (error) => {
426
+ console.error('Pub/Sub subscription error:', error);
427
+ });
428
+
429
+ return subscription;
430
+ }
@@ -0,0 +1,187 @@
1
+ import { describe, it, expect, vi, beforeEach } from 'vitest';
2
+ import fsPromises from 'node:fs/promises';
3
+ import fs from 'node:fs';
4
+ import {
5
+ GoogleChatConfigSchema,
6
+ isAuthorized,
7
+ readGoogleChatConfig,
8
+ getGoogleChatConfigPath,
9
+ } from './config.js';
10
+
11
+ vi.mock('node:fs/promises');
12
+ vi.mock('node:fs', () => ({
13
+ default: {
14
+ existsSync: vi.fn(),
15
+ },
16
+ existsSync: vi.fn(),
17
+ }));
18
+ vi.mock('../shared/workspace.js', () => ({
19
+ getClawminiDir: () => '/mock/clawmini',
20
+ }));
21
+
22
+ describe('Google Chat Adapter Configuration', () => {
23
+ describe('GoogleChatConfigSchema', () => {
24
+ it('should validate a correct configuration', () => {
25
+ const config = {
26
+ projectId: 'test-project',
27
+ subscriptionName: 'test-sub',
28
+ topicName: 'test-topic',
29
+ authorizedUsers: ['test@example.com'],
30
+ };
31
+ const result = GoogleChatConfigSchema.safeParse(config);
32
+ expect(result.success).toBe(true);
33
+ if (result.success) {
34
+ expect(result.data).toEqual({
35
+ ...config,
36
+ maxAttachmentSizeMB: 25,
37
+ chatId: 'default',
38
+ driveUploadEnabled: true,
39
+ requireMention: false,
40
+ });
41
+ }
42
+ });
43
+
44
+ it('should validate a custom maxAttachmentSizeMB', () => {
45
+ const config = {
46
+ projectId: 'test-project',
47
+ subscriptionName: 'test-sub',
48
+ topicName: 'test-topic',
49
+ authorizedUsers: ['test@example.com'],
50
+ maxAttachmentSizeMB: 50,
51
+ };
52
+ const result = GoogleChatConfigSchema.safeParse(config);
53
+ expect(result.success).toBe(true);
54
+ if (result.success) {
55
+ expect(result.data.maxAttachmentSizeMB).toBe(50);
56
+ }
57
+ });
58
+
59
+ it('should validate messages property', () => {
60
+ const config = {
61
+ projectId: 'test-project',
62
+ subscriptionName: 'test-sub',
63
+ topicName: 'test-topic',
64
+ authorizedUsers: ['test@example.com'],
65
+ messages: { all: true, test: false },
66
+ };
67
+ const result = GoogleChatConfigSchema.safeParse(config);
68
+ expect(result.success).toBe(true);
69
+ if (result.success) {
70
+ expect(result.data.messages).toEqual({ all: true, test: false });
71
+ }
72
+ });
73
+
74
+ it('should fail validation if fields are missing', () => {
75
+ const result = GoogleChatConfigSchema.safeParse({});
76
+ expect(result.success).toBe(false);
77
+ });
78
+
79
+ it('should fail validation if fields are empty', () => {
80
+ const result = GoogleChatConfigSchema.safeParse({
81
+ projectId: '',
82
+ subscriptionName: '',
83
+ topicName: 'test-topic',
84
+ authorizedUsers: [],
85
+ });
86
+ expect(result.success).toBe(false);
87
+ });
88
+ });
89
+
90
+ describe('isAuthorized', () => {
91
+ it('should return true if user ID or email is in authorized users list', () => {
92
+ expect(isAuthorized('user@example.com', ['user@example.com', 'other@example.com'])).toBe(
93
+ true
94
+ );
95
+ });
96
+
97
+ it('should return false if user ID or email is not in authorized users list', () => {
98
+ expect(isAuthorized('unauthorized@example.com', ['user@example.com'])).toBe(false);
99
+ });
100
+ });
101
+
102
+ describe('initGoogleChatConfig', () => {
103
+ beforeEach(async () => {
104
+ vi.clearAllMocks();
105
+ vi.mocked(fs.existsSync).mockReturnValue(false);
106
+ });
107
+
108
+ it('should create directory and template config file if they do not exist', async () => {
109
+ const { initGoogleChatConfig } = await import('./config.js');
110
+ await initGoogleChatConfig();
111
+
112
+ expect(fsPromises.mkdir).toHaveBeenCalledWith('/mock/clawmini/adapters/google-chat', {
113
+ recursive: true,
114
+ });
115
+ expect(fsPromises.writeFile).toHaveBeenCalled();
116
+ const writeCall = vi.mocked(fsPromises.writeFile).mock.calls[0];
117
+ expect(writeCall![0]).toBe(getGoogleChatConfigPath());
118
+ expect(JSON.parse(writeCall![1] as string)).toEqual({
119
+ projectId: 'YOUR_PROJECT_ID',
120
+ subscriptionName: 'YOUR_SUBSCRIPTION_NAME',
121
+ topicName: 'YOUR_TOPIC_NAME',
122
+ authorizedUsers: ['user@example.com'],
123
+ chatId: 'default',
124
+ requireMention: false,
125
+ oauthClientId: 'YOUR_OAUTH_CLIENT_ID',
126
+ oauthClientSecret: 'YOUR_OAUTH_CLIENT_SECRET',
127
+ });
128
+ });
129
+
130
+ it('should not overwrite existing config file', async () => {
131
+ vi.mocked(fs.existsSync).mockReturnValue(true);
132
+ const { initGoogleChatConfig } = await import('./config.js');
133
+ await initGoogleChatConfig();
134
+
135
+ expect(fsPromises.writeFile).not.toHaveBeenCalled();
136
+ });
137
+ });
138
+
139
+ describe('readGoogleChatConfig', () => {
140
+ beforeEach(() => {
141
+ vi.clearAllMocks();
142
+ });
143
+
144
+ it('should successfully read and parse a valid config file', async () => {
145
+ const mockConfig = {
146
+ projectId: 'test-project',
147
+ subscriptionName: 'test-sub',
148
+ topicName: 'test-topic',
149
+ authorizedUsers: ['user@example.com'],
150
+ };
151
+ vi.mocked(fsPromises.readFile).mockResolvedValue(JSON.stringify(mockConfig));
152
+
153
+ const config = await readGoogleChatConfig();
154
+ expect(config).toEqual({
155
+ ...mockConfig,
156
+ maxAttachmentSizeMB: 25,
157
+ chatId: 'default',
158
+ driveUploadEnabled: true,
159
+ requireMention: false,
160
+ });
161
+ expect(fsPromises.readFile).toHaveBeenCalledWith(getGoogleChatConfigPath(), 'utf-8');
162
+ });
163
+
164
+ it('should return null if the config file does not exist', async () => {
165
+ const error = new Error('File not found') as Error & { code: string };
166
+ error.code = 'ENOENT';
167
+ vi.mocked(fsPromises.readFile).mockRejectedValue(error);
168
+
169
+ const config = await readGoogleChatConfig();
170
+ expect(config).toBeNull();
171
+ });
172
+
173
+ it('should throw if the config file contains invalid JSON', async () => {
174
+ vi.mocked(fsPromises.readFile).mockResolvedValue('invalid-json');
175
+
176
+ await expect(readGoogleChatConfig()).rejects.toThrow();
177
+ });
178
+
179
+ it('should throw if the config fails schema validation', async () => {
180
+ vi.mocked(fsPromises.readFile).mockResolvedValue(
181
+ JSON.stringify({ subscriptionName: 'test' })
182
+ );
183
+
184
+ await expect(readGoogleChatConfig()).rejects.toThrow();
185
+ });
186
+ });
187
+ });
@@ -0,0 +1,82 @@
1
+ import fsPromises from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import { z } from 'zod';
4
+ import { getClawminiDir } from '../shared/workspace.js';
5
+ import fs from 'node:fs';
6
+
7
+ export const GoogleChatConfigSchema = z.looseObject({
8
+ projectId: z.string().min(1, 'GCP Project ID is required.'),
9
+ subscriptionName: z.string().min(1, 'Pub/Sub Subscription Name is required.'),
10
+ topicName: z.string().min(1, 'Pub/Sub Topic Name is required.'),
11
+ authorizedUsers: z.array(z.string()).min(1, 'At least one Authorized User is required.'),
12
+ maxAttachmentSizeMB: z.number().default(25).optional(),
13
+ chatId: z.string().default('default').optional(),
14
+ directMessageName: z.string().optional(),
15
+ driveUploadEnabled: z.boolean().default(true).optional(),
16
+ requireMention: z.boolean().default(false),
17
+ oauthClientId: z.string().optional(),
18
+ oauthClientSecret: z.string().optional(),
19
+ });
20
+
21
+ export type GoogleChatConfig = z.infer<typeof GoogleChatConfigSchema>;
22
+
23
+ export function getGoogleChatConfigPath(startDir = process.cwd()): string {
24
+ return path.join(getClawminiDir(startDir), 'adapters', 'google-chat', 'config.json');
25
+ }
26
+
27
+ export async function readGoogleChatConfig(
28
+ startDir = process.cwd()
29
+ ): Promise<GoogleChatConfig | null> {
30
+ const configPath = getGoogleChatConfigPath(startDir);
31
+ try {
32
+ const data = await fsPromises.readFile(configPath, 'utf-8');
33
+ const parsed = JSON.parse(data);
34
+ return GoogleChatConfigSchema.parse(parsed);
35
+ } catch (err: unknown) {
36
+ if ((err as NodeJS.ErrnoException).code === 'ENOENT') {
37
+ return null;
38
+ }
39
+ throw err;
40
+ }
41
+ }
42
+
43
+ export async function updateGoogleChatConfig(
44
+ config: GoogleChatConfig,
45
+ startDir = process.cwd()
46
+ ): Promise<void> {
47
+ const configPath = getGoogleChatConfigPath(startDir);
48
+ await fsPromises.writeFile(configPath, JSON.stringify(config, null, 2), 'utf-8');
49
+ }
50
+
51
+ export async function initGoogleChatConfig(startDir = process.cwd()): Promise<void> {
52
+ const configPath = getGoogleChatConfigPath(startDir);
53
+ const configDir = path.dirname(configPath);
54
+
55
+ await fsPromises.mkdir(configDir, { recursive: true });
56
+
57
+ if (fs.existsSync(configPath)) {
58
+ console.log(`Config file already exists at ${configPath}`);
59
+ return;
60
+ }
61
+
62
+ const templateConfig = {
63
+ projectId: 'YOUR_PROJECT_ID',
64
+ topicName: 'YOUR_TOPIC_NAME',
65
+ subscriptionName: 'YOUR_SUBSCRIPTION_NAME',
66
+ authorizedUsers: ['user@example.com'],
67
+ chatId: 'default',
68
+ requireMention: false,
69
+ oauthClientId: 'YOUR_OAUTH_CLIENT_ID',
70
+ oauthClientSecret: 'YOUR_OAUTH_CLIENT_SECRET',
71
+ };
72
+
73
+ await fsPromises.writeFile(configPath, JSON.stringify(templateConfig, null, 2), 'utf-8');
74
+ console.log(`Created template configuration file at ${configPath}`);
75
+ console.log(
76
+ 'Please update it with your actual GCP Project ID, Pub/Sub Topic Name, Pub/Sub Subscription Name, and Authorized Users.'
77
+ );
78
+ }
79
+
80
+ export function isAuthorized(userIdOrEmail: string, authorizedUsers: string[]): boolean {
81
+ return authorizedUsers.some((u) => u.toLowerCase() === userIdOrEmail.toLowerCase());
82
+ }