@rubytech/create-sitedesk-code 0.1.518 → 0.1.520
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/dist/__tests__/edge-bind-template-wiring.test.js +37 -0
- package/dist/__tests__/samba-provision.test.js +103 -1
- package/dist/index.js +19 -4
- package/dist/samba-provision.js +72 -0
- package/package.json +1 -1
- package/payload/platform/docs/superpowers/plans/2026-07-28-task-2097-email-signature-set.md +65 -0
- package/payload/platform/docs/superpowers/specs/2026-07-28-task-2097-email-signature-set-design.md +155 -0
- package/payload/platform/plugins/admin/skills/platform-architecture/SKILL.md +69 -19
- package/payload/platform/plugins/admin/skills/whats-new/SKILL.md +12 -0
- package/payload/platform/plugins/cloudflare/bin/portal-assemble.mjs +187 -0
- package/payload/platform/plugins/cloudflare/bin/portal-enrol.mjs +46 -5
- package/payload/platform/plugins/cloudflare/mcp/__tests__/auth-route.test.ts +19 -7
- package/payload/platform/plugins/cloudflare/mcp/__tests__/authorize.test.ts +154 -38
- package/payload/platform/plugins/cloudflare/mcp/__tests__/file-routes.test.ts +100 -29
- package/payload/platform/plugins/cloudflare/mcp/__tests__/portal-assemble.test.ts +143 -0
- package/payload/platform/plugins/cloudflare/mcp/__tests__/portal-delete-withdraw.test.ts +12 -5
- package/payload/platform/plugins/cloudflare/mcp/__tests__/portal-directory-listing.test.ts +15 -3
- package/payload/platform/plugins/cloudflare/mcp/__tests__/portal-enrol.test.ts +105 -11
- package/payload/platform/plugins/cloudflare/mcp/__tests__/portal-indexed-download.test.ts +15 -2
- package/payload/platform/plugins/cloudflare/mcp/__tests__/portal-moved-download.test.ts +9 -5
- package/payload/platform/plugins/cloudflare/mcp/__tests__/portal-schema-shape.test.ts +47 -5
- package/payload/platform/plugins/cloudflare/mcp/__tests__/portal-session-grant.test.ts +26 -2
- package/payload/platform/plugins/cloudflare/mcp/__tests__/portal-target.test.ts +12 -4
- package/payload/platform/plugins/cloudflare/mcp/__tests__/portal-upload-multipart.test.ts +23 -15
- package/payload/platform/plugins/cloudflare/mcp/__tests__/portal-upload-target.test.ts +23 -15
- package/payload/platform/plugins/cloudflare/mcp/__tests__/portal-uploads-listing.test.ts +8 -0
- package/payload/platform/plugins/cloudflare/mcp/__tests__/skill-contract.test.ts +23 -6
- package/payload/platform/plugins/cloudflare/mcp/__tests__/upload-route.test.ts +36 -20
- package/payload/platform/plugins/cloudflare/skills/data-portal/SKILL.md +85 -22
- package/payload/platform/plugins/cloudflare/skills/data-portal/schema.sql +47 -7
- package/payload/platform/plugins/cloudflare/skills/data-portal/template/functions/api/_lib/authorize.ts +27 -18
- package/payload/platform/plugins/cloudflare/skills/data-portal/template/functions/api/_lib/ratelimit.ts +10 -3
- package/payload/platform/plugins/cloudflare/skills/data-portal/template/functions/api/_lib/session.ts +27 -11
- package/payload/platform/plugins/cloudflare/skills/data-portal/template/functions/api/_lib/target.ts +18 -9
- package/payload/platform/plugins/cloudflare/skills/data-portal/template/functions/api/_lib/types.ts +16 -0
- package/payload/platform/plugins/cloudflare/skills/data-portal/template/functions/api/auth.ts +16 -4
- package/payload/platform/plugins/cloudflare/skills/data-portal/template/functions/api/delete.ts +26 -11
- package/payload/platform/plugins/cloudflare/skills/data-portal/template/functions/api/download.ts +14 -10
- package/payload/platform/plugins/cloudflare/skills/data-portal/template/functions/api/files.ts +13 -9
- package/payload/platform/plugins/cloudflare/skills/data-portal/template/functions/api/upload.ts +42 -22
- package/payload/platform/plugins/cloudflare/skills/data-portal/template/wrangler.toml +17 -4
- package/payload/platform/plugins/docs/references/admin-ui.md +43 -5
- package/payload/platform/plugins/docs/references/samba.md +2 -0
- package/payload/platform/plugins/email/.claude-plugin/plugin.json +1 -1
- package/payload/platform/plugins/email/PLUGIN.md +20 -1
- package/payload/platform/plugins/email/mcp/dist/__tests__/email-draft-list-tool.test.d.ts +2 -0
- package/payload/platform/plugins/email/mcp/dist/__tests__/email-draft-list-tool.test.d.ts.map +1 -0
- package/payload/platform/plugins/email/mcp/dist/__tests__/email-draft-list-tool.test.js +142 -0
- package/payload/platform/plugins/email/mcp/dist/__tests__/email-draft-list-tool.test.js.map +1 -0
- package/payload/platform/plugins/email/mcp/dist/__tests__/email-draft-list.test.d.ts +2 -0
- package/payload/platform/plugins/email/mcp/dist/__tests__/email-draft-list.test.d.ts.map +1 -0
- package/payload/platform/plugins/email/mcp/dist/__tests__/email-draft-list.test.js +153 -0
- package/payload/platform/plugins/email/mcp/dist/__tests__/email-draft-list.test.js.map +1 -0
- package/payload/platform/plugins/email/mcp/dist/__tests__/email-signature-set.test.d.ts +2 -0
- package/payload/platform/plugins/email/mcp/dist/__tests__/email-signature-set.test.d.ts.map +1 -0
- package/payload/platform/plugins/email/mcp/dist/__tests__/email-signature-set.test.js +203 -0
- package/payload/platform/plugins/email/mcp/dist/__tests__/email-signature-set.test.js.map +1 -0
- package/payload/platform/plugins/email/mcp/dist/index.js +69 -0
- package/payload/platform/plugins/email/mcp/dist/index.js.map +1 -1
- package/payload/platform/plugins/email/mcp/dist/lib/compose.d.ts.map +1 -1
- package/payload/platform/plugins/email/mcp/dist/lib/compose.js +5 -7
- package/payload/platform/plugins/email/mcp/dist/lib/compose.js.map +1 -1
- package/payload/platform/plugins/email/mcp/dist/lib/imap.d.ts +42 -0
- package/payload/platform/plugins/email/mcp/dist/lib/imap.d.ts.map +1 -1
- package/payload/platform/plugins/email/mcp/dist/lib/imap.js +76 -0
- package/payload/platform/plugins/email/mcp/dist/lib/imap.js.map +1 -1
- package/payload/platform/plugins/email/mcp/dist/lib/signature.d.ts +38 -0
- package/payload/platform/plugins/email/mcp/dist/lib/signature.d.ts.map +1 -1
- package/payload/platform/plugins/email/mcp/dist/lib/signature.js +67 -2
- package/payload/platform/plugins/email/mcp/dist/lib/signature.js.map +1 -1
- package/payload/platform/plugins/email/mcp/dist/tools/email-draft-list.d.ts +10 -0
- package/payload/platform/plugins/email/mcp/dist/tools/email-draft-list.d.ts.map +1 -0
- package/payload/platform/plugins/email/mcp/dist/tools/email-draft-list.js +73 -0
- package/payload/platform/plugins/email/mcp/dist/tools/email-draft-list.js.map +1 -0
- package/payload/platform/plugins/email/mcp/dist/tools/email-signature-set.d.ts +18 -0
- package/payload/platform/plugins/email/mcp/dist/tools/email-signature-set.d.ts.map +1 -0
- package/payload/platform/plugins/email/mcp/dist/tools/email-signature-set.js +38 -0
- package/payload/platform/plugins/email/mcp/dist/tools/email-signature-set.js.map +1 -0
- package/payload/platform/plugins/email/references/email-reference.md +14 -2
- package/payload/platform/plugins/email/skills/email-composition/SKILL.md +10 -0
- package/payload/platform/plugins/email/skills/email-signature/SKILL.md +61 -0
- package/payload/platform/plugins/scheduling/PLUGIN.md +8 -0
- package/payload/platform/plugins/scheduling/mcp/dist/index.js +26 -15
- package/payload/platform/plugins/scheduling/mcp/dist/index.js.map +1 -1
- package/payload/platform/plugins/scheduling/mcp/dist/lib/__tests__/schedule-audit.test.d.ts +2 -0
- package/payload/platform/plugins/scheduling/mcp/dist/lib/__tests__/schedule-audit.test.d.ts.map +1 -0
- package/payload/platform/plugins/scheduling/mcp/dist/lib/__tests__/schedule-audit.test.js +48 -0
- package/payload/platform/plugins/scheduling/mcp/dist/lib/__tests__/schedule-audit.test.js.map +1 -0
- package/payload/platform/plugins/scheduling/mcp/dist/lib/schedule-audit.d.ts +24 -0
- package/payload/platform/plugins/scheduling/mcp/dist/lib/schedule-audit.d.ts.map +1 -0
- package/payload/platform/plugins/scheduling/mcp/dist/lib/schedule-audit.js +33 -0
- package/payload/platform/plugins/scheduling/mcp/dist/lib/schedule-audit.js.map +1 -0
- package/payload/platform/plugins/scheduling/mcp/dist/scripts/reconcile-bookings.js +22 -1
- package/payload/platform/plugins/scheduling/mcp/dist/scripts/reconcile-bookings.js.map +1 -1
- package/payload/platform/plugins/scheduling/mcp/dist/tools/__tests__/schedule-cancel-meeting.test.d.ts +2 -0
- package/payload/platform/plugins/scheduling/mcp/dist/tools/__tests__/schedule-cancel-meeting.test.d.ts.map +1 -0
- package/payload/platform/plugins/scheduling/mcp/dist/tools/__tests__/schedule-cancel-meeting.test.js +30 -0
- package/payload/platform/plugins/scheduling/mcp/dist/tools/__tests__/schedule-cancel-meeting.test.js.map +1 -0
- package/payload/platform/plugins/scheduling/mcp/dist/tools/__tests__/schedule-get-meeting.test.d.ts +2 -0
- package/payload/platform/plugins/scheduling/mcp/dist/tools/__tests__/schedule-get-meeting.test.d.ts.map +1 -0
- package/payload/platform/plugins/scheduling/mcp/dist/tools/__tests__/schedule-get-meeting.test.js +43 -0
- package/payload/platform/plugins/scheduling/mcp/dist/tools/__tests__/schedule-get-meeting.test.js.map +1 -0
- package/payload/platform/plugins/scheduling/mcp/dist/tools/__tests__/schedule-list.test.d.ts +2 -0
- package/payload/platform/plugins/scheduling/mcp/dist/tools/__tests__/schedule-list.test.d.ts.map +1 -0
- package/payload/platform/plugins/scheduling/mcp/dist/tools/__tests__/schedule-list.test.js +87 -0
- package/payload/platform/plugins/scheduling/mcp/dist/tools/__tests__/schedule-list.test.js.map +1 -0
- package/payload/platform/plugins/scheduling/mcp/dist/tools/__tests__/schedule-update-meeting.test.d.ts +2 -0
- package/payload/platform/plugins/scheduling/mcp/dist/tools/__tests__/schedule-update-meeting.test.d.ts.map +1 -0
- package/payload/platform/plugins/scheduling/mcp/dist/tools/__tests__/schedule-update-meeting.test.js +48 -0
- package/payload/platform/plugins/scheduling/mcp/dist/tools/__tests__/schedule-update-meeting.test.js.map +1 -0
- package/payload/platform/plugins/scheduling/mcp/dist/tools/schedule-cancel.d.ts.map +1 -1
- package/payload/platform/plugins/scheduling/mcp/dist/tools/schedule-cancel.js +14 -1
- package/payload/platform/plugins/scheduling/mcp/dist/tools/schedule-cancel.js.map +1 -1
- package/payload/platform/plugins/scheduling/mcp/dist/tools/schedule-get.d.ts +4 -1
- package/payload/platform/plugins/scheduling/mcp/dist/tools/schedule-get.d.ts.map +1 -1
- package/payload/platform/plugins/scheduling/mcp/dist/tools/schedule-get.js +39 -2
- package/payload/platform/plugins/scheduling/mcp/dist/tools/schedule-get.js.map +1 -1
- package/payload/platform/plugins/scheduling/mcp/dist/tools/schedule-list.d.ts +4 -1
- package/payload/platform/plugins/scheduling/mcp/dist/tools/schedule-list.d.ts.map +1 -1
- package/payload/platform/plugins/scheduling/mcp/dist/tools/schedule-list.js +50 -1
- package/payload/platform/plugins/scheduling/mcp/dist/tools/schedule-list.js.map +1 -1
- package/payload/platform/plugins/scheduling/mcp/dist/tools/schedule-update.d.ts.map +1 -1
- package/payload/platform/plugins/scheduling/mcp/dist/tools/schedule-update.js +43 -1
- package/payload/platform/plugins/scheduling/mcp/dist/tools/schedule-update.js.map +1 -1
- package/payload/platform/plugins/whatsapp/references/channels-whatsapp.md +7 -3
- package/payload/platform/scripts/check-no-esm-require.mjs +5 -3
- package/payload/platform/services/claude-session-manager/dist/canonical-tool-names.generated.d.ts.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/canonical-tool-names.generated.js +2 -0
- package/payload/platform/services/claude-session-manager/dist/canonical-tool-names.generated.js.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/webchat-channel-mcp.d.ts.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/webchat-channel-mcp.js +5 -3
- package/payload/platform/services/claude-session-manager/dist/webchat-channel-mcp.js.map +1 -1
- package/payload/platform/services/telegram-channel/dist/instructions.d.ts.map +1 -1
- package/payload/platform/services/telegram-channel/dist/instructions.js +13 -10
- package/payload/platform/services/telegram-channel/dist/instructions.js.map +1 -1
- package/payload/platform/services/webchat-channel/dist/instructions.d.ts +6 -2
- package/payload/platform/services/webchat-channel/dist/instructions.d.ts.map +1 -1
- package/payload/platform/services/webchat-channel/dist/instructions.js +66 -32
- package/payload/platform/services/webchat-channel/dist/instructions.js.map +1 -1
- package/payload/platform/services/webchat-channel/dist/notification.d.ts +2 -1
- package/payload/platform/services/webchat-channel/dist/notification.d.ts.map +1 -1
- package/payload/platform/services/webchat-channel/dist/notification.js +11 -4
- package/payload/platform/services/webchat-channel/dist/notification.js.map +1 -1
- package/payload/platform/services/webchat-channel/dist/server.d.ts.map +1 -1
- package/payload/platform/services/webchat-channel/dist/server.js +30 -14
- package/payload/platform/services/webchat-channel/dist/server.js.map +1 -1
- package/payload/platform/services/webchat-channel/dist/turn-follow.d.ts.map +1 -1
- package/payload/platform/services/webchat-channel/dist/turn-follow.js +11 -5
- package/payload/platform/services/webchat-channel/dist/turn-follow.js.map +1 -1
- package/payload/platform/services/whatsapp-channel/dist/notification.d.ts +6 -3
- package/payload/platform/services/whatsapp-channel/dist/notification.d.ts.map +1 -1
- package/payload/platform/services/whatsapp-channel/dist/notification.js +9 -5
- package/payload/platform/services/whatsapp-channel/dist/notification.js.map +1 -1
- package/payload/platform/services/whatsapp-channel/dist/server.d.ts.map +1 -1
- package/payload/platform/services/whatsapp-channel/dist/server.js +6 -1
- package/payload/platform/services/whatsapp-channel/dist/server.js.map +1 -1
- package/payload/platform/services/whatsapp-channel/dist/targets.d.ts.map +1 -1
- package/payload/platform/services/whatsapp-channel/dist/targets.js +8 -2
- package/payload/platform/services/whatsapp-channel/dist/targets.js.map +1 -1
- package/payload/platform/templates/specialists/agents/personal-assistant.md +1 -1
- package/payload/platform/templates/systemd/edge.service.template +1 -1
- package/payload/server/chunk-PXNY2TAP.js +1980 -0
- package/payload/server/chunk-TIIWMF5G.js +4837 -0
- package/payload/server/{chunk-S6HYTIU3.js → chunk-UX2FRNO7.js} +72 -2031
- package/payload/server/manager-B6OFOXSW.js +61 -0
- package/payload/server/maxy-edge.js +5 -3
- package/payload/server/package.json +2 -1
- package/payload/server/public/activity.html +5 -5
- package/payload/server/public/agents.html +4 -4
- package/payload/server/public/assets/AdminLoginScreens-SW7mEMcP.js +1 -0
- package/payload/server/public/assets/AdminLoginScreens-SW7mEMcP.js.br +0 -0
- package/payload/server/public/assets/AdminLoginScreens-SW7mEMcP.js.gz +0 -0
- package/payload/server/public/assets/AdminShell-Djg6ndne.js +2 -0
- package/payload/server/public/assets/AdminShell-Djg6ndne.js.br +0 -0
- package/payload/server/public/assets/AdminShell-Djg6ndne.js.gz +0 -0
- package/payload/server/public/assets/{activity-BE9gv5xL.js → activity-sqbyX0pX.js} +1 -1
- package/payload/server/public/assets/activity-sqbyX0pX.js.br +0 -0
- package/payload/server/public/assets/activity-sqbyX0pX.js.gz +0 -0
- package/payload/server/public/assets/admin-D9Bt47GY.js +1 -0
- package/payload/server/public/assets/admin-D9Bt47GY.js.br +0 -0
- package/payload/server/public/assets/admin-D9Bt47GY.js.gz +0 -0
- package/payload/server/public/assets/{agents-B-x0Ai2v.js → agents-BFYM5EDA.js} +1 -1
- package/payload/server/public/assets/agents-BFYM5EDA.js.br +0 -0
- package/payload/server/public/assets/agents-BFYM5EDA.js.gz +0 -0
- package/payload/server/public/assets/{browser-ZEQUkPfs.js → browser-DP5GZe4G.js} +1 -1
- package/payload/server/public/assets/browser-DP5GZe4G.js.br +0 -0
- package/payload/server/public/assets/browser-DP5GZe4G.js.gz +0 -0
- package/payload/server/public/assets/calendar-BKaemLDG.js +1 -0
- package/payload/server/public/assets/calendar-BKaemLDG.js.br +0 -0
- package/payload/server/public/assets/calendar-BKaemLDG.js.gz +0 -0
- package/payload/server/public/assets/chat-DPhT6urD.js +1 -0
- package/payload/server/public/assets/chat-DPhT6urD.js.br +1 -0
- package/payload/server/public/assets/chat-DPhT6urD.js.gz +0 -0
- package/payload/server/public/assets/chevron-left-BSdXvJcH.js +1 -0
- package/payload/server/public/assets/chevron-right-DI7vI3IB.js +1 -0
- package/payload/server/public/assets/chevron-right-DI7vI3IB.js.br +0 -0
- package/payload/server/public/assets/clock-YUTPXZH5.js +1 -0
- package/payload/server/public/assets/clock-YUTPXZH5.js.br +0 -0
- package/payload/server/public/assets/clock-YUTPXZH5.js.gz +0 -0
- package/payload/server/public/assets/data-CfjttTvR.js +1 -0
- package/payload/server/public/assets/data-CfjttTvR.js.br +2 -0
- package/payload/server/public/assets/data-CfjttTvR.js.gz +0 -0
- package/payload/server/public/assets/{file-text-BTV6EdBv.js → file-text-BKK7R-k_.js} +1 -1
- package/payload/server/public/assets/file-text-BKK7R-k_.js.br +0 -0
- package/payload/server/public/assets/file-text-BKK7R-k_.js.gz +0 -0
- package/payload/server/public/assets/{graph-hD_gPbbL.js → graph-FuxYySjK.js} +3 -3
- package/payload/server/public/assets/graph-FuxYySjK.js.br +0 -0
- package/payload/server/public/assets/graph-FuxYySjK.js.gz +0 -0
- package/payload/server/public/assets/{graph-labels-CY1T19PY.js → graph-labels-Cip6fmKK.js} +1 -1
- package/payload/server/public/assets/graph-labels-Cip6fmKK.js.br +0 -0
- package/payload/server/public/assets/graph-labels-Cip6fmKK.js.gz +0 -0
- package/payload/server/public/assets/{maximize-2-BFV9VKHO.js → maximize-2-D79AhVDo.js} +1 -1
- package/payload/server/public/assets/maximize-2-D79AhVDo.js.br +0 -0
- package/payload/server/public/assets/maximize-2-D79AhVDo.js.gz +0 -0
- package/payload/server/public/assets/operator-BPAfgFhP.js +1 -0
- package/payload/server/public/assets/operator-BPAfgFhP.js.br +0 -0
- package/payload/server/public/assets/operator-BPAfgFhP.js.gz +0 -0
- package/payload/server/public/assets/page--78CbdFs.js +32 -0
- package/payload/server/public/assets/page--78CbdFs.js.br +0 -0
- package/payload/server/public/assets/page--78CbdFs.js.gz +0 -0
- package/payload/server/public/assets/page-BqEyS0W4.js +1 -0
- package/payload/server/public/assets/page-BqEyS0W4.js.br +0 -0
- package/payload/server/public/assets/page-BqEyS0W4.js.gz +0 -0
- package/payload/server/public/assets/{public-D6Tnj5LU.js → public-D1xs3FzD.js} +1 -1
- package/payload/server/public/assets/public-D1xs3FzD.js.br +0 -0
- package/payload/server/public/assets/public-D1xs3FzD.js.gz +0 -0
- package/payload/server/public/assets/{rotate-ccw-C-RfQUza.js → rotate-ccw-CcU_XD8B.js} +1 -1
- package/payload/server/public/assets/rotate-ccw-CcU_XD8B.js.br +0 -0
- package/payload/server/public/assets/rotate-ccw-CcU_XD8B.js.gz +0 -0
- package/payload/server/public/assets/{routines-NIMElUzV.js → routines-CA7D0myr.js} +1 -1
- package/payload/server/public/assets/routines-CA7D0myr.js.br +0 -0
- package/payload/server/public/assets/routines-CA7D0myr.js.gz +0 -0
- package/payload/server/public/assets/{skills-N0ZlHosx.js → skills-CfVPPSxO.js} +1 -1
- package/payload/server/public/assets/skills-CfVPPSxO.js.br +0 -0
- package/payload/server/public/assets/skills-CfVPPSxO.js.gz +0 -0
- package/payload/server/public/assets/tasks-DgieyZqz.js +1 -0
- package/payload/server/public/assets/tasks-DgieyZqz.js.br +0 -0
- package/payload/server/public/assets/tasks-DgieyZqz.js.gz +0 -0
- package/payload/server/public/assets/{time-entry-format-jQxlRPoI.js → time-entry-format-IFqinQJj.js} +1 -1
- package/payload/server/public/assets/time-entry-format-IFqinQJj.js.br +0 -0
- package/payload/server/public/assets/time-entry-format-IFqinQJj.js.gz +0 -0
- package/payload/server/public/assets/{triangle-alert-wUS4rpzv.js → triangle-alert-C5vH__sq.js} +1 -1
- package/payload/server/public/assets/triangle-alert-C5vH__sq.js.br +0 -0
- package/payload/server/public/assets/triangle-alert-C5vH__sq.js.gz +0 -0
- package/payload/server/public/assets/{useCopyFeedback-BRix_wIl.js → useCopyFeedback-BeL2gPl6.js} +1 -1
- package/payload/server/public/assets/useCopyFeedback-BeL2gPl6.js.br +0 -0
- package/payload/server/public/assets/useCopyFeedback-BeL2gPl6.js.gz +0 -0
- package/payload/server/public/assets/useSubAccountSwitcher-CH9qm2Ad.js +14 -0
- package/payload/server/public/assets/useSubAccountSwitcher-CH9qm2Ad.js.br +0 -0
- package/payload/server/public/assets/useSubAccountSwitcher-CH9qm2Ad.js.gz +0 -0
- package/payload/server/public/assets/useSubAccountSwitcher-DLqUQzOl.css +1 -0
- package/payload/server/public/assets/useSubAccountSwitcher-DLqUQzOl.css.br +0 -0
- package/payload/server/public/assets/useSubAccountSwitcher-DLqUQzOl.css.gz +0 -0
- package/payload/server/public/assets/useVoiceRecorder-Cpw3aOSt.js +2 -0
- package/payload/server/public/assets/useVoiceRecorder-Cpw3aOSt.js.br +0 -0
- package/payload/server/public/assets/useVoiceRecorder-Cpw3aOSt.js.gz +0 -0
- package/payload/server/public/assets/{wrench-sDivAjhf.js → wrench-BBEup3hd.js} +1 -1
- package/payload/server/public/assets/wrench-BBEup3hd.js.br +0 -0
- package/payload/server/public/assets/wrench-BBEup3hd.js.gz +0 -0
- package/payload/server/public/browser.html +4 -4
- package/payload/server/public/calendar.html +7 -7
- package/payload/server/public/chat.html +13 -13
- package/payload/server/public/data.html +11 -11
- package/payload/server/public/graph.html +9 -9
- package/payload/server/public/index.html +14 -14
- package/payload/server/public/operator.html +14 -14
- package/payload/server/public/public.html +13 -13
- package/payload/server/public/routines.html +6 -6
- package/payload/server/public/skills.html +5 -5
- package/payload/server/public/tasks.html +6 -6
- package/payload/server/server.js +8176 -11914
- package/payload/server/public/assets/AdminLoginScreens-CDpNkwkf.js +0 -1
- package/payload/server/public/assets/AdminLoginScreens-CDpNkwkf.js.br +0 -0
- package/payload/server/public/assets/AdminLoginScreens-CDpNkwkf.js.gz +0 -0
- package/payload/server/public/assets/AdminShell-TSa8KgID.js +0 -2
- package/payload/server/public/assets/AdminShell-TSa8KgID.js.br +0 -0
- package/payload/server/public/assets/AdminShell-TSa8KgID.js.gz +0 -0
- package/payload/server/public/assets/activity-BE9gv5xL.js.br +0 -0
- package/payload/server/public/assets/activity-BE9gv5xL.js.gz +0 -0
- package/payload/server/public/assets/admin-Cb0BZriv.js +0 -1
- package/payload/server/public/assets/admin-Cb0BZriv.js.br +0 -0
- package/payload/server/public/assets/admin-Cb0BZriv.js.gz +0 -0
- package/payload/server/public/assets/agents-B-x0Ai2v.js.br +0 -0
- package/payload/server/public/assets/agents-B-x0Ai2v.js.gz +0 -0
- package/payload/server/public/assets/browser-ZEQUkPfs.js.br +0 -0
- package/payload/server/public/assets/browser-ZEQUkPfs.js.gz +0 -0
- package/payload/server/public/assets/calendar-DsRxrW0G.js +0 -1
- package/payload/server/public/assets/calendar-DsRxrW0G.js.br +0 -0
- package/payload/server/public/assets/calendar-DsRxrW0G.js.gz +0 -0
- package/payload/server/public/assets/chat-BS9B9lDo.js +0 -1
- package/payload/server/public/assets/chat-BS9B9lDo.js.br +0 -1
- package/payload/server/public/assets/chat-BS9B9lDo.js.gz +0 -0
- package/payload/server/public/assets/chevron-left-DLtGuH6K.js +0 -1
- package/payload/server/public/assets/chevron-right-CY_trg5L.js +0 -1
- package/payload/server/public/assets/chevron-right-CY_trg5L.js.br +0 -0
- package/payload/server/public/assets/clock-oM5driJm.js +0 -1
- package/payload/server/public/assets/clock-oM5driJm.js.br +0 -0
- package/payload/server/public/assets/clock-oM5driJm.js.gz +0 -0
- package/payload/server/public/assets/data-Cio6PO0F.js +0 -1
- package/payload/server/public/assets/data-Cio6PO0F.js.br +0 -2
- package/payload/server/public/assets/data-Cio6PO0F.js.gz +0 -0
- package/payload/server/public/assets/file-text-BTV6EdBv.js.br +0 -0
- package/payload/server/public/assets/file-text-BTV6EdBv.js.gz +0 -0
- package/payload/server/public/assets/graph-hD_gPbbL.js.br +0 -0
- package/payload/server/public/assets/graph-hD_gPbbL.js.gz +0 -0
- package/payload/server/public/assets/graph-labels-CY1T19PY.js.br +0 -0
- package/payload/server/public/assets/graph-labels-CY1T19PY.js.gz +0 -0
- package/payload/server/public/assets/maximize-2-BFV9VKHO.js.br +0 -0
- package/payload/server/public/assets/maximize-2-BFV9VKHO.js.gz +0 -0
- package/payload/server/public/assets/operator-Dxze8CwV.js +0 -1
- package/payload/server/public/assets/operator-Dxze8CwV.js.br +0 -0
- package/payload/server/public/assets/operator-Dxze8CwV.js.gz +0 -0
- package/payload/server/public/assets/page-Bi86QF6_.js +0 -32
- package/payload/server/public/assets/page-Bi86QF6_.js.br +0 -0
- package/payload/server/public/assets/page-Bi86QF6_.js.gz +0 -0
- package/payload/server/public/assets/page-BnXly9aN.js +0 -1
- package/payload/server/public/assets/page-BnXly9aN.js.br +0 -0
- package/payload/server/public/assets/page-BnXly9aN.js.gz +0 -0
- package/payload/server/public/assets/public-D6Tnj5LU.js.br +0 -0
- package/payload/server/public/assets/public-D6Tnj5LU.js.gz +0 -0
- package/payload/server/public/assets/rotate-ccw-C-RfQUza.js.br +0 -0
- package/payload/server/public/assets/rotate-ccw-C-RfQUza.js.gz +0 -0
- package/payload/server/public/assets/routines-NIMElUzV.js.br +0 -0
- package/payload/server/public/assets/routines-NIMElUzV.js.gz +0 -0
- package/payload/server/public/assets/skills-N0ZlHosx.js.br +0 -0
- package/payload/server/public/assets/skills-N0ZlHosx.js.gz +0 -0
- package/payload/server/public/assets/tasks-BsB_0duv.js +0 -1
- package/payload/server/public/assets/tasks-BsB_0duv.js.br +0 -0
- package/payload/server/public/assets/tasks-BsB_0duv.js.gz +0 -0
- package/payload/server/public/assets/time-entry-format-jQxlRPoI.js.br +0 -0
- package/payload/server/public/assets/time-entry-format-jQxlRPoI.js.gz +0 -0
- package/payload/server/public/assets/triangle-alert-wUS4rpzv.js.br +0 -0
- package/payload/server/public/assets/triangle-alert-wUS4rpzv.js.gz +0 -0
- package/payload/server/public/assets/useCopyFeedback-BRix_wIl.js.br +0 -0
- package/payload/server/public/assets/useCopyFeedback-BRix_wIl.js.gz +0 -0
- package/payload/server/public/assets/useSubAccountSwitcher-DWMWxvYo.js +0 -14
- package/payload/server/public/assets/useSubAccountSwitcher-DWMWxvYo.js.br +0 -0
- package/payload/server/public/assets/useSubAccountSwitcher-DWMWxvYo.js.gz +0 -0
- package/payload/server/public/assets/useSubAccountSwitcher-UQQDcj0B.css +0 -1
- package/payload/server/public/assets/useSubAccountSwitcher-UQQDcj0B.css.br +0 -0
- package/payload/server/public/assets/useSubAccountSwitcher-UQQDcj0B.css.gz +0 -0
- package/payload/server/public/assets/useVoiceRecorder-1TAfCP2k.js +0 -2
- package/payload/server/public/assets/useVoiceRecorder-1TAfCP2k.js.br +0 -0
- package/payload/server/public/assets/useVoiceRecorder-1TAfCP2k.js.gz +0 -0
- package/payload/server/public/assets/wrench-sDivAjhf.js.br +0 -0
- package/payload/server/public/assets/wrench-sDivAjhf.js.gz +0 -0
package/payload/platform/plugins/cloudflare/skills/data-portal/template/functions/api/upload.ts
CHANGED
|
@@ -48,7 +48,7 @@ export async function processUpload(
|
|
|
48
48
|
// printed a hardcoded ms=0, which an operator reads as "took no time" rather
|
|
49
49
|
// than "was never measured".
|
|
50
50
|
const startedMs = now()
|
|
51
|
-
const session = await resolveSession(env.DB, sessionId, startedMs)
|
|
51
|
+
const session = await resolveSession(env.DB, sessionId, env.PORTAL_ACCOUNT_ID, startedMs)
|
|
52
52
|
if (!session) {
|
|
53
53
|
log('[data-portal] op=upload owner=none result=denied')
|
|
54
54
|
return { status: 401, payload: { ok: false, error: 'denied' } }
|
|
@@ -63,7 +63,7 @@ export async function processUpload(
|
|
|
63
63
|
|
|
64
64
|
// The target is resolved BEFORE any R2 call, the same ordering rule the owner
|
|
65
65
|
// gate follows: a denied caller must never be able to cause a store write.
|
|
66
|
-
const target = await checkTarget(env.DB,
|
|
66
|
+
const target = await checkTarget(env.DB, env.PORTAL_ACCOUNT_ID, session.folders, targetPath)
|
|
67
67
|
if (!target.ok) {
|
|
68
68
|
log(
|
|
69
69
|
`[data-portal] op=target-check uploadId=${uploadId} owner=${q(owner)} ` +
|
|
@@ -76,10 +76,11 @@ export async function processUpload(
|
|
|
76
76
|
`targetPath=${q(targetPath)} result=allowed`,
|
|
77
77
|
)
|
|
78
78
|
|
|
79
|
-
const key = uploadKey(owner, targetPath, filename)
|
|
80
|
-
// The key is derived from the session owner, then
|
|
81
|
-
// gate the other routes use — a filename carrying
|
|
82
|
-
|
|
79
|
+
const key = uploadKey(env.PORTAL_ACCOUNT_ID, owner, targetPath, filename)
|
|
80
|
+
// The key is derived from this portal's account and the session owner, then
|
|
81
|
+
// re-checked through the same gate the other routes use — a filename carrying
|
|
82
|
+
// '..' must not escape.
|
|
83
|
+
if (!authorizeKey(env.PORTAL_ACCOUNT_ID, owner, key)) {
|
|
83
84
|
log(`[data-portal] op=request uploadId=${uploadId} owner=${q(owner)} result=bad-filename`)
|
|
84
85
|
return { status: 400, payload: { ok: false, error: 'invalid filename' } }
|
|
85
86
|
}
|
|
@@ -143,8 +144,9 @@ export async function processUpload(
|
|
|
143
144
|
// request time, which is why this line exists.
|
|
144
145
|
const objectPresent = (await env.BUCKET.head(key)) !== null
|
|
145
146
|
const rowPresent =
|
|
146
|
-
(await env.DB.prepare('SELECT fileId FROM manifest WHERE objectKey = ?')
|
|
147
|
-
|
|
147
|
+
(await env.DB.prepare('SELECT fileId FROM manifest WHERE objectKey = ? AND accountId = ?')
|
|
148
|
+
.bind(key, env.PORTAL_ACCOUNT_ID)
|
|
149
|
+
.first()) !== null
|
|
148
150
|
log(
|
|
149
151
|
`[data-portal] op=complete uploadId=${uploadId} ms=${now() - startedMs} objectPresent=${objectPresent} rowPresent=${rowPresent}`,
|
|
150
152
|
)
|
|
@@ -158,10 +160,14 @@ async function writeManifestRow(
|
|
|
158
160
|
env: PortalEnv,
|
|
159
161
|
row: { fileId: string; owner: string; filename: string; key: string; size: number; at: string; targetPath: string },
|
|
160
162
|
): Promise<void> {
|
|
163
|
+
// The row carries its account and the upsert conflicts on (accountId,
|
|
164
|
+
// objectKey). Conflicting on objectKey alone would be a takeover on the
|
|
165
|
+
// shared store: another client's upload at the same key would seize the
|
|
166
|
+
// existing row, reset its ingested flag, and redirect the file.
|
|
161
167
|
await env.DB.prepare(
|
|
162
|
-
`INSERT INTO manifest (fileId, ownerId, filename, objectKey, size, uploadedAt, ingested, targetPath)
|
|
163
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
164
|
-
ON CONFLICT (objectKey) DO UPDATE SET
|
|
168
|
+
`INSERT INTO manifest (fileId, accountId, ownerId, filename, objectKey, size, uploadedAt, ingested, targetPath)
|
|
169
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
170
|
+
ON CONFLICT (accountId, objectKey) DO UPDATE SET
|
|
165
171
|
fileId = excluded.fileId,
|
|
166
172
|
size = excluded.size,
|
|
167
173
|
uploadedAt = excluded.uploadedAt,
|
|
@@ -170,7 +176,17 @@ async function writeManifestRow(
|
|
|
170
176
|
devicePath = '',
|
|
171
177
|
deleted = 0`,
|
|
172
178
|
)
|
|
173
|
-
.bind(
|
|
179
|
+
.bind(
|
|
180
|
+
row.fileId,
|
|
181
|
+
env.PORTAL_ACCOUNT_ID,
|
|
182
|
+
row.owner,
|
|
183
|
+
row.filename,
|
|
184
|
+
row.key,
|
|
185
|
+
row.size,
|
|
186
|
+
row.at,
|
|
187
|
+
0,
|
|
188
|
+
row.targetPath,
|
|
189
|
+
)
|
|
174
190
|
.run()
|
|
175
191
|
}
|
|
176
192
|
|
|
@@ -192,7 +208,7 @@ export async function processMultipartCreate(
|
|
|
192
208
|
now: () => number,
|
|
193
209
|
): Promise<Handler> {
|
|
194
210
|
const startedMs = now()
|
|
195
|
-
const session = await resolveSession(env.DB, sessionId, startedMs)
|
|
211
|
+
const session = await resolveSession(env.DB, sessionId, env.PORTAL_ACCOUNT_ID, startedMs)
|
|
196
212
|
if (!session) {
|
|
197
213
|
log('[data-portal] op=mp-create owner=none result=denied')
|
|
198
214
|
return { status: 401, payload: { ok: false, error: 'denied' } }
|
|
@@ -215,7 +231,7 @@ export async function processMultipartCreate(
|
|
|
215
231
|
}
|
|
216
232
|
}
|
|
217
233
|
|
|
218
|
-
const target = await checkTarget(env.DB,
|
|
234
|
+
const target = await checkTarget(env.DB, env.PORTAL_ACCOUNT_ID, session.folders, targetPath)
|
|
219
235
|
if (!target.ok) {
|
|
220
236
|
log(
|
|
221
237
|
`[data-portal] op=target-check uploadId=${uploadId} owner=${q(owner)} ` +
|
|
@@ -224,8 +240,8 @@ export async function processMultipartCreate(
|
|
|
224
240
|
return { status: 403, payload: { ok: false, error: 'target not allowed' } }
|
|
225
241
|
}
|
|
226
242
|
|
|
227
|
-
const key = uploadKey(owner, targetPath, filename)
|
|
228
|
-
if (!authorizeKey(owner, key)) {
|
|
243
|
+
const key = uploadKey(env.PORTAL_ACCOUNT_ID, owner, targetPath, filename)
|
|
244
|
+
if (!authorizeKey(env.PORTAL_ACCOUNT_ID, owner, key)) {
|
|
229
245
|
log(`[data-portal] op=mp-create uploadId=${uploadId} owner=${q(owner)} result=bad-filename`)
|
|
230
246
|
return { status: 400, payload: { ok: false, error: 'invalid filename' } }
|
|
231
247
|
}
|
|
@@ -250,12 +266,15 @@ export async function processMultipartPart(
|
|
|
250
266
|
env: PortalEnv,
|
|
251
267
|
log: Logger,
|
|
252
268
|
): Promise<Handler> {
|
|
253
|
-
const session = await resolveSession(env.DB, sessionId, Date.now())
|
|
269
|
+
const session = await resolveSession(env.DB, sessionId, env.PORTAL_ACCOUNT_ID, Date.now())
|
|
254
270
|
if (!session) {
|
|
255
271
|
log('[data-portal] op=r2-put owner=none result=denied')
|
|
256
272
|
return { status: 401, payload: { ok: false, error: 'denied' } }
|
|
257
273
|
}
|
|
258
|
-
|
|
274
|
+
// The part key round-trips through the client on every call, so the full
|
|
275
|
+
// account and owner prefix is re-checked here rather than trusted from the
|
|
276
|
+
// create step that minted it.
|
|
277
|
+
if (!authorizeKey(env.PORTAL_ACCOUNT_ID, session.ownerId, key)) {
|
|
259
278
|
log(`[data-portal] op=r2-put owner=${q(session.ownerId)} result=denied reason=not-owner`)
|
|
260
279
|
return { status: 403, payload: { ok: false, error: 'denied' } }
|
|
261
280
|
}
|
|
@@ -311,14 +330,14 @@ export async function processMultipartComplete(
|
|
|
311
330
|
now: () => number,
|
|
312
331
|
): Promise<Handler> {
|
|
313
332
|
const startedMs = now()
|
|
314
|
-
const session = await resolveSession(env.DB, sessionId, startedMs)
|
|
333
|
+
const session = await resolveSession(env.DB, sessionId, env.PORTAL_ACCOUNT_ID, startedMs)
|
|
315
334
|
if (!session) {
|
|
316
335
|
log('[data-portal] op=mp-complete owner=none result=denied')
|
|
317
336
|
return { status: 401, payload: { ok: false, error: 'denied' } }
|
|
318
337
|
}
|
|
319
338
|
const owner = session.ownerId
|
|
320
339
|
const uploadId = newId()
|
|
321
|
-
if (!authorizeKey(owner, key)) {
|
|
340
|
+
if (!authorizeKey(env.PORTAL_ACCOUNT_ID, owner, key)) {
|
|
322
341
|
log(`[data-portal] op=mp-complete uploadId=${uploadId} owner=${q(owner)} result=denied reason=not-owner`)
|
|
323
342
|
return { status: 403, payload: { ok: false, error: 'denied' } }
|
|
324
343
|
}
|
|
@@ -366,8 +385,9 @@ export async function processMultipartComplete(
|
|
|
366
385
|
|
|
367
386
|
const objectPresent = (await env.BUCKET.head(key)) !== null
|
|
368
387
|
const rowPresent =
|
|
369
|
-
(await env.DB.prepare('SELECT fileId FROM manifest WHERE objectKey = ?')
|
|
370
|
-
|
|
388
|
+
(await env.DB.prepare('SELECT fileId FROM manifest WHERE objectKey = ? AND accountId = ?')
|
|
389
|
+
.bind(key, env.PORTAL_ACCOUNT_ID)
|
|
390
|
+
.first()) !== null
|
|
371
391
|
log(
|
|
372
392
|
`[data-portal] op=complete uploadId=${uploadId} ms=${now() - startedMs} ` +
|
|
373
393
|
`objectPresent=${objectPresent} rowPresent=${rowPresent}`,
|
|
@@ -1,10 +1,18 @@
|
|
|
1
1
|
# Cloudflare Pages config for the data portal (data-portal skill).
|
|
2
|
-
#
|
|
2
|
+
#
|
|
3
|
+
# `bin/portal-assemble.mjs` fills these, and nothing else may: hand-filling them
|
|
4
|
+
# is what shipped a portal whose bucket name and config disagreed (Task 2099).
|
|
3
5
|
# __PROJECT_NAME__ the Pages project name
|
|
4
|
-
# __D1_DATABASE_NAME__ the
|
|
5
|
-
# __D1_DATABASE_ID__
|
|
6
|
-
# __R2_BUCKET_NAME__ the
|
|
6
|
+
# __D1_DATABASE_NAME__ the ONE shared house database (DATA_PORTAL_DB_NAME)
|
|
7
|
+
# __D1_DATABASE_ID__ its id (DATA_PORTAL_DB_ID)
|
|
8
|
+
# __R2_BUCKET_NAME__ the ONE shared house bucket (DATA_PORTAL_BUCKET)
|
|
9
|
+
# __PORTAL_ACCOUNT_ID__ this portal's own account id
|
|
7
10
|
# __PORTAL_INSTALL_ORIGIN__ the install's PUBLIC origin (https://<public-tunnel-hostname>), NOT the admin/operator host
|
|
11
|
+
#
|
|
12
|
+
# The first three come from platform/config/cloudflare-house.env and are
|
|
13
|
+
# IDENTICAL in every portal: one store backs all of them. __PORTAL_ACCOUNT_ID__
|
|
14
|
+
# is what differs, and it is the only thing separating this client's rows and
|
|
15
|
+
# objects from another client's.
|
|
8
16
|
name = "__PROJECT_NAME__"
|
|
9
17
|
pages_build_output_dir = "."
|
|
10
18
|
compatibility_date = "2024-01-01"
|
|
@@ -31,4 +39,9 @@ bucket_name = "__R2_BUCKET_NAME__"
|
|
|
31
39
|
# directory it is handed, so a secret in it is a secret in a repo and one deploy
|
|
32
40
|
# from being served. Set it with `wrangler pages secret put`.
|
|
33
41
|
[vars]
|
|
42
|
+
# The tenant scope for every query and every object key. A var, not a secret:
|
|
43
|
+
# it is this client's own account id, not a credential, and the worker needs it
|
|
44
|
+
# on every request. Its partner PORTAL_FETCH_SECRET stays out of this file for
|
|
45
|
+
# the reason given above.
|
|
46
|
+
PORTAL_ACCOUNT_ID = "__PORTAL_ACCOUNT_ID__"
|
|
34
47
|
PORTAL_INSTALL_ORIGIN = "__PORTAL_INSTALL_ORIGIN__"
|
|
@@ -79,7 +79,7 @@ either is a regression.
|
|
|
79
79
|
|
|
80
80
|
**`/chat` Claude-desktop transcript presentation.** The admin webchat keeps the shared `Transcript` shell (stream, follow-tail) but injects a /chat-only item renderer via the component's optional `renderItems` prop: `renderChatTimeline` in `app/chat/transcript-render.tsx`. Presentation: operator turns render as a right-aligned grey bubble showing the message text and, beneath it, a subtle always-visible time-of-day stamp (no label); delivered agent replies render as plain prose with the same time-of-day stamp beneath (the reply-document filename line stays, as prose); the stamp is HH:MM from the turn's `ts` (locale-formatted, right-aligned inside the operator bubble, left-aligned under agent prose), and a turn whose `ts` is null/unparseable shows no stamp, never an empty line — so only delivered operator and agent-reply turns are stamped, while tool runs, the collapsed "Thinking" block, and the agent-error banner stay time-free; a maximal consecutive run of `tool-call`/`tool-result` turns renders as one collapsed grey one-liner ("Used N tools ›") whose expansion shows every call and result payload — a lone call still gets the one-liner, and prose or a directive row ends a run. The WhatsApp/operator reader (`/whatsapp`, `OperatorConversations`) omits the prop and keeps the default `renderTimeline`, so its bubble/DM chrome is unchanged — except that since the attachment-render extraction the default renderer draws inbound `operator-inbound` uploads and `agent-file` deliveries inline (image thumbnail / pdf.js preview / voice-note player / download chip) via the attachment renderer shared with `/chat`, extracted to `app/chat/attachment-render.tsx` to break the cycle with `transcript-render.tsx`; `renderChatTimeline` is untouched. Both presentations are test-pinned (`app/whatsapp/__tests__/Transcript-*.test.tsx`, `app/chat/__tests__/transcript-render.test.tsx`, `app/chat/__tests__/pdf-preview.test.tsx`). **Day-divider:** both renderers insert a centered `.day-divider` row between two consecutive timeline items on different local calendar days (label `Today`/`Yesterday`, else `Sat 14 Jun 2026`), so a thread spanning midnight is never an ambiguous run of HH:MM; the first dated item gets a leading divider, a null/unparseable `ts` marks no boundary, dividers are computed over the filtered `visibleItems`, and in `/chat` a day crossover flushes any open tool/think run first so a collapsed run never spans it. Per-bubble HH:MM stamps are unchanged. Shared helpers `dayKey`/`dayLabel`/`itemTs`/`DayDivider` live in `app/whatsapp/Transcript.tsx`.
|
|
81
81
|
|
|
82
|
-
**One delivered bubble per turn
|
|
82
|
+
**One delivered bubble per turn on the admin `/chat`.** The admin webchat has no outbound transport, so the agent has no reply tool there: the text its turn ends with **is** the delivered answer, and it renders as the one prominent prose bubble. Mid-turn prose collapses into the folded run as before, so a turn shows exactly one delivered bubble. A turn that reaches its end with no trailing text delivers nothing and is logged as `op=turn-answer-absent`. Public webchat, WhatsApp and Telegram keep their reply tool, and every historical admin transcript still renders its recorded reply-tool bubbles unchanged.
|
|
83
83
|
|
|
84
84
|
**WhatsApp reader is read/write (WhatsApp-Web surface).** A WhatsApp store conversation in the operator Conversations reader (`OperatorConversations`) and the admin root channel viewer (`app/page.tsx`) no longer opens the plain read-only DM chrome — it opens `app/whatsapp/WhatsappWebConversation.tsx`, a WhatsApp-Web-styled surface: a conversation header, the delivered-only store transcript reskinned green/white under a `.wa-web` root class, and a reply composer (`app/whatsapp/ReplyComposer.tsx`). The operator can type text, insert emoji (a codepoint-data picker — no OS input, so the `check-no-emoji-in-jsx` gate stays green), record a voice note (MediaRecorder → the existing audio send path), attach a file **from device storage** (a modal browsing the DATA_ROOT tree via `GET /api/admin/files`, sent by reference — the OS file dialog is no longer the reader's picker), reply to a specific message (a per-bubble affordance that quotes the original in the composer and threads natively on WhatsApp), and search the loaded transcript (a header control that filters to matching turns and highlights the match). The header carries only that Search control — the former Video / Phone / menu icons are gone, because no call or presence data exists behind them. Sent files honour the webchat allowlist (`SUPPORTED_MIME_SET` / 50 MB / 5 files); a device-file reference is authorised server-side (`authorizeReplyFileRef`) against the send's own account (the conversation owner) **or** the session's pinned account (`getAccountIdForSession` — the exact scope the DeviceFilePicker used to offer it), so a house/managing-admin session can attach a managed-client file the picker listed. Because a reply sends bytes OUT to an external WhatsApp recipient, any other account, a `public` partition, and any install-wide / non-account path are refused fail-closed; the cross-account admission's authority is the session's pinned account, never the caller-supplied body `accountId` (the own-account admission matches the body `accountId`, which the route already trusts via `isValidAccountId` and the existing-conversation guard). A scope refusal returns a distinct HTTP 403 reason (`file-ref-cross-account` / `file-ref-public` / `file-ref-foreign`) and copy, never "device file not found" (reserved for a genuine resolve/stat miss). Per-ref `[wa-manual-send] op=reply-file-ref … via=own|session|deny reason=<…> ok=<bool>` logs the decision before any send; `via=deny ok=true` is the exfiltration signature that must never appear. **Send is reply-only and DM-only:** `POST /api/whatsapp-reader/reply` (`requireAdminSession`) refuses any target with no existing stored conversation (`reason=no-existing-conversation`, HTTP 403) — a cold first-contact send bans the account, so the composer is never a cold-contact path, enforced server-side — and refuses `@g.us` groups (`reason=group-send-blocked`), matching the agent's observe-only group invariant; a group thread shows a read-only note in place of the composer. The endpoint is the sole store writer for a manual send: right after each send resolves it appends the record itself, stamped `origin:'operator-manual'` (attributed "You", distinct from an assistant reply), because the Baileys echo that normally persists the store is gated on message text (`manager.ts:906`) and would leave an uncaptioned media send delivered-but-invisible. The record's `messageId` matches the echo's derivation so the async text echo's own persist dedups to a no-op; the send is confirmed by re-read (`op=recorded present=<bool>`). A sent send does not spawn an assistant turn (echo dispatch stays suppressed). Attachments deliver as WhatsApp media/documents and render **inline** in the store reader (image thumbnail / pdf preview / audio player / download chip), because the manual-send path records the message with the servable `attachmentId`. The `.wa-web` skin is scoped to WhatsApp store threads only, so `/chat`, the admin agent session view, and the public visitor webchat are unchanged; the visitor webchat gains no send capability. The in-bubble delivered-tick is cosmetic (no read-receipt data is stored); inbound bubbles keep their sender name so group threads stay attributable, and both directions carry a WhatsApp callout tail (`.wa-web`-scoped only). Non-WhatsApp reader rows (Telegram/webchat session JSONLs) keep the plain read-only `Transcript`. Observability rides `[wa-manual-send]` lines correlated by a per-request `sendId`. **Composer sizing matches the webchat composer:** the reply textarea auto-grows to fit multi-line content (measured from `scrollHeight`, clamped to a one-row minimum and a `viewport/3` maximum, scrollable past the cap with the scrollbar hidden), and a top-border grab handle drags (pointer) or nudges (ArrowUp/ArrowDown, keyboard-focusable) to an explicit height in the same clamp; a manual drag pins the height and overrides auto-grow until the field is cleared, which releases it back to auto-grow. A completed manual resize logs `[operator-ui] op=wa-composer-resize mode=drag|key h=<px>`; a bare click on the handle (no drag) logs nothing.
|
|
85
85
|
|
|
@@ -757,7 +757,7 @@ authoritative. This section names the surfaces and what backs each.
|
|
|
757
757
|
> a read-only reader. The item gates on the flyout handler being wired, exactly
|
|
758
758
|
> like the admin and lite items. The read-only inbound-channel **Conversations**
|
|
759
759
|
> reader (WhatsApp, Telegram, public webchat, via the admin-gated
|
|
760
|
-
> `/api/
|
|
760
|
+
> `/api/operator-conversations/conversations` route) remains on `/data`'s Conversations
|
|
761
761
|
> item and in the admin Sidebar; it is no longer reachable from the operator chat
|
|
762
762
|
> surface. The operator surface is documented in full in the internal doc
|
|
763
763
|
> `maxy-code/.docs/operator-dashboard.md`.
|
|
@@ -799,7 +799,7 @@ authoritative. This section names the surfaces and what backs each.
|
|
|
799
799
|
> count. On the operator **chat** surface the handler is always wired, so the
|
|
800
800
|
> item is always present and opens the live session switcher (above). On `/data`
|
|
801
801
|
> the handler is wired only when ≥1 reviewable inbound conversation exists
|
|
802
|
-
> (fetched from `/
|
|
802
|
+
> (fetched from `/operator-conversations/conversations` with the operator's session
|
|
803
803
|
> key), and selecting it renders the read-only reader in place (the file browser
|
|
804
804
|
> is swapped for it; Back returns) — so `/data` keeps its hide-when-empty
|
|
805
805
|
> behaviour. The flyout-open signal `[operator-ui] op=conversations-menu
|
|
@@ -813,8 +813,8 @@ authoritative. This section names the surfaces and what backs each.
|
|
|
813
813
|
| Nav rows (Chat / People / Agents / Projects / Tasks / Artefacts) | People, Agents, Tasks open the artefact-pane Graph filtered to the matching label. Chat selects the active conversation. Artefacts swaps the list to editable documents. **The "Projects" row is brand-aware:** its visible term and the graph label it filters to both come from the injected `window.__BRAND__.primaryContainer` (`brand.json` → `server/index.ts` inject → `brand.ts` `brandPrimaryContainer()`), so SiteDesk reads "Projects" → `:Project`, SiteDesk "Jobs" → `:Job`, Real Agent "Properties" → `:Property`. A brand JSON missing `primaryContainer` falls back to Project/Projects (server logs `[brand] op=primaryContainer-default reason=absent brand=<hostname>`; the populated case logs `[brand] op=inject primaryContainer=<label>/<term>`). | `graph-search.ts`, `graph-subgraph.ts`, `sidebar-artefacts.ts` |
|
|
814
814
|
| Sessions list (Active / Archived / All) | Live row store driven by SSE; manual reconcile button on the segmented control re-fetches the full id set. Every listed row carries two launch actions: the claude.ai/code resume (`ExternalLink`) and a chat-launch (`MessageSquare`) that opens `/chat?session=<full sessionId>` — the admin webchat pointed at that exact session; sending there resumes a stopped session with the webchat channel bound to that id (see `admin-webchat-native-channel.md`, "Session-targeted mode"). **Clicking the row's body** (live-dot, title, id, or timestamp) opens that same `/chat?session=<id>` as the chat-launch action. The main area stays a `<div>` (it wraps the inline rename input, which a `<button>` may not contain) carrying `role="button"`, `tabIndex`, an "Open <title> in webchat" aria-label, and Enter/Space activation, styled with `conv-main-clickable` for pointer + hover parity with the channel-row button. The click and keyboard handlers no-op while the row is being renamed, so an in-progress edit is preserved; the action icons and the `⋯` overflow menu are a sibling cluster running their own handlers, so activating them never triggers the row-body open. At the ≤720 px drawer breakpoint each row stacks into two lines — dot + title + id/stamp, then the action icons — with a divider between rows and actions at full opacity (the desktop hover-gated 0.5 opacity never resolves on touch). Below a 400 px list width each row's action icons collapse to a single `MoreHorizontal` ellipsis trigger; its popover menu lists the same actions as the inline icons but labels them with concise verbs (`Resume in claude.ai/code`, `Open in webchat`, `Stop`, `Archive` / `Unarchive`, `Rename`, `Delete`, `Re-seat`) rather than the inline icon buttons' verbose per-session aria-labels — the inline buttons keep those verbose labels because an icon-only control needs the full descriptive accessible name. **Re-seat:** Re-seat is a member action of the one `SessionRowActions` cluster, not a separate sibling control — wide it is the sliders icon inline with the other icons (sharing the cluster geometry and alignment); collapsed it is the `Re-seat` menu item, which expands the model/mode/effort form inline inside the overflow menu. The form (`SessionReseatControl`) forks the session via `/api/admin/session-reseat` and navigates to the fork (the same fork mechanism `/chat`'s pickers drive); its model picker defaults to the row's current model (a `model` field on each row, read from the JSONL tail), and mode and effort each carry a "Keep" option that omits the field. The overflow menu and the Re-seat form render through a `document.body` portal, fixed-positioned from the trigger's rect (right-aligned, flipped above the trigger when there is no room below), so neither is clipped by `.side-list`'s `overflow-y:auto`; Escape, a pointerdown outside the trigger/popover, and any scroll or resize dismiss them. | `/claude-sessions/events`, `/claude-sessions`, `/session-reseat` |
|
|
815
815
|
| Data pane | A "Data" nav row beside Artefacts/Sessions. Uses the same `activeNav` toggle, but its content — the headless `DataFileBrowser` (graph search + `{installDir}/data/` file browser) — renders in the shell's **main column** via `DataPaneSurface`, like the WhatsApp Transcript, not in the side-list; there is no side-list block under `activeNav==='data'`. `AdminShell` owns a `dataOpen` main-column state (mutually exclusive with the WhatsApp selection); the row lifts the choice through `onSelectData`. On `/` it opens in place; from any other route (`/graph`, `/chat`, `/browser`) it navigates to `/?data=1`, which the root shell hydrates on mount (`hydrateDataFromUrl`, then strips the query). The admin burger no longer carries a Data item (the operator surface, which has no Sidebar, keeps the burger → `/data` route). Client breadcrumbs: `[admin-ui] sidebar-nav surface=data …`, `[admin-ui] data-open route=… via=<in-place\|navigate>`, `[admin-ui] data-hydrate route=/`. | `sidebar-artefacts.ts`, `files.ts`, `graph-search.ts` |
|
|
816
|
-
| Channel reader nav rows (WhatsApp / Telegram) | One conditional nav row per channel that has ≥1 conversation. Each row carries its brand glyph (WhatsApp `#25D366`, Telegram `#229ED9`); a channel with no conversations shows no row, and no install ever shows an always-empty "No conversations" row. The channel list loads **once on sidebar mount**, so a failed load shows one muted, non-clickable line instead of channel rows. **WhatsApp rows are sourced from the on-disk message store** (`readAllConversations`), one row per stored conversation keyed by `remoteJid`, independent of whether any session JSONL exists — a blocked non-admin DM or a group-observed thread that never produced a session is listed here. **Telegram rows are session-sourced** (one per admin Telegram session), unchanged. Each conversation row shows a **display name**: for WhatsApp, the store record's `senderName` (else the `senderTelephone`, else the `remoteJid`); for Telegram, the precedence `operatorName ?? senderId ?? title` where `operatorName` is the bound `Person givenName familyName`. Under the name each row shows a **last-message-time breadcrumb** (`conv-timestamp`) from `lastMessageAt`, never "Invalid Date". **Scope colour (WhatsApp):** each WhatsApp row is coloured by its conversation's scope — a thin left accent bar, green for `admin`, orange for `public`, taken from the conversation's most-recent stored record (this replaces the former "Public" text pill). **Re-seat:** only session-sourced rows (Telegram, and webchat in the Sessions panel) carry a `SessionRowActions` Re-seat cluster; a WhatsApp store row has no session, so it carries no Re-seat. A click on a conversation **opens its transcript on the home surface**: on `/` it selects in place, and from any other route it navigates to `/?wa=<remoteJid>&acct=<accountId>`, which the root shell hydrates on mount (then strips the query). **Account-scoped** exactly as the sidebar Sessions list: both row sources (WhatsApp store rows and Telegram/public-webchat session rows) are scoped to the caller's pinned account (`getAccountIdForSession(cacheKey)`), multi-account-gated (`listValidAccounts().length > 1`). On a multi-account install a subaccount view shows only its own conversations; the house account's WhatsApp appears only under the house view; an untagged session sidecar is excluded and counted; an unresolvable scope fails closed to an empty list (logs `op=list-scope-unresolved`). A single-account install filters nothing, byte-identical to before. Server logs `[wa-reader] op=list pinnedAccount=<id8> resolvedAccount=<id8> multiAccount=<bool> conversations=<n> sessionRows=<m> storeRows=<k> adminScope=<n> publicScope=<n> sessionRowsExcludedUntagged=<n>` per list build (`pinnedAccount === resolvedAccount` is the post-fix invariant when `multiAccount=true`; `conversations=0` on an account with a non-empty store dir is the over-filtering drift signal); the client logs `[admin-ui] wa-open route=… via=<in-place\|navigate> …` and `[admin-ui] wa-hydrate route=/ remoteJid=…`. | `/
|
|
817
|
-
| Channel transcript (`<Transcript>`) | **WhatsApp** reads its conversation verbatim from the message store file (`<remoteJid>.jsonl`) over SSE (`/whatsapp-reader/store-stream`): both directions, `dateSent`-ordered, backlog on connect then a live tail so a newly stored message appears without reload — no session JSONL is read. **Telegram** still reads one session's JSONL over SSE (`/
|
|
816
|
+
| Channel reader nav rows (WhatsApp / Telegram) | One conditional nav row per channel that has ≥1 conversation. Each row carries its brand glyph (WhatsApp `#25D366`, Telegram `#229ED9`); a channel with no conversations shows no row, and no install ever shows an always-empty "No conversations" row. The channel list loads **once on sidebar mount**, so a failed load shows one muted, non-clickable line instead of channel rows. **WhatsApp rows are sourced from the on-disk message store** (`readAllConversations`), one row per stored conversation keyed by `remoteJid`, independent of whether any session JSONL exists — a blocked non-admin DM or a group-observed thread that never produced a session is listed here. **Telegram rows are session-sourced** (one per admin Telegram session), unchanged. Each conversation row shows a **display name**: for WhatsApp, the store record's `senderName` (else the `senderTelephone`, else the `remoteJid`); for Telegram, the precedence `operatorName ?? senderId ?? title` where `operatorName` is the bound `Person givenName familyName`. Under the name each row shows a **last-message-time breadcrumb** (`conv-timestamp`) from `lastMessageAt`, never "Invalid Date". **Scope colour (WhatsApp):** each WhatsApp row is coloured by its conversation's scope — a thin left accent bar, green for `admin`, orange for `public`, taken from the conversation's most-recent stored record (this replaces the former "Public" text pill). **Re-seat:** only session-sourced rows (Telegram, and webchat in the Sessions panel) carry a `SessionRowActions` Re-seat cluster; a WhatsApp store row has no session, so it carries no Re-seat. A click on a conversation **opens its transcript on the home surface**: on `/` it selects in place, and from any other route it navigates to `/?wa=<remoteJid>&acct=<accountId>`, which the root shell hydrates on mount (then strips the query). **Account-scoped** exactly as the sidebar Sessions list: both row sources (WhatsApp store rows and Telegram/public-webchat session rows) are scoped to the caller's pinned account (`getAccountIdForSession(cacheKey)`), multi-account-gated (`listValidAccounts().length > 1`). On a multi-account install a subaccount view shows only its own conversations; the house account's WhatsApp appears only under the house view; an untagged session sidecar is excluded and counted; an unresolvable scope fails closed to an empty list (logs `op=list-scope-unresolved`). A single-account install filters nothing, byte-identical to before. Server logs `[wa-reader] op=list pinnedAccount=<id8> resolvedAccount=<id8> multiAccount=<bool> conversations=<n> sessionRows=<m> storeRows=<k> adminScope=<n> publicScope=<n> sessionRowsExcludedUntagged=<n>` per list build (`pinnedAccount === resolvedAccount` is the post-fix invariant when `multiAccount=true`; `conversations=0` on an account with a non-empty store dir is the over-filtering drift signal); the client logs `[admin-ui] wa-open route=… via=<in-place\|navigate> …` and `[admin-ui] wa-hydrate route=/ remoteJid=…`. | `/operator-conversations/conversations` |
|
|
817
|
+
| Channel transcript (`<Transcript>`) | **WhatsApp** reads its conversation verbatim from the message store file (`<remoteJid>.jsonl`) over SSE (`/whatsapp-reader/store-stream`): both directions, `dateSent`-ordered, backlog on connect then a live tail so a newly stored message appears without reload — no session JSONL is read. **Telegram** still reads one session's JSONL over SSE (`/session-reader/stream`) and renders both sides plus tool calls — the turns claude.ai/code hides because channel inbound is stamped `isMeta`. Every turn shows its time-of-day (HH:MM from the turn's `ts`; a null/unparseable `ts` shows no time, never "Invalid Date"). On the Telegram session stream, `tool-call`/`tool-result` turns render as **collapsed cards** so the human↔agent text is not buried in JSON. The thread **follows the tail**: it opens at the newest turn and pins to the bottom on each live append **only while the operator is already within 32 px of the bottom**; while scrolled up a **jump-to-bottom control** (`.wa-jump`) appears. Both streams send a 20 s heartbeat so an idle Cloudflare tunnel does not idle-drop, tag each frame with a byte-offset `id:`, and resume from the client's `Last-Event-ID` on reconnect (no duplicate backlog); the client clears the "Stream disconnected" banner on reopen and latches it only on a terminal CLOSED state. The Telegram session stream also interleaves a collapsed `⚙ directive injected (N B)` row per turn (listed by `GET /session-reader/directives?sessionId=`, bytes served by `GET /session-reader/directive?sessionId=&name=`); a WhatsApp store conversation has no session, so it has no directives. A centered day-divider row (`.day-divider`) marks each local calendar-day crossover. This viewer renders **delivered messages only** (both WhatsApp store rows and Telegram session rows, via `forceDeliveredOnly`) with **no per-viewer toggle** — the former "Messages only" pill is retired; the admin `/chat` composer's own messages-only control is unaffected. The panel — wallpaper included — is **bounded to `--chat-measure` and centred** (`.platform > .wa-reader`), so it reads as a column the same width as `/data`, not an edge-to-edge beige surface. The store stream open logs `[wa-reader] op=open remoteJid=<jid> scope=<admin\|public> messages=<n> source=store`. | `/whatsapp-reader/store-stream`, `/session-reader/stream`, `/session-reader/directives`, `/session-reader/directive` |
|
|
818
818
|
| Conversations row hover actions | Inline rename, archive, delete, JSONL view / download per row. The historical `.conversations-modal` CSS block exists in `globals.css` but is no longer mounted from any TSX — Sidebar.tsx now owns every per-row affordance directly. | `claude-sessions.ts` |
|
|
819
819
|
| Artefacts list | Lists every `:FileArtifact` under this account's tree (`relativePath STARTS WITH 'accounts/'`, all file types, excluding the `uploads/<id>/` subtree) plus this account's IDENTITY / SOUL / KNOWLEDGE / specialist templates. Click downloads the row's backing file (`downloadPath` → `GET /api/admin/files/download`) so the operator opens it in their local app; rows whose file is outside `DATA_ROOT` (bundled-fallback templates) show a "can't be downloaded" pill. The in-app artefact pane is dead pending removal. | `sidebar-artefacts.ts`, `files.ts` |
|
|
820
820
|
| System-stats widget | CPU / RAM widget at the foot of the sidebar. | `system-stats.ts` (see above) |
|
|
@@ -916,6 +916,44 @@ equivalent wording: the reader reply path and the `/send-admin` route.
|
|
|
916
916
|
Consolidating them is tracked separately; do not copy their strings into
|
|
917
917
|
new callers.
|
|
918
918
|
|
|
919
|
+
## WhatsApp inbound notifications
|
|
920
|
+
|
|
921
|
+
An inbound WhatsApp message raises a WhatsApp-green card at the **top right** of
|
|
922
|
+
every admin surface within about a second of arrival. The card shows the
|
|
923
|
+
sub-account it arrived on, the sender (their WhatsApp profile name, or their
|
|
924
|
+
number when they have none), and the first two lines of the message, with an
|
|
925
|
+
ellipsis when there was more. A media message with no caption shows its media
|
|
926
|
+
type instead.
|
|
927
|
+
|
|
928
|
+
The card disappears within five seconds and is **stored nowhere** — there is no
|
|
929
|
+
history, no unread count, and nothing to click. It exists to tell the operator a
|
|
930
|
+
message landed; the reader is where the message itself is read.
|
|
931
|
+
|
|
932
|
+
**Bursts drain rather than pile up.** One card alone lasts five seconds. As more
|
|
933
|
+
arrive, every card on screen speeds up: two share 2.5 seconds each, three 1.7,
|
|
934
|
+
four 1.25, and five or more sit on a 1.2 second floor. A card's dwell only ever
|
|
935
|
+
shortens, so it does not linger once the stack empties. There is no cap on how
|
|
936
|
+
many cards can appear at once.
|
|
937
|
+
|
|
938
|
+
**Four sender types, one icon each:**
|
|
939
|
+
|
|
940
|
+
| Icon | Sender |
|
|
941
|
+
|---|---|
|
|
942
|
+
| Shield | **Admin** — the operator's own number, or a bound admin |
|
|
943
|
+
| Two-way arrows | **Account manager (active)** — a manager whose messages drive an agent turn |
|
|
944
|
+
| Eye | **Account manager (passive)** — a passive bind, which files work but never replies |
|
|
945
|
+
| Person | **Public** — a member of the public |
|
|
946
|
+
|
|
947
|
+
Cards from **every** sub-account appear regardless of which account the tab is
|
|
948
|
+
currently pinned to, and each is labelled with that sub-account's name. That is
|
|
949
|
+
deliberate: seeing another account's message is what tells the operator to
|
|
950
|
+
switch to it.
|
|
951
|
+
|
|
952
|
+
Messages that raise no card, by design: anything the operator sent, group
|
|
953
|
+
messages (groups are observation-only), history catch-up during a re-sync, and
|
|
954
|
+
any sender the access rules decline. A blocked or unrecognised contact is
|
|
955
|
+
silent.
|
|
956
|
+
|
|
919
957
|
## Related references
|
|
920
958
|
|
|
921
959
|
- [`platform.md`](platform.md) — UI layout, session reconcile model,
|
|
@@ -68,6 +68,8 @@ So:
|
|
|
68
68
|
|
|
69
69
|
The share is reachable solely from the box itself. Operators reach it by forwarding loopback over SSH — `ssh -L 4445:localhost:445 admin@<tunnel-host>` and then mounting `smb://localhost:4445` — or by joining the box to a private network and re-installing, which makes the private interface the chosen LAN bind.
|
|
70
70
|
|
|
71
|
+
That second remedy applies to SMB only. The admin edge uses a different rule: it binds loopback whenever the host carries a public IPv4, whether or not a private interface exists alongside it, so joining a private network does not make the edge LAN-reachable on a cloud box.
|
|
72
|
+
|
|
71
73
|
If the device has no non-loopback IPv4 interface at all, the installer refuses to provision and exits — there is nothing safe to bind to. This is distinct from the public-only case, which still provisions a working (host-local) share.
|
|
72
74
|
|
|
73
75
|
This classification is the structural guarantee that SMB never leaves the box (loopback-only) or the LAN (LAN-only), even if upstream firewall rules are misconfigured. The Cloudflare tunnel that fronts the admin UI carries HTTPS only; it does not route SMB.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "email",
|
|
3
|
-
"description": "Email — one or more dedicated email mailboxes per account, each addressable by a `mailbox` selector. Retrieval: email-graph-query for stored email-thread history as :ConversationArchive {source:'email'} with :Section chunks (recall, search, 'threads about X', 'what did Y send?'); email-read / email-search for live IMAP inbox previews (with best-effort per-archive section hints from the graph); email-fetch-body for the COMPLETE decoded body of one message (by uid or message_id) when a preview is not enough; email-fetch-attachment for the extracted TEXT of one attachment (PDF via pdftotext, text/* by decode) when data sits inside a file rather than the body. Ingestion (operator-initiated, human-in-the-loop): email-fetch lists new messages, email-ingest writes the operator-approved subset via the conversation-archive pipeline. Sending: email-send (new), email-reply (threaded — resolves the original via IMAP Message-ID search), email-draft (compose to the mailbox Drafts folder, unsent, for operator review; pass messageId for a threaded draft-reply), email-draft-edit (revise a stored draft, located by its Drafts UID or, when Gmail has renumbered it, its draft Message-ID), email-draft-send (dispatch a stored draft by Drafts UID, then remove it). Deleting: email-delete (move named UIDs from email-read/email-search to the mailbox Trash — recoverable, never an expunge). Config: email-provider-info (pre-password host + app-password lookup from an address alone), email-setup, email-status, email-otp-extract.",
|
|
3
|
+
"description": "Email — one or more dedicated email mailboxes per account, each addressable by a `mailbox` selector. Retrieval: email-graph-query for stored email-thread history as :ConversationArchive {source:'email'} with :Section chunks (recall, search, 'threads about X', 'what did Y send?'); email-read / email-search for live IMAP inbox previews (with best-effort per-archive section hints from the graph); email-fetch-body for the COMPLETE decoded body of one message (by uid or message_id) when a preview is not enough; email-fetch-attachment for the extracted TEXT of one attachment (PDF via pdftotext, text/* by decode) when data sits inside a file rather than the body. Ingestion (operator-initiated, human-in-the-loop): email-fetch lists new messages, email-ingest writes the operator-approved subset via the conversation-archive pipeline. Sending: email-send (new), email-reply (threaded — resolves the original via IMAP Message-ID search), email-draft (compose to the mailbox Drafts folder, unsent, for operator review; pass messageId for a threaded draft-reply), email-draft-edit (revise a stored draft, located by its Drafts UID or, when Gmail has renumbered it, its draft Message-ID), email-draft-send (dispatch a stored draft by Drafts UID, then remove it), email-draft-list (read the Drafts folder back and confirm what is actually in it — the only tool that can verify a draft's presence). Deleting: email-delete (move named UIDs from email-read/email-search to the mailbox Trash — recoverable, never an expunge). Config: email-provider-info (pre-password host + app-password lookup from an address alone), email-setup, email-signature-set (install the signature every email-draft then appends automatically — never hand-write a signature into a draft body), email-status, email-otp-extract.",
|
|
4
4
|
"version": "0.1.0",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Rubytech LLC"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: email
|
|
3
|
-
description: "Email — one or more dedicated email mailboxes per account, each addressable by a `mailbox` selector. Retrieval: email-graph-query for stored email-thread history as :ConversationArchive {source:'email'} with :Section chunks (recall, search, 'threads about X', 'what did Y send?'); email-read / email-search for live IMAP inbox previews (with best-effort per-archive section hints from the graph); email-fetch-body for the COMPLETE decoded body of one message (by uid or message_id) when a preview is not enough; email-fetch-attachment for the extracted TEXT of one attachment (PDF via pdftotext, text/* by decode) when data sits inside a file rather than the body. Ingestion (operator-initiated, human-in-the-loop): email-fetch lists new messages, email-ingest writes the operator-approved subset via the conversation-archive pipeline. Sending: email-send (new), email-reply (threaded — resolves the original via IMAP Message-ID search), email-draft (compose to the mailbox Drafts folder, unsent, for operator review; pass messageId for a threaded draft-reply), email-draft-edit (revise a stored draft, located by its Drafts UID or, when Gmail has renumbered it, its draft Message-ID), email-draft-send (dispatch a stored draft by Drafts UID, then remove it). Deleting: email-delete (move named UIDs from email-read/email-search to the mailbox Trash — recoverable, never an expunge). Config: email-provider-info (pre-password host + app-password lookup from an address alone), email-setup, email-status, email-otp-extract."
|
|
3
|
+
description: "Email — one or more dedicated email mailboxes per account, each addressable by a `mailbox` selector. Retrieval: email-graph-query for stored email-thread history as :ConversationArchive {source:'email'} with :Section chunks (recall, search, 'threads about X', 'what did Y send?'); email-read / email-search for live IMAP inbox previews (with best-effort per-archive section hints from the graph); email-fetch-body for the COMPLETE decoded body of one message (by uid or message_id) when a preview is not enough; email-fetch-attachment for the extracted TEXT of one attachment (PDF via pdftotext, text/* by decode) when data sits inside a file rather than the body. Ingestion (operator-initiated, human-in-the-loop): email-fetch lists new messages, email-ingest writes the operator-approved subset via the conversation-archive pipeline. Sending: email-send (new), email-reply (threaded — resolves the original via IMAP Message-ID search), email-draft (compose to the mailbox Drafts folder, unsent, for operator review; pass messageId for a threaded draft-reply), email-draft-edit (revise a stored draft, located by its Drafts UID or, when Gmail has renumbered it, its draft Message-ID), email-draft-send (dispatch a stored draft by Drafts UID, then remove it), email-draft-list (read the Drafts folder back and confirm what is actually in it — the only tool that can verify a draft's presence). Deleting: email-delete (move named UIDs from email-read/email-search to the mailbox Trash — recoverable, never an expunge). Config: email-provider-info (pre-password host + app-password lookup from an address alone), email-setup, email-signature-set (install the signature every email-draft then appends automatically — never hand-write a signature into a draft body), email-status, email-otp-extract."
|
|
4
4
|
tools:
|
|
5
5
|
- name: email-provider-info
|
|
6
6
|
publicAllowlist: false
|
|
@@ -10,6 +10,10 @@ tools:
|
|
|
10
10
|
publicAllowlist: false
|
|
11
11
|
adminAllowlist: false
|
|
12
12
|
riskClass: write_local
|
|
13
|
+
- name: email-signature-set
|
|
14
|
+
publicAllowlist: false
|
|
15
|
+
adminAllowlist: false
|
|
16
|
+
riskClass: write_local
|
|
13
17
|
- name: email-read
|
|
14
18
|
publicAllowlist: false
|
|
15
19
|
adminAllowlist: false
|
|
@@ -42,6 +46,13 @@ tools:
|
|
|
42
46
|
publicAllowlist: false
|
|
43
47
|
adminAllowlist: false
|
|
44
48
|
riskClass: external
|
|
49
|
+
# adminAllowlist is true here and false on every sibling: this tool exists to
|
|
50
|
+
# be run AGAINST the other email tools' claims, so it is useless if only their
|
|
51
|
+
# callers can reach it. It is a pure read and writes nothing.
|
|
52
|
+
- name: email-draft-list
|
|
53
|
+
publicAllowlist: false
|
|
54
|
+
adminAllowlist: true
|
|
55
|
+
riskClass: read
|
|
45
56
|
- name: email-search
|
|
46
57
|
publicAllowlist: false
|
|
47
58
|
adminAllowlist: false
|
|
@@ -101,6 +112,7 @@ Manages the agent's own dedicated email account — IMAP for reading, SMTP for s
|
|
|
101
112
|
|
|
102
113
|
- **Provider lookup:** `email-provider-info` — given just an email address, returns the inferred IMAP/SMTP hosts and the provider's app-password / trusted-application / IMAP-enable requirement, deterministically from the provider map. Call this BEFORE soliciting a password so the requirement is relayed first; most providers reject the normal login password. Returns "unknown" (operator supplies hosts) or "unsupported" (provider blocks IMAP/SMTP, e.g. Tuta).
|
|
103
114
|
- **Setup:** `email-setup` — collect credentials in plain chat and connect IMAP/SMTP. Hosts are auto-inferred for known providers; supports alias addresses (`agentAddress`). For hosts where the client-visible Drafts folder is not the one flagged `\Drafts`, pass `draftsFolder` to name it explicitly (e.g. `INBOX.Drafts`).
|
|
115
|
+
- **Signature:** `email-signature-set` — install the account's signature into the three files `email-draft` already reads at the account root (`email-signature.txt`, `.html`, `.font`). Plain text and HTML are stored and appended byte-for-byte; `font` is one CSS font-family value that wraps the HTML body and has no plain-text effect. **A part you omit is removed**, so a text-only call clears a stale HTML signature rather than leaving it to keep being appended; pass every part you want to keep on each call. A call with no parts at all is refused rather than treated as "erase everything". Because the append is deterministic and happens on every draft, a signature must never be written into a draft body by hand — that duplicates the stored one and drifts from it. Pass the operator's real signature read from its source of truth; reconstructing markup from memory or from stripped graph text is what put a dead logo URL into a client draft. Account-scoped, no `mailbox` selector: one signature per account.
|
|
104
116
|
- **Read inbox:** `email-read` — metadata only (UID, sender, subject, date), plus a best-effort `Archive`/`Archive-Section` hint when the IMAP message's `receivedAt` falls inside a stored `:ConversationArchive {source:'email'}` section, plus an `Attachments (N): filename (mime, size); …` footer when the message carries enclosures. Supports pagination (`before_uid`), folders (`inbox`/`sent`), filtering by sender/date/subject. Metadata is derived from IMAP `bodyStructure` — no bytes downloaded.
|
|
105
117
|
- **Search inbox:** `email-search` — live IMAP search by sender, subject, body keyword, date range. Same per-archive hint and attachment footer as `email-read`.
|
|
106
118
|
- **Delete (move to Trash):** `email-delete` — move the messages named by `uids` (from a prior `email-read`/`email-search`, scoped to a source `folder` of `inbox` or `sent`) into the mailbox's Trash folder. **Recoverable, never an expunge** — the message leaves the inbox and permanent removal is left to the operator's own mail client or the provider's Trash auto-purge; this tool never permanently deletes. Trash is resolved per provider: the `\Trash` special-use folder if advertised, else the first existing of `Trash`, `[Gmail]/Trash`, `Deleted Items`, `Deleted Messages`. `Junk` is never treated as Trash. A mailbox with no resolvable Trash yields a typed error and moves nothing — there is no silent expunge fallback. A `uid` that is already gone is counted not-moved without failing the rest; the result reports `Moved N of M message(s) to <Trash>`. It deletes only the UIDs you name, never by search criteria.
|
|
@@ -110,6 +122,12 @@ Manages the agent's own dedicated email account — IMAP for reading, SMTP for s
|
|
|
110
122
|
- **Reply:** `email-reply` — threaded reply to an existing email by `messageId`. Resolves the original envelope via IMAP `SEARCH HEADER Message-ID` (INBOX then `\Sent`), so threading works for any message still present on the server even if the graph never ingested it. Supports `replyAll`, `cc`/`bcc`, and `attachments` (same path rules as `email-send`). Explicit `cc`/`bcc` are added in addition to whatever `replyAll` places in `To`, passed through as given (no overlap de-duplication against `To`).
|
|
111
123
|
- **Compose to Drafts:** `email-draft` — builds the same MIME message as `email-send` (same `cc`/`bcc`/`attachments` rules; unlike a dispatched send, the stored draft keeps its `Bcc:` header, because the draft bytes are the only recipient carrier) but APPENDs it to the mailbox's `\Drafts` special-use folder with the `\Draft` flag instead of dispatching it. Nothing is sent. Pass `messageId` to compose a threaded draft-reply: the parent is resolved via IMAP and the draft carries `In-Reply-To`/`References` plus a `Re:` subject from the parent, exactly like `email-reply` (the `subject` argument is then optional and ignored). The success message names the Drafts folder, the APPEND UID, and the draft's own `Message-ID` (`draft id`) so a saved draft is never indistinguishable from a silent no-op and a later edit has a stable handle. Fails naming the mailbox when the server advertises no `\Drafts` special-use folder — no fallback folder is picked, unless a per-account `draftsFolder` override is configured (via `email-setup`), which is honoured before special-use discovery for hosts whose client-visible Drafts is a differently-named mailbox. Use when the operator wants to review and send by hand.
|
|
112
124
|
- **Revise a draft:** `email-draft-edit` — replace a stored draft. The `targetUid` is a fast-path hint; the draft's own `Message-ID` (`draftMessageId`) is the stable identity, because Gmail renumbers a draft's UID when it is touched. The tool tries the UID first, then relocates by a `Message-ID` header search, then composes the replacement from the parameters (same shape as `email-draft`, plus `targetUid`, optional `draftMessageId`, and an optional `messageId` for a threaded revision), APPENDs it with `\Draft`, and deletes the resolved old copy — the replacement is always appended before the old copy is expunged, so a mid-cycle failure never leaves zero copies. When neither identity resolves a live draft, the prior draft is gone: the replacement is saved as a fresh draft (`created`) rather than failing. A single transient transport drop is retried on a fresh connection; auth and other permanent errors surface immediately. Nothing is sent. The confirmation names the folder, the new UID, and the new draft id.
|
|
125
|
+
- **Verify a draft:** `email-draft-list` — read the Drafts folder back and report what is in it. This is the only tool that can confirm a draft's presence: `email-status` reports connectivity and an inbox count, and `email-read`/`email-search` are inbox/sent only. Optional `uid` or `draftMessageId` narrows to one draft, but the plain listing is preferred, because neither handle is stable — Gmail renumbers a draft's Drafts UID when it is touched, and `email-draft-edit` mints a **new** draft `Message-ID` for every revision, so a handle from earlier in the conversation can miss a draft that is present. A narrowing miss therefore never returns bare: it names the miss, says why a stale handle misses, and lists the drafts that *are* in the folder, so "that handle is not here" can never be read as "no draft is here". Lists every message in the resolved folder, not only `\Draft`-flagged ones, because that is what the operator's own mail client shows and a `draftsFolder` override may point at a folder this plugin did not write. A draft id matching two rows is reported as two rows, not collapsed — the edit path leaves a second copy when an expunge is not confirmed. Nothing is written, sent or deleted.
|
|
126
|
+
|
|
127
|
+
**Confirm before asserting.** After `email-draft` or `email-draft-edit`, and whenever anyone reports a draft they cannot see, read the folder with `email-draft-list` before saying the draft is there. The save confirmation is evidence that the write succeeded, not evidence that the draft is in the folder now.
|
|
128
|
+
|
|
129
|
+
**Never state an unverified cause.** When the draft is confirmed present and the customer's client still does not show it, the honest report is exactly that: the draft is on the server, and the client-side reason is unknown. Do not offer a mail-app setting, a sync toggle or a provider option as the cause unless it has been verified. A successful IMAP write to and read of the Drafts folder proves the folder is exposed over IMAP, so provider "show in IMAP" settings are excluded by the evidence rather than being candidates for it.
|
|
130
|
+
|
|
113
131
|
- **Send a draft:** `email-draft-send` — dispatch a stored draft addressed by its Drafts UID via SMTP, then relocate it into `\Sent`. Recipients come from the draft's own To/Cc/Bcc headers (the `\Drafts` copy keeps its `Bcc:`), deduped, sent from the agent alias with an explicit SMTP envelope. The draft is relocated only after SMTP accepts; an SMTP failure leaves it in place and surfaces verbatim. A draft with no recipients is refused before sending. Operator-approved sends only.
|
|
114
132
|
- **The Sent copy is verified, not assumed.** All three send paths read `\Sent` back after SMTP acceptance and search it by Message-ID, then state what is actually there as a fact separate from the send. A folder and UID are named only when they were read back; "no copy was found", "this server advertises no `\Sent` folder", and "the Sent folder could not be checked" are reported as three distinct outcomes, never collapsed. A send whose copy cannot be located is still a successful send. Each send also appends its observed outcome to `config/email-sent-ledger.jsonl`, and the install heartbeat spawns `dist/scripts/sent-copy-sweep.js` hourly to ask `\Sent`, per mailbox, whether each Message-ID sent in the trailing 24 h is actually present (`[email-sent-sweep] op=reconcile …`). Presence is asked per Message-ID rather than by comparing folder totals, because `\Sent` also holds everything the owner sends from other clients. See [`references/email-reference.md`](references/email-reference.md).
|
|
115
133
|
- **Ingestion — fetch:** `email-fetch` — list new IMAP messages since the stored high-water mark. Metadata + body preview only — including attachment names, mimetypes, and sizes so the operator sees what's attached before approving. No bytes downloaded, no graph writes. Stages the batch in-memory for `email-ingest`.
|
|
@@ -136,6 +154,7 @@ Email ingestion is **operator-initiated and human-in-the-loop**. There is no bac
|
|
|
136
154
|
|
|
137
155
|
| Skill | Purpose |
|
|
138
156
|
|-------|---------|
|
|
157
|
+
| [email-signature](skills/email-signature/SKILL.md) | Setting or changing the account's email signature. Carries the loader convention (three files at the account root, verbatim text/html, trimmed font), the rule that a signature is installed only via `email-signature-set`, and the rule that the agent never hand-builds one inside a draft body. |
|
|
139
158
|
| [email-ingest](skills/email-ingest/SKILL.md) | Operator-initiated ingestion. Drives "check my emails" → list via `email-fetch` → summarise → ask the operator per message → write the approved subset via `email-ingest`. |
|
|
140
159
|
| [email-composition](skills/email-composition/SKILL.md) | Conversational wrapper for drafting replies, new emails, and inbox triage. Orchestrates the sending `email-*` tools with human-in-the-loop approval on every send; draft intent ("leave it in drafts", "don't send yet") routes to `email-draft`, threaded drafts pass `messageId`, and a stored draft is revised via `email-draft-edit` or sent via `email-draft-send`. |
|
|
141
160
|
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"email-draft-list-tool.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/email-draft-list-tool.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
2
|
+
// The tool's rendering is the unit under test, so the reader is stubbed at the
|
|
3
|
+
// module boundary. A wholesale vi.mock replaces the WHOLE module, so the factory
|
|
4
|
+
// must name every symbol email-draft-list.ts imports from it — MAX_LIMIT as well
|
|
5
|
+
// as listDrafts — or the import lands undefined.
|
|
6
|
+
const { listMock } = vi.hoisted(() => ({ listMock: vi.fn() }));
|
|
7
|
+
vi.mock("../lib/imap.js", () => ({
|
|
8
|
+
listDrafts: listMock,
|
|
9
|
+
MAX_LIMIT: 50,
|
|
10
|
+
}));
|
|
11
|
+
vi.mock("../lib/credentials.js", () => ({
|
|
12
|
+
loadCredentials: vi.fn(async () => ({
|
|
13
|
+
credentials: { config: { email: "m@x.com" }, password: "pw" },
|
|
14
|
+
})),
|
|
15
|
+
}));
|
|
16
|
+
const { emailDraftList } = await import("../tools/email-draft-list.js");
|
|
17
|
+
const run = (params = {}) => emailDraftList({ accountId: "acc-1", ...params });
|
|
18
|
+
const listing = (over) => ({
|
|
19
|
+
folder: "[Gmail]/Drafts",
|
|
20
|
+
mode: "census",
|
|
21
|
+
resolved: true,
|
|
22
|
+
rows: [],
|
|
23
|
+
total: 0,
|
|
24
|
+
...over,
|
|
25
|
+
});
|
|
26
|
+
const quote = {
|
|
27
|
+
uid: 28316,
|
|
28
|
+
subject: "Quote",
|
|
29
|
+
to: ["jonathan@apexgrange.com"],
|
|
30
|
+
messageId: "<id-283@x>",
|
|
31
|
+
date: new Date("2026-07-28T09:00:00Z"),
|
|
32
|
+
};
|
|
33
|
+
describe("emailDraftList rendering", () => {
|
|
34
|
+
beforeEach(() => vi.clearAllMocks());
|
|
35
|
+
it("renders a census with the folder and every row", async () => {
|
|
36
|
+
listMock.mockResolvedValue(listing({
|
|
37
|
+
total: 2,
|
|
38
|
+
rows: [
|
|
39
|
+
quote,
|
|
40
|
+
{ uid: 10, subject: "Older", to: ["a@x.com"], messageId: "<id-10@x>", date: null },
|
|
41
|
+
],
|
|
42
|
+
}));
|
|
43
|
+
const text = await run();
|
|
44
|
+
expect(text).toContain("[Gmail]/Drafts");
|
|
45
|
+
expect(text).toContain("UID: 28316");
|
|
46
|
+
expect(text).toContain("jonathan@apexgrange.com");
|
|
47
|
+
expect(text).toContain("<id-283@x>");
|
|
48
|
+
expect(text).toContain("UID: 10");
|
|
49
|
+
});
|
|
50
|
+
it("an empty folder says so and is not an error", async () => {
|
|
51
|
+
listMock.mockResolvedValue(listing({ total: 0, rows: [] }));
|
|
52
|
+
const text = await run();
|
|
53
|
+
expect(text).toMatch(/No drafts in \[Gmail\]\/Drafts/);
|
|
54
|
+
});
|
|
55
|
+
it("a missed handle names the miss AND lists what is present", async () => {
|
|
56
|
+
listMock.mockResolvedValue(listing({ mode: "message-id", resolved: false, total: 1, rows: [quote] }));
|
|
57
|
+
const text = await run({ draftMessageId: "stale@x" });
|
|
58
|
+
expect(text).toContain("not found");
|
|
59
|
+
expect(text).toContain("UID: 28316");
|
|
60
|
+
expect(text).toMatch(/change when the draft is edited/);
|
|
61
|
+
});
|
|
62
|
+
it("a missed handle on a genuinely empty folder does not claim drafts exist", async () => {
|
|
63
|
+
listMock.mockResolvedValue(listing({ mode: "uid", resolved: false, total: 0, rows: [] }));
|
|
64
|
+
const text = await run({ uid: 999 });
|
|
65
|
+
expect(text).toContain("not found");
|
|
66
|
+
expect(text).toMatch(/no drafts at all/);
|
|
67
|
+
});
|
|
68
|
+
it("a confirmed hit reports the folder total alongside the draft", async () => {
|
|
69
|
+
listMock.mockResolvedValue(listing({ mode: "uid", resolved: true, total: 3, rows: [quote] }));
|
|
70
|
+
const text = await run({ uid: 28316 });
|
|
71
|
+
expect(text).toMatch(/Confirmed in \[Gmail\]\/Drafts/);
|
|
72
|
+
expect(text).toContain("3 drafts");
|
|
73
|
+
expect(text).toContain("UID: 28316");
|
|
74
|
+
});
|
|
75
|
+
it("names the duplicate-copy state when one draft id matches twice", async () => {
|
|
76
|
+
listMock.mockResolvedValue(listing({
|
|
77
|
+
mode: "message-id",
|
|
78
|
+
resolved: true,
|
|
79
|
+
total: 2,
|
|
80
|
+
rows: [quote, { ...quote, uid: 28317 }],
|
|
81
|
+
}));
|
|
82
|
+
const text = await run({ draftMessageId: "id-283@x" });
|
|
83
|
+
expect(text).toMatch(/2 copies match/);
|
|
84
|
+
expect(text).toMatch(/expunge is not confirmed/);
|
|
85
|
+
});
|
|
86
|
+
it("a miss over a truncated census states its window too", async () => {
|
|
87
|
+
listMock.mockResolvedValue(listing({
|
|
88
|
+
mode: "uid",
|
|
89
|
+
resolved: false,
|
|
90
|
+
total: 90,
|
|
91
|
+
rows: Array.from({ length: 20 }, (_, i) => ({
|
|
92
|
+
uid: i + 1,
|
|
93
|
+
subject: "s",
|
|
94
|
+
to: [],
|
|
95
|
+
messageId: null,
|
|
96
|
+
date: null,
|
|
97
|
+
})),
|
|
98
|
+
}));
|
|
99
|
+
const text = await run({ uid: 999 });
|
|
100
|
+
expect(text).toMatch(/holds 90 drafts; showing the newest 20/);
|
|
101
|
+
});
|
|
102
|
+
it("names both handles when both were tried", async () => {
|
|
103
|
+
listMock.mockResolvedValue(listing({ mode: "uid", resolved: false, total: 1, rows: [quote] }));
|
|
104
|
+
const text = await run({ uid: 999, draftMessageId: "gone@x" });
|
|
105
|
+
expect(text).toContain("UID 999");
|
|
106
|
+
expect(text).toContain("draft id gone@x");
|
|
107
|
+
});
|
|
108
|
+
it("truncation is stated and never mentions next_before_uid", async () => {
|
|
109
|
+
listMock.mockResolvedValue(listing({
|
|
110
|
+
folder: "Drafts",
|
|
111
|
+
total: 90,
|
|
112
|
+
rows: Array.from({ length: 50 }, (_, i) => ({
|
|
113
|
+
uid: i + 1,
|
|
114
|
+
subject: "s",
|
|
115
|
+
to: [],
|
|
116
|
+
messageId: null,
|
|
117
|
+
date: null,
|
|
118
|
+
})),
|
|
119
|
+
}));
|
|
120
|
+
const text = await run({ limit: 500 });
|
|
121
|
+
expect(text).toMatch(/newest 50 of 90/);
|
|
122
|
+
expect(text).not.toContain("next_before_uid");
|
|
123
|
+
});
|
|
124
|
+
it("clamps limit to MAX_LIMIT and floors it at 1", async () => {
|
|
125
|
+
listMock.mockResolvedValue(listing({}));
|
|
126
|
+
await run({ limit: 500 });
|
|
127
|
+
expect(listMock).toHaveBeenCalledWith(expect.anything(), "pw", expect.objectContaining({ limit: 50 }));
|
|
128
|
+
await run({ limit: 0 });
|
|
129
|
+
expect(listMock).toHaveBeenLastCalledWith(expect.anything(), "pw", expect.objectContaining({ limit: 1 }));
|
|
130
|
+
});
|
|
131
|
+
it("defaults the limit to 20 when none is given", async () => {
|
|
132
|
+
listMock.mockResolvedValue(listing({}));
|
|
133
|
+
await run();
|
|
134
|
+
expect(listMock).toHaveBeenCalledWith(expect.anything(), "pw", expect.objectContaining({ limit: 20 }));
|
|
135
|
+
});
|
|
136
|
+
it("says the listing is flag-agnostic so an operator can reconcile against their client", async () => {
|
|
137
|
+
listMock.mockResolvedValue(listing({ total: 1, rows: [quote] }));
|
|
138
|
+
const text = await run();
|
|
139
|
+
expect(text).toMatch(/not only \\Draft-flagged/);
|
|
140
|
+
});
|
|
141
|
+
});
|
|
142
|
+
//# sourceMappingURL=email-draft-list-tool.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"email-draft-list-tool.test.js","sourceRoot":"","sources":["../../src/__tests__/email-draft-list-tool.test.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"email-draft-list.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/email-draft-list.test.ts"],"names":[],"mappings":""}
|