clawmini 0.0.3 → 0.0.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +19 -0
- package/dist/adapter-discord/index.d.mts.map +1 -1
- package/dist/adapter-discord/index.mjs +398 -193
- package/dist/adapter-discord/index.mjs.map +1 -1
- package/dist/adapter-google-chat/index.d.mts +5 -0
- package/dist/adapter-google-chat/index.d.mts.map +1 -0
- package/dist/adapter-google-chat/index.mjs +1077 -0
- package/dist/adapter-google-chat/index.mjs.map +1 -0
- package/dist/cli/index.mjs +107 -14
- package/dist/cli/index.mjs.map +1 -1
- package/dist/cli/lite.mjs +175 -16
- package/dist/cli/lite.mjs.map +1 -1
- package/dist/cli/propose-policy.d.mts +1 -0
- package/dist/cli/propose-policy.mjs +7159 -0
- package/dist/cli/propose-policy.mjs.map +1 -0
- package/dist/daemon/index.d.mts.map +1 -1
- package/dist/daemon/index.mjs +1427 -513
- package/dist/daemon/index.mjs.map +1 -1
- package/dist/{lite-oSYSvaOr.mjs → lite-CBxOT1y5.mjs} +101 -24
- package/dist/lite-CBxOT1y5.mjs.map +1 -0
- package/dist/routing-D8rTxtaV.mjs +245 -0
- package/dist/routing-D8rTxtaV.mjs.map +1 -0
- package/dist/web/_app/immutable/assets/0.C-4eziNy.css +1 -0
- package/dist/web/_app/immutable/assets/4.Cc_xwLNl.css +1 -0
- package/dist/web/_app/immutable/chunks/B6YN0Nuq.js +1 -0
- package/dist/web/_app/immutable/chunks/{Dc-UOHw9.js → BmRlVmv6.js} +1 -1
- package/{web/.svelte-kit/output/client/_app/immutable/chunks/8YNcRyEk.js → dist/web/_app/immutable/chunks/C20lZMGz.js} +1 -1
- package/dist/web/_app/immutable/chunks/C9lbZ-kT.js +1 -0
- package/dist/web/_app/immutable/chunks/CK9JZLaG.js +2 -0
- package/dist/web/_app/immutable/chunks/CME08kGM.js +1 -0
- package/dist/web/_app/immutable/chunks/{BPy8HLo7.js → Ck-be5J2.js} +1 -1
- package/dist/web/_app/immutable/chunks/Ck3rYNON.js +1 -0
- package/dist/web/_app/immutable/chunks/DMtIqaiV.js +2 -0
- package/dist/web/_app/immutable/chunks/{B8yYFADm.js → DhD271EB.js} +1 -1
- package/dist/web/_app/immutable/chunks/{DcrmIfTj.js → DpuLqk8d.js} +1 -1
- package/dist/web/_app/immutable/chunks/{ZkLyk0mE.js → Drm9vgeP.js} +1 -1
- package/dist/web/_app/immutable/chunks/DsIToJCP.js +1 -0
- package/dist/web/_app/immutable/chunks/{CyNaE55B.js → Zeh-C-mx.js} +1 -1
- package/{web/.svelte-kit/output/client/_app/immutable/entry/app.DO5eYwVz.js → dist/web/_app/immutable/entry/app.BgB5VkRU.js} +2 -2
- package/dist/web/_app/immutable/entry/start.DuxJo6av.js +1 -0
- package/dist/web/_app/immutable/nodes/0.C9oFZP9h.js +1 -0
- package/dist/web/_app/immutable/nodes/1.BON2Wk6k.js +1 -0
- package/dist/web/_app/immutable/nodes/{2.CK3CLC0f.js → 2.BnwnD1Ki.js} +1 -1
- package/dist/web/_app/immutable/nodes/{3.ncP0xLO6.js → 3.CIs4tjjw.js} +1 -1
- package/dist/web/_app/immutable/nodes/4.DLarELN4.js +60 -0
- package/dist/web/_app/immutable/nodes/{5.BpJUN6QH.js → 5.CE_QKy_3.js} +1 -1
- package/dist/web/_app/version.json +1 -1
- package/dist/web/index.html +12 -12
- package/dist/{workspace-DjoNjhW0.mjs → workspace-BJmJBfKi.mjs} +103 -11
- package/dist/workspace-BJmJBfKi.mjs.map +1 -0
- package/docs/14_google_chat_adapter/development_log.md +40 -0
- package/docs/14_google_chat_adapter/notes.md +28 -0
- package/docs/14_google_chat_adapter/prd.md +35 -0
- package/docs/14_google_chat_adapter/questions.md +9 -0
- package/docs/14_google_chat_adapter/tickets.md +117 -0
- package/docs/15_sandbox_policies/tickets.md +33 -0
- package/docs/16_session_timeout/development_log.md +20 -0
- package/docs/16_session_timeout/notes.md +44 -0
- package/docs/16_session_timeout/prd.md +106 -0
- package/docs/16_session_timeout/questions.md +10 -0
- package/docs/16_session_timeout/tickets.md +64 -0
- package/docs/17_auto_approve_policy/development_log.md +29 -0
- package/docs/17_auto_approve_policy/notes.md +25 -0
- package/docs/17_auto_approve_policy/prd.md +34 -0
- package/docs/17_auto_approve_policy/questions.md +10 -0
- package/docs/17_auto_approve_policy/tickets.md +11 -0
- package/docs/18_clawmini_skills/development_log.md +36 -0
- package/docs/18_clawmini_skills/notes.md +8 -0
- package/docs/18_clawmini_skills/prd.md +45 -0
- package/docs/18_clawmini_skills/questions.md +10 -0
- package/docs/18_clawmini_skills/tickets.md +55 -0
- package/docs/19_subagents/development_log.md +69 -0
- package/docs/19_subagents/notes.md +18 -0
- package/docs/19_subagents/prd.md +156 -0
- package/docs/19_subagents/questions.md +13 -0
- package/docs/19_subagents/tickets.md +113 -0
- package/docs/20_chat_logs_cleanup/development_log.md +50 -0
- package/docs/20_chat_logs_cleanup/notes.md +43 -0
- package/docs/20_chat_logs_cleanup/prd.md +232 -0
- package/docs/20_chat_logs_cleanup/questions.md +2 -0
- package/docs/20_chat_logs_cleanup/tickets.md +98 -0
- package/docs/20_webui_markdown/development_log.md +36 -0
- package/docs/20_webui_markdown/notes.md +23 -0
- package/docs/20_webui_markdown/prd.md +49 -0
- package/docs/20_webui_markdown/questions.md +10 -0
- package/docs/20_webui_markdown/tickets.md +55 -0
- package/docs/21_adapter_filtering/development_log.md +29 -0
- package/docs/21_adapter_filtering/notes.md +25 -0
- package/docs/21_adapter_filtering/prd.md +44 -0
- package/docs/21_adapter_filtering/questions.md +12 -0
- package/docs/21_adapter_filtering/tickets.md +38 -0
- package/docs/21_built_in_routers/development_log.md +17 -0
- package/docs/21_built_in_routers/notes.md +27 -0
- package/docs/21_built_in_routers/prd.md +34 -0
- package/docs/21_built_in_routers/questions.md +4 -0
- package/docs/21_built_in_routers/tickets.md +25 -0
- package/docs/21_fancy_policies/development_log.md +38 -0
- package/docs/21_fancy_policies/notes.md +27 -0
- package/docs/21_fancy_policies/prd.md +58 -0
- package/docs/21_fancy_policies/questions.md +6 -0
- package/docs/21_fancy_policies/tickets.md +48 -0
- package/docs/22_adapter_multi_chat/development_log.md +76 -0
- package/docs/22_adapter_multi_chat/notes.md +42 -0
- package/docs/22_adapter_multi_chat/prd.md +76 -0
- package/docs/22_adapter_multi_chat/questions.md +16 -0
- package/docs/22_adapter_multi_chat/tickets.md +164 -0
- package/docs/23_custom_token_env/development_log.md +31 -0
- package/docs/23_custom_token_env/notes.md +16 -0
- package/docs/23_custom_token_env/prd.md +42 -0
- package/docs/23_custom_token_env/questions.md +8 -0
- package/docs/23_custom_token_env/tickets.md +54 -0
- package/docs/guides/discord_adapter_setup.md +15 -2
- package/docs/guides/google_chat_adapter_setup.md +145 -0
- package/napkin.md +5 -0
- package/package.json +7 -2
- package/src/adapter-discord/config.test.ts +27 -8
- package/src/adapter-discord/config.ts +6 -8
- package/src/adapter-discord/forwarder.test.ts +307 -114
- package/src/adapter-discord/forwarder.ts +260 -75
- package/src/adapter-discord/index.test.ts +278 -0
- package/src/adapter-discord/index.ts +160 -30
- package/src/adapter-discord/interactions.test.ts +96 -0
- package/src/adapter-discord/interactions.ts +156 -0
- package/src/adapter-discord/state.test.ts +9 -8
- package/src/adapter-discord/state.ts +51 -8
- package/src/adapter-google-chat/auth.test.ts +87 -0
- package/src/adapter-google-chat/auth.ts +132 -0
- package/src/adapter-google-chat/cards.ts +71 -0
- package/src/adapter-google-chat/client.test.ts +561 -0
- package/src/adapter-google-chat/client.ts +430 -0
- package/src/adapter-google-chat/config.test.ts +187 -0
- package/src/adapter-google-chat/config.ts +82 -0
- package/src/adapter-google-chat/cron.test.ts +143 -0
- package/src/adapter-google-chat/cron.ts +81 -0
- package/src/adapter-google-chat/forwarder.test.ts +537 -0
- package/src/adapter-google-chat/forwarder.ts +349 -0
- package/src/adapter-google-chat/index.test.ts +62 -0
- package/src/adapter-google-chat/index.ts +61 -0
- package/src/adapter-google-chat/state.test.ts +96 -0
- package/src/adapter-google-chat/state.ts +85 -0
- package/src/adapter-google-chat/subscriptions.ts +124 -0
- package/src/adapter-google-chat/upload.ts +88 -0
- package/src/adapter-google-chat/utils.test.ts +111 -0
- package/src/adapter-google-chat/utils.ts +133 -0
- package/src/cli/commands/init.ts +0 -7
- package/src/cli/commands/messages.ts +18 -3
- package/src/cli/commands/policies.ts +70 -0
- package/src/cli/commands/skills.ts +71 -0
- package/src/cli/commands/web-api/chats.ts +5 -1
- package/src/cli/e2e/basic.test.ts +1 -1
- package/src/cli/e2e/cron.test.ts +1 -1
- package/src/cli/e2e/daemon.test.ts +132 -4
- package/src/cli/e2e/export-lite-func.test.ts +54 -31
- package/src/cli/e2e/fallbacks.test.ts +8 -6
- package/src/cli/e2e/init.test.ts +7 -0
- package/src/cli/e2e/messages.test.ts +90 -55
- package/src/cli/e2e/propose-policy.test.ts +203 -0
- package/src/cli/e2e/requests.test.ts +15 -0
- package/src/cli/e2e/session-timeout.test.ts +192 -0
- package/src/cli/e2e/skills.test.ts +55 -0
- package/src/cli/e2e/slash-new.test.ts +93 -0
- package/src/cli/e2e/subagents.test.ts +106 -0
- package/src/cli/index.ts +4 -0
- package/src/cli/lite.ts +51 -11
- package/src/cli/propose-policy.ts +91 -0
- package/src/cli/subagent-commands.ts +215 -0
- package/src/daemon/agent/agent-context.ts +89 -0
- package/src/daemon/agent/agent-extractors.ts +68 -0
- package/src/daemon/agent/agent-runner.ts +153 -0
- package/src/daemon/agent/agent-session.ts +261 -0
- package/src/daemon/agent/chat-logger.test.ts +158 -0
- package/src/daemon/agent/chat-logger.ts +188 -0
- package/src/daemon/agent/task-scheduler.test.ts +202 -0
- package/src/daemon/agent/task-scheduler.ts +276 -0
- package/src/daemon/agent/types.ts +84 -0
- package/src/daemon/agent/utils.ts +7 -0
- package/src/daemon/api/agent-router.ts +166 -18
- package/src/daemon/api/index.test.ts +50 -18
- package/src/daemon/api/policy-request.test.ts +39 -2
- package/src/daemon/api/subagent-router.test.ts +108 -0
- package/src/daemon/api/subagent-router.ts +296 -0
- package/src/daemon/api/subagent-utils.test.ts +56 -0
- package/src/daemon/api/subagent-utils.ts +130 -0
- package/src/daemon/api/user-router.ts +30 -13
- package/src/daemon/auth.ts +1 -0
- package/src/daemon/chats.ts +6 -0
- package/src/daemon/cron.test.ts +66 -1
- package/src/daemon/cron.ts +35 -8
- package/src/daemon/index.ts +23 -0
- package/src/daemon/message-agent.test.ts +11 -25
- package/src/daemon/message-extraction.test.ts +10 -27
- package/src/daemon/message-fallbacks.test.ts +13 -35
- package/src/daemon/message-interruption.test.ts +70 -53
- package/src/daemon/message-jobs.test.ts +138 -0
- package/src/daemon/message-queue.test.ts +30 -43
- package/src/daemon/message-router.test.ts +12 -11
- package/src/daemon/message-session.test.ts +41 -28
- package/src/daemon/message-typing.test.ts +19 -6
- package/src/daemon/message.ts +103 -515
- package/src/daemon/policy-request-service.ts +8 -3
- package/src/daemon/policy-utils.ts +19 -1
- package/src/daemon/queue.ts +16 -0
- package/src/daemon/request-store.test.ts +4 -0
- package/src/daemon/routers/session-timeout.test.ts +122 -0
- package/src/daemon/routers/session-timeout.ts +71 -0
- package/src/daemon/routers/slash-new.ts +3 -1
- package/src/daemon/routers/slash-policies.test.ts +26 -13
- package/src/daemon/routers/slash-policies.ts +39 -29
- package/src/daemon/routers/types.ts +8 -0
- package/src/daemon/routers.ts +64 -2
- package/src/daemon/utils/spawn.ts +6 -8
- package/src/shared/adapters/commands.test.ts +155 -0
- package/src/shared/adapters/commands.ts +125 -0
- package/src/shared/adapters/filtering.test.ts +111 -0
- package/src/shared/adapters/filtering.ts +57 -0
- package/src/shared/adapters/routing.test.ts +144 -0
- package/src/shared/adapters/routing.ts +109 -0
- package/src/shared/agent-utils.ts +10 -0
- package/src/shared/chats.test.ts +145 -3
- package/src/shared/chats.ts +215 -18
- package/src/shared/config.ts +67 -15
- package/src/shared/lite.ts +22 -18
- package/src/shared/policies.ts +7 -0
- package/src/shared/workspace.test.ts +45 -1
- package/src/shared/workspace.ts +119 -6
- package/templates/debug/settings.json +5 -2
- package/templates/environments/cladding/env.json +2 -2
- package/templates/gemini/.gemini/hooks/check-subagents.mjs +23 -0
- package/templates/gemini/.gemini/hooks/clawmini-logging.sh +17 -0
- package/templates/gemini/.gemini/hooks/insert-pending.sh +9 -0
- package/templates/gemini/.gemini/settings.json +50 -0
- package/templates/gemini/settings.json +22 -8
- package/templates/gemini-claw/.gemini/base-system.md +100 -0
- package/templates/gemini-claw/.gemini/hooks/check-subagents.mjs +23 -0
- package/templates/gemini-claw/.gemini/hooks/clawmini-logging.sh +1 -1
- package/templates/gemini-claw/.gemini/settings.json +13 -0
- package/templates/gemini-claw/.gemini/subagent-system.md +7 -0
- package/templates/gemini-claw/.gemini/system.md +3 -99
- package/templates/gemini-claw/settings.json +27 -22
- package/templates/skills/clawmini-requests/SKILL.md +92 -0
- package/templates/skills/clawmini-subagents/SKILL.md +79 -0
- package/templates/skills/skill-creator/SKILL.md +60 -0
- package/tsdown.config.ts +10 -1
- package/web/.svelte-kit/generated/server/internal.js +2 -1
- package/web/.svelte-kit/non-ambient.d.ts +2 -0
- package/web/.svelte-kit/output/client/.vite/manifest.json +141 -138
- package/web/.svelte-kit/output/client/_app/immutable/assets/0.C-4eziNy.css +1 -0
- package/web/.svelte-kit/output/client/_app/immutable/assets/4.Cc_xwLNl.css +1 -0
- package/web/.svelte-kit/output/client/_app/immutable/chunks/B6YN0Nuq.js +1 -0
- package/web/.svelte-kit/output/client/_app/immutable/chunks/{Dc-UOHw9.js → BmRlVmv6.js} +1 -1
- package/{dist/web/_app/immutable/chunks/8YNcRyEk.js → web/.svelte-kit/output/client/_app/immutable/chunks/C20lZMGz.js} +1 -1
- package/web/.svelte-kit/output/client/_app/immutable/chunks/C9lbZ-kT.js +1 -0
- package/web/.svelte-kit/output/client/_app/immutable/chunks/CK9JZLaG.js +2 -0
- package/web/.svelte-kit/output/client/_app/immutable/chunks/CME08kGM.js +1 -0
- package/web/.svelte-kit/output/client/_app/immutable/chunks/{BPy8HLo7.js → Ck-be5J2.js} +1 -1
- package/web/.svelte-kit/output/client/_app/immutable/chunks/Ck3rYNON.js +1 -0
- package/web/.svelte-kit/output/client/_app/immutable/chunks/DMtIqaiV.js +2 -0
- package/web/.svelte-kit/output/client/_app/immutable/chunks/{B8yYFADm.js → DhD271EB.js} +1 -1
- package/web/.svelte-kit/output/client/_app/immutable/chunks/{DcrmIfTj.js → DpuLqk8d.js} +1 -1
- package/web/.svelte-kit/output/client/_app/immutable/chunks/{ZkLyk0mE.js → Drm9vgeP.js} +1 -1
- package/web/.svelte-kit/output/client/_app/immutable/chunks/DsIToJCP.js +1 -0
- package/web/.svelte-kit/output/client/_app/immutable/chunks/{CyNaE55B.js → Zeh-C-mx.js} +1 -1
- package/{dist/web/_app/immutable/entry/app.DO5eYwVz.js → web/.svelte-kit/output/client/_app/immutable/entry/app.BgB5VkRU.js} +2 -2
- package/web/.svelte-kit/output/client/_app/immutable/entry/start.DuxJo6av.js +1 -0
- package/web/.svelte-kit/output/client/_app/immutable/nodes/0.C9oFZP9h.js +1 -0
- package/web/.svelte-kit/output/client/_app/immutable/nodes/1.BON2Wk6k.js +1 -0
- package/web/.svelte-kit/output/client/_app/immutable/nodes/{2.CK3CLC0f.js → 2.BnwnD1Ki.js} +1 -1
- package/web/.svelte-kit/output/client/_app/immutable/nodes/{3.ncP0xLO6.js → 3.CIs4tjjw.js} +1 -1
- package/web/.svelte-kit/output/client/_app/immutable/nodes/4.DLarELN4.js +60 -0
- package/web/.svelte-kit/output/client/_app/immutable/nodes/{5.BpJUN6QH.js → 5.CE_QKy_3.js} +1 -1
- package/web/.svelte-kit/output/client/_app/version.json +1 -1
- package/web/.svelte-kit/output/server/.vite/manifest.json +12 -3
- package/web/.svelte-kit/output/server/_app/immutable/assets/_layout.C-4eziNy.css +1 -0
- package/web/.svelte-kit/output/server/_app/immutable/assets/_page.Cc_xwLNl.css +1 -0
- package/web/.svelte-kit/output/server/chunks/app-state.svelte.js +5 -0
- package/web/.svelte-kit/output/server/chunks/bot.js +4 -4
- package/web/.svelte-kit/output/server/chunks/client.js +2 -1
- package/web/.svelte-kit/output/server/chunks/exports.js +0 -1
- package/web/.svelte-kit/output/server/chunks/internal.js +2 -1
- package/web/.svelte-kit/output/server/chunks/root.js +482 -392
- package/web/.svelte-kit/output/server/entries/pages/_layout.svelte.js +57 -7
- package/web/.svelte-kit/output/server/entries/pages/chats/_id_/_page.svelte.js +234 -9
- package/web/.svelte-kit/output/server/index.js +82 -10
- package/web/.svelte-kit/output/server/manifest-full.js +1 -1
- package/web/.svelte-kit/output/server/manifest.js +1 -1
- package/web/.svelte-kit/output/server/nodes/0.js +2 -2
- package/web/.svelte-kit/output/server/nodes/1.js +1 -1
- package/web/.svelte-kit/output/server/nodes/2.js +1 -1
- package/web/.svelte-kit/output/server/nodes/3.js +1 -1
- package/web/.svelte-kit/output/server/nodes/4.js +2 -2
- package/web/.svelte-kit/output/server/nodes/5.js +1 -1
- package/web/.svelte-kit/types/src/routes/$types.d.ts +1 -2
- package/web/.svelte-kit/types/src/routes/agents/$types.d.ts +1 -2
- package/web/.svelte-kit/types/src/routes/chats/[id]/$types.d.ts +1 -2
- package/web/.svelte-kit/types/src/routes/chats/[id]/settings/$types.d.ts +1 -2
- package/web/package.json +8 -0
- package/web/src/lib/app-state.svelte.ts +5 -1
- package/web/src/lib/components/app/markdown-renderer.svelte +56 -0
- package/web/src/lib/components/app/markdown-renderer.svelte.spec.ts +44 -0
- package/web/src/lib/components/app/message-content.svelte +16 -0
- package/web/src/lib/types.ts +67 -3
- package/web/src/routes/+layout.svelte +31 -1
- package/web/src/routes/chats/[id]/+page.svelte +167 -18
- package/web/src/routes/chats/[id]/page.svelte.spec.ts +58 -7
- package/dist/lite-oSYSvaOr.mjs.map +0 -1
- package/dist/web/_app/immutable/assets/0.GI4C4dpV.css +0 -1
- package/dist/web/_app/immutable/chunks/B5abRDXp.js +0 -1
- package/dist/web/_app/immutable/chunks/Bi0jeV7Q.js +0 -1
- package/dist/web/_app/immutable/chunks/BmUXQ3wy.js +0 -2
- package/dist/web/_app/immutable/chunks/C3k55nDF.js +0 -1
- package/dist/web/_app/immutable/chunks/CpaGRn9L.js +0 -1
- package/dist/web/_app/immutable/chunks/DG5RZBw-.js +0 -2
- package/dist/web/_app/immutable/chunks/DQoygso7.js +0 -1
- package/dist/web/_app/immutable/entry/start.D48mVn1m.js +0 -1
- package/dist/web/_app/immutable/nodes/0.B-0CcADM.js +0 -1
- package/dist/web/_app/immutable/nodes/1.FixKgvRO.js +0 -1
- package/dist/web/_app/immutable/nodes/4.CQYJEgv8.js +0 -1
- package/dist/workspace-DjoNjhW0.mjs.map +0 -1
- package/src/daemon/message-verbosity.test.ts +0 -127
- package/web/.svelte-kit/output/client/_app/immutable/assets/0.GI4C4dpV.css +0 -1
- package/web/.svelte-kit/output/client/_app/immutable/chunks/B5abRDXp.js +0 -1
- package/web/.svelte-kit/output/client/_app/immutable/chunks/Bi0jeV7Q.js +0 -1
- package/web/.svelte-kit/output/client/_app/immutable/chunks/BmUXQ3wy.js +0 -2
- package/web/.svelte-kit/output/client/_app/immutable/chunks/C3k55nDF.js +0 -1
- package/web/.svelte-kit/output/client/_app/immutable/chunks/CpaGRn9L.js +0 -1
- package/web/.svelte-kit/output/client/_app/immutable/chunks/DG5RZBw-.js +0 -2
- package/web/.svelte-kit/output/client/_app/immutable/chunks/DQoygso7.js +0 -1
- package/web/.svelte-kit/output/client/_app/immutable/entry/start.D48mVn1m.js +0 -1
- package/web/.svelte-kit/output/client/_app/immutable/nodes/0.B-0CcADM.js +0 -1
- package/web/.svelte-kit/output/client/_app/immutable/nodes/1.FixKgvRO.js +0 -1
- package/web/.svelte-kit/output/client/_app/immutable/nodes/4.CQYJEgv8.js +0 -1
- package/web/.svelte-kit/output/server/_app/immutable/assets/_layout.GI4C4dpV.css +0 -1
- /package/templates/{gemini-claw/.gemini/skills → skills}/clawmini-jobs/SKILL.md +0 -0
|
@@ -0,0 +1,349 @@
|
|
|
1
|
+
/* eslint-disable max-lines */
|
|
2
|
+
import { google } from 'googleapis';
|
|
3
|
+
import { getAuthClient } from './auth.js';
|
|
4
|
+
import type { getTRPCClient } from './client.js';
|
|
5
|
+
import type { ChatMessage } from '../shared/chats.js';
|
|
6
|
+
import path from 'node:path';
|
|
7
|
+
import fs from 'node:fs';
|
|
8
|
+
import type { GoogleChatConfig } from './config.js';
|
|
9
|
+
import { readGoogleChatState, updateGoogleChatState, getGoogleChatStatePath } from './state.js';
|
|
10
|
+
import {
|
|
11
|
+
shouldDisplayMessage,
|
|
12
|
+
formatMessage,
|
|
13
|
+
type FilteringConfig,
|
|
14
|
+
} from '../shared/adapters/filtering.js';
|
|
15
|
+
import { buildPolicyCard, chunkString } from './utils.js';
|
|
16
|
+
import { uploadFilesToDrive } from './upload.js';
|
|
17
|
+
|
|
18
|
+
export async function startDaemonToGoogleChatForwarder(
|
|
19
|
+
trpc: ReturnType<typeof getTRPCClient>,
|
|
20
|
+
config: GoogleChatConfig,
|
|
21
|
+
filteringConfig: FilteringConfig,
|
|
22
|
+
signal?: AbortSignal
|
|
23
|
+
) {
|
|
24
|
+
const defaultChatId = config.chatId || 'default';
|
|
25
|
+
|
|
26
|
+
const activeSubscriptions = new Map<string, { unsubscribe: () => void }>();
|
|
27
|
+
let currentLastSyncedMessageIds = (await readGoogleChatState()).lastSyncedMessageIds || {};
|
|
28
|
+
|
|
29
|
+
const saveLastMessageId = async (chatId: string, id: string) => {
|
|
30
|
+
currentLastSyncedMessageIds = { ...currentLastSyncedMessageIds, [chatId]: id };
|
|
31
|
+
return updateGoogleChatState((state) => ({
|
|
32
|
+
lastSyncedMessageIds: {
|
|
33
|
+
...state.lastSyncedMessageIds,
|
|
34
|
+
...currentLastSyncedMessageIds,
|
|
35
|
+
},
|
|
36
|
+
}));
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
const startSubscriptionForChat = async (chatId: string) => {
|
|
40
|
+
if (activeSubscriptions.has(chatId)) return;
|
|
41
|
+
if (signal?.aborted) return;
|
|
42
|
+
|
|
43
|
+
let lastMessageId = currentLastSyncedMessageIds[chatId];
|
|
44
|
+
|
|
45
|
+
if (!lastMessageId) {
|
|
46
|
+
try {
|
|
47
|
+
const messages = await trpc.getMessages.query({ chatId, limit: 1 });
|
|
48
|
+
if (Array.isArray(messages) && messages.length > 0) {
|
|
49
|
+
const lastMsg = messages[messages.length - 1];
|
|
50
|
+
if (lastMsg) {
|
|
51
|
+
await saveLastMessageId(chatId, lastMsg.id);
|
|
52
|
+
lastMessageId = lastMsg.id;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
} catch (error) {
|
|
56
|
+
if (signal?.aborted) return;
|
|
57
|
+
console.error(`Failed to fetch initial messages from daemon for ${chatId}:`, error);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
console.log(
|
|
62
|
+
`Starting daemon-to-google-chat forwarder for chat ${chatId}, lastMessageId: ${lastMessageId}`
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
let retryDelay = 1000;
|
|
66
|
+
const maxRetryDelay = 30000;
|
|
67
|
+
|
|
68
|
+
let subscription: { unsubscribe: () => void } | null = null;
|
|
69
|
+
let messageQueue = Promise.resolve();
|
|
70
|
+
|
|
71
|
+
const connect = () => {
|
|
72
|
+
if (signal?.aborted || !activeSubscriptions.has(chatId)) {
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
subscription = trpc.waitForMessages.subscribe(
|
|
77
|
+
{ chatId, lastMessageId },
|
|
78
|
+
{
|
|
79
|
+
onData: (messages) => {
|
|
80
|
+
retryDelay = 1000;
|
|
81
|
+
|
|
82
|
+
if (!Array.isArray(messages) || messages.length === 0) {
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
messageQueue = messageQueue
|
|
87
|
+
.then(async () => {
|
|
88
|
+
for (const rawMessage of messages) {
|
|
89
|
+
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
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
await saveLastMessageId(chatId, message.id).catch(console.error);
|
|
238
|
+
lastMessageId = message.id;
|
|
239
|
+
}
|
|
240
|
+
})
|
|
241
|
+
.catch((error) => {
|
|
242
|
+
console.error('Message queue failed, forcing reconnect...', error);
|
|
243
|
+
subscription?.unsubscribe();
|
|
244
|
+
subscription = null;
|
|
245
|
+
if (signal?.aborted || !activeSubscriptions.has(chatId)) {
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
248
|
+
setTimeout(() => {
|
|
249
|
+
retryDelay = Math.min(retryDelay * 2, maxRetryDelay);
|
|
250
|
+
connect();
|
|
251
|
+
}, retryDelay);
|
|
252
|
+
});
|
|
253
|
+
},
|
|
254
|
+
onError: (error) => {
|
|
255
|
+
console.error(
|
|
256
|
+
`Error in daemon-to-google-chat forwarder subscription for ${chatId}. Retrying in ${retryDelay}ms.`,
|
|
257
|
+
error
|
|
258
|
+
);
|
|
259
|
+
subscription?.unsubscribe();
|
|
260
|
+
subscription = null;
|
|
261
|
+
|
|
262
|
+
if (signal?.aborted || !activeSubscriptions.has(chatId)) {
|
|
263
|
+
return;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
setTimeout(() => {
|
|
267
|
+
retryDelay = Math.min(retryDelay * 2, maxRetryDelay);
|
|
268
|
+
connect();
|
|
269
|
+
}, retryDelay);
|
|
270
|
+
},
|
|
271
|
+
onComplete: () => {
|
|
272
|
+
subscription = null;
|
|
273
|
+
if (!signal?.aborted && activeSubscriptions.has(chatId)) {
|
|
274
|
+
setTimeout(() => connect(), retryDelay);
|
|
275
|
+
}
|
|
276
|
+
},
|
|
277
|
+
}
|
|
278
|
+
);
|
|
279
|
+
};
|
|
280
|
+
|
|
281
|
+
activeSubscriptions.set(chatId, {
|
|
282
|
+
unsubscribe: () => subscription?.unsubscribe(),
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
connect();
|
|
286
|
+
};
|
|
287
|
+
|
|
288
|
+
const syncSubscriptions = async () => {
|
|
289
|
+
if (signal?.aborted) return;
|
|
290
|
+
const state = await readGoogleChatState();
|
|
291
|
+
|
|
292
|
+
// Update local copy of last message IDs
|
|
293
|
+
if (state.lastSyncedMessageIds) {
|
|
294
|
+
currentLastSyncedMessageIds = {
|
|
295
|
+
...state.lastSyncedMessageIds,
|
|
296
|
+
...currentLastSyncedMessageIds,
|
|
297
|
+
};
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
const targetChatIds = new Set<string>();
|
|
301
|
+
targetChatIds.add(defaultChatId);
|
|
302
|
+
|
|
303
|
+
if (state.channelChatMap) {
|
|
304
|
+
for (const mappedEntry of Object.values(state.channelChatMap)) {
|
|
305
|
+
if (mappedEntry.chatId) {
|
|
306
|
+
targetChatIds.add(mappedEntry.chatId);
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
for (const targetChatId of targetChatIds) {
|
|
312
|
+
if (!activeSubscriptions.has(targetChatId)) {
|
|
313
|
+
startSubscriptionForChat(targetChatId);
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
for (const [activeChatId, sub] of activeSubscriptions.entries()) {
|
|
318
|
+
if (!targetChatIds.has(activeChatId)) {
|
|
319
|
+
sub.unsubscribe();
|
|
320
|
+
activeSubscriptions.delete(activeChatId);
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
};
|
|
324
|
+
return new Promise<void>((resolve) => {
|
|
325
|
+
syncSubscriptions().catch(console.error);
|
|
326
|
+
|
|
327
|
+
const statePath = getGoogleChatStatePath();
|
|
328
|
+
const stateDir = path.dirname(statePath);
|
|
329
|
+
if (!fs.existsSync(stateDir)) {
|
|
330
|
+
fs.mkdirSync(stateDir, { recursive: true });
|
|
331
|
+
}
|
|
332
|
+
let debounceTimer: NodeJS.Timeout | null = null;
|
|
333
|
+
const watcher = fs.watch(stateDir, (eventType, filename) => {
|
|
334
|
+
if (filename === path.basename(statePath)) {
|
|
335
|
+
if (debounceTimer) clearTimeout(debounceTimer);
|
|
336
|
+
debounceTimer = setTimeout(() => {
|
|
337
|
+
syncSubscriptions().catch(console.error);
|
|
338
|
+
}, 200);
|
|
339
|
+
}
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
signal?.addEventListener('abort', () => {
|
|
343
|
+
if (debounceTimer) clearTimeout(debounceTimer);
|
|
344
|
+
watcher.close();
|
|
345
|
+
for (const sub of activeSubscriptions.values()) sub.unsubscribe();
|
|
346
|
+
resolve();
|
|
347
|
+
});
|
|
348
|
+
});
|
|
349
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
2
|
+
import { main } from './index.js';
|
|
3
|
+
import * as config from './config.js';
|
|
4
|
+
import * as client from './client.js';
|
|
5
|
+
import * as forwarder from './forwarder.js';
|
|
6
|
+
|
|
7
|
+
vi.mock('./config.js', () => ({
|
|
8
|
+
initGoogleChatConfig: vi.fn(),
|
|
9
|
+
readGoogleChatConfig: vi.fn(),
|
|
10
|
+
}));
|
|
11
|
+
|
|
12
|
+
vi.mock('./client.js', () => ({
|
|
13
|
+
getTRPCClient: vi.fn().mockReturnValue({}),
|
|
14
|
+
startGoogleChatIngestion: vi.fn(),
|
|
15
|
+
}));
|
|
16
|
+
|
|
17
|
+
vi.mock('./forwarder.js', () => ({
|
|
18
|
+
startDaemonToGoogleChatForwarder: vi.fn().mockResolvedValue(undefined),
|
|
19
|
+
}));
|
|
20
|
+
|
|
21
|
+
describe('Google Chat Adapter Entry Point', () => {
|
|
22
|
+
beforeEach(() => {
|
|
23
|
+
vi.clearAllMocks();
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it('should initialize config and exit if init argument is provided', async () => {
|
|
27
|
+
process.argv = ['node', 'index.js', 'init'];
|
|
28
|
+
await main();
|
|
29
|
+
expect(config.initGoogleChatConfig).toHaveBeenCalled();
|
|
30
|
+
expect(config.readGoogleChatConfig).not.toHaveBeenCalled();
|
|
31
|
+
process.argv = []; // reset
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it('should start ingestion and forwarder with valid config', async () => {
|
|
35
|
+
process.argv = ['node', 'index.js'];
|
|
36
|
+
const mockConfig = {
|
|
37
|
+
projectId: 'test-project',
|
|
38
|
+
subscriptionName: 'test-sub',
|
|
39
|
+
topicName: 'test-topic',
|
|
40
|
+
authorizedUsers: ['test@example.com'],
|
|
41
|
+
requireMention: false,
|
|
42
|
+
chatId: 'default',
|
|
43
|
+
};
|
|
44
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
45
|
+
vi.mocked(config.readGoogleChatConfig).mockResolvedValue(mockConfig as any);
|
|
46
|
+
|
|
47
|
+
await main();
|
|
48
|
+
|
|
49
|
+
expect(config.readGoogleChatConfig).toHaveBeenCalled();
|
|
50
|
+
expect(client.getTRPCClient).toHaveBeenCalled();
|
|
51
|
+
expect(client.startGoogleChatIngestion).toHaveBeenCalledWith(
|
|
52
|
+
mockConfig,
|
|
53
|
+
expect.any(Object),
|
|
54
|
+
{}
|
|
55
|
+
);
|
|
56
|
+
expect(forwarder.startDaemonToGoogleChatForwarder).toHaveBeenCalledWith(
|
|
57
|
+
expect.any(Object),
|
|
58
|
+
mockConfig,
|
|
59
|
+
{}
|
|
60
|
+
);
|
|
61
|
+
});
|
|
62
|
+
});
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { initGoogleChatConfig, readGoogleChatConfig } from './config.js';
|
|
4
|
+
import { readGoogleChatState } from './state.js';
|
|
5
|
+
import { getTRPCClient, startGoogleChatIngestion } from './client.js';
|
|
6
|
+
import { startDaemonToGoogleChatForwarder } from './forwarder.js';
|
|
7
|
+
import { getUserAuthClient } from './auth.js';
|
|
8
|
+
import { startSubscriptionRenewalCron } from './cron.js';
|
|
9
|
+
import type { FilteringConfig } from '../shared/adapters/filtering.js';
|
|
10
|
+
|
|
11
|
+
export async function main() {
|
|
12
|
+
const args = process.argv.slice(2);
|
|
13
|
+
|
|
14
|
+
if (args[0] === 'init') {
|
|
15
|
+
await initGoogleChatConfig();
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
console.log('Google Chat Adapter starting...');
|
|
20
|
+
|
|
21
|
+
const config = await readGoogleChatConfig();
|
|
22
|
+
if (!config) {
|
|
23
|
+
console.error(
|
|
24
|
+
'Failed to load Google Chat configuration. Please ensure .clawmini/adapters/google-chat/config.json exists and is valid.'
|
|
25
|
+
);
|
|
26
|
+
process.exit(1);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if (config.oauthClientId && config.oauthClientSecret) {
|
|
30
|
+
try {
|
|
31
|
+
console.log('Initializing Google User Authentication...');
|
|
32
|
+
await getUserAuthClient(config);
|
|
33
|
+
} catch (err) {
|
|
34
|
+
console.error('Failed to initialize Google User authentication:', err);
|
|
35
|
+
process.exit(1);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const trpc = getTRPCClient();
|
|
40
|
+
const state = await readGoogleChatState();
|
|
41
|
+
const filteringConfig: FilteringConfig = { filters: state.filters };
|
|
42
|
+
|
|
43
|
+
// Start ingestion from Pub/Sub
|
|
44
|
+
startGoogleChatIngestion(config, trpc, filteringConfig);
|
|
45
|
+
console.log(`Listening to Pub/Sub subscription: ${config.subscriptionName}`);
|
|
46
|
+
|
|
47
|
+
// Start forwarding from daemon to Google Chat API
|
|
48
|
+
startDaemonToGoogleChatForwarder(trpc, config, filteringConfig).catch((error) => {
|
|
49
|
+
console.error('Error in daemon-to-google-chat forwarder:', error);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
// Start background cron for renewing Space Subscriptions
|
|
53
|
+
startSubscriptionRenewalCron(config);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (process.env.NODE_ENV !== 'test') {
|
|
57
|
+
main().catch((error) => {
|
|
58
|
+
console.error('Unhandled error in Google Chat Adapter:', error);
|
|
59
|
+
process.exit(1);
|
|
60
|
+
});
|
|
61
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
2
|
+
import { updateGoogleChatState } from './state.js';
|
|
3
|
+
import fsPromises from 'node:fs/promises';
|
|
4
|
+
|
|
5
|
+
vi.mock('node:fs/promises', () => ({
|
|
6
|
+
default: {
|
|
7
|
+
readFile: vi.fn(),
|
|
8
|
+
writeFile: vi.fn(),
|
|
9
|
+
mkdir: vi.fn(),
|
|
10
|
+
},
|
|
11
|
+
}));
|
|
12
|
+
|
|
13
|
+
vi.mock('../shared/workspace.js', () => ({
|
|
14
|
+
getClawminiDir: () => '/mock/clawmini',
|
|
15
|
+
}));
|
|
16
|
+
|
|
17
|
+
describe('Google Chat State Updates', () => {
|
|
18
|
+
beforeEach(() => {
|
|
19
|
+
vi.clearAllMocks();
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it('should process concurrent state updates sequentially to prevent data loss', async () => {
|
|
23
|
+
// Create an initial state where lastSyncedMessageIds is 'A' and oauthTokens is 'Space1'
|
|
24
|
+
const initialState = { lastSyncedMessageId: 'A', oauthTokens: 'Space1' };
|
|
25
|
+
|
|
26
|
+
// We mock readFile to always return the LAST state that was written by writeFile.
|
|
27
|
+
// However, if the read/writes were not sequential (if they ran truly concurrently without a mutex),
|
|
28
|
+
// they might both read the initial state and then overwrite each other.
|
|
29
|
+
|
|
30
|
+
let currentMockStateJSON = JSON.stringify(initialState);
|
|
31
|
+
|
|
32
|
+
vi.mocked(fsPromises.readFile).mockImplementation(async () => {
|
|
33
|
+
// simulate delay to maximize chance of race condition if no mutex is used
|
|
34
|
+
await new Promise((resolve) => setTimeout(resolve, 5));
|
|
35
|
+
return currentMockStateJSON;
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
vi.mocked(fsPromises.writeFile).mockImplementation(async (_path, data) => {
|
|
39
|
+
await new Promise((resolve) => setTimeout(resolve, 5));
|
|
40
|
+
currentMockStateJSON = data as string;
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
// Fire two concurrent updates
|
|
44
|
+
const update1 = updateGoogleChatState({ lastSyncedMessageIds: { default: 'B' } });
|
|
45
|
+
const update2 = updateGoogleChatState({ oauthTokens: 'Space2' });
|
|
46
|
+
|
|
47
|
+
await Promise.all([update1, update2]);
|
|
48
|
+
|
|
49
|
+
// Read the final state
|
|
50
|
+
const finalState = JSON.parse(currentMockStateJSON);
|
|
51
|
+
|
|
52
|
+
// If a race condition occurred, finalState would likely be either:
|
|
53
|
+
// { lastSyncedMessageId: 'B', oauthTokens: 'Space1' } OR
|
|
54
|
+
// { lastSyncedMessageId: 'A', oauthTokens: 'Space2' }
|
|
55
|
+
// Because they are serialized, it should safely contain BOTH updates.
|
|
56
|
+
expect(finalState).toEqual({
|
|
57
|
+
lastSyncedMessageIds: { default: 'B' },
|
|
58
|
+
oauthTokens: 'Space2',
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('should process callback updates sequentially and receive latest state', async () => {
|
|
63
|
+
const initialState = { channelChatMap: { 'ext-1': { chatId: 'chat-1' } } };
|
|
64
|
+
let currentMockStateJSON = JSON.stringify(initialState);
|
|
65
|
+
|
|
66
|
+
vi.mocked(fsPromises.readFile).mockImplementation(async () => {
|
|
67
|
+
await new Promise((resolve) => setTimeout(resolve, 5));
|
|
68
|
+
return currentMockStateJSON;
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
vi.mocked(fsPromises.writeFile).mockImplementation(async (_path, data) => {
|
|
72
|
+
await new Promise((resolve) => setTimeout(resolve, 5));
|
|
73
|
+
currentMockStateJSON = data as string;
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
// Fire two concurrent callback updates
|
|
77
|
+
const update1 = updateGoogleChatState((latest) => ({
|
|
78
|
+
channelChatMap: { ...latest.channelChatMap, 'ext-2': { chatId: 'chat-2' } },
|
|
79
|
+
}));
|
|
80
|
+
const update2 = updateGoogleChatState((latest) => ({
|
|
81
|
+
channelChatMap: { ...latest.channelChatMap, 'ext-3': { chatId: 'chat-3' } },
|
|
82
|
+
}));
|
|
83
|
+
|
|
84
|
+
await Promise.all([update1, update2]);
|
|
85
|
+
|
|
86
|
+
const finalState = JSON.parse(currentMockStateJSON);
|
|
87
|
+
|
|
88
|
+
expect(finalState).toEqual({
|
|
89
|
+
channelChatMap: {
|
|
90
|
+
'ext-1': { chatId: 'chat-1' },
|
|
91
|
+
'ext-2': { chatId: 'chat-2' },
|
|
92
|
+
'ext-3': { chatId: 'chat-3' },
|
|
93
|
+
},
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
});
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import fsPromises from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { z } from 'zod';
|
|
4
|
+
import { getClawminiDir } from '../shared/workspace.js';
|
|
5
|
+
|
|
6
|
+
export const GoogleChatStateSchema = z.object({
|
|
7
|
+
lastSyncedMessageIds: z.record(z.string(), z.string()).optional(),
|
|
8
|
+
channelChatMap: z
|
|
9
|
+
.record(
|
|
10
|
+
z.string(),
|
|
11
|
+
z.object({
|
|
12
|
+
chatId: z.string().nullable().optional(),
|
|
13
|
+
subscriptionId: z.string().optional(),
|
|
14
|
+
expirationDate: z.string().optional(),
|
|
15
|
+
requireMention: z.boolean().optional(),
|
|
16
|
+
})
|
|
17
|
+
)
|
|
18
|
+
.optional(),
|
|
19
|
+
oauthTokens: z.any().optional(),
|
|
20
|
+
filters: z.record(z.string(), z.boolean()).optional(),
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
export type GoogleChatState = z.infer<typeof GoogleChatStateSchema>;
|
|
24
|
+
|
|
25
|
+
export function getGoogleChatStatePath(startDir = process.cwd()): string {
|
|
26
|
+
return path.join(getClawminiDir(startDir), 'adapters', 'google-chat', 'state.json');
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export async function readGoogleChatState(startDir = process.cwd()): Promise<GoogleChatState> {
|
|
30
|
+
const statePath = getGoogleChatStatePath(startDir);
|
|
31
|
+
try {
|
|
32
|
+
const data = await fsPromises.readFile(statePath, 'utf-8');
|
|
33
|
+
const parsed = JSON.parse(data);
|
|
34
|
+
|
|
35
|
+
// Migrate legacy state
|
|
36
|
+
if (parsed.lastSyncedMessageId && !parsed.lastSyncedMessageIds) {
|
|
37
|
+
parsed.lastSyncedMessageIds = { default: parsed.lastSyncedMessageId };
|
|
38
|
+
}
|
|
39
|
+
if (parsed.driveOauthTokens && !parsed.oauthTokens) {
|
|
40
|
+
parsed.oauthTokens = parsed.driveOauthTokens;
|
|
41
|
+
delete parsed.driveOauthTokens;
|
|
42
|
+
}
|
|
43
|
+
if (parsed.channelChatMap) {
|
|
44
|
+
for (const [key, value] of Object.entries(parsed.channelChatMap)) {
|
|
45
|
+
if (typeof value === 'string') {
|
|
46
|
+
parsed.channelChatMap[key] = { chatId: value };
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return GoogleChatStateSchema.parse(parsed);
|
|
52
|
+
} catch (err: unknown) {
|
|
53
|
+
if ((err as NodeJS.ErrnoException).code === 'ENOENT') {
|
|
54
|
+
return {
|
|
55
|
+
oauthTokens: undefined,
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
throw err;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
let stateUpdatePromise = Promise.resolve();
|
|
63
|
+
|
|
64
|
+
export function updateGoogleChatState(
|
|
65
|
+
updates: Partial<GoogleChatState> | ((state: GoogleChatState) => Partial<GoogleChatState>),
|
|
66
|
+
startDir = process.cwd()
|
|
67
|
+
): Promise<GoogleChatState> {
|
|
68
|
+
return new Promise((resolve, reject) => {
|
|
69
|
+
stateUpdatePromise = stateUpdatePromise.then(async () => {
|
|
70
|
+
try {
|
|
71
|
+
const currentState = await readGoogleChatState(startDir);
|
|
72
|
+
const resolvedUpdates = typeof updates === 'function' ? updates(currentState) : updates;
|
|
73
|
+
const newState = { ...currentState, ...resolvedUpdates };
|
|
74
|
+
const statePath = getGoogleChatStatePath(startDir);
|
|
75
|
+
const dir = path.dirname(statePath);
|
|
76
|
+
await fsPromises.mkdir(dir, { recursive: true });
|
|
77
|
+
await fsPromises.writeFile(statePath, JSON.stringify(newState, null, 2), 'utf-8');
|
|
78
|
+
resolve(newState);
|
|
79
|
+
} catch (err) {
|
|
80
|
+
console.error(`Failed to write Google Chat state:`, err);
|
|
81
|
+
reject(err);
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
}
|