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
@@ -1,39 +1,383 @@
1
1
  /* eslint-disable max-lines */
2
2
  import { google } from 'googleapis';
3
3
  import { getAuthClient } from './auth.js';
4
- import type { getTRPCClient } from './client.js';
4
+ import type { getTRPCClient, GoogleChatApi } from './client.js';
5
5
  import type { ChatMessage } from '../shared/chats.js';
6
6
  import path from 'node:path';
7
7
  import fs from 'node:fs';
8
8
  import type { GoogleChatConfig } from './config.js';
9
9
  import { readGoogleChatState, updateGoogleChatState, getGoogleChatStatePath } from './state.js';
10
+ import { resolveInbound } from './inbound-cache.js';
10
11
  import {
11
- shouldDisplayMessage,
12
+ routeMessage,
12
13
  formatMessage,
14
+ type Destination,
13
15
  type FilteringConfig,
14
16
  } from '../shared/adapters/filtering.js';
17
+ import { createTurnLogBuffer, type TurnLogBuffer } from '../shared/adapters/turn-log-buffer.js';
15
18
  import { buildPolicyCard, chunkString } from './utils.js';
16
19
  import { uploadFilesToDrive } from './upload.js';
17
20
 
21
+ /**
22
+ * Google Chat anchor handle: the space (`parent`) and the thread (`thread.name`)
23
+ * are both required by the spaces.messages.create API, so we carry them
24
+ * together as the buffer's opaque `TAnchor`.
25
+ */
26
+ interface GChatAnchor {
27
+ spaceName: string;
28
+ threadName: string;
29
+ }
30
+
31
+ export interface GoogleChatForwarderDeps {
32
+ /** Google Chat API client (defaults to `google.chat()` with ADC credentials). */
33
+ chatApi?: GoogleChatApi;
34
+ /** Root directory for resolving adapter state (defaults to `process.cwd()`). */
35
+ startDir?: string;
36
+ }
37
+
38
+ interface ThreadLogOptions {
39
+ maxToolPreview: number;
40
+ maxLogMessageChars: number;
41
+ editDebounceMs: number;
42
+ }
43
+
44
+ const DEFAULT_THREAD_LOG_OPTS: ThreadLogOptions = {
45
+ maxToolPreview: 400,
46
+ maxLogMessageChars: 3500,
47
+ editDebounceMs: 1000,
48
+ };
49
+
50
+ function resolveThreadLogOpts(config: GoogleChatConfig): ThreadLogOptions {
51
+ const v = config.visibility?.threadLog;
52
+ return {
53
+ maxToolPreview: v?.maxToolPreview ?? DEFAULT_THREAD_LOG_OPTS.maxToolPreview,
54
+ maxLogMessageChars: v?.maxLogMessageChars ?? DEFAULT_THREAD_LOG_OPTS.maxLogMessageChars,
55
+ editDebounceMs: v?.editDebounceMs ?? DEFAULT_THREAD_LOG_OPTS.editDebounceMs,
56
+ };
57
+ }
58
+
59
+ async function resolveSpaceForChat(
60
+ chatId: string,
61
+ startDir: string
62
+ ): Promise<{ spaceName: string; threadsDisabled: boolean } | null> {
63
+ const state = await readGoogleChatState(startDir);
64
+ const entry = Object.entries(state.channelChatMap || {}).find(([, v]) => v?.chatId === chatId);
65
+ if (!entry) return null;
66
+ const [spaceName, mapping] = entry;
67
+ return {
68
+ spaceName,
69
+ threadsDisabled: mapping.threadsDisabled === true,
70
+ };
71
+ }
72
+
18
73
  export async function startDaemonToGoogleChatForwarder(
19
74
  trpc: ReturnType<typeof getTRPCClient>,
20
75
  config: GoogleChatConfig,
21
76
  filteringConfig: FilteringConfig,
22
- signal?: AbortSignal
77
+ signal?: AbortSignal,
78
+ deps: GoogleChatForwarderDeps = {}
23
79
  ) {
24
80
  const defaultChatId = config.chatId || 'default';
81
+ const startDir = deps.startDir ?? process.cwd();
82
+ const threadLogOpts = resolveThreadLogOpts(config);
83
+ const threadsGloballyEnabled = config.visibility?.threads !== false;
84
+ const jobsMode: 'silent' | 'header' = config.visibility?.jobs ?? 'silent';
85
+
86
+ const getChatApi = async (): Promise<GoogleChatApi> => {
87
+ if (deps.chatApi) return deps.chatApi;
88
+ const authClient = await getAuthClient();
89
+ return google.chat({ version: 'v1', auth: authClient });
90
+ };
25
91
 
26
92
  const activeSubscriptions = new Map<string, { unsubscribe: () => void }>();
27
- let currentLastSyncedMessageIds = (await readGoogleChatState()).lastSyncedMessageIds || {};
93
+ /**
94
+ * When a turn has no inbound-user anchor (cron, subagent completion, any
95
+ * proactive turn), its first top-level post implicitly creates a GChat
96
+ * thread. Record `daemonMessageId -> gchatThreadName` here so a late
97
+ * `turnStarted` event can still resolve the anchor. LRU-bounded to keep
98
+ * memory predictable on long-running daemons.
99
+ */
100
+ const proactiveAnchors = new Map<string, string>();
101
+ const MAX_PROACTIVE_ANCHORS = 64;
102
+ const recordProactiveAnchor = (daemonMessageId: string, threadName: string) => {
103
+ while (proactiveAnchors.size >= MAX_PROACTIVE_ANCHORS) {
104
+ const oldest = proactiveAnchors.keys().next().value;
105
+ if (!oldest) break;
106
+ proactiveAnchors.delete(oldest);
107
+ }
108
+ proactiveAnchors.set(daemonMessageId, threadName);
109
+ };
110
+ let currentLastSyncedMessageIds =
111
+ (await readGoogleChatState(startDir)).lastSyncedMessageIds || {};
28
112
 
29
113
  const saveLastMessageId = async (chatId: string, id: string) => {
30
114
  currentLastSyncedMessageIds = { ...currentLastSyncedMessageIds, [chatId]: id };
31
- return updateGoogleChatState((state) => ({
32
- lastSyncedMessageIds: {
33
- ...state.lastSyncedMessageIds,
34
- ...currentLastSyncedMessageIds,
115
+ return updateGoogleChatState(
116
+ (state) => ({
117
+ lastSyncedMessageIds: {
118
+ ...state.lastSyncedMessageIds,
119
+ ...currentLastSyncedMessageIds,
120
+ },
121
+ }),
122
+ startDir
123
+ );
124
+ };
125
+
126
+ /**
127
+ * Post a top-level message (no `thread` field; GChat auto-creates a fresh
128
+ * thread). Returns the newly-created thread's `name` so callers can anchor
129
+ * subsequent threaded replies on it — used to thread activity under
130
+ * proactive turns (cron, subagent_update) that have no inbound user
131
+ * message to anchor on.
132
+ */
133
+ const postTopLevel = async (
134
+ spaceName: string,
135
+ text: string,
136
+ cardsV2?: ReturnType<typeof buildPolicyCard>
137
+ ): Promise<{ threadName: string | undefined }> => {
138
+ const chatApi = await getChatApi();
139
+ const extractThread = (res: unknown): string | undefined => {
140
+ const data =
141
+ (res as { data?: { thread?: { name?: string } } }).data ??
142
+ (res as { thread?: { name?: string } });
143
+ return data?.thread?.name ?? undefined;
144
+ };
145
+ if (cardsV2 && cardsV2.length > 0) {
146
+ const res = await chatApi.spaces.messages.create({
147
+ parent: spaceName,
148
+ requestBody: { text: text || '', cardsV2 },
149
+ });
150
+ return { threadName: extractThread(res) };
151
+ }
152
+ if (text.length > 4000) {
153
+ const chunks = chunkString(text, 4000);
154
+ let firstThread: string | undefined;
155
+ for (const chunk of chunks) {
156
+ const res = await chatApi.spaces.messages.create({
157
+ parent: spaceName,
158
+ requestBody: { text: chunk },
159
+ });
160
+ firstThread ??= extractThread(res);
161
+ }
162
+ return { threadName: firstThread };
163
+ }
164
+ const res = await chatApi.spaces.messages.create({
165
+ parent: spaceName,
166
+ requestBody: { text },
167
+ });
168
+ return { threadName: extractThread(res) };
169
+ };
170
+
171
+ const postThreaded = async (anchor: GChatAnchor, text: string): Promise<string | undefined> => {
172
+ const chatApi = await getChatApi();
173
+ const res = await chatApi.spaces.messages.create({
174
+ parent: anchor.spaceName,
175
+ requestBody: {
176
+ text: text || '',
177
+ thread: { name: anchor.threadName },
35
178
  },
36
- }));
179
+ messageReplyOption: 'REPLY_MESSAGE_FALLBACK_TO_NEW_THREAD',
180
+ });
181
+ const data = (res as { data?: { name?: string } }).data ?? (res as { name?: string });
182
+ return data?.name ?? undefined;
183
+ };
184
+
185
+ const editThreaded = async (
186
+ _anchor: GChatAnchor,
187
+ messageName: string,
188
+ text: string
189
+ ): Promise<void> => {
190
+ const chatApi = await getChatApi();
191
+ await chatApi.spaces.messages.update({
192
+ name: messageName,
193
+ updateMask: 'text',
194
+ requestBody: { text },
195
+ });
196
+ };
197
+
198
+ // GChat surfaces a missing message as HTTP 404 on update; treat that as the
199
+ // signal to open a fresh log message rather than retrying the edit.
200
+ const isMissingMessageError = (err: unknown): boolean => {
201
+ const status = (err as { code?: number; status?: number })?.code ?? 0;
202
+ return status === 404;
203
+ };
204
+
205
+ const turnLog: TurnLogBuffer<GChatAnchor> = createTurnLogBuffer<GChatAnchor>({
206
+ postThreaded,
207
+ editThreaded,
208
+ isMissingMessageError,
209
+ options: threadLogOpts,
210
+ threadsEnabled: threadsGloballyEnabled,
211
+ });
212
+
213
+ const handlePolicyCard = async (
214
+ message: Extract<ChatMessage, { role: 'policy' }>,
215
+ spaceName: string
216
+ ) => {
217
+ const cards = buildPolicyCard(message);
218
+ try {
219
+ await postTopLevel(spaceName, '', cards);
220
+ } catch (richError) {
221
+ console.warn(
222
+ 'Failed to send rich policy request to Google Chat, falling back to plain text:',
223
+ richError
224
+ );
225
+ const policyId = ('requestId' in message && message.requestId) || message.id;
226
+ const text = `Action Required: Policy Request\n\n${
227
+ message.content || 'A pending policy request requires your attention.'
228
+ }\n\nApprove: \`/approve ${policyId}\`\nReject: \`/reject ${policyId} <optional_rationale>\``;
229
+ await postTopLevel(spaceName, text);
230
+ }
231
+ };
232
+
233
+ const collapseDestination = (dest: Destination, turnId?: string): Destination => {
234
+ // Both the global `visibility.threads: false` kill switch and the
235
+ // per-space `threadsDisabled` flag mean "quiet bot": drop thread-log
236
+ // activity rather than promoting it top-level. Top-level spam is only
237
+ // opt-in via `filters` (e.g. `/show`), matching pre-threaded behavior.
238
+ if (dest.kind !== 'thread-log') return dest;
239
+ if (!threadsGloballyEnabled) return { kind: 'drop' };
240
+ if (turnId && turnLog.threadsDisabledFor(turnId)) return { kind: 'drop' };
241
+ return dest;
242
+ };
243
+
244
+ const handleMessageForChat = async (chatId: string, message: ChatMessage) => {
245
+ const routed = routeMessage(message, filteringConfig);
246
+
247
+ // Cron SystemMessages route to `drop` by default (silent mode: the
248
+ // activity log anchors on the agent's reply; nothing posts if it stays
249
+ // silent). In `header` mode, promote back to top-level and swap the
250
+ // prompt text for a terse `🕒 <jobId>` heartbeat.
251
+ const isCronHeader =
252
+ jobsMode === 'header' && message.role === 'system' && message.event === 'cron';
253
+
254
+ const effective = collapseDestination(routed, message.turnId);
255
+
256
+ if (!isCronHeader && effective.kind === 'drop') return;
257
+
258
+ const space = await resolveSpaceForChat(chatId, startDir);
259
+ if (!space) {
260
+ console.warn('No active Google Chat space to reply to. Ignoring message:', message.content);
261
+ return;
262
+ }
263
+
264
+ // Verbose-level legacy messages still drop silently.
265
+ if ('level' in message && (message as { level?: string }).level === 'verbose') return;
266
+
267
+ const hasContent = !!message.content?.trim();
268
+ const files = 'files' in message ? ((message as { files?: string[] }).files ?? []) : [];
269
+ const hasFiles = files.length > 0;
270
+
271
+ if (!isCronHeader && effective.kind === 'thread-log') {
272
+ if (!message.turnId) {
273
+ console.warn(`thread-log event for ${message.role} has no turnId — dropping.`);
274
+ return;
275
+ }
276
+ // No turn context: turnStarted may have been missed (adapter restart,
277
+ // subscription reconnect). Drop silently rather than flooding the space.
278
+ if (!turnLog.has(message.turnId)) return;
279
+ turnLog.append(message.turnId, message);
280
+ return;
281
+ }
282
+
283
+ // Top-level.
284
+ if (!isCronHeader && !hasContent && !hasFiles && message.role !== 'policy') return;
285
+
286
+ try {
287
+ if (message.role === 'policy' && message.status === 'pending') {
288
+ await handlePolicyCard(message, space.spaceName);
289
+ return;
290
+ }
291
+
292
+ let text: string;
293
+ if (isCronHeader) {
294
+ const cron = message as Extract<ChatMessage, { role: 'system' }>;
295
+ const label = cron.jobId ?? 'scheduled';
296
+ text = `🕒 ${label}`;
297
+ } else {
298
+ text = formatMessage(message) || '';
299
+ }
300
+
301
+ if (hasFiles) {
302
+ const fileNames = files.map((f: string) => path.basename(f)).join(', ');
303
+ if (
304
+ config.driveUploadEnabled !== false &&
305
+ config.oauthClientId &&
306
+ config.oauthClientSecret
307
+ ) {
308
+ text += `\n\n`;
309
+ try {
310
+ const uploadResults = await uploadFilesToDrive(files, config);
311
+ for (const result of uploadResults) {
312
+ text += `${result}\n`;
313
+ }
314
+ } catch (driveAuthErr) {
315
+ console.error('Drive API/Auth Failed, degrading to local files output:', driveAuthErr);
316
+ text += `*(Files generated: ${fileNames})*`;
317
+ }
318
+ } else {
319
+ text += `\n\n*(Files generated: ${fileNames})*`;
320
+ }
321
+ }
322
+
323
+ const { threadName: createdThread } = await postTopLevel(space.spaceName, text);
324
+
325
+ // If this post belongs to a turn that currently has no anchor, treat it
326
+ // as the implicit root: subsequent thread-log events for the same turn
327
+ // will be posted into the GChat thread that this top-level message just
328
+ // created. Covers cron-triggered and other proactive turns that have no
329
+ // inbound user message to anchor on.
330
+ if (createdThread && message.turnId) {
331
+ if (turnLog.has(message.turnId)) {
332
+ if (!turnLog.isAnchored(message.turnId)) {
333
+ turnLog.assignAnchor(message.turnId, {
334
+ spaceName: space.spaceName,
335
+ threadName: createdThread,
336
+ });
337
+ }
338
+ } else {
339
+ // turnStarted hasn't arrived yet — cache the mapping so it picks
340
+ // up the anchor when it does.
341
+ recordProactiveAnchor(message.id, createdThread);
342
+ }
343
+ }
344
+ } catch (err) {
345
+ console.error('Failed to send message to Google Chat:', err);
346
+ }
347
+ };
348
+
349
+ const handleTurnStarted = async (
350
+ chatId: string,
351
+ turnId: string,
352
+ rootMessageId: string,
353
+ externalRef?: string
354
+ ) => {
355
+ const space = await resolveSpaceForChat(chatId, startDir);
356
+ if (!space) {
357
+ console.warn(`turnStarted for chat ${chatId} with no mapped space.`);
358
+ return;
359
+ }
360
+
361
+ // The adapter sent `externalRef` as the GChat message.name of the inbound
362
+ // that triggered this turn, so we look up the thread anchor directly
363
+ // rather than guessing via FIFO pairing. Turns with no externalRef
364
+ // (proactive crons, CLI messages) fall back to the thread created by
365
+ // their first top-level post — recorded in `proactiveAnchors` — so
366
+ // subsequent activity can still anchor into the right thread.
367
+ const entry = externalRef ? resolveInbound(externalRef) : null;
368
+ const proactiveThread = entry ? undefined : proactiveAnchors.get(rootMessageId);
369
+ if (proactiveThread) proactiveAnchors.delete(rootMessageId);
370
+
371
+ const threadName = entry?.gchatThreadName ?? proactiveThread;
372
+ turnLog.start({
373
+ turnId,
374
+ threadsDisabled: space.threadsDisabled,
375
+ anchorThread: threadName ? { spaceName: space.spaceName, threadName } : undefined,
376
+ });
377
+ };
378
+
379
+ const handleTurnEnded = async (turnId: string) => {
380
+ await turnLog.end(turnId);
37
381
  };
38
382
 
39
383
  const startSubscriptionForChat = async (chatId: string) => {
@@ -66,185 +410,53 @@ export async function startDaemonToGoogleChatForwarder(
66
410
  const maxRetryDelay = 30000;
67
411
 
68
412
  let subscription: { unsubscribe: () => void } | null = null;
69
- let messageQueue = Promise.resolve();
413
+ let pending = Promise.resolve();
414
+
415
+ type StreamItem =
416
+ | { kind: 'message'; message: ChatMessage }
417
+ | {
418
+ kind: 'turn';
419
+ event:
420
+ | { type: 'started'; turnId: string; rootMessageId: string; externalRef?: string }
421
+ | { type: 'ended'; turnId: string; outcome: 'ok' | 'error' };
422
+ };
70
423
 
71
424
  const connect = () => {
72
- if (signal?.aborted || !activeSubscriptions.has(chatId)) {
73
- return;
74
- }
425
+ if (signal?.aborted || !activeSubscriptions.has(chatId)) return;
75
426
 
76
427
  subscription = trpc.waitForMessages.subscribe(
77
428
  { chatId, lastMessageId },
78
429
  {
79
- onData: (messages) => {
430
+ onData: (items) => {
80
431
  retryDelay = 1000;
432
+ if (!Array.isArray(items) || items.length === 0) return;
81
433
 
82
- if (!Array.isArray(messages) || messages.length === 0) {
83
- return;
84
- }
85
-
86
- messageQueue = messageQueue
434
+ pending = pending
87
435
  .then(async () => {
88
- for (const rawMessage of messages) {
436
+ for (const raw of items) {
89
437
  if (signal?.aborted || !activeSubscriptions.has(chatId)) break;
90
-
91
- const message = rawMessage as ChatMessage;
92
-
93
- const isDisplayed = shouldDisplayMessage(message, filteringConfig);
94
-
95
- if (isDisplayed) {
96
- const logMessage = message;
97
-
98
- const currentState = await readGoogleChatState();
99
- let activeSpaceName: string | undefined;
100
-
101
- if (!activeSpaceName && currentState.channelChatMap) {
102
- const entry = Object.entries(currentState.channelChatMap).find(
103
- ([_, mapChatId]) => mapChatId?.chatId === chatId
104
- );
105
- if (entry) {
106
- activeSpaceName = entry[0];
107
- }
108
- }
109
-
110
- // We no longer fallback to config.directMessageName. If it's not mapped, we'll drop it below.
111
-
112
- const isPolicyRequest =
113
- logMessage.role === 'policy' && logMessage.status === 'pending';
114
-
115
- if (isPolicyRequest) {
116
- if (!activeSpaceName) {
117
- console.warn(
118
- 'No active Google Chat space to reply to. Ignoring policy request:',
119
- logMessage.content
120
- );
121
- await saveLastMessageId(chatId, logMessage.id).catch(console.error);
122
- lastMessageId = logMessage.id;
123
- continue;
124
- }
125
-
126
- try {
127
- const client = await getAuthClient();
128
- const chatApi = google.chat({ version: 'v1', auth: client });
129
-
130
- try {
131
- await chatApi.spaces.messages.create({
132
- parent: activeSpaceName as string,
133
- requestBody: {
134
- text: '',
135
- cardsV2: buildPolicyCard(logMessage),
136
- },
137
- });
138
- } catch (richError) {
139
- console.warn(
140
- 'Failed to send rich policy request to Google Chat, falling back to plain text:',
141
- richError
142
- );
143
- const policyId =
144
- ('requestId' in logMessage && logMessage.requestId) || logMessage.id;
145
- await chatApi.spaces.messages.create({
146
- parent: activeSpaceName as string,
147
- requestBody: {
148
- text: `Action Required: Policy Request\n\n${logMessage.content || 'A pending policy request requires your attention.'}\n\nApprove: \`/approve ${policyId}\`\nReject: \`/reject ${policyId} <optional_rationale>\``,
149
- },
150
- });
151
- }
152
- } catch (error) {
153
- console.error('Failed to send policy request to Google Chat:', error);
154
- }
155
-
156
- await saveLastMessageId(chatId, logMessage.id).catch(console.error);
157
- lastMessageId = logMessage.id;
158
- continue;
159
- }
160
-
161
- const hasContent = !!logMessage.content?.trim();
162
- const files =
163
- 'files' in logMessage ? (logMessage.files as string[]) : undefined;
164
- const hasFiles = Array.isArray(files) && files.length > 0;
165
-
166
- if (
167
- ('level' in logMessage && logMessage.level === 'verbose') ||
168
- (!hasContent && !hasFiles)
169
- ) {
170
- await saveLastMessageId(chatId, logMessage.id).catch(console.error);
171
- lastMessageId = logMessage.id;
172
- continue;
173
- }
174
-
175
- if (!activeSpaceName) {
176
- console.warn(
177
- 'No active Google Chat space to reply to. Ignoring message:',
178
- logMessage.content
179
- );
180
- await saveLastMessageId(chatId, logMessage.id).catch(console.error);
181
- lastMessageId = logMessage.id;
182
- continue;
183
- }
184
-
185
- try {
186
- const client = await getAuthClient();
187
- const chatApi = google.chat({ version: 'v1', auth: client });
188
-
189
- let text = formatMessage(logMessage) || '';
190
-
191
- if (hasFiles && files) {
192
- const fileNames = files.map((f) => path.basename(f)).join(', ');
193
-
194
- if (
195
- config.driveUploadEnabled !== false &&
196
- config.oauthClientId &&
197
- config.oauthClientSecret
198
- ) {
199
- text += `\n\n`;
200
- try {
201
- const uploadResults = await uploadFilesToDrive(files, config);
202
- for (const result of uploadResults) {
203
- text += `${result}\n`;
204
- }
205
- } catch (driveAuthErr) {
206
- console.error(
207
- 'Drive API/Auth Failed, degrading to local files output:',
208
- driveAuthErr
209
- );
210
- text += `*(Files generated: ${fileNames})*`;
211
- }
212
- } else {
213
- text += `\n\n*(Files generated: ${fileNames})*`;
214
- }
215
- }
216
-
217
- if (text.length > 4000) {
218
- const chunks = chunkString(text, 4000);
219
- for (let i = 0; i < chunks.length; i++) {
220
- if (signal?.aborted || !activeSubscriptions.has(chatId)) break;
221
- await chatApi.spaces.messages.create({
222
- parent: activeSpaceName as string,
223
- requestBody: { text: chunks[i] as string },
224
- });
225
- }
226
- } else {
227
- await chatApi.spaces.messages.create({
228
- parent: activeSpaceName as string,
229
- requestBody: { text },
230
- });
231
- }
232
- } catch (error) {
233
- console.error('Failed to send message to Google Chat:', error);
234
- }
438
+ const item = raw as StreamItem;
439
+ if (item.kind === 'message') {
440
+ await handleMessageForChat(chatId, item.message);
441
+ await saveLastMessageId(chatId, item.message.id).catch(console.error);
442
+ lastMessageId = item.message.id;
443
+ } else if (item.event.type === 'started') {
444
+ await handleTurnStarted(
445
+ chatId,
446
+ item.event.turnId,
447
+ item.event.rootMessageId,
448
+ item.event.externalRef
449
+ );
450
+ } else {
451
+ await handleTurnEnded(item.event.turnId);
235
452
  }
236
-
237
- await saveLastMessageId(chatId, message.id).catch(console.error);
238
- lastMessageId = message.id;
239
453
  }
240
454
  })
241
455
  .catch((error) => {
242
- console.error('Message queue failed, forcing reconnect...', error);
456
+ console.error('Stream queue failed, forcing reconnect...', error);
243
457
  subscription?.unsubscribe();
244
458
  subscription = null;
245
- if (signal?.aborted || !activeSubscriptions.has(chatId)) {
246
- return;
247
- }
459
+ if (signal?.aborted || !activeSubscriptions.has(chatId)) return;
248
460
  setTimeout(() => {
249
461
  retryDelay = Math.min(retryDelay * 2, maxRetryDelay);
250
462
  connect();
@@ -258,11 +470,7 @@ export async function startDaemonToGoogleChatForwarder(
258
470
  );
259
471
  subscription?.unsubscribe();
260
472
  subscription = null;
261
-
262
- if (signal?.aborted || !activeSubscriptions.has(chatId)) {
263
- return;
264
- }
265
-
473
+ if (signal?.aborted || !activeSubscriptions.has(chatId)) return;
266
474
  setTimeout(() => {
267
475
  retryDelay = Math.min(retryDelay * 2, maxRetryDelay);
268
476
  connect();
@@ -279,7 +487,9 @@ export async function startDaemonToGoogleChatForwarder(
279
487
  };
280
488
 
281
489
  activeSubscriptions.set(chatId, {
282
- unsubscribe: () => subscription?.unsubscribe(),
490
+ unsubscribe: () => {
491
+ subscription?.unsubscribe();
492
+ },
283
493
  });
284
494
 
285
495
  connect();
@@ -287,9 +497,8 @@ export async function startDaemonToGoogleChatForwarder(
287
497
 
288
498
  const syncSubscriptions = async () => {
289
499
  if (signal?.aborted) return;
290
- const state = await readGoogleChatState();
500
+ const state = await readGoogleChatState(startDir);
291
501
 
292
- // Update local copy of last message IDs
293
502
  if (state.lastSyncedMessageIds) {
294
503
  currentLastSyncedMessageIds = {
295
504
  ...state.lastSyncedMessageIds,
@@ -302,9 +511,7 @@ export async function startDaemonToGoogleChatForwarder(
302
511
 
303
512
  if (state.channelChatMap) {
304
513
  for (const mappedEntry of Object.values(state.channelChatMap)) {
305
- if (mappedEntry.chatId) {
306
- targetChatIds.add(mappedEntry.chatId);
307
- }
514
+ if (mappedEntry.chatId) targetChatIds.add(mappedEntry.chatId);
308
515
  }
309
516
  }
310
517
 
@@ -321,16 +528,17 @@ export async function startDaemonToGoogleChatForwarder(
321
528
  }
322
529
  }
323
530
  };
531
+
324
532
  return new Promise<void>((resolve) => {
325
533
  syncSubscriptions().catch(console.error);
326
534
 
327
- const statePath = getGoogleChatStatePath();
535
+ const statePath = getGoogleChatStatePath(startDir);
328
536
  const stateDir = path.dirname(statePath);
329
537
  if (!fs.existsSync(stateDir)) {
330
538
  fs.mkdirSync(stateDir, { recursive: true });
331
539
  }
332
540
  let debounceTimer: NodeJS.Timeout | null = null;
333
- const watcher = fs.watch(stateDir, (eventType, filename) => {
541
+ const watcher = fs.watch(stateDir, (_eventType, filename) => {
334
542
  if (filename === path.basename(statePath)) {
335
543
  if (debounceTimer) clearTimeout(debounceTimer);
336
544
  debounceTimer = setTimeout(() => {
@@ -343,6 +551,7 @@ export async function startDaemonToGoogleChatForwarder(
343
551
  if (debounceTimer) clearTimeout(debounceTimer);
344
552
  watcher.close();
345
553
  for (const sub of activeSubscriptions.values()) sub.unsubscribe();
554
+ turnLog.shutdown();
346
555
  resolve();
347
556
  });
348
557
  });