kennel 0.2.3 → 0.2.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +23 -13
- package/dist/cli/dispatch.js +16 -3
- package/dist/cli/src/cli/dispatch.js +16 -3
- package/dist/cli/src/cli/usage.js +1 -1
- package/dist/cli/src/core/command.js +1 -0
- package/dist/cli/src/core/init.js +5 -5
- package/dist/cli/src/plugins/external.d.ts +28 -2
- package/dist/cli/src/plugins/external.js +77 -23
- package/dist/cli/src/site/server-plugins.d.ts +4 -9
- package/dist/cli/src/site/viewer.js +1 -0
- package/dist/cli/usage.js +1 -1
- package/dist/core/api-client.js +576 -10317
- package/dist/core/command.js +1 -0
- package/dist/core/core-folders.js +2313 -12088
- package/dist/core/init.js +5 -5
- package/dist/core/runtime.js +8237 -20713
- package/dist/package/preview.js +19617 -33352
- package/dist/plugins/external.d.ts +28 -2
- package/dist/plugins/external.js +77 -23
- package/dist/site/server-plugins.d.ts +4 -9
- package/dist/site/server-plugins.js +557 -10452
- package/dist/site/viewer.js +1 -0
- package/kernel/apps/web/package.json +2 -6
- package/kernel/apps/web/scripts/dev.mjs +53 -0
- package/kernel/apps/web/src/app.css +0 -3
- package/kernel/apps/web/src/external-plugins.ts +1 -1
- package/kernel/apps/web/src/folder-filter-icons.tsx +1 -1
- package/kernel/apps/web/src/folder-plugin-view.tsx +1 -1
- package/kernel/apps/web/src/main.tsx +0 -1
- package/kernel/apps/web/src/plugins.ts +1 -31
- package/kernel/apps/web/tsconfig.json +1 -2
- package/kernel/apps/web/vite.config.ts +32 -14
- package/kernel/packages/agents/package.json +1 -1
- package/kernel/packages/agents/src/agent-card.tsx +1 -1
- package/kernel/packages/agents/src/client.tsx +1 -1
- package/kernel/packages/agents/src/manifest.ts +1 -1
- package/kernel/packages/agents/src/server.ts +1 -1
- package/kernel/packages/agents/src/viewer.tsx +1 -1
- package/kernel/packages/api/package.json +1 -5
- package/kernel/packages/api/src/server/acp-live-plugin.ts +1 -1
- package/kernel/packages/api/src/server/api/create-agent-file.ts +2 -2
- package/kernel/packages/api/src/server/api/document-edit.ts +2 -2
- package/kernel/packages/api/src/server/api/open-path.ts +2 -2
- package/kernel/packages/api/src/server/folder-metadata-plugin.ts +17 -42
- package/kernel/packages/api/src/server/index.ts +0 -1
- package/kernel/packages/api/src/server/plugins.ts +52 -19
- package/kernel/packages/api/src/shared/viewer-route.ts +1 -1
- package/kernel/packages/api/test/core.test.ts +14 -26
- package/kernel/packages/markdown/README.md +1 -1
- package/kernel/packages/notes/package.json +1 -1
- package/kernel/packages/notes/src/client.tsx +1 -1
- package/kernel/packages/notes/src/manifest.ts +1 -1
- package/kernel/packages/notes/src/server.ts +1 -1
- package/kernel/packages/plugins/SKILL.md +39 -0
- package/kernel/{plugins/mail → packages/plugins}/package.json +53 -28
- package/kernel/packages/plugins/src/index.ts +52 -0
- package/kernel/packages/plugins/src/page.ts +17 -0
- package/kernel/{plugins/projects → packages/plugins}/tsconfig.json +5 -1
- package/kernel/packages/plugins/tsup.config.ts +25 -0
- package/kernel/packages/ui/src/components/content-issues.tsx +1 -1
- package/kernel/packages/ui/tsconfig.json +1 -1
- package/package.json +2 -3
- package/web/assets/{abnfDiagram-VRR7QNED-Bgfb1nEB.js → abnfDiagram-VRR7QNED-xd_g1uK3.js} +1 -1
- package/web/assets/app-CX20nNIa.js +2 -0
- package/web/assets/{app-options-D-RJiQ-X.js → app-options-CkPJE5ep.js} +1 -1
- package/web/assets/{app-view-header-BLvLq02T.js → app-view-header-CNk5KnA-.js} +1 -1
- package/web/assets/{arc-Dk_KI36B.js → arc-B0u5yBJA.js} +1 -1
- package/web/assets/{architectureDiagram-ZJ3FMSHR-NR_KhRCf.js → architectureDiagram-ZJ3FMSHR-DfxOAo8P.js} +1 -1
- package/web/assets/{ask-tray-TzeTcO6-.js → ask-tray-SscJecDY.js} +1 -1
- package/web/assets/{blockDiagram-677ZJIJ3-CkLkwx9C.js → blockDiagram-677ZJIJ3-Ci3Usxh6.js} +1 -1
- package/web/assets/{c4Diagram-LMCZKHZV-CFCUH8e_.js → c4Diagram-LMCZKHZV-BqgwIVgh.js} +1 -1
- package/web/assets/channel-X7WHyxxT.js +1 -0
- package/web/assets/{chunk-2Q5K7J3B-B_BPLWsW.js → chunk-2Q5K7J3B-CfX69l2U.js} +1 -1
- package/web/assets/{chunk-32BRIVSS-B1VB1hOy.js → chunk-32BRIVSS-CuFlzZkl.js} +1 -1
- package/web/assets/{chunk-5VM5RSS4-KFcYr8Co.js → chunk-5VM5RSS4-DidCMlJa.js} +1 -1
- package/web/assets/{chunk-EX3LRPZG-CpBX3kuK.js → chunk-EX3LRPZG-B5CDm1dS.js} +1 -1
- package/web/assets/{chunk-JWPE2WC7-BPSl40wG.js → chunk-JWPE2WC7-D5fGV_sH.js} +1 -1
- package/web/assets/{chunk-MOJQB5TN-D2RcYmoU.js → chunk-MOJQB5TN-ayhH5Q-H.js} +1 -1
- package/web/assets/{chunk-RYQCIY6F-m7Gyn-t3.js → chunk-RYQCIY6F-C4BfOOZn.js} +1 -1
- package/web/assets/{chunk-V7JOEXUC-TDvdopxS.js → chunk-V7JOEXUC-DpUEIM9V.js} +1 -1
- package/web/assets/{chunk-VR4S4FIN-DZTsG51a.js → chunk-VR4S4FIN-DoQuFFRh.js} +1 -1
- package/web/assets/{chunk-XXDRQBXY-Dp0--RE2.js → chunk-XXDRQBXY-CI9eiLwn.js} +1 -1
- package/web/assets/classDiagram-OUVF2IWQ-BjsLQ3dz.js +1 -0
- package/web/assets/classDiagram-v2-EOCWNBFH-BjsLQ3dz.js +1 -0
- package/web/assets/client-7OWw5kW2.js +6 -0
- package/web/assets/client-DYj_td4Q.js +17 -0
- package/web/assets/{client-j2kQmxsO.js → client-Dk-NBvJR.js} +4 -4
- package/web/assets/command-zkzH4hVz.js +38 -0
- package/web/assets/{copy-context-C0Uqsp-3.js → copy-context-D5oiWrYC.js} +1 -1
- package/web/assets/{cose-bilkent-JH36ORCC-CPu3PI8h.js → cose-bilkent-JH36ORCC-BHRk1mO3.js} +1 -1
- package/web/assets/{cynefin-VYW2F7L2-DWxftYAm.js → cynefin-VYW2F7L2-C1B09TRE.js} +1 -1
- package/web/assets/{cynefinDiagram-TSTJHNR4-DsmN2V-p.js → cynefinDiagram-TSTJHNR4-B27qXQbv.js} +1 -1
- package/web/assets/{dagre-VKFMJZFB-COEbApyY.js → dagre-VKFMJZFB-B1qEi32g.js} +1 -1
- package/web/assets/{diagram-FQU43EPY-0O9WGWKB.js → diagram-FQU43EPY-BMkkgADD.js} +1 -1
- package/web/assets/{diagram-G47NLZAW-CutEdMVh.js → diagram-G47NLZAW-Cb8_g1A5.js} +1 -1
- package/web/assets/{diagram-NH7WQ7WH-C9KZc3B2.js → diagram-NH7WQ7WH-B51wuc5m.js} +1 -1
- package/web/assets/{diagram-OA4YK3LP-D8kkPXbY.js → diagram-OA4YK3LP-xnsecbbI.js} +1 -1
- package/web/assets/{diagram-WEI45ONY-_YqiTaGP.js → diagram-WEI45ONY-DSZdyaAV.js} +1 -1
- package/web/assets/{document-Dgt5eZFj.js → document-Dc2dftTW.js} +3 -3
- package/web/assets/{ebnfDiagram-CCIWWBDH-JO-zQh6g.js → ebnfDiagram-CCIWWBDH-aTHmcUGy.js} +1 -1
- package/web/assets/{entities-C3wz7FCx.js → entities-CCUtpskY.js} +1 -1
- package/web/assets/{erDiagram-Q63AITRT-9UjQwJ4P.js → erDiagram-Q63AITRT-CUUPBcvo.js} +1 -1
- package/web/assets/{file-viewer-DZJ8nYnl.js → file-viewer-DJ3rsmef.js} +1 -1
- package/web/assets/{flowDiagram-23GEKE2U-Ca08tnYM.js → flowDiagram-23GEKE2U-aTQXx2Mh.js} +1 -1
- package/web/assets/{folder-image-BfGgxo7i.js → folder-image-Bnv43tzP.js} +1 -1
- package/web/assets/{folder-landing-qUKBCnG-.js → folder-landing-DCnzYXXY.js} +1 -1
- package/web/assets/{folder-list-BfFjv7rR.js → folder-list-DvVkVjiR.js} +1 -1
- package/web/assets/{folder-metadata-DCw8xzHk.js → folder-metadata-C9hMMzIH.js} +1 -1
- package/web/assets/{folder-mobile-bar-Ubz4babf.js → folder-mobile-bar-VoTL1GS0.js} +1 -1
- package/web/assets/folder-plugin-view-Cvd804ZU.js +6 -0
- package/web/assets/{ganttDiagram-NO4QXBWP-KnHzX5W3.js → ganttDiagram-NO4QXBWP-DiwdUp7u.js} +1 -1
- package/web/assets/{gitGraphDiagram-IHSO6WYX-Cm8woNtK.js → gitGraphDiagram-IHSO6WYX-BL2gG_IS.js} +1 -1
- package/web/assets/{index-CNyy98F8.js → index-CUTtMSyJ.js} +2 -2
- package/web/assets/index-CegvVQ1C.js +1 -0
- package/web/assets/index-Dwhf8F_U.css +1 -0
- package/web/assets/{infoDiagram-FWYZ7A6U-yyqmUtIb.js → infoDiagram-FWYZ7A6U-B3n0zZpt.js} +1 -1
- package/web/assets/{ishikawaDiagram-FXEZZL3T-BfvR_Zy4.js → ishikawaDiagram-FXEZZL3T-D7YytbEv.js} +1 -1
- package/web/assets/{journeyDiagram-5HDEW3XC-BKuw06HM.js → journeyDiagram-5HDEW3XC-BYwqEl3C.js} +1 -1
- package/web/assets/{kanban-definition-HUTT4EX6-n0010rmQ.js → kanban-definition-HUTT4EX6-BQHNJgXZ.js} +1 -1
- package/web/assets/{linear-C0d0lT8O.js → linear-BPiQCLNk.js} +1 -1
- package/web/assets/{mermaid.core-BDVbShyO.js → mermaid.core-CD-KvBjV.js} +4 -4
- package/web/assets/{metadata-KV0dpp4G.js → metadata-COJ7fZul.js} +1 -1
- package/web/assets/{mindmap-definition-LN4V7U3C--_0ks3GF.js → mindmap-definition-LN4V7U3C-C1L_gyoj.js} +1 -1
- package/web/assets/{pegDiagram-2B236MQR-CBoNIpKR.js → pegDiagram-2B236MQR-BDAfItqS.js} +1 -1
- package/web/assets/{pieDiagram-ENE6RG2P-MA5tkYh5.js → pieDiagram-ENE6RG2P-CkZYH3Vz.js} +1 -1
- package/web/assets/{popover-DBD7FF-W.js → popover-0oA2lLUV.js} +2 -2
- package/web/assets/{project-search-dialog-DM1IkSZu.js → project-search-dialog-B2oqMT6h.js} +1 -1
- package/web/assets/{quadrantDiagram-ABIIQ3AL-CWT1ryqF.js → quadrantDiagram-ABIIQ3AL-Dy8Al3YZ.js} +1 -1
- package/web/assets/{railroadDiagram-RFXS5EU6-ZiLB5FGi.js → railroadDiagram-RFXS5EU6-Dd9_yo7B.js} +1 -1
- package/web/assets/{requirementDiagram-TGXJPOKE-CQq9uneq.js → requirementDiagram-TGXJPOKE-DPhIUsbs.js} +1 -1
- package/web/assets/{sankeyDiagram-HTMAVEWB-qDG-0JqI.js → sankeyDiagram-HTMAVEWB-B2QkYtub.js} +1 -1
- package/web/assets/{search-DNrQA5fF.js → search-C1J0-xiB.js} +1 -1
- package/web/assets/select-CtXC_PLC.js +30 -0
- package/web/assets/{selection-popup-DmtTeRs2.js → selection-popup-qhlUFzGq.js} +1 -1
- package/web/assets/{sequenceDiagram-DBY2YBRQ-CiApurLO.js → sequenceDiagram-DBY2YBRQ-Bx7fcZDx.js} +1 -1
- package/web/assets/{sizeCapture-X5ZJPWSS-DZSR5QXm.js → sizeCapture-X5ZJPWSS-CIXIhl_A.js} +1 -1
- package/web/assets/{stateDiagram-2N3HPSRC-CjJJR9_z.js → stateDiagram-2N3HPSRC-DCrdAflB.js} +1 -1
- package/web/assets/stateDiagram-v2-6OUMAXLB-0KCFLYeQ.js +1 -0
- package/web/assets/{swimlanes-5IMT3BWC-CKmo1sg6.js → swimlanes-5IMT3BWC-DA2vc7iZ.js} +2 -2
- package/web/assets/swimlanesDiagram-G3AALYLV-CAw3AV84.js +8 -0
- package/web/assets/{timeline-definition-FHXFAJF6-D-NMwS3y.js → timeline-definition-FHXFAJF6-lKfJiV9V.js} +1 -1
- package/web/assets/{vennDiagram-L72KCM5P-CfL6LBWw.js → vennDiagram-L72KCM5P-DO1q2KzG.js} +1 -1
- package/web/assets/{view-context-gyDl_p2V.js → view-context-DXKM3-l1.js} +4 -4
- package/web/assets/{viewer-capabilities-Bpp8Uvcg.js → viewer-capabilities-Cig_xM9u.js} +1 -1
- package/web/assets/{wardleyDiagram-EHGQE667-DgldMxaj.js → wardleyDiagram-EHGQE667-Dal9LkuZ.js} +1 -1
- package/web/assets/{xychartDiagram-FW5EYKEG-CIoAIFX8.js → xychartDiagram-FW5EYKEG-B3dPLPII.js} +1 -1
- package/web/index.html +2 -2
- package/kernel/packages/api/test/partial-loading.test.ts +0 -43
- package/kernel/packages/folder-plugin/package.json +0 -50
- package/kernel/packages/folder-plugin/src/index.ts +0 -15
- package/kernel/packages/folder-plugin/tsconfig.json +0 -17
- package/kernel/plugins/calendar/SKILL.md +0 -173
- package/kernel/plugins/calendar/eslint.config.js +0 -10
- package/kernel/plugins/calendar/package.json +0 -73
- package/kernel/plugins/calendar/schema.json +0 -137
- package/kernel/plugins/calendar/src/calendar.tsx +0 -328
- package/kernel/plugins/calendar/src/client.tsx +0 -139
- package/kernel/plugins/calendar/src/context.ts +0 -48
- package/kernel/plugins/calendar/src/copy-button.tsx +0 -66
- package/kernel/plugins/calendar/src/event-dialog.tsx +0 -270
- package/kernel/plugins/calendar/src/index.ts +0 -4
- package/kernel/plugins/calendar/src/manifest.ts +0 -7
- package/kernel/plugins/calendar/src/parse.ts +0 -610
- package/kernel/plugins/calendar/src/server.ts +0 -160
- package/kernel/plugins/calendar/src/utils.ts +0 -80
- package/kernel/plugins/calendar/src/views.tsx +0 -418
- package/kernel/plugins/calendar/test/context.test.mjs +0 -60
- package/kernel/plugins/calendar/test/package.test.mjs +0 -43
- package/kernel/plugins/calendar/test/parse.test.mjs +0 -392
- package/kernel/plugins/calendar/test/server.test.mjs +0 -126
- package/kernel/plugins/calendar/tsconfig.json +0 -17
- package/kernel/plugins/mail/SKILL.md +0 -97
- package/kernel/plugins/mail/eslint.config.js +0 -10
- package/kernel/plugins/mail/src/api.ts +0 -359
- package/kernel/plugins/mail/src/bun-ffi.d.ts +0 -14
- package/kernel/plugins/mail/src/client.tsx +0 -14
- package/kernel/plugins/mail/src/flock.ts +0 -137
- package/kernel/plugins/mail/src/folders.ts +0 -37
- package/kernel/plugins/mail/src/history.ts +0 -485
- package/kernel/plugins/mail/src/index.ts +0 -3
- package/kernel/plugins/mail/src/mail-card.tsx +0 -123
- package/kernel/plugins/mail/src/mail-conversation.tsx +0 -153
- package/kernel/plugins/mail/src/mail-detail-page.tsx +0 -467
- package/kernel/plugins/mail/src/mail-hooks.ts +0 -252
- package/kernel/plugins/mail/src/mail-page.tsx +0 -109
- package/kernel/plugins/mail/src/mail-request.ts +0 -78
- package/kernel/plugins/mail/src/mail-send-box.tsx +0 -175
- package/kernel/plugins/mail/src/mailos.ts +0 -723
- package/kernel/plugins/mail/src/manifest.ts +0 -7
- package/kernel/plugins/mail/src/node.ts +0 -171
- package/kernel/plugins/mail/src/parse.ts +0 -139
- package/kernel/plugins/mail/src/server.ts +0 -125
- package/kernel/plugins/mail/src/types.ts +0 -10
- package/kernel/plugins/mail/src/viewer.tsx +0 -147
- package/kernel/plugins/mail/test/flock.test.mjs +0 -156
- package/kernel/plugins/mail/test/folders.test.mjs +0 -138
- package/kernel/plugins/mail/test/history.test.mjs +0 -460
- package/kernel/plugins/mail/test/mailos.test.mjs +0 -752
- package/kernel/plugins/mail/test/node.test.mjs +0 -207
- package/kernel/plugins/mail/test/package.test.mjs +0 -54
- package/kernel/plugins/mail/test/parse.test.mjs +0 -32
- package/kernel/plugins/mail/test/server.test.mjs +0 -116
- package/kernel/plugins/mail/test/viewer.test.mjs +0 -192
- package/kernel/plugins/mail/tsconfig.json +0 -17
- package/kernel/plugins/projects/SKILL.md +0 -73
- package/kernel/plugins/projects/eslint.config.js +0 -10
- package/kernel/plugins/projects/package.json +0 -70
- package/kernel/plugins/projects/src/client.tsx +0 -17
- package/kernel/plugins/projects/src/data/model.ts +0 -129
- package/kernel/plugins/projects/src/index.ts +0 -17
- package/kernel/plugins/projects/src/manifest.ts +0 -7
- package/kernel/plugins/projects/src/project-context.ts +0 -41
- package/kernel/plugins/projects/src/server.ts +0 -276
- package/kernel/plugins/projects/src/ui/project-card.tsx +0 -61
- package/kernel/plugins/projects/src/ui/projects-page.tsx +0 -102
- package/kernel/plugins/projects/test/package.test.mjs +0 -51
- package/kernel/plugins/tasks/SKILL.md +0 -341
- package/kernel/plugins/tasks/eslint.config.js +0 -10
- package/kernel/plugins/tasks/package.json +0 -74
- package/kernel/plugins/tasks/schema.json +0 -939
- package/kernel/plugins/tasks/src/api.ts +0 -212
- package/kernel/plugins/tasks/src/client.tsx +0 -23
- package/kernel/plugins/tasks/src/data/parse.ts +0 -544
- package/kernel/plugins/tasks/src/data/task-id.ts +0 -56
- package/kernel/plugins/tasks/src/data/todo-model.ts +0 -795
- package/kernel/plugins/tasks/src/index.ts +0 -34
- package/kernel/plugins/tasks/src/manifest.ts +0 -7
- package/kernel/plugins/tasks/src/pages/task-detail.tsx +0 -48
- package/kernel/plugins/tasks/src/pages/task-list.tsx +0 -88
- package/kernel/plugins/tasks/src/server.ts +0 -447
- package/kernel/plugins/tasks/src/styles.css +0 -9
- package/kernel/plugins/tasks/src/ui/compact-todo-list.tsx +0 -372
- package/kernel/plugins/tasks/src/ui/error-details.tsx +0 -78
- package/kernel/plugins/tasks/src/ui/task-page.tsx +0 -392
- package/kernel/plugins/tasks/src/ui/todo-agent-filter.tsx +0 -36
- package/kernel/plugins/tasks/src/ui/todo-filter-bar.tsx +0 -66
- package/kernel/plugins/tasks/src/ui/todo-item.tsx +0 -494
- package/kernel/plugins/tasks/src/ui/todo-status-filter.tsx +0 -60
- package/kernel/plugins/tasks/src/ui/todo-tag-filter.tsx +0 -48
- package/kernel/plugins/tasks/src/ui/todo-warnings.tsx +0 -106
- package/kernel/plugins/tasks/test/api.test.mjs +0 -99
- package/kernel/plugins/tasks/test/folder-sources.test.mjs +0 -46
- package/kernel/plugins/tasks/test/package.test.mjs +0 -54
- package/kernel/plugins/tasks/test/parse.test.mjs +0 -225
- package/kernel/plugins/tasks/test/server.test.mjs +0 -386
- package/kernel/plugins/tasks/tsconfig.json +0 -17
- package/web/assets/app-DoGr2Zi7.js +0 -2
- package/web/assets/channel-uMEjV8qc.js +0 -1
- package/web/assets/classDiagram-OUVF2IWQ-fbZocVor.js +0 -1
- package/web/assets/classDiagram-v2-EOCWNBFH-fbZocVor.js +0 -1
- package/web/assets/client-C6Nxdfif.js +0 -2
- package/web/assets/client-CXYfBko5.js +0 -17
- package/web/assets/client-CoKtOq-X.js +0 -7
- package/web/assets/client-DyRQz5RU.js +0 -53
- package/web/assets/client-OY6HUzuD.js +0 -98
- package/web/assets/client-YOYWZIFi.js +0 -34
- package/web/assets/command-BugZBGXE.js +0 -38
- package/web/assets/folder-plugin-view-DsWR1zmQ.js +0 -6
- package/web/assets/index-BaRnbYRw.js +0 -1
- package/web/assets/index-BexmLxIP.css +0 -1
- package/web/assets/index-DDirZMgx.js +0 -1
- package/web/assets/note-card-DBfX6gW1.js +0 -6
- package/web/assets/select-D8cqNgcp.js +0 -30
- package/web/assets/stateDiagram-v2-6OUMAXLB-BpeGSCu9.js +0 -1
- package/web/assets/swimlanesDiagram-G3AALYLV-D9RjLKb9.js +0 -8
- package/web/assets/use-folder-filter-LQyXns5Q.js +0 -1
- /package/kernel/packages/{folder-plugin → plugins}/eslint.config.js +0 -0
- /package/kernel/packages/{folder-plugin → plugins}/src/client.ts +0 -0
- /package/kernel/packages/{folder-plugin → plugins}/src/endpoint.ts +0 -0
- /package/kernel/packages/{folder-plugin → plugins}/src/server.ts +0 -0
- /package/kernel/packages/{folder-plugin → plugins}/src/subpath.ts +0 -0
- /package/kernel/packages/{folder-plugin → plugins}/src/types.ts +0 -0
- /package/kernel/packages/{folder-plugin → plugins}/src/validation.ts +0 -0
- /package/kernel/packages/{folder-plugin → plugins}/test/contract.test.mjs +0 -0
|
@@ -1,97 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: kennel-mail
|
|
3
|
-
description: Open and read local MailOS email drafts for a Kennel folder — Markdown draft files in `~/.email/drafts` with YAML frontmatter, filtered to drafts whose `folder` is the viewed folder or one of its subfolders. Use when the user asks to view drafts for a folder in the Kennel viewer, read mail drafts by subject, status, tags, or recipient, list which drafts belong to a workspace, open a specific draft, or see a draft's excerpt and revision.
|
|
4
|
-
---
|
|
5
|
-
|
|
6
|
-
# Kennel Mail Plugin
|
|
7
|
-
|
|
8
|
-
The Mail plugin (`@raggle-ai/mail`) shows local MailOS email drafts —
|
|
9
|
-
Markdown files with YAML frontmatter in `~/.email/drafts/*.md` — scoped to the
|
|
10
|
-
folder the user is viewing. It offers a folder selector, a draft list, and a
|
|
11
|
-
per-draft detail view, without invoking the MailOS binary.
|
|
12
|
-
|
|
13
|
-
This skill covers the agent-facing parts of that plugin: where drafts live,
|
|
14
|
-
which drafts are visible for a folder, and how to open and verify the view.
|
|
15
|
-
For the plugin package's implementation contract, see `plugins/README.md`
|
|
16
|
-
in the Kennel repo.
|
|
17
|
-
|
|
18
|
-
## When to use
|
|
19
|
-
|
|
20
|
-
- The user asks to open, list, or read email drafts for a folder in Kennel.
|
|
21
|
-
- The user wants to know which drafts belong to a workspace, or see a draft's
|
|
22
|
-
subject, status, tags, recipients, excerpt, or revision.
|
|
23
|
-
- Preview or verification work needs the Mail plugin tab to be available and
|
|
24
|
-
populated for a target folder.
|
|
25
|
-
|
|
26
|
-
## Workflow
|
|
27
|
-
|
|
28
|
-
1. **Identify the folder** whose drafts the user wants. The mail plugin shows
|
|
29
|
-
only drafts whose `folder` frontmatter value is that folder itself or a
|
|
30
|
-
real subfolder of it (checked through `realpath`, so symlink escapes and
|
|
31
|
-
sibling paths are rejected).
|
|
32
|
-
2. **Check for drafts.** Drafts live in `~/.email/drafts/` as `<id>.md`
|
|
33
|
-
files. Open the folder in the viewer and confirm the **Mail** plugin tab
|
|
34
|
-
appears; the tab only appears when at least one active draft belongs to the
|
|
35
|
-
folder, and its badge shows the draft count.
|
|
36
|
-
3. **Read the drafts.** Each draft is Markdown with YAML frontmatter followed
|
|
37
|
-
by the message body. Only drafts with a non-empty `id` and an active status
|
|
38
|
-
(`working` or `ready`) are listed — sent and canceled drafts are excluded,
|
|
39
|
-
even though MailOS keeps them in the same directory. Read values from the
|
|
40
|
-
files; do not invent recipients or subjects.
|
|
41
|
-
4. **Open the view.** `kennel serve <folder>` (or `kennel view <folder>` in a
|
|
42
|
-
source checkout), then point at the Mail tab. Launch, port, watch, and
|
|
43
|
-
link verification mechanics come from the `kennel-preview` skill — install
|
|
44
|
-
it alongside this one so the full workflow works.
|
|
45
|
-
5. **Verify before reporting.** Confirm the server is alive
|
|
46
|
-
(`curl -fsS "$URL/api/health"`), the printed URL serves the exact requested
|
|
47
|
-
folder root, and confirm the specific drafts the user asked about are in
|
|
48
|
-
the draft directory with an active status and the matching `folder` value.
|
|
49
|
-
|
|
50
|
-
## Content shape
|
|
51
|
-
|
|
52
|
-
### Draft file location
|
|
53
|
-
|
|
54
|
-
- Default mail directory: `~/.email/drafts/` (the server entry accepts a
|
|
55
|
-
`mailDir` override, intended for tests).
|
|
56
|
-
- Each draft is `<id>.md`; ids are validated (no path separators, no `.`/`..`)
|
|
57
|
-
before a file is read.
|
|
58
|
-
- Only files ending in `.md` are considered.
|
|
59
|
-
|
|
60
|
-
### Draft frontmatter
|
|
61
|
-
|
|
62
|
-
| Field | Meaning |
|
|
63
|
-
| --------------------------- | ------------------------------------------------------------------------------------------------- |
|
|
64
|
-
| `id` | Required for the draft to be listed |
|
|
65
|
-
| `status` | `working` \| `ready` (active, listed) — sent/canceled/other are hidden |
|
|
66
|
-
| `folder` | The folder this draft belongs to; must be the viewed folder or a real descendant (realpath check) |
|
|
67
|
-
| `tags` | Array of strings |
|
|
68
|
-
| `from`, `to`, `cc`, `bcc` | Sender; recipient lists (a raw string is wrapped into a single-element list) |
|
|
69
|
-
| `in_reply_to`, `references` | Reply threading |
|
|
70
|
-
| `subject` | Subject line |
|
|
71
|
-
| `created_at`, `updated_at` | Timestamps; the list is sorted by `updatedAt` (falling back to `createdAt`) descending |
|
|
72
|
-
|
|
73
|
-
The YAML body after the frontmatter is the message body. The list item also
|
|
74
|
-
carries an `excerpt` (first ~280 characters of the body) and a `revision` (a
|
|
75
|
-
sha256 of the exact draft file bytes — MailOS treats this hash as the draft
|
|
76
|
-
revision for guarded destructive actions).
|
|
77
|
-
|
|
78
|
-
## Verification
|
|
79
|
-
|
|
80
|
-
- Server alive: `curl -fsS --connect-timeout 3 --max-time 5 "$URL/api/health"`
|
|
81
|
-
- Folder root: request `/api/documents` and confirm `root` is the requested
|
|
82
|
-
absolute folder.
|
|
83
|
-
- Mail availability: the Mail plugin tab is present and its badge equals the
|
|
84
|
-
number of active drafts whose `folder` is inside the served folder.
|
|
85
|
-
- Per-draft checks: draft ids, subjects, statuses, and `folder` values in the
|
|
86
|
-
view match the `~/.email/drafts/*.md` source files; drafts with status
|
|
87
|
-
`sent`/`canceled` or a `folder` outside the served root do not appear.
|
|
88
|
-
|
|
89
|
-
## Boundaries
|
|
90
|
-
|
|
91
|
-
- This plugin is read-only over the draft directory: it lists and renders
|
|
92
|
-
drafts. Composing, sending, or changing draft status is done through MailOS
|
|
93
|
-
itself (the `mailos` CLI), not this plugin.
|
|
94
|
-
- Do not invent draft contents, recipients, or subjects — read them from the
|
|
95
|
-
files.
|
|
96
|
-
- The mail view only exists inside the Kennel viewer (server + web app); the
|
|
97
|
-
`@raggle-ai/mail` package is not a standalone CLI.
|
|
@@ -1,359 +0,0 @@
|
|
|
1
|
-
import type { IncomingMessage, ServerResponse } from "node:http"
|
|
2
|
-
|
|
3
|
-
import type { ApiRouteHandle } from "@raggle-ai/folder-plugin/endpoint"
|
|
4
|
-
import type { MailosService } from "@raggle-ai/mail/mailos"
|
|
5
|
-
import {
|
|
6
|
-
apiError,
|
|
7
|
-
jsonResponse,
|
|
8
|
-
parseJsonBody,
|
|
9
|
-
runApiRoute,
|
|
10
|
-
} from "@raggle-ai/folder-plugin/endpoint"
|
|
11
|
-
import {
|
|
12
|
-
deleteLocalDraftVerified,
|
|
13
|
-
MailAttachmentError,
|
|
14
|
-
MailDeleteError,
|
|
15
|
-
removeDraftAttachmentVerified,
|
|
16
|
-
} from "@raggle-ai/mail/history"
|
|
17
|
-
import {
|
|
18
|
-
createMailosService,
|
|
19
|
-
MailosCommandError,
|
|
20
|
-
MailSendFailure,
|
|
21
|
-
} from "@raggle-ai/mail/mailos"
|
|
22
|
-
import {
|
|
23
|
-
defaultMailDir,
|
|
24
|
-
isFolderWithin,
|
|
25
|
-
listMailForFolder,
|
|
26
|
-
readMailById,
|
|
27
|
-
} from "@raggle-ai/mail/node"
|
|
28
|
-
|
|
29
|
-
export type MailPreview = { body: string; signature: string | null }
|
|
30
|
-
|
|
31
|
-
export type MailPreviewer = (options: {
|
|
32
|
-
folder: string
|
|
33
|
-
id: string
|
|
34
|
-
mail: NonNullable<Awaited<ReturnType<typeof readMailById>>>
|
|
35
|
-
}) => Promise<MailPreview>
|
|
36
|
-
|
|
37
|
-
/**
|
|
38
|
-
* Context provided by the Vite plugin. `authorizeFolder` enforces the real
|
|
39
|
-
* viewer root (realpath + locked root), `watch` wires folder live refresh,
|
|
40
|
-
* and `mailPreviewer`/`mailDir`/`mailos` are test seams.
|
|
41
|
-
*/
|
|
42
|
-
export type MailApiContext = {
|
|
43
|
-
authorizeFolder: (value: string | null) => string
|
|
44
|
-
mailDir?: string
|
|
45
|
-
mailos?: ReturnType<typeof createMailosService>
|
|
46
|
-
mailPreviewer?: MailPreviewer
|
|
47
|
-
watch: (folder: string) => void
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
type AuthorizedMail = {
|
|
51
|
-
folder: string
|
|
52
|
-
mail: NonNullable<Awaited<ReturnType<typeof readMailById>>> & {
|
|
53
|
-
folder: string
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
type Handler = ApiRouteHandle<MailApiContext>
|
|
58
|
-
|
|
59
|
-
const defaultMailos = createMailosService()
|
|
60
|
-
|
|
61
|
-
function defaultPreviewer(mailos: MailosService): MailPreviewer {
|
|
62
|
-
return async ({ mail }) => ({
|
|
63
|
-
body: mail.body,
|
|
64
|
-
// A no_signature draft sends without any signature; do not show one.
|
|
65
|
-
signature: mail.noSignature
|
|
66
|
-
? null
|
|
67
|
-
: mail.folder
|
|
68
|
-
? await mailos.signature(mail.folder)
|
|
69
|
-
: null,
|
|
70
|
-
})
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
function mailDirOption(mailDir?: string) {
|
|
74
|
-
return mailDir ?? defaultMailDir()
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
function requireString(value: unknown, name: string) {
|
|
78
|
-
if (typeof value !== "string" || !value.trim()) {
|
|
79
|
-
throw apiError("bad-request", `Missing ${name} parameter`)
|
|
80
|
-
}
|
|
81
|
-
return value.trim()
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
function requireRevision(value: unknown) {
|
|
85
|
-
const revision = requireString(value, "revision")
|
|
86
|
-
if (!/^[a-f0-9]{64}$/i.test(revision)) {
|
|
87
|
-
throw apiError("bad-request", "Invalid revision parameter")
|
|
88
|
-
}
|
|
89
|
-
return revision.toLowerCase()
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
/**
|
|
93
|
-
* Shared authorized read: the folder must be a real root/descendant of the
|
|
94
|
-
* viewer root and the draft id must be a canonical filename with an active
|
|
95
|
-
* MailOS status; the draft's recorded folder must really be within the
|
|
96
|
-
* requested folder (symlink/traversal escapes collapse through realpath).
|
|
97
|
-
*/
|
|
98
|
-
async function authorizedMail(
|
|
99
|
-
context: MailApiContext,
|
|
100
|
-
id: string,
|
|
101
|
-
pathValue: string | null,
|
|
102
|
-
): Promise<AuthorizedMail> {
|
|
103
|
-
const trimmed = id.trim()
|
|
104
|
-
if (!trimmed) throw apiError("bad-request", "Missing id parameter")
|
|
105
|
-
const folder = context.authorizeFolder(pathValue)
|
|
106
|
-
const mail = await readMailById(trimmed, mailDirOption(context.mailDir))
|
|
107
|
-
if (!mail) throw apiError("not-found", "Mail not found")
|
|
108
|
-
if (!mail.folder) throw apiError("not-found", "Mail has no folder")
|
|
109
|
-
if (!isFolderWithin(folder, mail.folder)) {
|
|
110
|
-
throw apiError("not-found", "Mail not found for this folder")
|
|
111
|
-
}
|
|
112
|
-
return { folder, mail: { ...mail, folder: mail.folder } }
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
const listMail: Handler = async ({ context, query, response }) => {
|
|
116
|
-
const folder = context.authorizeFolder(query.get("path"))
|
|
117
|
-
const items = await listMailForFolder({
|
|
118
|
-
folder,
|
|
119
|
-
mailDir: mailDirOption(context.mailDir),
|
|
120
|
-
})
|
|
121
|
-
jsonResponse(response, 200, { folder, items })
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
const readItem: Handler = async ({ context, query, response }) => {
|
|
125
|
-
const id = requireString(query.get("id"), "id")
|
|
126
|
-
const { folder, mail } = await authorizedMail(context, id, query.get("path"))
|
|
127
|
-
context.watch(folder)
|
|
128
|
-
|
|
129
|
-
let preview: MailPreview
|
|
130
|
-
try {
|
|
131
|
-
const previewer =
|
|
132
|
-
context.mailPreviewer ?? defaultPreviewer(context.mailos ?? defaultMailos)
|
|
133
|
-
preview = await previewer({ id, folder, mail })
|
|
134
|
-
} catch (error) {
|
|
135
|
-
const detail =
|
|
136
|
-
error instanceof Error ? error.message : "Unknown preview error"
|
|
137
|
-
throw apiError("internal", `Mail preview failed: ${detail}`)
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
jsonResponse(response, 200, { mail, preview })
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
const resolveSender: Handler = async ({ context, query, response }) => {
|
|
144
|
-
const id = requireString(query.get("id"), "id")
|
|
145
|
-
const { mail } = await authorizedMail(context, id, query.get("path"))
|
|
146
|
-
const mailos = context.mailos ?? defaultMailos
|
|
147
|
-
const identity = await mailos.identity(mail.folder)
|
|
148
|
-
jsonResponse(response, 200, identity)
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
const readConversation: Handler = async ({
|
|
152
|
-
context,
|
|
153
|
-
query,
|
|
154
|
-
response,
|
|
155
|
-
signal,
|
|
156
|
-
}) => {
|
|
157
|
-
const id = requireString(query.get("id"), "id")
|
|
158
|
-
const { mail } = await authorizedMail(context, id, query.get("path"))
|
|
159
|
-
const mailos = context.mailos ?? defaultMailos
|
|
160
|
-
const references = [...mail.references, mail.inReplyTo].filter(
|
|
161
|
-
(reference): reference is string => Boolean(reference),
|
|
162
|
-
)
|
|
163
|
-
const result = await mailos.conversation(mail.folder, references, signal)
|
|
164
|
-
jsonResponse(response, 200, result)
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
/**
|
|
168
|
-
* Convert a CLI-level failure into a structured API error without leaking
|
|
169
|
-
* command output: a hard child timeout is a 504, an aborted child is
|
|
170
|
-
* rethrown for the endpoint's disconnect handling, and any other non-zero
|
|
171
|
-
* exit becomes a generic 500 with a fixed message.
|
|
172
|
-
*/
|
|
173
|
-
function commandFailureError(error: MailosCommandError): never {
|
|
174
|
-
if (error.timedOut || error.code === "timeout") {
|
|
175
|
-
throw apiError("timeout", "MailOS command timed out")
|
|
176
|
-
}
|
|
177
|
-
if (error.code === "aborted") throw error
|
|
178
|
-
throw apiError("failed", "MailOS command failed")
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
function sendFailureError(error: MailSendFailure) {
|
|
182
|
-
switch (error.reason) {
|
|
183
|
-
case "revision-match":
|
|
184
|
-
return apiError("conflict", error.message)
|
|
185
|
-
case "invalid-draft":
|
|
186
|
-
return apiError("bad-request", error.message)
|
|
187
|
-
case "unsupported-draft-dir":
|
|
188
|
-
return apiError("bad-request", error.message)
|
|
189
|
-
default:
|
|
190
|
-
return apiError("failed", error.message)
|
|
191
|
-
}
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
const runAction: Handler = async ({ body, context, response, signal }) => {
|
|
195
|
-
const parsed = parseJsonBody(body)
|
|
196
|
-
const action = requireString(parsed.action, "action")
|
|
197
|
-
const id = requireString(parsed.id, "id")
|
|
198
|
-
const revision = requireRevision(parsed.revision)
|
|
199
|
-
if (
|
|
200
|
-
action !== "archive" &&
|
|
201
|
-
action !== "delete" &&
|
|
202
|
-
action !== "remove-attachment"
|
|
203
|
-
) {
|
|
204
|
-
throw apiError("bad-request", "Invalid action parameter")
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
const { mail } = await authorizedMail(context, id, pathFromParsed(parsed))
|
|
208
|
-
if (mail.revision !== revision) {
|
|
209
|
-
throw apiError("conflict", "Draft changed since it was loaded")
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
const mailos = context.mailos ?? defaultMailos
|
|
213
|
-
if (action === "remove-attachment") {
|
|
214
|
-
const attachment = requireString(parsed.attachment, "attachment")
|
|
215
|
-
try {
|
|
216
|
-
await removeDraftAttachmentVerified(
|
|
217
|
-
mail,
|
|
218
|
-
mailDirOption(context.mailDir),
|
|
219
|
-
attachment,
|
|
220
|
-
)
|
|
221
|
-
} catch (error) {
|
|
222
|
-
if (error instanceof MailAttachmentError) {
|
|
223
|
-
switch (error.outcome) {
|
|
224
|
-
case "missing":
|
|
225
|
-
throw apiError("not-found", error.message)
|
|
226
|
-
case "invalid":
|
|
227
|
-
throw apiError("bad-request", error.message)
|
|
228
|
-
case "changed":
|
|
229
|
-
throw apiError("conflict", error.message)
|
|
230
|
-
}
|
|
231
|
-
}
|
|
232
|
-
throw error
|
|
233
|
-
}
|
|
234
|
-
} else if (action === "delete") {
|
|
235
|
-
try {
|
|
236
|
-
await deleteLocalDraftVerified(mail, mailDirOption(context.mailDir))
|
|
237
|
-
} catch (error) {
|
|
238
|
-
if (error instanceof MailDeleteError) {
|
|
239
|
-
switch (error.outcome) {
|
|
240
|
-
case "missing":
|
|
241
|
-
throw apiError("not-found", error.message)
|
|
242
|
-
case "invalid":
|
|
243
|
-
throw apiError("bad-request", error.message)
|
|
244
|
-
case "changed":
|
|
245
|
-
throw apiError("conflict", error.message)
|
|
246
|
-
}
|
|
247
|
-
}
|
|
248
|
-
throw error
|
|
249
|
-
}
|
|
250
|
-
} else {
|
|
251
|
-
try {
|
|
252
|
-
await mailos.archive(mail.folder, mail.id, {
|
|
253
|
-
draftDir: mailDirOption(context.mailDir),
|
|
254
|
-
signal,
|
|
255
|
-
})
|
|
256
|
-
} catch (error) {
|
|
257
|
-
if (error instanceof MailSendFailure) throw sendFailureError(error)
|
|
258
|
-
if (error instanceof MailosCommandError) commandFailureError(error)
|
|
259
|
-
throw error
|
|
260
|
-
}
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
jsonResponse(response, 200, { action, id: mail.id, ok: true })
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
const runSend: Handler = async ({ body, context, response, signal }) => {
|
|
267
|
-
const parsed = parseJsonBody(body)
|
|
268
|
-
const id = requireString(parsed.id, "id")
|
|
269
|
-
const dryRun = parsed.dryRun === true
|
|
270
|
-
const { mail } = await authorizedMail(context, id, pathFromParsed(parsed))
|
|
271
|
-
const mailos = context.mailos ?? defaultMailos
|
|
272
|
-
|
|
273
|
-
if (dryRun) {
|
|
274
|
-
const preview = await mailos
|
|
275
|
-
.sendPreview(mail.folder, mail.id, {
|
|
276
|
-
draftDir: mailDirOption(context.mailDir),
|
|
277
|
-
signal,
|
|
278
|
-
})
|
|
279
|
-
.catch((error: unknown) => {
|
|
280
|
-
if (error instanceof MailSendFailure) throw sendFailureError(error)
|
|
281
|
-
if (error instanceof MailosCommandError) commandFailureError(error)
|
|
282
|
-
throw error
|
|
283
|
-
})
|
|
284
|
-
jsonResponse(response, 200, preview)
|
|
285
|
-
return
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
const revision = requireRevision(parsed.revision)
|
|
289
|
-
if (mail.revision !== revision) {
|
|
290
|
-
throw apiError("conflict", "Draft changed since it was loaded")
|
|
291
|
-
}
|
|
292
|
-
const result = await mailos
|
|
293
|
-
.confirmSend(mail.folder, mail.id, revision, {
|
|
294
|
-
draftDir: mailDirOption(context.mailDir),
|
|
295
|
-
signal,
|
|
296
|
-
})
|
|
297
|
-
.catch((error: unknown) => {
|
|
298
|
-
if (error instanceof MailSendFailure) throw sendFailureError(error)
|
|
299
|
-
if (error instanceof MailosCommandError) commandFailureError(error)
|
|
300
|
-
throw error
|
|
301
|
-
})
|
|
302
|
-
jsonResponse(response, 200, result)
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
function pathFromParsed(parsed: Record<string, unknown>) {
|
|
306
|
-
return typeof parsed.path === "string" && parsed.path ? parsed.path : null
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
/**
|
|
310
|
-
* All mail HTTP routes. `/api/mail`, `/api/mail/item`, `/api/mail/from` and
|
|
311
|
-
* `/api/mail/conversation` are GET-only reads (GET never mutates);
|
|
312
|
-
* `/api/mail/action` and `/api/mail/send` are POST mutations guarded by the
|
|
313
|
-
* endpoint wrapper's same-origin check. Returns false for non-mail paths so
|
|
314
|
-
* the plugin can continue its own routing.
|
|
315
|
-
*/
|
|
316
|
-
export async function handleMailApiRoutes(
|
|
317
|
-
request: IncomingMessage,
|
|
318
|
-
response: ServerResponse,
|
|
319
|
-
context: MailApiContext,
|
|
320
|
-
): Promise<boolean> {
|
|
321
|
-
const url = new URL(request.url ?? "/", "http://127.0.0.1")
|
|
322
|
-
|
|
323
|
-
const specByPath: Record<
|
|
324
|
-
string,
|
|
325
|
-
{
|
|
326
|
-
method: "GET" | "POST"
|
|
327
|
-
maxBodyBytes?: number
|
|
328
|
-
timeoutMs?: number
|
|
329
|
-
handle: Handler
|
|
330
|
-
}
|
|
331
|
-
> = {
|
|
332
|
-
"/api/mail": { method: "GET", handle: listMail },
|
|
333
|
-
"/api/mail/item": { method: "GET", handle: readItem },
|
|
334
|
-
"/api/mail/from": { method: "GET", handle: resolveSender },
|
|
335
|
-
"/api/mail/conversation": {
|
|
336
|
-
method: "GET",
|
|
337
|
-
timeoutMs: 75_000,
|
|
338
|
-
handle: readConversation,
|
|
339
|
-
},
|
|
340
|
-
"/api/mail/action": {
|
|
341
|
-
method: "POST",
|
|
342
|
-
maxBodyBytes: 16 * 1024,
|
|
343
|
-
timeoutMs: 60_000,
|
|
344
|
-
handle: runAction,
|
|
345
|
-
},
|
|
346
|
-
"/api/mail/send": {
|
|
347
|
-
method: "POST",
|
|
348
|
-
maxBodyBytes: 16 * 1024,
|
|
349
|
-
timeoutMs: 90_000,
|
|
350
|
-
handle: runSend,
|
|
351
|
-
},
|
|
352
|
-
}
|
|
353
|
-
|
|
354
|
-
const spec = specByPath[url.pathname]
|
|
355
|
-
if (!spec) return false
|
|
356
|
-
|
|
357
|
-
await runApiRoute(request, response, url, spec, context)
|
|
358
|
-
return true
|
|
359
|
-
}
|
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Ambient typing for the `bun:ffi` surface flock.ts uses. Bun's runtime
|
|
3
|
-
* module provides `dlopen`; TypeScript cannot resolve it without this, and
|
|
4
|
-
* flock.ts keeps the import dynamic so the Node (koffi) path never touches
|
|
5
|
-
* bun's runtime.
|
|
6
|
-
*/
|
|
7
|
-
declare module "bun:ffi" {
|
|
8
|
-
export function dlopen(
|
|
9
|
-
path: string,
|
|
10
|
-
symbols: Record<string, { args: string[]; returns: string }>,
|
|
11
|
-
): {
|
|
12
|
-
symbols: Record<string, (...args: number[]) => number>
|
|
13
|
-
}
|
|
14
|
-
}
|
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
import type { FolderPluginViewProps } from "@raggle-ai/folder-plugin/client"
|
|
2
|
-
|
|
3
|
-
import { MailDetailPage } from "./mail-detail-page"
|
|
4
|
-
import { MailPage } from "./mail-page"
|
|
5
|
-
import { mailManifest } from "./manifest"
|
|
6
|
-
|
|
7
|
-
export function MailPlugin(props: FolderPluginViewProps) {
|
|
8
|
-
return props.subpath ? <MailDetailPage {...props} /> : <MailPage {...props} />
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
export const mailClient = {
|
|
12
|
-
...mailManifest,
|
|
13
|
-
component: MailPlugin,
|
|
14
|
-
}
|
|
@@ -1,137 +0,0 @@
|
|
|
1
|
-
// eslint-disable-next-line @typescript-eslint/triple-slash-reference -- ambient bun:ffi types for out-of-package consumers (packages/api compiles this file)
|
|
2
|
-
/// <reference path="./bun-ffi.d.ts" />
|
|
3
|
-
import { closeSync, constants, openSync } from "node:fs"
|
|
4
|
-
import { mkdir } from "node:fs/promises"
|
|
5
|
-
import { dirname, resolve } from "node:path"
|
|
6
|
-
|
|
7
|
-
/**
|
|
8
|
-
* Real POSIX advisory lock via `flock(2)` on MailOS-compatible lock files.
|
|
9
|
-
*
|
|
10
|
-
* MailOS (`emailos/internal/core/drafts.go` `withLocalDraftLock`) serializes
|
|
11
|
-
* draft mutations with an exclusive `flock` on `<draftDir>/.locks/<base>.lock`
|
|
12
|
-
* opened `O_CREATE|O_RDWR`. Kennel takes the *same* lock so destructive
|
|
13
|
-
* actions (delete/archive critical sections) coordinate with the MailOS
|
|
14
|
-
* binary itself, not just with other Kennel processes.
|
|
15
|
-
*
|
|
16
|
-
* Portability: `flock(2)` is reached through two supported FFI backends —
|
|
17
|
-
* `bun:ffi` when the process runs on Bun (the test runner), and the `koffi`
|
|
18
|
-
* N-API binding when it runs on Node (the Vite dev server / build). Both call
|
|
19
|
-
* the exact same libc symbol on the exact same lock files, so coordination
|
|
20
|
-
* holds across runtimes and against the Go MailOS binary. The wait is
|
|
21
|
-
* bounded so an abandoned holder cannot hang a server request forever.
|
|
22
|
-
*/
|
|
23
|
-
export class FlockTimeoutError extends Error {
|
|
24
|
-
readonly lockPath: string
|
|
25
|
-
|
|
26
|
-
constructor(lockPath: string, timeoutMs: number) {
|
|
27
|
-
super(
|
|
28
|
-
`Timed out after ${timeoutMs}ms waiting for the draft lock ${resolve(lockPath)}`,
|
|
29
|
-
)
|
|
30
|
-
this.name = "FlockTimeoutError"
|
|
31
|
-
this.lockPath = resolve(lockPath)
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
const LOCK_EX = 2
|
|
36
|
-
const LOCK_NB = 4
|
|
37
|
-
const LOCK_UN = 8
|
|
38
|
-
const EINTR = 4
|
|
39
|
-
const EWOULDBLOCK_DARWIN = 35
|
|
40
|
-
const EWOULDBLOCK_LINUX = 11
|
|
41
|
-
|
|
42
|
-
type FlockFn = (fd: number, operation: number) => number
|
|
43
|
-
|
|
44
|
-
type FlockBackend = {
|
|
45
|
-
flock: FlockFn
|
|
46
|
-
/** Returns the last libc errno; only used on Node (koffi exposes it). */
|
|
47
|
-
errno: () => number
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
function isBunRuntime() {
|
|
51
|
-
return (
|
|
52
|
-
typeof process !== "undefined" &&
|
|
53
|
-
typeof process.versions !== "undefined" &&
|
|
54
|
-
"bun" in process.versions
|
|
55
|
-
)
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
const libcPath =
|
|
59
|
-
process.platform === "darwin" ? "/usr/lib/libSystem.B.dylib" : "libc.so.6"
|
|
60
|
-
|
|
61
|
-
let backendPromise: Promise<FlockBackend> | null = null
|
|
62
|
-
|
|
63
|
-
async function loadBackend(): Promise<FlockBackend> {
|
|
64
|
-
backendPromise ??= (async () => {
|
|
65
|
-
if (isBunRuntime()) {
|
|
66
|
-
// bun:ffi does not surface errno; a non-zero flock result on the just
|
|
67
|
-
// opened descriptor is treated as "held elsewhere" (would-block /
|
|
68
|
-
// EINTR) and retried until the bounded deadline, which is the only
|
|
69
|
-
// realistic outcome for a valid descriptor.
|
|
70
|
-
const { dlopen } = await import("bun:ffi")
|
|
71
|
-
const libc = dlopen(libcPath, {
|
|
72
|
-
flock: { args: ["int", "int"], returns: "int" },
|
|
73
|
-
})
|
|
74
|
-
return { flock: libc.symbols.flock as FlockFn, errno: () => 0 }
|
|
75
|
-
}
|
|
76
|
-
// Node: koffi exposes libc errno so would-block/EINTR can be told
|
|
77
|
-
// apart from a real locking error.
|
|
78
|
-
const koffi = (await import("koffi")).default
|
|
79
|
-
const libc = koffi.load(libcPath)
|
|
80
|
-
return {
|
|
81
|
-
flock: libc.func("int flock(int fd, int operation)"),
|
|
82
|
-
errno: koffi.errno,
|
|
83
|
-
}
|
|
84
|
-
})()
|
|
85
|
-
return backendPromise
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
const wouldBlockErrno =
|
|
89
|
-
process.platform === "darwin" ? EWOULDBLOCK_DARWIN : EWOULDBLOCK_LINUX
|
|
90
|
-
|
|
91
|
-
function sleep(ms: number) {
|
|
92
|
-
return new Promise((resolveSleep) => {
|
|
93
|
-
setTimeout(resolveSleep, ms)
|
|
94
|
-
})
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
/**
|
|
98
|
-
* Run `action` while holding an exclusive `flock` on `lockPath`. The file is
|
|
99
|
-
* opened `O_RDWR|O_CREAT` (mode `0600`) exactly like MailOS; the lock is
|
|
100
|
-
* released (and the descriptor closed) before `withFlock` resolves, so a
|
|
101
|
-
* crash never leaves a stale advisory lock behind.
|
|
102
|
-
*/
|
|
103
|
-
export async function withFlock<T>(
|
|
104
|
-
lockPath: string,
|
|
105
|
-
timeoutMs: number,
|
|
106
|
-
action: () => Promise<T>,
|
|
107
|
-
): Promise<T> {
|
|
108
|
-
const resolved = resolve(lockPath)
|
|
109
|
-
// MailOS creates the lock directory with `os.MkdirAll(lockDir, 0o700)`.
|
|
110
|
-
await mkdir(dirname(resolved), { mode: 0o700, recursive: true }).catch(
|
|
111
|
-
() => undefined,
|
|
112
|
-
)
|
|
113
|
-
const { flock, errno } = await loadBackend()
|
|
114
|
-
const fd = openSync(resolved, constants.O_RDWR | constants.O_CREAT, 0o600)
|
|
115
|
-
try {
|
|
116
|
-
const deadline = Date.now() + timeoutMs
|
|
117
|
-
for (;;) {
|
|
118
|
-
if (flock(fd, LOCK_EX | LOCK_NB) === 0) break
|
|
119
|
-
const code = errno()
|
|
120
|
-
if (code !== 0 && code !== EINTR && code !== wouldBlockErrno) {
|
|
121
|
-
throw new Error(`flock failed on ${resolved} (errno ${code})`)
|
|
122
|
-
}
|
|
123
|
-
const remaining = deadline - Date.now()
|
|
124
|
-
if (remaining <= 0) {
|
|
125
|
-
throw new FlockTimeoutError(resolved, timeoutMs)
|
|
126
|
-
}
|
|
127
|
-
await sleep(Math.min(50, remaining))
|
|
128
|
-
}
|
|
129
|
-
return await action()
|
|
130
|
-
} finally {
|
|
131
|
-
try {
|
|
132
|
-
flock(fd, LOCK_UN)
|
|
133
|
-
} finally {
|
|
134
|
-
closeSync(fd)
|
|
135
|
-
}
|
|
136
|
-
}
|
|
137
|
-
}
|
|
@@ -1,37 +0,0 @@
|
|
|
1
|
-
import type { FolderFilterChoice } from "@raggle-ai/ui/lib/folder-filter"
|
|
2
|
-
import {
|
|
3
|
-
folderFilterChoices,
|
|
4
|
-
folderFilterLabel,
|
|
5
|
-
} from "@raggle-ai/ui/lib/folder-filter"
|
|
6
|
-
|
|
7
|
-
export type MailFolderChoice = FolderFilterChoice
|
|
8
|
-
|
|
9
|
-
/**
|
|
10
|
-
* Display text for a draft's owning folder relative to the viewed root:
|
|
11
|
-
* basename for root-owned drafts ("example-workspace"), relative path for
|
|
12
|
-
* descendants ("clients/client-one"). Null when the draft has no folder or it
|
|
13
|
-
* does not live under the viewed root.
|
|
14
|
-
*
|
|
15
|
-
* Delegates to the shared folder-filter lib.
|
|
16
|
-
*/
|
|
17
|
-
export function folderLabel(
|
|
18
|
-
folder: string | null,
|
|
19
|
-
rootFolder: string,
|
|
20
|
-
): string | null {
|
|
21
|
-
return folderFilterLabel(folder, rootFolder)
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
/**
|
|
25
|
-
* Exact owning-folder choices with per-draft counts for filtering the mail
|
|
26
|
-
* list. Keyed by the full folder path so duplicate leaf names stay
|
|
27
|
-
* distinct; ordered with the root folder first, then descendants
|
|
28
|
-
* alphabetically by relative path.
|
|
29
|
-
*
|
|
30
|
-
* Delegates to the shared folder-filter lib.
|
|
31
|
-
*/
|
|
32
|
-
export function folderChoices(
|
|
33
|
-
items: readonly { folder: string | null }[],
|
|
34
|
-
rootFolder: string,
|
|
35
|
-
): MailFolderChoice[] {
|
|
36
|
-
return folderFilterChoices(items, rootFolder, (item) => item.folder)
|
|
37
|
-
}
|