@rubytech/create-maxy-code 0.1.563 → 0.1.565
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/package.json +1 -1
- package/payload/platform/plugins/admin/PLUGIN.md +2 -2
- package/payload/platform/plugins/admin/hooks/__tests__/askuserquestion-channel-carrier-gate.test.sh +1 -1
- package/payload/platform/plugins/admin/hooks/askuserquestion-channel-carrier-gate.sh +4 -3
- package/payload/platform/plugins/admin/skills/platform-architecture/SKILL.md +27 -4
- package/payload/platform/plugins/admin/skills/whats-new/SKILL.md +12 -0
- package/payload/platform/plugins/cloudflare/mcp/__tests__/portal-visit-status-write.test.ts +118 -0
- package/payload/platform/plugins/cloudflare/skills/data-portal/SKILL.md +15 -0
- package/payload/platform/plugins/cloudflare/skills/data-portal/schema.sql +25 -7
- package/payload/platform/plugins/cloudflare/skills/data-portal/template/functions/api/visit-status.ts +62 -5
- package/payload/platform/plugins/docs/references/outlook-guide.md +7 -3
- package/payload/platform/plugins/docs/references/telegram-guide.md +19 -0
- package/payload/platform/plugins/memory/references/schema-construction.md +1 -1
- package/payload/platform/plugins/outlook/PLUGIN.md +2 -2
- package/payload/platform/plugins/outlook/mcp/dist/__tests__/attach.test.js +1 -1
- package/payload/platform/plugins/outlook/mcp/dist/__tests__/attach.test.js.map +1 -1
- package/payload/platform/plugins/outlook/mcp/dist/__tests__/calendar-write.test.js +1 -1
- package/payload/platform/plugins/outlook/mcp/dist/__tests__/calendar-write.test.js.map +1 -1
- package/payload/platform/plugins/outlook/mcp/dist/__tests__/draft-attachments.test.js +1 -1
- package/payload/platform/plugins/outlook/mcp/dist/__tests__/draft-attachments.test.js.map +1 -1
- package/payload/platform/plugins/outlook/mcp/dist/__tests__/draft-edit-attachments.test.js +1 -1
- package/payload/platform/plugins/outlook/mcp/dist/__tests__/draft-edit-attachments.test.js.map +1 -1
- package/payload/platform/plugins/outlook/mcp/dist/__tests__/draft-edit-removal.test.js +1 -1
- package/payload/platform/plugins/outlook/mcp/dist/__tests__/draft-edit-removal.test.js.map +1 -1
- package/payload/platform/plugins/outlook/mcp/dist/__tests__/draft-edit.test.js +1 -1
- package/payload/platform/plugins/outlook/mcp/dist/__tests__/draft-edit.test.js.map +1 -1
- package/payload/platform/plugins/outlook/mcp/dist/__tests__/draft-threaded.test.js +51 -1
- package/payload/platform/plugins/outlook/mcp/dist/__tests__/draft-threaded.test.js.map +1 -1
- package/payload/platform/plugins/outlook/mcp/dist/__tests__/graph-client-paged.test.js +1 -1
- package/payload/platform/plugins/outlook/mcp/dist/__tests__/graph-client-paged.test.js.map +1 -1
- package/payload/platform/plugins/outlook/mcp/dist/__tests__/graph-client-prefix-wiring.test.js +1 -0
- package/payload/platform/plugins/outlook/mcp/dist/__tests__/graph-client-prefix-wiring.test.js.map +1 -1
- package/payload/platform/plugins/outlook/mcp/dist/__tests__/graph-client-retry.test.js +2 -2
- package/payload/platform/plugins/outlook/mcp/dist/__tests__/graph-client-retry.test.js.map +1 -1
- package/payload/platform/plugins/outlook/mcp/dist/__tests__/graph-client.test.js +7 -7
- package/payload/platform/plugins/outlook/mcp/dist/__tests__/graph-client.test.js.map +1 -1
- package/payload/platform/plugins/outlook/mcp/dist/__tests__/has-attachments.test.js +1 -1
- package/payload/platform/plugins/outlook/mcp/dist/__tests__/has-attachments.test.js.map +1 -1
- package/payload/platform/plugins/outlook/mcp/dist/__tests__/mail-attachment.test.js +1 -1
- package/payload/platform/plugins/outlook/mcp/dist/__tests__/mail-attachment.test.js.map +1 -1
- package/payload/platform/plugins/outlook/mcp/dist/__tests__/mail-delete.test.js +1 -1
- package/payload/platform/plugins/outlook/mcp/dist/__tests__/mail-delete.test.js.map +1 -1
- package/payload/platform/plugins/outlook/mcp/dist/__tests__/mail-fetch-body.test.js +1 -1
- package/payload/platform/plugins/outlook/mcp/dist/__tests__/mail-fetch-body.test.js.map +1 -1
- package/payload/platform/plugins/outlook/mcp/dist/__tests__/mail-list-paging.test.js +1 -1
- package/payload/platform/plugins/outlook/mcp/dist/__tests__/mail-list-paging.test.js.map +1 -1
- package/payload/platform/plugins/outlook/mcp/dist/__tests__/mail-otp-extract.test.js +1 -1
- package/payload/platform/plugins/outlook/mcp/dist/__tests__/mail-otp-extract.test.js.map +1 -1
- package/payload/platform/plugins/outlook/mcp/dist/__tests__/mail-reply.test.js +159 -6
- package/payload/platform/plugins/outlook/mcp/dist/__tests__/mail-reply.test.js.map +1 -1
- package/payload/platform/plugins/outlook/mcp/dist/__tests__/mail-search-delegated-ids.test.js +1 -1
- package/payload/platform/plugins/outlook/mcp/dist/__tests__/mail-search-delegated-ids.test.js.map +1 -1
- package/payload/platform/plugins/outlook/mcp/dist/__tests__/mail-search-paging.test.js +1 -1
- package/payload/platform/plugins/outlook/mcp/dist/__tests__/mail-search-paging.test.js.map +1 -1
- package/payload/platform/plugins/outlook/mcp/dist/__tests__/mail-send-attachments.test.js +1 -1
- package/payload/platform/plugins/outlook/mcp/dist/__tests__/mail-send-attachments.test.js.map +1 -1
- package/payload/platform/plugins/outlook/mcp/dist/__tests__/shared-mail-only-guard.test.js +1 -0
- package/payload/platform/plugins/outlook/mcp/dist/__tests__/shared-mail-only-guard.test.js.map +1 -1
- package/payload/platform/plugins/outlook/mcp/dist/__tests__/shared-mailbox-add.test.js +1 -1
- package/payload/platform/plugins/outlook/mcp/dist/__tests__/shared-mailbox-add.test.js.map +1 -1
- package/payload/platform/plugins/outlook/mcp/dist/__tests__/write-tools.test.js +2 -1
- package/payload/platform/plugins/outlook/mcp/dist/__tests__/write-tools.test.js.map +1 -1
- package/payload/platform/plugins/outlook/mcp/dist/index.js +13 -4
- package/payload/platform/plugins/outlook/mcp/dist/index.js.map +1 -1
- package/payload/platform/plugins/outlook/mcp/dist/lib/graph-client.d.ts +9 -0
- package/payload/platform/plugins/outlook/mcp/dist/lib/graph-client.d.ts.map +1 -1
- package/payload/platform/plugins/outlook/mcp/dist/lib/graph-client.js.map +1 -1
- package/payload/platform/plugins/outlook/mcp/dist/tools/draft.d.ts +7 -0
- package/payload/platform/plugins/outlook/mcp/dist/tools/draft.d.ts.map +1 -1
- package/payload/platform/plugins/outlook/mcp/dist/tools/draft.js +16 -5
- package/payload/platform/plugins/outlook/mcp/dist/tools/draft.js.map +1 -1
- package/payload/platform/plugins/outlook/mcp/dist/tools/mail-reply.d.ts +5 -0
- package/payload/platform/plugins/outlook/mcp/dist/tools/mail-reply.d.ts.map +1 -1
- package/payload/platform/plugins/outlook/mcp/dist/tools/mail-reply.js +81 -2
- package/payload/platform/plugins/outlook/mcp/dist/tools/mail-reply.js.map +1 -1
- package/payload/platform/plugins/whatsapp/PLUGIN.md +2 -2
- package/payload/platform/plugins/whatsapp/references/channels-whatsapp.md +2 -2
- package/payload/platform/scripts/check-no-esm-require.mjs +10 -0
- package/payload/platform/services/claude-session-manager/dist/channel-mcp.d.ts.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/channel-mcp.js +9 -3
- package/payload/platform/services/claude-session-manager/dist/channel-mcp.js.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/http-server.d.ts.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/http-server.js +11 -2
- package/payload/platform/services/claude-session-manager/dist/http-server.js.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/index.js +0 -1
- package/payload/platform/services/claude-session-manager/dist/index.js.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/pty-spawner.d.ts +0 -1
- package/payload/platform/services/claude-session-manager/dist/pty-spawner.d.ts.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/pty-spawner.js +7 -2
- package/payload/platform/services/claude-session-manager/dist/pty-spawner.js.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/public-allowlist.d.ts +4 -2
- package/payload/platform/services/claude-session-manager/dist/public-allowlist.d.ts.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/public-allowlist.js +15 -18
- package/payload/platform/services/claude-session-manager/dist/public-allowlist.js.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/specialist-roster.d.ts +4 -2
- package/payload/platform/services/claude-session-manager/dist/specialist-roster.d.ts.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/specialist-roster.js +4 -2
- package/payload/platform/services/claude-session-manager/dist/specialist-roster.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 +31 -16
- package/payload/platform/services/telegram-channel/dist/instructions.js.map +1 -1
- package/payload/platform/services/telegram-channel/dist/server.d.ts.map +1 -1
- package/payload/platform/services/telegram-channel/dist/server.js +21 -38
- package/payload/platform/services/telegram-channel/dist/server.js.map +1 -1
- package/payload/platform/services/telegram-channel/dist/turn-follow.d.ts +6 -0
- package/payload/platform/services/telegram-channel/dist/turn-follow.d.ts.map +1 -1
- package/payload/platform/services/telegram-channel/dist/turn-follow.js +16 -1
- package/payload/platform/services/telegram-channel/dist/turn-follow.js.map +1 -1
- package/payload/platform/services/webchat-channel/dist/instructions.d.ts +0 -2
- package/payload/platform/services/webchat-channel/dist/instructions.d.ts.map +1 -1
- package/payload/platform/services/webchat-channel/dist/instructions.js +25 -19
- package/payload/platform/services/webchat-channel/dist/instructions.js.map +1 -1
- package/payload/platform/services/webchat-channel/dist/notification.d.ts +3 -2
- package/payload/platform/services/webchat-channel/dist/notification.d.ts.map +1 -1
- package/payload/platform/services/webchat-channel/dist/notification.js +14 -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 +16 -48
- package/payload/platform/services/webchat-channel/dist/server.js.map +1 -1
- package/payload/platform/services/webchat-channel/dist/turn-follow.d.ts +6 -0
- package/payload/platform/services/webchat-channel/dist/turn-follow.d.ts.map +1 -1
- package/payload/platform/services/webchat-channel/dist/turn-follow.js +16 -1
- package/payload/platform/services/webchat-channel/dist/turn-follow.js.map +1 -1
- package/payload/platform/services/whatsapp-channel/dist/notification.d.ts +5 -2
- package/payload/platform/services/whatsapp-channel/dist/notification.d.ts.map +1 -1
- package/payload/platform/services/whatsapp-channel/dist/notification.js +31 -14
- 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 +64 -161
- package/payload/platform/services/whatsapp-channel/dist/server.js.map +1 -1
- package/payload/platform/services/whatsapp-channel/dist/targets.d.ts +7 -38
- package/payload/platform/services/whatsapp-channel/dist/targets.d.ts.map +1 -1
- package/payload/platform/services/whatsapp-channel/dist/targets.js +7 -51
- package/payload/platform/services/whatsapp-channel/dist/targets.js.map +1 -1
- package/payload/platform/services/whatsapp-channel/dist/turn-follow.d.ts +6 -0
- package/payload/platform/services/whatsapp-channel/dist/turn-follow.d.ts.map +1 -1
- package/payload/platform/services/whatsapp-channel/dist/turn-follow.js +21 -1
- package/payload/platform/services/whatsapp-channel/dist/turn-follow.js.map +1 -1
- package/payload/server/{adminuser-self-heal-ZHFNDKJK.js → adminuser-self-heal-Y4U3UNKF.js} +10 -2
- package/payload/server/{chunk-G6ULACHG.js → chunk-637C2FGE.js} +3385 -3275
- package/payload/server/{chunk-HYQNUVGO.js → chunk-KVR2FT6O.js} +12 -1
- package/payload/server/{chunk-SKYZG6OT.js → chunk-PGGGWUBM.js} +372 -224
- package/payload/server/{chunk-JOVREC6E.js → chunk-Y4NH2KLF.js} +3789 -3484
- package/payload/server/{manager-2VEJKVEO.js → manager-R3XH3BNM.js} +3 -3
- package/payload/server/maxy-edge.js +27 -4
- package/payload/server/public/activity.html +5 -5
- package/payload/server/public/agents.html +4 -4
- package/payload/server/public/assets/{AdminLoginScreens-C6e5jf0S.js → AdminLoginScreens-5gI137xF.js} +1 -1
- package/payload/server/public/assets/AdminLoginScreens-5gI137xF.js.br +0 -0
- package/payload/server/public/assets/AdminLoginScreens-5gI137xF.js.gz +0 -0
- package/payload/server/public/assets/{AdminShell-DStA0lfm.js → AdminShell-Bfapavlp.js} +2 -2
- package/payload/server/public/assets/AdminShell-Bfapavlp.js.br +0 -0
- package/payload/server/public/assets/AdminShell-Bfapavlp.js.gz +0 -0
- package/payload/server/public/assets/{activity--yyBcNfB.js → activity-DnbsmUgn.js} +1 -1
- package/payload/server/public/assets/activity-DnbsmUgn.js.br +0 -0
- package/payload/server/public/assets/activity-DnbsmUgn.js.gz +0 -0
- package/payload/server/public/assets/admin-BXIv0_oj.js +1 -0
- package/payload/server/public/assets/admin-BXIv0_oj.js.br +0 -0
- package/payload/server/public/assets/admin-BXIv0_oj.js.gz +0 -0
- package/payload/server/public/assets/{agents-odyVOb5E.js → agents-Dl2wOcpj.js} +1 -1
- package/payload/server/public/assets/agents-Dl2wOcpj.js.br +0 -0
- package/payload/server/public/assets/agents-Dl2wOcpj.js.gz +0 -0
- package/payload/server/public/assets/{browser-D1K_MoHt.js → browser-D-Uc9ewO.js} +1 -1
- package/payload/server/public/assets/browser-D-Uc9ewO.js.br +0 -0
- package/payload/server/public/assets/browser-D-Uc9ewO.js.gz +0 -0
- package/payload/server/public/assets/{calendar-jqh9U7Ic.js → calendar-Cu3O6Bay.js} +1 -1
- package/payload/server/public/assets/calendar-Cu3O6Bay.js.br +0 -0
- package/payload/server/public/assets/calendar-Cu3O6Bay.js.gz +0 -0
- package/payload/server/public/assets/chat-Bso94e-f.js +1 -0
- package/payload/server/public/assets/chat-Bso94e-f.js.br +0 -0
- package/payload/server/public/assets/chat-Bso94e-f.js.gz +0 -0
- package/payload/server/public/assets/chevron-left-CDwfR9V-.js +1 -0
- package/payload/server/public/assets/chevron-left-CDwfR9V-.js.br +0 -0
- package/payload/server/public/assets/clock-DDVrNLt9.js +1 -0
- package/payload/server/public/assets/clock-DDVrNLt9.js.br +0 -0
- package/payload/server/public/assets/clock-DDVrNLt9.js.gz +0 -0
- package/payload/server/public/assets/{copy-DkuHwkaB.js → copy-DPBVV8KZ.js} +1 -1
- package/payload/server/public/assets/copy-DPBVV8KZ.js.br +0 -0
- package/payload/server/public/assets/copy-DPBVV8KZ.js.gz +0 -0
- package/payload/server/public/assets/data-Dei8n2EX.js +1 -0
- package/payload/server/public/assets/data-Dei8n2EX.js.br +0 -0
- package/payload/server/public/assets/data-Dei8n2EX.js.gz +0 -0
- package/payload/server/public/assets/{file-text-C6o_NRjj.js → file-text-Dn82O6ke.js} +1 -1
- package/payload/server/public/assets/file-text-Dn82O6ke.js.br +0 -0
- package/payload/server/public/assets/file-text-Dn82O6ke.js.gz +0 -0
- package/payload/server/public/assets/{graph-BN30v5hJ.js → graph-D2J7a7Ll.js} +1 -1
- package/payload/server/public/assets/graph-D2J7a7Ll.js.br +0 -0
- package/payload/server/public/assets/{graph-BN30v5hJ.js.gz → graph-D2J7a7Ll.js.gz} +0 -0
- package/payload/server/public/assets/{graph-labels-DHFyNXQt.js → graph-labels-D3gkY2IH.js} +1 -1
- package/payload/server/public/assets/graph-labels-D3gkY2IH.js.br +0 -0
- package/payload/server/public/assets/graph-labels-D3gkY2IH.js.gz +0 -0
- package/payload/server/public/assets/{operator-DwFYA4Fp.js → operator-Bz_yj1im.js} +1 -1
- package/payload/server/public/assets/operator-Bz_yj1im.js.br +0 -0
- package/payload/server/public/assets/operator-Bz_yj1im.js.gz +0 -0
- package/payload/server/public/assets/page-D6u2xYh7.js +1 -0
- package/payload/server/public/assets/page-D6u2xYh7.js.br +0 -0
- package/payload/server/public/assets/page-D6u2xYh7.js.gz +0 -0
- package/payload/server/public/assets/{page-CEWIP3VF.js → page-mH4nyQ75.js} +2 -2
- package/payload/server/public/assets/page-mH4nyQ75.js.br +0 -0
- package/payload/server/public/assets/page-mH4nyQ75.js.gz +0 -0
- package/payload/server/public/assets/play-ByuChQJJ.js +1 -0
- package/payload/server/public/assets/play-ByuChQJJ.js.br +0 -0
- package/payload/server/public/assets/play-ByuChQJJ.js.gz +0 -0
- package/payload/server/public/assets/{public-Ceb0n7Km.js → public-CtoYVjPR.js} +1 -1
- package/payload/server/public/assets/public-CtoYVjPR.js.br +0 -0
- package/payload/server/public/assets/public-CtoYVjPR.js.gz +0 -0
- package/payload/server/public/assets/{rotate-ccw-CDYdx91V.js → rotate-ccw-BXoIghE3.js} +1 -1
- package/payload/server/public/assets/rotate-ccw-BXoIghE3.js.br +0 -0
- package/payload/server/public/assets/rotate-ccw-BXoIghE3.js.gz +0 -0
- package/payload/server/public/assets/{routines-DwNTih0y.js → routines-DXleYpDv.js} +1 -1
- package/payload/server/public/assets/routines-DXleYpDv.js.br +0 -0
- package/payload/server/public/assets/routines-DXleYpDv.js.gz +0 -0
- package/payload/server/public/assets/{skills-TIDCdOLQ.js → skills-CMl38MKH.js} +1 -1
- package/payload/server/public/assets/skills-CMl38MKH.js.br +0 -0
- package/payload/server/public/assets/skills-CMl38MKH.js.gz +0 -0
- package/payload/server/public/assets/{tasks-eTXvaWnd.js → tasks-Cshaf0Md.js} +1 -1
- package/payload/server/public/assets/tasks-Cshaf0Md.js.br +0 -0
- package/payload/server/public/assets/tasks-Cshaf0Md.js.gz +0 -0
- package/payload/server/public/assets/{triangle-alert-JwLSrj17.js → triangle-alert-CS-NXr_9.js} +1 -1
- package/payload/server/public/assets/triangle-alert-CS-NXr_9.js.br +3 -0
- package/payload/server/public/assets/triangle-alert-CS-NXr_9.js.gz +0 -0
- package/payload/server/public/assets/{useCopyFeedback-DjTkNw2p.js → useCopyFeedback-pKLfQJNG.js} +1 -1
- package/payload/server/public/assets/useCopyFeedback-pKLfQJNG.js.br +0 -0
- package/payload/server/public/assets/useCopyFeedback-pKLfQJNG.js.gz +0 -0
- package/payload/server/public/assets/useMediaQuery-BHFn0-S9.css +1 -0
- package/payload/server/public/assets/useMediaQuery-BHFn0-S9.css.br +0 -0
- package/payload/server/public/assets/useMediaQuery-BHFn0-S9.css.gz +0 -0
- package/payload/server/public/assets/{useMediaQuery-D8VDA0RN.js → useMediaQuery-oDCasnxD.js} +1 -1
- package/payload/server/public/assets/useMediaQuery-oDCasnxD.js.br +0 -0
- package/payload/server/public/assets/{useMediaQuery-D8VDA0RN.js.gz → useMediaQuery-oDCasnxD.js.gz} +0 -0
- package/payload/server/public/assets/useVoiceRecorder-EgO1GmIg.js +2 -0
- package/payload/server/public/assets/useVoiceRecorder-EgO1GmIg.js.br +0 -0
- package/payload/server/public/assets/useVoiceRecorder-EgO1GmIg.js.gz +0 -0
- package/payload/server/public/assets/{wrench-DutUiRu7.js → wrench-czES9Rdy.js} +1 -1
- package/payload/server/public/assets/wrench-czES9Rdy.js.br +0 -0
- package/payload/server/public/assets/wrench-czES9Rdy.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 +13 -13
- package/payload/server/public/operator.html +14 -14
- package/payload/server/public/public.html +13 -13
- package/payload/server/public/routines.html +7 -7
- package/payload/server/public/skills.html +5 -5
- package/payload/server/public/tasks.html +6 -6
- package/payload/server/server.js +10213 -8397
- package/payload/server/{src-MM64CYYZ.js → src-IJCLNFYU.js} +1 -1
- package/payload/server/{userprofile-reconcile-TL4JKBPG.js → userprofile-reconcile-PUA657YX.js} +8 -1
- package/payload/server/public/assets/AdminLoginScreens-C6e5jf0S.js.br +0 -0
- package/payload/server/public/assets/AdminLoginScreens-C6e5jf0S.js.gz +0 -0
- package/payload/server/public/assets/AdminShell-DStA0lfm.js.br +0 -0
- package/payload/server/public/assets/AdminShell-DStA0lfm.js.gz +0 -0
- package/payload/server/public/assets/activity--yyBcNfB.js.br +0 -0
- package/payload/server/public/assets/activity--yyBcNfB.js.gz +0 -0
- package/payload/server/public/assets/admin-Dr6Jwe7W.js +0 -1
- package/payload/server/public/assets/admin-Dr6Jwe7W.js.br +0 -0
- package/payload/server/public/assets/admin-Dr6Jwe7W.js.gz +0 -0
- package/payload/server/public/assets/agents-odyVOb5E.js.br +0 -0
- package/payload/server/public/assets/agents-odyVOb5E.js.gz +0 -0
- package/payload/server/public/assets/browser-D1K_MoHt.js.br +0 -0
- package/payload/server/public/assets/browser-D1K_MoHt.js.gz +0 -0
- package/payload/server/public/assets/calendar-jqh9U7Ic.js.br +0 -0
- package/payload/server/public/assets/calendar-jqh9U7Ic.js.gz +0 -0
- package/payload/server/public/assets/chat-QQHBdVor.js +0 -1
- package/payload/server/public/assets/chat-QQHBdVor.js.br +0 -0
- package/payload/server/public/assets/chat-QQHBdVor.js.gz +0 -0
- package/payload/server/public/assets/chevron-left-DwTXgKwr.js +0 -1
- package/payload/server/public/assets/clock-DOmh9iKX.js +0 -1
- package/payload/server/public/assets/clock-DOmh9iKX.js.br +0 -0
- package/payload/server/public/assets/copy-DkuHwkaB.js.br +0 -0
- package/payload/server/public/assets/copy-DkuHwkaB.js.gz +0 -0
- package/payload/server/public/assets/data-DYmTNqrb.js +0 -1
- package/payload/server/public/assets/data-DYmTNqrb.js.br +0 -0
- package/payload/server/public/assets/data-DYmTNqrb.js.gz +0 -0
- package/payload/server/public/assets/file-text-C6o_NRjj.js.br +0 -0
- package/payload/server/public/assets/file-text-C6o_NRjj.js.gz +0 -0
- package/payload/server/public/assets/graph-BN30v5hJ.js.br +0 -0
- package/payload/server/public/assets/graph-labels-DHFyNXQt.js.br +0 -0
- package/payload/server/public/assets/graph-labels-DHFyNXQt.js.gz +0 -0
- package/payload/server/public/assets/operator-DwFYA4Fp.js.br +0 -0
- package/payload/server/public/assets/operator-DwFYA4Fp.js.gz +0 -0
- package/payload/server/public/assets/page-BALVRssF.js +0 -1
- package/payload/server/public/assets/page-BALVRssF.js.br +0 -0
- package/payload/server/public/assets/page-BALVRssF.js.gz +0 -0
- package/payload/server/public/assets/page-CEWIP3VF.js.br +0 -0
- package/payload/server/public/assets/page-CEWIP3VF.js.gz +0 -0
- package/payload/server/public/assets/play-Bg-BY_oj.js +0 -1
- package/payload/server/public/assets/play-Bg-BY_oj.js.br +0 -0
- package/payload/server/public/assets/play-Bg-BY_oj.js.gz +0 -0
- package/payload/server/public/assets/public-Ceb0n7Km.js.br +0 -0
- package/payload/server/public/assets/public-Ceb0n7Km.js.gz +0 -0
- package/payload/server/public/assets/rotate-ccw-CDYdx91V.js.gz +0 -0
- package/payload/server/public/assets/routines-DwNTih0y.js.br +0 -0
- package/payload/server/public/assets/routines-DwNTih0y.js.gz +0 -0
- package/payload/server/public/assets/skills-TIDCdOLQ.js.br +0 -0
- package/payload/server/public/assets/skills-TIDCdOLQ.js.gz +0 -0
- package/payload/server/public/assets/tasks-eTXvaWnd.js.br +0 -0
- package/payload/server/public/assets/tasks-eTXvaWnd.js.gz +0 -0
- package/payload/server/public/assets/triangle-alert-JwLSrj17.js.br +0 -0
- package/payload/server/public/assets/triangle-alert-JwLSrj17.js.gz +0 -0
- package/payload/server/public/assets/useCopyFeedback-DjTkNw2p.js.br +0 -0
- package/payload/server/public/assets/useCopyFeedback-DjTkNw2p.js.gz +0 -0
- package/payload/server/public/assets/useMediaQuery-BsBzbWaf.css +0 -1
- package/payload/server/public/assets/useMediaQuery-BsBzbWaf.css.br +0 -0
- package/payload/server/public/assets/useMediaQuery-BsBzbWaf.css.gz +0 -0
- package/payload/server/public/assets/useMediaQuery-D8VDA0RN.js.br +0 -0
- package/payload/server/public/assets/useVoiceRecorder-B8vxObZG.js +0 -2
- package/payload/server/public/assets/useVoiceRecorder-B8vxObZG.js.br +0 -0
- package/payload/server/public/assets/useVoiceRecorder-B8vxObZG.js.gz +0 -0
- package/payload/server/public/assets/wrench-DutUiRu7.js.br +0 -0
- package/payload/server/public/assets/wrench-DutUiRu7.js.gz +0 -0
package/package.json
CHANGED
|
@@ -274,8 +274,8 @@ Tools are available via the `admin` MCP server.
|
|
|
274
274
|
- `hooks/quote-render-pdf-conformance.sh` — **PostToolUse matcher=`mcp__plugin_browser_browser__browser-pdf-save`.** The PDF-time companion to the PreToolUse `quote-render-gate`: the HTML gates run inside `quote-render` before the receipt, but the two PDF-only conformance checks cannot, because the PDF does not exist until `browser-pdf-save` runs. For a gated client-quote PDF (`memory/users/<phone>/documents/quote-<jobId>.pdf` or `…-breakdown.pdf`) whose render receipt names the source HTML, it blocks (exit 2) when the printed PDF's `/URI` annotation count is below the source HTML's absolute-link count (a rasterised or flattened print that dropped links) or when an embedded `https://` link does not answer 200 (a Sign-Online link a pending redeploy would 404). The offline `/URI`-count check runs first and short-circuits, so a link-loss regression never depends on the network. **Block message:** `Blocked: the printed quote PDF is missing links the document declared, or an embedded link is not live (…). Do not send it. If a Sign-Online link is not live yet, deploy the signing page first; then re-render and re-print so the PDF carries every link the document declares.` **Log line** (stderr, on a block): `[quote-render] op=bypass jobId=<jobId> reason=pdf-link-conformance detail=<uri-count …|link <url> -> <code>>`. Every non-quote PDF, and any call with no receipt (the PreToolUse gate owns that block), passes at exit 0. **Fail-open** on an uninspectable call (tty / empty stdin / no `python3` / receipt or PDF absent). **Reach on upgrade:** `provision-account-dir.sh` writes this PostToolUse matcher only at account-provision time, so accounts provisioned before it landed keep a stale matcher and need a settings backfill (tracked as a follow-up task), the same reach limit the AskUserQuestion carrier gate hit.
|
|
275
275
|
- `hooks/webfetch-preflight.mjs` — short-circuits WebFetch on JS-SPA shells with a structured `WEBFETCH_CANNOT_READ_JS_SPA` error so the agent surfaces a loud failure to the owner instead of paying the 60s extraction timeout. Fail-open on any internal error.
|
|
276
276
|
- `hooks/askuserquestion-investigate-gate.sh` — PreToolUse matcher=`AskUserQuestion`. Blocks the question (exit 2) when no read-only investigation tool has fired since the latest real user turn in the session JSONL. The structural fix for the failure class where the agent fabricates a menu before evidence-gathering (session `c085ec2c-46fb-4b73-8865-68cf85866ea8` 2026-05-22 — "change remote access password" → invented options "Admin PIN / Cloudflare tunnel / WiFi password" with zero prior tool_use; post-correction the agent immediately fired `remote-auth-status` → `ToolSearch` → `remote-auth-set-password`, proving it knew the moves). **Allowlist** (exact, with trailing `__<tool>` suffix-match for namespaced `mcp__plugin_<plugin>_<server>__<tool>` aliases): `ToolSearch`, `Grep`, `Glob`, `Read`, `LS`, `NotebookRead`, `Bash`, `WebFetch`, `WebSearch`, plus the read-only admin / memory MCP tools (`*-status`, `*-list`, `*-read`, `skill-find`, `memory-find-candidates`, `profile-read`, `conversation-list`, `memory-list-attachments`, `memory-read-attachment`). **Block message:** `Blocked: AskUserQuestion requires at least one investigation tool (ToolSearch, Grep, Read, *-list, *-status, *-read, skill-find, ...) earlier in this turn. Search the operator's literal phrase first.` **Log line** (stderr, one per call): `[ask-gate] decision=<allow|block> sessionId=<id8> seen=<csv|-> reason=<allowlist-hit|no-investigation|fail-open-no-transcript|fail-open-parse-error>`. **Fail-open** on missing transcript or parse error — nudges, never bricks the UI.
|
|
277
|
-
- `hooks/askuserquestion-channel-carrier-gate.sh` — PreToolUse matcher=`AskUserQuestion`, registered as a **second command on the same matcher** as the investigate-gate (either exit 2 blocks; the two guard different conditions). Blocks the question (exit 2) when the session has a native channel attached, because the Claude Code channel protocol carries only the two permission methods — there is no carrier for the `AskUserQuestion` elicitation, so on an rc-spawn channel (webchat/WhatsApp/Telegram) session the harness would block the turn awaiting a selection that can never arrive and the reader renders only delivered replies, wedging the turn (live SiteDesk incident session `1a085718` 2026-07-11: `/chat` stalled on the delivered "On it…" ack with no card). **Signal:** the rc-spawn writes the channel MCP registration as a config file in `os.tmpdir()` keyed by session id (`maxy-channel-<sid>.json` / `maxy-webchat-channel-<sid>.json` / `maxy-wa-channel-<sid>.json` / `maxy-telegram-channel-<sid>.json`, `sid` sanitized `[^A-Za-z0-9_-]→_` exactly as the writers do); its presence at tool-call time is the authoritative "channel attached to this session" marker. The hook resolves the tmpdir the way Node's `os.tmpdir()` does (`${TMPDIR:-/tmp}`, trailing slash stripped). Correct in both readings of the claude.ai/code scoping question: a Pi channel process sees the tmpfile (block); a claude.ai/code process on another host never sees the Pi tmpfile (fail-open → allow, that surface can answer the card). **Block message:** `Blocked: this is a native-channel session and AskUserQuestion cannot be delivered over a channel (the channel protocol carries no elicitation card, so the turn would wedge). Ask the same question as prose in your
|
|
278
|
-
- `hooks/public-tool-surface.sh` — **PreToolUse matcher-less (every tool) + UserPromptSubmit + Stop.** Records the tool surface a `role='public'` spawn REALISES, which nothing recorded before: `tool-allowlist` records the surface a spawn was given, and `[public-surface] op=audit` reconciles MCP server names from spawn-tee filenames, so it is blind to `Read`, `Bash`, `Skill` and `ToolSearch` (no MCP child, no tee file). On 2026-08-01 five such tools ran in a session spawned with `--allowed-tools mcp__whatsapp-channel__reply --strict-mcp-config --permission-mode dontAsk` and produced no line. **Input:** `MAXY_PUBLIC_ALLOWED_TOOLS`, stamped by both spawn paths (`http-server.ts` rc-spawn, `pty-spawner.ts`) for `role='public'` only, carrying that spawn's final `--allowed-tools` argv set comma-joined. Absent means not a public spawn: the hook exits 0 in silence. **Log lines** (server.log only — a visitor's PTY gains no output): `[public-surface] op=tool-realised sessionId=<id8> tool=<name> allowed=<y|n>` per call, `allowed` being exact membership of the stamped set; `[public-surface] op=realised-census sessionId=<id8> calls=<n> disallowed=<n>` at Stop, emitted including zero so a session with no disallowed call is distinguishable from one the hook never saw. Per-call tallies are appends to `<logdir>/public-surface/<sanitised-session-id>`, removed by the census, so no counter mutex is needed. The `UserPromptSubmit` registration is the turn-start reset and emits nothing: the stuck-turn recycler kills a wedged PTY and resumes the SAME session id, so that turn's `Stop` never fires and its tally would otherwise be counted into the next turn's census (measured: three calls in a killed turn plus one call in the next reported `calls=4 disallowed=3`). **Never blocks:** exit 0 on every path, including unparseable stdin, absent `session_id`, missing `python3` and an unwritable log. **Reach on upgrade:** `provision-account-dir.sh` writes both entries only at account-provision time, so `setup-account.sh` runs `reconcile_all_accounts_public_surface` (`platform/scripts/lib/account-settings-public-surface.sh`) on every install to retrofit them onto every existing account, one `[backfill-2339] file=<f> status=<absent|no-hooks|already-set|updated|rewrite-failed>` line per account, contents never echoed.
|
|
277
|
+
- `hooks/askuserquestion-channel-carrier-gate.sh` — PreToolUse matcher=`AskUserQuestion`, registered as a **second command on the same matcher** as the investigate-gate (either exit 2 blocks; the two guard different conditions). Blocks the question (exit 2) when the session has a native channel attached, because the Claude Code channel protocol carries only the two permission methods — there is no carrier for the `AskUserQuestion` elicitation, so on an rc-spawn channel (webchat/WhatsApp/Telegram) session the harness would block the turn awaiting a selection that can never arrive and the reader renders only delivered replies, wedging the turn (live SiteDesk incident session `1a085718` 2026-07-11: `/chat` stalled on the delivered "On it…" ack with no card). **Signal:** the rc-spawn writes the channel MCP registration as a config file in `os.tmpdir()` keyed by session id (`maxy-channel-<sid>.json` / `maxy-webchat-channel-<sid>.json` / `maxy-wa-channel-<sid>.json` / `maxy-telegram-channel-<sid>.json`, `sid` sanitized `[^A-Za-z0-9_-]→_` exactly as the writers do); its presence at tool-call time is the authoritative "channel attached to this session" marker. The hook resolves the tmpdir the way Node's `os.tmpdir()` does (`${TMPDIR:-/tmp}`, trailing slash stripped). Correct in both readings of the claude.ai/code scoping question: a Pi channel process sees the tmpfile (block); a claude.ai/code process on another host never sees the Pi tmpfile (fail-open → allow, that surface can answer the card). **Block message:** `Blocked: this is a native-channel session and AskUserQuestion cannot be delivered over a channel (the channel protocol carries no elicitation card, so the turn would wedge). Ask the same question as prose in your answer and wait for the operator's next message.` **Log line** (stderr, one per decisive call): `[ask-channel] decision=<allow|block> sessionId=<id8> channel=<attached|none> reason=<channel-attached|no-channel|fail-open-no-envelope|fail-open-no-session>`. **Fail-open** on no envelope, blank session id, or absent channel file — nudges, never bricks the UI. The complementary standing check (`interactive-wedge` in the session manager) catches any un-carried interactive tool_use that slips past this hook; it scans every account's project-slug (not just the boot slug), so a client sub-account channel wedge is covered too. **Reach on upgrade:** `provision-account-dir.sh` writes this matcher only at account-provision time, and the upgrade path re-provisions only the house account — so an account provisioned before this gate landed keeps a stale matcher and re-wedges (live SiteDesk recurrence session `d39a22b0`, account `2078cb54`, 2026-07-15). `setup-account.sh` runs `reconcile_all_accounts_askgate` (`platform/scripts/lib/account-settings-askgate.sh`) on every install to retrofit the carrier gate onto every existing account's `settings.json` — idempotent surgical jq patch of the `AskUserQuestion` matcher, one `[backfill-1683] file=<f> status=<absent|no-hooks|already-set|updated|rewrite-failed>` line per account, contents never echoed.
|
|
278
|
+
- `hooks/public-tool-surface.sh` — **PreToolUse matcher-less (every tool) + UserPromptSubmit + Stop.** Records the tool surface a `role='public'` spawn REALISES, which nothing recorded before: `tool-allowlist` records the surface a spawn was given, and `[public-surface] op=audit` reconciles MCP server names from spawn-tee filenames, so it is blind to `Read`, `Bash`, `Skill` and `ToolSearch` (no MCP child, no tee file). On 2026-08-01 five such tools ran in a session spawned with `--allowed-tools mcp__whatsapp-channel__reply --strict-mcp-config --permission-mode dontAsk` (that reply tool is now retired; a public spawn's allowlist is the deny-basis anchor alone) and produced no line. **Input:** `MAXY_PUBLIC_ALLOWED_TOOLS`, stamped by both spawn paths (`http-server.ts` rc-spawn, `pty-spawner.ts`) for `role='public'` only, carrying that spawn's final `--allowed-tools` argv set comma-joined. Absent means not a public spawn: the hook exits 0 in silence. **Log lines** (server.log only — a visitor's PTY gains no output): `[public-surface] op=tool-realised sessionId=<id8> tool=<name> allowed=<y|n>` per call, `allowed` being exact membership of the stamped set; `[public-surface] op=realised-census sessionId=<id8> calls=<n> disallowed=<n>` at Stop, emitted including zero so a session with no disallowed call is distinguishable from one the hook never saw. Per-call tallies are appends to `<logdir>/public-surface/<sanitised-session-id>`, removed by the census, so no counter mutex is needed. The `UserPromptSubmit` registration is the turn-start reset and emits nothing: the stuck-turn recycler kills a wedged PTY and resumes the SAME session id, so that turn's `Stop` never fires and its tally would otherwise be counted into the next turn's census (measured: three calls in a killed turn plus one call in the next reported `calls=4 disallowed=3`). **Never blocks:** exit 0 on every path, including unparseable stdin, absent `session_id`, missing `python3` and an unwritable log. **Reach on upgrade:** `provision-account-dir.sh` writes both entries only at account-provision time, so `setup-account.sh` runs `reconcile_all_accounts_public_surface` (`platform/scripts/lib/account-settings-public-surface.sh`) on every install to retrofit them onto every existing account, one `[backfill-2339] file=<f> status=<absent|no-hooks|already-set|updated|rewrite-failed>` line per account, contents never echoed.
|
|
279
279
|
- `hooks/mcp-tool-missing.sh` — **PostToolUse hook on `mcp__.*` (directive 3).** Defence-in-depth for the `No such tool available: mcp__…` failure class that the name-binding is built to eliminate. Fires on any MCP tool call; no-op unless the `tool_response` carries `No such tool available` AND the qualified name resolves to a maxy plugin (read from the generated `hooks/lib/maxy-mcp-plugins.txt`). On a maxy match it logs one deterministic `[mcp-tool-missing] server=<server> tool=<tool>` line and exits 2 with a fixed envelope on stderr, so the agent relays a named server-unavailable failure instead of narrating "warming up" or blind-retrying. A missing non-maxy bridge tool (Playwright etc., upstream-owned) passes through (exit 0). The maxy-plugin list is regenerated and gate-diffed by `platform/scripts/check-canonical-tool-names.mjs`.
|
|
280
280
|
- `hooks/post-tool-use-agent.sh` — **PostToolUse hook on `Agent`.** Drains any subagent hook-decision buffers under `~/.maxy-code/logs/hook-decisions/` modified since this parent's previous PostToolUse-Agent fire (cursor file keyed by parent session id), prints one `[hook-propagate]` line per record to stdout — Claude Code attaches the stdout as a `hook_success` attachment on the parent JSONL, making the records grep-queryable from the parent session alone. Rotates consumed buffers to `consumed/`. Emits one `[hook-propagate-census] parentSession=<…> subagentHooksObserved=<N> attachmentsEmitted=<M>` line per fire to stdout and server.log; `N != M` is the propagation regression signal. The companion emitter library `hooks/lib/hook-emit.sh` is sourced by `post-tool-use-agent.sh` and any other hook that records a block decision (4 KB stderr truncation, `truncated=true` set on the record).
|
|
281
281
|
- `hooks/admin-authoring-observer.sh` — **PostToolUse hook on Write and Edit.** Observation only — never blocks; exits 0 on every path. Fires when the admin agent (not a specialist subagent — gated by `MAXY_SPECIALIST` env) writes or edits a file under `<accountDir>/output/`. Walks the session transcript from the latest real-user turn forward to detect any prior `Task` `tool_use` whose `subagent_type` starts with `specialists:`. Emits one stderr line `[admin-authoring] inline-write path=<rel> priorSpecialistSpawnInTurn=<true|false|unknown>`. A `false` value on a long-form prose file is the regression signal this hook was designed to make visible — the BioSymm proposal session (admin authored a customer-facing proposal inline despite content-producer being installed) is the failure mode this surfaces mechanically. Mechanical enforcement (refuse the write, force a re-spawn) is deferred per the task spec.
|
package/payload/platform/plugins/admin/hooks/__tests__/askuserquestion-channel-carrier-gate.test.sh
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
# The hook blocks AskUserQuestion when the session has a native channel
|
|
5
5
|
# attached — signalled by a channel MCP config file for this session id in
|
|
6
6
|
# tmpdir. A channel carries no elicitation card, so the turn would wedge; the
|
|
7
|
-
# hook steers the agent to ask in prose
|
|
7
|
+
# hook steers the agent to ask in prose in its own answer instead.
|
|
8
8
|
#
|
|
9
9
|
# Covers (per .tasks/1552):
|
|
10
10
|
# 1. Channel attached (webchat config file present) + AskUserQuestion → BLOCK channel-attached
|
|
@@ -10,8 +10,9 @@
|
|
|
10
10
|
# wedges — the operator sees the last delivered reply and a live-looking but
|
|
11
11
|
# dead composer (see .tasks/1552, live SiteDesk incident session 1a085718
|
|
12
12
|
# 2026-07-11). This hook removes that wedge deterministically by steering the
|
|
13
|
-
# agent to ask the same question as prose
|
|
14
|
-
#
|
|
13
|
+
# agent to ask the same question as prose in its answer, which rides the ordinary
|
|
14
|
+
# delivery path that works on every surface. Task 2078 — that path is the turn's
|
|
15
|
+
# own final text now; before it, the channel reply tool.
|
|
15
16
|
#
|
|
16
17
|
# This is a SEPARATE concern from askuserquestion-investigate-gate.sh (which
|
|
17
18
|
# blocks a fabricated menu raised before any investigation). Both are registered
|
|
@@ -129,7 +130,7 @@ done
|
|
|
129
130
|
|
|
130
131
|
if [ "$CHANNEL_ATTACHED" -eq 1 ]; then
|
|
131
132
|
echo "[ask-channel] decision=block sessionId=${SESSION_ID8} channel=attached reason=channel-attached" >&2
|
|
132
|
-
echo "Blocked: this is a native-channel session and AskUserQuestion cannot be delivered over a channel (the channel protocol carries no elicitation card, so the turn would wedge). Ask the same question as prose in your
|
|
133
|
+
echo "Blocked: this is a native-channel session and AskUserQuestion cannot be delivered over a channel (the channel protocol carries no elicitation card, so the turn would wedge). Ask the same question as prose in your answer and wait for the operator's next message." >&2
|
|
133
134
|
exit 2
|
|
134
135
|
fi
|
|
135
136
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: platform-architecture
|
|
3
3
|
description: Use when grounding any documented-surface claim about what Maxy ships — plugins, skills, specialists, install/deploy flows, internals. This is the install catalogue, not evidence of what is enabled on the current account. For install state on this account, call `capabilities-here`; for documented surface, cite the `Source:` URL inline.
|
|
4
|
-
content-hash: sha256:
|
|
4
|
+
content-hash: sha256:c0d108d50a5c5109da65ad22f4838284c614aed9161d4b5ab70c6db7c30904c2
|
|
5
5
|
brand: maxy-code
|
|
6
6
|
product-name: Maxy
|
|
7
7
|
---
|
|
@@ -2544,6 +2544,25 @@ Ask Maxy: "What messages has the bot received?" or "Show recent Telegram activit
|
|
|
2544
2544
|
|
|
2545
2545
|
**Messages not arriving:** Make sure the bot hasn't been blocked. Try sending `/start` to the bot directly.
|
|
2546
2546
|
|
|
2547
|
+
## Sending from the dashboard
|
|
2548
|
+
|
|
2549
|
+
Open a Telegram conversation in the dashboard and you can type a reply straight
|
|
2550
|
+
into it, the same way you already can with WhatsApp. The message goes out from
|
|
2551
|
+
your bot.
|
|
2552
|
+
|
|
2553
|
+
Two things to know:
|
|
2554
|
+
|
|
2555
|
+
- **Direct messages only.** Group and supergroup chats show no message box. A
|
|
2556
|
+
Telegram conversation is filed under the person who spoke, so a message sent
|
|
2557
|
+
from a group thread would reach that one person privately rather than the
|
|
2558
|
+
group — the box is hidden rather than doing something you did not intend.
|
|
2559
|
+
- **History starts now.** Messages are only shown from the point this feature
|
|
2560
|
+
was installed onward. A conversation that was already running looks empty
|
|
2561
|
+
until the next message arrives, and then fills up from there.
|
|
2562
|
+
|
|
2563
|
+
Text only for now. Photos, files and voice notes are not available on Telegram
|
|
2564
|
+
from the dashboard.
|
|
2565
|
+
|
|
2547
2566
|
---
|
|
2548
2567
|
# Outlook
|
|
2549
2568
|
Source: https://docs.getmaxy.com/outlook-guide.md
|
|
@@ -2570,7 +2589,7 @@ The `outlook` plugin gives the admin agent access to Microsoft 365 / Outlook.com
|
|
|
2570
2589
|
| `outlook-mail-search` | Microsoft Graph `$search` (KQL) over the mailbox. Preview-only, like `outlook-mail-list`, and pages the same way via `nextCursor`/`cursor`. Put sender/subject/recipient/date filters in the query: `from:`, `to:`, `subject:`, `received>=`. Graph forbids `$search`+`$filter`, so a strict date window is on `outlook-mail-list`. |
|
|
2571
2590
|
| `outlook-mail-fetch-body` | Complete body of one message by id (`GET /me/messages/{id}?$select=body`). HTML decoded to text, no preview cap. The full-read path — list/search return Microsoft's ~255-char preview only. Returns the envelope plus the whole body. |
|
|
2572
2591
|
| `outlook-mail-attachment` | List or download any message's attachments — received or draft. With `messageId` alone, LISTS them (`id`, `name`, `contentType`, `size`, `isInline`, and a `kind` of file/item/reference) via `GET /me/messages/{id}/attachments` and `$select` (no bytes). A draft is a message in the same collection, so passing a draftId lists what that draft carries; the returned `name`/`id` is what `outlook-draft-edit`'s `removeAttachments` takes. With `messageId`+`attachmentId`, DOWNLOADS one file attachment: its bytes are written to `{accountDir}/uploads/outlook/<messageHash>/<bytesHash>-<name>` at `0o600` and the saved path is returned, ready for the Read tool, `SendUserFile`, or an `outlook-mail-reply` attachment. Only file attachments download — item/reference kinds, empty payloads, and anything over 25 MB are refused with nothing written. `outlook-mail-list` / `outlook-mail-search` / `outlook-mail-fetch-body` report `hasAttachments` so the agent knows when to call this. Needs `Mail.Read`. |
|
|
2573
|
-
| `outlook-mail-reply` | Reply in-thread (`createReply` / `createReplyAll` + send). Threads natively via conversationId. `replyAll`, added cc/bcc, and account-scoped file attachments (up to 25 MB per file; files over Graph's ~3 MB inline limit use a chunked upload session). Body is plain text, prepended above the quote. Needs `Mail.ReadWrite` + `Mail.Send`. |
|
|
2592
|
+
| `outlook-mail-reply` | Reply in-thread (`createReply` / `createReplyAll` + send). Threads natively via conversationId. `replyAll`, added cc/bcc, and account-scoped file attachments (up to 25 MB per file; files over Graph's ~3 MB inline limit use a chunked upload session). Body is plain text, prepended above the quote. A reply whose recipients resolve **only** to the sending mailbox is **refused** before the send and its draft deleted: Graph addresses a reply to the original message's sender, so replying to your own sent message addresses it to yourself. An explicit `cc` does not exempt it — use `outlook-mail-send` with an explicit `to`. The resolved recipients are logged as `to=`. Needs `Mail.ReadWrite` + `Mail.Send`. |
|
|
2574
2593
|
| `outlook-mail-delete` | Move messages to Deleted Items (`POST /me/messages/{id}/move`). Recoverable — never hard-deletes. An id already gone is counted not-moved. Needs `Mail.ReadWrite`. |
|
|
2575
2594
|
| `outlook-mail-ingest` | Write operator-reviewed messages into the business graph as `:ConversationArchive {source:'email'}` threads (the analogue of the IMAP `email-ingest`). Input `decisions`: one `{messageId, disposition:'ingest'\|'discard'}` per listed message. Each `ingest` message's full body + envelope is fetched, threads group by Graph `conversationId`, and participants resolve closed-set against Neo4j (`:Person`/`:AdminUser` by `accountId`); an unresolved From/To/Cc address skips its whole thread and is returned to the operator (no `:Person` auto-create). Resolvable threads dispatch through the shared `conversation-archive-ingest.sh --source email`. Human-in-the-loop: nothing lands without an explicit per-message decision. The plugin's only Neo4j coupling. Needs `Mail.Read`. |
|
|
2576
2595
|
| `outlook-mail-otp-extract` | Poll the inbox for a one-time code from a sender (domain). Reads full bodies to extract it; optional subject regex and timeout (default 60 s). |
|
|
@@ -2587,7 +2606,7 @@ The `outlook` plugin gives the admin agent access to Microsoft 365 / Outlook.com
|
|
|
2587
2606
|
| `outlook-shared-mailbox-add` | Attach a shared mailbox (no sign-in, no licence) through the Full Access of a mailbox that is already connected. `address` is the shared mailbox; `via` names the connected mailbox when more than one is attached. Proves the access before storing anything, so a refusal is the real answer. |
|
|
2588
2607
|
| `outlook-shared-mailbox-remove` | Detach a shared mailbox. Drops the pointer only; no credential is touched. |
|
|
2589
2608
|
| `outlook-mail-send` | Send a message. No attachments → `POST /me/sendMail`; with attachments → draft, attach, send (`/me/sendMail` carries attachments inline and cannot fit a large file). to/cc/bcc arrays, subject, body, isHtml, attachments (account-dir paths, 25 MB/file). Success asserted on Graph 202; no message id on either route. Needs `Mail.Send` (+ `Mail.ReadWrite` to attach). |
|
|
2590
|
-
| `outlook-draft` | Create a draft in Drafts, either a new message or a threaded reply. Returns the draft id; never sends. Attachments are attached before the id is returned, so the draft is ready to send. Needs `Mail.ReadWrite`. **New message** (no `replyToMessageId`): `POST /me/messages`, requires `to` + `subject`, same body model as `outlook-mail-send`, threads under nothing. **Threaded reply** (`replyToMessageId` = the id of the message being answered): `POST /me/messages/{id}/createReply` — the draft carries the parent's conversationId, so it lands under the original. This is the only way to draft a reply without sending it: `outlook-mail-reply` threads but always sends. Graph sets the recipients and `RE:` subject from the original and composes the body above its own quote, matching what `outlook-mail-reply` sends, so the threaded path is **plain text only** — `to`, `subject`, and `isHtml: true` are **refused** (not ignored) when replying. `replyAll` includes everyone on the original; `cc`/`bcc` add recipients. |
|
|
2609
|
+
| `outlook-draft` | Create a draft in Drafts, either a new message or a threaded reply. Returns the draft id; never sends. Attachments are attached before the id is returned, so the draft is ready to send. Needs `Mail.ReadWrite`. **New message** (no `replyToMessageId`): `POST /me/messages`, requires `to` + `subject`, same body model as `outlook-mail-send`, threads under nothing. **Threaded reply** (`replyToMessageId` = the id of the message being answered): `POST /me/messages/{id}/createReply` — the draft carries the parent's conversationId, so it lands under the original. This is the only way to draft a reply without sending it: `outlook-mail-reply` threads but always sends. Graph sets the recipients and `RE:` subject from the original and composes the body above its own quote, matching what `outlook-mail-reply` sends, so the threaded path is **plain text only** — `to`, `subject`, and `isHtml: true` are **refused** (not ignored) when replying. `replyAll` includes everyone on the original; `cc`/`bcc` add recipients. The result carries `to`, the recipients the draft holds, so the self-addressed case above is visible without fetching the draft back; on this path it is reported rather than refused, because a draft is not sent and `outlook-draft-edit` can re-address it. |
|
|
2591
2610
|
| `outlook-draft-edit` | Update an existing draft in place (`PATCH /me/messages/{draftId}`) and/or add and remove attachments. Graph drafts are mutable — pass draftId plus the fields to change (to/cc/bcc/subject/body/isHtml/attachments/removeAttachments). `attachments` adds; `removeAttachments` takes files off by name or attachment id; both together replace a file in one call. Sent messages cannot be edited. Needs `Mail.ReadWrite`. |
|
|
2592
2611
|
| `outlook-draft-send` | Send an existing draft by id (`POST /me/messages/{id}/send`). Graph consumes the draft, returns 202. Needs `Mail.Send`. |
|
|
2593
2612
|
|
|
@@ -2657,8 +2676,12 @@ All log lines start with `[outlook-mcp]` and write to `server.log`. They are key
|
|
|
2657
2676
|
| Send args | `send-args account=<id> recipients=<N> hasBody=<bool>` |
|
|
2658
2677
|
| Sent | `sent account=<id> status=202 messageId=<id-or-null>` |
|
|
2659
2678
|
| Send failed | `send-failed account=<id> error=<msg>` |
|
|
2679
|
+
| Reply sent | `mail-reply op=reply account=<id> id=<parentId> convId=<id\|null> replyAll=<bool> attachments=<N> to=<csv>` — `to=` is the recipients Graph resolved; before it, who a sent reply actually went to existed nowhere on disk |
|
|
2680
|
+
| Reply refused (self-addressed) | `mail-reply op=self-reply-refused account=<id> id=<parentId> to=<csv>` — terminal, nothing sent, draft deleted |
|
|
2681
|
+
| Self-reply guard could not run | `mail-reply op=self-reply-guard state=no-address account=<id> id=<parentId>` — the config carries no mailbox address, so the comparison is impossible and the send proceeds. Distinguishes "guard passed" from "guard absent", which a missing line cannot |
|
|
2682
|
+
| Refused, draft not cleaned up | `mail-reply op=self-reply-cleanup-failed account=<id> draftId=<id> error=<msg>` — followed by the refusal; the orphan is visible in Drafts |
|
|
2660
2683
|
| Draft request | `draft-request account=<id> threaded=<bool>` |
|
|
2661
|
-
| Draft created | `draft-created account=<id> id=<draftId> threaded=<bool> convId=<id\|null>` |
|
|
2684
|
+
| Draft created | `draft-created account=<id> id=<draftId> threaded=<bool> convId=<id\|null> to=<csv>` |
|
|
2662
2685
|
| Draft failed | `draft-failed account=<id> threaded=<bool> error=<msg>` |
|
|
2663
2686
|
| Draft-send request | `draft-send-request account=<id> id=<draftId>` |
|
|
2664
2687
|
| Draft sent | `draft-sent account=<id> id=<draftId> status=202` |
|
|
@@ -9,6 +9,18 @@ Invoked by the admin agent directly.
|
|
|
9
9
|
|
|
10
10
|
This is the platform's release timeline, newest first. Each entry shows the date it shipped and the version it shipped in, so you can tell the operator how current their install is. To compare, read the installed version from `capabilities-here` and match it against the versions below. Keep answers high level and in plain English; this is a summary, not a full commit log.
|
|
11
11
|
|
|
12
|
+
## 2026-08-05 (0.1.565)
|
|
13
|
+
|
|
14
|
+
- Every channel now delivers the assistant's answer the same way the admin webchat does: the answer is taken from the conversation record and sent. Previously WhatsApp, Telegram and the public webchat asked the assistant to call a send tool, so an answer was suppressed when it called and lost entirely when it did not. Long answers arrive as ordered segments, and an answer meant for you fans out to every channel you have live, naming any that could not be reached.
|
|
15
|
+
- A sub-account conversation no longer sees the main account's specialists. It was being given the house list while dispatch used its own, so the two disagreed.
|
|
16
|
+
- Replying to a message your own mailbox sent no longer emails you instead of the intended recipient, and the reply reports who it is going to before it is sent rather than after.
|
|
17
|
+
|
|
18
|
+
## 2026-08-05 (0.1.564)
|
|
19
|
+
|
|
20
|
+
- You can now reply to a Telegram conversation from the dashboard. Telegram was read-only, so a conversation you could watch was one you could not answer; it now has a composer like WhatsApp. Sending is restricted to private chats.
|
|
21
|
+
- An arriving Telegram message now raises a card and a desktop notification, the same as WhatsApp. Until now it arrived silently on every admin surface.
|
|
22
|
+
- A portal job sheet now records time, so a day's labour reaches the timesheet. Field work could be marked done but carried no hours and wrote no labour cost at either end.
|
|
23
|
+
|
|
12
24
|
## 2026-08-05 (0.1.563)
|
|
13
25
|
|
|
14
26
|
- You can now update from the dashboard. The installed version sits in the sidebar header rather than buried in the footer menu, and when a newer one is available the control there starts the update and shows its progress live. Previously the only route was asking the assistant in chat, and an available update was invisible until you opened the menu.
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { describe, it, expect } from 'vitest'
|
|
2
2
|
import {
|
|
3
|
+
MAX_NOTE_LENGTH,
|
|
3
4
|
MAX_STATUS_LENGTH,
|
|
5
|
+
WORK_DATE_RE,
|
|
4
6
|
processVisitStatus,
|
|
5
7
|
} from '../../skills/data-portal/template/functions/api/visit-status'
|
|
6
8
|
import type {
|
|
@@ -174,3 +176,119 @@ describe('the visit-status write gate', () => {
|
|
|
174
176
|
expect(joined).not.toContain('granary')
|
|
175
177
|
})
|
|
176
178
|
})
|
|
179
|
+
|
|
180
|
+
describe('the working day a status edit may carry', () => {
|
|
181
|
+
const day = (extra: Record<string, unknown>) => ({ visitId: 'v1', status: 'done', ...extra })
|
|
182
|
+
|
|
183
|
+
it('inserts the day, the date and the note when the claim is well formed', async () => {
|
|
184
|
+
const { database, inserted } = db(ROWS, person('alice', 'own'))
|
|
185
|
+
const h = await processVisitStatus(
|
|
186
|
+
day({ dayHundredths: 100, workDate: '2026-08-05', workNote: 'second fix' }),
|
|
187
|
+
'sess',
|
|
188
|
+
env(database),
|
|
189
|
+
() => {},
|
|
190
|
+
1,
|
|
191
|
+
)
|
|
192
|
+
expect(h.status).toBe(200)
|
|
193
|
+
expect(inserted[0]).toContain(100)
|
|
194
|
+
expect(inserted[0]).toContain('2026-08-05')
|
|
195
|
+
expect(inserted[0]).toContain('second fix')
|
|
196
|
+
})
|
|
197
|
+
|
|
198
|
+
it('defaults to no claim when the body carries no day', async () => {
|
|
199
|
+
// A status-only edit is the common case and must stay a one-field write.
|
|
200
|
+
const { database, inserted } = db(ROWS, person('alice', 'own'))
|
|
201
|
+
const h = await processVisitStatus(day({}), 'sess', env(database), () => {}, 1)
|
|
202
|
+
expect(h.status).toBe(200)
|
|
203
|
+
expect(inserted[0]).toContain(0)
|
|
204
|
+
expect(inserted[0]).toContain('')
|
|
205
|
+
})
|
|
206
|
+
|
|
207
|
+
it('refuses a day outside 0..100 rather than writing it into the graph', async () => {
|
|
208
|
+
// A value above a full day is described by no shipped surface. Refused at
|
|
209
|
+
// the door, exactly as an over-long status is.
|
|
210
|
+
const { database, inserted } = db(ROWS, person('alice', 'own'))
|
|
211
|
+
const e = env(database)
|
|
212
|
+
for (const d of [101, -1, 50.5, '100']) {
|
|
213
|
+
const h = await processVisitStatus(
|
|
214
|
+
day({ dayHundredths: d, workDate: '2026-08-05' }),
|
|
215
|
+
'sess',
|
|
216
|
+
e,
|
|
217
|
+
() => {},
|
|
218
|
+
1,
|
|
219
|
+
)
|
|
220
|
+
expect(h.status).toBe(400)
|
|
221
|
+
}
|
|
222
|
+
expect(inserted).toEqual([])
|
|
223
|
+
})
|
|
224
|
+
|
|
225
|
+
it('refuses a claim whose date is missing or malformed', async () => {
|
|
226
|
+
// The date becomes part of the cost key. A claim the roll-up cannot date is
|
|
227
|
+
// told to the person now, not left sitting in the queue.
|
|
228
|
+
const { database, inserted } = db(ROWS, person('alice', 'own'))
|
|
229
|
+
const e = env(database)
|
|
230
|
+
for (const wd of [undefined, '', '5th Aug', '2026-8-5']) {
|
|
231
|
+
const h = await processVisitStatus(
|
|
232
|
+
day({ dayHundredths: 100, workDate: wd }),
|
|
233
|
+
'sess',
|
|
234
|
+
e,
|
|
235
|
+
() => {},
|
|
236
|
+
1,
|
|
237
|
+
)
|
|
238
|
+
expect(h.status).toBe(400)
|
|
239
|
+
}
|
|
240
|
+
expect(inserted).toEqual([])
|
|
241
|
+
})
|
|
242
|
+
|
|
243
|
+
it('accepts a date only in the shape the pull will key on', () => {
|
|
244
|
+
expect(WORK_DATE_RE.test('2026-08-05')).toBe(true)
|
|
245
|
+
expect(WORK_DATE_RE.test('2026-08-05T09:00:00Z')).toBe(false)
|
|
246
|
+
})
|
|
247
|
+
|
|
248
|
+
it('refuses a note longer than the cap rather than truncating it', async () => {
|
|
249
|
+
const { database, inserted } = db(ROWS, person('alice', 'own'))
|
|
250
|
+
const h = await processVisitStatus(
|
|
251
|
+
day({
|
|
252
|
+
dayHundredths: 100,
|
|
253
|
+
workDate: '2026-08-05',
|
|
254
|
+
workNote: 'x'.repeat(MAX_NOTE_LENGTH + 1),
|
|
255
|
+
}),
|
|
256
|
+
'sess',
|
|
257
|
+
env(database),
|
|
258
|
+
() => {},
|
|
259
|
+
1,
|
|
260
|
+
)
|
|
261
|
+
expect(h.status).toBe(400)
|
|
262
|
+
expect(inserted).toEqual([])
|
|
263
|
+
})
|
|
264
|
+
|
|
265
|
+
it('logs whether the edit carried a day, and never the note', async () => {
|
|
266
|
+
const lines: string[] = []
|
|
267
|
+
const { database } = db(ROWS, person('alice', 'own'))
|
|
268
|
+
await processVisitStatus(
|
|
269
|
+
day({ dayHundredths: 100, workDate: '2026-08-05', workNote: 'granary court rewire' }),
|
|
270
|
+
'sess',
|
|
271
|
+
env(database),
|
|
272
|
+
(l) => lines.push(l),
|
|
273
|
+
1,
|
|
274
|
+
)
|
|
275
|
+
const joined = lines.join('\n')
|
|
276
|
+
expect(joined).toContain('labour=true')
|
|
277
|
+
expect(joined).not.toContain('granary')
|
|
278
|
+
expect(joined).not.toContain('2026-08-05')
|
|
279
|
+
})
|
|
280
|
+
|
|
281
|
+
it('still refuses another person visit when the edit carries a day', async () => {
|
|
282
|
+
// The gate is the SELECT and the new fields must not have moved it.
|
|
283
|
+
const { database, inserted } = db(ROWS, person('alice', 'own'))
|
|
284
|
+
const h = await processVisitStatus(
|
|
285
|
+
{ visitId: 'v2', status: 'done', dayHundredths: 100, workDate: '2026-08-05' },
|
|
286
|
+
'sess',
|
|
287
|
+
env(database),
|
|
288
|
+
() => {},
|
|
289
|
+
1,
|
|
290
|
+
)
|
|
291
|
+
expect(h.status).toBe(404)
|
|
292
|
+
expect(inserted).toEqual([])
|
|
293
|
+
})
|
|
294
|
+
})
|
|
@@ -147,6 +147,21 @@ ALTER TABLE people ADD COLUMN recordScope TEXT NOT NULL DEFAULT 'own';
|
|
|
147
147
|
No backfill follows it. Everyone already enrolled lands on the narrow scope, which is the intended
|
|
148
148
|
outcome, and widening one person is a re-run of enrolment for that person.
|
|
149
149
|
|
|
150
|
+
**A status edit may also carry a working day, and a store that predates those three columns needs
|
|
151
|
+
them added once too.** `dayHundredths` is integer hundredths of a day, which is what the timesheet
|
|
152
|
+
engine consumes: 100 is a full day, and 0 means the edit claims no labour. `workDate` is the day
|
|
153
|
+
worked, and the pull refuses a claim without one rather than dating it from when the sheet was filed.
|
|
154
|
+
`workNote` is what the worker did. The same `IF NOT EXISTS` rule applies, so run all three:
|
|
155
|
+
|
|
156
|
+
```sql
|
|
157
|
+
ALTER TABLE visit_status ADD COLUMN dayHundredths INTEGER NOT NULL DEFAULT 0;
|
|
158
|
+
ALTER TABLE visit_status ADD COLUMN workDate TEXT NOT NULL DEFAULT '';
|
|
159
|
+
ALTER TABLE visit_status ADD COLUMN workNote TEXT NOT NULL DEFAULT '';
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
Until they exist the endpoint's INSERT names columns the table lacks and fails loudly, which is the
|
|
163
|
+
intended outcome: a status edit silently losing its hours is the failure worth avoiding.
|
|
164
|
+
|
|
150
165
|
**A passcode is never stored in plaintext** — not in `data-portal.json`, not in a log line, nowhere
|
|
151
166
|
on the device. The operator conveys it to the person out of band. Rotating a passcode is a re-run of
|
|
152
167
|
the script for that person; there is no separate editor.
|
|
@@ -309,14 +309,32 @@ CREATE TABLE IF NOT EXISTS visits_state (
|
|
|
309
309
|
-- is counted by the audit. It is never written under another id and never
|
|
310
310
|
-- dropped: a dropped edit is a promise broken to the person who made it, and it
|
|
311
311
|
-- emits nothing at the time it happens.
|
|
312
|
+
-- `dayHundredths`, `workDate` and `workNote` are the working day the edit
|
|
313
|
+
-- carries (Task 2432). `dayHundredths` is integer hundredths of a day, which is
|
|
314
|
+
-- exactly what the timesheet engine consumes: 100 is a full day. 0 means this
|
|
315
|
+
-- edit carries no labour claim, the same absent-sentinel convention `ownerId`
|
|
316
|
+
-- uses above for unbound. The pull writes a labour :JobCost only when it is
|
|
317
|
+
-- above 0, and refuses a claim carrying no `workDate` rather than dating it
|
|
318
|
+
-- from anything else: `statusAt` would record the wrong day for a sheet filed
|
|
319
|
+
-- the next morning, and a visit's scheduled date is not evidence of when it was
|
|
320
|
+
-- attended.
|
|
321
|
+
--
|
|
322
|
+
-- IF NOT EXISTS adds none of the three to a table that predates them. Add them
|
|
323
|
+
-- by hand in that case, in this order:
|
|
324
|
+
-- ALTER TABLE visit_status ADD COLUMN dayHundredths INTEGER NOT NULL DEFAULT 0;
|
|
325
|
+
-- ALTER TABLE visit_status ADD COLUMN workDate TEXT NOT NULL DEFAULT '';
|
|
326
|
+
-- ALTER TABLE visit_status ADD COLUMN workNote TEXT NOT NULL DEFAULT '';
|
|
312
327
|
CREATE TABLE IF NOT EXISTS visit_status (
|
|
313
|
-
id
|
|
314
|
-
accountId
|
|
315
|
-
visitId
|
|
316
|
-
ownerId
|
|
317
|
-
status
|
|
318
|
-
statusAt
|
|
319
|
-
|
|
328
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
329
|
+
accountId TEXT NOT NULL,
|
|
330
|
+
visitId TEXT NOT NULL,
|
|
331
|
+
ownerId TEXT NOT NULL,
|
|
332
|
+
status TEXT NOT NULL,
|
|
333
|
+
statusAt TEXT NOT NULL,
|
|
334
|
+
dayHundredths INTEGER NOT NULL DEFAULT 0,
|
|
335
|
+
workDate TEXT NOT NULL DEFAULT '',
|
|
336
|
+
workNote TEXT NOT NULL DEFAULT '',
|
|
337
|
+
claimed INTEGER NOT NULL DEFAULT 0
|
|
320
338
|
);
|
|
321
339
|
|
|
322
340
|
-- The pull selects unclaimed rows for one account every sixty seconds, so the
|
|
@@ -12,6 +12,27 @@ import { q } from './_lib/log'
|
|
|
12
12
|
*/
|
|
13
13
|
export const MAX_STATUS_LENGTH = 64
|
|
14
14
|
|
|
15
|
+
/**
|
|
16
|
+
* The longest work note a person may attach to a day.
|
|
17
|
+
*
|
|
18
|
+
* The note reaches `:JobCost.description` through the device-side pull, so it
|
|
19
|
+
* is graph content arriving from the open web and gets the same treatment the
|
|
20
|
+
* status gets: refused at the door, never truncated. The size is chosen for a
|
|
21
|
+
* description cell on the A4 timesheet row, not derived from anything.
|
|
22
|
+
*/
|
|
23
|
+
export const MAX_NOTE_LENGTH = 500
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* The only date shape a claim may carry.
|
|
27
|
+
*
|
|
28
|
+
* The date becomes part of the cost key the pull merges on, so a free-form
|
|
29
|
+
* string would produce a key nobody can reproduce. A COPY of this check lives
|
|
30
|
+
* in `platform/ui/server/portal-visits-pull.ts` as the invariant at the writer;
|
|
31
|
+
* the two cannot share a constant because this file ships to Cloudflare Pages
|
|
32
|
+
* and cannot import from the platform tree.
|
|
33
|
+
*/
|
|
34
|
+
export const WORK_DATE_RE = /^\d{4}-\d{2}-\d{2}$/
|
|
35
|
+
|
|
15
36
|
/**
|
|
16
37
|
* Record a status a person set against one of their visits.
|
|
17
38
|
*
|
|
@@ -56,10 +77,39 @@ export async function processVisitStatus(
|
|
|
56
77
|
return { status: 400, payload: { ok: false, error: 'status too long' } }
|
|
57
78
|
}
|
|
58
79
|
|
|
80
|
+
// The working day the edit carries, if any. Absent means a status-only edit,
|
|
81
|
+
// which is the common case and stays a zero-claim write.
|
|
82
|
+
const dayHundredths = b.dayHundredths === undefined ? 0 : b.dayHundredths
|
|
83
|
+
if (
|
|
84
|
+
typeof dayHundredths !== 'number' ||
|
|
85
|
+
!Number.isInteger(dayHundredths) ||
|
|
86
|
+
dayHundredths < 0 ||
|
|
87
|
+
dayHundredths > 100
|
|
88
|
+
) {
|
|
89
|
+
log(`[data-portal] op=visit-status owner=${q(session.ownerId)} result=bad-day`)
|
|
90
|
+
return { status: 400, payload: { ok: false, error: 'dayHundredths must be an integer 0..100' } }
|
|
91
|
+
}
|
|
92
|
+
const workDate = typeof b.workDate === 'string' ? b.workDate : ''
|
|
93
|
+
const workNote = typeof b.workNote === 'string' ? b.workNote : ''
|
|
94
|
+
// A claim the roll-up could not date is refused now rather than queued. The
|
|
95
|
+
// pull holds the same invariant, so a row reaching the table another way is
|
|
96
|
+
// still never written under a guessed date.
|
|
97
|
+
if (dayHundredths > 0 && !WORK_DATE_RE.test(workDate)) {
|
|
98
|
+
log(`[data-portal] op=visit-status owner=${q(session.ownerId)} result=undated-claim`)
|
|
99
|
+
return {
|
|
100
|
+
status: 400,
|
|
101
|
+
payload: { ok: false, error: 'a day claim requires workDate as YYYY-MM-DD' },
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
if (workNote.length > MAX_NOTE_LENGTH) {
|
|
105
|
+
log(`[data-portal] op=visit-status owner=${q(session.ownerId)} result=note-too-long`)
|
|
106
|
+
return { status: 400, payload: { ok: false, error: 'note too long' } }
|
|
107
|
+
}
|
|
108
|
+
|
|
59
109
|
const scoped = session.recordScope === 'own'
|
|
60
110
|
const sql =
|
|
61
|
-
`INSERT INTO visit_status (accountId, ownerId, status, statusAt, claimed, visitId)
|
|
62
|
-
SELECT ?, ?, ?, ?, 0, v.visitId
|
|
111
|
+
`INSERT INTO visit_status (accountId, ownerId, status, statusAt, dayHundredths, workDate, workNote, claimed, visitId)
|
|
112
|
+
SELECT ?, ?, ?, ?, ?, ?, ?, 0, v.visitId
|
|
63
113
|
FROM visits v
|
|
64
114
|
JOIN visits_state s
|
|
65
115
|
ON s.accountId = v.accountId AND s.currentGeneration = v.generation
|
|
@@ -74,6 +124,9 @@ export async function processVisitStatus(
|
|
|
74
124
|
session.ownerId,
|
|
75
125
|
status,
|
|
76
126
|
statusAt,
|
|
127
|
+
dayHundredths,
|
|
128
|
+
workDate,
|
|
129
|
+
workNote,
|
|
77
130
|
env.PORTAL_ACCOUNT_ID,
|
|
78
131
|
session.ownerId,
|
|
79
132
|
visitId,
|
|
@@ -83,6 +136,9 @@ export async function processVisitStatus(
|
|
|
83
136
|
session.ownerId,
|
|
84
137
|
status,
|
|
85
138
|
statusAt,
|
|
139
|
+
dayHundredths,
|
|
140
|
+
workDate,
|
|
141
|
+
workNote,
|
|
86
142
|
env.PORTAL_ACCOUNT_ID,
|
|
87
143
|
visitId,
|
|
88
144
|
)
|
|
@@ -100,11 +156,12 @@ export async function processVisitStatus(
|
|
|
100
156
|
return { status: 404, payload: { ok: false, error: 'not found' } }
|
|
101
157
|
}
|
|
102
158
|
|
|
103
|
-
// Counts, the scope and the outcome. Never the
|
|
104
|
-
// a real person was
|
|
159
|
+
// Counts, the scope, whether a day was claimed, and the outcome. Never the
|
|
160
|
+
// status text, the date or the note: they describe what a real person was
|
|
161
|
+
// doing and when.
|
|
105
162
|
log(
|
|
106
163
|
`[data-portal] op=visit-status owner=${q(session.ownerId)} account=${q(session.accountId)} ` +
|
|
107
|
-
`scope=${session.recordScope} queued=${changes} result=ok`,
|
|
164
|
+
`scope=${session.recordScope} labour=${dayHundredths > 0} queued=${changes} result=ok`,
|
|
108
165
|
)
|
|
109
166
|
return { status: 200, payload: { ok: true, queued: changes } }
|
|
110
167
|
}
|
|
@@ -20,7 +20,7 @@ The `outlook` plugin gives the admin agent access to Microsoft 365 / Outlook.com
|
|
|
20
20
|
| `outlook-mail-search` | Microsoft Graph `$search` (KQL) over the mailbox. Preview-only, like `outlook-mail-list`, and pages the same way via `nextCursor`/`cursor`. Put sender/subject/recipient/date filters in the query: `from:`, `to:`, `subject:`, `received>=`. Graph forbids `$search`+`$filter`, so a strict date window is on `outlook-mail-list`. |
|
|
21
21
|
| `outlook-mail-fetch-body` | Complete body of one message by id (`GET /me/messages/{id}?$select=body`). HTML decoded to text, no preview cap. The full-read path — list/search return Microsoft's ~255-char preview only. Returns the envelope plus the whole body. |
|
|
22
22
|
| `outlook-mail-attachment` | List or download any message's attachments — received or draft. With `messageId` alone, LISTS them (`id`, `name`, `contentType`, `size`, `isInline`, and a `kind` of file/item/reference) via `GET /me/messages/{id}/attachments` and `$select` (no bytes). A draft is a message in the same collection, so passing a draftId lists what that draft carries; the returned `name`/`id` is what `outlook-draft-edit`'s `removeAttachments` takes. With `messageId`+`attachmentId`, DOWNLOADS one file attachment: its bytes are written to `{accountDir}/uploads/outlook/<messageHash>/<bytesHash>-<name>` at `0o600` and the saved path is returned, ready for the Read tool, `SendUserFile`, or an `outlook-mail-reply` attachment. Only file attachments download — item/reference kinds, empty payloads, and anything over 25 MB are refused with nothing written. `outlook-mail-list` / `outlook-mail-search` / `outlook-mail-fetch-body` report `hasAttachments` so the agent knows when to call this. Needs `Mail.Read`. |
|
|
23
|
-
| `outlook-mail-reply` | Reply in-thread (`createReply` / `createReplyAll` + send). Threads natively via conversationId. `replyAll`, added cc/bcc, and account-scoped file attachments (up to 25 MB per file; files over Graph's ~3 MB inline limit use a chunked upload session). Body is plain text, prepended above the quote. Needs `Mail.ReadWrite` + `Mail.Send`. |
|
|
23
|
+
| `outlook-mail-reply` | Reply in-thread (`createReply` / `createReplyAll` + send). Threads natively via conversationId. `replyAll`, added cc/bcc, and account-scoped file attachments (up to 25 MB per file; files over Graph's ~3 MB inline limit use a chunked upload session). Body is plain text, prepended above the quote. A reply whose recipients resolve **only** to the sending mailbox is **refused** before the send and its draft deleted: Graph addresses a reply to the original message's sender, so replying to your own sent message addresses it to yourself. An explicit `cc` does not exempt it — use `outlook-mail-send` with an explicit `to`. The resolved recipients are logged as `to=`. Needs `Mail.ReadWrite` + `Mail.Send`. |
|
|
24
24
|
| `outlook-mail-delete` | Move messages to Deleted Items (`POST /me/messages/{id}/move`). Recoverable — never hard-deletes. An id already gone is counted not-moved. Needs `Mail.ReadWrite`. |
|
|
25
25
|
| `outlook-mail-ingest` | Write operator-reviewed messages into the business graph as `:ConversationArchive {source:'email'}` threads (the analogue of the IMAP `email-ingest`). Input `decisions`: one `{messageId, disposition:'ingest'\|'discard'}` per listed message. Each `ingest` message's full body + envelope is fetched, threads group by Graph `conversationId`, and participants resolve closed-set against Neo4j (`:Person`/`:AdminUser` by `accountId`); an unresolved From/To/Cc address skips its whole thread and is returned to the operator (no `:Person` auto-create). Resolvable threads dispatch through the shared `conversation-archive-ingest.sh --source email`. Human-in-the-loop: nothing lands without an explicit per-message decision. The plugin's only Neo4j coupling. Needs `Mail.Read`. |
|
|
26
26
|
| `outlook-mail-otp-extract` | Poll the inbox for a one-time code from a sender (domain). Reads full bodies to extract it; optional subject regex and timeout (default 60 s). |
|
|
@@ -37,7 +37,7 @@ The `outlook` plugin gives the admin agent access to Microsoft 365 / Outlook.com
|
|
|
37
37
|
| `outlook-shared-mailbox-add` | Attach a shared mailbox (no sign-in, no licence) through the Full Access of a mailbox that is already connected. `address` is the shared mailbox; `via` names the connected mailbox when more than one is attached. Proves the access before storing anything, so a refusal is the real answer. |
|
|
38
38
|
| `outlook-shared-mailbox-remove` | Detach a shared mailbox. Drops the pointer only; no credential is touched. |
|
|
39
39
|
| `outlook-mail-send` | Send a message. No attachments → `POST /me/sendMail`; with attachments → draft, attach, send (`/me/sendMail` carries attachments inline and cannot fit a large file). to/cc/bcc arrays, subject, body, isHtml, attachments (account-dir paths, 25 MB/file). Success asserted on Graph 202; no message id on either route. Needs `Mail.Send` (+ `Mail.ReadWrite` to attach). |
|
|
40
|
-
| `outlook-draft` | Create a draft in Drafts, either a new message or a threaded reply. Returns the draft id; never sends. Attachments are attached before the id is returned, so the draft is ready to send. Needs `Mail.ReadWrite`. **New message** (no `replyToMessageId`): `POST /me/messages`, requires `to` + `subject`, same body model as `outlook-mail-send`, threads under nothing. **Threaded reply** (`replyToMessageId` = the id of the message being answered): `POST /me/messages/{id}/createReply` — the draft carries the parent's conversationId, so it lands under the original. This is the only way to draft a reply without sending it: `outlook-mail-reply` threads but always sends. Graph sets the recipients and `RE:` subject from the original and composes the body above its own quote, matching what `outlook-mail-reply` sends, so the threaded path is **plain text only** — `to`, `subject`, and `isHtml: true` are **refused** (not ignored) when replying. `replyAll` includes everyone on the original; `cc`/`bcc` add recipients. |
|
|
40
|
+
| `outlook-draft` | Create a draft in Drafts, either a new message or a threaded reply. Returns the draft id; never sends. Attachments are attached before the id is returned, so the draft is ready to send. Needs `Mail.ReadWrite`. **New message** (no `replyToMessageId`): `POST /me/messages`, requires `to` + `subject`, same body model as `outlook-mail-send`, threads under nothing. **Threaded reply** (`replyToMessageId` = the id of the message being answered): `POST /me/messages/{id}/createReply` — the draft carries the parent's conversationId, so it lands under the original. This is the only way to draft a reply without sending it: `outlook-mail-reply` threads but always sends. Graph sets the recipients and `RE:` subject from the original and composes the body above its own quote, matching what `outlook-mail-reply` sends, so the threaded path is **plain text only** — `to`, `subject`, and `isHtml: true` are **refused** (not ignored) when replying. `replyAll` includes everyone on the original; `cc`/`bcc` add recipients. The result carries `to`, the recipients the draft holds, so the self-addressed case above is visible without fetching the draft back; on this path it is reported rather than refused, because a draft is not sent and `outlook-draft-edit` can re-address it. |
|
|
41
41
|
| `outlook-draft-edit` | Update an existing draft in place (`PATCH /me/messages/{draftId}`) and/or add and remove attachments. Graph drafts are mutable — pass draftId plus the fields to change (to/cc/bcc/subject/body/isHtml/attachments/removeAttachments). `attachments` adds; `removeAttachments` takes files off by name or attachment id; both together replace a file in one call. Sent messages cannot be edited. Needs `Mail.ReadWrite`. |
|
|
42
42
|
| `outlook-draft-send` | Send an existing draft by id (`POST /me/messages/{id}/send`). Graph consumes the draft, returns 202. Needs `Mail.Send`. |
|
|
43
43
|
|
|
@@ -107,8 +107,12 @@ All log lines start with `[outlook-mcp]` and write to `server.log`. They are key
|
|
|
107
107
|
| Send args | `send-args account=<id> recipients=<N> hasBody=<bool>` |
|
|
108
108
|
| Sent | `sent account=<id> status=202 messageId=<id-or-null>` |
|
|
109
109
|
| Send failed | `send-failed account=<id> error=<msg>` |
|
|
110
|
+
| Reply sent | `mail-reply op=reply account=<id> id=<parentId> convId=<id\|null> replyAll=<bool> attachments=<N> to=<csv>` — `to=` is the recipients Graph resolved; before it, who a sent reply actually went to existed nowhere on disk |
|
|
111
|
+
| Reply refused (self-addressed) | `mail-reply op=self-reply-refused account=<id> id=<parentId> to=<csv>` — terminal, nothing sent, draft deleted |
|
|
112
|
+
| Self-reply guard could not run | `mail-reply op=self-reply-guard state=no-address account=<id> id=<parentId>` — the config carries no mailbox address, so the comparison is impossible and the send proceeds. Distinguishes "guard passed" from "guard absent", which a missing line cannot |
|
|
113
|
+
| Refused, draft not cleaned up | `mail-reply op=self-reply-cleanup-failed account=<id> draftId=<id> error=<msg>` — followed by the refusal; the orphan is visible in Drafts |
|
|
110
114
|
| Draft request | `draft-request account=<id> threaded=<bool>` |
|
|
111
|
-
| Draft created | `draft-created account=<id> id=<draftId> threaded=<bool> convId=<id\|null>` |
|
|
115
|
+
| Draft created | `draft-created account=<id> id=<draftId> threaded=<bool> convId=<id\|null> to=<csv>` |
|
|
112
116
|
| Draft failed | `draft-failed account=<id> threaded=<bool> error=<msg>` |
|
|
113
117
|
| Draft-send request | `draft-send-request account=<id> id=<draftId>` |
|
|
114
118
|
| Draft sent | `draft-sent account=<id> id=<draftId> status=202` |
|
|
@@ -56,3 +56,22 @@ Ask {{productName}}: "What messages has the bot received?" or "Show recent Teleg
|
|
|
56
56
|
**Can't send to a group:** The bot must be a member of the group. Add it via the group settings in Telegram, then try again.
|
|
57
57
|
|
|
58
58
|
**Messages not arriving:** Make sure the bot hasn't been blocked. Try sending `/start` to the bot directly.
|
|
59
|
+
|
|
60
|
+
## Sending from the dashboard
|
|
61
|
+
|
|
62
|
+
Open a Telegram conversation in the dashboard and you can type a reply straight
|
|
63
|
+
into it, the same way you already can with WhatsApp. The message goes out from
|
|
64
|
+
your bot.
|
|
65
|
+
|
|
66
|
+
Two things to know:
|
|
67
|
+
|
|
68
|
+
- **Direct messages only.** Group and supergroup chats show no message box. A
|
|
69
|
+
Telegram conversation is filed under the person who spoke, so a message sent
|
|
70
|
+
from a group thread would reach that one person privately rather than the
|
|
71
|
+
group — the box is hidden rather than doing something you did not intend.
|
|
72
|
+
- **History starts now.** Messages are only shown from the point this feature
|
|
73
|
+
was installed onward. A conversation that was already running looks empty
|
|
74
|
+
until the next message arrives, and then fills up from there.
|
|
75
|
+
|
|
76
|
+
Text only for now. Photos, files and voice notes are not available on Telegram
|
|
77
|
+
from the dashboard.
|
|
@@ -116,7 +116,7 @@ MERGE keys per label:
|
|
|
116
116
|
- `:Contact` — `(accountId, contactId)` — write-time MERGE
|
|
117
117
|
- `:PurchaseOrderLine` — `(accountId, poNumber, lineId)` — child of `:PurchaseOrder`
|
|
118
118
|
- `:Note` — `(accountId, noteId)` — child of `:Job` via `(:Note)-[:ABOUT]->(:Job)`, append-only CREATE (a fresh `noteId` per note, not a content MERGE), so a second note never overwrites the first
|
|
119
|
-
- `:Engineer` — `(accountId,
|
|
119
|
+
- `:Engineer` — `(accountId, engineerId)` — additional label on `:Person`, set via `memory-update`
|
|
120
120
|
- `:Supplier` — `(accountId, organizationId)` — additional label on `:Organization`
|
|
121
121
|
|
|
122
122
|
---
|
|
@@ -133,10 +133,10 @@ Microsoft Graph access for Outlook.com / Microsoft 365 mailboxes. Mail is read (
|
|
|
133
133
|
- **Multiple mailboxes:** one account can hold several mailboxes. Every operational tool takes an optional `mailbox` argument (email or graphUserId). With one mailbox, omit it. With two or more, name it — omitting it returns a refusal listing the connected mailboxes. `outlook-mailbox-list` shows them all.
|
|
134
134
|
- **Mail (read):** `outlook-mail-list` returns messages newest-first with an optional `since`/`before` date window, and `outlook-mail-search` runs a Graph `$search` (KQL) query where `from:`/`to:`/`subject:`/`received>=` filters live — both return Microsoft's short `bodyPreview` (~255 chars) for cheap triage and a `nextCursor` for paging beyond the 250-per-page window (the cursor is an opaque `@odata.nextLink`; pass it back as `cursor`). Graph forbids `$search`+`$filter`, so the strict date window is on the list tool and sender/subject filtering is on the search tool. `outlook-mail-fetch-body` reads the COMPLETE body of one message by id (HTML decoded to text, no preview cap) — the full-read path. All three read tools report `hasAttachments` per message, so the agent knows to reach for the attachment tool.
|
|
135
135
|
- **Mail (attachments):** `outlook-mail-attachment` lists or downloads any message's attachments — received or draft. Called with `messageId` alone it LISTS them (`id`, `name`, `contentType`, `size`, `isInline`, and a `kind` of `file`/`item`/`reference`) via `GET /me/messages/{id}/attachments` with `$select` (no bytes); a draft is a message in the same collection, so passing a draftId lists what that draft carries, and the returned `name`/`id` is the handle `outlook-draft-edit`'s `removeAttachments` takes. Called with `messageId`+`attachmentId` it DOWNLOADS one file attachment: the bytes are written to `{accountDir}/uploads/outlook/<messageHash>/<bytesHash>-<name>` at `0o600` and the saved absolute path is returned, ready for the Read tool, `SendUserFile`, or an `outlook-mail-reply` attachment. Only file attachments download — `item` (embedded message/event) and `reference` (cloud-drive link) kinds are refused, as are empty payloads and anything over the 25 MB cap; in each case nothing is written. Requires `Mail.Read`.
|
|
136
|
-
- **Mail (reply / delete):** `outlook-mail-reply` replies in-thread (native `conversationId` threading) with `replyAll`, added cc/bcc, and account-scoped file attachments (up to 25 MB per file — files over Graph's ~3 MB inline limit stream through a chunked upload session); the body is prepended above the quoted original. `outlook-mail-delete` moves messages to Deleted Items (recoverable — never hard-deletes). Both require `Mail.ReadWrite` (reply also needs `Mail.Send`).
|
|
136
|
+
- **Mail (reply / delete):** `outlook-mail-reply` replies in-thread (native `conversationId` threading) with `replyAll`, added cc/bcc, and account-scoped file attachments (up to 25 MB per file — files over Graph's ~3 MB inline limit stream through a chunked upload session); the body is prepended above the quoted original. A reply whose recipients resolve **only** to the sending mailbox is refused before the send and its draft deleted: Graph addresses a reply to the original message's sender, so replying to your own sent message addresses it to yourself. An explicit `cc` does not exempt it — use `outlook-mail-send` with an explicit `to` instead. Both `outlook-mail-reply` and `outlook-draft` log the resolved recipients as `to=`. `outlook-mail-delete` moves messages to Deleted Items (recoverable — never hard-deletes). Both require `Mail.ReadWrite` (reply also needs `Mail.Send`).
|
|
137
137
|
- **Mail (ingest to graph):** `outlook-mail-ingest` writes operator-reviewed messages into the business graph as `:ConversationArchive {source:'email'}` threads, the analogue of the IMAP `email-ingest`. Input is `decisions`: one `{messageId, disposition:'ingest'|'discard'}` per listed message. Each `ingest` message's full body + envelope is fetched via `runMailFetchBody`, threads are grouped by Graph `conversationId`, and participant resolution is closed-set — every From/To/Cc address must already resolve to a `:Person.email` or `:AdminUser.email` for the `accountId`, or its thread is skipped and the address is returned to the operator (no `:Person` auto-create). Resolvable threads dispatch one at a time through the shared `plugins/memory/bin/conversation-archive-ingest.sh --source email` (same RFC822 thread-JSON shape the `email` normaliser consumes). This is the plugin's only Neo4j coupling (`neo4j-driver`). Human-in-the-loop: nothing enters the graph without an explicit per-message decision; the `outlook-mail-ingest` skill drives the list → summarise → ask → ingest loop. Requires `Mail.Read`.
|
|
138
138
|
- **Mail (OTP):** `outlook-mail-otp-extract` polls the inbox for a one-time verification code from a named sender, reading full bodies to extract it — used during service authentication.
|
|
139
|
-
- **Mail (send / draft):** `outlook-mail-send` sends a new message — via `POST /me/sendMail` with no attachments, or composed as a draft, attached, and sent when `attachments` is present (`/me/sendMail` carries attachments inline and cannot fit a large file); `outlook-draft` creates a draft, attaches any files, and returns its id — either a new message (`to` + `subject` required) or, with `replyToMessageId`, a threaded reply created via `POST /me/messages/{id}/createReply` that lands under the original conversation (the only way to draft a reply without sending it; `outlook-mail-reply` threads but always sends). On the threaded path Graph sets the recipients and `RE:` subject from the original and composes the body above its own quote — the same email `outlook-mail-reply` sends — so that path is plain text only and `to`, `subject`, and `isHtml: true` are refused rather than silently dropped; `replyAll` and `cc`/`bcc` work as on reply. `outlook-draft-edit` updates an existing draft in place (Graph drafts are mutable) and edits its attachments — `attachments` adds files, `removeAttachments` takes them off by name or attachment id, and both together replace a file in one call; `outlook-draft-send` sends an existing draft by id. Recipients are to/cc/bcc arrays; body is HTML or plain text via `isHtml`; `attachments` are file paths inside the account directory, capped at 25 MB per file and validated before any Graph write. Success asserted on Graph 202. Requires `Mail.Send` + `Mail.ReadWrite`; a mailbox on the old read-only consent returns an actionable "re-register to grant send" signal.
|
|
139
|
+
- **Mail (send / draft):** `outlook-mail-send` sends a new message — via `POST /me/sendMail` with no attachments, or composed as a draft, attached, and sent when `attachments` is present (`/me/sendMail` carries attachments inline and cannot fit a large file); `outlook-draft` creates a draft, attaches any files, and returns its id — either a new message (`to` + `subject` required) or, with `replyToMessageId`, a threaded reply created via `POST /me/messages/{id}/createReply` that lands under the original conversation (the only way to draft a reply without sending it; `outlook-mail-reply` threads but always sends). On the threaded path Graph sets the recipients and `RE:` subject from the original and composes the body above its own quote — the same email `outlook-mail-reply` sends — so that path is plain text only and `to`, `subject`, and `isHtml: true` are refused rather than silently dropped; `replyAll` and `cc`/`bcc` work as on reply. The draft result carries `to`, the recipients the draft holds, so the self-addressed case above is visible without fetching the draft back; on this path it is reported rather than refused, because a draft is not sent. `outlook-draft-edit` updates an existing draft in place (Graph drafts are mutable) and edits its attachments — `attachments` adds files, `removeAttachments` takes them off by name or attachment id, and both together replace a file in one call; `outlook-draft-send` sends an existing draft by id. Recipients are to/cc/bcc arrays; body is HTML or plain text via `isHtml`; `attachments` are file paths inside the account directory, capped at 25 MB per file and validated before any Graph write. Success asserted on Graph 202. Requires `Mail.Send` + `Mail.ReadWrite`; a mailbox on the old read-only consent returns an actionable "re-register to grant send" signal.
|
|
140
140
|
- **Calendar (read + control):** `outlook-calendar-list` and `outlook-calendar-event` read events; `outlook-calendar-create` books an event; `outlook-calendar-update` reschedules or edits one; `outlook-calendar-cancel` cancels a meeting (notifying attendees when the account organises it) or deletes a personal appointment; `outlook-calendar-respond` accepts, declines, or tentatively accepts an invite; `outlook-calendar-freebusy` reports availability for one or more addresses over a window. Writes require the `Calendars.ReadWrite` consent; a mailbox on stale read-only consent returns a distinct signal naming the scope and `outlook-account-register`.
|
|
141
141
|
- **Contacts (read-only):** `outlook-contacts-list` returns contacts.
|
|
142
142
|
- **Health:** `outlook-mailbox-info` reports auth state, refresh-window status, and top-level folder count for one mailbox (name it with `mailbox` when several are attached). `outlook-mailbox-list` lists every attached mailbox with its email, scopes, and token health, reading local stores only, never the network. It reports an `addressable` count and a per-mailbox `status` (`ok` / `blank-email` / `refresh-expired` / `unreadable`) so an authenticated, addressable mailbox is distinguishable from one that is merely present: a bare `count` of 1 is not "connected" unless that mailbox is `status:ok`. Use these to answer "did mailbox X auth?" without grepping logs.
|
|
@@ -10,7 +10,7 @@ function configWithToken(accountId) {
|
|
|
10
10
|
const dir = mkdtempSync(join(tmpdir(), "outlook-attach-"));
|
|
11
11
|
const tokenStore = new TokenStore(accountId, dir);
|
|
12
12
|
tokenStore.store("access-token", "refresh-token", 3600, { scopes: ["Mail.ReadWrite", "Mail.Send"] });
|
|
13
|
-
return { clientId: "test-client", tenantId: "common", accountId, tokenStore };
|
|
13
|
+
return { clientId: "test-client", tenantId: "common", accountId, tokenStore, mailboxAddress: null };
|
|
14
14
|
}
|
|
15
15
|
const UPLOAD_URL = "https://upload.example.com/session/attach";
|
|
16
16
|
function mockRouted() {
|