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,610 +0,0 @@
1
- import {
2
- addDays,
3
- addMonths,
4
- addWeeks,
5
- differenceInCalendarDays,
6
- differenceInCalendarMonths,
7
- endOfDay,
8
- isSameDay,
9
- } from "date-fns"
10
-
11
- import type { ContentIssue } from "@raggle-ai/folder-plugin/validation"
12
- import {
13
- issueSummary,
14
- parseJsonSource,
15
- } from "@raggle-ai/folder-plugin/validation"
16
-
17
- export type CalendarColor =
18
- "blue" | "green" | "red" | "yellow" | "purple" | "orange"
19
-
20
- export type CalendarRepeating = "daily" | "weekly" | "monthly"
21
- export type CalendarStatus = "confirmed" | "tentative" | "cancelled"
22
-
23
- export interface CalendarEvent {
24
- id: string
25
- title: string
26
- start: Date
27
- end: Date
28
- /**
29
- * Absolute owning folder of the `calendar.json` that declared this event,
30
- * stamped by the server at merge time (not part of the source document).
31
- */
32
- folder?: string
33
- description?: string
34
- group?: string
35
- groupColor?: CalendarColor
36
- color?: CalendarColor
37
- repeating?: CalendarRepeating
38
- allDay?: boolean
39
- attendees?: string[]
40
- excludeDates?: Date[]
41
- location?: string
42
- organizer?: string
43
- reminders?: number[]
44
- repeatUntil?: Date
45
- status?: CalendarStatus
46
- tags?: string[]
47
- url?: string
48
- }
49
-
50
- export interface CalendarDocument {
51
- color?: CalendarColor
52
- group?: string
53
- title?: string
54
- /** Optional anchor date: the calendar view opens here instead of today. */
55
- startDate?: string
56
- events: CalendarEvent[]
57
- }
58
-
59
- export interface CalendarSource {
60
- calendar: CalendarDocument
61
- /** Absolute directory that owns the `calendar.json` file. */
62
- folder: string
63
- group: string
64
- path: string
65
- }
66
-
67
- type RawEvent = Record<string, unknown>
68
-
69
- type RawCalendar = {
70
- color?: unknown
71
- events?: RawEvent[]
72
- group?: unknown
73
- startDate?: unknown
74
- title?: unknown
75
- }
76
-
77
- const colors = new Set<CalendarColor>([
78
- "blue",
79
- "green",
80
- "red",
81
- "yellow",
82
- "purple",
83
- "orange",
84
- ])
85
-
86
- const repeats = new Set<CalendarRepeating>(["daily", "weekly", "monthly"])
87
- const statuses = new Set<CalendarStatus>([
88
- "confirmed",
89
- "tentative",
90
- "cancelled",
91
- ])
92
-
93
- function valueAt(record: RawEvent, keys: string[]) {
94
- for (const key of keys) {
95
- const value = record[key]
96
- if (value !== undefined && value !== null && value !== "") return value
97
- }
98
- return undefined
99
- }
100
-
101
- function text(record: RawEvent, keys: string[]) {
102
- const value = valueAt(record, keys)
103
- return typeof value === "string" && value.trim() ? value.trim() : undefined
104
- }
105
-
106
- function texts(value: unknown) {
107
- if (!Array.isArray(value)) return undefined
108
- const values = [
109
- ...new Set(
110
- value
111
- .filter((item): item is string => typeof item === "string")
112
- .map((item) => item.trim())
113
- .filter(Boolean),
114
- ),
115
- ]
116
- return values.length ? values : undefined
117
- }
118
-
119
- function numbers(value: unknown) {
120
- if (!Array.isArray(value)) return undefined
121
- const values = [
122
- ...new Set(
123
- value.filter(
124
- (item): item is number =>
125
- typeof item === "number" && Number.isInteger(item) && item >= 0,
126
- ),
127
- ),
128
- ].sort((left, right) => left - right)
129
- return values.length ? values : undefined
130
- }
131
-
132
- const DATE_ONLY = /^\d{4}-\d{2}-\d{2}$/
133
-
134
- function localDateOnly(value: string): Date | undefined {
135
- const trimmed = value.trim()
136
- if (!DATE_ONLY.test(trimmed)) return undefined
137
- const [year, month, day] = trimmed.split("-").map(Number)
138
- return new Date(year ?? 0, (month ?? 1) - 1, day ?? 1)
139
- }
140
-
141
- function date(value: unknown, fallback: Date) {
142
- if (value instanceof Date && !Number.isNaN(value.getTime())) return value
143
- if (typeof value === "number" && Number.isFinite(value))
144
- return new Date(value)
145
- if (typeof value === "string" && value.trim()) {
146
- const parsed = localDateOnly(value) ?? new Date(value)
147
- if (!Number.isNaN(parsed.getTime())) return parsed
148
- }
149
- return fallback
150
- }
151
-
152
- function color(value: unknown): CalendarColor | undefined {
153
- if (typeof value !== "string") return undefined
154
- const candidate = value.toLowerCase()
155
- return colors.has(candidate as CalendarColor)
156
- ? (candidate as CalendarColor)
157
- : undefined
158
- }
159
-
160
- function repeating(value: unknown): CalendarRepeating | undefined {
161
- if (typeof value !== "string") return undefined
162
- const candidate = value.toLowerCase()
163
- return repeats.has(candidate as CalendarRepeating)
164
- ? (candidate as CalendarRepeating)
165
- : undefined
166
- }
167
-
168
- function status(value: unknown): CalendarStatus | undefined {
169
- if (typeof value !== "string") return undefined
170
- const candidate = value.toLowerCase()
171
- return statuses.has(candidate as CalendarStatus)
172
- ? (candidate as CalendarStatus)
173
- : undefined
174
- }
175
-
176
- function url(value: unknown) {
177
- if (typeof value !== "string" || !value.trim()) return undefined
178
- const candidate = value.trim()
179
- return /^https?:\/\/[^\s]+$/i.test(candidate) ? candidate : undefined
180
- }
181
-
182
- function dates(value: unknown) {
183
- if (!Array.isArray(value)) return undefined
184
- const values = value
185
- .map((item) => date(item, new Date(Number.NaN)))
186
- .filter((item) => !Number.isNaN(item.getTime()))
187
- return values.length ? values : undefined
188
- }
189
-
190
- function until(value: unknown) {
191
- if (typeof value === "string" && /^\d{4}-\d{2}-\d{2}$/.test(value)) {
192
- const parsed = date(value, new Date(Number.NaN))
193
- return Number.isNaN(parsed.getTime()) ? undefined : endOfDay(parsed)
194
- }
195
- const parsed = date(value, new Date(Number.NaN))
196
- return Number.isNaN(parsed.getTime()) ? undefined : parsed
197
- }
198
-
199
- export function parseCalendar(calendar: unknown): CalendarDocument | undefined {
200
- if (Array.isArray(calendar)) return { events: parseEvents(calendar) }
201
- if (!calendar || typeof calendar !== "object") return undefined
202
-
203
- const record = calendar as RawCalendar
204
- if (!Array.isArray(record.events)) return undefined
205
-
206
- return {
207
- color: color(record.color),
208
- startDate:
209
- typeof record.startDate === "string" && record.startDate.trim()
210
- ? record.startDate.trim()
211
- : undefined,
212
- group:
213
- typeof record.group === "string" && record.group.trim()
214
- ? record.group.trim()
215
- : undefined,
216
- title:
217
- typeof record.title === "string" && record.title.trim()
218
- ? record.title.trim()
219
- : undefined,
220
- events: parseEvents(record.events),
221
- }
222
- }
223
-
224
- export function parseCalendarJson(
225
- source: string,
226
- ): CalendarDocument | undefined {
227
- try {
228
- return parseCalendar(JSON.parse(source))
229
- } catch {
230
- return undefined
231
- }
232
- }
233
-
234
- export function parseEvents(events: RawEvent[]): CalendarEvent[] {
235
- return events.map((record, index) => normalizeEvent(record, index))
236
- }
237
-
238
- export function normalizeEvent(record: RawEvent, index: number): CalendarEvent {
239
- const id = valueAt(record, ["id", "uid", "key"])?.toString() ?? String(index)
240
-
241
- const startValue = date(
242
- valueAt(record, ["start", "startDate", "start_date", "date", "begin"]),
243
- new Date(0),
244
- )
245
- const endValue = date(
246
- valueAt(record, ["end", "endDate", "end_date", "due", "finish"]),
247
- startValue,
248
- )
249
-
250
- const allDay = typeof record.allDay === "boolean" ? record.allDay : undefined
251
- const start = startValue
252
- const end =
253
- endValue > start
254
- ? endValue
255
- : new Date(start.getTime() + (allDay ? 86_400_000 : 3_600_000))
256
-
257
- const rawColor =
258
- valueAt(record, [
259
- "color",
260
- "colour",
261
- "calendarId",
262
- "calendar",
263
- "category",
264
- ]) ?? valueAt(record, ["calendars"])
265
- const eventColor =
266
- color(rawColor) ?? color(Array.isArray(rawColor) ? rawColor[0] : undefined)
267
-
268
- return {
269
- id,
270
- start,
271
- end,
272
- color: eventColor,
273
- folder: typeof record.folder === "string" ? record.folder : undefined,
274
- group: text(record, ["group"]),
275
- groupColor: color(record.groupColor),
276
- repeating: repeating(record.repeating),
277
- allDay,
278
- attendees: texts(record.attendees),
279
- excludeDates: dates(record.excludeDates),
280
- location: text(record, ["location"]),
281
- organizer: text(record, ["organizer"]),
282
- reminders: numbers(record.reminders),
283
- repeatUntil: until(record.repeatUntil),
284
- status: status(record.status),
285
- tags: texts(record.tags),
286
- url: url(record.url),
287
- description: text(record, [
288
- "description",
289
- "detail",
290
- "notes",
291
- "summary",
292
- "body",
293
- ]),
294
- title:
295
- text(record, ["title", "name", "subject", "summary", "label"]) ??
296
- "Untitled",
297
- }
298
- }
299
-
300
- const AUTO_PALETTE: CalendarColor[] = [
301
- "green",
302
- "orange",
303
- "purple",
304
- "red",
305
- "yellow",
306
- "blue",
307
- ]
308
-
309
- /**
310
- * Resolve render colors for a merged event list. Precedence per event:
311
- * explicit event color, then folder/group color, then an auto-assigned color
312
- * for the event's group. Groups without any declared color get distinct
313
- * palette colors in deterministic first-seen order, so filter chips and
314
- * events for the same group always match without any configuration.
315
- */
316
- export function resolveColors(events: CalendarEvent[]): CalendarEvent[] {
317
- const groupColor = new Map<string, CalendarColor | undefined>()
318
- const used = new Set<CalendarColor>()
319
- for (const event of events) {
320
- if (!event.group || groupColor.has(event.group)) continue
321
- const resolved = event.color ?? event.groupColor
322
- groupColor.set(event.group, resolved)
323
- if (resolved) used.add(resolved)
324
- }
325
-
326
- let next = 0
327
- for (const [group, resolved] of groupColor) {
328
- if (resolved !== undefined) continue
329
- let chosen: CalendarColor | undefined
330
- for (let attempt = 0; attempt < AUTO_PALETTE.length; attempt++) {
331
- const candidate = AUTO_PALETTE[(next + attempt) % AUTO_PALETTE.length]!
332
- if (!used.has(candidate)) {
333
- chosen = candidate
334
- next += attempt + 1
335
- break
336
- }
337
- }
338
- if (!chosen) {
339
- chosen = AUTO_PALETTE[next % AUTO_PALETTE.length]!
340
- next += 1
341
- }
342
- used.add(chosen)
343
- groupColor.set(group, chosen)
344
- }
345
-
346
- return events.map((event) => ({
347
- ...event,
348
- color:
349
- event.color ??
350
- event.groupColor ??
351
- groupColor.get(event.group ?? "") ??
352
- "blue",
353
- }))
354
- }
355
-
356
- export function mergeCalendars(sources: CalendarSource[]): CalendarDocument {
357
- const root = sources.find((source) => source.path === "calendar.json")
358
-
359
- return {
360
- title: root?.calendar.title ?? sources[0]?.calendar.title,
361
- startDate: root?.calendar.startDate ?? sources[0]?.calendar.startDate,
362
- events: sources.flatMap((source) => {
363
- const group = source.calendar.group ?? source.group
364
- return source.calendar.events.map((event) => ({
365
- ...event,
366
- folder: source.folder,
367
- group: event.group ?? group,
368
- groupColor: event.groupColor ?? source.calendar.color,
369
- id: `${source.path}:${event.id}`,
370
- }))
371
- }),
372
- }
373
- }
374
-
375
- function repeatStart(event: CalendarEvent, index: number) {
376
- if (event.repeating === "daily") return addDays(event.start, index)
377
- if (event.repeating === "weekly") return addWeeks(event.start, index)
378
- return addMonths(event.start, index)
379
- }
380
-
381
- function repeatIndex(event: CalendarEvent, date: Date) {
382
- if (event.repeating === "daily") {
383
- return Math.max(0, differenceInCalendarDays(date, event.start) - 1)
384
- }
385
- if (event.repeating === "weekly") {
386
- return Math.max(
387
- 0,
388
- Math.floor(differenceInCalendarDays(date, event.start) / 7) - 1,
389
- )
390
- }
391
- return Math.max(0, differenceInCalendarMonths(date, event.start) - 1)
392
- }
393
-
394
- export function eventsInRange(events: CalendarEvent[], start: Date, end: Date) {
395
- if (
396
- Number.isNaN(start.getTime()) ||
397
- Number.isNaN(end.getTime()) ||
398
- end <= start
399
- )
400
- return []
401
-
402
- const occurrences: CalendarEvent[] = []
403
- for (const event of events) {
404
- if (!event.repeating) {
405
- if (event.end > start && event.start < end) occurrences.push(event)
406
- continue
407
- }
408
-
409
- const duration = event.end.getTime() - event.start.getTime()
410
- const earliest = new Date(start.getTime() - duration)
411
- for (let index = repeatIndex(event, earliest); ; index += 1) {
412
- const occurrenceStart = repeatStart(event, index)
413
- if (Number.isNaN(occurrenceStart.getTime()) || occurrenceStart >= end)
414
- break
415
- if (event.repeatUntil && occurrenceStart > event.repeatUntil) break
416
- const occurrenceEnd = new Date(occurrenceStart.getTime() + duration)
417
- if (occurrenceEnd <= start) continue
418
- if (
419
- event.excludeDates?.some((excluded) =>
420
- isSameDay(excluded, occurrenceStart),
421
- )
422
- )
423
- continue
424
- occurrences.push({
425
- ...event,
426
- id: `${event.id}:${occurrenceStart.toISOString()}`,
427
- start: occurrenceStart,
428
- end: occurrenceEnd,
429
- })
430
- }
431
- }
432
- return occurrences.sort(
433
- (left, right) => left.start.getTime() - right.start.getTime(),
434
- )
435
- }
436
-
437
- export function eventCount(calendar: CalendarDocument | undefined) {
438
- return calendar?.events.length ?? 0
439
- }
440
-
441
- const EVENT_TITLE_KEYS = [
442
- "title",
443
- "name",
444
- "subject",
445
- "summary",
446
- "label",
447
- ] as const
448
- const EVENT_START_KEYS = [
449
- "start",
450
- "startDate",
451
- "start_date",
452
- "date",
453
- "begin",
454
- ] as const
455
- const EVENT_END_KEYS = ["end", "endDate", "end_date", "due", "finish"] as const
456
-
457
- function isRecord(value: unknown): value is Record<string, unknown> {
458
- return Boolean(value) && typeof value === "object" && !Array.isArray(value)
459
- }
460
-
461
- function strictDate(value: unknown): Date | undefined {
462
- if (value instanceof Date && !Number.isNaN(value.getTime())) return value
463
- if (typeof value === "number" && Number.isFinite(value))
464
- return new Date(value)
465
- if (typeof value === "string" && value.trim()) {
466
- const parsed = localDateOnly(value) ?? new Date(value)
467
- if (!Number.isNaN(parsed.getTime())) return parsed
468
- }
469
- return undefined
470
- }
471
-
472
- /**
473
- * Validate a parsed `calendar.json` value before tolerant rendering. Covers
474
- * what the tolerant parser would silently drop or default (missing events
475
- * array, events without any title, unparseable dates that would fall back to
476
- * epoch, bad enums). Returns `{ path, message }` issues; empty means valid.
477
- */
478
- export function validateCalendarFile(value: unknown): ContentIssue[] {
479
- if (Array.isArray(value)) {
480
- return value.flatMap((event, index) =>
481
- validateEvent(event, `events[${index}]`),
482
- )
483
- }
484
- if (!isRecord(value)) {
485
- return [
486
- {
487
- path: "#",
488
- message: "calendar.json must contain an object or an array of events",
489
- },
490
- ]
491
- }
492
-
493
- const issues: ContentIssue[] = []
494
- if (!Array.isArray(value.events)) {
495
- issues.push({ path: "events", message: "must be an array of events" })
496
- } else {
497
- value.events.forEach((event, index) => {
498
- issues.push(...validateEvent(event, `events[${index}]`))
499
- })
500
- }
501
- return issues
502
- }
503
-
504
- function validateEvent(event: unknown, path: string): ContentIssue[] {
505
- if (!isRecord(event)) {
506
- return [{ path, message: "must be an event object" }]
507
- }
508
- const issues: ContentIssue[] = []
509
-
510
- const hasTitle = EVENT_TITLE_KEYS.some(
511
- (key) => typeof event[key] === "string" && event[key].trim().length > 0,
512
- )
513
- if (!hasTitle) {
514
- issues.push({
515
- path,
516
- message:
517
- "has no title (expected one of title, name, subject, summary, label)",
518
- })
519
- }
520
-
521
- const startValue = EVENT_START_KEYS.find((key) => event[key] !== undefined)
522
- if (startValue !== undefined && !strictDate(event[startValue])) {
523
- issues.push({
524
- path: `${path}.${startValue}`,
525
- message: "could not be parsed as a date",
526
- })
527
- }
528
-
529
- const endValue = EVENT_END_KEYS.find((key) => event[key] !== undefined)
530
- if (
531
- endValue !== undefined &&
532
- event[endValue] !== null &&
533
- !strictDate(event[endValue])
534
- ) {
535
- issues.push({
536
- path: `${path}.${endValue}`,
537
- message: "could not be parsed as a date",
538
- })
539
- }
540
-
541
- if (event.allDay !== undefined && typeof event.allDay !== "boolean") {
542
- issues.push({ path: `${path}.allDay`, message: "must be a boolean" })
543
- }
544
- if (
545
- event.repeating !== undefined &&
546
- !["daily", "weekly", "monthly"].includes(String(event.repeating))
547
- ) {
548
- issues.push({
549
- path: `${path}.repeating`,
550
- message: "must be one of daily, weekly, monthly",
551
- })
552
- }
553
- if (
554
- event.status !== undefined &&
555
- !["confirmed", "tentative", "cancelled"].includes(String(event.status))
556
- ) {
557
- issues.push({
558
- path: `${path}.status`,
559
- message: "must be one of confirmed, tentative, cancelled",
560
- })
561
- }
562
- if (event.excludeDates !== undefined && !Array.isArray(event.excludeDates)) {
563
- issues.push({
564
- path: `${path}.excludeDates`,
565
- message: "must be an array of dates",
566
- })
567
- }
568
- return issues
569
- }
570
-
571
- /**
572
- * Parse a `calendar.json` source string and validate it. Never throws:
573
- * invalid JSON and structural mistakes come back as issues with paths.
574
- */
575
- export function parseCalendarSource(source: string) {
576
- return parseJsonSource<unknown>(source, validateCalendarFile)
577
- }
578
-
579
- /**
580
- * Compact validation summary for nav badges and error headers.
581
- */
582
- export function calendarIssueSummary(issues: ContentIssue[]): string {
583
- return issueSummary(issues, "calendar.json")
584
- }
585
-
586
- /**
587
- * Starter `calendar.json` for `kennel init`: one sample event in the shape
588
- * the tolerant parser reads directly.
589
- */
590
- export function starterCalendarSource(): string {
591
- return `${JSON.stringify(
592
- {
593
- color: "blue",
594
- events: [
595
- {
596
- id: "1",
597
- title: "First event",
598
- description: "Describe what happens.",
599
- start: "2030-01-01T09:00:00",
600
- end: "2030-01-01T10:00:00",
601
- allDay: false,
602
- status: "confirmed",
603
- attendees: [],
604
- },
605
- ],
606
- },
607
- null,
608
- 2,
609
- )}\n`
610
- }