@dxos/plugin-magazine 0.0.0

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 (390) hide show
  1. package/LICENSE +105 -0
  2. package/PLUGIN.mdl +653 -0
  3. package/dist/lib/neutral/FeedArticle-PEWCFFSP.mjs +90 -0
  4. package/dist/lib/neutral/FeedArticle-PEWCFFSP.mjs.map +7 -0
  5. package/dist/lib/neutral/FeedProperties-EIO7IZOA.mjs +97 -0
  6. package/dist/lib/neutral/FeedProperties-EIO7IZOA.mjs.map +7 -0
  7. package/dist/lib/neutral/MagazineArticle-DEUTR23F.mjs +325 -0
  8. package/dist/lib/neutral/MagazineArticle-DEUTR23F.mjs.map +7 -0
  9. package/dist/lib/neutral/MagazinePlugin.mjs +62 -0
  10. package/dist/lib/neutral/MagazinePlugin.mjs.map +7 -0
  11. package/dist/lib/neutral/MagazinePlugin.workerd.mjs +12 -0
  12. package/dist/lib/neutral/MagazinePlugin.workerd.mjs.map +7 -0
  13. package/dist/lib/neutral/PostArticle-RERHXPXC.mjs +230 -0
  14. package/dist/lib/neutral/PostArticle-RERHXPXC.mjs.map +7 -0
  15. package/dist/lib/neutral/PostCard-VDGN7BBA.mjs +60 -0
  16. package/dist/lib/neutral/PostCard-VDGN7BBA.mjs.map +7 -0
  17. package/dist/lib/neutral/app-graph-builder-JDPRBVHL.mjs +121 -0
  18. package/dist/lib/neutral/app-graph-builder-JDPRBVHL.mjs.map +7 -0
  19. package/dist/lib/neutral/capabilities/index.mjs +21 -0
  20. package/dist/lib/neutral/capabilities/index.mjs.map +7 -0
  21. package/dist/lib/neutral/chunk-3K4AQKBZ.mjs +12 -0
  22. package/dist/lib/neutral/chunk-3K4AQKBZ.mjs.map +7 -0
  23. package/dist/lib/neutral/chunk-AFPBWBTK.mjs +591 -0
  24. package/dist/lib/neutral/chunk-AFPBWBTK.mjs.map +7 -0
  25. package/dist/lib/neutral/chunk-APQRTYT3.mjs +57 -0
  26. package/dist/lib/neutral/chunk-APQRTYT3.mjs.map +7 -0
  27. package/dist/lib/neutral/chunk-GU725ZF4.mjs +11 -0
  28. package/dist/lib/neutral/chunk-GU725ZF4.mjs.map +7 -0
  29. package/dist/lib/neutral/chunk-J5LGTIGS.mjs +10 -0
  30. package/dist/lib/neutral/chunk-J5LGTIGS.mjs.map +7 -0
  31. package/dist/lib/neutral/chunk-JFPR6OV4.mjs +60 -0
  32. package/dist/lib/neutral/chunk-JFPR6OV4.mjs.map +7 -0
  33. package/dist/lib/neutral/chunk-KTYLUOUG.mjs +36 -0
  34. package/dist/lib/neutral/chunk-KTYLUOUG.mjs.map +7 -0
  35. package/dist/lib/neutral/chunk-MBPX7RPI.mjs +14 -0
  36. package/dist/lib/neutral/chunk-MBPX7RPI.mjs.map +7 -0
  37. package/dist/lib/neutral/chunk-MJ2AY2CK.mjs +8 -0
  38. package/dist/lib/neutral/chunk-MJ2AY2CK.mjs.map +7 -0
  39. package/dist/lib/neutral/chunk-OYCDRSWB.mjs +624 -0
  40. package/dist/lib/neutral/chunk-OYCDRSWB.mjs.map +7 -0
  41. package/dist/lib/neutral/chunk-SAWKLGJU.mjs +28 -0
  42. package/dist/lib/neutral/chunk-SAWKLGJU.mjs.map +7 -0
  43. package/dist/lib/neutral/chunk-XCNCK47N.mjs +15 -0
  44. package/dist/lib/neutral/chunk-XCNCK47N.mjs.map +7 -0
  45. package/dist/lib/neutral/clear-magazine-KNY4OWY4.mjs +59 -0
  46. package/dist/lib/neutral/clear-magazine-KNY4OWY4.mjs.map +7 -0
  47. package/dist/lib/neutral/components/index.mjs +338 -0
  48. package/dist/lib/neutral/components/index.mjs.map +7 -0
  49. package/dist/lib/neutral/containers/index.mjs +17 -0
  50. package/dist/lib/neutral/containers/index.mjs.map +7 -0
  51. package/dist/lib/neutral/create-object-SYZMRR4H.mjs +98 -0
  52. package/dist/lib/neutral/create-object-SYZMRR4H.mjs.map +7 -0
  53. package/dist/lib/neutral/curate-magazine-MA6OLK2X.mjs +184 -0
  54. package/dist/lib/neutral/curate-magazine-MA6OLK2X.mjs.map +7 -0
  55. package/dist/lib/neutral/fetch-article-content-CZO6D3QD.mjs +31 -0
  56. package/dist/lib/neutral/fetch-article-content-CZO6D3QD.mjs.map +7 -0
  57. package/dist/lib/neutral/index.mjs +18 -0
  58. package/dist/lib/neutral/index.mjs.map +7 -0
  59. package/dist/lib/neutral/load-post-content-XGL6VFF6.mjs +65 -0
  60. package/dist/lib/neutral/load-post-content-XGL6VFF6.mjs.map +7 -0
  61. package/dist/lib/neutral/meta.json +1 -0
  62. package/dist/lib/neutral/meta.mjs +8 -0
  63. package/dist/lib/neutral/meta.mjs.map +7 -0
  64. package/dist/lib/neutral/navigation-resolver-IN4OMCDR.mjs +18 -0
  65. package/dist/lib/neutral/navigation-resolver-IN4OMCDR.mjs.map +7 -0
  66. package/dist/lib/neutral/operation-handler-JE3743BU.mjs +8 -0
  67. package/dist/lib/neutral/operation-handler-JE3743BU.mjs.map +7 -0
  68. package/dist/lib/neutral/operations/index.mjs +8 -0
  69. package/dist/lib/neutral/operations/index.mjs.map +7 -0
  70. package/dist/lib/neutral/plugin.mjs +16 -0
  71. package/dist/lib/neutral/plugin.mjs.map +7 -0
  72. package/dist/lib/neutral/plugin.workerd.mjs +16 -0
  73. package/dist/lib/neutral/plugin.workerd.mjs.map +7 -0
  74. package/dist/lib/neutral/react-surface-TZESAMG2.mjs +60 -0
  75. package/dist/lib/neutral/react-surface-TZESAMG2.mjs.map +7 -0
  76. package/dist/lib/neutral/routine-templates-A2XTJOZO.mjs +52 -0
  77. package/dist/lib/neutral/routine-templates-A2XTJOZO.mjs.map +7 -0
  78. package/dist/lib/neutral/skill-definition-3KLMUEC5.mjs +8 -0
  79. package/dist/lib/neutral/skill-definition-3KLMUEC5.mjs.map +7 -0
  80. package/dist/lib/neutral/skills/index.mjs +46 -0
  81. package/dist/lib/neutral/skills/index.mjs.map +7 -0
  82. package/dist/lib/neutral/sync-feed-UZX2XHSQ.mjs +99 -0
  83. package/dist/lib/neutral/sync-feed-UZX2XHSQ.mjs.map +7 -0
  84. package/dist/lib/neutral/translations.mjs +81 -0
  85. package/dist/lib/neutral/translations.mjs.map +7 -0
  86. package/dist/lib/neutral/types/index.mjs +14 -0
  87. package/dist/lib/neutral/types/index.mjs.map +7 -0
  88. package/dist/types/dx.config.d.ts +28 -0
  89. package/dist/types/dx.config.d.ts.map +1 -0
  90. package/dist/types/src/MagazinePlugin.d.ts +4 -0
  91. package/dist/types/src/MagazinePlugin.d.ts.map +1 -0
  92. package/dist/types/src/MagazinePlugin.test.d.ts +2 -0
  93. package/dist/types/src/MagazinePlugin.test.d.ts.map +1 -0
  94. package/dist/types/src/MagazinePlugin.workerd.d.ts +4 -0
  95. package/dist/types/src/MagazinePlugin.workerd.d.ts.map +1 -0
  96. package/dist/types/src/atoms/atoms.test.d.ts +2 -0
  97. package/dist/types/src/atoms/atoms.test.d.ts.map +1 -0
  98. package/dist/types/src/atoms/index.d.ts +7 -0
  99. package/dist/types/src/atoms/index.d.ts.map +1 -0
  100. package/dist/types/src/atoms/magazine-posts.d.ts +16 -0
  101. package/dist/types/src/atoms/magazine-posts.d.ts.map +1 -0
  102. package/dist/types/src/atoms/post-content.d.ts +7 -0
  103. package/dist/types/src/atoms/post-content.d.ts.map +1 -0
  104. package/dist/types/src/atoms/post-curation.d.ts +16 -0
  105. package/dist/types/src/atoms/post-curation.d.ts.map +1 -0
  106. package/dist/types/src/atoms/post-display.d.ts +22 -0
  107. package/dist/types/src/atoms/post-display.d.ts.map +1 -0
  108. package/dist/types/src/atoms/post-read.d.ts +14 -0
  109. package/dist/types/src/atoms/post-read.d.ts.map +1 -0
  110. package/dist/types/src/atoms/post-tags.d.ts +16 -0
  111. package/dist/types/src/atoms/post-tags.d.ts.map +1 -0
  112. package/dist/types/src/capabilities/app-graph-builder.d.ts +5 -0
  113. package/dist/types/src/capabilities/app-graph-builder.d.ts.map +1 -0
  114. package/dist/types/src/capabilities/create-object.d.ts +5 -0
  115. package/dist/types/src/capabilities/create-object.d.ts.map +1 -0
  116. package/dist/types/src/capabilities/index.d.ts +10 -0
  117. package/dist/types/src/capabilities/index.d.ts.map +1 -0
  118. package/dist/types/src/capabilities/navigation-resolver.d.ts +5 -0
  119. package/dist/types/src/capabilities/navigation-resolver.d.ts.map +1 -0
  120. package/dist/types/src/capabilities/operation-handler.d.ts +6 -0
  121. package/dist/types/src/capabilities/operation-handler.d.ts.map +1 -0
  122. package/dist/types/src/capabilities/react-surface.d.ts +5 -0
  123. package/dist/types/src/capabilities/react-surface.d.ts.map +1 -0
  124. package/dist/types/src/capabilities/routine-templates.d.ts +5 -0
  125. package/dist/types/src/capabilities/routine-templates.d.ts.map +1 -0
  126. package/dist/types/src/capabilities/skill-definition.d.ts +5 -0
  127. package/dist/types/src/capabilities/skill-definition.d.ts.map +1 -0
  128. package/dist/types/src/components/PostContent/PostContent.d.ts +30 -0
  129. package/dist/types/src/components/PostContent/PostContent.d.ts.map +1 -0
  130. package/dist/types/src/components/PostContent/PostContent.test.d.ts +2 -0
  131. package/dist/types/src/components/PostContent/PostContent.test.d.ts.map +1 -0
  132. package/dist/types/src/components/PostContent/index.d.ts +2 -0
  133. package/dist/types/src/components/PostContent/index.d.ts.map +1 -0
  134. package/dist/types/src/components/PostStack/PostStack.d.ts +17 -0
  135. package/dist/types/src/components/PostStack/PostStack.d.ts.map +1 -0
  136. package/dist/types/src/components/PostStack/PostStack.stories.d.ts +10 -0
  137. package/dist/types/src/components/PostStack/PostStack.stories.d.ts.map +1 -0
  138. package/dist/types/src/components/PostStack/index.d.ts +2 -0
  139. package/dist/types/src/components/PostStack/index.d.ts.map +1 -0
  140. package/dist/types/src/components/SubscriptionStack/SubscriptionStack.d.ts +23 -0
  141. package/dist/types/src/components/SubscriptionStack/SubscriptionStack.d.ts.map +1 -0
  142. package/dist/types/src/components/SubscriptionStack/SubscriptionStack.stories.d.ts +8 -0
  143. package/dist/types/src/components/SubscriptionStack/SubscriptionStack.stories.d.ts.map +1 -0
  144. package/dist/types/src/components/SubscriptionStack/index.d.ts +2 -0
  145. package/dist/types/src/components/SubscriptionStack/index.d.ts.map +1 -0
  146. package/dist/types/src/components/index.d.ts +4 -0
  147. package/dist/types/src/components/index.d.ts.map +1 -0
  148. package/dist/types/src/containers/FeedArticle/FeedArticle.d.ts +6 -0
  149. package/dist/types/src/containers/FeedArticle/FeedArticle.d.ts.map +1 -0
  150. package/dist/types/src/containers/FeedArticle/FeedArticle.stories.d.ts +12 -0
  151. package/dist/types/src/containers/FeedArticle/FeedArticle.stories.d.ts.map +1 -0
  152. package/dist/types/src/containers/FeedArticle/FeedToolbar.d.ts +10 -0
  153. package/dist/types/src/containers/FeedArticle/FeedToolbar.d.ts.map +1 -0
  154. package/dist/types/src/containers/FeedArticle/index.d.ts +2 -0
  155. package/dist/types/src/containers/FeedArticle/index.d.ts.map +1 -0
  156. package/dist/types/src/containers/FeedProperties/FeedProperties.d.ts +6 -0
  157. package/dist/types/src/containers/FeedProperties/FeedProperties.d.ts.map +1 -0
  158. package/dist/types/src/containers/FeedProperties/index.d.ts +2 -0
  159. package/dist/types/src/containers/FeedProperties/index.d.ts.map +1 -0
  160. package/dist/types/src/containers/MagazineArticle/MagazineArticle.d.ts +6 -0
  161. package/dist/types/src/containers/MagazineArticle/MagazineArticle.d.ts.map +1 -0
  162. package/dist/types/src/containers/MagazineArticle/MagazineArticle.stories.d.ts +9 -0
  163. package/dist/types/src/containers/MagazineArticle/MagazineArticle.stories.d.ts.map +1 -0
  164. package/dist/types/src/containers/MagazineArticle/MagazineTile.d.ts +11 -0
  165. package/dist/types/src/containers/MagazineArticle/MagazineTile.d.ts.map +1 -0
  166. package/dist/types/src/containers/MagazineArticle/index.d.ts +2 -0
  167. package/dist/types/src/containers/MagazineArticle/index.d.ts.map +1 -0
  168. package/dist/types/src/containers/MagazineArticle/useToolbar.d.ts +18 -0
  169. package/dist/types/src/containers/MagazineArticle/useToolbar.d.ts.map +1 -0
  170. package/dist/types/src/containers/PostArticle/PostArticle.d.ts +6 -0
  171. package/dist/types/src/containers/PostArticle/PostArticle.d.ts.map +1 -0
  172. package/dist/types/src/containers/PostArticle/PostArticle.stories.d.ts +8 -0
  173. package/dist/types/src/containers/PostArticle/PostArticle.stories.d.ts.map +1 -0
  174. package/dist/types/src/containers/PostArticle/PostToolbar.d.ts +19 -0
  175. package/dist/types/src/containers/PostArticle/PostToolbar.d.ts.map +1 -0
  176. package/dist/types/src/containers/PostArticle/index.d.ts +2 -0
  177. package/dist/types/src/containers/PostArticle/index.d.ts.map +1 -0
  178. package/dist/types/src/containers/PostCard/PostCard.d.ts +11 -0
  179. package/dist/types/src/containers/PostCard/PostCard.d.ts.map +1 -0
  180. package/dist/types/src/containers/PostCard/index.d.ts +2 -0
  181. package/dist/types/src/containers/PostCard/index.d.ts.map +1 -0
  182. package/dist/types/src/containers/SubscriptionsArticle/SubscriptionsArticle.d.ts +5 -0
  183. package/dist/types/src/containers/SubscriptionsArticle/SubscriptionsArticle.d.ts.map +1 -0
  184. package/dist/types/src/containers/SubscriptionsArticle/SubscriptionsArticle.stories.d.ts +8 -0
  185. package/dist/types/src/containers/SubscriptionsArticle/SubscriptionsArticle.stories.d.ts.map +1 -0
  186. package/dist/types/src/containers/SubscriptionsArticle/index.d.ts +2 -0
  187. package/dist/types/src/containers/SubscriptionsArticle/index.d.ts.map +1 -0
  188. package/dist/types/src/containers/index.d.ts +7 -0
  189. package/dist/types/src/containers/index.d.ts.map +1 -0
  190. package/dist/types/src/index.d.ts +3 -0
  191. package/dist/types/src/index.d.ts.map +1 -0
  192. package/dist/types/src/meta.d.ts +33 -0
  193. package/dist/types/src/meta.d.ts.map +1 -0
  194. package/dist/types/src/operations/clear-magazine.d.ts +10 -0
  195. package/dist/types/src/operations/clear-magazine.d.ts.map +1 -0
  196. package/dist/types/src/operations/curate-magazine.d.ts +25 -0
  197. package/dist/types/src/operations/curate-magazine.d.ts.map +1 -0
  198. package/dist/types/src/operations/curate-magazine.skill.test.d.ts +2 -0
  199. package/dist/types/src/operations/curate-magazine.skill.test.d.ts.map +1 -0
  200. package/dist/types/src/operations/curate-magazine.test.d.ts +2 -0
  201. package/dist/types/src/operations/curate-magazine.test.d.ts.map +1 -0
  202. package/dist/types/src/operations/extraction/article.d.ts +37 -0
  203. package/dist/types/src/operations/extraction/article.d.ts.map +1 -0
  204. package/dist/types/src/operations/extraction/article.test.d.ts +2 -0
  205. package/dist/types/src/operations/extraction/article.test.d.ts.map +1 -0
  206. package/dist/types/src/operations/extraction/index.d.ts +2 -0
  207. package/dist/types/src/operations/extraction/index.d.ts.map +1 -0
  208. package/dist/types/src/operations/fetch-article-content.d.ts +5 -0
  209. package/dist/types/src/operations/fetch-article-content.d.ts.map +1 -0
  210. package/dist/types/src/operations/index.d.ts +3 -0
  211. package/dist/types/src/operations/index.d.ts.map +1 -0
  212. package/dist/types/src/operations/load-post-content.d.ts +8 -0
  213. package/dist/types/src/operations/load-post-content.d.ts.map +1 -0
  214. package/dist/types/src/operations/sources/article.d.ts +24 -0
  215. package/dist/types/src/operations/sources/article.d.ts.map +1 -0
  216. package/dist/types/src/operations/sources/cors.d.ts +12 -0
  217. package/dist/types/src/operations/sources/cors.d.ts.map +1 -0
  218. package/dist/types/src/operations/sources/feed-fetcher.d.ts +22 -0
  219. package/dist/types/src/operations/sources/feed-fetcher.d.ts.map +1 -0
  220. package/dist/types/src/operations/sources/feed-fetcher.test.d.ts +2 -0
  221. package/dist/types/src/operations/sources/feed-fetcher.test.d.ts.map +1 -0
  222. package/dist/types/src/operations/sources/http.d.ts +9 -0
  223. package/dist/types/src/operations/sources/http.d.ts.map +1 -0
  224. package/dist/types/src/operations/sources/index.d.ts +6 -0
  225. package/dist/types/src/operations/sources/index.d.ts.map +1 -0
  226. package/dist/types/src/operations/sources/rss.d.ts +4 -0
  227. package/dist/types/src/operations/sources/rss.d.ts.map +1 -0
  228. package/dist/types/src/operations/sources/rss.test.d.ts +2 -0
  229. package/dist/types/src/operations/sources/rss.test.d.ts.map +1 -0
  230. package/dist/types/src/operations/sources/standard-site.d.ts +33 -0
  231. package/dist/types/src/operations/sources/standard-site.d.ts.map +1 -0
  232. package/dist/types/src/operations/sync-feed.d.ts +5 -0
  233. package/dist/types/src/operations/sync-feed.d.ts.map +1 -0
  234. package/dist/types/src/operations/sync-feed.test.d.ts +2 -0
  235. package/dist/types/src/operations/sync-feed.test.d.ts.map +1 -0
  236. package/dist/types/src/operations/util.d.ts +25 -0
  237. package/dist/types/src/operations/util.d.ts.map +1 -0
  238. package/dist/types/src/operations/util.test.d.ts +2 -0
  239. package/dist/types/src/operations/util.test.d.ts.map +1 -0
  240. package/dist/types/src/paths.d.ts +3 -0
  241. package/dist/types/src/paths.d.ts.map +1 -0
  242. package/dist/types/src/plugin.d.ts +4 -0
  243. package/dist/types/src/plugin.d.ts.map +1 -0
  244. package/dist/types/src/plugin.workerd.d.ts +3 -0
  245. package/dist/types/src/plugin.workerd.d.ts.map +1 -0
  246. package/dist/types/src/skills/index.d.ts +2 -0
  247. package/dist/types/src/skills/index.d.ts.map +1 -0
  248. package/dist/types/src/skills/magazine-skill.d.ts +4 -0
  249. package/dist/types/src/skills/magazine-skill.d.ts.map +1 -0
  250. package/dist/types/src/stories/ArticleExtractor.stories.d.ts +267 -0
  251. package/dist/types/src/stories/ArticleExtractor.stories.d.ts.map +1 -0
  252. package/dist/types/src/stories/MagazineCurate.stories.d.ts +32 -0
  253. package/dist/types/src/stories/MagazineCurate.stories.d.ts.map +1 -0
  254. package/dist/types/src/stories/theregister-fixture.test.d.ts +2 -0
  255. package/dist/types/src/stories/theregister-fixture.test.d.ts.map +1 -0
  256. package/dist/types/src/templates/magazine-curation.d.ts +7 -0
  257. package/dist/types/src/templates/magazine-curation.d.ts.map +1 -0
  258. package/dist/types/src/testing/builder.d.ts +28 -0
  259. package/dist/types/src/testing/builder.d.ts.map +1 -0
  260. package/dist/types/src/testing/index.d.ts +2 -0
  261. package/dist/types/src/testing/index.d.ts.map +1 -0
  262. package/dist/types/src/translations.d.ts +247 -0
  263. package/dist/types/src/translations.d.ts.map +1 -0
  264. package/dist/types/src/types/CreateSubscription.d.ts +29 -0
  265. package/dist/types/src/types/CreateSubscription.d.ts.map +1 -0
  266. package/dist/types/src/types/FeedOperation.d.ts +52 -0
  267. package/dist/types/src/types/FeedOperation.d.ts.map +1 -0
  268. package/dist/types/src/types/Magazine.d.ts +87 -0
  269. package/dist/types/src/types/Magazine.d.ts.map +1 -0
  270. package/dist/types/src/types/Magazine.test.d.ts +2 -0
  271. package/dist/types/src/types/Magazine.test.d.ts.map +1 -0
  272. package/dist/types/src/types/Subscription.d.ts +144 -0
  273. package/dist/types/src/types/Subscription.d.ts.map +1 -0
  274. package/dist/types/src/types/Subscription.test.d.ts +2 -0
  275. package/dist/types/src/types/Subscription.test.d.ts.map +1 -0
  276. package/dist/types/src/types/index.d.ts +5 -0
  277. package/dist/types/src/types/index.d.ts.map +1 -0
  278. package/dist/types/src/util/date.d.ts +8 -0
  279. package/dist/types/src/util/date.d.ts.map +1 -0
  280. package/dist/types/src/util/index.d.ts +4 -0
  281. package/dist/types/src/util/index.d.ts.map +1 -0
  282. package/dist/types/src/util/post-content.d.ts +14 -0
  283. package/dist/types/src/util/post-content.d.ts.map +1 -0
  284. package/dist/types/src/util/text.d.ts +18 -0
  285. package/dist/types/src/util/text.d.ts.map +1 -0
  286. package/dist/types/tsconfig.tsbuildinfo +1 -0
  287. package/dx.config.ts +48 -0
  288. package/package.json +164 -0
  289. package/src/MagazinePlugin.test.ts +36 -0
  290. package/src/MagazinePlugin.tsx +61 -0
  291. package/src/MagazinePlugin.workerd.ts +31 -0
  292. package/src/atoms/atoms.test.ts +323 -0
  293. package/src/atoms/index.ts +10 -0
  294. package/src/atoms/magazine-posts.ts +70 -0
  295. package/src/atoms/post-content.ts +34 -0
  296. package/src/atoms/post-curation.ts +36 -0
  297. package/src/atoms/post-display.ts +59 -0
  298. package/src/atoms/post-read.ts +40 -0
  299. package/src/atoms/post-tags.ts +66 -0
  300. package/src/capabilities/app-graph-builder.ts +125 -0
  301. package/src/capabilities/create-object.ts +127 -0
  302. package/src/capabilities/index.ts +17 -0
  303. package/src/capabilities/navigation-resolver.ts +21 -0
  304. package/src/capabilities/operation-handler.ts +16 -0
  305. package/src/capabilities/react-surface.tsx +53 -0
  306. package/src/capabilities/routine-templates.ts +16 -0
  307. package/src/capabilities/skill-definition.ts +14 -0
  308. package/src/components/PostContent/PostContent.test.ts +81 -0
  309. package/src/components/PostContent/PostContent.tsx +111 -0
  310. package/src/components/PostContent/index.ts +5 -0
  311. package/src/components/PostStack/PostStack.stories.tsx +39 -0
  312. package/src/components/PostStack/PostStack.tsx +151 -0
  313. package/src/components/PostStack/index.ts +5 -0
  314. package/src/components/SubscriptionStack/SubscriptionStack.stories.tsx +52 -0
  315. package/src/components/SubscriptionStack/SubscriptionStack.tsx +161 -0
  316. package/src/components/SubscriptionStack/index.ts +5 -0
  317. package/src/components/index.ts +7 -0
  318. package/src/containers/FeedArticle/FeedArticle.stories.tsx +102 -0
  319. package/src/containers/FeedArticle/FeedArticle.tsx +68 -0
  320. package/src/containers/FeedArticle/FeedToolbar.tsx +47 -0
  321. package/src/containers/FeedArticle/index.ts +5 -0
  322. package/src/containers/FeedProperties/FeedProperties.tsx +107 -0
  323. package/src/containers/FeedProperties/index.ts +5 -0
  324. package/src/containers/MagazineArticle/MagazineArticle.stories.tsx +244 -0
  325. package/src/containers/MagazineArticle/MagazineArticle.tsx +146 -0
  326. package/src/containers/MagazineArticle/MagazineTile.tsx +88 -0
  327. package/src/containers/MagazineArticle/index.ts +5 -0
  328. package/src/containers/MagazineArticle/useToolbar.tsx +145 -0
  329. package/src/containers/PostArticle/PostArticle.stories.tsx +117 -0
  330. package/src/containers/PostArticle/PostArticle.tsx +140 -0
  331. package/src/containers/PostArticle/PostToolbar.tsx +120 -0
  332. package/src/containers/PostArticle/index.ts +5 -0
  333. package/src/containers/PostCard/PostCard.tsx +70 -0
  334. package/src/containers/PostCard/index.ts +5 -0
  335. package/src/containers/SubscriptionsArticle/SubscriptionsArticle.stories.tsx +80 -0
  336. package/src/containers/SubscriptionsArticle/SubscriptionsArticle.tsx +99 -0
  337. package/src/containers/SubscriptionsArticle/index.ts +5 -0
  338. package/src/containers/index.ts +11 -0
  339. package/src/index.ts +6 -0
  340. package/src/meta.ts +9 -0
  341. package/src/operations/clear-magazine.ts +83 -0
  342. package/src/operations/curate-magazine.skill.conversations.json +1 -0
  343. package/src/operations/curate-magazine.skill.test.ts +167 -0
  344. package/src/operations/curate-magazine.test.ts +91 -0
  345. package/src/operations/curate-magazine.ts +178 -0
  346. package/src/operations/extraction/article.test.ts +154 -0
  347. package/src/operations/extraction/article.ts +270 -0
  348. package/src/operations/extraction/index.ts +5 -0
  349. package/src/operations/fetch-article-content.ts +27 -0
  350. package/src/operations/index.ts +13 -0
  351. package/src/operations/load-post-content.ts +76 -0
  352. package/src/operations/sources/article.ts +122 -0
  353. package/src/operations/sources/cors.ts +19 -0
  354. package/src/operations/sources/feed-fetcher.test.ts +200 -0
  355. package/src/operations/sources/feed-fetcher.ts +26 -0
  356. package/src/operations/sources/http.ts +42 -0
  357. package/src/operations/sources/index.ts +9 -0
  358. package/src/operations/sources/rss.test.ts +201 -0
  359. package/src/operations/sources/rss.ts +138 -0
  360. package/src/operations/sources/standard-site.ts +381 -0
  361. package/src/operations/sources/testing/feed.xml +477 -0
  362. package/src/operations/sync-feed.test.ts +146 -0
  363. package/src/operations/sync-feed.ts +140 -0
  364. package/src/operations/util.test.ts +67 -0
  365. package/src/operations/util.ts +65 -0
  366. package/src/paths.ts +14 -0
  367. package/src/plugin.ts +11 -0
  368. package/src/plugin.workerd.ts +6 -0
  369. package/src/skills/index.ts +5 -0
  370. package/src/skills/magazine-skill.ts +45 -0
  371. package/src/stories/ArticleExtractor.stories.tsx +204 -0
  372. package/src/stories/MagazineCurate.stories.tsx +217 -0
  373. package/src/stories/fixtures/theregister-ai.xml +45 -0
  374. package/src/stories/theregister-fixture.test.ts +35 -0
  375. package/src/templates/magazine-curation.ts +47 -0
  376. package/src/testing/builder.ts +77 -0
  377. package/src/testing/index.ts +5 -0
  378. package/src/translations.ts +81 -0
  379. package/src/types/CreateSubscription.ts +61 -0
  380. package/src/types/FeedOperation.ts +134 -0
  381. package/src/types/Magazine.test.ts +69 -0
  382. package/src/types/Magazine.ts +214 -0
  383. package/src/types/Subscription.test.ts +171 -0
  384. package/src/types/Subscription.ts +366 -0
  385. package/src/types/index.ts +8 -0
  386. package/src/util/date.ts +27 -0
  387. package/src/util/index.ts +7 -0
  388. package/src/util/post-content.ts +19 -0
  389. package/src/util/text.ts +98 -0
  390. package/src/vite-env.d.ts +10 -0
@@ -0,0 +1,146 @@
1
+ //
2
+ // Copyright 2026 DXOS.org
3
+ //
4
+
5
+ import { describe, it } from '@effect/vitest';
6
+ import * as Effect from 'effect/Effect';
7
+ import { afterEach, beforeEach, vi } from 'vitest';
8
+
9
+ import { Operation } from '@dxos/compute';
10
+ import { Database, Feed, Filter, Ref, Tag } from '@dxos/echo';
11
+ import { TestHelpers } from '@dxos/effect/testing';
12
+ import { AssistantTestLayer } from '@dxos/functions-runtime/testing';
13
+ import { EntityId } from '@dxos/keys';
14
+
15
+ import { FeedOperation, Subscription } from '../types';
16
+ import { MagazineOperationHandlerSet } from './index';
17
+
18
+ EntityId.dangerouslyDisableRandomness();
19
+
20
+ const TestLayer = AssistantTestLayer({
21
+ operationHandlers: MagazineOperationHandlerSet,
22
+ types: [Feed.Feed, Subscription.Subscription, Subscription.Post, Tag.Tag],
23
+ disableLlmMemoization: true,
24
+ });
25
+
26
+ /**
27
+ * Canned RSS XML returned by the stubbed global `fetch`. Two items with stable
28
+ * guids — mirrors what an upstream RSS feed serves on repeated polls when no
29
+ * new posts have been published.
30
+ */
31
+ const RSS_XML = `<?xml version="1.0" encoding="UTF-8"?>
32
+ <rss version="2.0">
33
+ <channel>
34
+ <title>Test Feed</title>
35
+ <description>A test feed</description>
36
+ <item>
37
+ <title>Post A</title>
38
+ <link>https://example.com/a</link>
39
+ <guid>post-a</guid>
40
+ <description>A body</description>
41
+ <pubDate>Wed, 01 Apr 2026 00:00:00 GMT</pubDate>
42
+ </item>
43
+ <item>
44
+ <title>Post B</title>
45
+ <link>https://example.com/b</link>
46
+ <guid>post-b</guid>
47
+ <description>B body</description>
48
+ <pubDate>Thu, 02 Apr 2026 00:00:00 GMT</pubDate>
49
+ </item>
50
+ </channel>
51
+ </rss>`;
52
+
53
+ /**
54
+ * End-to-end tests for the {@link FeedOperation.SyncFeed} operation against
55
+ * the full operation harness (Database + Feed services + handler registry).
56
+ *
57
+ * Stubs only the network boundary (`globalThis.fetch`) so the test exercises
58
+ * the real RSS parser, real ECHO writes, and the real operation runtime.
59
+ *
60
+ * The original bug: each sync re-appended every previously-seen post because
61
+ * the cursor filter `post.guid !== cursor` only excluded the single post whose
62
+ * guid matched the cursor — every older post passed through and was appended
63
+ * again. These tests pin the corrected, guid-deduped behavior.
64
+ */
65
+ describe('SyncFeed', () => {
66
+ beforeEach(() => {
67
+ vi.stubGlobal(
68
+ 'fetch',
69
+ vi.fn(async () => new Response(RSS_XML, { headers: { 'content-type': 'application/xml' } })),
70
+ );
71
+ });
72
+
73
+ afterEach(() => {
74
+ vi.unstubAllGlobals();
75
+ });
76
+
77
+ it.effect(
78
+ 're-syncing the same upstream response does not duplicate posts',
79
+ Effect.fnUntraced(
80
+ function* ({ expect }) {
81
+ const subscriptionFeed = yield* Database.add(
82
+ Subscription.makeSubscription({ name: 'feed', url: 'https://example.com/rss' }),
83
+ );
84
+ yield* Database.flush();
85
+ const echoFeed = yield* Database.load(subscriptionFeed.feed);
86
+
87
+ yield* Operation.invoke(FeedOperation.SyncFeed, { feed: Ref.make(subscriptionFeed) });
88
+ yield* Operation.invoke(FeedOperation.SyncFeed, { feed: Ref.make(subscriptionFeed) });
89
+
90
+ const items = yield* Feed.query(echoFeed, Filter.type(Subscription.Post)).run;
91
+ expect(items).toHaveLength(2);
92
+ const guids = items.map((post) => post.guid).sort();
93
+ expect(guids).toEqual(['post-a', 'post-b']);
94
+ },
95
+ Effect.provide(TestLayer),
96
+ TestHelpers.provideTestContext,
97
+ ),
98
+ );
99
+
100
+ it.effect(
101
+ 'within-batch and link-fallback dedup: same link served twice in one response is appended once',
102
+ Effect.fnUntraced(
103
+ function* ({ expect }) {
104
+ // RSS items with NO `<guid>` element — the dedup key falls back to
105
+ // `link`. Two items share the same link, simulating a misbehaving
106
+ // upstream that serves the same post twice in one response.
107
+ const xml = `<?xml version="1.0" encoding="UTF-8"?>
108
+ <rss version="2.0">
109
+ <channel>
110
+ <title>Duplicate Feed</title>
111
+ <item>
112
+ <title>First copy</title>
113
+ <link>https://example.com/shared</link>
114
+ <description>body</description>
115
+ </item>
116
+ <item>
117
+ <title>Second copy</title>
118
+ <link>https://example.com/shared</link>
119
+ <description>body</description>
120
+ </item>
121
+ </channel>
122
+ </rss>`;
123
+ vi.stubGlobal(
124
+ 'fetch',
125
+ vi.fn(async () => new Response(xml, { headers: { 'content-type': 'application/xml' } })),
126
+ );
127
+
128
+ const subscriptionFeed = yield* Database.add(
129
+ Subscription.makeSubscription({ name: 'feed', url: 'https://example.com/rss' }),
130
+ );
131
+ yield* Database.flush();
132
+ const echoFeed = yield* Database.load(subscriptionFeed.feed);
133
+
134
+ yield* Operation.invoke(FeedOperation.SyncFeed, { feed: Ref.make(subscriptionFeed) });
135
+ // Second sync also exercises the persisted-post dedup path for link-keyed items.
136
+ yield* Operation.invoke(FeedOperation.SyncFeed, { feed: Ref.make(subscriptionFeed) });
137
+
138
+ const items = yield* Feed.query(echoFeed, Filter.type(Subscription.Post)).run;
139
+ expect(items).toHaveLength(1);
140
+ expect(items[0].link).toBe('https://example.com/shared');
141
+ },
142
+ Effect.provide(TestLayer),
143
+ TestHelpers.provideTestContext,
144
+ ),
145
+ );
146
+ });
@@ -0,0 +1,140 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ import * as Effect from 'effect/Effect';
6
+
7
+ import { Operation } from '@dxos/compute';
8
+ import { Database, Feed, Filter, Obj, Ref } from '@dxos/echo';
9
+ import { invariant } from '@dxos/invariant';
10
+
11
+ import { FeedOperation, Subscription } from '../types';
12
+ import { type FeedFetcher, browserCorsProxy, fetchRss, fetchStandardSite } from './sources';
13
+
14
+ /** Stable dedup key for a {@link Subscription.Post}. Both fields are optional, but every current fetcher populates `guid` (RSS falls back to `link`, Standard.site uses the record AT-URI). */
15
+ const postKey = (post: { guid?: string; link?: string }): string | undefined => post.guid ?? post.link;
16
+
17
+ /** Resolves the appropriate fetcher for the given feed type. */
18
+ const getFetcher = (type: Subscription.FeedType | undefined): FeedFetcher => {
19
+ switch (type) {
20
+ case 'standard-site':
21
+ return fetchStandardSite;
22
+ case 'rss':
23
+ default:
24
+ return fetchRss;
25
+ }
26
+ };
27
+
28
+ const handler: Operation.WithHandler<typeof FeedOperation.SyncFeed> = FeedOperation.SyncFeed.pipe(
29
+ Operation.withHandler(
30
+ Effect.fnUntraced(function* ({ feed }) {
31
+ const subscriptionFeed = yield* Database.load(feed);
32
+ const url = subscriptionFeed.url;
33
+ invariant(url, 'Feed URL is required.');
34
+ const echoFeed = yield* Database.load(subscriptionFeed.feed);
35
+ invariant(echoFeed, 'Backing ECHO feed not found.');
36
+ invariant(Feed.getQueueUri(echoFeed), 'Feed not stored in a space.');
37
+
38
+ const fetcher = getFetcher(subscriptionFeed.type);
39
+ const { feed: feedMeta, posts } = yield* fetcher(url, { corsProxy: browserCorsProxy() });
40
+
41
+ // Dedup against existing posts already in the backing queue. The
42
+ // `cursor` field on the subscription was previously used as a single-guid
43
+ // filter, but RSS feeds re-serve the same items across polls — filtering
44
+ // out only the cursor's own guid let every other previously-synced post
45
+ // re-pass the filter and re-append on each sync. Key-based dedup against
46
+ // the queue is the source of truth and tolerant of out-of-order or
47
+ // rotated-off-the-window upstream responses.
48
+ //
49
+ // The dedup key is `guid ?? link`: `Post.guid` is optional in the schema,
50
+ // and while every current fetcher populates it (RSS falls back to `link`,
51
+ // Standard.site uses the record AT-URI), a future fetcher or malformed item
52
+ // could omit it. Falling back to `link` keeps such items dedup-able. Items
53
+ // with neither field cannot be deduplicated and will sync as new every
54
+ // time — that's an upstream data quality issue with no clean recovery.
55
+ //
56
+ // Pull the backing queue before reading existing posts: this handler runs in a
57
+ // freshly-spawned process whose local queue replica may not have the blocks yet. Without
58
+ // this, the query returns an empty set, `seenKeys` is empty, and every fetched item
59
+ // re-appends — duplicating the whole feed on a cold sync.
60
+ yield* Feed.sync(echoFeed, { shouldPush: false });
61
+ const existing = yield* Feed.query(echoFeed, Filter.type(Subscription.Post)).run;
62
+ const seenKeys = new Set<string>();
63
+ for (const post of existing) {
64
+ const key = postKey(post);
65
+ if (key) {
66
+ seenKeys.add(key);
67
+ }
68
+ }
69
+ const newPosts: typeof posts = [];
70
+ for (const post of posts) {
71
+ const key = postKey(post);
72
+ // Within-batch dedup: if the upstream serves the same item twice in one
73
+ // response, only the first occurrence is appended.
74
+ if (key && seenKeys.has(key)) {
75
+ continue;
76
+ }
77
+ if (key) {
78
+ seenKeys.add(key);
79
+ }
80
+ newPosts.push(post);
81
+ }
82
+
83
+ // Append new posts to the ECHO feed.
84
+ // NOTE: The `Subscription.Subscription.keep` bound is currently NOT enforced
85
+ // here via `Feed.remove()`. Doing so wipes the underlying queue's
86
+ // `_objectCache` for the deleted posts, but those same Post objects
87
+ // persist in `space.db` (they were added there when first curated
88
+ // into a Magazine via `createRef` → `database.add`). On the next
89
+ // sync/curate, `Feed.query(...).run` returns fresh proxies for the
90
+ // kept items, and any magazine refs to *deleted* posts now
91
+ // reference proxies whose `_internals.database` link is unset —
92
+ // `createRef` then tries to re-add them, hitting the
93
+ // `!_objects.has(core.id)` invariant in `EntityManager.addCore`.
94
+ // Until feed/db lifecycle is reworked we leave the feed unbounded;
95
+ // the `Magazine.keep` bound (enforced in
96
+ // `MagazineArticle.handleCurate`) prevents the visible list from
97
+ // growing unboundedly.
98
+ if (newPosts.length > 0) {
99
+ const feedRef = Ref.make(subscriptionFeed);
100
+ const postObjects = newPosts.map((post) =>
101
+ Obj.make(Subscription.Post, {
102
+ source: feedRef,
103
+ title: post.title,
104
+ link: post.link,
105
+ description: post.description,
106
+ // Persist the body so Standard.site articles render offline (see `load-post-content`);
107
+ // RSS `content:encoded` rides along but its fetch path is unchanged.
108
+ content: post.content,
109
+ author: post.author,
110
+ published: post.published,
111
+ guid: post.guid,
112
+ }),
113
+ );
114
+ yield* Feed.append(echoFeed, postObjects);
115
+
116
+ // Advance cursor to the newest post.
117
+ const newestGuid = posts[0]?.guid;
118
+ if (newestGuid) {
119
+ Obj.update(subscriptionFeed, (subscriptionFeed) => {
120
+ subscriptionFeed.cursor = newestGuid;
121
+ });
122
+ }
123
+
124
+ // Update feed metadata from channel if not already set.
125
+ if (feedMeta.name && !subscriptionFeed.name) {
126
+ Obj.update(subscriptionFeed, (subscriptionFeed) => {
127
+ subscriptionFeed.name = feedMeta.name;
128
+ });
129
+ }
130
+ if (feedMeta.description && !subscriptionFeed.description) {
131
+ Obj.update(subscriptionFeed, (subscriptionFeed) => {
132
+ subscriptionFeed.description = feedMeta.description;
133
+ });
134
+ }
135
+ }
136
+ }),
137
+ ),
138
+ );
139
+
140
+ export default handler;
@@ -0,0 +1,67 @@
1
+ //
2
+ // Copyright 2026 DXOS.org
3
+ //
4
+
5
+ import { describe, expect, test } from 'vitest';
6
+
7
+ import { Obj } from '@dxos/echo';
8
+
9
+ import { Subscription } from '../types';
10
+ import { partitionByKeepBound } from './util';
11
+
12
+ const makePost = (props: { title: string; published?: string }): Subscription.Post =>
13
+ Obj.make(Subscription.Post, { title: props.title, published: props.published });
14
+
15
+ describe('partitionByKeepBound', () => {
16
+ test('keeps the newest N non-starred posts and drops the rest', () => {
17
+ const posts = [
18
+ makePost({ title: 'A', published: '2026-04-01T00:00:00Z' }),
19
+ makePost({ title: 'B', published: '2026-04-02T00:00:00Z' }),
20
+ makePost({ title: 'C', published: '2026-04-03T00:00:00Z' }),
21
+ makePost({ title: 'D', published: '2026-04-04T00:00:00Z' }),
22
+ makePost({ title: 'E', published: '2026-04-05T00:00:00Z' }),
23
+ ];
24
+ const { kept, dropped } = partitionByKeepBound(posts, 2, () => false);
25
+ // Keep newest two (E, D); drop the older three.
26
+ expect(kept.map((post) => post.title).sort()).toEqual(['D', 'E']);
27
+ expect(dropped.map((post) => post.title).sort()).toEqual(['A', 'B', 'C']);
28
+ });
29
+
30
+ test('preserves starred posts beyond the keep bound', () => {
31
+ const posts = [
32
+ makePost({ title: 'A-old-starred', published: '2026-04-01T00:00:00Z' }),
33
+ makePost({ title: 'B', published: '2026-04-02T00:00:00Z' }),
34
+ makePost({ title: 'C', published: '2026-04-03T00:00:00Z' }),
35
+ makePost({ title: 'D', published: '2026-04-04T00:00:00Z' }),
36
+ makePost({ title: 'E', published: '2026-04-05T00:00:00Z' }),
37
+ ];
38
+ const isStarred = (post: Subscription.Post) => post.title === 'A-old-starred';
39
+ const { kept, dropped } = partitionByKeepBound(posts, 2, isStarred);
40
+ // Starred A always kept; newest non-starred E and D fill the bound; B and C dropped.
41
+ expect(kept.map((post) => post.title).sort()).toEqual(['A-old-starred', 'D', 'E']);
42
+ expect(dropped.map((post) => post.title).sort()).toEqual(['B', 'C']);
43
+ });
44
+
45
+ test('falls back gracefully when posts have no published date', () => {
46
+ const posts = [
47
+ makePost({ title: 'A' }),
48
+ makePost({ title: 'B', published: '2026-04-02T00:00:00Z' }),
49
+ makePost({ title: 'C', published: '2026-04-03T00:00:00Z' }),
50
+ ];
51
+ const { kept, dropped } = partitionByKeepBound(posts, 1, () => false);
52
+ // Only the newest dated post is kept; the undated post sorts to the bottom.
53
+ expect(kept.map((post) => post.title)).toEqual(['C']);
54
+ expect(dropped.map((post) => post.title).sort()).toEqual(['A', 'B']);
55
+ });
56
+
57
+ test('keep bound of 0 drops all non-starred posts', () => {
58
+ const posts = [
59
+ makePost({ title: 'A', published: '2026-04-01T00:00:00Z' }),
60
+ makePost({ title: 'B-starred', published: '2026-04-02T00:00:00Z' }),
61
+ ];
62
+ const isStarred = (post: Subscription.Post) => post.title === 'B-starred';
63
+ const { kept, dropped } = partitionByKeepBound(posts, 0, isStarred);
64
+ expect(kept.map((post) => post.title)).toEqual(['B-starred']);
65
+ expect(dropped.map((post) => post.title)).toEqual(['A']);
66
+ });
67
+ });
@@ -0,0 +1,65 @@
1
+ //
2
+ // Copyright 2026 DXOS.org
3
+ //
4
+
5
+ import * as Effect from 'effect/Effect';
6
+
7
+ import { Database, Filter, Query } from '@dxos/echo';
8
+
9
+ import { type Magazine, Subscription } from '../types';
10
+ import { publishedTimestamp } from '../util/date';
11
+
12
+ /**
13
+ * Resolves the Magazine's referenced feeds and the Posts in each feed's backing
14
+ * ECHO queue, filtering out Posts already curated into `magazine.posts` (and cross-feed duplicates).
15
+ */
16
+ export const collectCandidates = (magazine: Magazine.Magazine) =>
17
+ Effect.gen(function* () {
18
+ const seen = new Set(magazine.posts.map((ref) => ref.target?.id ?? ref.uri));
19
+ const candidates: Array<{ post: Subscription.Post; feed: Subscription.Subscription }> = [];
20
+ for (const feedRef of magazine.feeds) {
21
+ const feed = yield* Database.load(feedRef);
22
+ const echoFeed = feed.feed?.target;
23
+ if (!echoFeed) {
24
+ continue;
25
+ }
26
+ const posts = yield* Database.query(Query.select(Filter.type(Subscription.Post)).from(echoFeed)).run;
27
+ for (const post of posts) {
28
+ if (seen.has(post.id)) {
29
+ continue;
30
+ }
31
+ seen.add(post.id);
32
+ candidates.push({ post, feed });
33
+ }
34
+ }
35
+ return candidates;
36
+ });
37
+
38
+ /**
39
+ * Partitions posts into those to keep and those to drop, given a maximum
40
+ * non-starred retention bound. Starred posts are always kept; the remaining
41
+ * non-starred posts are sorted newest-first by `published` and the top
42
+ * `keep` retained.
43
+ *
44
+ * `isStarred` is injectable; callers with a starred-tag uri pass a predicate backed by it
45
+ * (defaults to treating nothing as starred).
46
+ */
47
+ export const partitionByKeepBound = <T extends Subscription.Post>(
48
+ posts: readonly T[],
49
+ keep: number,
50
+ isStarred: (post: T) => boolean = () => false,
51
+ ): { kept: T[]; dropped: T[] } => {
52
+ const kept: T[] = [];
53
+ const candidates: T[] = [];
54
+ for (const post of posts) {
55
+ if (isStarred(post)) {
56
+ kept.push(post);
57
+ } else {
58
+ candidates.push(post);
59
+ }
60
+ }
61
+ candidates.sort((a, b) => publishedTimestamp(b.published) - publishedTimestamp(a.published));
62
+ const retained = candidates.slice(0, Math.max(0, keep));
63
+ const dropped = candidates.slice(Math.max(0, keep));
64
+ return { kept: [...kept, ...retained], dropped };
65
+ };
package/src/paths.ts ADDED
@@ -0,0 +1,14 @@
1
+ //
2
+ // Copyright 2026 DXOS.org
3
+ //
4
+
5
+ import { Paths } from '@dxos/app-toolkit';
6
+
7
+ import { Magazine } from '#types';
8
+
9
+ const { getSectionPath: getMagazinesPath, getObjectPath: getMagazinePath } = Paths.createTypeSectionPaths(
10
+ Magazine.Magazine,
11
+ { groupId: Paths.GroupSegments.content },
12
+ );
13
+
14
+ export { getMagazinePath, getMagazinesPath };
package/src/plugin.ts ADDED
@@ -0,0 +1,11 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ import { Plugin } from '@dxos/app-framework';
6
+
7
+ import { meta } from './meta';
8
+
9
+ export const MagazinePlugin = Plugin.lazy(meta, () => import('#plugin'));
10
+
11
+ export { MagazineOperationHandlerSet } from './operations';
@@ -0,0 +1,6 @@
1
+ //
2
+ // Copyright 2026 DXOS.org
3
+ //
4
+
5
+ export { MagazinePlugin, default } from './MagazinePlugin.workerd';
6
+ export { MagazineOperationHandlerSet } from './operations';
@@ -0,0 +1,5 @@
1
+ //
2
+ // Copyright 2026 DXOS.org
3
+ //
4
+
5
+ export { default as MagazineSkill } from './magazine-skill';
@@ -0,0 +1,45 @@
1
+ //
2
+ // Copyright 2026 DXOS.org
3
+ //
4
+
5
+ import { Skill, Template } from '@dxos/compute';
6
+ import { trim } from '@dxos/util';
7
+
8
+ import { FeedOperation } from '#types';
9
+ import { Magazine } from '#types';
10
+
11
+ const operations = [FeedOperation.FetchArticleContent];
12
+
13
+ const make = () =>
14
+ Skill.make({
15
+ key: Magazine.SKILL_KEY,
16
+ name: 'Magazine Curator',
17
+ tools: Skill.toolDefinitions({ operations }),
18
+ instructions: Template.make({
19
+ // HOW only: input shape, tool usage, and the completeJob output contract. The editorial
20
+ // methodology (what to select, dedup, snippet/image guidance) and the magazine's topic live on
21
+ // the Routine's instructions (see Magazine.DEFAULT_INSTRUCTIONS), not here.
22
+ source: trim`
23
+ The candidate Posts are provided in the <input> as a JSON array under "candidates"; each
24
+ candidate has an "id" plus title, description, author, feedName, published, and link. The
25
+ editorial brief (the Topic and selection guidance) is provided in your instructions.
26
+
27
+ When a candidate's title and description are not enough to judge relevance, you MAY call
28
+ fetchArticleContent with the candidate's id to read the full article text (and find an image)
29
+ before deciding.
30
+
31
+ When you are done, call completeJob with the structured output:
32
+ { "posts": [{ "id": "<candidate id>", "snippet": "...", "imageUrl": "..." }, ...] }
33
+ listing the selected candidates in the order you want them shown. Only use ids that appear
34
+ in the input — never invent ids. Omit "imageUrl" when no image is available. Do not attempt to
35
+ add Posts yourself; the Magazine is updated mechanically from your output.
36
+ `,
37
+ }),
38
+ });
39
+
40
+ const skill: Skill.Definition = {
41
+ key: Magazine.SKILL_KEY,
42
+ make,
43
+ };
44
+
45
+ export default skill;