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.
Files changed (274) 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 +28 -2
  8. package/dist/cli/src/plugins/external.js +77 -23
  9. package/dist/cli/src/site/server-plugins.d.ts +4 -9
  10. package/dist/cli/src/site/viewer.js +1 -0
  11. package/dist/cli/usage.js +1 -1
  12. package/dist/core/api-client.js +576 -10317
  13. package/dist/core/command.js +1 -0
  14. package/dist/core/core-folders.js +2313 -12088
  15. package/dist/core/init.js +5 -5
  16. package/dist/core/runtime.js +8237 -20713
  17. package/dist/package/preview.js +19617 -33352
  18. package/dist/plugins/external.d.ts +28 -2
  19. package/dist/plugins/external.js +77 -23
  20. package/dist/site/server-plugins.d.ts +4 -9
  21. package/dist/site/server-plugins.js +557 -10452
  22. package/dist/site/viewer.js +1 -0
  23. package/kernel/apps/web/package.json +2 -6
  24. package/kernel/apps/web/scripts/dev.mjs +53 -0
  25. package/kernel/apps/web/src/app.css +0 -3
  26. package/kernel/apps/web/src/external-plugins.ts +1 -1
  27. package/kernel/apps/web/src/folder-filter-icons.tsx +1 -1
  28. package/kernel/apps/web/src/folder-plugin-view.tsx +1 -1
  29. package/kernel/apps/web/src/main.tsx +0 -1
  30. package/kernel/apps/web/src/plugins.ts +1 -31
  31. package/kernel/apps/web/tsconfig.json +1 -2
  32. package/kernel/apps/web/vite.config.ts +32 -14
  33. package/kernel/packages/agents/package.json +1 -1
  34. package/kernel/packages/agents/src/agent-card.tsx +1 -1
  35. package/kernel/packages/agents/src/client.tsx +1 -1
  36. package/kernel/packages/agents/src/manifest.ts +1 -1
  37. package/kernel/packages/agents/src/server.ts +1 -1
  38. package/kernel/packages/agents/src/viewer.tsx +1 -1
  39. package/kernel/packages/api/package.json +1 -5
  40. package/kernel/packages/api/src/server/acp-live-plugin.ts +1 -1
  41. package/kernel/packages/api/src/server/api/create-agent-file.ts +2 -2
  42. package/kernel/packages/api/src/server/api/document-edit.ts +2 -2
  43. package/kernel/packages/api/src/server/api/open-path.ts +2 -2
  44. package/kernel/packages/api/src/server/folder-metadata-plugin.ts +17 -42
  45. package/kernel/packages/api/src/server/index.ts +0 -1
  46. package/kernel/packages/api/src/server/plugins.ts +52 -19
  47. package/kernel/packages/api/src/shared/viewer-route.ts +1 -1
  48. package/kernel/packages/api/test/core.test.ts +14 -26
  49. package/kernel/packages/markdown/README.md +1 -1
  50. package/kernel/packages/notes/package.json +1 -1
  51. package/kernel/packages/notes/src/client.tsx +1 -1
  52. package/kernel/packages/notes/src/manifest.ts +1 -1
  53. package/kernel/packages/notes/src/server.ts +1 -1
  54. package/kernel/packages/plugins/SKILL.md +39 -0
  55. package/kernel/{plugins/mail → packages/plugins}/package.json +53 -28
  56. package/kernel/packages/plugins/src/index.ts +52 -0
  57. package/kernel/packages/plugins/src/page.ts +17 -0
  58. package/kernel/{plugins/projects → packages/plugins}/tsconfig.json +5 -1
  59. package/kernel/packages/plugins/tsup.config.ts +25 -0
  60. package/kernel/packages/ui/src/components/content-issues.tsx +1 -1
  61. package/kernel/packages/ui/tsconfig.json +1 -1
  62. package/package.json +2 -3
  63. package/web/assets/{abnfDiagram-VRR7QNED-Bgfb1nEB.js → abnfDiagram-VRR7QNED-xd_g1uK3.js} +1 -1
  64. package/web/assets/app-CX20nNIa.js +2 -0
  65. package/web/assets/{app-options-D-RJiQ-X.js → app-options-CkPJE5ep.js} +1 -1
  66. package/web/assets/{app-view-header-BLvLq02T.js → app-view-header-CNk5KnA-.js} +1 -1
  67. package/web/assets/{arc-Dk_KI36B.js → arc-B0u5yBJA.js} +1 -1
  68. package/web/assets/{architectureDiagram-ZJ3FMSHR-NR_KhRCf.js → architectureDiagram-ZJ3FMSHR-DfxOAo8P.js} +1 -1
  69. package/web/assets/{ask-tray-TzeTcO6-.js → ask-tray-SscJecDY.js} +1 -1
  70. package/web/assets/{blockDiagram-677ZJIJ3-CkLkwx9C.js → blockDiagram-677ZJIJ3-Ci3Usxh6.js} +1 -1
  71. package/web/assets/{c4Diagram-LMCZKHZV-CFCUH8e_.js → c4Diagram-LMCZKHZV-BqgwIVgh.js} +1 -1
  72. package/web/assets/channel-X7WHyxxT.js +1 -0
  73. package/web/assets/{chunk-2Q5K7J3B-B_BPLWsW.js → chunk-2Q5K7J3B-CfX69l2U.js} +1 -1
  74. package/web/assets/{chunk-32BRIVSS-B1VB1hOy.js → chunk-32BRIVSS-CuFlzZkl.js} +1 -1
  75. package/web/assets/{chunk-5VM5RSS4-KFcYr8Co.js → chunk-5VM5RSS4-DidCMlJa.js} +1 -1
  76. package/web/assets/{chunk-EX3LRPZG-CpBX3kuK.js → chunk-EX3LRPZG-B5CDm1dS.js} +1 -1
  77. package/web/assets/{chunk-JWPE2WC7-BPSl40wG.js → chunk-JWPE2WC7-D5fGV_sH.js} +1 -1
  78. package/web/assets/{chunk-MOJQB5TN-D2RcYmoU.js → chunk-MOJQB5TN-ayhH5Q-H.js} +1 -1
  79. package/web/assets/{chunk-RYQCIY6F-m7Gyn-t3.js → chunk-RYQCIY6F-C4BfOOZn.js} +1 -1
  80. package/web/assets/{chunk-V7JOEXUC-TDvdopxS.js → chunk-V7JOEXUC-DpUEIM9V.js} +1 -1
  81. package/web/assets/{chunk-VR4S4FIN-DZTsG51a.js → chunk-VR4S4FIN-DoQuFFRh.js} +1 -1
  82. package/web/assets/{chunk-XXDRQBXY-Dp0--RE2.js → chunk-XXDRQBXY-CI9eiLwn.js} +1 -1
  83. package/web/assets/classDiagram-OUVF2IWQ-BjsLQ3dz.js +1 -0
  84. package/web/assets/classDiagram-v2-EOCWNBFH-BjsLQ3dz.js +1 -0
  85. package/web/assets/client-7OWw5kW2.js +6 -0
  86. package/web/assets/client-DYj_td4Q.js +17 -0
  87. package/web/assets/{client-j2kQmxsO.js → client-Dk-NBvJR.js} +4 -4
  88. package/web/assets/command-zkzH4hVz.js +38 -0
  89. package/web/assets/{copy-context-C0Uqsp-3.js → copy-context-D5oiWrYC.js} +1 -1
  90. package/web/assets/{cose-bilkent-JH36ORCC-CPu3PI8h.js → cose-bilkent-JH36ORCC-BHRk1mO3.js} +1 -1
  91. package/web/assets/{cynefin-VYW2F7L2-DWxftYAm.js → cynefin-VYW2F7L2-C1B09TRE.js} +1 -1
  92. package/web/assets/{cynefinDiagram-TSTJHNR4-DsmN2V-p.js → cynefinDiagram-TSTJHNR4-B27qXQbv.js} +1 -1
  93. package/web/assets/{dagre-VKFMJZFB-COEbApyY.js → dagre-VKFMJZFB-B1qEi32g.js} +1 -1
  94. package/web/assets/{diagram-FQU43EPY-0O9WGWKB.js → diagram-FQU43EPY-BMkkgADD.js} +1 -1
  95. package/web/assets/{diagram-G47NLZAW-CutEdMVh.js → diagram-G47NLZAW-Cb8_g1A5.js} +1 -1
  96. package/web/assets/{diagram-NH7WQ7WH-C9KZc3B2.js → diagram-NH7WQ7WH-B51wuc5m.js} +1 -1
  97. package/web/assets/{diagram-OA4YK3LP-D8kkPXbY.js → diagram-OA4YK3LP-xnsecbbI.js} +1 -1
  98. package/web/assets/{diagram-WEI45ONY-_YqiTaGP.js → diagram-WEI45ONY-DSZdyaAV.js} +1 -1
  99. package/web/assets/{document-Dgt5eZFj.js → document-Dc2dftTW.js} +3 -3
  100. package/web/assets/{ebnfDiagram-CCIWWBDH-JO-zQh6g.js → ebnfDiagram-CCIWWBDH-aTHmcUGy.js} +1 -1
  101. package/web/assets/{entities-C3wz7FCx.js → entities-CCUtpskY.js} +1 -1
  102. package/web/assets/{erDiagram-Q63AITRT-9UjQwJ4P.js → erDiagram-Q63AITRT-CUUPBcvo.js} +1 -1
  103. package/web/assets/{file-viewer-DZJ8nYnl.js → file-viewer-DJ3rsmef.js} +1 -1
  104. package/web/assets/{flowDiagram-23GEKE2U-Ca08tnYM.js → flowDiagram-23GEKE2U-aTQXx2Mh.js} +1 -1
  105. package/web/assets/{folder-image-BfGgxo7i.js → folder-image-Bnv43tzP.js} +1 -1
  106. package/web/assets/{folder-landing-qUKBCnG-.js → folder-landing-DCnzYXXY.js} +1 -1
  107. package/web/assets/{folder-list-BfFjv7rR.js → folder-list-DvVkVjiR.js} +1 -1
  108. package/web/assets/{folder-metadata-DCw8xzHk.js → folder-metadata-C9hMMzIH.js} +1 -1
  109. package/web/assets/{folder-mobile-bar-Ubz4babf.js → folder-mobile-bar-VoTL1GS0.js} +1 -1
  110. package/web/assets/folder-plugin-view-Cvd804ZU.js +6 -0
  111. package/web/assets/{ganttDiagram-NO4QXBWP-KnHzX5W3.js → ganttDiagram-NO4QXBWP-DiwdUp7u.js} +1 -1
  112. package/web/assets/{gitGraphDiagram-IHSO6WYX-Cm8woNtK.js → gitGraphDiagram-IHSO6WYX-BL2gG_IS.js} +1 -1
  113. package/web/assets/{index-CNyy98F8.js → index-CUTtMSyJ.js} +2 -2
  114. package/web/assets/index-CegvVQ1C.js +1 -0
  115. package/web/assets/index-Dwhf8F_U.css +1 -0
  116. package/web/assets/{infoDiagram-FWYZ7A6U-yyqmUtIb.js → infoDiagram-FWYZ7A6U-B3n0zZpt.js} +1 -1
  117. package/web/assets/{ishikawaDiagram-FXEZZL3T-BfvR_Zy4.js → ishikawaDiagram-FXEZZL3T-D7YytbEv.js} +1 -1
  118. package/web/assets/{journeyDiagram-5HDEW3XC-BKuw06HM.js → journeyDiagram-5HDEW3XC-BYwqEl3C.js} +1 -1
  119. package/web/assets/{kanban-definition-HUTT4EX6-n0010rmQ.js → kanban-definition-HUTT4EX6-BQHNJgXZ.js} +1 -1
  120. package/web/assets/{linear-C0d0lT8O.js → linear-BPiQCLNk.js} +1 -1
  121. package/web/assets/{mermaid.core-BDVbShyO.js → mermaid.core-CD-KvBjV.js} +4 -4
  122. package/web/assets/{metadata-KV0dpp4G.js → metadata-COJ7fZul.js} +1 -1
  123. package/web/assets/{mindmap-definition-LN4V7U3C--_0ks3GF.js → mindmap-definition-LN4V7U3C-C1L_gyoj.js} +1 -1
  124. package/web/assets/{pegDiagram-2B236MQR-CBoNIpKR.js → pegDiagram-2B236MQR-BDAfItqS.js} +1 -1
  125. package/web/assets/{pieDiagram-ENE6RG2P-MA5tkYh5.js → pieDiagram-ENE6RG2P-CkZYH3Vz.js} +1 -1
  126. package/web/assets/{popover-DBD7FF-W.js → popover-0oA2lLUV.js} +2 -2
  127. package/web/assets/{project-search-dialog-DM1IkSZu.js → project-search-dialog-B2oqMT6h.js} +1 -1
  128. package/web/assets/{quadrantDiagram-ABIIQ3AL-CWT1ryqF.js → quadrantDiagram-ABIIQ3AL-Dy8Al3YZ.js} +1 -1
  129. package/web/assets/{railroadDiagram-RFXS5EU6-ZiLB5FGi.js → railroadDiagram-RFXS5EU6-Dd9_yo7B.js} +1 -1
  130. package/web/assets/{requirementDiagram-TGXJPOKE-CQq9uneq.js → requirementDiagram-TGXJPOKE-DPhIUsbs.js} +1 -1
  131. package/web/assets/{sankeyDiagram-HTMAVEWB-qDG-0JqI.js → sankeyDiagram-HTMAVEWB-B2QkYtub.js} +1 -1
  132. package/web/assets/{search-DNrQA5fF.js → search-C1J0-xiB.js} +1 -1
  133. package/web/assets/select-CtXC_PLC.js +30 -0
  134. package/web/assets/{selection-popup-DmtTeRs2.js → selection-popup-qhlUFzGq.js} +1 -1
  135. package/web/assets/{sequenceDiagram-DBY2YBRQ-CiApurLO.js → sequenceDiagram-DBY2YBRQ-Bx7fcZDx.js} +1 -1
  136. package/web/assets/{sizeCapture-X5ZJPWSS-DZSR5QXm.js → sizeCapture-X5ZJPWSS-CIXIhl_A.js} +1 -1
  137. package/web/assets/{stateDiagram-2N3HPSRC-CjJJR9_z.js → stateDiagram-2N3HPSRC-DCrdAflB.js} +1 -1
  138. package/web/assets/stateDiagram-v2-6OUMAXLB-0KCFLYeQ.js +1 -0
  139. package/web/assets/{swimlanes-5IMT3BWC-CKmo1sg6.js → swimlanes-5IMT3BWC-DA2vc7iZ.js} +2 -2
  140. package/web/assets/swimlanesDiagram-G3AALYLV-CAw3AV84.js +8 -0
  141. package/web/assets/{timeline-definition-FHXFAJF6-D-NMwS3y.js → timeline-definition-FHXFAJF6-lKfJiV9V.js} +1 -1
  142. package/web/assets/{vennDiagram-L72KCM5P-CfL6LBWw.js → vennDiagram-L72KCM5P-DO1q2KzG.js} +1 -1
  143. package/web/assets/{view-context-gyDl_p2V.js → view-context-DXKM3-l1.js} +4 -4
  144. package/web/assets/{viewer-capabilities-Bpp8Uvcg.js → viewer-capabilities-Cig_xM9u.js} +1 -1
  145. package/web/assets/{wardleyDiagram-EHGQE667-DgldMxaj.js → wardleyDiagram-EHGQE667-Dal9LkuZ.js} +1 -1
  146. package/web/assets/{xychartDiagram-FW5EYKEG-CIoAIFX8.js → xychartDiagram-FW5EYKEG-B3dPLPII.js} +1 -1
  147. package/web/index.html +2 -2
  148. package/kernel/packages/api/test/partial-loading.test.ts +0 -43
  149. package/kernel/packages/folder-plugin/package.json +0 -50
  150. package/kernel/packages/folder-plugin/src/index.ts +0 -15
  151. package/kernel/packages/folder-plugin/tsconfig.json +0 -17
  152. package/kernel/plugins/calendar/SKILL.md +0 -173
  153. package/kernel/plugins/calendar/eslint.config.js +0 -10
  154. package/kernel/plugins/calendar/package.json +0 -73
  155. package/kernel/plugins/calendar/schema.json +0 -137
  156. package/kernel/plugins/calendar/src/calendar.tsx +0 -328
  157. package/kernel/plugins/calendar/src/client.tsx +0 -139
  158. package/kernel/plugins/calendar/src/context.ts +0 -48
  159. package/kernel/plugins/calendar/src/copy-button.tsx +0 -66
  160. package/kernel/plugins/calendar/src/event-dialog.tsx +0 -270
  161. package/kernel/plugins/calendar/src/index.ts +0 -4
  162. package/kernel/plugins/calendar/src/manifest.ts +0 -7
  163. package/kernel/plugins/calendar/src/parse.ts +0 -610
  164. package/kernel/plugins/calendar/src/server.ts +0 -160
  165. package/kernel/plugins/calendar/src/utils.ts +0 -80
  166. package/kernel/plugins/calendar/src/views.tsx +0 -418
  167. package/kernel/plugins/calendar/test/context.test.mjs +0 -60
  168. package/kernel/plugins/calendar/test/package.test.mjs +0 -43
  169. package/kernel/plugins/calendar/test/parse.test.mjs +0 -392
  170. package/kernel/plugins/calendar/test/server.test.mjs +0 -126
  171. package/kernel/plugins/calendar/tsconfig.json +0 -17
  172. package/kernel/plugins/mail/SKILL.md +0 -97
  173. package/kernel/plugins/mail/eslint.config.js +0 -10
  174. package/kernel/plugins/mail/src/api.ts +0 -359
  175. package/kernel/plugins/mail/src/bun-ffi.d.ts +0 -14
  176. package/kernel/plugins/mail/src/client.tsx +0 -14
  177. package/kernel/plugins/mail/src/flock.ts +0 -137
  178. package/kernel/plugins/mail/src/folders.ts +0 -37
  179. package/kernel/plugins/mail/src/history.ts +0 -485
  180. package/kernel/plugins/mail/src/index.ts +0 -3
  181. package/kernel/plugins/mail/src/mail-card.tsx +0 -123
  182. package/kernel/plugins/mail/src/mail-conversation.tsx +0 -153
  183. package/kernel/plugins/mail/src/mail-detail-page.tsx +0 -467
  184. package/kernel/plugins/mail/src/mail-hooks.ts +0 -252
  185. package/kernel/plugins/mail/src/mail-page.tsx +0 -109
  186. package/kernel/plugins/mail/src/mail-request.ts +0 -78
  187. package/kernel/plugins/mail/src/mail-send-box.tsx +0 -175
  188. package/kernel/plugins/mail/src/mailos.ts +0 -723
  189. package/kernel/plugins/mail/src/manifest.ts +0 -7
  190. package/kernel/plugins/mail/src/node.ts +0 -171
  191. package/kernel/plugins/mail/src/parse.ts +0 -139
  192. package/kernel/plugins/mail/src/server.ts +0 -125
  193. package/kernel/plugins/mail/src/types.ts +0 -10
  194. package/kernel/plugins/mail/src/viewer.tsx +0 -147
  195. package/kernel/plugins/mail/test/flock.test.mjs +0 -156
  196. package/kernel/plugins/mail/test/folders.test.mjs +0 -138
  197. package/kernel/plugins/mail/test/history.test.mjs +0 -460
  198. package/kernel/plugins/mail/test/mailos.test.mjs +0 -752
  199. package/kernel/plugins/mail/test/node.test.mjs +0 -207
  200. package/kernel/plugins/mail/test/package.test.mjs +0 -54
  201. package/kernel/plugins/mail/test/parse.test.mjs +0 -32
  202. package/kernel/plugins/mail/test/server.test.mjs +0 -116
  203. package/kernel/plugins/mail/test/viewer.test.mjs +0 -192
  204. package/kernel/plugins/mail/tsconfig.json +0 -17
  205. package/kernel/plugins/projects/SKILL.md +0 -73
  206. package/kernel/plugins/projects/eslint.config.js +0 -10
  207. package/kernel/plugins/projects/package.json +0 -70
  208. package/kernel/plugins/projects/src/client.tsx +0 -17
  209. package/kernel/plugins/projects/src/data/model.ts +0 -129
  210. package/kernel/plugins/projects/src/index.ts +0 -17
  211. package/kernel/plugins/projects/src/manifest.ts +0 -7
  212. package/kernel/plugins/projects/src/project-context.ts +0 -41
  213. package/kernel/plugins/projects/src/server.ts +0 -276
  214. package/kernel/plugins/projects/src/ui/project-card.tsx +0 -61
  215. package/kernel/plugins/projects/src/ui/projects-page.tsx +0 -102
  216. package/kernel/plugins/projects/test/package.test.mjs +0 -51
  217. package/kernel/plugins/tasks/SKILL.md +0 -341
  218. package/kernel/plugins/tasks/eslint.config.js +0 -10
  219. package/kernel/plugins/tasks/package.json +0 -74
  220. package/kernel/plugins/tasks/schema.json +0 -939
  221. package/kernel/plugins/tasks/src/api.ts +0 -212
  222. package/kernel/plugins/tasks/src/client.tsx +0 -23
  223. package/kernel/plugins/tasks/src/data/parse.ts +0 -544
  224. package/kernel/plugins/tasks/src/data/task-id.ts +0 -56
  225. package/kernel/plugins/tasks/src/data/todo-model.ts +0 -795
  226. package/kernel/plugins/tasks/src/index.ts +0 -34
  227. package/kernel/plugins/tasks/src/manifest.ts +0 -7
  228. package/kernel/plugins/tasks/src/pages/task-detail.tsx +0 -48
  229. package/kernel/plugins/tasks/src/pages/task-list.tsx +0 -88
  230. package/kernel/plugins/tasks/src/server.ts +0 -447
  231. package/kernel/plugins/tasks/src/styles.css +0 -9
  232. package/kernel/plugins/tasks/src/ui/compact-todo-list.tsx +0 -372
  233. package/kernel/plugins/tasks/src/ui/error-details.tsx +0 -78
  234. package/kernel/plugins/tasks/src/ui/task-page.tsx +0 -392
  235. package/kernel/plugins/tasks/src/ui/todo-agent-filter.tsx +0 -36
  236. package/kernel/plugins/tasks/src/ui/todo-filter-bar.tsx +0 -66
  237. package/kernel/plugins/tasks/src/ui/todo-item.tsx +0 -494
  238. package/kernel/plugins/tasks/src/ui/todo-status-filter.tsx +0 -60
  239. package/kernel/plugins/tasks/src/ui/todo-tag-filter.tsx +0 -48
  240. package/kernel/plugins/tasks/src/ui/todo-warnings.tsx +0 -106
  241. package/kernel/plugins/tasks/test/api.test.mjs +0 -99
  242. package/kernel/plugins/tasks/test/folder-sources.test.mjs +0 -46
  243. package/kernel/plugins/tasks/test/package.test.mjs +0 -54
  244. package/kernel/plugins/tasks/test/parse.test.mjs +0 -225
  245. package/kernel/plugins/tasks/test/server.test.mjs +0 -386
  246. package/kernel/plugins/tasks/tsconfig.json +0 -17
  247. package/web/assets/app-DoGr2Zi7.js +0 -2
  248. package/web/assets/channel-uMEjV8qc.js +0 -1
  249. package/web/assets/classDiagram-OUVF2IWQ-fbZocVor.js +0 -1
  250. package/web/assets/classDiagram-v2-EOCWNBFH-fbZocVor.js +0 -1
  251. package/web/assets/client-C6Nxdfif.js +0 -2
  252. package/web/assets/client-CXYfBko5.js +0 -17
  253. package/web/assets/client-CoKtOq-X.js +0 -7
  254. package/web/assets/client-DyRQz5RU.js +0 -53
  255. package/web/assets/client-OY6HUzuD.js +0 -98
  256. package/web/assets/client-YOYWZIFi.js +0 -34
  257. package/web/assets/command-BugZBGXE.js +0 -38
  258. package/web/assets/folder-plugin-view-DsWR1zmQ.js +0 -6
  259. package/web/assets/index-BaRnbYRw.js +0 -1
  260. package/web/assets/index-BexmLxIP.css +0 -1
  261. package/web/assets/index-DDirZMgx.js +0 -1
  262. package/web/assets/note-card-DBfX6gW1.js +0 -6
  263. package/web/assets/select-D8cqNgcp.js +0 -30
  264. package/web/assets/stateDiagram-v2-6OUMAXLB-BpeGSCu9.js +0 -1
  265. package/web/assets/swimlanesDiagram-G3AALYLV-D9RjLKb9.js +0 -8
  266. package/web/assets/use-folder-filter-LQyXns5Q.js +0 -1
  267. /package/kernel/packages/{folder-plugin → plugins}/eslint.config.js +0 -0
  268. /package/kernel/packages/{folder-plugin → plugins}/src/client.ts +0 -0
  269. /package/kernel/packages/{folder-plugin → plugins}/src/endpoint.ts +0 -0
  270. /package/kernel/packages/{folder-plugin → plugins}/src/server.ts +0 -0
  271. /package/kernel/packages/{folder-plugin → plugins}/src/subpath.ts +0 -0
  272. /package/kernel/packages/{folder-plugin → plugins}/src/types.ts +0 -0
  273. /package/kernel/packages/{folder-plugin → plugins}/src/validation.ts +0 -0
  274. /package/kernel/packages/{folder-plugin → plugins}/test/contract.test.mjs +0 -0
@@ -1,485 +0,0 @@
1
- import { createHash } from "node:crypto"
2
- import {
3
- lstat,
4
- mkdir,
5
- readFile,
6
- realpath,
7
- rename,
8
- unlink,
9
- writeFile,
10
- } from "node:fs/promises"
11
- import { dirname, isAbsolute, relative, resolve, sep } from "node:path"
12
-
13
- import { FlockTimeoutError, withFlock } from "./flock.ts"
14
- import { parseMailMarkdown } from "./parse.ts"
15
-
16
- export type DraftIdentity = {
17
- id: string
18
- filePath: string
19
- folder: string | null
20
- /** sha256 of the draft bytes the client last loaded. */
21
- revision: string
22
- }
23
-
24
- export type DeleteOutcome = "deleted" | "changed" | "missing" | "invalid"
25
-
26
- export class MailDeleteError extends Error {
27
- readonly outcome: DeleteOutcome
28
-
29
- constructor(outcome: DeleteOutcome, message: string) {
30
- super(message)
31
- this.name = "MailDeleteError"
32
- this.outcome = outcome
33
- }
34
- }
35
-
36
- /**
37
- * How long delete waits for the MailOS-compatible flock before giving up.
38
- * MailOS blocks indefinitely; Kennel bounds the wait so an abandoned holder
39
- * cannot hang a request, and reports the result as a conflict instead.
40
- */
41
- export const deleteLockTimeoutMs = 30_000
42
-
43
- function validDraftId(id: string) {
44
- const trimmed = id.trim()
45
- return (
46
- Boolean(trimmed) &&
47
- trimmed !== "." &&
48
- trimmed !== ".." &&
49
- !/[\\/]/.test(trimmed)
50
- )
51
- }
52
-
53
- function draftRevision(content: string) {
54
- return createHash("sha256").update(content).digest("hex")
55
- }
56
-
57
- /**
58
- * Walk `childPath` (which must be inside canonical `anchor`) and make sure
59
- * every existing component is a real directory — never a symlink —
60
- * creating missing components ourselves. This closes the history/locks
61
- * directory symlink-escape: a planted `history`, `<id>`, or `.locks` symlink
62
- * is rejected instead of followed.
63
- */
64
- async function ensureRealDirChain(
65
- anchor: string,
66
- childPath: string,
67
- ): Promise<void> {
68
- const rel = relative(anchor, childPath)
69
- if (!rel || rel === ".." || rel.startsWith(`..${sep}`) || isAbsolute(rel)) {
70
- throw new MailDeleteError(
71
- "invalid",
72
- "Lock/history path escapes the mail folder",
73
- )
74
- }
75
- let current = anchor
76
- for (const segment of rel.split(sep)) {
77
- if (!segment) continue
78
- current = resolve(current, segment)
79
- let info = await lstat(current).catch(() => undefined)
80
- if (!info) {
81
- await mkdir(current, { mode: 0o700 }).catch(() => undefined)
82
- info = await lstat(current).catch(() => undefined)
83
- if (!info || info.isSymbolicLink() || !info.isDirectory()) {
84
- throw new MailDeleteError(
85
- "invalid",
86
- "Could not create a real directory for the draft lock/history",
87
- )
88
- }
89
- continue
90
- }
91
- if (info.isSymbolicLink() || !info.isDirectory()) {
92
- throw new MailDeleteError(
93
- "invalid",
94
- "Draft lock/history path must use real directories (no symlinked components)",
95
- )
96
- }
97
- }
98
- }
99
-
100
- /**
101
- * Delete a local draft with a byte-exact, verified history snapshot, under
102
- * the real MailOS advisory lock.
103
- *
104
- * Locking — `withFlock` takes the exclusive `flock(2)` on
105
- * `<mailDir>/.locks/<id>.md.lock`, the exact file MailOS holds
106
- * (`emailos/internal/core/drafts.go withLocalDraftLock`) while it
107
- * snapshots/updates/sends a draft. Kennel's delete/archive critical section
108
- * therefore serializes against the MailOS binary itself, not only against
109
- * other Kennel processes.
110
- *
111
- * Byte-level guarantees (inside the lock):
112
- * - the draft file's current bytes must still hash to the revision the client
113
- * loaded, or the delete is refused ("changed") and newer work survives;
114
- * - the snapshot written to history/<id>/<revision>.md is byte-identical to
115
- * the draft content (temp sibling + rename + verified re-read/hash);
116
- * - pre-existing history is verified byte-for-byte first — corrupt existing
117
- * history is never overwritten or deleted, and the draft is left in place;
118
- * - the draft is re-read and re-hashed immediately before the unlink, and its
119
- * realpath re-verified, so the final unlink only ever removes the exact
120
- * file whose bytes matched the loaded revision at that moment.
121
- *
122
- * Exact guarantee (limitation): a non-cooperating writer that does not take
123
- * the flock and rewrites the file *after* the final re-read but *before* the
124
- * unlink can still have newer work deleted — the same two-instruction window
125
- * MailOS itself has between its locked re-read and remove. No filesystem API
126
- * can make a byte-compare-and-unlink atomic; the flock + closing re-read +
127
- * realpath re-check reduce this to the unavoidable sub-microsecond window,
128
- * and cooperative writers (Kennel, MailOS send/archive/snapshot) are fully
129
- * serialized.
130
- */
131
- export async function deleteLocalDraftVerified(
132
- mail: DraftIdentity,
133
- mailDir: string,
134
- ): Promise<"deleted"> {
135
- if (!validDraftId(mail.id)) {
136
- throw new MailDeleteError("invalid", "Invalid mail id")
137
- }
138
- const mailDirReal = await realpath(resolve(mailDir))
139
- const target = resolve(mail.filePath)
140
- const targetDir = await realpath(dirname(target)).catch(() => null)
141
- if (targetDir !== mailDirReal) {
142
- throw new MailDeleteError(
143
- "invalid",
144
- "Draft path is outside the mail folder",
145
- )
146
- }
147
- // The draft file itself must be the plain expected file: a symlink that
148
- // realpaths somewhere else is rejected rather than followed.
149
- const expected = resolve(mailDirReal, `${mail.id.trim()}.md`)
150
- const targetReal = await realpath(target).catch(() => null)
151
- if (targetReal === null) {
152
- throw new MailDeleteError("missing", "Draft no longer exists")
153
- }
154
- if (targetReal !== expected) {
155
- throw new MailDeleteError(
156
- "invalid",
157
- "Draft path is not a direct file in the mail folder",
158
- )
159
- }
160
-
161
- if (!/^[a-f0-9]{64}$/i.test(mail.revision)) {
162
- throw new MailDeleteError("invalid", "Invalid draft revision")
163
- }
164
-
165
- const locksDir = resolve(mailDirReal, ".locks")
166
- await ensureRealDirChain(mailDirReal, locksDir)
167
- const lockPath = resolve(locksDir, `${mail.id.trim()}.md.lock`)
168
-
169
- try {
170
- return await withFlock(lockPath, deleteLockTimeoutMs, async () => {
171
- // Under the lock: the re-read, the hash, the snapshot, the re-verify
172
- // and the unlink are one critical section.
173
- const content = await readFile(target, "utf8").catch(() => null)
174
- if (content === null) {
175
- throw new MailDeleteError("missing", "Draft no longer exists")
176
- }
177
- if (draftRevision(content) !== mail.revision.toLowerCase()) {
178
- throw new MailDeleteError(
179
- "changed",
180
- "Draft changed since it was loaded; refusing to delete newer work",
181
- )
182
- }
183
-
184
- const historyDir = resolve(mailDirReal, "history", mail.id)
185
- await ensureRealDirChain(mailDirReal, historyDir)
186
- const historyPath = resolve(historyDir, `${mail.revision}.md`)
187
-
188
- const existingLstat = await lstat(historyPath).catch(() => undefined)
189
- if (existingLstat?.isSymbolicLink()) {
190
- throw new MailDeleteError(
191
- "invalid",
192
- "History snapshot path is a symlink; refusing to touch it",
193
- )
194
- }
195
- const existing = existingLstat ?? undefined
196
- if (existing?.isFile()) {
197
- // Never overwrite or trust an existing snapshot: verify its bytes
198
- // still hash to the revision, exactly like MailOS
199
- // (readVerifiedLocalDraftRevisionForPath). A corrupt existing history
200
- // blocks the delete rather than being clobbered.
201
- const existingContent = await readFile(historyPath, "utf8").catch(
202
- () => null,
203
- )
204
- if (
205
- existingContent === null ||
206
- draftRevision(existingContent) !== mail.revision.toLowerCase()
207
- ) {
208
- throw new MailDeleteError(
209
- "changed",
210
- "Existing draft history does not match this revision; refusing to delete",
211
- )
212
- }
213
- } else if (existing) {
214
- throw new MailDeleteError(
215
- "invalid",
216
- "History snapshot path is not a regular file",
217
- )
218
- } else {
219
- // Write to a temp sibling then rename so a crash never leaves a
220
- // partial snapshot under the revision name. "wx" refuses to follow or
221
- // overwrite a planted temp symlink.
222
- const tempPath = resolve(historyDir, `${mail.revision}.md.tmp`)
223
- for (let attempt = 0; ; attempt += 1) {
224
- try {
225
- await writeFile(tempPath, content, { flag: "wx", mode: 0o600 })
226
- break
227
- } catch (error) {
228
- if ((error as NodeJS.ErrnoException).code !== "EEXIST") throw error
229
- const tempInfo = await lstat(tempPath).catch(() => undefined)
230
- if (attempt === 0 && tempInfo?.isFile()) {
231
- // A stale temp from an interrupted run: remove and write once
232
- // more. A symlink/directory is never followed.
233
- await unlink(tempPath)
234
- continue
235
- }
236
- throw new MailDeleteError(
237
- "invalid",
238
- "Temp snapshot path is not a plain file; refusing to write through it",
239
- )
240
- }
241
- }
242
- const written = await readFile(tempPath, "utf8").catch(() => null)
243
- if (
244
- written !== content ||
245
- draftRevision(written ?? "") !== mail.revision.toLowerCase()
246
- ) {
247
- throw new MailDeleteError(
248
- "changed",
249
- "Draft changed while snapshotting; refusing to delete",
250
- )
251
- }
252
- await rename(tempPath, historyPath)
253
- }
254
-
255
- // Re-verify the draft is byte-identical to the snapshot immediately
256
- // before unlinking, and that it is still the same regular file; an
257
- // editor or symlink that raced in is left untouched.
258
- const current = await readFile(target, "utf8").catch(() => null)
259
- if (current === null) {
260
- throw new MailDeleteError("missing", "Draft no longer exists")
261
- }
262
- if (draftRevision(current) !== mail.revision.toLowerCase()) {
263
- throw new MailDeleteError(
264
- "changed",
265
- "Draft changed since it was loaded; refusing to delete newer work",
266
- )
267
- }
268
- const finalReal = await realpath(target).catch(() => null)
269
- if (finalReal !== expected) {
270
- throw new MailDeleteError(
271
- "invalid",
272
- "Draft path changed identity before deletion; refusing to unlink",
273
- )
274
- }
275
-
276
- await unlink(target)
277
- return "deleted" as const
278
- })
279
- } catch (error) {
280
- if (error instanceof FlockTimeoutError) {
281
- throw new MailDeleteError(
282
- "changed",
283
- "Draft is locked by another process (MailOS or another delete); try again",
284
- )
285
- }
286
- throw error
287
- }
288
- }
289
-
290
- export type AttachmentRemoveOutcome =
291
- "removed" | "changed" | "missing" | "invalid"
292
-
293
- export class MailAttachmentError extends Error {
294
- readonly outcome: AttachmentRemoveOutcome
295
-
296
- constructor(outcome: AttachmentRemoveOutcome, message: string) {
297
- super(message)
298
- this.name = "MailAttachmentError"
299
- this.outcome = outcome
300
- }
301
- }
302
-
303
- /**
304
- * Rewrite the frontmatter `file:` block in place: drop the given attachment
305
- * path and bump `updated_at`. Key order, quoting, and all other lines stay
306
- * byte-identical, so MailOS reads the file exactly as before with one fewer
307
- * linked file. Returns null when there is no `file:` key.
308
- */
309
- function rewriteFileFrontmatter(
310
- frontmatterRaw: string,
311
- remaining: string[],
312
- updatedAt: string,
313
- ): string | null {
314
- const lines = frontmatterRaw.split("\n")
315
- const fileIndex = lines.findIndex((line) => /^file:/.test(line))
316
- if (fileIndex < 0) return null
317
-
318
- // Eat the block-list item lines directly below an empty `file:` key.
319
- let end = fileIndex + 1
320
- if ((lines[fileIndex] ?? "").trim() === "file:") {
321
- while (end < lines.length) {
322
- if (!/^\s+- /.test(lines[end] ?? "")) break
323
- end += 1
324
- }
325
- }
326
-
327
- const fileBlock = remaining.length
328
- ? ["file:", ...remaining.map((path) => ` - ${JSON.stringify(path)}`)]
329
- : ["file: []"]
330
- lines.splice(fileIndex, end - fileIndex, ...fileBlock)
331
-
332
- const updatedIndex = lines.findIndex((line) => /^updated_at:/.test(line))
333
- if (updatedIndex >= 0) {
334
- lines[updatedIndex] = `updated_at: ${JSON.stringify(updatedAt)}`
335
- }
336
-
337
- return lines.join("\n")
338
- }
339
-
340
- /**
341
- * Remove one attachment path from a local draft's frontmatter `file:` list.
342
- * Mirrors deleteLocalDraftVerified's safety pattern: the draft must be a real
343
- * file in the mail folder, the edit runs under the same MailOS-compatible
344
- * flock with a byte-exact revision guard, the rewritten file is written to a
345
- * temp sibling and atomically renamed, and `updated_at` is bumped. The
346
- * attachment file itself is never touched — only the link in the draft.
347
- * Returns the new draft revision so callers can continue against fresh data.
348
- */
349
- export async function removeDraftAttachmentVerified(
350
- mail: DraftIdentity,
351
- mailDir: string,
352
- attachment: string,
353
- ): Promise<{ revision: string }> {
354
- if (!validDraftId(mail.id)) {
355
- throw new MailAttachmentError("invalid", "Invalid mail id")
356
- }
357
- const mailDirReal = await realpath(resolve(mailDir))
358
- const target = resolve(mail.filePath)
359
- const targetDir = await realpath(dirname(target)).catch(() => null)
360
- if (targetDir !== mailDirReal) {
361
- throw new MailAttachmentError(
362
- "invalid",
363
- "Draft path is outside the mail folder",
364
- )
365
- }
366
- const expected = resolve(mailDirReal, `${mail.id.trim()}.md`)
367
- const targetReal = await realpath(target).catch(() => null)
368
- if (targetReal === null) {
369
- throw new MailAttachmentError("missing", "Draft no longer exists")
370
- }
371
- if (targetReal !== expected) {
372
- throw new MailAttachmentError(
373
- "invalid",
374
- "Draft path is not a direct file in the mail folder",
375
- )
376
- }
377
- if (!/^[a-f0-9]{64}$/i.test(mail.revision)) {
378
- throw new MailAttachmentError("invalid", "Invalid draft revision")
379
- }
380
-
381
- const locksDir = resolve(mailDirReal, ".locks")
382
- await ensureRealDirChain(mailDirReal, locksDir)
383
- const lockPath = resolve(locksDir, `${mail.id.trim()}.md.lock`)
384
-
385
- try {
386
- return await withFlock(lockPath, deleteLockTimeoutMs, async () => {
387
- const revision = mail.revision.toLowerCase()
388
- const content = await readFile(target, "utf8").catch(() => null)
389
- if (content === null) {
390
- throw new MailAttachmentError("missing", "Draft no longer exists")
391
- }
392
- if (draftRevision(content) !== revision) {
393
- throw new MailAttachmentError(
394
- "changed",
395
- "Draft changed since it was loaded; refusing to edit",
396
- )
397
- }
398
- if (!content.startsWith("---\n")) {
399
- throw new MailAttachmentError("invalid", "Draft has no frontmatter")
400
- }
401
- const end = content.indexOf("\n---\n")
402
- if (end < 0) {
403
- throw new MailAttachmentError("invalid", "Draft has no frontmatter")
404
- }
405
-
406
- const parsed = parseMailMarkdown(content)
407
- if (!parsed.attachments.includes(attachment)) {
408
- throw new MailAttachmentError(
409
- "missing",
410
- "Attachment is not linked to this draft",
411
- )
412
- }
413
- const remaining = parsed.attachments.filter((path) => path !== attachment)
414
- const frontmatterRaw = content.slice(4, end)
415
- const newFrontmatter = rewriteFileFrontmatter(
416
- frontmatterRaw,
417
- remaining,
418
- new Date().toISOString(),
419
- )
420
- if (newFrontmatter === null) {
421
- throw new MailAttachmentError(
422
- "invalid",
423
- "Draft frontmatter has no file list",
424
- )
425
- }
426
- const newContent = `---\n${newFrontmatter}\n---\n${content.slice(
427
- end + "\n---\n".length,
428
- )}`
429
-
430
- const tempPath = resolve(mailDirReal, `${mail.id.trim()}.md.tmp`)
431
- for (let attempt = 0; ; attempt += 1) {
432
- try {
433
- await writeFile(tempPath, newContent, { flag: "wx", mode: 0o600 })
434
- break
435
- } catch (error) {
436
- if ((error as NodeJS.ErrnoException).code !== "EEXIST") throw error
437
- const tempInfo = await lstat(tempPath).catch(() => undefined)
438
- if (attempt === 0 && tempInfo?.isFile()) {
439
- await unlink(tempPath)
440
- continue
441
- }
442
- throw new MailAttachmentError(
443
- "invalid",
444
- "Temp draft path is not a plain file; refusing to write through it",
445
- )
446
- }
447
- }
448
- const written = await readFile(tempPath, "utf8").catch(() => null)
449
- if (written !== newContent) {
450
- throw new MailAttachmentError(
451
- "changed",
452
- "Draft changed while writing; refusing to save",
453
- )
454
- }
455
-
456
- // The draft must still hash to the client's revision immediately
457
- // before the rename, so a concurrent editor is never overwritten.
458
- const current = await readFile(target, "utf8").catch(() => null)
459
- if (current === null) {
460
- throw new MailAttachmentError("missing", "Draft no longer exists")
461
- }
462
- if (draftRevision(current) !== revision) {
463
- throw new MailAttachmentError(
464
- "changed",
465
- "Draft changed since it was loaded; refusing to edit",
466
- )
467
- }
468
- await rename(tempPath, target)
469
-
470
- return { revision: draftRevision(newContent) }
471
- })
472
- } catch (error) {
473
- if (error instanceof FlockTimeoutError) {
474
- throw new MailAttachmentError(
475
- "changed",
476
- "Draft is locked by another process (MailOS or another edit); try again",
477
- )
478
- }
479
- throw error
480
- }
481
- }
482
-
483
- export function historyDirFor(mail: DraftIdentity, mailDir: string) {
484
- return resolve(resolve(mailDir), "history", mail.id)
485
- }
@@ -1,3 +0,0 @@
1
- export type { Mail, MailListItem, MailStatus } from "./parse.ts"
2
- export { mailExcerpt, parseMailMarkdown } from "./parse.ts"
3
- export type { MailPreview } from "./types.ts"
@@ -1,123 +0,0 @@
1
- import type { ReactNode } from "react"
2
- import { Folder, Mail } from "lucide-react"
3
-
4
- import type { MailListItem } from "./viewer.tsx"
5
-
6
- function relativeTime(value: string | null) {
7
- if (!value) return "—"
8
- const parsed = Date.parse(value)
9
- if (Number.isNaN(parsed)) return value
10
- const delta = Date.now() - parsed
11
- const minutes = Math.round(delta / 60_000)
12
- if (minutes < 1) return "just now"
13
- if (minutes < 60) return `${minutes}m ago`
14
- const hours = Math.round(minutes / 60)
15
- if (hours < 48) return `${hours}h ago`
16
- const days = Math.round(hours / 24)
17
- return `${days}d ago`
18
- }
19
-
20
- export function MailCard({
21
- actions,
22
- folderLabel,
23
- href,
24
- item,
25
- }: {
26
- actions?: (item: MailListItem) => ReactNode
27
- folderLabel?: string | null
28
- href: string
29
- item: MailListItem
30
- }) {
31
- const recipientGroup = item.to.length
32
- ? { label: "To", items: item.to }
33
- : item.cc.length
34
- ? { label: "CC", items: item.cc }
35
- : item.bcc.length
36
- ? { label: "BCC", items: item.bcc }
37
- : null
38
-
39
- const recipient = item.to[0] ?? ""
40
- const address = /<([^<>]+)>/.exec(recipient)?.[1] ?? recipient
41
- const domain = /^[^\s@<>]+@([a-z0-9.-]+\.[a-z]{2,})$/i
42
- .exec(address.trim())?.[1]
43
- ?.toLowerCase()
44
-
45
- return (
46
- <li className="relative min-w-0">
47
- <a
48
- className="grid grid-cols-[2rem_minmax(0,1fr)] items-start gap-3
49
- rounded-xl border border-[#e7ebe7] bg-white px-3.5 py-2.5 pr-10
50
- text-[#2f382f] no-underline shadow-[0_1px_2px_rgb(31_41_55/0.05)]
51
- hover:bg-[#fafbfa] focus-visible:outline-2
52
- focus-visible:outline-offset-1 focus-visible:outline-[#56725e]"
53
- href={href}
54
- >
55
- <span
56
- aria-hidden="true"
57
- className="relative flex size-8 items-center justify-center rounded-md
58
- bg-[#f3f5f2] text-[#6c7a6d]"
59
- >
60
- <Mail className="size-4" />
61
- {domain ? (
62
- <img
63
- key={domain}
64
- alt=""
65
- className="absolute size-6 rounded-sm bg-[#f3f5f2] object-contain"
66
- height={24}
67
- loading="lazy"
68
- onError={(event) => {
69
- event.currentTarget.style.display = "none"
70
- }}
71
- referrerPolicy="no-referrer"
72
- src={`https://www.google.com/s2/favicons?domain=${encodeURIComponent(domain)}&sz=32`}
73
- width={24}
74
- />
75
- ) : null}
76
- </span>
77
- <div className="grid min-w-0 gap-1">
78
- <span className="line-clamp-2 min-w-0 font-semibold break-words">
79
- {item.subject ?? "(no subject)"}
80
- </span>
81
-
82
- <div className="flex items-start gap-2">
83
- <span
84
- className="shrink-0 text-[0.75rem] whitespace-nowrap
85
- text-[#6c7a6d]"
86
- >
87
- {relativeTime(item.updatedAt ?? item.createdAt)}
88
- </span>
89
- {recipientGroup ? (
90
- <span className="min-w-0 truncate text-[0.82rem] text-[#4a554b]">
91
- {recipientGroup.label}: {recipientGroup.items[0]}
92
- {recipientGroup.items.length > 1
93
- ? ` +${recipientGroup.items.length - 1}`
94
- : ""}
95
- </span>
96
- ) : (
97
- <span className="text-[0.82rem] text-[#6c7a6d]">
98
- No recipients
99
- </span>
100
- )}
101
- </div>
102
- {folderLabel ? (
103
- <span
104
- className="flex min-w-0 items-center gap-1 text-xs text-[#6c7a6d]"
105
- title={item.folder ?? undefined}
106
- >
107
- <Folder aria-hidden="true" className="size-3 shrink-0" />
108
- <span className="truncate">{folderLabel}</span>
109
- </span>
110
- ) : null}
111
- {item.excerpt ? (
112
- <p className="m-0 min-w-0 truncate text-[0.82rem] text-[#6c7a6d]">
113
- {item.excerpt}
114
- </p>
115
- ) : null}
116
- </div>
117
- </a>
118
- {actions ? (
119
- <div className="absolute top-2 right-2">{actions(item)}</div>
120
- ) : null}
121
- </li>
122
- )
123
- }