@recursiv/sdk 0.7.72 → 0.7.73

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/README.md CHANGED
@@ -468,6 +468,17 @@ opaque; topic catalogs have no pagination and the Mastodon post window stops at
468
468
  current human project-bound app key. Nostr does not advertise trends. Provider
469
469
  failures propagate as SDK errors, and neither method ingests or publishes posts.
470
470
 
471
+ A clip player scoped to one network asks for its video window instead (0.7.73):
472
+ `getPublicTrends({ protocol: 'bluesky', limit: 25, media: 'video', cursor })`, or keyless
473
+ `getSharedTrends(projectId, { protocol: 'bluesky', limit: 25, media: 'video', cursor })`.
474
+ The answer is a `kind: 'posts'` page holding only posts that carry a video (Bluesky: What's
475
+ Hot's videos, one provider request a page), paged by its opaque cursor until it is `null`;
476
+ a page may be empty while its cursor goes on. It takes no `category` and judges no layout;
477
+ `compass: 'hide'` (0.7.72) leaves the political clips out of it as it does from trends.
478
+ A signed-out read holds only clips whose video the visitor may see, and counts the rest in
479
+ `mediaFilter.withheld`. A network without a video window answers 404 `unsupported_protocol`;
480
+ an API that predates `media` answers 400 `invalid_input`, so fall back to its trends.
481
+
471
482
  ### Native Nostr signing
472
483
 
473
484
  Use `protocols.nativeNostrAccount()` to check the app's explicit native-write
@@ -1289,6 +1289,11 @@ export type SocialTrends = SocialTrendProvenance & ({
1289
1289
  safetyFilter?: {
1290
1290
  withheld: number;
1291
1291
  };
1292
+ /** A `media: 'video'` page read signed out: the clips left off because the visitor may not see their video yet (counts only). */
1293
+ mediaFilter?: {
1294
+ media: 'video';
1295
+ withheld: number;
1296
+ };
1292
1297
  }) & {
1293
1298
  /** Present when `getPublicTrends` named a filtering `compass`: what Hide politics did to this page (counts only). */
1294
1299
  meta?: {
@@ -1773,6 +1778,11 @@ export declare class ProtocolsResource {
1773
1778
  * out the topics and posts whose cached browse category is politics, for every visitor, and the page says what it did
1774
1779
  * in `meta.compass_filter` (`basis: 'flags'` until the operator serves visitors the full filter). An API older than
1775
1780
  * 0.7.72 refuses `compass` here with a 400.
1781
+ * `media: 'video'` (0.7.73; Bluesky) reads the network's own window of video posts instead of its trends: a `posts`
1782
+ * page paged by `cursor`, with no category and no layout, and `compass: 'hide'` applies to it as to trends. A visitor
1783
+ * is served only the clips whose video they may see; `mediaFilter.withheld` counts the rest. A network without a video
1784
+ * window answers 404 `unsupported_protocol`, and an API that predates the parameter 400 `invalid_input`: read its
1785
+ * trends instead.
1776
1786
  */
1777
1787
  getSharedTrends(projectId: string, params: {
1778
1788
  protocol: string;
@@ -1782,6 +1792,7 @@ export declare class ProtocolsResource {
1782
1792
  category?: string;
1783
1793
  discovery?: boolean;
1784
1794
  compass?: string;
1795
+ media?: 'video';
1785
1796
  }): Promise<SingleResponse<SocialTrends>>;
1786
1797
  /** Open a provider-listed topic through the same public-only app boundary, with the same safety floor. */
1787
1798
  getSharedTrendFeed(projectId: string, params: {
@@ -1850,7 +1861,9 @@ export declare class ProtocolsResource {
1850
1861
  * choice, spelled as `posts.list` takes it): Hide politics (`hide`) leaves out the topics and posts whose cached
1851
1862
  * browse category is politics, for every member (from 0.7.72; before, only where the filter was served to them), with
1852
1863
  * `meta.compass_filter.basis` 'flags' where the full filter is not served; a side or Mix is answered unfiltered. The
1853
- * page then says what it did in `meta.compass_filter`.
1864
+ * page then says what it did in `meta.compass_filter`. `media: 'video'` (0.7.73; Bluesky) reads the network's own window
1865
+ * of video posts instead of its trends, as `getSharedTrends` does, with every video kept for a member and Hide politics
1866
+ * applied where asked.
1854
1867
  */
1855
1868
  getPublicTrends(params: {
1856
1869
  protocol: string;
@@ -1860,6 +1873,7 @@ export declare class ProtocolsResource {
1860
1873
  category?: string;
1861
1874
  discovery?: boolean;
1862
1875
  compass?: string;
1876
+ media?: 'video';
1863
1877
  }, options?: SocialReadOptions): Promise<SingleResponse<SocialTrends>>;
1864
1878
  /** Open a topic currently listed by the provider. Use its nativeId as topic and preserve the opaque cursor. Screened like `getPublicTrends`. */
1865
1879
  getPublicTrendFeed(params: {
@@ -128,10 +128,16 @@ export class ProtocolsResource {
128
128
  * out the topics and posts whose cached browse category is politics, for every visitor, and the page says what it did
129
129
  * in `meta.compass_filter` (`basis: 'flags'` until the operator serves visitors the full filter). An API older than
130
130
  * 0.7.72 refuses `compass` here with a 400.
131
+ * `media: 'video'` (0.7.73; Bluesky) reads the network's own window of video posts instead of its trends: a `posts`
132
+ * page paged by `cursor`, with no category and no layout, and `compass: 'hide'` applies to it as to trends. A visitor
133
+ * is served only the clips whose video they may see; `mediaFilter.withheld` counts the rest. A network without a video
134
+ * window answers 404 `unsupported_protocol`, and an API that predates the parameter 400 `invalid_input`: read its
135
+ * trends instead.
131
136
  */
132
137
  getSharedTrends(projectId, params) {
133
138
  return this.client.get(`/protocols/public/${encodeURIComponent(projectId)}/trends`, { protocol: params.protocol, limit: params.limit, cursor: params.cursor,
134
- includePreviews: params.includePreviews, category: params.category, discovery: params.discovery, compass: params.compass });
139
+ includePreviews: params.includePreviews, category: params.category, discovery: params.discovery, compass: params.compass,
140
+ ...(params.media !== undefined ? { media: params.media } : {}) });
135
141
  }
136
142
  /** Open a provider-listed topic through the same public-only app boundary, with the same safety floor. */
137
143
  getSharedTrendFeed(projectId, params) {
@@ -213,14 +219,17 @@ export class ProtocolsResource {
213
219
  * choice, spelled as `posts.list` takes it): Hide politics (`hide`) leaves out the topics and posts whose cached
214
220
  * browse category is politics, for every member (from 0.7.72; before, only where the filter was served to them), with
215
221
  * `meta.compass_filter.basis` 'flags' where the full filter is not served; a side or Mix is answered unfiltered. The
216
- * page then says what it did in `meta.compass_filter`.
222
+ * page then says what it did in `meta.compass_filter`. `media: 'video'` (0.7.73; Bluesky) reads the network's own window
223
+ * of video posts instead of its trends, as `getSharedTrends` does, with every video kept for a member and Hide politics
224
+ * applied where asked.
217
225
  */
218
226
  getPublicTrends(params, options) {
219
227
  return this.client.get('/protocols/social/trends', { protocol: params.protocol, limit: params.limit, cursor: params.cursor,
220
228
  ...(params.includePreviews !== undefined ? { includePreviews: params.includePreviews } : {}),
221
229
  ...(params.category !== undefined ? { category: params.category } : {}),
222
230
  ...(params.discovery !== undefined ? { discovery: params.discovery } : {}),
223
- ...(params.compass !== undefined ? { compass: params.compass } : {}) }, ...readOptions(options));
231
+ ...(params.compass !== undefined ? { compass: params.compass } : {}),
232
+ ...(params.media !== undefined ? { media: params.media } : {}) }, ...readOptions(options));
224
233
  }
225
234
  /** Open a topic currently listed by the provider. Use its nativeId as topic and preserve the opaque cursor. Screened like `getPublicTrends`. */
226
235
  getPublicTrendFeed(params, options) {
package/llm.md CHANGED
@@ -832,7 +832,7 @@ await r.appSubscriptions.changeTier({ tier: 'pro', proration_date: quote.prorati
832
832
  - Anonymous discovery: `const visitor = new Recursiv({ anonymous: true, inheritEnvironmentApiKey: false }); const { data } = await visitor.protocols.getSharedTrends(projectId, { protocol: 'nostr', limit: 20, discovery: true })`. Use `sharedNetworks(projectId)` capabilities; open a returned topic with `getSharedTrendFeed(projectId, { protocol: 'bluesky', topic: trend.nativeId, cursor })`. Configured apps only; no account authority or writes. App moderation and per-IP/app rate limits apply. Categories and Jev layouts reuse cached judgments without initiating paid inference; handle partial categories and finite provider windows. Category depth: `r.posts.list({ category: 'science', sort: 'hot', discovery: true })` pages the hot window by offset and then by keyset (`meta.next` is `{ offset }`, `{ cursor }` or `null`; pass a cursor back as `cursor`); when a stage ends, `meta.fallover` lists the next feeds in order (`category_older`, `category_network`, `category` with `via`, `for_you`, `all`; `feed` with `ref` is declared in 0.7.60 but not sent yet, so skip kinds you cannot open). One call for a category's modules: `const { data } = await r.discovery.category({ category: 'science' })` returns `stories`, `network`, `clips`, `people`, `groups`, each `{ items, ms, reason? }`. Watch's Minds videos: `const { data, meta } = await r.discovery.videos({ limit: 12 })` returns public video posts in hot order from a list shared per app (rebuilt about once a minute), each re-checked per call for the viewer; page on `meta.next_cursor`.
833
833
  - Progressive Discover feed (0.7.71): `const feed = r.discovery.createProgressiveFeed({ networks, category, recommend, top: { ref: 'builtin:all' }, seen, seenClusters }); const round = await feed.next(part => draw(part))` opens one `createFeed` session per source (Minds with the radar and suggestions, one per network with `public_trends`) and hands each source's page on as a `DiscoveryPart` (`{ source, round, modules, composed?, mixed? }`) the moment it lands, instead of a page that waits for its slowest network (up to 15 s); a round waits `DISCOVERY_STRAGGLER_MS` past its first answer and rejects only when every source failed. The first load is held until Minds and one other network answered (or `FIRST_SCREEN_MS`, 2.5 s, after the first answer; `firstScreenMs: VISITOR_FIRST_SCREEN_MS` for a visitor) and arrives as one `composed: true` part from `composeFirstScreen`: the top's first post when it is a playable video, else a picture lead from the best 8 by engagement (never a story, rail, video strip, column, `isSensitivePost` post or `linksAdultSite` porn link), then every network's posts by engagement (`DiscoveryEngagementMerge`) with row 2 from another network than the lead's when one is left (`FIRST_SCREEN_NETWORK_ROWS`), a story as row 3 and a rail as row 5, nothing dropped. Merge later `mixed` parts with `mergeDiscoveryArrival(rows, arriving, reachedIds)`. `top`/`seen`/`seenClusters`/`bucket` reach every session; the top is read once per open. Also `retryFailed()`, `onStatus(listener)`, `setNetworks(catalog)`; settings `perSource`, `firstScreen`, `onLanded`. For you: `createProgressiveFeed({ createFeed: o => r.discovery.createPersonalFeed(o) }, options, { perSource: false })`. A session's `seen` now also leaves seen network posts out of trend pages, previews and topic pages; in a category a topic page with nothing new ends its topic and after `DISCOVERY_EMPTY_TOPIC_PAGES` (3) in a row the network's other topics are skipped.
834
834
  - Viral read (0.7.59; off until the server enables it for the app): `const { data } = await r.protocols.getPublicViral({ media: 'video', category: 'science', limit: 20 })` (member key, users:read + posts:read, never cached) or keyless `visitor.protocols.getSharedViral(projectId, { media: 'video', compass: 'hide' })` (visitors send compass `all` or `hide` only). Params, all optional: `network` (all, minds, bluesky, activitypub, nostr), `category`, `media: 'video'`, `limit` 5/10/20/40, `cursor` (the last page's; a cursor whose 30-minute build expired ends the list), `compass`. Each item: `key` (stable; drop ones already shown), `protocol`, `post` (checking `protocol` narrows it: `ViralMindsCard` for minds, open at /post/:id; otherwise a `SocialPost`), `label` viral/rising/hot, `score`, `rank`, `category`, `velocity` (show counts with `countsReportedBy` as reported, never verified), `spread`. A visitor's read serves media only where the server cleared it, so even with `media: 'video'` some items arrive with the video withheld (empty `media`): `import { viralItemHasVideo } from '@recursiv/sdk'` and keep `data.items.filter(viralItemHasVideo)` before a vertical video player. `status` ok/building/screening/off: off resolves with no items, and `getSharedViral` also resolves an older API's 401 `missing_api_key` as off; `getPublicViral` throws every 401/403 (never off), so catch it and skip that step.
835
- - Public trends: require `public_trends` from `publicNetworks()`, then `getPublicTrends({ protocol, limit?, cursor?, compass? })` (`compass: 'hide'` leaves out politics, 0.7.62); render `kind: topics` as source topics and `kind: posts` as source posts. With `trend_feed`, open only a returned current topic using `getPublicTrendFeed({ protocol, topic: trend.nativeId, limit?, cursor? })`. Show provider, observedAt and provider_ranked_window coverage; null rankingWindow is unknown, not a daily/global ranking. Preserve source order, canonical IDs and readRef, and opaque cursors. Topic catalogs do not paginate; Mastodon trends stop at 200 source positions. Both methods require users:read plus posts:read on the current human app key; Nostr trends are unsupported. Propagate errors; trend_unavailable means refresh the topic list. No ingestion or native writes.
835
+ - Public trends: require `public_trends` from `publicNetworks()`, then `getPublicTrends({ protocol, limit?, cursor?, compass?, media? })` (`compass: 'hide'` leaves out politics, 0.7.62; `media: 'video'`, 0.7.73, Bluesky: the network's own window of video posts, a `kind: posts` page paged by its cursor, no category or layout, 404 `unsupported_protocol` for a network without one, 400 on an API that predates it; keyless `getSharedTrends(projectId, { ..., media: 'video' })` holds only clips a visitor may see and counts the rest in `mediaFilter.withheld`); render `kind: topics` as source topics and `kind: posts` as source posts. With `trend_feed`, open only a returned current topic using `getPublicTrendFeed({ protocol, topic: trend.nativeId, limit?, cursor? })`. Show provider, observedAt and provider_ranked_window coverage; null rankingWindow is unknown, not a daily/global ranking. Preserve source order, canonical IDs and readRef, and opaque cursors. Topic catalogs do not paginate; Mastodon trends stop at 200 source positions. Both methods require users:read plus posts:read on the current human app key; Nostr trends are unsupported. Propagate errors; trend_unavailable means refresh the topic list. No ingestion or native writes.
836
836
  - One outlet count per story (0.7.71): a radar story (list, story read, archive), a World Today story (`outlets`, with `outlet_count` its total and `lane_mix` its lanes), a category strip story, a `feeds.top`/`feeds.stories` story and a search story carry `outlets: { total, mainstream, independent, other }`, the distinct outlets of the articles the story lists (an allow-listed outlet once, an unlisted host in `other`; the buckets sum to `total`). Show `outlets.total`; `sources.outlet` counts articles. `DiscoveryCluster.outletCount` reads it. An older server omits it.
837
837
  - Political stories (0.7.67): every radar story (`getPublicRadar`, `getSharedRadar`, a story read) and every World Today story (`r.curator.today()`) carries `political`: true when a quarter of its judged posts are politics, its theme or topic is politics, or its headline or summary names an election, a war or troop deployment, a court or justice, a legislature, or a party or politician. `compass: 'hide'` leaves those out whatever their theme; `r.curator.today({ compass: 'hide' })` does the same to the front page, the next story leads, and `meta.compass_filter` counts what was left out. A story from `r.feeds.stories()` or a `r.feeds.top()` lead carries `political` too, and both reads, and `r.curator.forYouWorld()`, leave political stories out under `compass: 'hide'` whatever their theme. An older server omits `political`: decide by `theme` as before.
838
838
  - Hide politics as a floor (0.7.72): the radar, `r.curator.today()`, `getPublicTrends`, keyless `visitor.protocols.getSharedTrends(projectId, { protocol, compass: 'hide' })` (visitors send `all` or `hide` only; an older API answers 400 to `compass` there), the viral read, `r.discovery.videos` and a visitor's Discover posts apply `compass: 'hide'` for every caller. Where the operator does not serve the caller the full filter, the answer's `meta.compass_filter` is `{ status: 'applied', basis: 'flags', ... }`: left out by the server's own `political` flags and browse categories, no judgment read. Treat `applied` with or without `basis` as filtered. Choose sides and Mix sides still answer `unavailable` until the operator serves them, and the catalog's `compass_filter` capability still says when to offer Politics in Filters.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@recursiv/sdk",
3
- "version": "0.7.72",
3
+ "version": "0.7.73",
4
4
  "description": "TypeScript SDK for the Recursiv API",
5
5
  "license": "FSL-1.1-ALv2",
6
6
  "type": "module",