clawmini 0.0.8 → 0.0.9

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 (367) hide show
  1. package/.changeset/README.md +8 -0
  2. package/.changeset/config.json +14 -0
  3. package/.github/workflows/release.yml +49 -0
  4. package/CHANGELOG.md +36 -0
  5. package/README.md +5 -4
  6. package/dist/adapter-discord/index.d.mts.map +1 -1
  7. package/dist/adapter-discord/index.mjs +465 -282
  8. package/dist/adapter-discord/index.mjs.map +1 -1
  9. package/dist/adapter-google-chat/index.mjs +367 -243
  10. package/dist/adapter-google-chat/index.mjs.map +1 -1
  11. package/dist/cli/index.mjs +684 -24
  12. package/dist/cli/index.mjs.map +1 -1
  13. package/dist/cli/lite.mjs +43 -13
  14. package/dist/cli/lite.mjs.map +1 -1
  15. package/dist/cli/{propose-policy.mjs → manage-policies.mjs} +270 -47
  16. package/dist/cli/manage-policies.mjs.map +1 -0
  17. package/dist/cli/run-host.d.mts +1 -0
  18. package/dist/cli/run-host.mjs +3090 -0
  19. package/dist/cli/run-host.mjs.map +1 -0
  20. package/dist/config-CPFQIGdG.mjs +57 -0
  21. package/dist/config-CPFQIGdG.mjs.map +1 -0
  22. package/dist/config-Dvl-Pov4.mjs +76 -0
  23. package/dist/config-Dvl-Pov4.mjs.map +1 -0
  24. package/dist/daemon/index.d.mts.map +1 -1
  25. package/dist/daemon/index.mjs +970 -332
  26. package/dist/daemon/index.mjs.map +1 -1
  27. package/dist/supervisor-actions-CiW56eLi.mjs +843 -0
  28. package/dist/supervisor-actions-CiW56eLi.mjs.map +1 -0
  29. package/dist/turn-log-buffer-DRgW53gl.mjs +767 -0
  30. package/dist/turn-log-buffer-DRgW53gl.mjs.map +1 -0
  31. package/dist/web/_app/immutable/chunks/{Drm9vgeP.js → 3AZlWB6U.js} +1 -1
  32. package/dist/web/_app/immutable/chunks/BhRSsUCh.js +2 -0
  33. package/dist/web/_app/immutable/chunks/BiLeM2i1.js +1 -0
  34. package/{web/.svelte-kit/output/client/_app/immutable/chunks/CME08kGM.js → dist/web/_app/immutable/chunks/BmBj85Ll.js} +1 -1
  35. package/dist/web/_app/immutable/chunks/BrERcKAH.js +1 -0
  36. package/dist/web/_app/immutable/chunks/Bv9252RM.js +1 -0
  37. package/dist/web/_app/immutable/chunks/CIXNBPKi.js +1 -0
  38. package/dist/web/_app/immutable/chunks/DISKL3GN.js +2 -0
  39. package/dist/web/_app/immutable/chunks/{Zeh-C-mx.js → DcpaLzmX.js} +1 -1
  40. package/dist/web/_app/immutable/chunks/DnQ3vS13.js +1 -0
  41. package/dist/web/_app/immutable/chunks/KsloHTKS.js +1 -0
  42. package/{web/.svelte-kit/output/client/_app/immutable/chunks/Ck-be5J2.js → dist/web/_app/immutable/chunks/RsHsUj-8.js} +2 -2
  43. package/dist/web/_app/immutable/chunks/{vDehDcuJ.js → wpfV79dV.js} +1 -1
  44. package/dist/web/_app/immutable/entry/app.CIw1Qj0n.js +2 -0
  45. package/dist/web/_app/immutable/entry/start.Di0-Jhte.js +1 -0
  46. package/dist/web/_app/immutable/nodes/{0.CUGC2p-K.js → 0.DYyUA1au.js} +1 -1
  47. package/dist/web/_app/immutable/nodes/1.D-3QEMMZ.js +1 -0
  48. package/dist/web/_app/immutable/nodes/{2.BnwnD1Ki.js → 2.4olHnH7U.js} +1 -1
  49. package/{web/.svelte-kit/output/client/_app/immutable/nodes/3.0arZe_Uf.js → dist/web/_app/immutable/nodes/3.4w0bE-m2.js} +3 -3
  50. package/dist/web/_app/immutable/nodes/4.CZvjhVHt.js +60 -0
  51. package/dist/web/_app/immutable/nodes/{5.Bq2JzCEj.js → 5.DLbPVJY2.js} +1 -1
  52. package/dist/web/_app/version.json +1 -1
  53. package/dist/web/index.html +12 -12
  54. package/dist/workspace-oWmVh5mi.mjs +1001 -0
  55. package/dist/workspace-oWmVh5mi.mjs.map +1 -0
  56. package/docs/23_adapter_slash_autocomplete/development_log.md +19 -0
  57. package/docs/23_adapter_slash_autocomplete/notes.md +18 -0
  58. package/docs/23_adapter_slash_autocomplete/prd.md +46 -0
  59. package/docs/23_adapter_slash_autocomplete/questions.md +6 -0
  60. package/docs/23_adapter_slash_autocomplete/tickets.md +21 -0
  61. package/docs/24_subagent_job_policy_fixes/development_log.md +22 -0
  62. package/docs/24_subagent_job_policy_fixes/notes.md +28 -0
  63. package/docs/24_subagent_job_policy_fixes/prd.md +59 -0
  64. package/docs/24_subagent_job_policy_fixes/questions.md +3 -0
  65. package/docs/24_subagent_job_policy_fixes/tickets.md +49 -0
  66. package/docs/25_e2e_test_improvements/development_log.md +30 -0
  67. package/docs/25_e2e_test_improvements/notes.md +29 -0
  68. package/docs/25_e2e_test_improvements/prd.md +43 -0
  69. package/docs/25_e2e_test_improvements/questions.md +12 -0
  70. package/docs/25_e2e_test_improvements/tickets-2.md +22 -0
  71. package/docs/25_e2e_test_improvements/tickets.md +22 -0
  72. package/docs/25_policy_cwd/development_log.md +30 -0
  73. package/docs/25_policy_cwd/notes.md +28 -0
  74. package/docs/25_policy_cwd/prd.md +77 -0
  75. package/docs/25_policy_cwd/questions.md +6 -0
  76. package/docs/25_policy_cwd/tickets.md +77 -0
  77. package/docs/CLI_REFERENCE.md +3 -1
  78. package/docs/PHILOSOPHY.md +35 -0
  79. package/docs/adapter-visibility/SPEC.md +461 -0
  80. package/docs/adapter-visibility/SPEC_v2.md +202 -0
  81. package/docs/auto-update/SPEC.md +344 -0
  82. package/docs/backups/SPEC.md +296 -0
  83. package/docs/backups/clawmini.gitignore +69 -0
  84. package/docs/guides/assets/clawmini-avatar.png +0 -0
  85. package/docs/guides/backups.md +332 -0
  86. package/docs/guides/discord_adapter_setup.md +1 -1
  87. package/docs/guides/google_chat_adapter_setup.md +81 -0
  88. package/docs/unified-startup/SPEC.md +203 -0
  89. package/e2e/_helpers/test-environment.test.ts +49 -0
  90. package/e2e/_helpers/test-environment.ts +548 -0
  91. package/e2e/adapters/_google-chat-fixtures.ts +340 -0
  92. package/{src/cli/e2e → e2e/adapters}/adapter-discord.test.ts +22 -23
  93. package/e2e/adapters/adapter-google-chat-downtime.test.ts +157 -0
  94. package/e2e/adapters/adapter-google-chat-inbound.test.ts +697 -0
  95. package/e2e/adapters/adapter-google-chat-outbound.test.ts +297 -0
  96. package/e2e/adapters/adapter-google-chat-roundtrip.test.ts +56 -0
  97. package/e2e/adapters/adapter-google-chat-threads.test.ts +1078 -0
  98. package/e2e/agents/custom-api-env.test.ts +80 -0
  99. package/e2e/agents/export-lite-func.test.ts +104 -0
  100. package/e2e/agents/fallbacks.test.ts +124 -0
  101. package/e2e/agents/interrupt.test.ts +50 -0
  102. package/e2e/agents/no-reply-necessary.test.ts +57 -0
  103. package/e2e/agents/session-timeout-subagents.test.ts +76 -0
  104. package/e2e/agents/subagent-authorization.test.ts +246 -0
  105. package/e2e/agents/subagent-env.test.ts +49 -0
  106. package/e2e/agents/subagent-lifecycle.test.ts +782 -0
  107. package/e2e/agents/subagents-depth.test.ts +47 -0
  108. package/e2e/cli/agents.test.ts +176 -0
  109. package/e2e/cli/auto-update.test.ts +741 -0
  110. package/e2e/cli/basic.test.ts +44 -0
  111. package/{src/cli/e2e → e2e/cli}/export-lite.test.ts +16 -12
  112. package/e2e/cli/init-gitignore.test.ts +86 -0
  113. package/e2e/cli/init.test.ts +76 -0
  114. package/e2e/cli/messages.test.ts +363 -0
  115. package/e2e/cli/serve.test.ts +76 -0
  116. package/{src/cli/e2e → e2e/cli}/skills.test.ts +11 -10
  117. package/{src/cli/e2e → e2e/daemon}/daemon.test.ts +57 -195
  118. package/e2e/jobs/agent-jobs.test.ts +216 -0
  119. package/e2e/jobs/cron.test.ts +64 -0
  120. package/e2e/jobs/restart.test.ts +108 -0
  121. package/e2e/policies/approval-session.test.ts +69 -0
  122. package/e2e/policies/auto-create-policies-file.test.ts +35 -0
  123. package/e2e/policies/builtin-manage-policies.test.ts +184 -0
  124. package/e2e/policies/builtin-run-host.test.ts +180 -0
  125. package/e2e/policies/environment-policies.test.ts +177 -0
  126. package/e2e/policies/manage-policies.test.ts +566 -0
  127. package/e2e/policies/output-size.test.ts +98 -0
  128. package/e2e/policies/policies-context-cwd.test.ts +160 -0
  129. package/e2e/policies/relative-script-path.test.ts +60 -0
  130. package/e2e/policies/requests-show.test.ts +135 -0
  131. package/e2e/policies/requests.test.ts +208 -0
  132. package/e2e/policies/slash-policies.test.ts +308 -0
  133. package/e2e/policies/startup-cleanup.test.ts +48 -0
  134. package/e2e/routers/session-timeout.test.ts +106 -0
  135. package/e2e/routers/slash-model.test.ts +152 -0
  136. package/e2e/routers/slash-new.test.ts +50 -0
  137. package/e2e/routers/slash-restart-adapter.test.ts +96 -0
  138. package/e2e/routers/slash-restart.test.ts +114 -0
  139. package/e2e/routers/slash-shutdown.test.ts +55 -0
  140. package/e2e/routers/slash-stop.test.ts +232 -0
  141. package/e2e/routers/slash-upgrade.test.ts +88 -0
  142. package/{src/cli/e2e → e2e/sandbox}/environments.test.ts +14 -13
  143. package/eslint.config.js +6 -0
  144. package/napkin.md +1 -1
  145. package/package.json +8 -3
  146. package/src/adapter-discord/commands.test.ts +42 -0
  147. package/src/adapter-discord/commands.ts +33 -0
  148. package/src/adapter-discord/config.ts +12 -0
  149. package/src/adapter-discord/forwarder.test.ts +499 -21
  150. package/src/adapter-discord/forwarder.ts +343 -124
  151. package/src/adapter-discord/inbound-cache.test.ts +47 -0
  152. package/src/adapter-discord/inbound-cache.ts +37 -0
  153. package/src/adapter-discord/index.test.ts +67 -2
  154. package/src/adapter-discord/index.ts +84 -216
  155. package/src/adapter-discord/interactions.test.ts +54 -3
  156. package/src/adapter-discord/interactions.ts +97 -53
  157. package/src/adapter-discord/processMessage.ts +239 -0
  158. package/src/adapter-discord/state.ts +1 -0
  159. package/src/adapter-google-chat/auth.test.ts +9 -5
  160. package/src/adapter-google-chat/auth.ts +29 -23
  161. package/src/adapter-google-chat/cards.ts +7 -2
  162. package/src/adapter-google-chat/client.test.ts +37 -2
  163. package/src/adapter-google-chat/client.ts +138 -38
  164. package/src/adapter-google-chat/config.ts +19 -0
  165. package/src/adapter-google-chat/forwarder.test.ts +81 -56
  166. package/src/adapter-google-chat/forwarder.ts +394 -185
  167. package/src/adapter-google-chat/inbound-cache.test.ts +61 -0
  168. package/src/adapter-google-chat/inbound-cache.ts +36 -0
  169. package/src/adapter-google-chat/state.test.ts +1 -0
  170. package/src/adapter-google-chat/state.ts +9 -1
  171. package/src/adapter-google-chat/subscriptions.ts +8 -6
  172. package/src/cli/builtin-policies.ts +44 -0
  173. package/src/cli/commands/agents.ts +59 -5
  174. package/src/cli/commands/down.ts +54 -2
  175. package/src/cli/commands/environments.ts +8 -2
  176. package/src/cli/commands/init.ts +31 -0
  177. package/src/cli/commands/logs.ts +116 -0
  178. package/src/cli/commands/policies.ts +6 -4
  179. package/src/cli/commands/serve.test.ts +67 -0
  180. package/src/cli/commands/serve.ts +284 -0
  181. package/src/cli/commands/up.ts +122 -2
  182. package/src/cli/commands/web-api/agents.ts +3 -2
  183. package/src/cli/index.ts +4 -0
  184. package/src/cli/install-detection.test.ts +72 -0
  185. package/src/cli/install-detection.ts +48 -0
  186. package/src/cli/lite.ts +54 -22
  187. package/src/cli/manage-policies-utils.ts +104 -0
  188. package/src/cli/manage-policies.ts +291 -0
  189. package/src/cli/run-host.ts +45 -0
  190. package/src/cli/supervisor-actions.ts +267 -0
  191. package/src/cli/supervisor-control.test.ts +129 -0
  192. package/src/cli/supervisor-control.ts +155 -0
  193. package/src/cli/supervisor-pid.ts +68 -0
  194. package/src/cli/supervisor.ts +277 -0
  195. package/src/daemon/agent/agent-context.ts +11 -11
  196. package/src/daemon/agent/agent-session.ts +8 -1
  197. package/src/daemon/agent/chat-logger.test.ts +78 -9
  198. package/src/daemon/agent/chat-logger.ts +25 -5
  199. package/src/daemon/agent/turn-registry.test.ts +89 -0
  200. package/src/daemon/agent/turn-registry.ts +94 -0
  201. package/src/daemon/agent/types.ts +2 -0
  202. package/src/daemon/api/agent-policy-endpoints.ts +263 -0
  203. package/src/daemon/api/agent-router.ts +47 -126
  204. package/src/daemon/api/index.test.ts +1 -0
  205. package/src/daemon/api/policy-request.test.ts +7 -5
  206. package/src/daemon/api/router-utils.ts +6 -5
  207. package/src/daemon/api/subagent-router.ts +110 -74
  208. package/src/daemon/api/subagent-utils.test.ts +60 -0
  209. package/src/daemon/api/subagent-utils.ts +113 -87
  210. package/src/daemon/api/user-router.ts +34 -8
  211. package/src/daemon/auth.ts +1 -0
  212. package/src/daemon/cron.test.ts +62 -4
  213. package/src/daemon/cron.ts +42 -16
  214. package/src/daemon/events.ts +65 -0
  215. package/src/daemon/index.ts +24 -1
  216. package/src/daemon/message-interruption.test.ts +1 -0
  217. package/src/daemon/message-jobs.test.ts +1 -0
  218. package/src/daemon/message.ts +78 -14
  219. package/src/daemon/observation.test.ts +26 -18
  220. package/src/daemon/pending-replies.test.ts +112 -0
  221. package/src/daemon/pending-replies.ts +162 -0
  222. package/src/daemon/policy-request-service.ts +3 -1
  223. package/src/daemon/policy-utils.test.ts +66 -1
  224. package/src/daemon/policy-utils.ts +126 -1
  225. package/src/daemon/request-store.ts +31 -0
  226. package/src/daemon/routers/session-timeout.ts +4 -0
  227. package/src/daemon/routers/slash-model.test.ts +344 -0
  228. package/src/daemon/routers/slash-model.ts +207 -0
  229. package/src/daemon/routers/slash-policies.test.ts +38 -32
  230. package/src/daemon/routers/slash-policies.ts +84 -33
  231. package/src/daemon/routers/slash-restart.test.ts +69 -0
  232. package/src/daemon/routers/slash-restart.ts +36 -0
  233. package/src/daemon/routers/slash-shutdown.test.ts +50 -0
  234. package/src/daemon/routers/slash-shutdown.ts +28 -0
  235. package/src/daemon/routers/slash-upgrade.test.ts +116 -0
  236. package/src/daemon/routers/slash-upgrade.ts +76 -0
  237. package/src/daemon/routers/types.ts +7 -0
  238. package/src/daemon/routers.ts +16 -0
  239. package/src/shared/adapters/blockquote.test.ts +28 -0
  240. package/src/shared/adapters/blockquote.ts +20 -0
  241. package/src/shared/adapters/filtering.test.ts +224 -10
  242. package/src/shared/adapters/filtering.ts +95 -7
  243. package/src/shared/adapters/inbound-cache.test.ts +48 -0
  244. package/src/shared/adapters/inbound-cache.ts +54 -0
  245. package/src/shared/adapters/turn-log-buffer.ts +266 -0
  246. package/src/shared/adapters/turn-log.test.ts +389 -0
  247. package/src/shared/adapters/turn-log.ts +357 -0
  248. package/src/shared/agent-utils.ts +12 -5
  249. package/src/shared/chats.test.ts +4 -0
  250. package/src/shared/chats.ts +9 -0
  251. package/src/shared/config.ts +16 -1
  252. package/src/shared/lite.ts +76 -2
  253. package/src/shared/policies.ts +26 -0
  254. package/src/shared/template-manifest.ts +267 -0
  255. package/src/shared/utils/shell.ts +61 -0
  256. package/src/shared/version.ts +34 -0
  257. package/src/shared/workspace.test.ts +217 -0
  258. package/src/shared/workspace.ts +626 -48
  259. package/templates/environments/cladding/allowlist-domain.mjs +125 -0
  260. package/templates/environments/cladding/env.json +21 -1
  261. package/templates/environments/cladding/run-with-network.mjs +54 -0
  262. package/templates/environments/macos-proxy/allowlist-domain.mjs +95 -0
  263. package/templates/environments/macos-proxy/env.json +8 -1
  264. package/templates/environments/macos-proxy/proxy.mjs +0 -1
  265. package/templates/gemini/template.json +5 -0
  266. package/templates/gemini-claw/template.json +13 -0
  267. package/templates/skills/clawmini-requests/SKILL.md +69 -10
  268. package/templates/skills/run-host/SKILL.md +51 -0
  269. package/templates/skills/skill-creator/SKILL.md +4 -3
  270. package/templates/skills/skill-creator/scripts/validate.sh +52 -0
  271. package/tsdown.config.ts +10 -1
  272. package/vitest.config.ts +2 -2
  273. package/web/.svelte-kit/ambient.d.ts +292 -118
  274. package/web/.svelte-kit/generated/server/internal.js +1 -1
  275. package/web/.svelte-kit/output/client/.vite/manifest.json +126 -136
  276. package/web/.svelte-kit/output/client/_app/immutable/chunks/{Drm9vgeP.js → 3AZlWB6U.js} +1 -1
  277. package/web/.svelte-kit/output/client/_app/immutable/chunks/BhRSsUCh.js +2 -0
  278. package/web/.svelte-kit/output/client/_app/immutable/chunks/BiLeM2i1.js +1 -0
  279. package/{dist/web/_app/immutable/chunks/CME08kGM.js → web/.svelte-kit/output/client/_app/immutable/chunks/BmBj85Ll.js} +1 -1
  280. package/web/.svelte-kit/output/client/_app/immutable/chunks/BrERcKAH.js +1 -0
  281. package/web/.svelte-kit/output/client/_app/immutable/chunks/Bv9252RM.js +1 -0
  282. package/web/.svelte-kit/output/client/_app/immutable/chunks/CIXNBPKi.js +1 -0
  283. package/web/.svelte-kit/output/client/_app/immutable/chunks/DISKL3GN.js +2 -0
  284. package/web/.svelte-kit/output/client/_app/immutable/chunks/{Zeh-C-mx.js → DcpaLzmX.js} +1 -1
  285. package/web/.svelte-kit/output/client/_app/immutable/chunks/DnQ3vS13.js +1 -0
  286. package/web/.svelte-kit/output/client/_app/immutable/chunks/KsloHTKS.js +1 -0
  287. package/{dist/web/_app/immutable/chunks/Ck-be5J2.js → web/.svelte-kit/output/client/_app/immutable/chunks/RsHsUj-8.js} +2 -2
  288. package/web/.svelte-kit/output/client/_app/immutable/chunks/{vDehDcuJ.js → wpfV79dV.js} +1 -1
  289. package/web/.svelte-kit/output/client/_app/immutable/entry/app.CIw1Qj0n.js +2 -0
  290. package/web/.svelte-kit/output/client/_app/immutable/entry/start.Di0-Jhte.js +1 -0
  291. package/web/.svelte-kit/output/client/_app/immutable/nodes/{0.CUGC2p-K.js → 0.DYyUA1au.js} +1 -1
  292. package/web/.svelte-kit/output/client/_app/immutable/nodes/1.D-3QEMMZ.js +1 -0
  293. package/web/.svelte-kit/output/client/_app/immutable/nodes/{2.BnwnD1Ki.js → 2.4olHnH7U.js} +1 -1
  294. package/{dist/web/_app/immutable/nodes/3.0arZe_Uf.js → web/.svelte-kit/output/client/_app/immutable/nodes/3.4w0bE-m2.js} +3 -3
  295. package/web/.svelte-kit/output/client/_app/immutable/nodes/4.CZvjhVHt.js +60 -0
  296. package/web/.svelte-kit/output/client/_app/immutable/nodes/{5.Bq2JzCEj.js → 5.DLbPVJY2.js} +1 -1
  297. package/web/.svelte-kit/output/client/_app/version.json +1 -1
  298. package/web/.svelte-kit/output/server/.vite/manifest.json +12 -10
  299. package/web/.svelte-kit/output/server/chunks/Icon.js +1 -1
  300. package/web/.svelte-kit/output/server/chunks/client.js +1 -1
  301. package/web/.svelte-kit/output/server/chunks/exports.js +1 -1
  302. package/web/.svelte-kit/output/server/chunks/index-server.js +2 -1
  303. package/web/.svelte-kit/output/server/chunks/internal.js +1 -1
  304. package/web/.svelte-kit/output/server/chunks/render-context.js +77 -0
  305. package/web/.svelte-kit/output/server/chunks/root.js +739 -788
  306. package/web/.svelte-kit/output/server/chunks/shared.js +234 -21
  307. package/web/.svelte-kit/output/server/index.js +126 -90
  308. package/web/.svelte-kit/output/server/manifest-full.js +1 -1
  309. package/web/.svelte-kit/output/server/manifest.js +1 -1
  310. package/web/.svelte-kit/output/server/nodes/0.js +1 -1
  311. package/web/.svelte-kit/output/server/nodes/1.js +1 -1
  312. package/web/.svelte-kit/output/server/nodes/2.js +1 -1
  313. package/web/.svelte-kit/output/server/nodes/3.js +1 -1
  314. package/web/.svelte-kit/output/server/nodes/4.js +1 -1
  315. package/web/.svelte-kit/output/server/nodes/5.js +1 -1
  316. package/web/.svelte-kit/output/server/remote-entry.js +245 -81
  317. package/web/.svelte-kit/tsconfig.json +4 -1
  318. package/dist/cli/propose-policy.mjs.map +0 -1
  319. package/dist/lite-CBxOT1y5.mjs +0 -241
  320. package/dist/lite-CBxOT1y5.mjs.map +0 -1
  321. package/dist/routing-D8rTxtaV.mjs +0 -245
  322. package/dist/routing-D8rTxtaV.mjs.map +0 -1
  323. package/dist/web/_app/immutable/chunks/B6YN0Nuq.js +0 -1
  324. package/dist/web/_app/immutable/chunks/BmRlVmv6.js +0 -1
  325. package/dist/web/_app/immutable/chunks/CK9JZLaG.js +0 -2
  326. package/dist/web/_app/immutable/chunks/Ck3rYNON.js +0 -1
  327. package/dist/web/_app/immutable/chunks/D5iV40bG.js +0 -1
  328. package/dist/web/_app/immutable/chunks/DMtIqaiV.js +0 -2
  329. package/dist/web/_app/immutable/chunks/DhD271EB.js +0 -1
  330. package/dist/web/_app/immutable/chunks/DpuLqk8d.js +0 -1
  331. package/dist/web/_app/immutable/chunks/DsIToJCP.js +0 -1
  332. package/dist/web/_app/immutable/entry/app.BCSV3nrG.js +0 -2
  333. package/dist/web/_app/immutable/entry/start.D4eLEZUM.js +0 -1
  334. package/dist/web/_app/immutable/nodes/1.CGC_42IQ.js +0 -1
  335. package/dist/web/_app/immutable/nodes/4.ClM1bXLE.js +0 -60
  336. package/dist/workspace-BJmJBfKi.mjs +0 -456
  337. package/dist/workspace-BJmJBfKi.mjs.map +0 -1
  338. package/src/cli/e2e/agents.test.ts +0 -140
  339. package/src/cli/e2e/basic.test.ts +0 -43
  340. package/src/cli/e2e/cron.test.ts +0 -132
  341. package/src/cli/e2e/export-lite-func.test.ts +0 -206
  342. package/src/cli/e2e/fallbacks.test.ts +0 -175
  343. package/src/cli/e2e/init.test.ts +0 -77
  344. package/src/cli/e2e/messages.test.ts +0 -332
  345. package/src/cli/e2e/propose-policy.test.ts +0 -203
  346. package/src/cli/e2e/requests.test.ts +0 -180
  347. package/src/cli/e2e/session-timeout.test.ts +0 -192
  348. package/src/cli/e2e/slash-new.test.ts +0 -93
  349. package/src/cli/e2e/subagents.test.ts +0 -106
  350. package/src/cli/e2e/utils.ts +0 -66
  351. package/src/cli/propose-policy.ts +0 -91
  352. package/web/.svelte-kit/output/client/_app/immutable/chunks/B6YN0Nuq.js +0 -1
  353. package/web/.svelte-kit/output/client/_app/immutable/chunks/BmRlVmv6.js +0 -1
  354. package/web/.svelte-kit/output/client/_app/immutable/chunks/CK9JZLaG.js +0 -2
  355. package/web/.svelte-kit/output/client/_app/immutable/chunks/Ck3rYNON.js +0 -1
  356. package/web/.svelte-kit/output/client/_app/immutable/chunks/D5iV40bG.js +0 -1
  357. package/web/.svelte-kit/output/client/_app/immutable/chunks/DMtIqaiV.js +0 -2
  358. package/web/.svelte-kit/output/client/_app/immutable/chunks/DhD271EB.js +0 -1
  359. package/web/.svelte-kit/output/client/_app/immutable/chunks/DpuLqk8d.js +0 -1
  360. package/web/.svelte-kit/output/client/_app/immutable/chunks/DsIToJCP.js +0 -1
  361. package/web/.svelte-kit/output/client/_app/immutable/entry/app.BCSV3nrG.js +0 -2
  362. package/web/.svelte-kit/output/client/_app/immutable/entry/start.D4eLEZUM.js +0 -1
  363. package/web/.svelte-kit/output/client/_app/immutable/nodes/1.CGC_42IQ.js +0 -1
  364. package/web/.svelte-kit/output/client/_app/immutable/nodes/4.ClM1bXLE.js +0 -60
  365. package/web/.svelte-kit/output/server/chunks/false.js +0 -4
  366. /package/dist/cli/{propose-policy.d.mts → manage-policies.d.mts} +0 -0
  367. /package/{src/cli/e2e → e2e/_helpers}/global-setup.ts +0 -0
@@ -5,8 +5,10 @@ import {
5
5
  TextInputStyle,
6
6
  type Interaction,
7
7
  } from 'discord.js';
8
- import { readDiscordState } from './state.js';
9
8
  import type { DiscordConfig } from './config.js';
9
+ import { readDiscordState } from './state.js';
10
+ import { type FilteringConfig } from '../shared/adapters/filtering.js';
11
+ import { processDiscordMessage } from './processMessage.js';
10
12
 
11
13
  function isAuthorized(userId: string, authorizedUserId: string): boolean {
12
14
  return userId === authorizedUserId;
@@ -16,9 +18,16 @@ export async function handleDiscordInteraction(
16
18
  interaction: Interaction,
17
19
  config: DiscordConfig,
18
20
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
19
- trpc: any
21
+ trpc: any,
22
+ filteringConfig: FilteringConfig
20
23
  ) {
21
- if (!interaction.isButton() && !interaction.isModalSubmit()) return;
24
+ if (
25
+ !interaction.isButton() &&
26
+ !interaction.isModalSubmit() &&
27
+ !interaction.isChatInputCommand()
28
+ ) {
29
+ return;
30
+ }
22
31
 
23
32
  if (!isAuthorized(interaction.user.id, config.authorizedUserId)) {
24
33
  if (interaction.isRepliable()) {
@@ -30,6 +39,48 @@ export async function handleDiscordInteraction(
30
39
  return;
31
40
  }
32
41
 
42
+ if (interaction.isChatInputCommand()) {
43
+ const { commandName } = interaction;
44
+ let commandStr = `/${commandName}`;
45
+
46
+ if (commandName === 'approve' || commandName === 'reject') {
47
+ const policyId = interaction.options.getString('policy_id');
48
+ if (policyId) commandStr += ` ${policyId}`;
49
+ }
50
+ if (commandName === 'reject') {
51
+ const rationale = interaction.options.getString('rationale');
52
+ if (rationale) commandStr += ` ${rationale}`;
53
+ }
54
+
55
+ await interaction.deferReply({ ephemeral: true });
56
+
57
+ const currentState = await readDiscordState();
58
+ const targetChatId = interaction.channelId
59
+ ? currentState.channelChatMap?.[interaction.channelId]?.chatId || config.chatId
60
+ : config.chatId;
61
+
62
+ let replied = false;
63
+ await processDiscordMessage(
64
+ commandStr,
65
+ interaction.user,
66
+ interaction.channelId,
67
+ interaction.guild,
68
+ async (text) => {
69
+ replied = true;
70
+ await interaction.followUp({ content: text, ephemeral: true });
71
+ },
72
+ config,
73
+ trpc,
74
+ filteringConfig,
75
+ { explicitChatId: targetChatId, mentionsBot: true }
76
+ );
77
+
78
+ if (!replied) {
79
+ await interaction.deleteReply();
80
+ }
81
+ return;
82
+ }
83
+
33
84
  if (interaction.isButton()) {
34
85
  if (
35
86
  interaction.customId.startsWith('approve_') ||
@@ -46,30 +97,27 @@ export async function handleDiscordInteraction(
46
97
 
47
98
  await interaction.update({ components: [] });
48
99
  await interaction.followUp({ content: `Approving policy ${policyId}...`, ephemeral: true });
49
- try {
50
- const currentState = await readDiscordState();
51
- const targetChatId =
52
- explicitChatId ||
53
- (interaction.channelId
54
- ? currentState.channelChatMap?.[interaction.channelId]?.chatId || config.chatId
55
- : config.chatId);
56
- await trpc.sendMessage.mutate({
57
- type: 'send-message',
58
- client: 'cli',
59
- data: {
60
- message: `/approve ${policyId}`,
61
- chatId: targetChatId,
62
- adapter: 'discord',
63
- noWait: true,
64
- },
65
- });
66
- } catch (error) {
67
- console.error('Failed to send approve command to daemon:', error);
68
- await interaction.followUp({
69
- content: `Failed to approve policy ${policyId}.`,
70
- ephemeral: true,
71
- });
72
- }
100
+
101
+ const currentState = await readDiscordState();
102
+ const targetChatId =
103
+ explicitChatId ||
104
+ (interaction.channelId
105
+ ? currentState.channelChatMap?.[interaction.channelId]?.chatId || config.chatId
106
+ : config.chatId);
107
+
108
+ await processDiscordMessage(
109
+ `/approve ${policyId}`,
110
+ interaction.user,
111
+ interaction.channelId,
112
+ interaction.guild,
113
+ async (text) => {
114
+ await interaction.followUp({ content: text, ephemeral: true });
115
+ },
116
+ config,
117
+ trpc,
118
+ filteringConfig,
119
+ { explicitChatId: targetChatId, mentionsBot: true }
120
+ );
73
121
  } else if (
74
122
  interaction.customId.startsWith('reject_') ||
75
123
  interaction.customId.startsWith('reject|')
@@ -123,34 +171,30 @@ export async function handleDiscordInteraction(
123
171
  ephemeral: true,
124
172
  });
125
173
  } else {
126
- await interaction.reply({ content: `Rejecting policy ${policyId}...`, ephemeral: true });
174
+ await interaction.deferReply({ ephemeral: true });
175
+ await interaction.followUp({ content: `Rejecting policy ${policyId}...`, ephemeral: true });
127
176
  }
128
177
 
129
- try {
130
- const currentState = await readDiscordState();
131
- const targetChatId =
132
- explicitChatId ||
133
- (interaction.channelId
134
- ? currentState.channelChatMap?.[interaction.channelId]?.chatId || config.chatId
135
- : config.chatId);
136
-
137
- await trpc.sendMessage.mutate({
138
- type: 'send-message',
139
- client: 'cli',
140
- data: {
141
- message: command,
142
- chatId: targetChatId,
143
- adapter: 'discord',
144
- noWait: true,
145
- },
146
- });
147
- } catch (error) {
148
- console.error('Failed to send reject command to daemon:', error);
149
- await interaction.followUp({
150
- content: `Failed to reject policy ${policyId}.`,
151
- ephemeral: true,
152
- });
153
- }
178
+ const currentState = await readDiscordState();
179
+ const targetChatId =
180
+ explicitChatId ||
181
+ (interaction.channelId
182
+ ? currentState.channelChatMap?.[interaction.channelId]?.chatId || config.chatId
183
+ : config.chatId);
184
+
185
+ await processDiscordMessage(
186
+ command,
187
+ interaction.user,
188
+ interaction.channelId,
189
+ interaction.guild,
190
+ async (text) => {
191
+ await interaction.followUp({ content: text, ephemeral: true });
192
+ },
193
+ config,
194
+ trpc,
195
+ filteringConfig,
196
+ { explicitChatId: targetChatId, mentionsBot: true }
197
+ );
154
198
  }
155
199
  }
156
200
  }
@@ -0,0 +1,239 @@
1
+ import { readDiscordState, updateDiscordState } from './state.js';
2
+ import type { DiscordConfig } from './config.js';
3
+ import { recordInbound } from './inbound-cache.js';
4
+ import { handleAdapterCommand, type CommandTrpcClient } from '../shared/adapters/commands.js';
5
+ import { formatMessage, type FilteringConfig } from '../shared/adapters/filtering.js';
6
+ import { handleRoutingCommand, type RoutingTrpcClient } from '../shared/adapters/routing.js';
7
+ import { prependBlockquote } from '../shared/adapters/blockquote.js';
8
+ import { getClawminiDir } from '../shared/workspace.js';
9
+ import path from 'node:path';
10
+ import fs from 'node:fs/promises';
11
+
12
+ export type ProcessMessageOptions = {
13
+ mentionsBot?: boolean;
14
+ isReplyToBot?: boolean;
15
+ attachments?: { name: string; size: number; url: string }[];
16
+ referenceContent?: string;
17
+ referenceAuthor?: string;
18
+ explicitChatId?: string;
19
+ /**
20
+ * Discord message id of the inbound. Recorded in the in-memory inbound
21
+ * cache and sent to the daemon as `externalRef` so the forwarder can
22
+ * resolve the user's message and start a Discord thread anchored on it
23
+ * when `turnStarted` arrives.
24
+ */
25
+ messageId?: string;
26
+ };
27
+
28
+ export async function processDiscordMessage(
29
+ content: string,
30
+ author: { id: string; tag: string; bot?: boolean },
31
+ channelId: string | null,
32
+ guild: unknown | null,
33
+ reply: (text: string) => Promise<unknown>,
34
+ config: DiscordConfig,
35
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
36
+ trpc: any,
37
+ filteringConfig: FilteringConfig,
38
+ options: ProcessMessageOptions = {}
39
+ ) {
40
+ if (author.bot) return;
41
+
42
+ const externalContextId = channelId || 'default';
43
+ const currentState = await readDiscordState();
44
+ const mappedChatId =
45
+ options.explicitChatId || (channelId ? currentState.channelChatMap?.[channelId]?.chatId : null);
46
+ const isRoutingCommand = content.startsWith('/chat') || content.startsWith('/agent');
47
+
48
+ // Enforce requireMention config for guild messages
49
+ if (guild && channelId) {
50
+ const channelConfig = currentState.channelChatMap?.[channelId];
51
+ const requiresMention =
52
+ channelConfig?.requireMention !== undefined
53
+ ? channelConfig.requireMention
54
+ : config.requireMention;
55
+
56
+ if (requiresMention) {
57
+ if (!options.mentionsBot && !options.isReplyToBot) {
58
+ return;
59
+ }
60
+ }
61
+ }
62
+
63
+ function isAuthorized(userId: string, authorizedUserId: string): boolean {
64
+ return userId === authorizedUserId;
65
+ }
66
+
67
+ // Check if the user is authorized
68
+ if (!isAuthorized(author.id, config.authorizedUserId)) {
69
+ console.log(`Unauthorized message from ${author.tag} (${author.id}) ignored.`);
70
+ return;
71
+ }
72
+
73
+ console.log(`Received message from ${author.tag}: ${content}`);
74
+
75
+ if (isRoutingCommand) {
76
+ const stringChatMap = Object.fromEntries(
77
+ Object.entries(currentState.channelChatMap || {}).map(([k, v]) => [k, v.chatId || ''])
78
+ );
79
+ const routingResult = await handleRoutingCommand(
80
+ content,
81
+ externalContextId,
82
+ stringChatMap,
83
+ 'discord',
84
+ trpc as unknown as RoutingTrpcClient
85
+ );
86
+
87
+ if (routingResult) {
88
+ if (routingResult.type === 'mapped') {
89
+ await updateDiscordState((latestState) => ({
90
+ channelChatMap: {
91
+ ...(latestState.channelChatMap || {}),
92
+ [externalContextId]: {
93
+ ...(latestState.channelChatMap?.[externalContextId] || {}),
94
+ chatId: routingResult.newChatId,
95
+ },
96
+ },
97
+ }));
98
+ }
99
+ await reply(routingResult.text);
100
+ return;
101
+ }
102
+ }
103
+
104
+ let targetChatId = mappedChatId;
105
+
106
+ if (!targetChatId && !isRoutingCommand) {
107
+ const isFirstEverMessage =
108
+ !currentState.channelChatMap ||
109
+ Object.values(currentState.channelChatMap).every((entry) => !entry.chatId);
110
+
111
+ if (isFirstEverMessage) {
112
+ targetChatId = config.chatId || 'default';
113
+ console.log(
114
+ `First contact detected. Automatically mapping channel ${externalContextId} to chat ${targetChatId}.`
115
+ );
116
+ await updateDiscordState((latestState) => ({
117
+ channelChatMap: {
118
+ ...(latestState.channelChatMap || {}),
119
+ [externalContextId]: {
120
+ ...(latestState.channelChatMap?.[externalContextId] || {}),
121
+ chatId: targetChatId as string,
122
+ },
123
+ },
124
+ }));
125
+ } else {
126
+ const isDirectMessage = !guild;
127
+ const isSlashCommand = content.startsWith('/');
128
+ if (isDirectMessage || options.mentionsBot || isSlashCommand) {
129
+ console.log(`Unmapped channel ${externalContextId}, sending first contact warning.`);
130
+ await reply(
131
+ 'This channel/space is not currently mapped to a daemon chat. Please use `/chat [chat-id]` or `/agent [agent-id]` to map it.'
132
+ );
133
+ } else {
134
+ console.log(`Unmapped channel ${externalContextId}, silently ignoring background message.`);
135
+ }
136
+ return;
137
+ }
138
+ }
139
+
140
+ // Fallback typing safeguard
141
+ if (!targetChatId) targetChatId = config.chatId || 'default';
142
+
143
+ const commandResult = await handleAdapterCommand(
144
+ content,
145
+ filteringConfig,
146
+ trpc as unknown as CommandTrpcClient,
147
+ targetChatId
148
+ );
149
+
150
+ if (commandResult) {
151
+ if (commandResult.type === 'text') {
152
+ if (commandResult.newConfig) {
153
+ filteringConfig.filters = commandResult.newConfig.filters;
154
+ await updateDiscordState({ filters: filteringConfig.filters });
155
+ }
156
+ await reply(commandResult.text);
157
+ } else if (commandResult.type === 'debug') {
158
+ // Debug output echoes raw message content (which may include @everyone /
159
+ // @here as a substring). The `reply` lambda already strips mentions, so
160
+ // there's no extra escaping to do here.
161
+ const formatted =
162
+ commandResult.messages.length === 0
163
+ ? 'No ignored background messages found.'
164
+ : `**Debug Output (${commandResult.messages.length} ignored messages):**\n\n` +
165
+ commandResult.messages.map((msg) => formatMessage(msg)).join('\n\n---\n\n');
166
+ await reply(formatted);
167
+ }
168
+ return;
169
+ }
170
+
171
+ const downloadedFiles: string[] = [];
172
+ if (options.attachments && options.attachments.length > 0) {
173
+ const tmpDir = path.join(getClawminiDir(process.cwd()), 'tmp', 'discord');
174
+ await fs.mkdir(tmpDir, { recursive: true });
175
+ const maxSizeMB = config.maxAttachmentSizeMB ?? 25;
176
+ const maxSizeBytes = maxSizeMB * 1024 * 1024;
177
+
178
+ for (const attachment of options.attachments) {
179
+ if (attachment.size > maxSizeBytes) {
180
+ console.warn(
181
+ `Attachment ${attachment.name} exceeds size limit (${maxSizeMB}MB). Ignoring.`
182
+ );
183
+ await reply(
184
+ `Warning: Attachment ${attachment.name} exceeds the size limit of ${maxSizeMB}MB and was ignored.`
185
+ );
186
+ continue;
187
+ }
188
+
189
+ try {
190
+ const res = await fetch(attachment.url);
191
+ if (!res.ok) {
192
+ console.error(`Failed to download attachment ${attachment.name}`);
193
+ continue;
194
+ }
195
+
196
+ const uniqueName = `${Date.now()}-${attachment.name}`;
197
+ const filePath = path.join(tmpDir, uniqueName);
198
+ const arrayBuffer = await res.arrayBuffer();
199
+ await fs.writeFile(filePath, Buffer.from(arrayBuffer));
200
+ downloadedFiles.push(filePath);
201
+ } catch (err) {
202
+ console.error(`Error downloading attachment ${attachment.name}:`, err);
203
+ }
204
+ }
205
+ }
206
+
207
+ let finalContent = content;
208
+
209
+ if (options.referenceContent) {
210
+ finalContent = prependBlockquote(
211
+ options.referenceContent,
212
+ finalContent,
213
+ options.referenceAuthor
214
+ );
215
+ }
216
+
217
+ console.log(`Forwarding message to daemon: ${finalContent}`);
218
+ if (options.messageId && channelId) {
219
+ recordInbound({ messageId: options.messageId, channelId });
220
+ }
221
+ try {
222
+ await trpc.sendMessage.mutate({
223
+ type: 'send-message',
224
+ client: 'cli',
225
+ data: {
226
+ message: finalContent,
227
+ chatId: targetChatId,
228
+ files: downloadedFiles.length > 0 ? downloadedFiles : undefined,
229
+ adapter: 'discord',
230
+ noWait: true,
231
+ ...(options.messageId ? { externalRef: options.messageId } : {}),
232
+ },
233
+ });
234
+ console.log('Message forwarded to daemon successfully.');
235
+ } catch (error) {
236
+ console.error('Failed to forward message to daemon:', error);
237
+ await reply('Failed to forward message to the daemon. Please check the logs.');
238
+ }
239
+ }
@@ -11,6 +11,7 @@ export const DiscordStateSchema = z.object({
11
11
  z.object({
12
12
  chatId: z.string().nullable().optional(),
13
13
  requireMention: z.boolean().optional(),
14
+ threadsDisabled: z.boolean().optional(),
14
15
  })
15
16
  )
16
17
  .optional(),
@@ -71,17 +71,21 @@ describe('auth.ts', () => {
71
71
  const newTokens = { access_token: 'new_token' };
72
72
  await tokenCallback(newTokens);
73
73
 
74
- expect(state.updateGoogleChatState).toHaveBeenCalledWith({
75
- oauthTokens: {
76
- access_token: 'new_token',
74
+ expect(state.updateGoogleChatState).toHaveBeenCalledWith(
75
+ {
76
+ oauthTokens: {
77
+ access_token: 'new_token',
78
+ },
77
79
  },
78
- });
80
+ expect.any(String)
81
+ );
79
82
 
80
83
  expect(state.updateGoogleChatState).not.toHaveBeenCalledWith(
81
84
  expect.objectContaining({
82
85
  lastSyncedMessageIds: { default: '123' },
83
86
  activeSpaceName: 'Space1',
84
- })
87
+ }),
88
+ expect.any(String)
85
89
  );
86
90
  });
87
91
  });
@@ -13,12 +13,14 @@ export async function getAuthClient() {
13
13
  return authClient;
14
14
  }
15
15
 
16
- let userAuthClient: InstanceType<typeof google.auth.OAuth2> | null = null;
17
- let userAuthPromise: Promise<InstanceType<typeof google.auth.OAuth2>> | null = null;
16
+ const userAuthClients = new Map<string, InstanceType<typeof google.auth.OAuth2>>();
17
+ const userAuthPromises = new Map<string, Promise<InstanceType<typeof google.auth.OAuth2>>>();
18
18
 
19
- export async function getUserAuthClient(config: GoogleChatConfig) {
20
- if (userAuthClient) return userAuthClient;
21
- if (userAuthPromise) return userAuthPromise;
19
+ export async function getUserAuthClient(config: GoogleChatConfig, startDir = process.cwd()) {
20
+ const cached = userAuthClients.get(startDir);
21
+ if (cached) return cached;
22
+ const pending = userAuthPromises.get(startDir);
23
+ if (pending) return pending;
22
24
 
23
25
  if (!config.oauthClientId || !config.oauthClientSecret) {
24
26
  console.error('DEBUG config:', config);
@@ -27,7 +29,7 @@ export async function getUserAuthClient(config: GoogleChatConfig) {
27
29
  );
28
30
  }
29
31
 
30
- userAuthPromise = (async () => {
32
+ const promise = (async () => {
31
33
  const oauth2Client = new google.auth.OAuth2(
32
34
  config.oauthClientId,
33
35
  config.oauthClientSecret,
@@ -36,23 +38,26 @@ export async function getUserAuthClient(config: GoogleChatConfig) {
36
38
 
37
39
  oauth2Client.on('tokens', async (tokens) => {
38
40
  try {
39
- const currentState = await readGoogleChatState();
40
- await updateGoogleChatState({
41
- oauthTokens: {
42
- ...currentState.oauthTokens,
43
- ...tokens,
41
+ const currentState = await readGoogleChatState(startDir);
42
+ await updateGoogleChatState(
43
+ {
44
+ oauthTokens: {
45
+ ...currentState.oauthTokens,
46
+ ...tokens,
47
+ },
44
48
  },
45
- });
49
+ startDir
50
+ );
46
51
  } catch (err) {
47
52
  console.error('Failed to save refreshed Google User tokens', err);
48
53
  }
49
54
  });
50
55
 
51
- const state = await readGoogleChatState();
56
+ const state = await readGoogleChatState(startDir);
52
57
  if (state.oauthTokens) {
53
58
  oauth2Client.setCredentials(state.oauthTokens);
54
- userAuthClient = oauth2Client;
55
- userAuthPromise = null;
59
+ userAuthClients.set(startDir, oauth2Client);
60
+ userAuthPromises.delete(startDir);
56
61
  return oauth2Client;
57
62
  }
58
63
 
@@ -86,22 +91,22 @@ export async function getUserAuthClient(config: GoogleChatConfig) {
86
91
  const { tokens } = await oauth2Client.getToken(code);
87
92
  oauth2Client.setCredentials(tokens);
88
93
 
89
- await updateGoogleChatState({ oauthTokens: tokens });
94
+ await updateGoogleChatState({ oauthTokens: tokens }, startDir);
90
95
 
91
96
  console.log('Google User authorization successful!');
92
- userAuthClient = oauth2Client;
93
- userAuthPromise = null;
97
+ userAuthClients.set(startDir, oauth2Client);
98
+ userAuthPromises.delete(startDir);
94
99
  resolve(oauth2Client);
95
100
  } catch (err) {
96
101
  console.error('Failed to get token', err);
97
- userAuthPromise = null;
102
+ userAuthPromises.delete(startDir);
98
103
  reject(err);
99
104
  }
100
105
  } else {
101
106
  res.end('Authentication failed!');
102
107
  clearTimeout(timeoutId);
103
108
  server.close();
104
- userAuthPromise = null;
109
+ userAuthPromises.delete(startDir);
105
110
  reject(new Error('No code provided in OAuth callback'));
106
111
  }
107
112
  }
@@ -110,7 +115,7 @@ export async function getUserAuthClient(config: GoogleChatConfig) {
110
115
  server.on('error', (err) => {
111
116
  console.error('Failed to start local OAuth server on port 31338', err);
112
117
  clearTimeout(timeoutId);
113
- userAuthPromise = null;
118
+ userAuthPromises.delete(startDir);
114
119
  reject(err);
115
120
  });
116
121
 
@@ -118,7 +123,7 @@ export async function getUserAuthClient(config: GoogleChatConfig) {
118
123
  timeoutId = setTimeout(
119
124
  () => {
120
125
  server.close();
121
- userAuthPromise = null;
126
+ userAuthPromises.delete(startDir);
122
127
  console.error('Google User authorization timed out after 5 minutes.');
123
128
  reject(new Error('Google User authorization timed out.'));
124
129
  },
@@ -128,5 +133,6 @@ export async function getUserAuthClient(config: GoogleChatConfig) {
128
133
  });
129
134
  })();
130
135
 
131
- return userAuthPromise;
136
+ userAuthPromises.set(startDir, promise);
137
+ return promise;
132
138
  }
@@ -2,11 +2,14 @@ import { google } from 'googleapis';
2
2
  import { getAuthClient } from './auth.js';
3
3
  import type { RoutingTrpcClient } from '../shared/adapters/routing.js';
4
4
 
5
+ type ChatApiLike = ReturnType<typeof google.chat>;
6
+
5
7
  export async function handleCardClicked(
6
8
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
7
9
  event: any,
8
10
  targetChatId: string,
9
- trpc: RoutingTrpcClient
11
+ trpc: RoutingTrpcClient,
12
+ getChatApi?: () => Promise<ChatApiLike>
10
13
  ) {
11
14
  const action = event.action;
12
15
  if (!action) return;
@@ -22,7 +25,9 @@ export async function handleCardClicked(
22
25
 
23
26
  if (event.message?.name) {
24
27
  try {
25
- const chatApi = google.chat({ version: 'v1', auth: await getAuthClient() });
28
+ const chatApi = getChatApi
29
+ ? await getChatApi()
30
+ : google.chat({ version: 'v1', auth: await getAuthClient() });
26
31
 
27
32
  const originalCards = event.message.cardsV2 || [];
28
33
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
@@ -1,5 +1,5 @@
1
1
  import { describe, it, expect, vi, beforeEach } from 'vitest';
2
- import { getTRPCClient, startGoogleChatIngestion } from './client.js';
2
+ import { formatQuotedSender, getTRPCClient, startGoogleChatIngestion } from './client.js';
3
3
  import fs from 'node:fs';
4
4
  import fsPromises from 'node:fs/promises';
5
5
  import * as workspace from '../shared/workspace.js';
@@ -87,6 +87,41 @@ describe('Google Chat Adapter Client', () => {
87
87
  vi.clearAllMocks();
88
88
  });
89
89
 
90
+ describe('formatQuotedSender', () => {
91
+ const authorized = ['user@example.com', 'users/42'];
92
+
93
+ it('returns undefined when sender is missing', () => {
94
+ expect(formatQuotedSender(undefined, authorized)).toBeUndefined();
95
+ });
96
+
97
+ it('labels bots as "Assistant"', () => {
98
+ expect(formatQuotedSender({ type: 'BOT' }, authorized)).toBe('Assistant');
99
+ });
100
+
101
+ it('returns undefined for authorized users by email', () => {
102
+ expect(
103
+ formatQuotedSender({ email: 'user@example.com', type: 'HUMAN' }, authorized)
104
+ ).toBeUndefined();
105
+ });
106
+
107
+ it('returns undefined for authorized users by user resource name', () => {
108
+ expect(formatQuotedSender({ name: 'users/42', type: 'HUMAN' }, authorized)).toBeUndefined();
109
+ });
110
+
111
+ it('falls back to email for other people', () => {
112
+ expect(
113
+ formatQuotedSender(
114
+ { email: 'other@example.com', name: 'users/9', type: 'HUMAN' },
115
+ authorized
116
+ )
117
+ ).toBe('other@example.com');
118
+ });
119
+
120
+ it('falls back to user resource name when email is absent', () => {
121
+ expect(formatQuotedSender({ name: 'users/9', type: 'HUMAN' }, authorized)).toBe('users/9');
122
+ });
123
+ });
124
+
90
125
  describe('getTRPCClient', () => {
91
126
  it('should throw error if daemon socket does not exist', () => {
92
127
  vi.mocked(workspace.getSocketPath).mockReturnValue('/tmp/test.sock');
@@ -533,7 +568,7 @@ describe('Google Chat Adapter Client', () => {
533
568
 
534
569
  vi.mocked(google.chat({ version: 'v1' }).spaces.messages.list).mockResolvedValueOnce({
535
570
  data: { messages: [{ sender: { type: 'BOT' } }] },
536
- } as any);
571
+ } as never);
537
572
 
538
573
  const mockMsg = {
539
574
  data: Buffer.from(