@rubytech/create-maxy-code 0.1.297 → 0.1.298

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (146) hide show
  1. package/package.json +1 -1
  2. package/payload/platform/lib/models/dist/index.d.ts +7 -0
  3. package/payload/platform/lib/models/dist/index.d.ts.map +1 -1
  4. package/payload/platform/lib/models/dist/index.js +16 -1
  5. package/payload/platform/lib/models/dist/index.js.map +1 -1
  6. package/payload/platform/lib/models/src/index.ts +16 -0
  7. package/payload/platform/plugins/admin/skills/platform-architecture/SKILL.md +2 -2
  8. package/payload/platform/plugins/docs/references/admin-ui.md +1 -1
  9. package/payload/platform/plugins/email/.claude-plugin/plugin.json +1 -1
  10. package/payload/platform/plugins/email/PLUGIN.md +11 -3
  11. package/payload/platform/plugins/email/mcp/dist/__tests__/email-draft-edit.test.d.ts +2 -0
  12. package/payload/platform/plugins/email/mcp/dist/__tests__/email-draft-edit.test.d.ts.map +1 -0
  13. package/payload/platform/plugins/email/mcp/dist/__tests__/email-draft-edit.test.js +71 -0
  14. package/payload/platform/plugins/email/mcp/dist/__tests__/email-draft-edit.test.js.map +1 -0
  15. package/payload/platform/plugins/email/mcp/dist/__tests__/email-draft-send.test.d.ts +2 -0
  16. package/payload/platform/plugins/email/mcp/dist/__tests__/email-draft-send.test.d.ts.map +1 -0
  17. package/payload/platform/plugins/email/mcp/dist/__tests__/email-draft-send.test.js +95 -0
  18. package/payload/platform/plugins/email/mcp/dist/__tests__/email-draft-send.test.js.map +1 -0
  19. package/payload/platform/plugins/email/mcp/dist/__tests__/email-draft.test.js +56 -2
  20. package/payload/platform/plugins/email/mcp/dist/__tests__/email-draft.test.js.map +1 -1
  21. package/payload/platform/plugins/email/mcp/dist/__tests__/imap-drafts.test.js +93 -2
  22. package/payload/platform/plugins/email/mcp/dist/__tests__/imap-drafts.test.js.map +1 -1
  23. package/payload/platform/plugins/email/mcp/dist/__tests__/send-raw.test.d.ts +2 -0
  24. package/payload/platform/plugins/email/mcp/dist/__tests__/send-raw.test.d.ts.map +1 -0
  25. package/payload/platform/plugins/email/mcp/dist/__tests__/send-raw.test.js +38 -0
  26. package/payload/platform/plugins/email/mcp/dist/__tests__/send-raw.test.js.map +1 -0
  27. package/payload/platform/plugins/email/mcp/dist/index.js +76 -2
  28. package/payload/platform/plugins/email/mcp/dist/index.js.map +1 -1
  29. package/payload/platform/plugins/email/mcp/dist/lib/compose.d.ts +47 -0
  30. package/payload/platform/plugins/email/mcp/dist/lib/compose.d.ts.map +1 -0
  31. package/payload/platform/plugins/email/mcp/dist/lib/compose.js +75 -0
  32. package/payload/platform/plugins/email/mcp/dist/lib/compose.js.map +1 -0
  33. package/payload/platform/plugins/email/mcp/dist/lib/imap.d.ts +37 -0
  34. package/payload/platform/plugins/email/mcp/dist/lib/imap.d.ts.map +1 -1
  35. package/payload/platform/plugins/email/mcp/dist/lib/imap.js +117 -0
  36. package/payload/platform/plugins/email/mcp/dist/lib/imap.js.map +1 -1
  37. package/payload/platform/plugins/email/mcp/dist/lib/smtp.d.ts +18 -0
  38. package/payload/platform/plugins/email/mcp/dist/lib/smtp.d.ts.map +1 -1
  39. package/payload/platform/plugins/email/mcp/dist/lib/smtp.js +25 -0
  40. package/payload/platform/plugins/email/mcp/dist/lib/smtp.js.map +1 -1
  41. package/payload/platform/plugins/email/mcp/dist/tools/email-draft-edit.d.ts +9 -0
  42. package/payload/platform/plugins/email/mcp/dist/tools/email-draft-edit.d.ts.map +1 -0
  43. package/payload/platform/plugins/email/mcp/dist/tools/email-draft-edit.js +23 -0
  44. package/payload/platform/plugins/email/mcp/dist/tools/email-draft-edit.js.map +1 -0
  45. package/payload/platform/plugins/email/mcp/dist/tools/email-draft-send.d.ts +8 -0
  46. package/payload/platform/plugins/email/mcp/dist/tools/email-draft-send.d.ts.map +1 -0
  47. package/payload/platform/plugins/email/mcp/dist/tools/email-draft-send.js +52 -0
  48. package/payload/platform/plugins/email/mcp/dist/tools/email-draft-send.js.map +1 -0
  49. package/payload/platform/plugins/email/mcp/dist/tools/email-draft.d.ts +2 -8
  50. package/payload/platform/plugins/email/mcp/dist/tools/email-draft.d.ts.map +1 -1
  51. package/payload/platform/plugins/email/mcp/dist/tools/email-draft.js +6 -34
  52. package/payload/platform/plugins/email/mcp/dist/tools/email-draft.js.map +1 -1
  53. package/payload/platform/plugins/email/mcp/dist/tools/email-reply.d.ts.map +1 -1
  54. package/payload/platform/plugins/email/mcp/dist/tools/email-reply.js +1 -8
  55. package/payload/platform/plugins/email/mcp/dist/tools/email-reply.js.map +1 -1
  56. package/payload/platform/plugins/email/skills/email-composition/SKILL.md +15 -4
  57. package/payload/platform/services/claude-session-manager/dist/canonical-tool-names.generated.d.ts.map +1 -1
  58. package/payload/platform/services/claude-session-manager/dist/canonical-tool-names.generated.js +2 -0
  59. package/payload/platform/services/claude-session-manager/dist/canonical-tool-names.generated.js.map +1 -1
  60. package/payload/platform/services/claude-session-manager/dist/http-server.js +1 -1
  61. package/payload/platform/services/claude-session-manager/dist/http-server.js.map +1 -1
  62. package/payload/platform/services/claude-session-manager/dist/rc-daemon.d.ts +13 -0
  63. package/payload/platform/services/claude-session-manager/dist/rc-daemon.d.ts.map +1 -1
  64. package/payload/platform/services/claude-session-manager/dist/rc-daemon.js +71 -1
  65. package/payload/platform/services/claude-session-manager/dist/rc-daemon.js.map +1 -1
  66. package/payload/platform/templates/specialists/agents/personal-assistant.md +1 -1
  67. package/payload/server/public/assets/{AdminShell-DpjiG4KO.js → AdminShell-BnhxfX97.js} +1 -1
  68. package/payload/server/public/assets/{Checkbox-o5EbM1Sd.js → Checkbox-CVPUf_7I.js} +1 -1
  69. package/payload/server/public/assets/{Transcript-B-roO2vK.js → Transcript-FYmq-mL1.js} +1 -1
  70. package/payload/server/public/assets/{admin-DmBG61gA.js → admin-CF8K3hqE.js} +1 -1
  71. package/payload/server/public/assets/{architectureDiagram-Q4EWVU46-DCLfcVZf.js → architectureDiagram-Q4EWVU46-IVt2X8Dk.js} +1 -1
  72. package/payload/server/public/assets/audio-attachment-mime-DaInZ9sa.js +1 -0
  73. package/payload/server/public/assets/{blockDiagram-DXYQGD6D-DtTb6oGR.js → blockDiagram-DXYQGD6D-CQDngnsc.js} +1 -1
  74. package/payload/server/public/assets/browser-D_hZgtfv.js +1 -0
  75. package/payload/server/public/assets/{c4Diagram-AHTNJAMY-DHMgkbse.js → c4Diagram-AHTNJAMY-BjV_YA-L.js} +1 -1
  76. package/payload/server/public/assets/channel-gFnUr8f4.js +1 -0
  77. package/payload/server/public/assets/chat-COcrNTJi.js +1 -0
  78. package/payload/server/public/assets/{chunk-336JU56O-DQja-fc4.js → chunk-336JU56O-B0yBhSVq.js} +2 -2
  79. package/payload/server/public/assets/{chunk-426QAEUC-uBZXKz4Z.js → chunk-426QAEUC-BCmWl6vi.js} +1 -1
  80. package/payload/server/public/assets/{chunk-4TB4RGXK-DViPFnGL.js → chunk-4TB4RGXK-BImQYhPF.js} +1 -1
  81. package/payload/server/public/assets/{chunk-5FUZZQ4R-CdKJ7vdW.js → chunk-5FUZZQ4R-DrDSHQZv.js} +1 -1
  82. package/payload/server/public/assets/{chunk-5PVQY5BW-BIJiayBe.js → chunk-5PVQY5BW-CKX_LIok.js} +1 -1
  83. package/payload/server/public/assets/{chunk-EDXVE4YY-CDDqx-TB.js → chunk-EDXVE4YY-D1zHB217.js} +1 -1
  84. package/payload/server/public/assets/{chunk-ENJZ2VHE-BVNFvue5.js → chunk-ENJZ2VHE-SFpdtszU.js} +1 -1
  85. package/payload/server/public/assets/{chunk-ICPOFSXX-DEZQolUW.js → chunk-ICPOFSXX-cYdq7RKF.js} +1 -1
  86. package/payload/server/public/assets/{chunk-OYMX7WX6-Dn0reHna.js → chunk-OYMX7WX6-B7JLCext.js} +1 -1
  87. package/payload/server/public/assets/{chunk-U2HBQHQK-sqMpBHaS.js → chunk-U2HBQHQK-DWCeFwem.js} +1 -1
  88. package/payload/server/public/assets/{chunk-X2U36JSP-ByhTOa14.js → chunk-X2U36JSP-DGnHsoJ3.js} +1 -1
  89. package/payload/server/public/assets/{chunk-YZCP3GAM-CGo_et0d.js → chunk-YZCP3GAM-sg4lGGJV.js} +1 -1
  90. package/payload/server/public/assets/{chunk-ZZ45TVLE-B9aPwq_o.js → chunk-ZZ45TVLE-DtUoGmKE.js} +1 -1
  91. package/payload/server/public/assets/classDiagram-6PBFFD2Q-C_wlfUTz.js +1 -0
  92. package/payload/server/public/assets/classDiagram-v2-HSJHXN6E-Ltv5s3OY.js +1 -0
  93. package/payload/server/public/assets/clone-D0XoXc_v.js +1 -0
  94. package/payload/server/public/assets/{dagre-DE88U4Vk.js → dagre-Dfj_jbTe.js} +1 -1
  95. package/payload/server/public/assets/{dagre-KV5264BT-oeoQWi1f.js → dagre-KV5264BT-Bd1VlhUJ.js} +1 -1
  96. package/payload/server/public/assets/{data-6mvcS2_6.js → data-DkbQI40a.js} +1 -1
  97. package/payload/server/public/assets/{diagram-5BDNPKRD-HmWmPhlY.js → diagram-5BDNPKRD-Dqqyk8GE.js} +1 -1
  98. package/payload/server/public/assets/{diagram-G4DWMVQ6-Bkao4d6n.js → diagram-G4DWMVQ6-Xk3WKR_U.js} +1 -1
  99. package/payload/server/public/assets/{diagram-MMDJMWI5-Eh2fnSb-.js → diagram-MMDJMWI5-CAdGL9U7.js} +1 -1
  100. package/payload/server/public/assets/{diagram-TYMM5635-C8705_ZE.js → diagram-TYMM5635-DY1iYQVY.js} +1 -1
  101. package/payload/server/public/assets/{erDiagram-SMLLAGMA-Bkhe0ZPv.js → erDiagram-SMLLAGMA-BJ5HYKeb.js} +1 -1
  102. package/payload/server/public/assets/{file-download-DVUMxBgB.js → file-download-BYJN3Kgm.js} +1 -1
  103. package/payload/server/public/assets/{flowDiagram-DWJPFMVM-rQKH0HF0.js → flowDiagram-DWJPFMVM-B-q6x5Q_.js} +1 -1
  104. package/payload/server/public/assets/{ganttDiagram-T4ZO3ILL-C3oSBbAe.js → ganttDiagram-T4ZO3ILL-DbjwY_bG.js} +1 -1
  105. package/payload/server/public/assets/{gitGraphDiagram-UUTBAWPF-28GEOzIR.js → gitGraphDiagram-UUTBAWPF-C3bLrOmb.js} +1 -1
  106. package/payload/server/public/assets/{graph-B5R9qDya.js → graph-DmikPM4A.js} +2 -2
  107. package/payload/server/public/assets/{graph-labels-Bcq_R9X9.js → graph-labels-CtG6ckdG.js} +1 -1
  108. package/payload/server/public/assets/{graphlib-DhAwTSya.js → graphlib-BlOjhixq.js} +1 -1
  109. package/payload/server/public/assets/{infoDiagram-42DDH7IO-DnSzN29F.js → infoDiagram-42DDH7IO-BpaHbiXO.js} +1 -1
  110. package/payload/server/public/assets/{ishikawaDiagram-UXIWVN3A-B0mYhBS0.js → ishikawaDiagram-UXIWVN3A-B_DzwK8l.js} +1 -1
  111. package/payload/server/public/assets/{journeyDiagram-VCZTEJTY-CIcZskca.js → journeyDiagram-VCZTEJTY-COzTF-qc.js} +1 -1
  112. package/payload/server/public/assets/{jsx-runtime-DqAwfhuD.css → jsx-runtime-BjFkEGXb.css} +1 -1
  113. package/payload/server/public/assets/{kanban-definition-6JOO6SKY-DWEpWX0D.js → kanban-definition-6JOO6SKY-C2cOIZU6.js} +1 -1
  114. package/payload/server/public/assets/{line-D6evlE4c.js → line-Bwa6aUYZ.js} +1 -1
  115. package/payload/server/public/assets/{mermaid-parser.core-DyRhDebz.js → mermaid-parser.core-BFAF0BAk.js} +1 -1
  116. package/payload/server/public/assets/{mermaid.core-DaD391Xr.js → mermaid.core-CMOXmawT.js} +3 -3
  117. package/payload/server/public/assets/{mindmap-definition-QFDTVHPH-D7IMib-k.js → mindmap-definition-QFDTVHPH-bu0QLO17.js} +1 -1
  118. package/payload/server/public/assets/{pieDiagram-DEJITSTG-CyOSJozN.js → pieDiagram-DEJITSTG-DQUe6W2Z.js} +1 -1
  119. package/payload/server/public/assets/{public-Cj7vjoG1.js → public-CSEIGhb4.js} +5 -5
  120. package/payload/server/public/assets/{quadrantDiagram-34T5L4WZ-CHWWRW-7.js → quadrantDiagram-34T5L4WZ-ChhcwVRA.js} +1 -1
  121. package/payload/server/public/assets/{requirementDiagram-MS252O5E-x4-mHKrQ.js → requirementDiagram-MS252O5E-DwQXqTA9.js} +1 -1
  122. package/payload/server/public/assets/{sankeyDiagram-XADWPNL6-DvKrAe78.js → sankeyDiagram-XADWPNL6-zYqdptBM.js} +1 -1
  123. package/payload/server/public/assets/{sequenceDiagram-FGHM5R23-DW778VnP.js → sequenceDiagram-FGHM5R23-x2g84gb4.js} +1 -1
  124. package/payload/server/public/assets/{stateDiagram-FHFEXIEX-DVnPPuE8.js → stateDiagram-FHFEXIEX-CeUrqs2P.js} +1 -1
  125. package/payload/server/public/assets/stateDiagram-v2-QKLJ7IA2-B27DBP9X.js +1 -0
  126. package/payload/server/public/assets/{timeline-definition-GMOUNBTQ-CO4Sp85K.js → timeline-definition-GMOUNBTQ-CL-c9XN5.js} +1 -1
  127. package/payload/server/public/assets/{useSelectionMode-BK4pznA6.js → useSelectionMode-C1_ky4B6.js} +1 -1
  128. package/payload/server/public/assets/{vennDiagram-DHZGUBPP-OCcztoT8.js → vennDiagram-DHZGUBPP-By2HBDYr.js} +1 -1
  129. package/payload/server/public/assets/{wardleyDiagram-NUSXRM2D-C_1o3jux.js → wardleyDiagram-NUSXRM2D-iahvFvIS.js} +1 -1
  130. package/payload/server/public/assets/{xychartDiagram-5P7HB3ND-BPJxl9U1.js → xychartDiagram-5P7HB3ND-33qqdRhI.js} +1 -1
  131. package/payload/server/public/browser.html +6 -6
  132. package/payload/server/public/chat.html +8 -8
  133. package/payload/server/public/data.html +5 -5
  134. package/payload/server/public/graph.html +8 -8
  135. package/payload/server/public/index.html +8 -8
  136. package/payload/server/public/public.html +6 -6
  137. package/payload/server/server.js +11 -7
  138. package/payload/server/public/assets/audio-attachment-mime-D_YMUIBb.js +0 -1
  139. package/payload/server/public/assets/browser-DPqQnY8B.js +0 -1
  140. package/payload/server/public/assets/channel-BGB5Q-QM.js +0 -1
  141. package/payload/server/public/assets/chat-BpceL2qA.js +0 -1
  142. package/payload/server/public/assets/classDiagram-6PBFFD2Q-DdUR3hKH.js +0 -1
  143. package/payload/server/public/assets/classDiagram-v2-HSJHXN6E-B0ZE2WR0.js +0 -1
  144. package/payload/server/public/assets/clone-B5aO4uPo.js +0 -1
  145. package/payload/server/public/assets/stateDiagram-v2-QKLJ7IA2-B3nWH_hq.js +0 -1
  146. /package/payload/server/public/assets/{jsx-runtime-CynkhuPm.js → jsx-runtime-UQ0Z6nwN.js} +0 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rubytech/create-maxy-code",
3
- "version": "0.1.297",
3
+ "version": "0.1.298",
4
4
  "description": "Install Maxy — AI for Productive People",
5
5
  "bin": {
6
6
  "create-maxy-code": "./dist/index.js"
@@ -5,4 +5,11 @@ export declare const HAIKU_MODEL = "claude-haiku-4-5";
5
5
  export declare const MODEL_CONTEXT_WINDOW: Record<string, number>;
6
6
  /** Look up context window size; defaults to 200k for unknown models. */
7
7
  export declare function contextWindow(model: string): number;
8
+ /** Friendly labels for the canonical model ids, for the /chat composer's
9
+ * native-CC model dropdown (Task 820). A lever id outside this map renders
10
+ * with its raw id as the label (the composer's append-fallback). */
11
+ export declare const MODEL_DISPLAY_NAMES: Record<string, string>;
12
+ /** The dropdown label for a model id: friendly name when canonical, else the
13
+ * raw id (never misrepresents an off-map persisted lever). */
14
+ export declare function modelDisplayName(id: string): string;
8
15
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAOA,eAAO,MAAM,WAAW,uBAAuB,CAAC;AAChD,eAAO,MAAM,UAAU,wBAAwB,CAAC;AAChD,eAAO,MAAM,YAAY,sBAAsB,CAAC;AAChD,eAAO,MAAM,WAAW,qBAAqB,CAAC;AAE9C,eAAO,MAAM,oBAAoB,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAKvD,CAAC;AAEF,wEAAwE;AACxE,wBAAgB,aAAa,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAEnD"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAOA,eAAO,MAAM,WAAW,uBAAuB,CAAC;AAChD,eAAO,MAAM,UAAU,wBAAwB,CAAC;AAChD,eAAO,MAAM,YAAY,sBAAsB,CAAC;AAChD,eAAO,MAAM,WAAW,qBAAqB,CAAC;AAE9C,eAAO,MAAM,oBAAoB,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAKvD,CAAC;AAEF,wEAAwE;AACxE,wBAAgB,aAAa,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAEnD;AAED;;qEAEqE;AACrE,eAAO,MAAM,mBAAmB,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAKtD,CAAC;AAEF;+DAC+D;AAC/D,wBAAgB,gBAAgB,CAAC,EAAE,EAAE,MAAM,GAAG,MAAM,CAEnD"}
@@ -3,8 +3,9 @@
3
3
  // Templates, docs, and skills reference these values as strings;
4
4
  // all TypeScript consumers import from here.
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.MODEL_CONTEXT_WINDOW = exports.HAIKU_MODEL = exports.SONNET_MODEL = exports.OPUS_MODEL = exports.FABLE_MODEL = void 0;
6
+ exports.MODEL_DISPLAY_NAMES = exports.MODEL_CONTEXT_WINDOW = exports.HAIKU_MODEL = exports.SONNET_MODEL = exports.OPUS_MODEL = exports.FABLE_MODEL = void 0;
7
7
  exports.contextWindow = contextWindow;
8
+ exports.modelDisplayName = modelDisplayName;
8
9
  // Fable and Opus default to the 1M-context variant ([1m] is Claude Code's
9
10
  // context-extension suffix, valid in settings.json "model" and agent
10
11
  // frontmatter); Sonnet/Haiku stay on the standard 200k window.
@@ -22,4 +23,18 @@ exports.MODEL_CONTEXT_WINDOW = {
22
23
  function contextWindow(model) {
23
24
  return exports.MODEL_CONTEXT_WINDOW[model] ?? 200_000;
24
25
  }
26
+ /** Friendly labels for the canonical model ids, for the /chat composer's
27
+ * native-CC model dropdown (Task 820). A lever id outside this map renders
28
+ * with its raw id as the label (the composer's append-fallback). */
29
+ exports.MODEL_DISPLAY_NAMES = {
30
+ [exports.FABLE_MODEL]: "Fable 5 (1M context)",
31
+ [exports.OPUS_MODEL]: "Opus 4.8 (1M context)",
32
+ [exports.SONNET_MODEL]: "Sonnet 4.6",
33
+ [exports.HAIKU_MODEL]: "Haiku 4.5",
34
+ };
35
+ /** The dropdown label for a model id: friendly name when canonical, else the
36
+ * raw id (never misrepresents an off-map persisted lever). */
37
+ function modelDisplayName(id) {
38
+ return exports.MODEL_DISPLAY_NAMES[id] ?? id;
39
+ }
25
40
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AAAA,0DAA0D;AAC1D,iEAAiE;AACjE,6CAA6C;;;AAkB7C,sCAEC;AAlBD,0EAA0E;AAC1E,qEAAqE;AACrE,+DAA+D;AAClD,QAAA,WAAW,GAAG,oBAAoB,CAAC;AACnC,QAAA,UAAU,GAAG,qBAAqB,CAAC;AACnC,QAAA,YAAY,GAAG,mBAAmB,CAAC;AACnC,QAAA,WAAW,GAAG,kBAAkB,CAAC;AAEjC,QAAA,oBAAoB,GAA2B;IAC1D,CAAC,mBAAW,CAAC,EAAE,SAAS;IACxB,CAAC,kBAAU,CAAC,EAAE,SAAS;IACvB,CAAC,oBAAY,CAAC,EAAE,OAAO;IACvB,CAAC,mBAAW,CAAC,EAAE,OAAO;CACvB,CAAC;AAEF,wEAAwE;AACxE,SAAgB,aAAa,CAAC,KAAa;IACzC,OAAO,4BAAoB,CAAC,KAAK,CAAC,IAAI,OAAO,CAAC;AAChD,CAAC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AAAA,0DAA0D;AAC1D,iEAAiE;AACjE,6CAA6C;;;AAkB7C,sCAEC;AAcD,4CAEC;AAlCD,0EAA0E;AAC1E,qEAAqE;AACrE,+DAA+D;AAClD,QAAA,WAAW,GAAG,oBAAoB,CAAC;AACnC,QAAA,UAAU,GAAG,qBAAqB,CAAC;AACnC,QAAA,YAAY,GAAG,mBAAmB,CAAC;AACnC,QAAA,WAAW,GAAG,kBAAkB,CAAC;AAEjC,QAAA,oBAAoB,GAA2B;IAC1D,CAAC,mBAAW,CAAC,EAAE,SAAS;IACxB,CAAC,kBAAU,CAAC,EAAE,SAAS;IACvB,CAAC,oBAAY,CAAC,EAAE,OAAO;IACvB,CAAC,mBAAW,CAAC,EAAE,OAAO;CACvB,CAAC;AAEF,wEAAwE;AACxE,SAAgB,aAAa,CAAC,KAAa;IACzC,OAAO,4BAAoB,CAAC,KAAK,CAAC,IAAI,OAAO,CAAC;AAChD,CAAC;AAED;;qEAEqE;AACxD,QAAA,mBAAmB,GAA2B;IACzD,CAAC,mBAAW,CAAC,EAAE,sBAAsB;IACrC,CAAC,kBAAU,CAAC,EAAE,uBAAuB;IACrC,CAAC,oBAAY,CAAC,EAAE,YAAY;IAC5B,CAAC,mBAAW,CAAC,EAAE,WAAW;CAC3B,CAAC;AAEF;+DAC+D;AAC/D,SAAgB,gBAAgB,CAAC,EAAU;IACzC,OAAO,2BAAmB,CAAC,EAAE,CAAC,IAAI,EAAE,CAAC;AACvC,CAAC"}
@@ -21,3 +21,19 @@ export const MODEL_CONTEXT_WINDOW: Record<string, number> = {
21
21
  export function contextWindow(model: string): number {
22
22
  return MODEL_CONTEXT_WINDOW[model] ?? 200_000;
23
23
  }
24
+
25
+ /** Friendly labels for the canonical model ids, for the /chat composer's
26
+ * native-CC model dropdown (Task 820). A lever id outside this map renders
27
+ * with its raw id as the label (the composer's append-fallback). */
28
+ export const MODEL_DISPLAY_NAMES: Record<string, string> = {
29
+ [FABLE_MODEL]: "Fable 5 (1M context)",
30
+ [OPUS_MODEL]: "Opus 4.8 (1M context)",
31
+ [SONNET_MODEL]: "Sonnet 4.6",
32
+ [HAIKU_MODEL]: "Haiku 4.5",
33
+ };
34
+
35
+ /** The dropdown label for a model id: friendly name when canonical, else the
36
+ * raw id (never misrepresents an off-map persisted lever). */
37
+ export function modelDisplayName(id: string): string {
38
+ return MODEL_DISPLAY_NAMES[id] ?? id;
39
+ }
@@ -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:e5c0c490243571baf0b7414b4e9a9f4806d3730b7f12829e9c7ed16c9445ed43
4
+ content-hash: sha256:63c8b9e4a7f9fedd5a441431b9fc4e94a4e19a6fcddd033bd76bee3963ed9eee
5
5
  brand: maxy-code
6
6
  product-name: Maxy
7
7
  ---
@@ -2267,7 +2267,7 @@ either is a regression.
2267
2267
  | `/session-rc-spawn` | POST `{ sessionId?, name? }` (Task 543). Fire-and-forget `claude --remote-control [name] [--session-id <sid>]`. Present `sessionId` resumes; absent starts a fresh session (also used by the sidebar's "New session" button — it no longer opens claude.ai/code directly). Proxies to the manager's `/rc-spawn`, which waits up to **60 s** (Task 648, raised from 12 s) for the spawned PTY to bind and returns `{ spawnedPid, sessionId, bridgeSessionId, slug, outcome, reason }`. The Sidebar navigates the opened tab to `claude.ai/code/session_<id>` on `outcome=bound`; on `timeout` or `spawn-failed` it shows an error modal (reason + sessionId) and **never** opens a bare claude.ai/code tab. The new process registers itself as its own Remote Control entry in claude.ai/code. | `POST /` |
2268
2268
  | `/claude-sessions` | **Spawn surface only** (Task 500). `POST /` is the Sidebar new-session-with-prompt path, cookie-auth only (Task 626 removed the recorder loopback caller; LinkedIn ingest moved to `/rc-spawn`). The former UI-facing handlers (SSE row feed, list, resume, stop, rename, archive, delete, `/:id/meta`, `/:id/input`, `/:id/log`) were removed — the maxy dashboard no longer manages or displays sessions. | `POST /` |
2269
2269
 
2270
- Task 500 — **admin session management moved entirely to claude's own interfaces** (claude.ai/code, claude desktop). A manager-owned per-account `claude rc --spawn same-dir` daemon registers the device as a Remote Control target there; the composer creates / resumes / stops / renames / archives / deletes sessions, with model + permission-mode applied at inception. The model lever is `account.json.adminModel` → `CLAUDE_CONFIG_DIR/settings.json "model"`, written by the daemon supervisor at boot; the reasoning-effort lever is `account.json.effort` → `settings.json "effortLevel"` (accepted values `low|medium|high|xhigh`; any other value, e.g. legacy `"auto"`, leaves the key unset so claude's own default governs), written by the same read-merge-write at boot. The maxy admin UI keeps a single "New session" link (`https://claude.ai/code`, opens in a new tab) and no session list, viewer, controls, or model/mode picker. The daemon supervisor lives at [`platform/services/claude-session-manager/src/rc-daemon.ts`](../../../services/claude-session-manager/src/rc-daemon.ts). The `/session-defaults` route and `SpawnPreference` node were deleted with the picker. `/new-session-failure`, `/new-session-submit`, and `/claude-capabilities` are now orphaned (consumed only by the deleted NewSessionModal) — see [`.tasks/501`](../../../.tasks/) for their removal.
2270
+ Task 500 — **admin session management moved entirely to claude's own interfaces** (claude.ai/code, claude desktop). A manager-owned per-account `claude rc --spawn same-dir` daemon registers the device as a Remote Control target there; the composer creates / resumes / stops / renames / archives / deletes sessions, with model + permission-mode applied at inception. The model lever is `account.json.adminModel` → `CLAUDE_CONFIG_DIR/settings.json "model"`, written by the daemon supervisor at boot; the reasoning-effort lever is `account.json.effort` → `settings.json "effortLevel"` (accepted values `low|medium|high|xhigh`; any other value, e.g. legacy `"auto"`, leaves the key unset so claude's own default governs), written by the same read-merge-write at boot. **Task 820** adds the third inception lever, permission mode: `account.json.adminPermissionMode` → `settings.json "permissions.defaultMode"` (the 5 composer-writable modes `default|acceptEdits|plan|auto|bypassPermissions`; `dontAsk` is out of scope). Unlike model/effort (top-level keys), this lever **deep-merges** the nested `permissions.defaultMode`, owning only that key and preserving sibling `permissions` keys (allow/deny/…) — what the binary and the Task 798 spawn lifeline actually read. All three levers are now changeable from the `/chat` composer (the model is a friendly-labelled checkmark dropdown via `MODEL_DISPLAY_NAMES`, effort a Faster↔Smarter slider, mode a 5-mode checkmark dropdown), each writing its `account.json` key then `POST /reapply-levers` so the next spawn honours it; the full write path is in `.docs/admin-webchat-native-channel.md`. Beyond the composer, the maxy admin UI keeps a single "New session" link (`https://claude.ai/code`, opens in a new tab) and no separate session list, viewer, controls, or model/mode picker. The daemon supervisor lives at [`platform/services/claude-session-manager/src/rc-daemon.ts`](../../../services/claude-session-manager/src/rc-daemon.ts). The `/session-defaults` route and `SpawnPreference` node were deleted with the picker. `/new-session-failure`, `/new-session-submit`, and `/claude-capabilities` are now orphaned (consumed only by the deleted NewSessionModal) — see [`.tasks/501`](../../../.tasks/) for their removal.
2271
2271
 
2272
2272
  **Row title resolution.** Both the sidebar (`/sidebar-sessions`) and the manager's own row payload resolve a row's title in the same order: operator rename → Claude Code `ai-title` → first non-CLI user message → 8-char sessionId prefix. The operator-rename tier is the on-disk `<accountDir>/session-titles.json` (the manager's `UserTitleStore`), keyed by the CC sessionId — the sidebar reads that same file, so a write to the store lights up both surfaces with one write.
2273
2273
 
@@ -49,7 +49,7 @@ either is a regression.
49
49
  | `/session-rc-spawn` | POST `{ sessionId?, name? }` (Task 543). Fire-and-forget `claude --remote-control [name] [--session-id <sid>]`. Present `sessionId` resumes; absent starts a fresh session (also used by the sidebar's "New session" button — it no longer opens claude.ai/code directly). Proxies to the manager's `/rc-spawn`, which waits up to **60 s** (Task 648, raised from 12 s) for the spawned PTY to bind and returns `{ spawnedPid, sessionId, bridgeSessionId, slug, outcome, reason }`. The Sidebar navigates the opened tab to `claude.ai/code/session_<id>` on `outcome=bound`; on `timeout` or `spawn-failed` it shows an error modal (reason + sessionId) and **never** opens a bare claude.ai/code tab. The new process registers itself as its own Remote Control entry in claude.ai/code. | `POST /` |
50
50
  | `/claude-sessions` | **Spawn surface only** (Task 500). `POST /` is the Sidebar new-session-with-prompt path, cookie-auth only (Task 626 removed the recorder loopback caller; LinkedIn ingest moved to `/rc-spawn`). The former UI-facing handlers (SSE row feed, list, resume, stop, rename, archive, delete, `/:id/meta`, `/:id/input`, `/:id/log`) were removed — the maxy dashboard no longer manages or displays sessions. | `POST /` |
51
51
 
52
- Task 500 — **admin session management moved entirely to claude's own interfaces** (claude.ai/code, claude desktop). A manager-owned per-account `claude rc --spawn same-dir` daemon registers the device as a Remote Control target there; the composer creates / resumes / stops / renames / archives / deletes sessions, with model + permission-mode applied at inception. The model lever is `account.json.adminModel` → `CLAUDE_CONFIG_DIR/settings.json "model"`, written by the daemon supervisor at boot; the reasoning-effort lever is `account.json.effort` → `settings.json "effortLevel"` (accepted values `low|medium|high|xhigh`; any other value, e.g. legacy `"auto"`, leaves the key unset so claude's own default governs), written by the same read-merge-write at boot. The maxy admin UI keeps a single "New session" link (`https://claude.ai/code`, opens in a new tab) and no session list, viewer, controls, or model/mode picker. The daemon supervisor lives at [`platform/services/claude-session-manager/src/rc-daemon.ts`](../../../services/claude-session-manager/src/rc-daemon.ts). The `/session-defaults` route and `SpawnPreference` node were deleted with the picker. `/new-session-failure`, `/new-session-submit`, and `/claude-capabilities` are now orphaned (consumed only by the deleted NewSessionModal) — see [`.tasks/501`](../../../.tasks/) for their removal.
52
+ Task 500 — **admin session management moved entirely to claude's own interfaces** (claude.ai/code, claude desktop). A manager-owned per-account `claude rc --spawn same-dir` daemon registers the device as a Remote Control target there; the composer creates / resumes / stops / renames / archives / deletes sessions, with model + permission-mode applied at inception. The model lever is `account.json.adminModel` → `CLAUDE_CONFIG_DIR/settings.json "model"`, written by the daemon supervisor at boot; the reasoning-effort lever is `account.json.effort` → `settings.json "effortLevel"` (accepted values `low|medium|high|xhigh`; any other value, e.g. legacy `"auto"`, leaves the key unset so claude's own default governs), written by the same read-merge-write at boot. **Task 820** adds the third inception lever, permission mode: `account.json.adminPermissionMode` → `settings.json "permissions.defaultMode"` (the 5 composer-writable modes `default|acceptEdits|plan|auto|bypassPermissions`; `dontAsk` is out of scope). Unlike model/effort (top-level keys), this lever **deep-merges** the nested `permissions.defaultMode`, owning only that key and preserving sibling `permissions` keys (allow/deny/…) — what the binary and the Task 798 spawn lifeline actually read. All three levers are now changeable from the `/chat` composer (the model is a friendly-labelled checkmark dropdown via `MODEL_DISPLAY_NAMES`, effort a Faster↔Smarter slider, mode a 5-mode checkmark dropdown), each writing its `account.json` key then `POST /reapply-levers` so the next spawn honours it; the full write path is in `.docs/admin-webchat-native-channel.md`. Beyond the composer, the maxy admin UI keeps a single "New session" link (`https://claude.ai/code`, opens in a new tab) and no separate session list, viewer, controls, or model/mode picker. The daemon supervisor lives at [`platform/services/claude-session-manager/src/rc-daemon.ts`](../../../services/claude-session-manager/src/rc-daemon.ts). The `/session-defaults` route and `SpawnPreference` node were deleted with the picker. `/new-session-failure`, `/new-session-submit`, and `/claude-capabilities` are now orphaned (consumed only by the deleted NewSessionModal) — see [`.tasks/501`](../../../.tasks/) for their removal.
53
53
 
54
54
  **Row title resolution.** Both the sidebar (`/sidebar-sessions`) and the manager's own row payload resolve a row's title in the same order: operator rename → Claude Code `ai-title` → first non-CLI user message → 8-char sessionId prefix. The operator-rename tier is the on-disk `<accountDir>/session-titles.json` (the manager's `UserTitleStore`), keyed by the CC sessionId — the sidebar reads that same file, so a write to the store lights up both surfaces with one write.
55
55
 
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "email",
3
- "description": "Email — dedicated email account. Retrieval: email-graph-query for stored email-thread history as :ConversationArchive {source:'email'} with :Section chunks (recall, search, 'threads about X', 'what did Y send?'); email-read / email-search for live IMAP inbox (with best-effort per-archive section hints from the graph). Ingestion (operator-initiated, human-in-the-loop): email-fetch lists new messages, email-ingest writes the operator-approved subset via the conversation-archive pipeline. Sending: email-send (new), email-reply (threaded — resolves the original via IMAP Message-ID search), email-draft (compose to the mailbox Drafts folder, unsent, for operator review). Config: email-provider-info (pre-password host + app-password lookup from an address alone), email-setup, email-status, email-otp-extract.",
3
+ "description": "Email — dedicated email account. Retrieval: email-graph-query for stored email-thread history as :ConversationArchive {source:'email'} with :Section chunks (recall, search, 'threads about X', 'what did Y send?'); email-read / email-search for live IMAP inbox (with best-effort per-archive section hints from the graph). Ingestion (operator-initiated, human-in-the-loop): email-fetch lists new messages, email-ingest writes the operator-approved subset via the conversation-archive pipeline. Sending: email-send (new), email-reply (threaded — resolves the original via IMAP Message-ID search), email-draft (compose to the mailbox Drafts folder, unsent, for operator review; pass messageId for a threaded draft-reply), email-draft-edit (revise a stored draft by Drafts UID), email-draft-send (dispatch a stored draft by Drafts UID, then remove it). Config: email-provider-info (pre-password host + app-password lookup from an address alone), email-setup, email-status, email-otp-extract.",
4
4
  "version": "0.1.0",
5
5
  "author": {
6
6
  "name": "Rubytech LLC"
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: email
3
- description: "Email — dedicated email account. Retrieval: email-graph-query for stored email-thread history as :ConversationArchive {source:'email'} with :Section chunks (recall, search, 'threads about X', 'what did Y send?'); email-read / email-search for live IMAP inbox (with best-effort per-archive section hints from the graph). Ingestion (operator-initiated, human-in-the-loop): email-fetch lists new messages, email-ingest writes the operator-approved subset via the conversation-archive pipeline. Sending: email-send (new), email-reply (threaded — resolves the original via IMAP Message-ID search), email-draft (compose to the mailbox Drafts folder, unsent, for operator review). Config: email-provider-info (pre-password host + app-password lookup from an address alone), email-setup, email-status, email-otp-extract."
3
+ description: "Email — dedicated email account. Retrieval: email-graph-query for stored email-thread history as :ConversationArchive {source:'email'} with :Section chunks (recall, search, 'threads about X', 'what did Y send?'); email-read / email-search for live IMAP inbox (with best-effort per-archive section hints from the graph). Ingestion (operator-initiated, human-in-the-loop): email-fetch lists new messages, email-ingest writes the operator-approved subset via the conversation-archive pipeline. Sending: email-send (new), email-reply (threaded — resolves the original via IMAP Message-ID search), email-draft (compose to the mailbox Drafts folder, unsent, for operator review; pass messageId for a threaded draft-reply), email-draft-edit (revise a stored draft by Drafts UID), email-draft-send (dispatch a stored draft by Drafts UID, then remove it). Config: email-provider-info (pre-password host + app-password lookup from an address alone), email-setup, email-status, email-otp-extract."
4
4
  tools:
5
5
  - name: email-provider-info
6
6
  publicAllowlist: false
@@ -20,6 +20,12 @@ tools:
20
20
  - name: email-draft
21
21
  publicAllowlist: false
22
22
  adminAllowlist: false
23
+ - name: email-draft-edit
24
+ publicAllowlist: false
25
+ adminAllowlist: false
26
+ - name: email-draft-send
27
+ publicAllowlist: false
28
+ adminAllowlist: false
23
29
  - name: email-search
24
30
  publicAllowlist: false
25
31
  adminAllowlist: false
@@ -71,7 +77,9 @@ Manages the agent's own dedicated email account — IMAP for reading, SMTP for s
71
77
  - **Search inbox:** `email-search` — live IMAP search by sender, subject, body keyword, date range. Same per-archive hint and attachment footer as `email-read`.
72
78
  - **Send:** `email-send` — new outbound email. Pass `cc`/`bcc` (each an array of email addresses) to copy or blind-copy recipients — CC appears in the message header, BCC recipients receive the message without appearing in any header. Pass `attachments` (absolute file paths under the account directory, ≤25 MB each) to attach files as SMTP attachments; an invalid path fails the send with no email dispatched. The success message names the CC recipients and reports the BCC count without naming them.
73
79
  - **Reply:** `email-reply` — threaded reply to an existing email by `messageId`. Resolves the original envelope via IMAP `SEARCH HEADER Message-ID` (INBOX then `\Sent`), so threading works for any message still present on the server even if the graph never ingested it. Supports `replyAll`, `cc`/`bcc`, and `attachments` (same path rules as `email-send`). Explicit `cc`/`bcc` are added in addition to whatever `replyAll` places in `To`, passed through as given (no overlap de-duplication against `To`).
74
- - **Compose to Drafts:** `email-draft` — builds the same MIME message as `email-send` (same `cc`/`bcc`/`attachments` rules; unlike a dispatched send, the stored draft keeps its `Bcc:` header, because the draft bytes are the only recipient carrier) but APPENDs it to the mailbox's `\Drafts` special-use folder with the `\Draft` flag instead of dispatching it. Nothing is sent. The success message names the Drafts folder and the APPEND UID so a saved draft is never indistinguishable from a silent no-op. Fails naming the mailbox when the server advertises no `\Drafts` special-use folder — no fallback folder is picked. Use when the operator wants to review and send by hand.
80
+ - **Compose to Drafts:** `email-draft` — builds the same MIME message as `email-send` (same `cc`/`bcc`/`attachments` rules; unlike a dispatched send, the stored draft keeps its `Bcc:` header, because the draft bytes are the only recipient carrier) but APPENDs it to the mailbox's `\Drafts` special-use folder with the `\Draft` flag instead of dispatching it. Nothing is sent. Pass `messageId` to compose a threaded draft-reply: the parent is resolved via IMAP and the draft carries `In-Reply-To`/`References` plus a `Re:` subject from the parent, exactly like `email-reply` (the `subject` argument is then optional and ignored). The success message names the Drafts folder and the APPEND UID so a saved draft is never indistinguishable from a silent no-op. Fails naming the mailbox when the server advertises no `\Drafts` special-use folder — no fallback folder is picked. Use when the operator wants to review and send by hand.
81
+ - **Revise a draft:** `email-draft-edit` — replace a stored draft addressed by its Drafts UID. Composes the full replacement from the parameters (same shape as `email-draft`, plus `targetUid` and an optional `messageId` for a threaded revision), APPENDs it with `\Draft`, then deletes the old UID — the replacement is always appended before the old copy is expunged, so a mid-cycle failure never leaves zero copies. An absent `targetUid` fails before any append. Nothing is sent. The confirmation names the folder and both the old and new UIDs.
82
+ - **Send a draft:** `email-draft-send` — dispatch a stored draft addressed by its Drafts UID via SMTP, then remove it from Drafts. Recipients come from the draft's own To/Cc/Bcc headers (the `\Drafts` copy keeps its `Bcc:`), deduped, sent from the agent alias with an explicit SMTP envelope. The draft is deleted only after SMTP accepts; an SMTP failure leaves it in place and surfaces verbatim. A draft with no recipients is refused before sending. Operator-approved sends only.
75
83
  - **Ingestion — fetch:** `email-fetch` — list new IMAP messages since the stored high-water mark. Metadata + body preview only — including attachment names, mimetypes, and sizes so the operator sees what's attached before approving (Task 402). No bytes downloaded, no graph writes. Stages the batch in-memory for `email-ingest`.
76
84
  - **Ingestion — apply:** `email-ingest` — apply the operator's per-message decisions to the staged batch. Each Message-ID must carry a disposition (`ingest` or `discard`). Approved messages land on `:ConversationArchive {source:'email'}` via the source-agnostic conversation-archive pipeline (one archive per thread, sessionised into `:Section` chunks). Attachment bytes are fetched, archived to `{accountDir}/archive/email/<uidValidity>-<uid>/<sha256>-<filename>` (0o600, 25 MB cap), and each attachment lands as a content-addressed `:DigitalDocument` keyed on `attachmentId = sha256(bytes)`; the `:HAS_ENCLOSURE` edge from the parent `:ConversationArchive` is wired by `memory-ingest` on its next pass via the `pendingArchiveEdges` queue (Task 402). Advances the inbox high-water mark only after a successful write.
77
85
  - **Recall/history:** `email-graph-query` — search stored email archives in Neo4j by natural language, participant address, or date range. Each thread is one `:ConversationArchive {source:'email'}` with `:Section` chunks; operator participants attach via `:PARTICIPANT_IN` edges from `:Person`/`:AdminUser`. List mode surfaces a per-archive summary built from concatenated `:Section.summary` values; query mode runs vector search over the `section_embedding` index and dedups to the parent archive.
@@ -93,7 +101,7 @@ Email ingestion is **operator-initiated and human-in-the-loop**. There is no bac
93
101
  | Skill | Purpose |
94
102
  |-------|---------|
95
103
  | [email-ingest](skills/email-ingest/SKILL.md) | Operator-initiated ingestion. Drives "check my emails" → list via `email-fetch` → summarise → ask the operator per message → write the approved subset via `email-ingest`. |
96
- | [email-composition](skills/email-composition/SKILL.md) | Conversational wrapper for drafting replies, new emails, and inbox triage. Orchestrates the six `email-*` tools with human-in-the-loop approval on every send; draft intent ("leave it in drafts", "don't send yet") routes to `email-draft`. |
104
+ | [email-composition](skills/email-composition/SKILL.md) | Conversational wrapper for drafting replies, new emails, and inbox triage. Orchestrates the sending `email-*` tools with human-in-the-loop approval on every send; draft intent ("leave it in drafts", "don't send yet") routes to `email-draft`, threaded drafts pass `messageId`, and a stored draft is revised via `email-draft-edit` or sent via `email-draft-send`. |
97
105
 
98
106
  ## References
99
107
 
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=email-draft-edit.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"email-draft-edit.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/email-draft-edit.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,71 @@
1
+ import { beforeEach, describe, expect, it, vi } from "vitest";
2
+ const { composeMock, replaceMock } = vi.hoisted(() => ({
3
+ composeMock: vi.fn(async () => ({
4
+ raw: Buffer.from("MIME"),
5
+ resolvedSubject: "Re: Quote",
6
+ threadedParentId: null,
7
+ attachments: [],
8
+ })),
9
+ replaceMock: vi.fn(async () => ({ folder: "Drafts", oldUid: 5, newUid: 99, expunged: true })),
10
+ }));
11
+ vi.mock("../lib/compose.js", () => ({ composeDraft: composeMock }));
12
+ vi.mock("../lib/imap.js", () => ({ replaceDraft: replaceMock }));
13
+ vi.mock("../lib/credentials.js", () => ({
14
+ loadCredentials: vi.fn(async () => ({
15
+ credentials: { config: { email: "a@x.y", agentAddress: "a@x.y" }, password: "pw" },
16
+ })),
17
+ }));
18
+ import { emailDraftEdit } from "../tools/email-draft-edit.js";
19
+ describe("Task 819 — email-draft-edit", () => {
20
+ beforeEach(() => {
21
+ vi.clearAllMocks();
22
+ composeMock.mockResolvedValue({
23
+ raw: Buffer.from("MIME"),
24
+ resolvedSubject: "Re: Quote",
25
+ threadedParentId: null,
26
+ attachments: [],
27
+ });
28
+ replaceMock.mockResolvedValue({ folder: "Drafts", oldUid: 5, newUid: 99, expunged: true });
29
+ });
30
+ it("composes the replacement then replaces by UID; names the folder and both UIDs", async () => {
31
+ const text = await emailDraftEdit({
32
+ accountId: "acc-1",
33
+ targetUid: 5,
34
+ to: "c@d.e",
35
+ subject: "Quote",
36
+ body: "Revised body.",
37
+ });
38
+ expect(composeMock).toHaveBeenCalledTimes(1);
39
+ expect(replaceMock).toHaveBeenCalledWith(expect.anything(), "pw", 5, expect.any(Buffer));
40
+ expect(text).toContain("Drafts");
41
+ expect(text).toContain("5");
42
+ expect(text).toContain("99");
43
+ expect(text).toMatch(/unsent/); // a revise never dispatches
44
+ });
45
+ it("names the threaded parent when the revision is a threaded draft", async () => {
46
+ composeMock.mockResolvedValueOnce({
47
+ raw: Buffer.from("MIME"),
48
+ resolvedSubject: "Re: Quote",
49
+ threadedParentId: "<parent@x>",
50
+ attachments: [],
51
+ });
52
+ const text = await emailDraftEdit({
53
+ accountId: "acc-1",
54
+ targetUid: 5,
55
+ to: "c@d.e",
56
+ body: "v2",
57
+ messageId: "<parent@x>",
58
+ });
59
+ expect(text).toContain("threaded reply to <parent@x>");
60
+ });
61
+ it("propagates a parent-not-found compose error and never calls replaceDraft", async () => {
62
+ composeMock.mockRejectedValueOnce(new Error("No email found with Message-ID <gone@x>"));
63
+ await expect(emailDraftEdit({ accountId: "acc-1", targetUid: 5, to: "c@d.e", body: "v2", messageId: "<gone@x>" })).rejects.toThrow(/No email found/);
64
+ expect(replaceMock).not.toHaveBeenCalled();
65
+ });
66
+ it("surfaces a missing-target-UID error from replaceDraft verbatim", async () => {
67
+ replaceMock.mockRejectedValueOnce(new Error("Draft UID 404 not found in Drafts."));
68
+ await expect(emailDraftEdit({ accountId: "acc-1", targetUid: 404, to: "c@d.e", subject: "Q", body: "v2" })).rejects.toThrow(/Draft UID 404 not found/);
69
+ });
70
+ });
71
+ //# sourceMappingURL=email-draft-edit.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"email-draft-edit.test.js","sourceRoot":"","sources":["../../src/__tests__/email-draft-edit.test.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=email-draft-send.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"email-draft-send.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/email-draft-send.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,95 @@
1
+ import { beforeEach, describe, expect, it, vi } from "vitest";
2
+ const { fetchMock, sendRawMock, deleteMock } = vi.hoisted(() => ({
3
+ fetchMock: vi.fn(),
4
+ sendRawMock: vi.fn(),
5
+ deleteMock: vi.fn(),
6
+ }));
7
+ vi.mock("../lib/imap.js", () => ({ fetchDraftSource: fetchMock, deleteDraft: deleteMock }));
8
+ vi.mock("../lib/smtp.js", async (importOriginal) => {
9
+ const actual = await importOriginal();
10
+ return { ...actual, sendRaw: sendRawMock };
11
+ });
12
+ vi.mock("../lib/credentials.js", () => ({
13
+ loadCredentials: vi.fn(async () => ({
14
+ credentials: { config: { email: "a@x.y", agentAddress: "a@x.y" }, password: "pw" },
15
+ })),
16
+ }));
17
+ import { emailDraftSend } from "../tools/email-draft-send.js";
18
+ const draft = {
19
+ folder: "Drafts",
20
+ source: Buffer.from("MIME bytes"),
21
+ toAddresses: ["c@d.e"],
22
+ ccAddresses: ["f@g.h"],
23
+ bccAddresses: ["i@j.k"],
24
+ };
25
+ describe("Task 819 — email-draft-send", () => {
26
+ beforeEach(() => {
27
+ vi.clearAllMocks();
28
+ fetchMock.mockResolvedValue({ ...draft });
29
+ sendRawMock.mockResolvedValue({ messageId: "<sent@x>" });
30
+ deleteMock.mockResolvedValue({ folder: "Drafts", deleted: true });
31
+ });
32
+ it("sends raw with a To+Cc+Bcc envelope, THEN removes the draft", async () => {
33
+ const text = await emailDraftSend({ accountId: "acc-1", uid: 5 });
34
+ expect(sendRawMock).toHaveBeenCalledWith(expect.anything(), "pw", draft.source, {
35
+ from: "a@x.y",
36
+ to: ["c@d.e", "f@g.h", "i@j.k"],
37
+ });
38
+ expect(deleteMock).toHaveBeenCalledWith(expect.anything(), "pw", 5);
39
+ expect(text).toMatch(/sent to/i);
40
+ expect(text).toContain("<sent@x>");
41
+ expect(text).toContain("removed");
42
+ });
43
+ it("missing draft throws and never sends", async () => {
44
+ fetchMock.mockResolvedValueOnce(null);
45
+ await expect(emailDraftSend({ accountId: "acc-1", uid: 404 })).rejects.toThrow(/No draft with UID 404/);
46
+ expect(sendRawMock).not.toHaveBeenCalled();
47
+ });
48
+ it("a removal failure AFTER a successful send still reports success with a caveat", async () => {
49
+ deleteMock.mockRejectedValueOnce(new Error("Draft UID 5 not found in Drafts."));
50
+ const text = await emailDraftSend({ accountId: "acc-1", uid: 5 });
51
+ expect(sendRawMock).toHaveBeenCalledTimes(1);
52
+ expect(text).toMatch(/sent to/i);
53
+ expect(text).toContain("<sent@x>");
54
+ expect(text).toMatch(/could not be confirmed removed/);
55
+ });
56
+ it("SMTP failure leaves the draft in place (no delete) and surfaces verbatim", async () => {
57
+ sendRawMock.mockRejectedValueOnce(new Error("SMTP 550 rejected"));
58
+ await expect(emailDraftSend({ accountId: "acc-1", uid: 5 })).rejects.toThrow(/SMTP 550/);
59
+ expect(deleteMock).not.toHaveBeenCalled();
60
+ });
61
+ it("a draft with no recipients throws before sending", async () => {
62
+ fetchMock.mockResolvedValueOnce({
63
+ folder: "Drafts",
64
+ source: Buffer.from("x"),
65
+ toAddresses: [],
66
+ ccAddresses: [],
67
+ bccAddresses: [],
68
+ });
69
+ await expect(emailDraftSend({ accountId: "acc-1", uid: 5 })).rejects.toThrow(/no recipients/i);
70
+ expect(sendRawMock).not.toHaveBeenCalled();
71
+ });
72
+ it("de-duplicates an address present in both To and Cc", async () => {
73
+ fetchMock.mockResolvedValueOnce({
74
+ folder: "Drafts",
75
+ source: Buffer.from("x"),
76
+ toAddresses: ["c@d.e"],
77
+ ccAddresses: ["c@d.e"],
78
+ bccAddresses: [],
79
+ });
80
+ await emailDraftSend({ accountId: "acc-1", uid: 5 });
81
+ expect(sendRawMock).toHaveBeenCalledWith(expect.anything(), "pw", expect.any(Buffer), {
82
+ from: "a@x.y",
83
+ to: ["c@d.e"],
84
+ });
85
+ });
86
+ it("adds the alias diagnostic on SMTP failure when agentAddress differs from the login", async () => {
87
+ const { loadCredentials } = await import("../lib/credentials.js");
88
+ loadCredentials.mockResolvedValueOnce({
89
+ credentials: { config: { email: "login@x.y", agentAddress: "alias@x.y" }, password: "pw" },
90
+ });
91
+ sendRawMock.mockRejectedValueOnce(new Error("SMTP 553"));
92
+ await expect(emailDraftSend({ accountId: "acc-1", uid: 5 })).rejects.toThrow(/sending as alias@x\.y \(authenticated as login@x\.y\)/);
93
+ });
94
+ });
95
+ //# sourceMappingURL=email-draft-send.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"email-draft-send.test.js","sourceRoot":"","sources":["../../src/__tests__/email-draft-send.test.ts"],"names":[],"mappings":"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"}
@@ -1,9 +1,13 @@
1
1
  import { beforeEach, describe, expect, it, vi } from "vitest";
2
- const { appendDraftMock, sendEmailMock } = vi.hoisted(() => ({
2
+ const { appendDraftMock, sendEmailMock, fetchEnvMock } = vi.hoisted(() => ({
3
3
  appendDraftMock: vi.fn(async () => ({ folder: "[Gmail]/Drafts", uid: 27675 })),
4
4
  sendEmailMock: vi.fn(),
5
+ fetchEnvMock: vi.fn(),
6
+ }));
7
+ vi.mock("../lib/imap.js", () => ({
8
+ appendDraft: appendDraftMock,
9
+ fetchEnvelopeByMessageId: fetchEnvMock,
5
10
  }));
6
- vi.mock("../lib/imap.js", () => ({ appendDraft: appendDraftMock }));
7
11
  vi.mock("../lib/smtp.js", async (importOriginal) => {
8
12
  const actual = await importOriginal();
9
13
  // Real buildMailOptions/formatCopyNote (the shared composer under test);
@@ -72,4 +76,54 @@ describe("Task 815 — email-draft tool", () => {
72
76
  await expect(emailDraft({ accountId: "acc-1", to: "j@x.y", subject: "s", body: "b" })).rejects.toThrow(/Drafts folder not found for agent@example\.com/);
73
77
  });
74
78
  });
79
+ describe("Task 818 — threaded draft via messageId", () => {
80
+ beforeEach(() => {
81
+ vi.clearAllMocks();
82
+ });
83
+ it("threads via messageId: In-Reply-To/References + Re: subject from the parent", async () => {
84
+ fetchEnvMock.mockResolvedValueOnce({
85
+ messageId: "<parent@x>",
86
+ subject: "Quote",
87
+ fromAddress: "client@x.y",
88
+ });
89
+ const text = await emailDraft({
90
+ accountId: "acc-1",
91
+ to: "client@x.y",
92
+ body: "Sounds good.",
93
+ messageId: "<parent@x>",
94
+ });
95
+ expect(fetchEnvMock).toHaveBeenCalledWith(expect.anything(), "pw", "<parent@x>");
96
+ const [, , raw] = appendDraftMock.mock.calls[0];
97
+ const mime = raw.toString();
98
+ expect(mime).toMatch(/^In-Reply-To: <parent@x>/m);
99
+ expect(mime).toMatch(/^References: <parent@x>/m);
100
+ expect(mime).toMatch(/^Subject: Re: Quote/m);
101
+ expect(text).toContain("threaded reply to <parent@x>");
102
+ });
103
+ it("strips an existing Re: on the parent subject (single Re: prefix)", async () => {
104
+ fetchEnvMock.mockResolvedValueOnce({
105
+ messageId: "<p2@x>",
106
+ subject: "Re: Lease terms",
107
+ fromAddress: "client@x.y",
108
+ });
109
+ await emailDraft({ accountId: "acc-1", to: "client@x.y", body: "ok", messageId: "<p2@x>" });
110
+ const [, , raw] = appendDraftMock.mock.calls[0];
111
+ expect(raw.toString()).toMatch(/^Subject: Re: Lease terms/m);
112
+ });
113
+ it("standalone path takes no envelope lookup and stamps no threading headers", async () => {
114
+ await emailDraft({ accountId: "acc-1", to: "client@x.y", subject: "Hello", body: "b" });
115
+ expect(fetchEnvMock).not.toHaveBeenCalled();
116
+ const [, , raw] = appendDraftMock.mock.calls[0];
117
+ expect(raw.toString()).not.toMatch(/^In-Reply-To:/m);
118
+ });
119
+ it("parent not found: throws verbatim and never appends", async () => {
120
+ fetchEnvMock.mockResolvedValueOnce(null);
121
+ await expect(emailDraft({ accountId: "acc-1", to: "client@x.y", body: "b", messageId: "<gone@x>" })).rejects.toThrow(/No email found with Message-ID <gone@x>/);
122
+ expect(appendDraftMock).not.toHaveBeenCalled();
123
+ });
124
+ it("no messageId and no subject: throws before any IMAP work", async () => {
125
+ await expect(emailDraft({ accountId: "acc-1", to: "client@x.y", body: "b" })).rejects.toThrow(/requires a subject/);
126
+ expect(appendDraftMock).not.toHaveBeenCalled();
127
+ });
128
+ });
75
129
  //# sourceMappingURL=email-draft.test.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"email-draft.test.js","sourceRoot":"","sources":["../../src/__tests__/email-draft.test.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"email-draft.test.js","sourceRoot":"","sources":["../../src/__tests__/email-draft.test.ts"],"names":[],"mappings":"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"}
@@ -1,9 +1,22 @@
1
1
  import { beforeEach, describe, expect, it, vi } from "vitest";
2
2
  // Mock the imapflow module so appendDraft's internal createClient() yields our spy client.
3
+ // `order` records the wire sequence of append vs delete so the atomicity contract
4
+ // (append-before-expunge) can be asserted directly.
5
+ const order = [];
3
6
  const mockClient = {
4
7
  connect: vi.fn(async () => { }),
5
8
  list: vi.fn(async () => []),
6
- append: vi.fn(async () => ({ destination: "[Gmail]/Drafts", uid: 27675 })),
9
+ mailboxOpen: vi.fn(async () => ({})),
10
+ getMailboxLock: vi.fn(async () => ({ release: vi.fn() })),
11
+ append: vi.fn(async () => {
12
+ order.push("append");
13
+ return { destination: "[Gmail]/Drafts", uid: 27675 };
14
+ }),
15
+ messageDelete: vi.fn(async () => {
16
+ order.push("delete");
17
+ return true;
18
+ }),
19
+ fetchOne: vi.fn(async () => ({ uid: 5 })),
7
20
  logout: vi.fn(async () => { }),
8
21
  close: vi.fn(() => { }),
9
22
  };
@@ -13,7 +26,7 @@ vi.mock("imapflow", () => ({
13
26
  return mockClient;
14
27
  },
15
28
  }));
16
- import { appendDraft, resolveFolderName } from "../lib/imap.js";
29
+ import { appendDraft, resolveFolderName, replaceDraft, fetchDraftSource, deleteDraft, } from "../lib/imap.js";
17
30
  const config = {
18
31
  email: "agent@example.com",
19
32
  imapHost: "imap.example.com",
@@ -71,4 +84,82 @@ describe("Task 815 — appendDraft", () => {
71
84
  expect(mockClient.logout).toHaveBeenCalled();
72
85
  });
73
86
  });
87
+ describe("Task 819 — replaceDraft (read-modify-replace)", () => {
88
+ beforeEach(() => {
89
+ order.length = 0;
90
+ vi.clearAllMocks();
91
+ mockClient.list.mockResolvedValue([{ path: "Drafts", specialUse: "\\Drafts" }]);
92
+ mockClient.append.mockImplementation(async () => {
93
+ order.push("append");
94
+ return { destination: "Drafts", uid: 99 };
95
+ });
96
+ mockClient.messageDelete.mockImplementation(async () => {
97
+ order.push("delete");
98
+ return true;
99
+ });
100
+ mockClient.fetchOne.mockResolvedValue({ uid: 5 });
101
+ });
102
+ it("appends the replacement BEFORE expunging the old UID and returns both UIDs", async () => {
103
+ const r = await replaceDraft(config, "pw", 5, Buffer.from("new bytes"));
104
+ expect(order).toEqual(["append", "delete"]);
105
+ expect(mockClient.append).toHaveBeenCalledWith("Drafts", expect.any(Buffer), ["\\Draft"]);
106
+ expect(mockClient.messageDelete).toHaveBeenCalledWith("5", { uid: true });
107
+ expect(r).toEqual({ folder: "Drafts", oldUid: 5, newUid: 99, expunged: true });
108
+ });
109
+ it("throws and NEVER appends when the old UID is absent (no orphan created)", async () => {
110
+ mockClient.fetchOne.mockResolvedValueOnce(false);
111
+ await expect(replaceDraft(config, "pw", 404, Buffer.from("new"))).rejects.toThrow(/Draft UID 404 not found/);
112
+ expect(mockClient.append).not.toHaveBeenCalled();
113
+ expect(mockClient.messageDelete).not.toHaveBeenCalled();
114
+ });
115
+ it("append failure leaves the old UID intact (delete never runs)", async () => {
116
+ mockClient.append.mockRejectedValueOnce(new Error("APPEND failed"));
117
+ await expect(replaceDraft(config, "pw", 5, Buffer.from("new"))).rejects.toThrow(/APPEND failed/);
118
+ expect(mockClient.messageDelete).not.toHaveBeenCalled();
119
+ });
120
+ it("reports expunged:false when the old UID could not be deleted", async () => {
121
+ mockClient.messageDelete.mockResolvedValueOnce(false);
122
+ const r = await replaceDraft(config, "pw", 5, Buffer.from("new"));
123
+ expect(r.expunged).toBe(false);
124
+ expect(r.newUid).toBe(99);
125
+ });
126
+ });
127
+ describe("Task 819 — fetchDraftSource / deleteDraft", () => {
128
+ beforeEach(() => {
129
+ vi.clearAllMocks();
130
+ mockClient.list.mockResolvedValue([{ path: "Drafts", specialUse: "\\Drafts" }]);
131
+ mockClient.messageDelete.mockResolvedValue(true);
132
+ mockClient.fetchOne.mockResolvedValue({
133
+ uid: 5,
134
+ source: Buffer.from("From: a@b.c\r\nTo: c@d.e\r\nCc: f@g.h\r\nBcc: i@j.k\r\n\r\nbody"),
135
+ envelope: {
136
+ to: [{ address: "c@d.e" }],
137
+ cc: [{ address: "f@g.h" }],
138
+ bcc: [{ address: "i@j.k" }],
139
+ },
140
+ });
141
+ });
142
+ it("fetchDraftSource returns the raw source and envelope recipients (To/Cc/Bcc)", async () => {
143
+ const r = await fetchDraftSource(config, "pw", 5);
144
+ expect(r?.folder).toBe("Drafts");
145
+ expect(r?.source.toString()).toContain("To: c@d.e");
146
+ expect(r?.toAddresses).toEqual(["c@d.e"]);
147
+ expect(r?.ccAddresses).toEqual(["f@g.h"]);
148
+ expect(r?.bccAddresses).toEqual(["i@j.k"]);
149
+ });
150
+ it("fetchDraftSource returns null when the UID is absent", async () => {
151
+ mockClient.fetchOne.mockResolvedValueOnce(false);
152
+ expect(await fetchDraftSource(config, "pw", 404)).toBeNull();
153
+ });
154
+ it("deleteDraft expunges an existing UID", async () => {
155
+ const r = await deleteDraft(config, "pw", 5);
156
+ expect(mockClient.messageDelete).toHaveBeenCalledWith("5", { uid: true });
157
+ expect(r).toEqual({ folder: "Drafts", deleted: true });
158
+ });
159
+ it("deleteDraft throws when the UID is absent", async () => {
160
+ mockClient.fetchOne.mockResolvedValueOnce(false);
161
+ await expect(deleteDraft(config, "pw", 404)).rejects.toThrow(/Draft UID 404 not found/);
162
+ expect(mockClient.messageDelete).not.toHaveBeenCalled();
163
+ });
164
+ });
74
165
  //# sourceMappingURL=imap-drafts.test.js.map