clawmini 0.0.7 → 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.
- package/.changeset/README.md +8 -0
- package/.changeset/config.json +14 -0
- package/.github/workflows/release.yml +49 -0
- package/CHANGELOG.md +36 -0
- package/README.md +5 -4
- package/dist/adapter-discord/index.d.mts.map +1 -1
- package/dist/adapter-discord/index.mjs +465 -282
- package/dist/adapter-discord/index.mjs.map +1 -1
- package/dist/adapter-google-chat/index.mjs +367 -243
- package/dist/adapter-google-chat/index.mjs.map +1 -1
- package/dist/cli/index.mjs +684 -24
- package/dist/cli/index.mjs.map +1 -1
- package/dist/cli/lite.mjs +43 -13
- package/dist/cli/lite.mjs.map +1 -1
- package/dist/cli/{propose-policy.mjs → manage-policies.mjs} +270 -47
- package/dist/cli/manage-policies.mjs.map +1 -0
- package/dist/cli/run-host.d.mts +1 -0
- package/dist/cli/run-host.mjs +3090 -0
- package/dist/cli/run-host.mjs.map +1 -0
- package/dist/config-CPFQIGdG.mjs +57 -0
- package/dist/config-CPFQIGdG.mjs.map +1 -0
- package/dist/config-Dvl-Pov4.mjs +76 -0
- package/dist/config-Dvl-Pov4.mjs.map +1 -0
- package/dist/daemon/index.d.mts.map +1 -1
- package/dist/daemon/index.mjs +970 -332
- package/dist/daemon/index.mjs.map +1 -1
- package/dist/supervisor-actions-CiW56eLi.mjs +843 -0
- package/dist/supervisor-actions-CiW56eLi.mjs.map +1 -0
- package/dist/turn-log-buffer-DRgW53gl.mjs +767 -0
- package/dist/turn-log-buffer-DRgW53gl.mjs.map +1 -0
- package/dist/web/_app/immutable/chunks/{Drm9vgeP.js → 3AZlWB6U.js} +1 -1
- package/dist/web/_app/immutable/chunks/BhRSsUCh.js +2 -0
- package/dist/web/_app/immutable/chunks/BiLeM2i1.js +1 -0
- package/{web/.svelte-kit/output/client/_app/immutable/chunks/CME08kGM.js → dist/web/_app/immutable/chunks/BmBj85Ll.js} +1 -1
- package/dist/web/_app/immutable/chunks/BrERcKAH.js +1 -0
- package/dist/web/_app/immutable/chunks/Bv9252RM.js +1 -0
- package/dist/web/_app/immutable/chunks/CIXNBPKi.js +1 -0
- package/dist/web/_app/immutable/chunks/DISKL3GN.js +2 -0
- package/dist/web/_app/immutable/chunks/{Zeh-C-mx.js → DcpaLzmX.js} +1 -1
- package/dist/web/_app/immutable/chunks/DnQ3vS13.js +1 -0
- package/dist/web/_app/immutable/chunks/KsloHTKS.js +1 -0
- package/{web/.svelte-kit/output/client/_app/immutable/chunks/Ck-be5J2.js → dist/web/_app/immutable/chunks/RsHsUj-8.js} +2 -2
- package/dist/web/_app/immutable/chunks/{G_zz-Gou.js → wpfV79dV.js} +1 -1
- package/dist/web/_app/immutable/entry/app.CIw1Qj0n.js +2 -0
- package/dist/web/_app/immutable/entry/start.Di0-Jhte.js +1 -0
- package/dist/web/_app/immutable/nodes/{0.CYS8iApT.js → 0.DYyUA1au.js} +1 -1
- package/dist/web/_app/immutable/nodes/1.D-3QEMMZ.js +1 -0
- package/dist/web/_app/immutable/nodes/{2.BnwnD1Ki.js → 2.4olHnH7U.js} +1 -1
- package/{web/.svelte-kit/output/client/_app/immutable/nodes/3.Dr0ot9sV.js → dist/web/_app/immutable/nodes/3.4w0bE-m2.js} +3 -3
- package/dist/web/_app/immutable/nodes/4.CZvjhVHt.js +60 -0
- package/dist/web/_app/immutable/nodes/{5.BBGQ_i84.js → 5.DLbPVJY2.js} +1 -1
- package/dist/web/_app/version.json +1 -1
- package/dist/web/index.html +12 -12
- package/dist/workspace-oWmVh5mi.mjs +1001 -0
- package/dist/workspace-oWmVh5mi.mjs.map +1 -0
- package/docs/23_adapter_slash_autocomplete/development_log.md +19 -0
- package/docs/23_adapter_slash_autocomplete/notes.md +18 -0
- package/docs/23_adapter_slash_autocomplete/prd.md +46 -0
- package/docs/23_adapter_slash_autocomplete/questions.md +6 -0
- package/docs/23_adapter_slash_autocomplete/tickets.md +21 -0
- package/docs/24_subagent_job_policy_fixes/development_log.md +22 -0
- package/docs/24_subagent_job_policy_fixes/notes.md +28 -0
- package/docs/24_subagent_job_policy_fixes/prd.md +59 -0
- package/docs/24_subagent_job_policy_fixes/questions.md +3 -0
- package/docs/24_subagent_job_policy_fixes/tickets.md +49 -0
- package/docs/25_e2e_test_improvements/development_log.md +30 -0
- package/docs/25_e2e_test_improvements/notes.md +29 -0
- package/docs/25_e2e_test_improvements/prd.md +43 -0
- package/docs/25_e2e_test_improvements/questions.md +12 -0
- package/docs/25_e2e_test_improvements/tickets-2.md +22 -0
- package/docs/25_e2e_test_improvements/tickets.md +22 -0
- package/docs/25_policy_cwd/development_log.md +30 -0
- package/docs/25_policy_cwd/notes.md +28 -0
- package/docs/25_policy_cwd/prd.md +77 -0
- package/docs/25_policy_cwd/questions.md +6 -0
- package/docs/25_policy_cwd/tickets.md +77 -0
- package/docs/CLI_REFERENCE.md +3 -1
- package/docs/PHILOSOPHY.md +35 -0
- package/docs/adapter-visibility/SPEC.md +461 -0
- package/docs/adapter-visibility/SPEC_v2.md +202 -0
- package/docs/auto-update/SPEC.md +344 -0
- package/docs/backups/SPEC.md +296 -0
- package/docs/backups/clawmini.gitignore +69 -0
- package/docs/guides/assets/clawmini-avatar.png +0 -0
- package/docs/guides/backups.md +332 -0
- package/docs/guides/discord_adapter_setup.md +1 -1
- package/docs/guides/google_chat_adapter_setup.md +81 -0
- package/docs/unified-startup/SPEC.md +203 -0
- package/e2e/_helpers/test-environment.test.ts +49 -0
- package/e2e/_helpers/test-environment.ts +548 -0
- package/e2e/adapters/_google-chat-fixtures.ts +340 -0
- package/{src/cli/e2e → e2e/adapters}/adapter-discord.test.ts +22 -23
- package/e2e/adapters/adapter-google-chat-downtime.test.ts +157 -0
- package/e2e/adapters/adapter-google-chat-inbound.test.ts +697 -0
- package/e2e/adapters/adapter-google-chat-outbound.test.ts +297 -0
- package/e2e/adapters/adapter-google-chat-roundtrip.test.ts +56 -0
- package/e2e/adapters/adapter-google-chat-threads.test.ts +1078 -0
- package/e2e/agents/custom-api-env.test.ts +80 -0
- package/e2e/agents/export-lite-func.test.ts +104 -0
- package/e2e/agents/fallbacks.test.ts +124 -0
- package/e2e/agents/interrupt.test.ts +50 -0
- package/e2e/agents/no-reply-necessary.test.ts +57 -0
- package/e2e/agents/session-timeout-subagents.test.ts +76 -0
- package/e2e/agents/subagent-authorization.test.ts +246 -0
- package/e2e/agents/subagent-env.test.ts +49 -0
- package/e2e/agents/subagent-lifecycle.test.ts +782 -0
- package/e2e/agents/subagents-depth.test.ts +47 -0
- package/e2e/cli/agents.test.ts +176 -0
- package/e2e/cli/auto-update.test.ts +741 -0
- package/e2e/cli/basic.test.ts +44 -0
- package/{src/cli/e2e → e2e/cli}/export-lite.test.ts +16 -12
- package/e2e/cli/init-gitignore.test.ts +86 -0
- package/e2e/cli/init.test.ts +76 -0
- package/e2e/cli/messages.test.ts +363 -0
- package/e2e/cli/serve.test.ts +76 -0
- package/{src/cli/e2e → e2e/cli}/skills.test.ts +11 -10
- package/{src/cli/e2e → e2e/daemon}/daemon.test.ts +57 -195
- package/e2e/jobs/agent-jobs.test.ts +216 -0
- package/e2e/jobs/cron.test.ts +64 -0
- package/e2e/jobs/restart.test.ts +108 -0
- package/e2e/policies/approval-session.test.ts +69 -0
- package/e2e/policies/auto-create-policies-file.test.ts +35 -0
- package/e2e/policies/builtin-manage-policies.test.ts +184 -0
- package/e2e/policies/builtin-run-host.test.ts +180 -0
- package/e2e/policies/environment-policies.test.ts +177 -0
- package/e2e/policies/manage-policies.test.ts +566 -0
- package/e2e/policies/output-size.test.ts +98 -0
- package/e2e/policies/policies-context-cwd.test.ts +160 -0
- package/e2e/policies/relative-script-path.test.ts +60 -0
- package/e2e/policies/requests-show.test.ts +135 -0
- package/e2e/policies/requests.test.ts +208 -0
- package/e2e/policies/slash-policies.test.ts +308 -0
- package/e2e/policies/startup-cleanup.test.ts +48 -0
- package/e2e/routers/session-timeout.test.ts +106 -0
- package/e2e/routers/slash-model.test.ts +152 -0
- package/e2e/routers/slash-new.test.ts +50 -0
- package/e2e/routers/slash-restart-adapter.test.ts +96 -0
- package/e2e/routers/slash-restart.test.ts +114 -0
- package/e2e/routers/slash-shutdown.test.ts +55 -0
- package/e2e/routers/slash-stop.test.ts +232 -0
- package/e2e/routers/slash-upgrade.test.ts +88 -0
- package/{src/cli/e2e → e2e/sandbox}/environments.test.ts +14 -13
- package/eslint.config.js +6 -0
- package/napkin.md +1 -1
- package/package.json +8 -3
- package/src/adapter-discord/commands.test.ts +42 -0
- package/src/adapter-discord/commands.ts +33 -0
- package/src/adapter-discord/config.ts +12 -0
- package/src/adapter-discord/forwarder.test.ts +499 -21
- package/src/adapter-discord/forwarder.ts +343 -124
- package/src/adapter-discord/inbound-cache.test.ts +47 -0
- package/src/adapter-discord/inbound-cache.ts +37 -0
- package/src/adapter-discord/index.test.ts +67 -2
- package/src/adapter-discord/index.ts +84 -216
- package/src/adapter-discord/interactions.test.ts +54 -3
- package/src/adapter-discord/interactions.ts +97 -53
- package/src/adapter-discord/processMessage.ts +239 -0
- package/src/adapter-discord/state.ts +1 -0
- package/src/adapter-google-chat/auth.test.ts +9 -5
- package/src/adapter-google-chat/auth.ts +29 -23
- package/src/adapter-google-chat/cards.ts +7 -2
- package/src/adapter-google-chat/client.test.ts +37 -2
- package/src/adapter-google-chat/client.ts +138 -38
- package/src/adapter-google-chat/config.ts +19 -0
- package/src/adapter-google-chat/forwarder.test.ts +81 -56
- package/src/adapter-google-chat/forwarder.ts +394 -185
- package/src/adapter-google-chat/inbound-cache.test.ts +61 -0
- package/src/adapter-google-chat/inbound-cache.ts +36 -0
- package/src/adapter-google-chat/state.test.ts +1 -0
- package/src/adapter-google-chat/state.ts +9 -1
- package/src/adapter-google-chat/subscriptions.ts +8 -6
- package/src/cli/builtin-policies.ts +44 -0
- package/src/cli/commands/agents.ts +59 -5
- package/src/cli/commands/down.ts +54 -2
- package/src/cli/commands/environments.ts +8 -2
- package/src/cli/commands/init.ts +31 -0
- package/src/cli/commands/logs.ts +116 -0
- package/src/cli/commands/policies.ts +6 -4
- package/src/cli/commands/serve.test.ts +67 -0
- package/src/cli/commands/serve.ts +284 -0
- package/src/cli/commands/up.ts +122 -2
- package/src/cli/commands/web-api/agents.ts +3 -2
- package/src/cli/index.ts +4 -0
- package/src/cli/install-detection.test.ts +72 -0
- package/src/cli/install-detection.ts +48 -0
- package/src/cli/lite.ts +54 -22
- package/src/cli/manage-policies-utils.ts +104 -0
- package/src/cli/manage-policies.ts +291 -0
- package/src/cli/run-host.ts +45 -0
- package/src/cli/supervisor-actions.ts +267 -0
- package/src/cli/supervisor-control.test.ts +129 -0
- package/src/cli/supervisor-control.ts +155 -0
- package/src/cli/supervisor-pid.ts +68 -0
- package/src/cli/supervisor.ts +277 -0
- package/src/daemon/agent/agent-context.ts +11 -11
- package/src/daemon/agent/agent-session.ts +8 -1
- package/src/daemon/agent/chat-logger.test.ts +78 -9
- package/src/daemon/agent/chat-logger.ts +25 -5
- package/src/daemon/agent/turn-registry.test.ts +89 -0
- package/src/daemon/agent/turn-registry.ts +94 -0
- package/src/daemon/agent/types.ts +2 -0
- package/src/daemon/api/agent-policy-endpoints.ts +263 -0
- package/src/daemon/api/agent-router.ts +47 -126
- package/src/daemon/api/index.test.ts +1 -0
- package/src/daemon/api/policy-request.test.ts +7 -5
- package/src/daemon/api/router-utils.ts +6 -5
- package/src/daemon/api/subagent-router.ts +110 -74
- package/src/daemon/api/subagent-utils.test.ts +60 -0
- package/src/daemon/api/subagent-utils.ts +113 -87
- package/src/daemon/api/user-router.ts +34 -8
- package/src/daemon/auth.ts +1 -0
- package/src/daemon/cron.test.ts +62 -4
- package/src/daemon/cron.ts +42 -16
- package/src/daemon/events.ts +65 -0
- package/src/daemon/index.ts +24 -1
- package/src/daemon/message-interruption.test.ts +1 -0
- package/src/daemon/message-jobs.test.ts +1 -0
- package/src/daemon/message.ts +78 -14
- package/src/daemon/observation.test.ts +26 -18
- package/src/daemon/pending-replies.test.ts +112 -0
- package/src/daemon/pending-replies.ts +162 -0
- package/src/daemon/policy-request-service.ts +3 -1
- package/src/daemon/policy-utils.test.ts +66 -1
- package/src/daemon/policy-utils.ts +126 -1
- package/src/daemon/request-store.ts +31 -0
- package/src/daemon/routers/session-timeout.ts +4 -0
- package/src/daemon/routers/slash-model.test.ts +344 -0
- package/src/daemon/routers/slash-model.ts +207 -0
- package/src/daemon/routers/slash-policies.test.ts +38 -32
- package/src/daemon/routers/slash-policies.ts +84 -33
- package/src/daemon/routers/slash-restart.test.ts +69 -0
- package/src/daemon/routers/slash-restart.ts +36 -0
- package/src/daemon/routers/slash-shutdown.test.ts +50 -0
- package/src/daemon/routers/slash-shutdown.ts +28 -0
- package/src/daemon/routers/slash-upgrade.test.ts +116 -0
- package/src/daemon/routers/slash-upgrade.ts +76 -0
- package/src/daemon/routers/types.ts +7 -0
- package/src/daemon/routers.ts +16 -0
- package/src/shared/adapters/blockquote.test.ts +28 -0
- package/src/shared/adapters/blockquote.ts +20 -0
- package/src/shared/adapters/filtering.test.ts +224 -10
- package/src/shared/adapters/filtering.ts +95 -7
- package/src/shared/adapters/inbound-cache.test.ts +48 -0
- package/src/shared/adapters/inbound-cache.ts +54 -0
- package/src/shared/adapters/turn-log-buffer.ts +266 -0
- package/src/shared/adapters/turn-log.test.ts +389 -0
- package/src/shared/adapters/turn-log.ts +357 -0
- package/src/shared/agent-utils.ts +12 -5
- package/src/shared/chats.test.ts +4 -0
- package/src/shared/chats.ts +9 -0
- package/src/shared/config.ts +16 -1
- package/src/shared/lite.ts +76 -2
- package/src/shared/policies.ts +26 -0
- package/src/shared/template-manifest.ts +267 -0
- package/src/shared/utils/shell.ts +61 -0
- package/src/shared/version.ts +34 -0
- package/src/shared/workspace.test.ts +217 -0
- package/src/shared/workspace.ts +626 -48
- package/templates/environments/cladding/allowlist-domain.mjs +125 -0
- package/templates/environments/cladding/env.json +21 -1
- package/templates/environments/cladding/run-with-network.mjs +54 -0
- package/templates/environments/macos-proxy/allowlist-domain.mjs +95 -0
- package/templates/environments/macos-proxy/env.json +8 -1
- package/templates/environments/macos-proxy/proxy.mjs +42 -13
- package/templates/gemini/template.json +5 -0
- package/templates/gemini-claw/template.json +13 -0
- package/templates/skills/clawmini-requests/SKILL.md +69 -10
- package/templates/skills/run-host/SKILL.md +51 -0
- package/templates/skills/skill-creator/SKILL.md +4 -3
- package/templates/skills/skill-creator/scripts/validate.sh +52 -0
- package/tsdown.config.ts +10 -1
- package/vitest.config.ts +2 -2
- package/web/.svelte-kit/ambient.d.ts +292 -176
- package/web/.svelte-kit/generated/server/internal.js +1 -1
- package/web/.svelte-kit/output/client/.vite/manifest.json +127 -137
- package/web/.svelte-kit/output/client/_app/immutable/chunks/{Drm9vgeP.js → 3AZlWB6U.js} +1 -1
- package/web/.svelte-kit/output/client/_app/immutable/chunks/BhRSsUCh.js +2 -0
- package/web/.svelte-kit/output/client/_app/immutable/chunks/BiLeM2i1.js +1 -0
- package/{dist/web/_app/immutable/chunks/CME08kGM.js → web/.svelte-kit/output/client/_app/immutable/chunks/BmBj85Ll.js} +1 -1
- package/web/.svelte-kit/output/client/_app/immutable/chunks/BrERcKAH.js +1 -0
- package/web/.svelte-kit/output/client/_app/immutable/chunks/Bv9252RM.js +1 -0
- package/web/.svelte-kit/output/client/_app/immutable/chunks/CIXNBPKi.js +1 -0
- package/web/.svelte-kit/output/client/_app/immutable/chunks/DISKL3GN.js +2 -0
- package/web/.svelte-kit/output/client/_app/immutable/chunks/{Zeh-C-mx.js → DcpaLzmX.js} +1 -1
- package/web/.svelte-kit/output/client/_app/immutable/chunks/DnQ3vS13.js +1 -0
- package/web/.svelte-kit/output/client/_app/immutable/chunks/KsloHTKS.js +1 -0
- package/{dist/web/_app/immutable/chunks/Ck-be5J2.js → web/.svelte-kit/output/client/_app/immutable/chunks/RsHsUj-8.js} +2 -2
- package/web/.svelte-kit/output/client/_app/immutable/chunks/{G_zz-Gou.js → wpfV79dV.js} +1 -1
- package/web/.svelte-kit/output/client/_app/immutable/entry/app.CIw1Qj0n.js +2 -0
- package/web/.svelte-kit/output/client/_app/immutable/entry/start.Di0-Jhte.js +1 -0
- package/web/.svelte-kit/output/client/_app/immutable/nodes/{0.CYS8iApT.js → 0.DYyUA1au.js} +1 -1
- package/web/.svelte-kit/output/client/_app/immutable/nodes/1.D-3QEMMZ.js +1 -0
- package/web/.svelte-kit/output/client/_app/immutable/nodes/{2.BnwnD1Ki.js → 2.4olHnH7U.js} +1 -1
- package/{dist/web/_app/immutable/nodes/3.Dr0ot9sV.js → web/.svelte-kit/output/client/_app/immutable/nodes/3.4w0bE-m2.js} +3 -3
- package/web/.svelte-kit/output/client/_app/immutable/nodes/4.CZvjhVHt.js +60 -0
- package/web/.svelte-kit/output/client/_app/immutable/nodes/{5.BBGQ_i84.js → 5.DLbPVJY2.js} +1 -1
- package/web/.svelte-kit/output/client/_app/version.json +1 -1
- package/web/.svelte-kit/output/server/.vite/manifest.json +12 -10
- package/web/.svelte-kit/output/server/chunks/Icon.js +1 -1
- package/web/.svelte-kit/output/server/chunks/client.js +1 -1
- package/web/.svelte-kit/output/server/chunks/exports.js +1 -1
- package/web/.svelte-kit/output/server/chunks/index-server.js +2 -1
- package/web/.svelte-kit/output/server/chunks/internal.js +1 -1
- package/web/.svelte-kit/output/server/chunks/render-context.js +77 -0
- package/web/.svelte-kit/output/server/chunks/root.js +739 -788
- package/web/.svelte-kit/output/server/chunks/shared.js +234 -21
- package/web/.svelte-kit/output/server/index.js +126 -90
- 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 +1 -1
- 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 +1 -1
- package/web/.svelte-kit/output/server/nodes/5.js +1 -1
- package/web/.svelte-kit/output/server/remote-entry.js +245 -81
- package/web/.svelte-kit/tsconfig.json +4 -1
- package/dist/cli/propose-policy.mjs.map +0 -1
- package/dist/lite-CBxOT1y5.mjs +0 -241
- package/dist/lite-CBxOT1y5.mjs.map +0 -1
- package/dist/routing-D8rTxtaV.mjs +0 -245
- package/dist/routing-D8rTxtaV.mjs.map +0 -1
- package/dist/web/_app/immutable/chunks/B6YN0Nuq.js +0 -1
- package/dist/web/_app/immutable/chunks/BmRlVmv6.js +0 -1
- package/dist/web/_app/immutable/chunks/CK9JZLaG.js +0 -2
- package/dist/web/_app/immutable/chunks/Ck3rYNON.js +0 -1
- package/dist/web/_app/immutable/chunks/DMtIqaiV.js +0 -2
- package/dist/web/_app/immutable/chunks/DhD271EB.js +0 -1
- package/dist/web/_app/immutable/chunks/DpuLqk8d.js +0 -1
- package/dist/web/_app/immutable/chunks/DsIToJCP.js +0 -1
- package/dist/web/_app/immutable/chunks/bBmtyQMj.js +0 -1
- package/dist/web/_app/immutable/entry/app.CJmSwntr.js +0 -2
- package/dist/web/_app/immutable/entry/start.ZpUrT2ak.js +0 -1
- package/dist/web/_app/immutable/nodes/1.Bli0Hqzn.js +0 -1
- package/dist/web/_app/immutable/nodes/4.oBhvQhcA.js +0 -60
- package/dist/workspace-BJmJBfKi.mjs +0 -456
- package/dist/workspace-BJmJBfKi.mjs.map +0 -1
- package/src/cli/e2e/agents.test.ts +0 -140
- package/src/cli/e2e/basic.test.ts +0 -43
- package/src/cli/e2e/cron.test.ts +0 -132
- package/src/cli/e2e/export-lite-func.test.ts +0 -206
- package/src/cli/e2e/fallbacks.test.ts +0 -175
- package/src/cli/e2e/init.test.ts +0 -77
- package/src/cli/e2e/messages.test.ts +0 -332
- package/src/cli/e2e/propose-policy.test.ts +0 -203
- package/src/cli/e2e/requests.test.ts +0 -180
- package/src/cli/e2e/session-timeout.test.ts +0 -192
- package/src/cli/e2e/slash-new.test.ts +0 -93
- package/src/cli/e2e/subagents.test.ts +0 -106
- package/src/cli/e2e/utils.ts +0 -66
- package/src/cli/propose-policy.ts +0 -91
- package/web/.svelte-kit/output/client/_app/immutable/chunks/B6YN0Nuq.js +0 -1
- package/web/.svelte-kit/output/client/_app/immutable/chunks/BmRlVmv6.js +0 -1
- package/web/.svelte-kit/output/client/_app/immutable/chunks/CK9JZLaG.js +0 -2
- package/web/.svelte-kit/output/client/_app/immutable/chunks/Ck3rYNON.js +0 -1
- package/web/.svelte-kit/output/client/_app/immutable/chunks/DMtIqaiV.js +0 -2
- package/web/.svelte-kit/output/client/_app/immutable/chunks/DhD271EB.js +0 -1
- package/web/.svelte-kit/output/client/_app/immutable/chunks/DpuLqk8d.js +0 -1
- package/web/.svelte-kit/output/client/_app/immutable/chunks/DsIToJCP.js +0 -1
- package/web/.svelte-kit/output/client/_app/immutable/chunks/bBmtyQMj.js +0 -1
- package/web/.svelte-kit/output/client/_app/immutable/entry/app.CJmSwntr.js +0 -2
- package/web/.svelte-kit/output/client/_app/immutable/entry/start.ZpUrT2ak.js +0 -1
- package/web/.svelte-kit/output/client/_app/immutable/nodes/1.Bli0Hqzn.js +0 -1
- package/web/.svelte-kit/output/client/_app/immutable/nodes/4.oBhvQhcA.js +0 -60
- package/web/.svelte-kit/output/server/chunks/false.js +0 -4
- /package/dist/cli/{propose-policy.d.mts → manage-policies.d.mts} +0 -0
- /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
|
-
|
|
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
|
-
|
|
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(
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
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
|
|
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: (
|
|
430
|
+
onData: (items) => {
|
|
80
431
|
retryDelay = 1000;
|
|
432
|
+
if (!Array.isArray(items) || items.length === 0) return;
|
|
81
433
|
|
|
82
|
-
|
|
83
|
-
return;
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
messageQueue = messageQueue
|
|
434
|
+
pending = pending
|
|
87
435
|
.then(async () => {
|
|
88
|
-
for (const
|
|
436
|
+
for (const raw of items) {
|
|
89
437
|
if (signal?.aborted || !activeSubscriptions.has(chatId)) break;
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
if (
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
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('
|
|
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: () =>
|
|
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, (
|
|
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
|
});
|