@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.
- package/LICENSE +105 -0
- package/PLUGIN.mdl +653 -0
- package/dist/lib/neutral/FeedArticle-PEWCFFSP.mjs +90 -0
- package/dist/lib/neutral/FeedArticle-PEWCFFSP.mjs.map +7 -0
- package/dist/lib/neutral/FeedProperties-EIO7IZOA.mjs +97 -0
- package/dist/lib/neutral/FeedProperties-EIO7IZOA.mjs.map +7 -0
- package/dist/lib/neutral/MagazineArticle-DEUTR23F.mjs +325 -0
- package/dist/lib/neutral/MagazineArticle-DEUTR23F.mjs.map +7 -0
- package/dist/lib/neutral/MagazinePlugin.mjs +62 -0
- package/dist/lib/neutral/MagazinePlugin.mjs.map +7 -0
- package/dist/lib/neutral/MagazinePlugin.workerd.mjs +12 -0
- package/dist/lib/neutral/MagazinePlugin.workerd.mjs.map +7 -0
- package/dist/lib/neutral/PostArticle-RERHXPXC.mjs +230 -0
- package/dist/lib/neutral/PostArticle-RERHXPXC.mjs.map +7 -0
- package/dist/lib/neutral/PostCard-VDGN7BBA.mjs +60 -0
- package/dist/lib/neutral/PostCard-VDGN7BBA.mjs.map +7 -0
- package/dist/lib/neutral/app-graph-builder-JDPRBVHL.mjs +121 -0
- package/dist/lib/neutral/app-graph-builder-JDPRBVHL.mjs.map +7 -0
- package/dist/lib/neutral/capabilities/index.mjs +21 -0
- package/dist/lib/neutral/capabilities/index.mjs.map +7 -0
- package/dist/lib/neutral/chunk-3K4AQKBZ.mjs +12 -0
- package/dist/lib/neutral/chunk-3K4AQKBZ.mjs.map +7 -0
- package/dist/lib/neutral/chunk-AFPBWBTK.mjs +591 -0
- package/dist/lib/neutral/chunk-AFPBWBTK.mjs.map +7 -0
- package/dist/lib/neutral/chunk-APQRTYT3.mjs +57 -0
- package/dist/lib/neutral/chunk-APQRTYT3.mjs.map +7 -0
- package/dist/lib/neutral/chunk-GU725ZF4.mjs +11 -0
- package/dist/lib/neutral/chunk-GU725ZF4.mjs.map +7 -0
- package/dist/lib/neutral/chunk-J5LGTIGS.mjs +10 -0
- package/dist/lib/neutral/chunk-J5LGTIGS.mjs.map +7 -0
- package/dist/lib/neutral/chunk-JFPR6OV4.mjs +60 -0
- package/dist/lib/neutral/chunk-JFPR6OV4.mjs.map +7 -0
- package/dist/lib/neutral/chunk-KTYLUOUG.mjs +36 -0
- package/dist/lib/neutral/chunk-KTYLUOUG.mjs.map +7 -0
- package/dist/lib/neutral/chunk-MBPX7RPI.mjs +14 -0
- package/dist/lib/neutral/chunk-MBPX7RPI.mjs.map +7 -0
- package/dist/lib/neutral/chunk-MJ2AY2CK.mjs +8 -0
- package/dist/lib/neutral/chunk-MJ2AY2CK.mjs.map +7 -0
- package/dist/lib/neutral/chunk-OYCDRSWB.mjs +624 -0
- package/dist/lib/neutral/chunk-OYCDRSWB.mjs.map +7 -0
- package/dist/lib/neutral/chunk-SAWKLGJU.mjs +28 -0
- package/dist/lib/neutral/chunk-SAWKLGJU.mjs.map +7 -0
- package/dist/lib/neutral/chunk-XCNCK47N.mjs +15 -0
- package/dist/lib/neutral/chunk-XCNCK47N.mjs.map +7 -0
- package/dist/lib/neutral/clear-magazine-KNY4OWY4.mjs +59 -0
- package/dist/lib/neutral/clear-magazine-KNY4OWY4.mjs.map +7 -0
- package/dist/lib/neutral/components/index.mjs +338 -0
- package/dist/lib/neutral/components/index.mjs.map +7 -0
- package/dist/lib/neutral/containers/index.mjs +17 -0
- package/dist/lib/neutral/containers/index.mjs.map +7 -0
- package/dist/lib/neutral/create-object-SYZMRR4H.mjs +98 -0
- package/dist/lib/neutral/create-object-SYZMRR4H.mjs.map +7 -0
- package/dist/lib/neutral/curate-magazine-MA6OLK2X.mjs +184 -0
- package/dist/lib/neutral/curate-magazine-MA6OLK2X.mjs.map +7 -0
- package/dist/lib/neutral/fetch-article-content-CZO6D3QD.mjs +31 -0
- package/dist/lib/neutral/fetch-article-content-CZO6D3QD.mjs.map +7 -0
- package/dist/lib/neutral/index.mjs +18 -0
- package/dist/lib/neutral/index.mjs.map +7 -0
- package/dist/lib/neutral/load-post-content-XGL6VFF6.mjs +65 -0
- package/dist/lib/neutral/load-post-content-XGL6VFF6.mjs.map +7 -0
- package/dist/lib/neutral/meta.json +1 -0
- package/dist/lib/neutral/meta.mjs +8 -0
- package/dist/lib/neutral/meta.mjs.map +7 -0
- package/dist/lib/neutral/navigation-resolver-IN4OMCDR.mjs +18 -0
- package/dist/lib/neutral/navigation-resolver-IN4OMCDR.mjs.map +7 -0
- package/dist/lib/neutral/operation-handler-JE3743BU.mjs +8 -0
- package/dist/lib/neutral/operation-handler-JE3743BU.mjs.map +7 -0
- package/dist/lib/neutral/operations/index.mjs +8 -0
- package/dist/lib/neutral/operations/index.mjs.map +7 -0
- package/dist/lib/neutral/plugin.mjs +16 -0
- package/dist/lib/neutral/plugin.mjs.map +7 -0
- package/dist/lib/neutral/plugin.workerd.mjs +16 -0
- package/dist/lib/neutral/plugin.workerd.mjs.map +7 -0
- package/dist/lib/neutral/react-surface-TZESAMG2.mjs +60 -0
- package/dist/lib/neutral/react-surface-TZESAMG2.mjs.map +7 -0
- package/dist/lib/neutral/routine-templates-A2XTJOZO.mjs +52 -0
- package/dist/lib/neutral/routine-templates-A2XTJOZO.mjs.map +7 -0
- package/dist/lib/neutral/skill-definition-3KLMUEC5.mjs +8 -0
- package/dist/lib/neutral/skill-definition-3KLMUEC5.mjs.map +7 -0
- package/dist/lib/neutral/skills/index.mjs +46 -0
- package/dist/lib/neutral/skills/index.mjs.map +7 -0
- package/dist/lib/neutral/sync-feed-UZX2XHSQ.mjs +99 -0
- package/dist/lib/neutral/sync-feed-UZX2XHSQ.mjs.map +7 -0
- package/dist/lib/neutral/translations.mjs +81 -0
- package/dist/lib/neutral/translations.mjs.map +7 -0
- package/dist/lib/neutral/types/index.mjs +14 -0
- package/dist/lib/neutral/types/index.mjs.map +7 -0
- package/dist/types/dx.config.d.ts +28 -0
- package/dist/types/dx.config.d.ts.map +1 -0
- package/dist/types/src/MagazinePlugin.d.ts +4 -0
- package/dist/types/src/MagazinePlugin.d.ts.map +1 -0
- package/dist/types/src/MagazinePlugin.test.d.ts +2 -0
- package/dist/types/src/MagazinePlugin.test.d.ts.map +1 -0
- package/dist/types/src/MagazinePlugin.workerd.d.ts +4 -0
- package/dist/types/src/MagazinePlugin.workerd.d.ts.map +1 -0
- package/dist/types/src/atoms/atoms.test.d.ts +2 -0
- package/dist/types/src/atoms/atoms.test.d.ts.map +1 -0
- package/dist/types/src/atoms/index.d.ts +7 -0
- package/dist/types/src/atoms/index.d.ts.map +1 -0
- package/dist/types/src/atoms/magazine-posts.d.ts +16 -0
- package/dist/types/src/atoms/magazine-posts.d.ts.map +1 -0
- package/dist/types/src/atoms/post-content.d.ts +7 -0
- package/dist/types/src/atoms/post-content.d.ts.map +1 -0
- package/dist/types/src/atoms/post-curation.d.ts +16 -0
- package/dist/types/src/atoms/post-curation.d.ts.map +1 -0
- package/dist/types/src/atoms/post-display.d.ts +22 -0
- package/dist/types/src/atoms/post-display.d.ts.map +1 -0
- package/dist/types/src/atoms/post-read.d.ts +14 -0
- package/dist/types/src/atoms/post-read.d.ts.map +1 -0
- package/dist/types/src/atoms/post-tags.d.ts +16 -0
- package/dist/types/src/atoms/post-tags.d.ts.map +1 -0
- package/dist/types/src/capabilities/app-graph-builder.d.ts +5 -0
- package/dist/types/src/capabilities/app-graph-builder.d.ts.map +1 -0
- package/dist/types/src/capabilities/create-object.d.ts +5 -0
- package/dist/types/src/capabilities/create-object.d.ts.map +1 -0
- package/dist/types/src/capabilities/index.d.ts +10 -0
- package/dist/types/src/capabilities/index.d.ts.map +1 -0
- package/dist/types/src/capabilities/navigation-resolver.d.ts +5 -0
- package/dist/types/src/capabilities/navigation-resolver.d.ts.map +1 -0
- package/dist/types/src/capabilities/operation-handler.d.ts +6 -0
- package/dist/types/src/capabilities/operation-handler.d.ts.map +1 -0
- package/dist/types/src/capabilities/react-surface.d.ts +5 -0
- package/dist/types/src/capabilities/react-surface.d.ts.map +1 -0
- package/dist/types/src/capabilities/routine-templates.d.ts +5 -0
- package/dist/types/src/capabilities/routine-templates.d.ts.map +1 -0
- package/dist/types/src/capabilities/skill-definition.d.ts +5 -0
- package/dist/types/src/capabilities/skill-definition.d.ts.map +1 -0
- package/dist/types/src/components/PostContent/PostContent.d.ts +30 -0
- package/dist/types/src/components/PostContent/PostContent.d.ts.map +1 -0
- package/dist/types/src/components/PostContent/PostContent.test.d.ts +2 -0
- package/dist/types/src/components/PostContent/PostContent.test.d.ts.map +1 -0
- package/dist/types/src/components/PostContent/index.d.ts +2 -0
- package/dist/types/src/components/PostContent/index.d.ts.map +1 -0
- package/dist/types/src/components/PostStack/PostStack.d.ts +17 -0
- package/dist/types/src/components/PostStack/PostStack.d.ts.map +1 -0
- package/dist/types/src/components/PostStack/PostStack.stories.d.ts +10 -0
- package/dist/types/src/components/PostStack/PostStack.stories.d.ts.map +1 -0
- package/dist/types/src/components/PostStack/index.d.ts +2 -0
- package/dist/types/src/components/PostStack/index.d.ts.map +1 -0
- package/dist/types/src/components/SubscriptionStack/SubscriptionStack.d.ts +23 -0
- package/dist/types/src/components/SubscriptionStack/SubscriptionStack.d.ts.map +1 -0
- package/dist/types/src/components/SubscriptionStack/SubscriptionStack.stories.d.ts +8 -0
- package/dist/types/src/components/SubscriptionStack/SubscriptionStack.stories.d.ts.map +1 -0
- package/dist/types/src/components/SubscriptionStack/index.d.ts +2 -0
- package/dist/types/src/components/SubscriptionStack/index.d.ts.map +1 -0
- package/dist/types/src/components/index.d.ts +4 -0
- package/dist/types/src/components/index.d.ts.map +1 -0
- package/dist/types/src/containers/FeedArticle/FeedArticle.d.ts +6 -0
- package/dist/types/src/containers/FeedArticle/FeedArticle.d.ts.map +1 -0
- package/dist/types/src/containers/FeedArticle/FeedArticle.stories.d.ts +12 -0
- package/dist/types/src/containers/FeedArticle/FeedArticle.stories.d.ts.map +1 -0
- package/dist/types/src/containers/FeedArticle/FeedToolbar.d.ts +10 -0
- package/dist/types/src/containers/FeedArticle/FeedToolbar.d.ts.map +1 -0
- package/dist/types/src/containers/FeedArticle/index.d.ts +2 -0
- package/dist/types/src/containers/FeedArticle/index.d.ts.map +1 -0
- package/dist/types/src/containers/FeedProperties/FeedProperties.d.ts +6 -0
- package/dist/types/src/containers/FeedProperties/FeedProperties.d.ts.map +1 -0
- package/dist/types/src/containers/FeedProperties/index.d.ts +2 -0
- package/dist/types/src/containers/FeedProperties/index.d.ts.map +1 -0
- package/dist/types/src/containers/MagazineArticle/MagazineArticle.d.ts +6 -0
- package/dist/types/src/containers/MagazineArticle/MagazineArticle.d.ts.map +1 -0
- package/dist/types/src/containers/MagazineArticle/MagazineArticle.stories.d.ts +9 -0
- package/dist/types/src/containers/MagazineArticle/MagazineArticle.stories.d.ts.map +1 -0
- package/dist/types/src/containers/MagazineArticle/MagazineTile.d.ts +11 -0
- package/dist/types/src/containers/MagazineArticle/MagazineTile.d.ts.map +1 -0
- package/dist/types/src/containers/MagazineArticle/index.d.ts +2 -0
- package/dist/types/src/containers/MagazineArticle/index.d.ts.map +1 -0
- package/dist/types/src/containers/MagazineArticle/useToolbar.d.ts +18 -0
- package/dist/types/src/containers/MagazineArticle/useToolbar.d.ts.map +1 -0
- package/dist/types/src/containers/PostArticle/PostArticle.d.ts +6 -0
- package/dist/types/src/containers/PostArticle/PostArticle.d.ts.map +1 -0
- package/dist/types/src/containers/PostArticle/PostArticle.stories.d.ts +8 -0
- package/dist/types/src/containers/PostArticle/PostArticle.stories.d.ts.map +1 -0
- package/dist/types/src/containers/PostArticle/PostToolbar.d.ts +19 -0
- package/dist/types/src/containers/PostArticle/PostToolbar.d.ts.map +1 -0
- package/dist/types/src/containers/PostArticle/index.d.ts +2 -0
- package/dist/types/src/containers/PostArticle/index.d.ts.map +1 -0
- package/dist/types/src/containers/PostCard/PostCard.d.ts +11 -0
- package/dist/types/src/containers/PostCard/PostCard.d.ts.map +1 -0
- package/dist/types/src/containers/PostCard/index.d.ts +2 -0
- package/dist/types/src/containers/PostCard/index.d.ts.map +1 -0
- package/dist/types/src/containers/SubscriptionsArticle/SubscriptionsArticle.d.ts +5 -0
- package/dist/types/src/containers/SubscriptionsArticle/SubscriptionsArticle.d.ts.map +1 -0
- package/dist/types/src/containers/SubscriptionsArticle/SubscriptionsArticle.stories.d.ts +8 -0
- package/dist/types/src/containers/SubscriptionsArticle/SubscriptionsArticle.stories.d.ts.map +1 -0
- package/dist/types/src/containers/SubscriptionsArticle/index.d.ts +2 -0
- package/dist/types/src/containers/SubscriptionsArticle/index.d.ts.map +1 -0
- package/dist/types/src/containers/index.d.ts +7 -0
- package/dist/types/src/containers/index.d.ts.map +1 -0
- package/dist/types/src/index.d.ts +3 -0
- package/dist/types/src/index.d.ts.map +1 -0
- package/dist/types/src/meta.d.ts +33 -0
- package/dist/types/src/meta.d.ts.map +1 -0
- package/dist/types/src/operations/clear-magazine.d.ts +10 -0
- package/dist/types/src/operations/clear-magazine.d.ts.map +1 -0
- package/dist/types/src/operations/curate-magazine.d.ts +25 -0
- package/dist/types/src/operations/curate-magazine.d.ts.map +1 -0
- package/dist/types/src/operations/curate-magazine.skill.test.d.ts +2 -0
- package/dist/types/src/operations/curate-magazine.skill.test.d.ts.map +1 -0
- package/dist/types/src/operations/curate-magazine.test.d.ts +2 -0
- package/dist/types/src/operations/curate-magazine.test.d.ts.map +1 -0
- package/dist/types/src/operations/extraction/article.d.ts +37 -0
- package/dist/types/src/operations/extraction/article.d.ts.map +1 -0
- package/dist/types/src/operations/extraction/article.test.d.ts +2 -0
- package/dist/types/src/operations/extraction/article.test.d.ts.map +1 -0
- package/dist/types/src/operations/extraction/index.d.ts +2 -0
- package/dist/types/src/operations/extraction/index.d.ts.map +1 -0
- package/dist/types/src/operations/fetch-article-content.d.ts +5 -0
- package/dist/types/src/operations/fetch-article-content.d.ts.map +1 -0
- package/dist/types/src/operations/index.d.ts +3 -0
- package/dist/types/src/operations/index.d.ts.map +1 -0
- package/dist/types/src/operations/load-post-content.d.ts +8 -0
- package/dist/types/src/operations/load-post-content.d.ts.map +1 -0
- package/dist/types/src/operations/sources/article.d.ts +24 -0
- package/dist/types/src/operations/sources/article.d.ts.map +1 -0
- package/dist/types/src/operations/sources/cors.d.ts +12 -0
- package/dist/types/src/operations/sources/cors.d.ts.map +1 -0
- package/dist/types/src/operations/sources/feed-fetcher.d.ts +22 -0
- package/dist/types/src/operations/sources/feed-fetcher.d.ts.map +1 -0
- package/dist/types/src/operations/sources/feed-fetcher.test.d.ts +2 -0
- package/dist/types/src/operations/sources/feed-fetcher.test.d.ts.map +1 -0
- package/dist/types/src/operations/sources/http.d.ts +9 -0
- package/dist/types/src/operations/sources/http.d.ts.map +1 -0
- package/dist/types/src/operations/sources/index.d.ts +6 -0
- package/dist/types/src/operations/sources/index.d.ts.map +1 -0
- package/dist/types/src/operations/sources/rss.d.ts +4 -0
- package/dist/types/src/operations/sources/rss.d.ts.map +1 -0
- package/dist/types/src/operations/sources/rss.test.d.ts +2 -0
- package/dist/types/src/operations/sources/rss.test.d.ts.map +1 -0
- package/dist/types/src/operations/sources/standard-site.d.ts +33 -0
- package/dist/types/src/operations/sources/standard-site.d.ts.map +1 -0
- package/dist/types/src/operations/sync-feed.d.ts +5 -0
- package/dist/types/src/operations/sync-feed.d.ts.map +1 -0
- package/dist/types/src/operations/sync-feed.test.d.ts +2 -0
- package/dist/types/src/operations/sync-feed.test.d.ts.map +1 -0
- package/dist/types/src/operations/util.d.ts +25 -0
- package/dist/types/src/operations/util.d.ts.map +1 -0
- package/dist/types/src/operations/util.test.d.ts +2 -0
- package/dist/types/src/operations/util.test.d.ts.map +1 -0
- package/dist/types/src/paths.d.ts +3 -0
- package/dist/types/src/paths.d.ts.map +1 -0
- package/dist/types/src/plugin.d.ts +4 -0
- package/dist/types/src/plugin.d.ts.map +1 -0
- package/dist/types/src/plugin.workerd.d.ts +3 -0
- package/dist/types/src/plugin.workerd.d.ts.map +1 -0
- package/dist/types/src/skills/index.d.ts +2 -0
- package/dist/types/src/skills/index.d.ts.map +1 -0
- package/dist/types/src/skills/magazine-skill.d.ts +4 -0
- package/dist/types/src/skills/magazine-skill.d.ts.map +1 -0
- package/dist/types/src/stories/ArticleExtractor.stories.d.ts +267 -0
- package/dist/types/src/stories/ArticleExtractor.stories.d.ts.map +1 -0
- package/dist/types/src/stories/MagazineCurate.stories.d.ts +32 -0
- package/dist/types/src/stories/MagazineCurate.stories.d.ts.map +1 -0
- package/dist/types/src/stories/theregister-fixture.test.d.ts +2 -0
- package/dist/types/src/stories/theregister-fixture.test.d.ts.map +1 -0
- package/dist/types/src/templates/magazine-curation.d.ts +7 -0
- package/dist/types/src/templates/magazine-curation.d.ts.map +1 -0
- package/dist/types/src/testing/builder.d.ts +28 -0
- package/dist/types/src/testing/builder.d.ts.map +1 -0
- package/dist/types/src/testing/index.d.ts +2 -0
- package/dist/types/src/testing/index.d.ts.map +1 -0
- package/dist/types/src/translations.d.ts +247 -0
- package/dist/types/src/translations.d.ts.map +1 -0
- package/dist/types/src/types/CreateSubscription.d.ts +29 -0
- package/dist/types/src/types/CreateSubscription.d.ts.map +1 -0
- package/dist/types/src/types/FeedOperation.d.ts +52 -0
- package/dist/types/src/types/FeedOperation.d.ts.map +1 -0
- package/dist/types/src/types/Magazine.d.ts +87 -0
- package/dist/types/src/types/Magazine.d.ts.map +1 -0
- package/dist/types/src/types/Magazine.test.d.ts +2 -0
- package/dist/types/src/types/Magazine.test.d.ts.map +1 -0
- package/dist/types/src/types/Subscription.d.ts +144 -0
- package/dist/types/src/types/Subscription.d.ts.map +1 -0
- package/dist/types/src/types/Subscription.test.d.ts +2 -0
- package/dist/types/src/types/Subscription.test.d.ts.map +1 -0
- package/dist/types/src/types/index.d.ts +5 -0
- package/dist/types/src/types/index.d.ts.map +1 -0
- package/dist/types/src/util/date.d.ts +8 -0
- package/dist/types/src/util/date.d.ts.map +1 -0
- package/dist/types/src/util/index.d.ts +4 -0
- package/dist/types/src/util/index.d.ts.map +1 -0
- package/dist/types/src/util/post-content.d.ts +14 -0
- package/dist/types/src/util/post-content.d.ts.map +1 -0
- package/dist/types/src/util/text.d.ts +18 -0
- package/dist/types/src/util/text.d.ts.map +1 -0
- package/dist/types/tsconfig.tsbuildinfo +1 -0
- package/dx.config.ts +48 -0
- package/package.json +164 -0
- package/src/MagazinePlugin.test.ts +36 -0
- package/src/MagazinePlugin.tsx +61 -0
- package/src/MagazinePlugin.workerd.ts +31 -0
- package/src/atoms/atoms.test.ts +323 -0
- package/src/atoms/index.ts +10 -0
- package/src/atoms/magazine-posts.ts +70 -0
- package/src/atoms/post-content.ts +34 -0
- package/src/atoms/post-curation.ts +36 -0
- package/src/atoms/post-display.ts +59 -0
- package/src/atoms/post-read.ts +40 -0
- package/src/atoms/post-tags.ts +66 -0
- package/src/capabilities/app-graph-builder.ts +125 -0
- package/src/capabilities/create-object.ts +127 -0
- package/src/capabilities/index.ts +17 -0
- package/src/capabilities/navigation-resolver.ts +21 -0
- package/src/capabilities/operation-handler.ts +16 -0
- package/src/capabilities/react-surface.tsx +53 -0
- package/src/capabilities/routine-templates.ts +16 -0
- package/src/capabilities/skill-definition.ts +14 -0
- package/src/components/PostContent/PostContent.test.ts +81 -0
- package/src/components/PostContent/PostContent.tsx +111 -0
- package/src/components/PostContent/index.ts +5 -0
- package/src/components/PostStack/PostStack.stories.tsx +39 -0
- package/src/components/PostStack/PostStack.tsx +151 -0
- package/src/components/PostStack/index.ts +5 -0
- package/src/components/SubscriptionStack/SubscriptionStack.stories.tsx +52 -0
- package/src/components/SubscriptionStack/SubscriptionStack.tsx +161 -0
- package/src/components/SubscriptionStack/index.ts +5 -0
- package/src/components/index.ts +7 -0
- package/src/containers/FeedArticle/FeedArticle.stories.tsx +102 -0
- package/src/containers/FeedArticle/FeedArticle.tsx +68 -0
- package/src/containers/FeedArticle/FeedToolbar.tsx +47 -0
- package/src/containers/FeedArticle/index.ts +5 -0
- package/src/containers/FeedProperties/FeedProperties.tsx +107 -0
- package/src/containers/FeedProperties/index.ts +5 -0
- package/src/containers/MagazineArticle/MagazineArticle.stories.tsx +244 -0
- package/src/containers/MagazineArticle/MagazineArticle.tsx +146 -0
- package/src/containers/MagazineArticle/MagazineTile.tsx +88 -0
- package/src/containers/MagazineArticle/index.ts +5 -0
- package/src/containers/MagazineArticle/useToolbar.tsx +145 -0
- package/src/containers/PostArticle/PostArticle.stories.tsx +117 -0
- package/src/containers/PostArticle/PostArticle.tsx +140 -0
- package/src/containers/PostArticle/PostToolbar.tsx +120 -0
- package/src/containers/PostArticle/index.ts +5 -0
- package/src/containers/PostCard/PostCard.tsx +70 -0
- package/src/containers/PostCard/index.ts +5 -0
- package/src/containers/SubscriptionsArticle/SubscriptionsArticle.stories.tsx +80 -0
- package/src/containers/SubscriptionsArticle/SubscriptionsArticle.tsx +99 -0
- package/src/containers/SubscriptionsArticle/index.ts +5 -0
- package/src/containers/index.ts +11 -0
- package/src/index.ts +6 -0
- package/src/meta.ts +9 -0
- package/src/operations/clear-magazine.ts +83 -0
- package/src/operations/curate-magazine.skill.conversations.json +1 -0
- package/src/operations/curate-magazine.skill.test.ts +167 -0
- package/src/operations/curate-magazine.test.ts +91 -0
- package/src/operations/curate-magazine.ts +178 -0
- package/src/operations/extraction/article.test.ts +154 -0
- package/src/operations/extraction/article.ts +270 -0
- package/src/operations/extraction/index.ts +5 -0
- package/src/operations/fetch-article-content.ts +27 -0
- package/src/operations/index.ts +13 -0
- package/src/operations/load-post-content.ts +76 -0
- package/src/operations/sources/article.ts +122 -0
- package/src/operations/sources/cors.ts +19 -0
- package/src/operations/sources/feed-fetcher.test.ts +200 -0
- package/src/operations/sources/feed-fetcher.ts +26 -0
- package/src/operations/sources/http.ts +42 -0
- package/src/operations/sources/index.ts +9 -0
- package/src/operations/sources/rss.test.ts +201 -0
- package/src/operations/sources/rss.ts +138 -0
- package/src/operations/sources/standard-site.ts +381 -0
- package/src/operations/sources/testing/feed.xml +477 -0
- package/src/operations/sync-feed.test.ts +146 -0
- package/src/operations/sync-feed.ts +140 -0
- package/src/operations/util.test.ts +67 -0
- package/src/operations/util.ts +65 -0
- package/src/paths.ts +14 -0
- package/src/plugin.ts +11 -0
- package/src/plugin.workerd.ts +6 -0
- package/src/skills/index.ts +5 -0
- package/src/skills/magazine-skill.ts +45 -0
- package/src/stories/ArticleExtractor.stories.tsx +204 -0
- package/src/stories/MagazineCurate.stories.tsx +217 -0
- package/src/stories/fixtures/theregister-ai.xml +45 -0
- package/src/stories/theregister-fixture.test.ts +35 -0
- package/src/templates/magazine-curation.ts +47 -0
- package/src/testing/builder.ts +77 -0
- package/src/testing/index.ts +5 -0
- package/src/translations.ts +81 -0
- package/src/types/CreateSubscription.ts +61 -0
- package/src/types/FeedOperation.ts +134 -0
- package/src/types/Magazine.test.ts +69 -0
- package/src/types/Magazine.ts +214 -0
- package/src/types/Subscription.test.ts +171 -0
- package/src/types/Subscription.ts +366 -0
- package/src/types/index.ts +8 -0
- package/src/util/date.ts +27 -0
- package/src/util/index.ts +7 -0
- package/src/util/post-content.ts +19 -0
- package/src/util/text.ts +98 -0
- package/src/vite-env.d.ts +10 -0
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2026 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { type Meta, type StoryObj } from '@storybook/react-vite';
|
|
6
|
+
import React, { useCallback, useMemo, useState } from 'react';
|
|
7
|
+
|
|
8
|
+
import { IconButton, Input, Panel, ScrollArea, Select, Toolbar } from '@dxos/react-ui';
|
|
9
|
+
import { composable } from '@dxos/react-ui';
|
|
10
|
+
import { SyntaxHighlighter } from '@dxos/react-ui-syntax-highlighter';
|
|
11
|
+
import { withLayout, withTheme } from '@dxos/react-ui/testing';
|
|
12
|
+
|
|
13
|
+
import { translations } from '#translations';
|
|
14
|
+
|
|
15
|
+
import { PostContent } from '../components';
|
|
16
|
+
import { type ExtractedArticle, extractArticle } from '../operations/extraction';
|
|
17
|
+
import { Subscription } from '../types';
|
|
18
|
+
|
|
19
|
+
type State =
|
|
20
|
+
| { status: 'idle' }
|
|
21
|
+
| { status: 'loading' }
|
|
22
|
+
| { status: 'ok'; article: ExtractedArticle; sourceLength: number }
|
|
23
|
+
| { status: 'error'; message: string };
|
|
24
|
+
|
|
25
|
+
const SAMPLE_URLS = [
|
|
26
|
+
'https://www.inkandswitch.com/essay/local-first/',
|
|
27
|
+
'https://martinfowler.com/articles/cant-buy-integration.html',
|
|
28
|
+
'https://overreacted.io/before-you-memo/',
|
|
29
|
+
'https://www.theregister.com/2026/04/24/deepseek_v4/',
|
|
30
|
+
];
|
|
31
|
+
|
|
32
|
+
const formatBytes = (n: number): string => (n < 1024 ? `${n} B` : `${(n / 1024).toFixed(1)} KB`);
|
|
33
|
+
|
|
34
|
+
const DefaultStory = () => {
|
|
35
|
+
const [url, setUrl] = useState(SAMPLE_URLS[0]);
|
|
36
|
+
const [state, setState] = useState<State>({ status: 'idle' });
|
|
37
|
+
const [showMarkdown, setShowMarkdown] = useState(false);
|
|
38
|
+
|
|
39
|
+
const handleFetch = useCallback(async () => {
|
|
40
|
+
if (!url) {
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
setState({ status: 'loading' });
|
|
44
|
+
try {
|
|
45
|
+
// Storybook ships the same `/api/rss?url=` middleware as composer-app for CORS-free fetches.
|
|
46
|
+
const proxyUrl = `/api/rss?url=${encodeURIComponent(url)}`;
|
|
47
|
+
const response = await fetch(proxyUrl, { redirect: 'follow' });
|
|
48
|
+
if (!response.ok) {
|
|
49
|
+
throw new Error(`Fetch failed: ${response.status} ${response.statusText}`);
|
|
50
|
+
}
|
|
51
|
+
const html = await response.text();
|
|
52
|
+
const article = await extractArticle(html, url);
|
|
53
|
+
setState({ status: 'ok', article, sourceLength: html.length });
|
|
54
|
+
} catch (error) {
|
|
55
|
+
setState({ status: 'error', message: String(error) });
|
|
56
|
+
}
|
|
57
|
+
}, [url]);
|
|
58
|
+
|
|
59
|
+
return (
|
|
60
|
+
<Panel.Root>
|
|
61
|
+
<Panel.Toolbar asChild>
|
|
62
|
+
<Toolbar.Root>
|
|
63
|
+
<Input.Root>
|
|
64
|
+
<Input.TextInput
|
|
65
|
+
placeholder='Article URL'
|
|
66
|
+
value={url}
|
|
67
|
+
onChange={(event) => setUrl(event.target.value)}
|
|
68
|
+
onKeyDown={(event) => {
|
|
69
|
+
if (event.key === 'Enter') {
|
|
70
|
+
void handleFetch();
|
|
71
|
+
}
|
|
72
|
+
}}
|
|
73
|
+
classNames='is-full min-is-[24rem]'
|
|
74
|
+
/>
|
|
75
|
+
</Input.Root>
|
|
76
|
+
<Select.Root
|
|
77
|
+
value={url}
|
|
78
|
+
onValueChange={(sample) => {
|
|
79
|
+
setUrl(sample);
|
|
80
|
+
setState({ status: 'idle' });
|
|
81
|
+
}}
|
|
82
|
+
>
|
|
83
|
+
<Toolbar.Button asChild>
|
|
84
|
+
<Select.TriggerButton placeholder='Sample URL' />
|
|
85
|
+
</Toolbar.Button>
|
|
86
|
+
<Select.Portal>
|
|
87
|
+
<Select.Content>
|
|
88
|
+
<Select.Viewport>
|
|
89
|
+
{SAMPLE_URLS.map((sample) => (
|
|
90
|
+
<Select.Option key={sample} value={sample}>
|
|
91
|
+
{new URL(sample).hostname}
|
|
92
|
+
</Select.Option>
|
|
93
|
+
))}
|
|
94
|
+
</Select.Viewport>
|
|
95
|
+
<Select.Arrow />
|
|
96
|
+
</Select.Content>
|
|
97
|
+
</Select.Portal>
|
|
98
|
+
</Select.Root>
|
|
99
|
+
<Toolbar.IconButton
|
|
100
|
+
icon='ph--arrow-clockwise--regular'
|
|
101
|
+
iconOnly
|
|
102
|
+
label='Fetch'
|
|
103
|
+
onClick={() => void handleFetch()}
|
|
104
|
+
disabled={state.status === 'loading'}
|
|
105
|
+
/>
|
|
106
|
+
<IconButton
|
|
107
|
+
label={showMarkdown ? 'Show preview' : 'Show Markdown'}
|
|
108
|
+
icon={showMarkdown ? 'ph--article--regular' : 'ph--code--regular'}
|
|
109
|
+
iconOnly
|
|
110
|
+
aria-pressed={showMarkdown}
|
|
111
|
+
onClick={() => setShowMarkdown((showMarkdown) => !showMarkdown)}
|
|
112
|
+
/>
|
|
113
|
+
</Toolbar.Root>
|
|
114
|
+
</Panel.Toolbar>
|
|
115
|
+
<Panel.Content asChild>
|
|
116
|
+
{state.status === 'ok' ? (
|
|
117
|
+
<ResultView article={state.article} sourceLength={state.sourceLength} showMarkdown={showMarkdown} />
|
|
118
|
+
) : (
|
|
119
|
+
<div>
|
|
120
|
+
{state.status === 'idle' && (
|
|
121
|
+
<p className='p-2 text-sm text-subdued'>Paste an article URL and press Fetch to see the extraction.</p>
|
|
122
|
+
)}
|
|
123
|
+
{state.status === 'loading' && <p className='p-2 text-sm text-subdued'>Fetching and extracting…</p>}
|
|
124
|
+
{state.status === 'error' && (
|
|
125
|
+
<pre className='p-2 text-sm text-error whitespace-pre-wrap break-all'>{state.message}</pre>
|
|
126
|
+
)}
|
|
127
|
+
</div>
|
|
128
|
+
)}
|
|
129
|
+
</Panel.Content>
|
|
130
|
+
</Panel.Root>
|
|
131
|
+
);
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
type ResultViewProps = {
|
|
135
|
+
article: ExtractedArticle;
|
|
136
|
+
sourceLength: number;
|
|
137
|
+
showMarkdown: boolean;
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
const ResultView = composable<HTMLDivElement, ResultViewProps>(
|
|
141
|
+
({ article, sourceLength, showMarkdown, ...props }, forwardedRef) => {
|
|
142
|
+
const post = useMemo(
|
|
143
|
+
() =>
|
|
144
|
+
Subscription.makePost({
|
|
145
|
+
title: article.title,
|
|
146
|
+
author: article.author,
|
|
147
|
+
published: article.published,
|
|
148
|
+
// The story renders the extracted body via `description`; the new
|
|
149
|
+
// Post schema no longer carries `content`/`imageUrl` — those live
|
|
150
|
+
// on Subscription.postState now. The storybook flow is a
|
|
151
|
+
// standalone preview (no Subscription), so we just feed the body
|
|
152
|
+
// through `description`.
|
|
153
|
+
description: article.markdown,
|
|
154
|
+
}),
|
|
155
|
+
[article],
|
|
156
|
+
);
|
|
157
|
+
|
|
158
|
+
const metadata = [
|
|
159
|
+
article.domain,
|
|
160
|
+
article.wordCount && `${article.wordCount.toLocaleString()} words`,
|
|
161
|
+
`source ${formatBytes(sourceLength)}`,
|
|
162
|
+
`${article.imageUrls.length} images`,
|
|
163
|
+
].filter((value): value is string => Boolean(value));
|
|
164
|
+
|
|
165
|
+
if (showMarkdown) {
|
|
166
|
+
return (
|
|
167
|
+
<ScrollArea.Root {...props} orientation='vertical' thin ref={forwardedRef}>
|
|
168
|
+
<ScrollArea.Viewport>
|
|
169
|
+
<SyntaxHighlighter language='markdown' classNames='m-4'>
|
|
170
|
+
{article.markdown}
|
|
171
|
+
</SyntaxHighlighter>
|
|
172
|
+
</ScrollArea.Viewport>
|
|
173
|
+
</ScrollArea.Root>
|
|
174
|
+
);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
return <PostContent {...props} ref={forwardedRef} post={post} metadata={metadata} />;
|
|
178
|
+
},
|
|
179
|
+
);
|
|
180
|
+
|
|
181
|
+
ResultView.displayName = 'ResultView';
|
|
182
|
+
|
|
183
|
+
const meta = {
|
|
184
|
+
title: 'plugins/plugin-magazine/stories/ArticleExtractor',
|
|
185
|
+
render: DefaultStory,
|
|
186
|
+
decorators: [withTheme(), withLayout({ layout: 'column' })],
|
|
187
|
+
parameters: {
|
|
188
|
+
layout: 'fullscreen',
|
|
189
|
+
translations,
|
|
190
|
+
},
|
|
191
|
+
} satisfies Meta<typeof DefaultStory>;
|
|
192
|
+
|
|
193
|
+
export default meta;
|
|
194
|
+
|
|
195
|
+
type Story = StoryObj<typeof meta>;
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Live preview of the defuddle-based article extractor.
|
|
199
|
+
*
|
|
200
|
+
* Paste a URL → the storybook-react vite middleware proxies the fetch via
|
|
201
|
+
* `/api/rss?url=` to bypass browser CORS → the HTML is run through
|
|
202
|
+
* `extractArticle` → the result is rendered with `MarkdownView`.
|
|
203
|
+
*/
|
|
204
|
+
export const Default: Story = {};
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2026 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { type Meta, type StoryObj } from '@storybook/react-vite';
|
|
6
|
+
import * as Effect from 'effect/Effect';
|
|
7
|
+
import * as Layer from 'effect/Layer';
|
|
8
|
+
import React from 'react';
|
|
9
|
+
import { expect, userEvent, waitFor, within } from 'storybook/test';
|
|
10
|
+
|
|
11
|
+
import { AiService } from '@dxos/ai';
|
|
12
|
+
import { AiServiceTestingPreset } from '@dxos/ai/testing';
|
|
13
|
+
import { ActivationEvents, Capabilities, Capability, Plugin } from '@dxos/app-framework';
|
|
14
|
+
import { withPluginManager } from '@dxos/app-framework/testing';
|
|
15
|
+
import { Surface } from '@dxos/app-framework/ui';
|
|
16
|
+
import { AppPlugin } from '@dxos/app-toolkit';
|
|
17
|
+
import { AppSurface } from '@dxos/app-toolkit/ui';
|
|
18
|
+
import { AgentHandlers } from '@dxos/assistant-toolkit';
|
|
19
|
+
import { type Client } from '@dxos/client';
|
|
20
|
+
import { type Space } from '@dxos/client/echo';
|
|
21
|
+
import { LayerSpec } from '@dxos/compute';
|
|
22
|
+
import { DXN, Feed, Filter, Ref } from '@dxos/echo';
|
|
23
|
+
import { ClientPlugin, initializeIdentity } from '@dxos/plugin-client/testing';
|
|
24
|
+
import { RoutinePlugin } from '@dxos/plugin-routine/plugin';
|
|
25
|
+
import { SpacePlugin } from '@dxos/plugin-space/testing';
|
|
26
|
+
import { StorybookPlugin, corePlugins } from '@dxos/plugin-testing';
|
|
27
|
+
import { useQuery, useSpaces } from '@dxos/react-client/echo';
|
|
28
|
+
import { Panel } from '@dxos/react-ui';
|
|
29
|
+
import { Loading, withLayout } from '@dxos/react-ui/testing';
|
|
30
|
+
import { Text } from '@dxos/schema';
|
|
31
|
+
|
|
32
|
+
import { MagazineSkill } from '#skills';
|
|
33
|
+
import { translations } from '#translations';
|
|
34
|
+
import { Magazine, Subscription } from '#types';
|
|
35
|
+
|
|
36
|
+
import { MagazineArticle } from '../containers/MagazineArticle/MagazineArticle';
|
|
37
|
+
import { MagazinePlugin } from '../MagazinePlugin';
|
|
38
|
+
|
|
39
|
+
// Curation runs the agent (CurateMagazine → RunInstructions). The process-manager runtime therefore needs
|
|
40
|
+
// the full agent stack: RoutinePlugin supplies the OpaqueToolkit / Registry / Trace LayerSpecs and
|
|
41
|
+
// MagazinePlugin the curation handlers; this story-local plugin adds the RunInstructions handler and a
|
|
42
|
+
// live edge AiService (the 'edge-remote' testing preset — no client edge-config or credentials needed).
|
|
43
|
+
// Excluded from CI (the `Test` story is `!test`); intended for manual, signed-in, online use.
|
|
44
|
+
const aiServiceSpec = LayerSpec.make({ affinity: 'space', requires: [], provides: [AiService.AiService] }, () =>
|
|
45
|
+
AiServiceTestingPreset('edge-remote').pipe(Layer.orDie),
|
|
46
|
+
);
|
|
47
|
+
|
|
48
|
+
const AgentRuntimePlugin = Plugin.define(
|
|
49
|
+
Plugin.makeMeta({ key: DXN.make('org.dxos.plugin.magazineStoryAgent'), name: 'Magazine Story Agent Runtime' }),
|
|
50
|
+
).pipe(
|
|
51
|
+
AppPlugin.addOperationHandlerModule({
|
|
52
|
+
activate: Capability.makeModule(() =>
|
|
53
|
+
Effect.succeed([Capability.contributes(Capabilities.OperationHandler, AgentHandlers)]),
|
|
54
|
+
),
|
|
55
|
+
}),
|
|
56
|
+
Plugin.addModule({
|
|
57
|
+
id: 'ai-service',
|
|
58
|
+
activatesOn: ActivationEvents.SetupProcessManager,
|
|
59
|
+
activate: Capability.makeModule(() =>
|
|
60
|
+
Effect.succeed([Capability.contributes(Capabilities.LayerSpec, aiServiceSpec)]),
|
|
61
|
+
),
|
|
62
|
+
}),
|
|
63
|
+
Plugin.make,
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
// Source the magazine from the live The Register "AI + ML" feed.
|
|
67
|
+
const REGISTER_FEED_URL =
|
|
68
|
+
'https://api.theregister.com/api/v1/article?query=tag:%22ai%20and%20ml%22&orderBy=published&site_id=2&remapper=rss&limit=25';
|
|
69
|
+
|
|
70
|
+
// Mock fetcher: serve the bundled fixture instead of hitting the network so the
|
|
71
|
+
// story is deterministic offline. Matches both the direct URL (seed) and the
|
|
72
|
+
// browser CORS-proxied form (`/api/rss?url=<encoded>`) used by SyncFeed; the
|
|
73
|
+
// encoded URL still contains the literal `theregister.com`. Other URLs delegate
|
|
74
|
+
// to the original fetch.
|
|
75
|
+
// eslint-disable-next-line no-restricted-globals
|
|
76
|
+
const originalFetch = globalThis.fetch.bind(globalThis);
|
|
77
|
+
|
|
78
|
+
const installRegisterFetchMock = async () => {
|
|
79
|
+
const { default: registerFeedXml } = await import('./fixtures/theregister-ai.xml?raw');
|
|
80
|
+
const registerFetchMock: typeof fetch = async (input, init) => {
|
|
81
|
+
const url = typeof input === 'string' ? input : input instanceof URL ? input.toString() : input.url;
|
|
82
|
+
if (url.includes('theregister.com')) {
|
|
83
|
+
return new Response(registerFeedXml, { status: 200, headers: { 'content-type': 'application/rss+xml' } });
|
|
84
|
+
}
|
|
85
|
+
return originalFetch(input, init);
|
|
86
|
+
};
|
|
87
|
+
globalThis.fetch = registerFetchMock;
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
// Bridge from the seed (runs in the plugin-manager/client-init context) to the `Curate` play test,
|
|
91
|
+
// which reads ECHO state directly. A module-level variable does not survive Storybook's seed↔play
|
|
92
|
+
// boundary, so a shared global is used (mirrors plugin-space/plugin-debug story seeds).
|
|
93
|
+
declare global {
|
|
94
|
+
// eslint-disable-next-line no-var
|
|
95
|
+
var __magazineStoryContext: { space: Space; magazine: Magazine.Magazine } | undefined;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const DefaultStory = () => {
|
|
99
|
+
const spaces = useSpaces();
|
|
100
|
+
const space = spaces[spaces.length - 1];
|
|
101
|
+
const magazines = useQuery(space?.db, Filter.type(Magazine.Magazine));
|
|
102
|
+
const magazine = magazines[0];
|
|
103
|
+
if (!magazine) {
|
|
104
|
+
return <Loading />;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return (
|
|
108
|
+
<Panel.Root classNames='border-is border-separator'>
|
|
109
|
+
<Panel.Content className='px-3 grid grid-cols-2 gap-3'>
|
|
110
|
+
{/* Rendered directly: the `article` role is shared with plugin-space's catch-all RecordArticle
|
|
111
|
+
(position:last), so a raw `Surface type={Article}` here is ambiguous and resolves to the
|
|
112
|
+
fallback — the deck disambiguates via the app-graph node, which a story has no equivalent of. */}
|
|
113
|
+
<MagazineArticle role='article' subject={magazine} attendableId='story' />
|
|
114
|
+
{/* The object-properties companion surface (plugin-space DefaultProperties); 'settings' is unambiguous. */}
|
|
115
|
+
<Surface.Surface type={AppSurface.Article} data={{ subject: 'settings', companionTo: magazine }} />
|
|
116
|
+
</Panel.Content>
|
|
117
|
+
</Panel.Root>
|
|
118
|
+
);
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
const seedRegisterMagazine = ({ client }: { client: Client }) =>
|
|
122
|
+
Effect.gen(function* () {
|
|
123
|
+
globalThis.__magazineStoryContext = undefined;
|
|
124
|
+
yield* Effect.promise(() => installRegisterFetchMock());
|
|
125
|
+
yield* initializeIdentity(client);
|
|
126
|
+
const space = (yield* Effect.promise(() => client.spaces.create())) as Space;
|
|
127
|
+
yield* Effect.promise(() => space.waitUntilReady());
|
|
128
|
+
|
|
129
|
+
// Feed points at the real Register AI/ML URL; the mock fetcher serves the
|
|
130
|
+
// bundled fixture. Posts are NOT pre-seeded — clicking Curate drives the live
|
|
131
|
+
// flow (SyncFeed fetches + parses, then CurateMagazine runs the Routine via the
|
|
132
|
+
// agent to select Posts into `magazine.posts`).
|
|
133
|
+
const feed = space.db.add(
|
|
134
|
+
Subscription.makeSubscription({ name: 'The Register — AI + ML', url: REGISTER_FEED_URL, type: 'rss' }),
|
|
135
|
+
);
|
|
136
|
+
|
|
137
|
+
const magazine = Magazine.make({ name: 'AI', feeds: [Ref.make(feed)] });
|
|
138
|
+
space.db.add(magazine);
|
|
139
|
+
// Curation resolves the base methodology skill from the registry; register it here since the
|
|
140
|
+
// automation plugin (which normally syncs SkillDefinition capabilities) isn't in this story.
|
|
141
|
+
client.graph.registry.add([MagazineSkill.make()]);
|
|
142
|
+
yield* Effect.promise(() => space.db.flush());
|
|
143
|
+
|
|
144
|
+
globalThis.__magazineStoryContext = { space, magazine };
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
const meta: Meta<typeof DefaultStory> = {
|
|
148
|
+
title: 'plugins/plugin-magazine/stories/MagazineCurate',
|
|
149
|
+
component: DefaultStory,
|
|
150
|
+
decorators: [
|
|
151
|
+
withLayout({ layout: 'fullscreen' }),
|
|
152
|
+
withPluginManager({
|
|
153
|
+
plugins: [
|
|
154
|
+
...corePlugins(),
|
|
155
|
+
ClientPlugin({
|
|
156
|
+
types: [Feed.Feed, Subscription.Subscription, Subscription.Post, Magazine.Magazine, Text.Text],
|
|
157
|
+
onClientInitialized: seedRegisterMagazine,
|
|
158
|
+
}),
|
|
159
|
+
SpacePlugin({}),
|
|
160
|
+
StorybookPlugin({}),
|
|
161
|
+
RoutinePlugin(),
|
|
162
|
+
MagazinePlugin(),
|
|
163
|
+
AgentRuntimePlugin(),
|
|
164
|
+
],
|
|
165
|
+
}),
|
|
166
|
+
],
|
|
167
|
+
parameters: {
|
|
168
|
+
layout: 'fullscreen',
|
|
169
|
+
translations,
|
|
170
|
+
},
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
export default meta;
|
|
174
|
+
|
|
175
|
+
type Story = StoryObj<typeof meta>;
|
|
176
|
+
|
|
177
|
+
export const Default: Story = {};
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Live, agent-driven curation end-to-end against the The Register "AI + ML" feed (the feed fetch is
|
|
181
|
+
* served from a bundled fixture, but the LLM call is real). It verifies:
|
|
182
|
+
* 1. the curation Routine exists — created with the magazine (Magazine.make), so it is present
|
|
183
|
+
* before any curate; and
|
|
184
|
+
* 2. clicking Curate pulls the feed (SyncFeed) and runs that Routine via RunInstructions against the
|
|
185
|
+
* live edge model, which selects matching Posts into `magazine.posts` (rendered as tiles).
|
|
186
|
+
*
|
|
187
|
+
* The seed stashes the space + magazine in module scope so the play can read ECHO state directly.
|
|
188
|
+
*
|
|
189
|
+
* Excluded from CI (`!test`) — needs network + a reachable edge model and is non-deterministic/slow;
|
|
190
|
+
* run it manually (signed in, online). Deterministic pieces are covered by `theregister-fixture.test.ts`
|
|
191
|
+
* (fetch → parse), `curate-magazine.test.ts`, and `Magazine.test.ts`; the agent path by
|
|
192
|
+
* `curate-magazine.skill.test.ts`.
|
|
193
|
+
*/
|
|
194
|
+
export const Curate: Story = {
|
|
195
|
+
tags: ['!test'],
|
|
196
|
+
play: async ({ canvasElement }) => {
|
|
197
|
+
const canvas = within(canvasElement);
|
|
198
|
+
// The seed sets the context at the end of its async init, which can land just after the magazine
|
|
199
|
+
// first renders — so wait for it rather than reading once.
|
|
200
|
+
await waitFor(() => expect(globalThis.__magazineStoryContext, 'story seed did not run').toBeDefined(), {
|
|
201
|
+
timeout: 15_000,
|
|
202
|
+
});
|
|
203
|
+
const { magazine } = globalThis.__magazineStoryContext!;
|
|
204
|
+
|
|
205
|
+
// The curation Routine is created with the magazine, not on curate.
|
|
206
|
+
await expect(magazine.instructions).toBeDefined();
|
|
207
|
+
|
|
208
|
+
// Starts empty; clicking Curate pulls the feed and runs the Routine.
|
|
209
|
+
await expect(await canvas.findByText(/no articles yet/i, {}, { timeout: 10_000 })).toBeVisible();
|
|
210
|
+
await userEvent.click(await canvas.findByRole('button', { name: /^curate$/i }));
|
|
211
|
+
|
|
212
|
+
// Feed pulled + Routine run: the agent (live edge model) selects synced candidates into the
|
|
213
|
+
// magazine — tiles replace the empty state.
|
|
214
|
+
await waitFor(() => expect(magazine.posts.length).toBeGreaterThan(0), { timeout: 120_000 });
|
|
215
|
+
await waitFor(() => expect(canvas.queryByText(/no articles yet/i)).toBeNull(), { timeout: 5_000 });
|
|
216
|
+
},
|
|
217
|
+
};
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
2
|
+
<rss version="2.0" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/">
|
|
3
|
+
<channel>
|
|
4
|
+
<title>The Register - AI + ML</title>
|
|
5
|
+
<link>https://www.theregister.com/software/ai_ml/</link>
|
|
6
|
+
<description>Biting the hand that feeds IT — AI and machine learning coverage.</description>
|
|
7
|
+
<language>en-us</language>
|
|
8
|
+
<item>
|
|
9
|
+
<title>Sovereign AI push has nations racing to build domestic compute</title>
|
|
10
|
+
<link>https://www.theregister.com/2026/05/28/sovereign_ai_compute/</link>
|
|
11
|
+
<dc:creator>Tobias Mann</dc:creator>
|
|
12
|
+
<pubDate>Wed, 28 May 2026 09:30:00 GMT</pubDate>
|
|
13
|
+
<guid isPermaLink="true">https://www.theregister.com/2026/05/28/sovereign_ai_compute/</guid>
|
|
14
|
+
<description><![CDATA[<p>Governments from the EU to the Gulf are pouring billions into <strong>sovereign AI</strong> infrastructure, determined to keep models, data, and GPUs within national borders.</p>]]></description>
|
|
15
|
+
<content:encoded><![CDATA[<p>Governments from the EU to the Gulf are pouring billions into <strong>sovereign AI</strong> infrastructure, determined to keep models, data, and GPUs within national borders. Analysts say the trend marks a decisive break from the cloud-centric assumptions of the last decade.</p><p>The shift is reshaping procurement, with state-backed clouds demanding on-shore training runs and audited supply chains.</p>]]></content:encoded>
|
|
16
|
+
</item>
|
|
17
|
+
<item>
|
|
18
|
+
<title>Open-weights model claims parity with frontier labs at a fraction of the cost</title>
|
|
19
|
+
<link>https://www.theregister.com/2026/05/27/open_weights_frontier/</link>
|
|
20
|
+
<dc:creator>Katyanna Quach</dc:creator>
|
|
21
|
+
<pubDate>Tue, 27 May 2026 14:05:00 GMT</pubDate>
|
|
22
|
+
<guid isPermaLink="true">https://www.theregister.com/2026/05/27/open_weights_frontier/</guid>
|
|
23
|
+
<description><![CDATA[<p>A new open-weights release is the latest to argue that smaller, cheaper models can match the big labs on reasoning benchmarks.</p>]]></description>
|
|
24
|
+
<content:encoded><![CDATA[<p>A new open-weights release is the latest to argue that smaller, cheaper models can match the big labs on reasoning benchmarks — and it ships under a permissive licence.</p>]]></content:encoded>
|
|
25
|
+
</item>
|
|
26
|
+
<item>
|
|
27
|
+
<title>Datacenter power demand from AI training forces grid rethink</title>
|
|
28
|
+
<link>https://www.theregister.com/2026/05/26/ai_datacenter_power/</link>
|
|
29
|
+
<dc:creator>Dan Robinson</dc:creator>
|
|
30
|
+
<pubDate>Mon, 26 May 2026 11:20:00 GMT</pubDate>
|
|
31
|
+
<guid isPermaLink="true">https://www.theregister.com/2026/05/26/ai_datacenter_power/</guid>
|
|
32
|
+
<description><![CDATA[<p>Utilities warn that the appetite of AI training clusters is outpacing new generation capacity in several regions.</p>]]></description>
|
|
33
|
+
<content:encoded><![CDATA[<p>Utilities warn that the appetite of AI training clusters is outpacing new generation capacity, pushing operators toward on-site generation and long-term nuclear deals.</p>]]></content:encoded>
|
|
34
|
+
</item>
|
|
35
|
+
<item>
|
|
36
|
+
<title>Regulators circle AI agents as autonomous tooling spreads</title>
|
|
37
|
+
<link>https://www.theregister.com/2026/05/25/ai_agent_regulation/</link>
|
|
38
|
+
<dc:creator>Brandon Vigliarolo</dc:creator>
|
|
39
|
+
<pubDate>Sun, 25 May 2026 08:00:00 GMT</pubDate>
|
|
40
|
+
<guid isPermaLink="true">https://www.theregister.com/2026/05/25/ai_agent_regulation/</guid>
|
|
41
|
+
<description><![CDATA[<p>As agentic systems take actions on users' behalf, lawmakers are weighing liability and audit requirements.</p>]]></description>
|
|
42
|
+
<content:encoded><![CDATA[<p>As agentic systems take actions on users' behalf, lawmakers are weighing liability and audit requirements for the software that decides and acts.</p>]]></content:encoded>
|
|
43
|
+
</item>
|
|
44
|
+
</channel>
|
|
45
|
+
</rss>
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2026 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { afterEach, beforeEach, describe, test, vi } from 'vitest';
|
|
6
|
+
|
|
7
|
+
import { EffectEx } from '@dxos/effect';
|
|
8
|
+
|
|
9
|
+
import { fetchRss } from '../operations/sources';
|
|
10
|
+
import registerFeedXml from './fixtures/theregister-ai.xml?raw';
|
|
11
|
+
|
|
12
|
+
const REGISTER_FEED_URL = 'https://api.theregister.com/api/v1/article?remapper=rss';
|
|
13
|
+
|
|
14
|
+
describe('theregister fixture', () => {
|
|
15
|
+
beforeEach(() => {
|
|
16
|
+
vi.stubGlobal(
|
|
17
|
+
'fetch',
|
|
18
|
+
vi.fn(
|
|
19
|
+
async () => new Response(registerFeedXml, { status: 200, headers: { 'content-type': 'application/rss+xml' } }),
|
|
20
|
+
),
|
|
21
|
+
);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
afterEach(() => {
|
|
25
|
+
vi.unstubAllGlobals();
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
test('parses into Posts via the mock fetcher', async ({ expect }) => {
|
|
29
|
+
const { feed, posts } = await EffectEx.runPromise(fetchRss(REGISTER_FEED_URL));
|
|
30
|
+
expect(feed.name).toMatch(/register/i);
|
|
31
|
+
expect(posts.length).toBe(4);
|
|
32
|
+
expect(posts.some((post) => /sovereign ai/i.test(post.title ?? ''))).toBe(true);
|
|
33
|
+
expect(posts.every((post) => Boolean(post.description))).toBe(true);
|
|
34
|
+
});
|
|
35
|
+
});
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2026 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import * as Effect from 'effect/Effect';
|
|
6
|
+
|
|
7
|
+
import { Trigger } from '@dxos/compute';
|
|
8
|
+
import { Ref } from '@dxos/echo';
|
|
9
|
+
import { invariant } from '@dxos/invariant';
|
|
10
|
+
import { Routine, type RoutineCapabilities } from '@dxos/plugin-routine/types';
|
|
11
|
+
|
|
12
|
+
import { FeedOperation, Magazine } from '#types';
|
|
13
|
+
|
|
14
|
+
/** Default cron for a magazine curation routine: daily at 9 AM. The user edits the schedule from the trigger. */
|
|
15
|
+
const DEFAULT_CRON = '0 9 * * *';
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Magazine curation template. Creates a routine that invokes {@link FeedOperation.CurateMagazine} for
|
|
19
|
+
* the companion's Magazine subject on a daily schedule. Only visible in Magazine object companions.
|
|
20
|
+
*/
|
|
21
|
+
export const magazineCuration: RoutineCapabilities.Template = {
|
|
22
|
+
id: 'org.dxos.routine.magazineCuration',
|
|
23
|
+
label: 'Curate Magazine',
|
|
24
|
+
icon: 'ph--sparkle--regular',
|
|
25
|
+
appliesTo: (subject) => subject != null && Magazine.instanceOf(subject),
|
|
26
|
+
scaffold: ({ name, subject }) =>
|
|
27
|
+
Effect.gen(function* () {
|
|
28
|
+
invariant(
|
|
29
|
+
subject != null && Magazine.instanceOf(subject),
|
|
30
|
+
'Magazine curation template requires a Magazine subject.',
|
|
31
|
+
);
|
|
32
|
+
|
|
33
|
+
const magazine = subject;
|
|
34
|
+
|
|
35
|
+
// Pre-populate the trigger's input so the magazine binding is preserved through the save flow.
|
|
36
|
+
return Routine.make({
|
|
37
|
+
name: name ?? magazine.name ?? 'Curate Magazine',
|
|
38
|
+
// Bind the CurateMagazine operation directly as the action (an operation action, not instructions-based).
|
|
39
|
+
spec: { kind: 'runnable', runnable: Ref.fromURI(FeedOperation.CurateMagazine.meta.key) },
|
|
40
|
+
trigger: Trigger.make({
|
|
41
|
+
enabled: false,
|
|
42
|
+
spec: Trigger.specTimer(DEFAULT_CRON),
|
|
43
|
+
input: { magazine: Ref.make(magazine) },
|
|
44
|
+
}),
|
|
45
|
+
});
|
|
46
|
+
}),
|
|
47
|
+
};
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2025 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { subDays } from 'date-fns';
|
|
6
|
+
|
|
7
|
+
import { Ref } from '@dxos/echo';
|
|
8
|
+
import { random } from '@dxos/random';
|
|
9
|
+
|
|
10
|
+
import { Magazine, Subscription } from '#types';
|
|
11
|
+
|
|
12
|
+
/** Generates an array of random posts. */
|
|
13
|
+
export const generatePosts = (count: number): Subscription.Post[] => {
|
|
14
|
+
const now = new Date();
|
|
15
|
+
const start = subDays(now, 30);
|
|
16
|
+
const rangeMs = now.getTime() - start.getTime();
|
|
17
|
+
|
|
18
|
+
return Array.from({ length: count }, () => {
|
|
19
|
+
const published = new Date(start.getTime() + Math.random() * rangeMs);
|
|
20
|
+
return Subscription.makePost({
|
|
21
|
+
title: random.lorem.sentence(random.number.int({ min: 4, max: 10 })),
|
|
22
|
+
link: random.internet.url(),
|
|
23
|
+
description: random.lorem.paragraph(random.number.int({ min: 1, max: 3 })),
|
|
24
|
+
author: random.person.fullName(),
|
|
25
|
+
published: published.toISOString(),
|
|
26
|
+
guid: random.string.uuid(),
|
|
27
|
+
});
|
|
28
|
+
}).sort((postA, postB) => (postB.published ?? '').localeCompare(postA.published ?? ''));
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
/** Generates a random subscription feed. */
|
|
32
|
+
export const generateFeed = (
|
|
33
|
+
props: Partial<{ name: string; url: string; description: string }> = {},
|
|
34
|
+
): Subscription.Subscription =>
|
|
35
|
+
Subscription.makeSubscription({
|
|
36
|
+
name: props.name ?? random.company.name() + ' Blog',
|
|
37
|
+
url: props.url ?? random.internet.url(),
|
|
38
|
+
description: props.description ?? random.lorem.sentence(),
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Generates a Post. Note: per-Post state (snippet, imageUrl, readAt) is no
|
|
43
|
+
* longer on the Post itself — it lives on the source Subscription's or
|
|
44
|
+
* Magazine's `postState` map. The caller is responsible for seeding those if
|
|
45
|
+
* the story / test needs them. The `read` / `imageUrl` params are kept on
|
|
46
|
+
* the signature for back-compat call sites; they are currently no-ops and
|
|
47
|
+
* will be wired through state-seeding helpers in a follow-up.
|
|
48
|
+
*/
|
|
49
|
+
export const generateCuratedPost = (_props: { imageUrl?: string; read?: boolean } = {}): Subscription.Post => {
|
|
50
|
+
const description = random.lorem.paragraph(random.number.int({ min: 2, max: 4 }));
|
|
51
|
+
// TODO: seed Magazine.postState / Subscription.postState here once the
|
|
52
|
+
// builder has access to them; for now Posts are bare feed entries and
|
|
53
|
+
// tile snippets are recomputed on render from `description`.
|
|
54
|
+
return Subscription.makePost({
|
|
55
|
+
title: random.lorem.sentence(random.number.int({ min: 4, max: 10 })),
|
|
56
|
+
link: random.internet.url(),
|
|
57
|
+
description,
|
|
58
|
+
author: random.person.fullName(),
|
|
59
|
+
published: new Date(Date.now() - Math.random() * 7 * 24 * 3600 * 1000).toISOString(),
|
|
60
|
+
guid: random.string.uuid(),
|
|
61
|
+
});
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
/** Generates a Magazine with the given curated posts. Feeds default to empty. */
|
|
65
|
+
export const generateMagazine = (
|
|
66
|
+
props: Partial<{
|
|
67
|
+
name: string;
|
|
68
|
+
feeds: Subscription.Subscription[];
|
|
69
|
+
posts: Subscription.Post[];
|
|
70
|
+
}> = {},
|
|
71
|
+
): Magazine.Magazine => {
|
|
72
|
+
return Magazine.make({
|
|
73
|
+
name: props.name ?? random.company.name() + ' Reading List',
|
|
74
|
+
feeds: (props.feeds ?? []).map((feed) => Ref.make(feed)),
|
|
75
|
+
posts: (props.posts ?? []).map((post) => Ref.make(post)),
|
|
76
|
+
});
|
|
77
|
+
};
|