kennel 0.2.2 → 0.2.5

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 (237) hide show
  1. package/README.md +23 -13
  2. package/dist/cli/dispatch.js +16 -3
  3. package/dist/cli/src/cli/dispatch.js +16 -3
  4. package/dist/cli/src/cli/usage.js +1 -1
  5. package/dist/cli/src/core/command.js +1 -0
  6. package/dist/cli/src/core/init.js +5 -5
  7. package/dist/cli/src/plugins/external.d.ts +29 -3
  8. package/dist/cli/src/plugins/external.js +78 -24
  9. package/dist/cli/src/site/server-plugins.d.ts +2 -7
  10. package/dist/cli/src/site/viewer.js +1 -0
  11. package/dist/cli/usage.js +1 -1
  12. package/dist/core/command.js +1 -0
  13. package/dist/core/init.js +5 -5
  14. package/dist/core/runtime.js +357 -3207
  15. package/dist/package/preview.js +1232 -5113
  16. package/dist/plugins/external.d.ts +29 -3
  17. package/dist/plugins/external.js +78 -24
  18. package/dist/site/server-plugins.d.ts +2 -7
  19. package/dist/site/server-plugins.js +552 -10447
  20. package/dist/site/viewer.js +1 -0
  21. package/kernel/apps/web/package.json +1 -5
  22. package/kernel/apps/web/scripts/dev.mjs +53 -0
  23. package/kernel/apps/web/src/app.css +0 -3
  24. package/kernel/apps/web/src/main.tsx +0 -1
  25. package/kernel/apps/web/src/plugins.ts +0 -30
  26. package/kernel/apps/web/tsconfig.json +1 -2
  27. package/kernel/apps/web/vite.config.ts +31 -13
  28. package/kernel/packages/api/package.json +0 -4
  29. package/kernel/packages/api/src/server/folder-metadata-plugin.ts +13 -38
  30. package/kernel/packages/api/src/server/index.ts +0 -1
  31. package/kernel/packages/api/src/server/plugins.ts +49 -16
  32. package/kernel/packages/api/test/core.test.ts +13 -25
  33. package/kernel/packages/markdown/README.md +1 -1
  34. package/package.json +1 -1
  35. package/web/assets/{abnfDiagram-VRR7QNED-Bgfb1nEB.js → abnfDiagram-VRR7QNED-xd_g1uK3.js} +1 -1
  36. package/web/assets/app-CX20nNIa.js +2 -0
  37. package/web/assets/{app-options-D-RJiQ-X.js → app-options-CkPJE5ep.js} +1 -1
  38. package/web/assets/{app-view-header-BLvLq02T.js → app-view-header-CNk5KnA-.js} +1 -1
  39. package/web/assets/{arc-Dk_KI36B.js → arc-B0u5yBJA.js} +1 -1
  40. package/web/assets/{architectureDiagram-ZJ3FMSHR-NR_KhRCf.js → architectureDiagram-ZJ3FMSHR-DfxOAo8P.js} +1 -1
  41. package/web/assets/{ask-tray-TzeTcO6-.js → ask-tray-SscJecDY.js} +1 -1
  42. package/web/assets/{blockDiagram-677ZJIJ3-CkLkwx9C.js → blockDiagram-677ZJIJ3-Ci3Usxh6.js} +1 -1
  43. package/web/assets/{c4Diagram-LMCZKHZV-CFCUH8e_.js → c4Diagram-LMCZKHZV-BqgwIVgh.js} +1 -1
  44. package/web/assets/channel-X7WHyxxT.js +1 -0
  45. package/web/assets/{chunk-2Q5K7J3B-B_BPLWsW.js → chunk-2Q5K7J3B-CfX69l2U.js} +1 -1
  46. package/web/assets/{chunk-32BRIVSS-B1VB1hOy.js → chunk-32BRIVSS-CuFlzZkl.js} +1 -1
  47. package/web/assets/{chunk-5VM5RSS4-KFcYr8Co.js → chunk-5VM5RSS4-DidCMlJa.js} +1 -1
  48. package/web/assets/{chunk-EX3LRPZG-CpBX3kuK.js → chunk-EX3LRPZG-B5CDm1dS.js} +1 -1
  49. package/web/assets/{chunk-JWPE2WC7-BPSl40wG.js → chunk-JWPE2WC7-D5fGV_sH.js} +1 -1
  50. package/web/assets/{chunk-MOJQB5TN-D2RcYmoU.js → chunk-MOJQB5TN-ayhH5Q-H.js} +1 -1
  51. package/web/assets/{chunk-RYQCIY6F-m7Gyn-t3.js → chunk-RYQCIY6F-C4BfOOZn.js} +1 -1
  52. package/web/assets/{chunk-V7JOEXUC-TDvdopxS.js → chunk-V7JOEXUC-DpUEIM9V.js} +1 -1
  53. package/web/assets/{chunk-VR4S4FIN-DZTsG51a.js → chunk-VR4S4FIN-DoQuFFRh.js} +1 -1
  54. package/web/assets/{chunk-XXDRQBXY-Dp0--RE2.js → chunk-XXDRQBXY-CI9eiLwn.js} +1 -1
  55. package/web/assets/classDiagram-OUVF2IWQ-BjsLQ3dz.js +1 -0
  56. package/web/assets/classDiagram-v2-EOCWNBFH-BjsLQ3dz.js +1 -0
  57. package/web/assets/client-7OWw5kW2.js +6 -0
  58. package/web/assets/client-DYj_td4Q.js +17 -0
  59. package/web/assets/{client-j2kQmxsO.js → client-Dk-NBvJR.js} +4 -4
  60. package/web/assets/command-zkzH4hVz.js +38 -0
  61. package/web/assets/{copy-context-C0Uqsp-3.js → copy-context-D5oiWrYC.js} +1 -1
  62. package/web/assets/{cose-bilkent-JH36ORCC-CPu3PI8h.js → cose-bilkent-JH36ORCC-BHRk1mO3.js} +1 -1
  63. package/web/assets/{cynefin-VYW2F7L2-DWxftYAm.js → cynefin-VYW2F7L2-C1B09TRE.js} +1 -1
  64. package/web/assets/{cynefinDiagram-TSTJHNR4-DsmN2V-p.js → cynefinDiagram-TSTJHNR4-B27qXQbv.js} +1 -1
  65. package/web/assets/{dagre-VKFMJZFB-COEbApyY.js → dagre-VKFMJZFB-B1qEi32g.js} +1 -1
  66. package/web/assets/{diagram-FQU43EPY-0O9WGWKB.js → diagram-FQU43EPY-BMkkgADD.js} +1 -1
  67. package/web/assets/{diagram-G47NLZAW-CutEdMVh.js → diagram-G47NLZAW-Cb8_g1A5.js} +1 -1
  68. package/web/assets/{diagram-NH7WQ7WH-C9KZc3B2.js → diagram-NH7WQ7WH-B51wuc5m.js} +1 -1
  69. package/web/assets/{diagram-OA4YK3LP-D8kkPXbY.js → diagram-OA4YK3LP-xnsecbbI.js} +1 -1
  70. package/web/assets/{diagram-WEI45ONY-_YqiTaGP.js → diagram-WEI45ONY-DSZdyaAV.js} +1 -1
  71. package/web/assets/{document-Dgt5eZFj.js → document-Dc2dftTW.js} +3 -3
  72. package/web/assets/{ebnfDiagram-CCIWWBDH-JO-zQh6g.js → ebnfDiagram-CCIWWBDH-aTHmcUGy.js} +1 -1
  73. package/web/assets/{entities-C3wz7FCx.js → entities-CCUtpskY.js} +1 -1
  74. package/web/assets/{erDiagram-Q63AITRT-9UjQwJ4P.js → erDiagram-Q63AITRT-CUUPBcvo.js} +1 -1
  75. package/web/assets/{file-viewer-DZJ8nYnl.js → file-viewer-DJ3rsmef.js} +1 -1
  76. package/web/assets/{flowDiagram-23GEKE2U-Ca08tnYM.js → flowDiagram-23GEKE2U-aTQXx2Mh.js} +1 -1
  77. package/web/assets/{folder-image-BfGgxo7i.js → folder-image-Bnv43tzP.js} +1 -1
  78. package/web/assets/{folder-landing-qUKBCnG-.js → folder-landing-DCnzYXXY.js} +1 -1
  79. package/web/assets/{folder-list-BfFjv7rR.js → folder-list-DvVkVjiR.js} +1 -1
  80. package/web/assets/{folder-metadata-DCw8xzHk.js → folder-metadata-C9hMMzIH.js} +1 -1
  81. package/web/assets/{folder-mobile-bar-Ubz4babf.js → folder-mobile-bar-VoTL1GS0.js} +1 -1
  82. package/web/assets/folder-plugin-view-Cvd804ZU.js +6 -0
  83. package/web/assets/{ganttDiagram-NO4QXBWP-KnHzX5W3.js → ganttDiagram-NO4QXBWP-DiwdUp7u.js} +1 -1
  84. package/web/assets/{gitGraphDiagram-IHSO6WYX-Cm8woNtK.js → gitGraphDiagram-IHSO6WYX-BL2gG_IS.js} +1 -1
  85. package/web/assets/{index-CNyy98F8.js → index-CUTtMSyJ.js} +2 -2
  86. package/web/assets/index-CegvVQ1C.js +1 -0
  87. package/web/assets/index-Dwhf8F_U.css +1 -0
  88. package/web/assets/{infoDiagram-FWYZ7A6U-yyqmUtIb.js → infoDiagram-FWYZ7A6U-B3n0zZpt.js} +1 -1
  89. package/web/assets/{ishikawaDiagram-FXEZZL3T-BfvR_Zy4.js → ishikawaDiagram-FXEZZL3T-D7YytbEv.js} +1 -1
  90. package/web/assets/{journeyDiagram-5HDEW3XC-BKuw06HM.js → journeyDiagram-5HDEW3XC-BYwqEl3C.js} +1 -1
  91. package/web/assets/{kanban-definition-HUTT4EX6-n0010rmQ.js → kanban-definition-HUTT4EX6-BQHNJgXZ.js} +1 -1
  92. package/web/assets/{linear-C0d0lT8O.js → linear-BPiQCLNk.js} +1 -1
  93. package/web/assets/{mermaid.core-BDVbShyO.js → mermaid.core-CD-KvBjV.js} +4 -4
  94. package/web/assets/{metadata-KV0dpp4G.js → metadata-COJ7fZul.js} +1 -1
  95. package/web/assets/{mindmap-definition-LN4V7U3C--_0ks3GF.js → mindmap-definition-LN4V7U3C-C1L_gyoj.js} +1 -1
  96. package/web/assets/{pegDiagram-2B236MQR-CBoNIpKR.js → pegDiagram-2B236MQR-BDAfItqS.js} +1 -1
  97. package/web/assets/{pieDiagram-ENE6RG2P-MA5tkYh5.js → pieDiagram-ENE6RG2P-CkZYH3Vz.js} +1 -1
  98. package/web/assets/{popover-DBD7FF-W.js → popover-0oA2lLUV.js} +2 -2
  99. package/web/assets/{project-search-dialog-DM1IkSZu.js → project-search-dialog-B2oqMT6h.js} +1 -1
  100. package/web/assets/{quadrantDiagram-ABIIQ3AL-CWT1ryqF.js → quadrantDiagram-ABIIQ3AL-Dy8Al3YZ.js} +1 -1
  101. package/web/assets/{railroadDiagram-RFXS5EU6-ZiLB5FGi.js → railroadDiagram-RFXS5EU6-Dd9_yo7B.js} +1 -1
  102. package/web/assets/{requirementDiagram-TGXJPOKE-CQq9uneq.js → requirementDiagram-TGXJPOKE-DPhIUsbs.js} +1 -1
  103. package/web/assets/{sankeyDiagram-HTMAVEWB-qDG-0JqI.js → sankeyDiagram-HTMAVEWB-B2QkYtub.js} +1 -1
  104. package/web/assets/{search-DNrQA5fF.js → search-C1J0-xiB.js} +1 -1
  105. package/web/assets/select-CtXC_PLC.js +30 -0
  106. package/web/assets/{selection-popup-DmtTeRs2.js → selection-popup-qhlUFzGq.js} +1 -1
  107. package/web/assets/{sequenceDiagram-DBY2YBRQ-CiApurLO.js → sequenceDiagram-DBY2YBRQ-Bx7fcZDx.js} +1 -1
  108. package/web/assets/{sizeCapture-X5ZJPWSS-DZSR5QXm.js → sizeCapture-X5ZJPWSS-CIXIhl_A.js} +1 -1
  109. package/web/assets/{stateDiagram-2N3HPSRC-CjJJR9_z.js → stateDiagram-2N3HPSRC-DCrdAflB.js} +1 -1
  110. package/web/assets/stateDiagram-v2-6OUMAXLB-0KCFLYeQ.js +1 -0
  111. package/web/assets/{swimlanes-5IMT3BWC-CKmo1sg6.js → swimlanes-5IMT3BWC-DA2vc7iZ.js} +2 -2
  112. package/web/assets/swimlanesDiagram-G3AALYLV-CAw3AV84.js +8 -0
  113. package/web/assets/{timeline-definition-FHXFAJF6-D-NMwS3y.js → timeline-definition-FHXFAJF6-lKfJiV9V.js} +1 -1
  114. package/web/assets/{vennDiagram-L72KCM5P-CfL6LBWw.js → vennDiagram-L72KCM5P-DO1q2KzG.js} +1 -1
  115. package/web/assets/{view-context-gyDl_p2V.js → view-context-DXKM3-l1.js} +4 -4
  116. package/web/assets/{viewer-capabilities-Bpp8Uvcg.js → viewer-capabilities-Cig_xM9u.js} +1 -1
  117. package/web/assets/{wardleyDiagram-EHGQE667-DgldMxaj.js → wardleyDiagram-EHGQE667-Dal9LkuZ.js} +1 -1
  118. package/web/assets/{xychartDiagram-FW5EYKEG-CIoAIFX8.js → xychartDiagram-FW5EYKEG-B3dPLPII.js} +1 -1
  119. package/web/index.html +2 -2
  120. package/kernel/packages/api/test/partial-loading.test.ts +0 -43
  121. package/kernel/plugins/calendar/SKILL.md +0 -173
  122. package/kernel/plugins/calendar/eslint.config.js +0 -10
  123. package/kernel/plugins/calendar/package.json +0 -73
  124. package/kernel/plugins/calendar/schema.json +0 -137
  125. package/kernel/plugins/calendar/src/calendar.tsx +0 -328
  126. package/kernel/plugins/calendar/src/client.tsx +0 -139
  127. package/kernel/plugins/calendar/src/context.ts +0 -48
  128. package/kernel/plugins/calendar/src/copy-button.tsx +0 -66
  129. package/kernel/plugins/calendar/src/event-dialog.tsx +0 -270
  130. package/kernel/plugins/calendar/src/index.ts +0 -4
  131. package/kernel/plugins/calendar/src/manifest.ts +0 -7
  132. package/kernel/plugins/calendar/src/parse.ts +0 -610
  133. package/kernel/plugins/calendar/src/server.ts +0 -160
  134. package/kernel/plugins/calendar/src/utils.ts +0 -80
  135. package/kernel/plugins/calendar/src/views.tsx +0 -418
  136. package/kernel/plugins/calendar/test/context.test.mjs +0 -60
  137. package/kernel/plugins/calendar/test/package.test.mjs +0 -43
  138. package/kernel/plugins/calendar/test/parse.test.mjs +0 -392
  139. package/kernel/plugins/calendar/test/server.test.mjs +0 -126
  140. package/kernel/plugins/calendar/tsconfig.json +0 -17
  141. package/kernel/plugins/mail/SKILL.md +0 -97
  142. package/kernel/plugins/mail/eslint.config.js +0 -10
  143. package/kernel/plugins/mail/package.json +0 -79
  144. package/kernel/plugins/mail/src/api.ts +0 -359
  145. package/kernel/plugins/mail/src/bun-ffi.d.ts +0 -14
  146. package/kernel/plugins/mail/src/client.tsx +0 -14
  147. package/kernel/plugins/mail/src/flock.ts +0 -137
  148. package/kernel/plugins/mail/src/folders.ts +0 -37
  149. package/kernel/plugins/mail/src/history.ts +0 -485
  150. package/kernel/plugins/mail/src/index.ts +0 -3
  151. package/kernel/plugins/mail/src/mail-card.tsx +0 -123
  152. package/kernel/plugins/mail/src/mail-conversation.tsx +0 -153
  153. package/kernel/plugins/mail/src/mail-detail-page.tsx +0 -467
  154. package/kernel/plugins/mail/src/mail-hooks.ts +0 -252
  155. package/kernel/plugins/mail/src/mail-page.tsx +0 -109
  156. package/kernel/plugins/mail/src/mail-request.ts +0 -78
  157. package/kernel/plugins/mail/src/mail-send-box.tsx +0 -175
  158. package/kernel/plugins/mail/src/mailos.ts +0 -723
  159. package/kernel/plugins/mail/src/manifest.ts +0 -7
  160. package/kernel/plugins/mail/src/node.ts +0 -171
  161. package/kernel/plugins/mail/src/parse.ts +0 -139
  162. package/kernel/plugins/mail/src/server.ts +0 -125
  163. package/kernel/plugins/mail/src/types.ts +0 -10
  164. package/kernel/plugins/mail/src/viewer.tsx +0 -147
  165. package/kernel/plugins/mail/test/flock.test.mjs +0 -156
  166. package/kernel/plugins/mail/test/folders.test.mjs +0 -138
  167. package/kernel/plugins/mail/test/history.test.mjs +0 -460
  168. package/kernel/plugins/mail/test/mailos.test.mjs +0 -752
  169. package/kernel/plugins/mail/test/node.test.mjs +0 -207
  170. package/kernel/plugins/mail/test/package.test.mjs +0 -54
  171. package/kernel/plugins/mail/test/parse.test.mjs +0 -32
  172. package/kernel/plugins/mail/test/server.test.mjs +0 -116
  173. package/kernel/plugins/mail/test/viewer.test.mjs +0 -192
  174. package/kernel/plugins/mail/tsconfig.json +0 -17
  175. package/kernel/plugins/projects/SKILL.md +0 -73
  176. package/kernel/plugins/projects/eslint.config.js +0 -10
  177. package/kernel/plugins/projects/package.json +0 -70
  178. package/kernel/plugins/projects/src/client.tsx +0 -17
  179. package/kernel/plugins/projects/src/data/model.ts +0 -129
  180. package/kernel/plugins/projects/src/index.ts +0 -17
  181. package/kernel/plugins/projects/src/manifest.ts +0 -7
  182. package/kernel/plugins/projects/src/project-context.ts +0 -41
  183. package/kernel/plugins/projects/src/server.ts +0 -276
  184. package/kernel/plugins/projects/src/ui/project-card.tsx +0 -61
  185. package/kernel/plugins/projects/src/ui/projects-page.tsx +0 -102
  186. package/kernel/plugins/projects/test/package.test.mjs +0 -51
  187. package/kernel/plugins/projects/tsconfig.json +0 -17
  188. package/kernel/plugins/tasks/SKILL.md +0 -341
  189. package/kernel/plugins/tasks/eslint.config.js +0 -10
  190. package/kernel/plugins/tasks/package.json +0 -74
  191. package/kernel/plugins/tasks/schema.json +0 -939
  192. package/kernel/plugins/tasks/src/api.ts +0 -212
  193. package/kernel/plugins/tasks/src/client.tsx +0 -23
  194. package/kernel/plugins/tasks/src/data/parse.ts +0 -544
  195. package/kernel/plugins/tasks/src/data/task-id.ts +0 -56
  196. package/kernel/plugins/tasks/src/data/todo-model.ts +0 -795
  197. package/kernel/plugins/tasks/src/index.ts +0 -34
  198. package/kernel/plugins/tasks/src/manifest.ts +0 -7
  199. package/kernel/plugins/tasks/src/pages/task-detail.tsx +0 -48
  200. package/kernel/plugins/tasks/src/pages/task-list.tsx +0 -88
  201. package/kernel/plugins/tasks/src/server.ts +0 -447
  202. package/kernel/plugins/tasks/src/styles.css +0 -9
  203. package/kernel/plugins/tasks/src/ui/compact-todo-list.tsx +0 -372
  204. package/kernel/plugins/tasks/src/ui/error-details.tsx +0 -78
  205. package/kernel/plugins/tasks/src/ui/task-page.tsx +0 -392
  206. package/kernel/plugins/tasks/src/ui/todo-agent-filter.tsx +0 -36
  207. package/kernel/plugins/tasks/src/ui/todo-filter-bar.tsx +0 -66
  208. package/kernel/plugins/tasks/src/ui/todo-item.tsx +0 -494
  209. package/kernel/plugins/tasks/src/ui/todo-status-filter.tsx +0 -60
  210. package/kernel/plugins/tasks/src/ui/todo-tag-filter.tsx +0 -48
  211. package/kernel/plugins/tasks/src/ui/todo-warnings.tsx +0 -106
  212. package/kernel/plugins/tasks/test/api.test.mjs +0 -99
  213. package/kernel/plugins/tasks/test/folder-sources.test.mjs +0 -46
  214. package/kernel/plugins/tasks/test/package.test.mjs +0 -54
  215. package/kernel/plugins/tasks/test/parse.test.mjs +0 -225
  216. package/kernel/plugins/tasks/test/server.test.mjs +0 -386
  217. package/kernel/plugins/tasks/tsconfig.json +0 -17
  218. package/web/assets/app-DoGr2Zi7.js +0 -2
  219. package/web/assets/channel-uMEjV8qc.js +0 -1
  220. package/web/assets/classDiagram-OUVF2IWQ-fbZocVor.js +0 -1
  221. package/web/assets/classDiagram-v2-EOCWNBFH-fbZocVor.js +0 -1
  222. package/web/assets/client-C6Nxdfif.js +0 -2
  223. package/web/assets/client-CXYfBko5.js +0 -17
  224. package/web/assets/client-CoKtOq-X.js +0 -7
  225. package/web/assets/client-DyRQz5RU.js +0 -53
  226. package/web/assets/client-OY6HUzuD.js +0 -98
  227. package/web/assets/client-YOYWZIFi.js +0 -34
  228. package/web/assets/command-BugZBGXE.js +0 -38
  229. package/web/assets/folder-plugin-view-DsWR1zmQ.js +0 -6
  230. package/web/assets/index-BaRnbYRw.js +0 -1
  231. package/web/assets/index-BexmLxIP.css +0 -1
  232. package/web/assets/index-DDirZMgx.js +0 -1
  233. package/web/assets/note-card-DBfX6gW1.js +0 -6
  234. package/web/assets/select-D8cqNgcp.js +0 -30
  235. package/web/assets/stateDiagram-v2-6OUMAXLB-BpeGSCu9.js +0 -1
  236. package/web/assets/swimlanesDiagram-G3AALYLV-D9RjLKb9.js +0 -8
  237. package/web/assets/use-folder-filter-LQyXns5Q.js +0 -1
@@ -1,207 +0,0 @@
1
- import { mkdir, mkdtemp, rm, symlink, writeFile } from "node:fs/promises"
2
- import { tmpdir } from "node:os"
3
- import { join, resolve } from "node:path"
4
- import { afterAll, beforeAll, describe, expect, test } from "bun:test"
5
-
6
- import { isFolderWithin, listMailForFolder, readMailById } from "../src/node.ts"
7
-
8
- let tmpBase
9
- let root
10
- let sibling
11
- let outside
12
-
13
- beforeAll(async () => {
14
- tmpBase = await mkdtemp(join(tmpdir(), "kennel-mail-nested-"))
15
- root = join(tmpBase, "project")
16
- sibling = join(tmpBase, "project-x")
17
- outside = join(tmpBase, "outside")
18
- await mkdir(join(root, "clients", "client-one"), { recursive: true })
19
- await mkdir(join(root, "client-one"), { recursive: true })
20
- await mkdir(sibling, { recursive: true })
21
- await mkdir(outside, { recursive: true })
22
- // Symlink escape: a "folder" inside root that realpaths outside it.
23
- await mkdir(join(root, "clients"), { recursive: true })
24
- await symlink(outside, join(root, "clients", "escape"))
25
- })
26
-
27
- afterAll(async () => {
28
- await rm(tmpBase, { recursive: true, force: true })
29
- })
30
-
31
- describe("isFolderWithin", () => {
32
- test("exact folder is within itself", () => {
33
- expect(isFolderWithin(root, root)).toBe(true)
34
- })
35
-
36
- test("real descendant is within", () => {
37
- expect(isFolderWithin(root, join(root, "clients", "client-one"))).toBe(true)
38
- expect(isFolderWithin(root, join(root, "clients"))).toBe(true)
39
- })
40
-
41
- test("sibling prefix path is rejected", () => {
42
- expect(isFolderWithin(root, sibling)).toBe(false)
43
- })
44
-
45
- test("outside ancestor is rejected", () => {
46
- expect(isFolderWithin(root, tmpBase)).toBe(false)
47
- expect(isFolderWithin(root, tmpdir())).toBe(false)
48
- })
49
-
50
- test("traversal escapes collapse and are resolved against real paths", () => {
51
- expect(isFolderWithin(root, join(root, "clients", "..", ".."))).toBe(false)
52
- expect(
53
- isFolderWithin(root, join(root, "clients", "..", "client-one")),
54
- ).toBe(true)
55
- })
56
-
57
- test("symlink escape is rejected", () => {
58
- expect(isFolderWithin(root, join(root, "clients", "escape"))).toBe(false)
59
- })
60
-
61
- test("unresolved (missing) candidate is rejected", () => {
62
- expect(isFolderWithin(root, join(root, "not-created-yet"))).toBe(false)
63
- expect(isFolderWithin(join(root, "not-created-yet"), root)).toBe(false)
64
- })
65
-
66
- test("null or undefined candidate is rejected", () => {
67
- expect(isFolderWithin(root, null)).toBe(false)
68
- expect(isFolderWithin(root, undefined)).toBe(false)
69
- })
70
- })
71
-
72
- function draftFile(folder, id, subject = `Subject ${id}`) {
73
- const folderLine = folder ? `folder: ${folder}` : ""
74
- return `---\nid: ${id}\n${folderLine}\nstatus: working\nsubject: ${subject}\n---\n\nBody of ${id}\n`
75
- }
76
-
77
- describe("listMailForFolder", () => {
78
- test("lists exact and descendant folders only", async () => {
79
- const mailDir = join(tmpBase, "drafts")
80
- await mkdir(mailDir, { recursive: true })
81
- await writeFile(
82
- join(mailDir, "d_root.md"),
83
- draftFile(resolve(root), "d_root"),
84
- )
85
- await writeFile(
86
- join(mailDir, "d_nested.md"),
87
- draftFile(resolve(root, "clients", "client-one"), "d_nested"),
88
- )
89
- await writeFile(
90
- join(mailDir, "d_nested2.md"),
91
- draftFile(resolve(root, "clients"), "d_nested2"),
92
- )
93
- await writeFile(
94
- join(mailDir, "d_sibling.md"),
95
- draftFile(resolve(sibling), "d_sibling"),
96
- )
97
- await writeFile(
98
- join(mailDir, "d_outside.md"),
99
- draftFile(resolve(outside), "d_outside"),
100
- )
101
- await writeFile(
102
- join(mailDir, "d_escape.md"),
103
- draftFile(resolve(root, "clients", "escape"), "d_escape"),
104
- )
105
- await writeFile(
106
- join(mailDir, "d_nofolder.md"),
107
- draftFile(null, "d_nofolder"),
108
- )
109
-
110
- const items = await listMailForFolder({ mailDir, folder: root })
111
- const ids = items.map((item) => item.id).sort()
112
- expect(ids).toEqual(["d_nested", "d_nested2", "d_root"])
113
-
114
- const nested = items.find((item) => item.id === "d_nested")
115
- expect(nested?.folder).toBe(resolve(root, "clients", "client-one"))
116
- expect(nested?.subject).toBe("Subject d_nested")
117
- })
118
-
119
- test("nested folder scope works for a subfolder row", async () => {
120
- const mailDir = join(tmpBase, "drafts")
121
- const items = await listMailForFolder({
122
- mailDir,
123
- folder: resolve(root, "clients"),
124
- })
125
- const ids = items.map((item) => item.id).sort()
126
- expect(ids).toContain("d_nested")
127
- expect(ids).toContain("d_nested2")
128
- expect(ids).not.toContain("d_root")
129
- })
130
-
131
- test("reads only working and ready drafts from the mail folder", async () => {
132
- const mailDir = join(tmpBase, "active-drafts")
133
- await mkdir(mailDir, { recursive: true })
134
-
135
- const statusDraft = (id, status) =>
136
- `---\nid: ${id}\nfolder: ${resolve(root)}\nstatus: ${status}\nsubject: ${id}\n---\n\nBody of ${id}\n`
137
- await writeFile(
138
- join(mailDir, "d_aaaaaa.md"),
139
- statusDraft("d_aaaaaa", "working"),
140
- )
141
- await writeFile(
142
- join(mailDir, "d_ccccc1.md"),
143
- statusDraft("d_ccccc1", "ready"),
144
- )
145
- await writeFile(
146
- join(mailDir, "d_bbbbbb.md"),
147
- statusDraft("d_bbbbbb", "sent"),
148
- )
149
- await writeFile(
150
- join(mailDir, "d_dddddd.md"),
151
- statusDraft("d_dddddd", "archived"),
152
- )
153
- await writeFile(join(mailDir, "d_eeeeee.md"), `# No status\n`)
154
-
155
- const items = await listMailForFolder({ mailDir, folder: root })
156
- const ids = items.map((item) => item.id).sort()
157
- expect(ids).toEqual(["d_aaaaaa", "d_ccccc1"])
158
- })
159
- })
160
-
161
- describe("readMailById", () => {
162
- test("reads the authoritative draft file by id", async () => {
163
- const mailDir = join(tmpBase, "direct-path")
164
- await mkdir(mailDir, { recursive: true })
165
-
166
- const path = join(mailDir, "d_cccccc.md")
167
- await writeFile(path, draftFile(resolve(root), "d_cccccc"))
168
-
169
- const mail = await readMailById("d_cccccc", mailDir)
170
-
171
- expect(mail?.id).toBe("d_cccccc")
172
- expect(mail?.filePath).toBe(path)
173
- expect(mail?.body).toBe(`Body of d_cccccc\n`)
174
- })
175
-
176
- test("supports MailOS descriptive draft ids", async () => {
177
- const mailDir = join(tmpBase, "descriptive-ids")
178
- await mkdir(mailDir, { recursive: true })
179
-
180
- const id = "draft_20251129_215834_Test-Draft"
181
- await writeFile(join(mailDir, `${id}.md`), draftFile(resolve(root), id))
182
-
183
- const mail = await readMailById(id, mailDir)
184
- expect(mail?.id).toBe(id)
185
- })
186
-
187
- test("rejects drafts that are no longer active", async () => {
188
- const mailDir = join(tmpBase, "inactive-drafts")
189
- await mkdir(mailDir, { recursive: true })
190
-
191
- await writeFile(
192
- join(mailDir, "d_bbbbbb.md"),
193
- `---\nid: d_bbbbbb\nfolder: ${resolve(root)}\nstatus: sent\nsubject: Sent\n---\n\nBody\n`,
194
- )
195
-
196
- const mail = await readMailById("d_bbbbbb", mailDir)
197
- expect(mail).toBeNull()
198
- })
199
-
200
- test("rejects traversal ids", () => {
201
- const mailDir = join(tmpBase, "traversal-drafts")
202
- expect(() => readMailById("../d_aaaaaa", mailDir)).toThrow(
203
- "Invalid mail id",
204
- )
205
- expect(() => readMailById("a/b", mailDir)).toThrow("Invalid mail id")
206
- })
207
- })
@@ -1,54 +0,0 @@
1
- import assert from "node:assert/strict"
2
- import { readFile } from "node:fs/promises"
3
- import test from "node:test"
4
-
5
- const packageJson = JSON.parse(
6
- await readFile(new URL("../package.json", import.meta.url), "utf8"),
7
- )
8
-
9
- test("mail package is ready for public npm publishing", () => {
10
- assert.equal(packageJson.private, undefined)
11
- assert.equal(packageJson.version, "0.1.0")
12
- assert.equal(packageJson.license, "MIT")
13
- assert.equal(packageJson.publishConfig?.access, "public")
14
- assert.equal(packageJson.repository?.directory, "plugins/mail")
15
- assert.deepEqual(packageJson.files, ["src"])
16
- })
17
-
18
- test("mail package ships the standard plugin script set", () => {
19
- for (const script of [
20
- "test",
21
- "typecheck",
22
- "format",
23
- "lint",
24
- "clean",
25
- "pack:check",
26
- "publish:public:dry-run",
27
- ]) {
28
- assert.ok(
29
- typeof packageJson.scripts?.[script] === "string",
30
- `missing script "${script}"`,
31
- )
32
- }
33
- })
34
-
35
- test("mail package exposes the core plugin deep exports", () => {
36
- const keys = Object.keys(packageJson.exports).sort()
37
- for (const required of [
38
- ".",
39
- "./client",
40
- "./manifest",
41
- "./parse",
42
- "./server",
43
- ]) {
44
- assert.ok(keys.includes(required), `missing export "${required}"`)
45
- }
46
- })
47
-
48
- test("mail client entry does not import the Node server entry", async () => {
49
- const clientSource = await readFile(
50
- new URL("../src/client.tsx", import.meta.url),
51
- "utf8",
52
- )
53
- assert.doesNotMatch(clientSource, /node:|\/server"/)
54
- })
@@ -1,32 +0,0 @@
1
- import { describe, expect, test } from "bun:test"
2
-
3
- import { parseMailMarkdown } from "../src/parse.ts"
4
-
5
- function baseMail(body = "Hello") {
6
- return `---\nid: d_abcdef\nfolder: /tmp/project\n---\n\n${body}\n`
7
- }
8
-
9
- describe("parseMailMarkdown", () => {
10
- test("parses normal frontmatter", () => {
11
- const parsed = parseMailMarkdown(baseMail("Body"))
12
- expect(parsed.id).toBe("d_abcdef")
13
- expect(parsed.folder).toBe("/tmp/project")
14
- expect(parsed.body).toBe("Body\n")
15
- })
16
-
17
- test("empty frontmatter does not throw and returns defaults", () => {
18
- const content = `---\n---\n\nBody\n`
19
- const parsed = parseMailMarkdown(content)
20
- expect(parsed.id).toBe(null)
21
- expect(parsed.folder).toBe(null)
22
- expect(parsed.body).toBe("Body\n")
23
- })
24
-
25
- test("non-object frontmatter does not throw and returns defaults", () => {
26
- const content = `---\n[]\n---\n\nBody\n`
27
- const parsed = parseMailMarkdown(content)
28
- expect(parsed.id).toBe(null)
29
- expect(parsed.folder).toBe(null)
30
- expect(parsed.body).toBe("Body\n")
31
- })
32
- })
@@ -1,116 +0,0 @@
1
- import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises"
2
- import { tmpdir } from "node:os"
3
- import { join } from "node:path"
4
- import { afterAll, beforeAll, expect, test } from "bun:test"
5
-
6
- import { createMailPlugin } from "../src/server.ts"
7
-
8
- function draftFile(folder, id) {
9
- return `---\nid: ${id}\nfolder: ${folder}\nstatus: working\nsubject: ${id}\n---\n\nBody of ${id}\n`
10
- }
11
-
12
- let base
13
- let root
14
- let mailDir
15
-
16
- beforeAll(async () => {
17
- base = await mkdtemp(join(tmpdir(), "kennel-mail-plugin-"))
18
- root = join(base, "project")
19
- mailDir = join(base, "drafts")
20
- await mkdir(join(root, "clients"), { recursive: true })
21
- await mkdir(mailDir, { recursive: true })
22
- })
23
-
24
- afterAll(async () => {
25
- await rm(base, { recursive: true, force: true })
26
- })
27
-
28
- test("detect is unavailable with no active drafts", async () => {
29
- const plugin = createMailPlugin({ mailDir })
30
- const detection = await plugin.detect({ folder: root })
31
- expect(detection.status).toBe("unavailable")
32
- })
33
-
34
- test("detect reports available with a count of matching active drafts", async () => {
35
- const nested = join(root, "clients", "nested")
36
- await mkdir(nested, { recursive: true })
37
- await writeFile(join(mailDir, "d_root.md"), draftFile(root, "d_root"))
38
- await writeFile(join(mailDir, "d_nested.md"), draftFile(nested, "d_nested"))
39
-
40
- const plugin = createMailPlugin({ mailDir })
41
- const detection = await plugin.detect({ folder: root })
42
- expect(detection.status).toBe("available")
43
- expect(detection.count).toBe(2)
44
- })
45
-
46
- test("handle list returns only folder-and-descendant active drafts", async () => {
47
- const plugin = createMailPlugin({ mailDir })
48
- const result = await plugin.handle({ folder: root }, "")
49
- expect(result.kind).toBe("available")
50
- if (result.kind !== "available") return
51
- const payload = result.data
52
- expect(payload.status).toBe("available")
53
- const ids = payload.items.map((item) => item.id).sort()
54
- expect(ids).toEqual(["d_nested", "d_root"])
55
- })
56
-
57
- test("handle detail returns the mail and injected preview", async () => {
58
- const calls = []
59
- const plugin = createMailPlugin({
60
- mailDir,
61
- previewer: async (options) => {
62
- calls.push(options.id)
63
- return { body: "COMPOSED BODY", signature: "--\nSig" }
64
- },
65
- })
66
- const result = await plugin.handle({ folder: root }, "d_root")
67
- expect(result.kind).toBe("available")
68
- if (result.kind !== "available") return
69
- expect(result.data.mail.id).toBe("d_root")
70
- expect(result.data.preview.body).toBe("COMPOSED BODY")
71
- expect(calls).toEqual(["d_root"])
72
- })
73
-
74
- test("handle detail rejects a folder mismatch before preview", async () => {
75
- const other = join(base, "other")
76
- await mkdir(other, { recursive: true })
77
- let called = false
78
- const plugin = createMailPlugin({
79
- mailDir,
80
- previewer: async () => {
81
- called = true
82
- return { body: "x", signature: null }
83
- },
84
- })
85
- await expect(plugin.handle({ folder: other }, "d_root")).rejects.toThrow(
86
- "Mail not found for this folder",
87
- )
88
- expect(called).toBe(false)
89
- })
90
-
91
- test("handle detail maps a preview read failure to a server failure", async () => {
92
- const plugin = createMailPlugin({
93
- mailDir,
94
- previewer: async () => {
95
- throw new Error("mailos parse failed")
96
- },
97
- })
98
- await expect(plugin.handle({ folder: root }, "d_root")).rejects.toMatchObject(
99
- {
100
- name: "FolderPluginFailure",
101
- },
102
- )
103
- await expect(plugin.handle({ folder: root }, "d_root")).rejects.toThrow(
104
- "Mail preview failed",
105
- )
106
- })
107
-
108
- test("handle detail rejects traversal ids", async () => {
109
- const plugin = createMailPlugin({ mailDir })
110
- await expect(plugin.handle({ folder: root }, "../x")).rejects.toThrow(
111
- "Invalid mail id",
112
- )
113
- await expect(plugin.handle({ folder: root }, "a/b")).rejects.toThrow(
114
- "Invalid mail id",
115
- )
116
- })
@@ -1,192 +0,0 @@
1
- import { createElement as h } from "react"
2
- import { describe, expect, test } from "bun:test"
3
- import { renderToStaticMarkup } from "react-dom/server"
4
-
5
- import { folderChoices, folderLabel } from "../src/folders.ts"
6
- import { MailFolderFilter, mailItemContext, MailList } from "../src/viewer.tsx"
7
-
8
- const WORKSPACE = "/tmp/kennel-example/example-workspace"
9
- const CLIENT_ONE = `${WORKSPACE}/clients/client-one`
10
- const CLIENT_TWO = `${WORKSPACE}/clients/client-two`
11
- const CLIENT_THREE = `${WORKSPACE}/clients/client-three`
12
-
13
- function mailItem({ folder, id, subject }) {
14
- return {
15
- bcc: [],
16
- cc: [],
17
- createdAt: "2026-09-18T08:23:23.805127Z",
18
- excerpt: "Draft excerpt",
19
- filePath: `/tmp/${id}.md`,
20
- folder,
21
- id,
22
- status: "working",
23
- subject,
24
- tags: [],
25
- to: ["ed@example.com"],
26
- updatedAt: null,
27
- }
28
- }
29
-
30
- function renderMailList(items) {
31
- return renderToStaticMarkup(
32
- h(MailList, {
33
- folderLabel: (folder) => folderLabel(folder, WORKSPACE),
34
- items,
35
- mailHref: (id) => `/folder/x/mail/${id}`,
36
- }),
37
- )
38
- }
39
-
40
- describe("MailList cards", () => {
41
- test("each card shows its owning folder and keeps a stable link", () => {
42
- const html = renderMailList([
43
- mailItem({ folder: WORKSPACE, id: "d_root0", subject: "Root draft" }),
44
- mailItem({
45
- folder: CLIENT_ONE,
46
- id: "d_client-one0",
47
- subject: "Client One draft",
48
- }),
49
- mailItem({ folder: null, id: "d_none0", subject: "No folder draft" }),
50
- ])
51
-
52
- expect(html).toContain("Root draft")
53
- expect(html).toContain("Client One draft")
54
- expect(html).toContain("No folder draft")
55
- // Stable per-draft links survive filtering.
56
- expect(html).toContain('href="/folder/x/mail/d_root0"')
57
- expect(html).toContain('href="/folder/x/mail/d_client-one0"')
58
- expect(html).toContain('href="/folder/x/mail/d_none0"')
59
- expect(html).toContain(">example-workspace<")
60
- expect(html).toContain(">clients/client-one<")
61
- expect(html).toContain(`title="${CLIENT_ONE}"`)
62
- expect(html.match(/lucide-folder /g)).toHaveLength(2)
63
- expect(html).toContain('class="truncate">clients/client-one</span>')
64
- })
65
-
66
- test("shows the To domain favicon and a truncated message preview", () => {
67
- const html = renderMailList([
68
- {
69
- ...mailItem({ folder: CLIENT_ONE, id: "d_preview", subject: "Draft" }),
70
- to: ["Ed <ed@example.com>"],
71
- },
72
- ])
73
-
74
- expect(html).toContain(
75
- "https://www.google.com/s2/favicons?domain=example.com&amp;sz=32",
76
- )
77
- expect(html).not.toContain("domain=ed")
78
- expect(html).toContain('referrerPolicy="no-referrer"')
79
- expect(html).toContain("grid-cols-[2rem_minmax(0,1fr)]")
80
- expect(html).toMatch(/<p class="[^"]*truncate[^"]*">Draft excerpt<\/p>/)
81
- })
82
-
83
- test("uses a mail icon without a favicon request for missing or invalid To addresses", () => {
84
- for (const to of [[], ["invalid"], ["user@localhost"]]) {
85
- const html = renderMailList([
86
- {
87
- ...mailItem({ folder: null, id: "d_fallback", subject: "Draft" }),
88
- to,
89
- cc: ["cc@example.com"],
90
- excerpt: "",
91
- },
92
- ])
93
-
94
- expect(html).toContain("lucide-mail")
95
- expect(html).not.toContain("s2/favicons")
96
- expect(html).not.toMatch(/<p[\s>]/)
97
- }
98
- })
99
-
100
- test("time precedes the recipient in a left-aligned row", () => {
101
- const html = renderMailList([
102
- {
103
- ...mailItem({ folder: CLIENT_ONE, id: "d_time", subject: "Draft" }),
104
- updatedAt: new Date().toISOString(),
105
- },
106
- ])
107
-
108
- expect(html).toContain("just now")
109
- expect(html.indexOf("just now")).toBeLessThan(html.indexOf("To:"))
110
- expect(html).toContain('class="flex items-start gap-2"')
111
- })
112
- })
113
-
114
- describe("mailItemContext", () => {
115
- test("formats one draft's fields into an agent-friendly prompt", () => {
116
- const item = mailItem({
117
- folder: WORKSPACE,
118
- id: "d_ctx",
119
- subject: "Pricing follow-up",
120
- })
121
- const text = mailItemContext(item, "Hi Ed,\n\nConfirming the numbers.")
122
-
123
- expect(text).toContain("Instructions for the LLM:")
124
- expect(text).toContain("- Subject: Pricing follow-up")
125
- expect(text).toContain("- To: ed@example.com")
126
- expect(text).toContain("- Status: working")
127
- expect(text).toContain(`- Draft file: /tmp/d_ctx.md`)
128
- expect(text).toContain("Draft body:")
129
- expect(text).toContain("Confirming the numbers.")
130
- })
131
-
132
- test("falls back to the excerpt and a no-subject placeholder", () => {
133
- const item = mailItem({ folder: null, id: "d_none", subject: null })
134
- const text = mailItemContext(item)
135
-
136
- expect(text).toContain("Subject: (no subject)")
137
- expect(text).toContain("Draft excerpt:")
138
- expect(text).toContain("Draft excerpt")
139
- expect(text).not.toContain("Draft body:")
140
- })
141
- })
142
-
143
- describe("MailFolderFilter markup", () => {
144
- const drafts = [
145
- WORKSPACE,
146
- WORKSPACE,
147
- CLIENT_ONE,
148
- CLIENT_TWO,
149
- CLIENT_THREE,
150
- ].map((folder, index) => ({ folder, id: `d_${index}` }))
151
- const choices = folderChoices(drafts, WORKSPACE)
152
-
153
- test("one trigger button shows the total when nothing is selected", () => {
154
- const html = renderToStaticMarkup(
155
- h(MailFolderFilter, {
156
- choices,
157
- onSelect: () => {},
158
- selected: [],
159
- total: drafts.length,
160
- }),
161
- )
162
-
163
- // Single trigger with accessible name and the unfiltered total badge.
164
- expect(html).toContain('aria-label="Filter drafts by folder"')
165
- expect(html).toContain("lucide-folder-search")
166
- expect(html).toContain(">Folder<")
167
- expect(html).toContain(">5<")
168
- // The All folders row lives inside the portal, so no SSR markup carries it.
169
- expect(html).not.toContain("All folders")
170
- // No pressed chips outside the portal dropdown.
171
- const pressed = html.match(/aria-pressed=/g) ?? []
172
- expect(pressed.length).toBe(0)
173
- })
174
-
175
- test("a single selection shows the count on the trigger", () => {
176
- const html = renderToStaticMarkup(
177
- h(MailFolderFilter, {
178
- choices,
179
- onSelect: () => {},
180
- selected: [CLIENT_ONE],
181
- total: drafts.length,
182
- }),
183
- )
184
-
185
- // Same single trigger; the badge shows the selected count.
186
- expect(html).toContain('aria-label="Filter drafts by folder"')
187
- expect(html).toContain(">Folder<")
188
- expect(html).toContain(">1<")
189
- expect(html).not.toContain("lucide-x")
190
- expect(html).not.toContain("All folders")
191
- })
192
- })
@@ -1,17 +0,0 @@
1
- {
2
- "$schema": "https://json.schemastore.org/tsconfig",
3
- "extends": "@raggle-ai/tsconfig/base.json",
4
- "compilerOptions": {
5
- "target": "ES2022",
6
- "lib": ["dom", "dom.iterable", "esnext"],
7
- "jsx": "preserve",
8
- "baseUrl": ".",
9
- "allowImportingTsExtensions": true,
10
- // local copy: bun doesn't read paths inherited via `extends`
11
- "paths": {
12
- "~/*": ["${configDir}/../../packages/*/src"]
13
- }
14
- },
15
- "include": ["src"],
16
- "exclude": ["node_modules", "dist", "eslint.config.mjs"]
17
- }