@recursiv/sdk 0.7.74 → 0.7.78

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
@@ -93,7 +93,7 @@ for await (const chunk of r.agents.chatStream(agent.id, { message: 'Hello!' }))
93
93
  ### Social
94
94
  | Resource | Key methods |
95
95
  |----------|------------|
96
- | `r.posts` | `create`, `update`, `list`, `liked`, `bookmarked`, `bookmark`, `unbookmark`, `search`, `replies`, `react` |
96
+ | `r.posts` | `create`, `update`, `list`, `get`, `getMany(ids)` (up to 25 posts by id per request, no thread; a `Map` of each id's post or `NotFoundError`; 0.7.77), `liked`, `bookmarked`, `bookmark`, `unbookmark`, `search`, `replies`, `react` |
97
97
  | `r.bookmarks` | `save`, `unsave`, `posts`, `list`, `keys`, `merge` — the member's saved posts, open-network posts and articles, synced across devices; `collections`: `list`, `create`, `update`, `remove`, `add`, `removeItem`, `items`, `posts` — private named collections of saves (0.7.68, where `network.getConfig()` advertises `capabilities.bookmark_collections`) |
98
98
  | `r.feeds` | `top` (what the member is most likely to engage with, rotated by what they saw, with open-network lanes and the lead story), `stories` (the story tail), `publicTop` (a visitor's top, keyless and edge-cacheable) |
99
99
  | `r.communities` | `create`, `join`, `search`, `members` |
@@ -120,7 +120,7 @@ for await (const chunk of r.agents.chatStream(agent.id, { message: 'Hello!' }))
120
120
  | `r.moderation` | Moderation actions: `publicLog`, `mine`, `appeal`; staff `apply`, `resolveAppeal`, `list({ target_type?, target_id? })` (one target's recorded actions, newest first, at most 100; none named = the network's latest; 0.7.74) and `reverse(actionId)` (sent once: a repeat answers 404 `action_not_found`; 0.7.74); agent grants `listAgentGrants`, `grantAgent`, `revokeAgent` |
121
121
  | `r.dispatcher` | Task queue, claims, signals, outcomes, webhooks |
122
122
  | `r.goals` | Org goals with task rollups (group dispatcher tasks) |
123
- | `r.protocols` | Private Minds safety: `sourceControls` (current enablement and saved choices; filter with `control`, each item may carry a `display` label), `setSourceControl`, `onSourceControlChange`, `reportSource`. Delegated accounts: `nativeSocialAccounts`, `startNativeSocialConnection`, `finishNativeSocialConnection`, `disconnectNativeSocialAccount`. Native follows: `nativeFollowStates` (every connected account's state for one source, one read), `nativeFollowState`, `prepareNativeFollow`, `submitNativeFollow`, `nativeFollowStatus` (native following or pending approval, distinct from a Minds feed subscription; preserve the complete signed Nostr list). Native reposts and undo: `nativeRepostState`, `prepareNativeRepost`, `submitNativeRepost`, `nativeRepostStatus` (public posts, explicit approval, connected account, no uncertain retries). Native likes and unlikes: `nativeLikeState`, `prepareNativeLike`, `submitNativeLike`, `nativeLikeStatus` (connected authority, app enablement and explicit approval; resume pending actions instead of duplicating them). Reviewed replies and optional linked posts: `prepareNativeReply`, `submitNativeReply`, `nativeReplyStatus`, `nativeReplyHistory`, `prepareNativeReplyRemoval`, `removeNativeReply` (require deployed API support and app enablement). Keyless shared post links: `sharedNetworks(projectId)`, `getSharedConversation(projectId, { protocol, post, readRef? })`. Member public reading: `searchPublicPosts` (selected account required for Bluesky/ActivityPub; public Nostr search), `getConnectedSocialConversation` (preserve result readerAccountId/readRef), `publicNetworks`, `searchPublicProfiles`, `getPublicProfile`, `getPublicAuthorFeed`, `getFollowingFeed`, `getPublicConversation`, `getPublicTrends`, `getPublicTrendFeed`; radar stories: `getPublicRadarStory` (one story by id with its posts; one that left the radar within its minimum lifetime, or within the last half hour, still answers live with `liveUntil` and the story that continues it; one that has left for longer comes with its archived copy and the live story it continues as; the id may be a World Today edition story's, and the answer then names it in `editionStory` under the story's radar id), keyless `getSharedRadarStory(projectId, { cluster })` and `getSharedRadarPosts(projectId, { cluster })` (keyless reads carry images and videos only from outlets and members marked `visitorMedia`; signed-in reads keep all media, so a share card uses only those); the Viral read (fastest-moving posts across Bluesky, the Fediverse, Nostr and Minds, each against its own network's pace; 0.7.59): `getPublicViral({ network?, category?, media?: 'video', limit?: 5 | 10 | 20 | 40, cursor?, compass? })` under the member's key and keyless `getSharedViral(projectId, params)` (visitors: `compass` `all` or `hide` only; a visitor's `media: 'video'` read can carry items whose video the server withheld, so filter with the exported `viralItemHasVideo` before a video player; `item.post` narrows on `item.protocol`). `status: 'off'` resolves with no items until the app enables the read, and `getSharedViral` also resolves an older API's 401 `missing_api_key` as off; `getPublicViral` throws every 401 and 403, so a session failure is never read as off; private app subscriptions: `listSubscriptions`, `subscribeToProfile`, `unsubscribeFromProfile`, `onSubscriptionChange`. `list` returns bounded adapter `maturity`, `limitations`, and exhaustive `supported`/`planned`/`unsupported` implementation capabilities (not provider health or enablement); settings, candidate refresh/status, search, and read-only quarantined candidate review via `listCandidates` / `getCandidate` |
123
+ | `r.protocols` | Private Minds safety: `sourceControls` (current enablement and saved choices; filter with `control`, each item may carry a `display` label), `setSourceControl`, `onSourceControlChange`, `reportSource`. Delegated accounts: `nativeSocialAccounts`, `startNativeSocialConnection`, `finishNativeSocialConnection`, `disconnectNativeSocialAccount`. Native follows: `nativeFollowStates` (every connected account's state for one source, one read), `nativeFollowState`, `prepareNativeFollow`, `submitNativeFollow`, `nativeFollowStatus` (native following or pending approval, distinct from a Minds feed subscription; preserve the complete signed Nostr list). Native reposts and undo: `nativeRepostState`, `prepareNativeRepost`, `submitNativeRepost`, `nativeRepostStatus` (public posts, explicit approval, connected account, no uncertain retries). Native likes and unlikes: `nativeLikeState`, `prepareNativeLike`, `submitNativeLike`, `nativeLikeStatus` (connected authority, app enablement and explicit approval; resume pending actions instead of duplicating them). Reviewed replies and optional linked posts: `prepareNativeReply`, `submitNativeReply`, `nativeReplyStatus`, `nativeReplyHistory`, `prepareNativeReplyRemoval`, `removeNativeReply` (require deployed API support and app enablement). Keyless shared post links: `sharedNetworks(projectId)`, `getSharedConversation(projectId, { protocol, post, readRef?, replies?: 'none' })` (`replies: 'none'`, 0.7.78: the post alone for a card, with `repliesStatus: 'not_requested'`; `getPublicConversation` and `getConnectedSocialConversation` take it too). Member public reading: `searchPublicPosts` (selected account required for Bluesky/ActivityPub; public Nostr search), `getConnectedSocialConversation` (preserve result readerAccountId/readRef), `publicNetworks`, `searchPublicProfiles`, `getPublicProfile`, `getPublicAuthorFeed`, `getFollowingFeed`, `getPublicConversation`, `getPublicTrends`, `getPublicTrendFeed`; radar stories: `getPublicRadarStory` (one story by id with its posts; one that left the radar within its minimum lifetime, or within the last half hour, still answers live with `liveUntil` and the story that continues it; one that has left for longer comes with its archived copy and the live story it continues as; the id may be a World Today edition story's, and the answer then names it in `editionStory` under the story's radar id; a story a World Today story seeded carries `volume`, the first page of the posts that story counts across Minds and the open networks, whose `cursor` goes on with `curator.storyPosts` or `curator.memberStoryPosts`), keyless `getSharedRadarStory(projectId, { cluster })` and `getSharedRadarPosts(projectId, { cluster })` (keyless reads carry images and videos only from outlets and members marked `visitorMedia`; signed-in reads keep all media, so a share card uses only those); the Viral read (fastest-moving posts across Bluesky, the Fediverse, Nostr and Minds, each against its own network's pace; 0.7.59): `getPublicViral({ network?, category?, media?: 'video', limit?: 5 | 10 | 20 | 40, cursor?, compass? })` under the member's key and keyless `getSharedViral(projectId, params)` (visitors: `compass` `all` or `hide` only; a visitor's `media: 'video'` read can carry items whose video the server withheld, so filter with the exported `viralItemHasVideo` before a video player; `item.post` narrows on `item.protocol`). `status: 'off'` resolves with no items until the app enables the read, and `getSharedViral` also resolves an older API's 401 `missing_api_key` as off; `getPublicViral` throws every 401 and 403, so a session failure is never read as off; private app subscriptions: `listSubscriptions`, `subscribeToProfile`, `unsubscribeFromProfile`, `onSubscriptionChange`. `list` returns bounded adapter `maturity`, `limitations`, and exhaustive `supported`/`planned`/`unsupported` implementation capabilities (not provider health or enablement); settings, candidate refresh/status, search, and read-only quarantined candidate review via `listCandidates` / `getCandidate` |
124
124
 
125
125
  Full reference: see [llm.md](./llm.md) (ships with this package) or the type definitions in `dist/`.
126
126
 
@@ -875,7 +875,9 @@ key with `users:read`; `people` and `groups` need `users:read`). `sdk.discovery.
875
875
  is Watch's Minds read: the app's public video posts in hot order, from one list the server shares among every viewer of
876
876
  the app and rebuilds about once a minute, each post checked again on every call under the public floors and the viewer's
877
877
  blocks and mutes, in the `Post` shape with the viewer's own `user_reaction` and `reposted_by_viewer_id` (page on
878
- `meta.next_cursor`; a feed session's Minds slot can read it as its `list`). A plain category page
878
+ `meta.next_cursor`; a feed session's Minds slot can read it as its `list`). With no category, about one row in four is
879
+ an open-network clip (0.7.78): `{ kind: 'social', id, post }` (`WatchNetworkClip`, `isWatchNetworkClip(row)`), its `post`
880
+ a `SocialPost` cleared for visitors; a feed session hands it on as a `social` item under the network's own key. A plain category page
879
881
  (`posts.list({ category })` with no other filter) reads on past a slice the safety floors
880
882
  thinned, up to six reads, until it holds `limit` posts or its candidates end: `meta.scanned`
881
883
  is how many it examined (the next offset is `offset + scanned`), and the stage ends on the
package/dist/index.d.ts CHANGED
@@ -158,8 +158,8 @@ export { RealtimeClient } from './realtime.js';
158
158
  export type { RealtimeConnectOptions } from './realtime.js';
159
159
  export { AuthResource } from './resources/auth.js';
160
160
  export type { AuthSession, CurrentSession, SignUpInput, SignInInput, UsernameSignInInput, AppContext, ResetPasswordInput, TwoFactorConfirmation, TwoFactorEnrollment, TwoFactorChallenge, CompleteTwoFactorSignInInput, OAuthClientInfo, SendOtpInput, VerifyOtpInput, CreateApiKeyInput, ApiKeyResult, LegacyHandoffOptions, LegacyHandoffSession, } from './resources/auth.js';
161
- export { PostsResource } from './resources/posts.js';
162
- export type { CategoryOrder, CategoryOrderMeta, CategoryOrderReason } from './resources/posts.js';
161
+ export { POSTS_BATCH_MAX, PostsResource } from './resources/posts.js';
162
+ export type { CategoryOrder, CategoryOrderMeta, CategoryOrderReason, PostBatch, PostBatchItem } from './resources/posts.js';
163
163
  export { UsersResource } from './resources/users.js';
164
164
  export { CommunitiesResource } from './resources/communities.js';
165
165
  export { ChatResource } from './resources/chat.js';
package/dist/index.js CHANGED
@@ -229,7 +229,7 @@ export class Recursiv {
229
229
  export { HttpClient } from './client.js';
230
230
  export { RealtimeClient } from './realtime.js';
231
231
  export { AuthResource } from './resources/auth.js';
232
- export { PostsResource } from './resources/posts.js';
232
+ export { POSTS_BATCH_MAX, PostsResource } from './resources/posts.js';
233
233
  export { UsersResource } from './resources/users.js';
234
234
  export { CommunitiesResource } from './resources/communities.js';
235
235
  export { ChatResource } from './resources/chat.js';
@@ -23,10 +23,10 @@ export interface WatchVideosParams {
23
23
  /** A position in the list to start a first read at (0 to 400); a later page sends `cursor`. */
24
24
  offset?: number;
25
25
  /**
26
- * A Politics choice, spelled as `posts.list` takes it. Only Hide politics (`hide`) is applied here: the videos whose
27
- * browse category is politics (or whose compass judgment places them in politics) leave the page and the next ones
28
- * fill it, with or without compass judgments on the server. A side or Mix is answered unfiltered, marked
29
- * `unavailable`. The page says what it did in `meta.compass_filter`.
26
+ * A Politics choice, spelled as `posts.list` takes it. Only Hide politics (`hide`) is applied here, strictly: the
27
+ * videos whose browse category is politics, whose compass judgment calls them political or maybe, or whose title or
28
+ * words name politics leave the page and the next ones fill it, with or without compass judgments on the server. A
29
+ * side or Mix is answered unfiltered, marked `unavailable`. The page says what it did in `meta.compass_filter`.
30
30
  */
31
31
  compass?: string;
32
32
  }
@@ -60,9 +60,26 @@ export interface WatchVideoPost extends Post {
60
60
  reposted_by_viewer_id: string | null;
61
61
  media: WatchVideoMedia[];
62
62
  }
63
+ /**
64
+ * An open-network clip in Watch's Everything list (0.7.78): a row of its own kind among the Minds posts, about one place
65
+ * in four, its `post` the network post as a signed-out visitor's trends read serves it (its video cleared for visitors,
66
+ * not sensitive, under Hide politics where asked). `id` (`social:<protocol>:<native id>`) is unique in the list and is
67
+ * never a Minds post id: open it, react to it and share it as the network's post. It has no `media` of its own, so an
68
+ * app that reads every row as a `Post` finds no clip in it. The discovery session hands it on as a `social` item.
69
+ */
70
+ export interface WatchNetworkClip {
71
+ kind: 'social';
72
+ id: string;
73
+ post: SocialPost;
74
+ }
75
+ /** Whether a row of `discovery.videos` is an open-network clip rather than a Minds post. */
76
+ export declare function isWatchNetworkClip(row: unknown): row is WatchNetworkClip;
63
77
  export interface WatchVideosResponse {
64
- /** Public Minds video posts in hot order (with the viewer's own `user_reaction` and `reposted_by_viewer_id`). */
65
- data: WatchVideoPost[];
78
+ /**
79
+ * Public Minds video posts in hot order (with the viewer's own `user_reaction` and `reposted_by_viewer_id`), and on
80
+ * a page with no category open-network clips among them (WatchNetworkClip, isWatchNetworkClip).
81
+ */
82
+ data: Array<WatchVideoPost | WatchNetworkClip>;
66
83
  meta: {
67
84
  limit: number;
68
85
  /** The position in the list this page started at. */
@@ -78,6 +95,17 @@ export interface WatchVideosResponse {
78
95
  reason?: 'no_app';
79
96
  /** Present when the request named a filtering `compass`: what the Politics choice did to this page. */
80
97
  compass_filter?: CompassFilterMeta;
98
+ /**
99
+ * A page with no category (0.7.78): the open-network clips it serves, how many the pool its Politics choice reads
100
+ * holds (0 when none was read in time: the page is then Minds' own), the video posts the visitor media floor withheld
101
+ * when the pool was built, and when it was built.
102
+ */
103
+ open_network?: {
104
+ served: number;
105
+ pool: number;
106
+ withheld: number;
107
+ built_at: string | null;
108
+ };
81
109
  };
82
110
  }
83
111
  /** What GET /discover/recommendations suggests: members, groups, tags, open-network accounts or World Today stories. */
@@ -5,6 +5,12 @@ export * from './discovery-first-screen.js';
5
5
  export * from './discovery-merge.js';
6
6
  export * from './discovery-progressive.js';
7
7
  export * from './discovery-safety.js';
8
+ /** Whether a row of `discovery.videos` is an open-network clip rather than a Minds post. */
9
+ export function isWatchNetworkClip(row) {
10
+ const value = row;
11
+ return !!value && value.kind === 'social' && typeof value.id === 'string' && !!value.post && typeof value.post.protocol === 'string'
12
+ && typeof value.post.nativeId === 'string' && !!value.post.author && Array.isArray(value.post.media);
13
+ }
8
14
  export const isDiscoveryPost = (item) => item.kind === 'minds' || item.kind === 'social';
9
15
  /** At most this many candidates per recommendation module. */
10
16
  export const RECOMMENDATION_MAX_ITEMS = 6;
@@ -452,7 +458,11 @@ export class DiscoveryFeedSession {
452
458
  const fallover = readFalloverSteps(response.meta.fallover);
453
459
  if (fallover)
454
460
  stream.fallover = fallover;
455
- const items = response.data.map(post => ({ key: key('minds', post.id), kind: 'minds', post }));
461
+ // Watch's Everything list carries open-network clips among its Minds posts (WatchNetworkClip): each goes on as the
462
+ // network's post, under the key the network's own lane gives it, so the two never show it twice.
463
+ const items = response.data.map((row) => (isWatchNetworkClip(row)
464
+ ? { key: key(row.post.protocol, row.post.nativeId), kind: 'social', post: row.post }
465
+ : { key: key('minds', row.id), kind: 'minds', post: row }));
456
466
  const read = { layout: response.meta.discovery ?? ordinary, partial: response.meta.category_filter?.status === 'partial',
457
467
  ...(response.meta.compass_filter ? { compass: response.meta.compass_filter } : {}) };
458
468
  if (!this.filtering)
@@ -1,4 +1,5 @@
1
1
  import type { HttpClient } from '../client.js';
2
+ import { RecursivError } from '../errors.js';
2
3
  import type { PaginatedResponse, SingleResponse, DeleteResponse, Post, PostDetail, CreatePostInput, CreatedPost, UpdatePostInput, ListPostsParams, ListPostRepliesParams, PostRepliesPage, SearchPostsParams, SearchPostsResponse, ReactionType } from '../types.js';
3
4
  import type { BookmarkedPost, BookmarksPage, SavedBookmark, UnsavedBookmark } from './bookmarks.js';
4
5
  /** How GET /posts/categories ordered its answer: the member's own order, the app's popularity order, or the fixed order. */
@@ -14,6 +15,21 @@ export interface CategoryOrderMeta {
14
15
  /** Whether For You can personalize for this member yet; absent when the member's order was not read. */
15
16
  personalized?: boolean;
16
17
  }
18
+ /** The most ids one GET /posts/batch request carries: `posts.getMany` sends one request per this many (0.7.77). */
19
+ export declare const POSTS_BATCH_MAX = 25;
20
+ /** A post `posts.getMany` read: the post as `posts.get` serves it to the caller, without its thread (no `replies`). */
21
+ export type PostBatchItem = Omit<PostDetail, 'replies' | 'replies_next_cursor'>;
22
+ /**
23
+ * What `posts.getMany` found for each id it read: the post, or the error that id's read answered. A `NotFoundError`
24
+ * (status 404) is a post that is gone, was never there, or is not for this caller, as `posts.get` answers it.
25
+ */
26
+ export type PostBatch = Map<string, {
27
+ data: PostBatchItem;
28
+ error?: undefined;
29
+ } | {
30
+ data?: undefined;
31
+ error: RecursivError;
32
+ }>;
17
33
  export declare class PostsResource {
18
34
  private client;
19
35
  constructor(client: HttpClient);
@@ -54,6 +70,15 @@ export declare class PostsResource {
54
70
  }): Promise<PaginatedResponse<Post>>;
55
71
  /** Get a single post with its newest replies; `replies_next_cursor` is set when the thread continues. */
56
72
  get(id: string): Promise<SingleResponse<PostDetail>>;
73
+ /**
74
+ * Several posts by id, one request per POSTS_BATCH_MAX ids (GET /posts/batch; 0.7.77), each id answered on its own:
75
+ * the post as `get` serves it to the caller, `viewer` and the caller's own vote and repost included, without its
76
+ * thread (the server reads no replies and no chain for it, so a batch of six answers about as fast as one post), or
77
+ * the error that read answered (`NotFoundError` for one that is gone or not for this caller). Each id is asked once
78
+ * (blank ones are dropped) and the Map is keyed by the ids as given. A request that fails as a whole throws, as an
79
+ * API without this route does (404): read those ids one by one with `get`.
80
+ */
81
+ getMany(ids: readonly string[]): Promise<PostBatch>;
57
82
  /** One page of a post's replies, newest first. Start from `replies_next_cursor` on `get`, then follow `meta.next_cursor`. */
58
83
  replies(id: string, params?: ListPostRepliesParams): Promise<PostRepliesPage>;
59
84
  /** Create a new post */
@@ -1,3 +1,13 @@
1
+ import { errorBodyFromResponse, NotFoundError, RecursivError } from '../errors.js';
2
+ /** The most ids one GET /posts/batch request carries: `posts.getMany` sends one request per this many (0.7.77). */
3
+ export const POSTS_BATCH_MAX = 25;
4
+ /** One entry's error as the error `posts.get` throws for it. */
5
+ function postBatchError(error) {
6
+ const status = error && typeof error === 'object' && typeof error.status === 'number'
7
+ ? error.status : 404;
8
+ const body = errorBodyFromResponse(status, undefined, { error });
9
+ return status === 404 ? new NotFoundError(body) : new RecursivError(status, body);
10
+ }
1
11
  export class PostsResource {
2
12
  client;
3
13
  constructor(client) {
@@ -48,6 +58,35 @@ export class PostsResource {
48
58
  get(id) {
49
59
  return this.client.get(`/posts/${id}`);
50
60
  }
61
+ /**
62
+ * Several posts by id, one request per POSTS_BATCH_MAX ids (GET /posts/batch; 0.7.77), each id answered on its own:
63
+ * the post as `get` serves it to the caller, `viewer` and the caller's own vote and repost included, without its
64
+ * thread (the server reads no replies and no chain for it, so a batch of six answers about as fast as one post), or
65
+ * the error that read answered (`NotFoundError` for one that is gone or not for this caller). Each id is asked once
66
+ * (blank ones are dropped) and the Map is keyed by the ids as given. A request that fails as a whole throws, as an
67
+ * API without this route does (404): read those ids one by one with `get`.
68
+ */
69
+ async getMany(ids) {
70
+ const unique = [...new Set(ids.filter((id) => typeof id === 'string' && id.trim()))];
71
+ const found = new Map();
72
+ for (let start = 0; start < unique.length; start += POSTS_BATCH_MAX) {
73
+ const chunk = unique.slice(start, start + POSTS_BATCH_MAX);
74
+ const answer = await this.client.get('/posts/batch', { ids: chunk.join(',') });
75
+ // A uuid's case is not part of it: the server answers each id once, by its first spelling.
76
+ const entries = new Map((Array.isArray(answer?.data) ? answer.data : [])
77
+ .filter((entry) => entry && typeof entry.id === 'string')
78
+ .map((entry) => [entry.id.trim().toLowerCase(), entry]));
79
+ for (const id of chunk) {
80
+ const entry = entries.get(id.trim().toLowerCase());
81
+ if (!entry) {
82
+ throw new RecursivError(502, { type: 'api_error', code: 'invalid_response',
83
+ message: 'GET /posts/batch did not answer every id it was asked; read them one by one with posts.get' });
84
+ }
85
+ found.set(id, entry.data && !entry.error ? { data: entry.data } : { error: postBatchError(entry.error) });
86
+ }
87
+ }
88
+ return found;
89
+ }
51
90
  /** One page of a post's replies, newest first. Start from `replies_next_cursor` on `get`, then follow `meta.next_cursor`. */
52
91
  replies(id, params) {
53
92
  return this.client.get(`/posts/${encodeURIComponent(id)}/replies`, params ? { limit: params.limit, cursor: params.cursor } : undefined);
@@ -1,5 +1,6 @@
1
1
  import type { HttpClient } from '../client.js';
2
2
  import type { CompassFilterMeta, DiscoveryLayout, PaginatedResponse, PaginationParams, SingleResponse, StoryOutlets } from '../types.js';
3
+ import type { CuratorStoryPostsPage } from './curator.js';
3
4
  /** Canonical protocol operation names returned by the adapter registry. */
4
5
  export declare const PROTOCOL_CAPABILITY_OPERATIONS: readonly ["discover", "stream", "backfill", "search", "resolveIdentity", "fetchProfile", "fetchThread", "publish", "reply", "react", "repost", "follow", "delete", "health", "checkpoint"];
5
6
  export declare const PROTOCOL_CAPABILITY_STATUSES: readonly ["supported", "planned", "unsupported"];
@@ -1509,6 +1510,14 @@ export interface SocialRadarStory {
1509
1510
  * seeded, and `id` is its radar id, the address to link to. A server that cannot resolve edition story ids answers 400.
1510
1511
  */
1511
1512
  editionStory?: string;
1513
+ /**
1514
+ * The posts the World Today story that seeded this story counts across Minds, Bluesky, the Fediverse and Nostr: the
1515
+ * first page of the reader's pool, exactly as `curator.storyPosts` (signed out) or `curator.memberStoryPosts` (signed
1516
+ * in) serves it for `volume.story_key`, whose `cursor` continues there (or walk it with `curator.storyPostPages`).
1517
+ * `volume.discussion` is the count the pages list toward. Absent while the server does not serve World Story Volume
1518
+ * for the app, and for a story no World Today story seeded or whose pool holds no post for this reader.
1519
+ */
1520
+ volume?: CuratorStoryPostsPage;
1512
1521
  }
1513
1522
  /**
1514
1523
  * The Viral read (`getPublicViral`, `getSharedViral`): what is moving fastest on Bluesky, the Fediverse, Nostr and
@@ -1716,6 +1725,13 @@ export interface SocialConversation {
1716
1725
  replies: SocialPost[];
1717
1726
  coverage: 'public_provider_window';
1718
1727
  limited: boolean;
1728
+ /**
1729
+ * Present only when the post's parents and replies were not read (`ancestors` and `replies` are empty, `limited` is
1730
+ * true): `not_requested` when the caller asked for the post alone (`replies: 'none'`, 0.7.78), `unavailable` when they
1731
+ * could not be read this time (ask again after `retryAfter` seconds).
1732
+ */
1733
+ repliesStatus?: 'unavailable' | 'not_requested';
1734
+ retryAfter?: number;
1719
1735
  /**
1720
1736
  * The member hid this post, or blocks or mutes its author: `focus` is the post, so the app can show it on request,
1721
1737
  * `replies` is empty, and `viewerControls` are the member's controls to undo. Absent otherwise (a post the app's
@@ -1753,11 +1769,16 @@ export declare class ProtocolsResource {
1753
1769
  onSourceControlChange(listener: (change: SocialSourceControlChange) => void): () => void;
1754
1770
  /** Public-only conversation catalog for a configured app. Works with an anonymous client. */
1755
1771
  sharedNetworks(projectId: string): Promise<SingleResponse<SocialNetwork[]>>;
1756
- /** Keyless public post read with app moderation; no account state or connected write authority. */
1772
+ /**
1773
+ * Keyless public post read with app moderation; no account state or connected write authority. `replies: 'none'`
1774
+ * (0.7.78) reads the post alone, for a card: `repliesStatus: 'not_requested'`, no parents or replies. An API older
1775
+ * than 0.7.78 refuses it with a 400 `invalid_input`.
1776
+ */
1757
1777
  getSharedConversation(projectId: string, params: {
1758
1778
  protocol: string;
1759
1779
  post: string;
1760
1780
  readRef?: string;
1781
+ replies?: 'none';
1761
1782
  }): Promise<SingleResponse<SocialConversation>>;
1762
1783
  /** Public, app-moderated actor identity without member subscription/account state. */
1763
1784
  getSharedProfile(projectId: string, params: {
@@ -1827,12 +1848,16 @@ export declare class ProtocolsResource {
1827
1848
  publicNetworks(): Promise<SingleResponse<SocialNetwork[]>>;
1828
1849
  /** Query the live public network. It does not import users or posts. */
1829
1850
  searchPublicPosts(params: SearchPublicPostsParams): Promise<SingleResponse<SocialPostSearch>>;
1830
- /** Read public-only conversation content through the current owner's selected account. */
1851
+ /**
1852
+ * Read public-only conversation content through the current owner's selected account. `replies: 'none'` (0.7.78)
1853
+ * reads the post alone, for a card, as getPublicConversation does.
1854
+ */
1831
1855
  getConnectedSocialConversation(params: {
1832
1856
  protocol: 'bluesky' | 'activitypub';
1833
1857
  accountId: string;
1834
1858
  post: string;
1835
1859
  readRef?: string;
1860
+ replies?: 'none';
1836
1861
  }): Promise<SingleResponse<SocialConversation>>;
1837
1862
  /** Search public profiles from a network's configured provider. */
1838
1863
  searchPublicProfiles(params: SearchPublicProfilesParams): Promise<SingleResponse<SocialProfilePage>>;
@@ -1849,11 +1874,17 @@ export declare class ProtocolsResource {
1849
1874
  protocol?: string;
1850
1875
  cursor?: string;
1851
1876
  }): Promise<SingleResponse<SocialFollowingFeed>>;
1852
- /** Read the selected source post and its bounded public conversation; never publishes or imports it. */
1877
+ /**
1878
+ * Read the selected source post and its bounded public conversation; never publishes or imports it. `replies: 'none'`
1879
+ * (0.7.78) reads the post alone, for a card (a post shared into a chat): no parents or replies are read, the answer
1880
+ * has `repliesStatus: 'not_requested'`, and a Nostr note answers in one relay read instead of a whole thread's. An API
1881
+ * older than 0.7.78 refuses it with a 400 `invalid_input`: read the conversation without it.
1882
+ */
1853
1883
  getPublicConversation(params: {
1854
1884
  protocol: string;
1855
1885
  post: string;
1856
1886
  readRef?: string;
1887
+ replies?: 'none';
1857
1888
  }): Promise<SingleResponse<SocialConversation>>;
1858
1889
  /**
1859
1890
  * Current provider-ranked topics or posts, with explicit source and coverage. Does not ingest content.
@@ -106,10 +106,15 @@ export class ProtocolsResource {
106
106
  sharedNetworks(projectId) {
107
107
  return this.client.get(`/protocols/public/${encodeURIComponent(projectId)}/networks`);
108
108
  }
109
- /** Keyless public post read with app moderation; no account state or connected write authority. */
109
+ /**
110
+ * Keyless public post read with app moderation; no account state or connected write authority. `replies: 'none'`
111
+ * (0.7.78) reads the post alone, for a card: `repliesStatus: 'not_requested'`, no parents or replies. An API older
112
+ * than 0.7.78 refuses it with a 400 `invalid_input`.
113
+ */
110
114
  getSharedConversation(projectId, params) {
111
115
  return this.client.get(`/protocols/public/${encodeURIComponent(projectId)}/conversation`, {
112
116
  protocol: params.protocol, post: params.post, readRef: params.readRef,
117
+ ...(params.replies !== undefined ? { replies: params.replies } : {}),
113
118
  });
114
119
  }
115
120
  /** Public, app-moderated actor identity without member subscription/account state. */
@@ -184,9 +189,13 @@ export class ProtocolsResource {
184
189
  return this.client.get('/protocols/social/post-search', { protocol: params.protocol, query: params.query,
185
190
  accountId: params.accountId, limit: params.limit, cursor: params.cursor });
186
191
  }
187
- /** Read public-only conversation content through the current owner's selected account. */
192
+ /**
193
+ * Read public-only conversation content through the current owner's selected account. `replies: 'none'` (0.7.78)
194
+ * reads the post alone, for a card, as getPublicConversation does.
195
+ */
188
196
  getConnectedSocialConversation(params) {
189
- return this.client.get('/protocols/social/connected-conversation', { protocol: params.protocol, accountId: params.accountId, post: params.post, readRef: params.readRef });
197
+ return this.client.get('/protocols/social/connected-conversation', { protocol: params.protocol, accountId: params.accountId, post: params.post, readRef: params.readRef,
198
+ ...(params.replies !== undefined ? { replies: params.replies } : {}) });
190
199
  }
191
200
  /** Search public profiles from a network's configured provider. */
192
201
  searchPublicProfiles(params) {
@@ -209,9 +218,15 @@ export class ProtocolsResource {
209
218
  getFollowingFeed(params = {}) {
210
219
  return this.client.get('/protocols/social/following-feed', { protocol: params.protocol, cursor: params.cursor });
211
220
  }
212
- /** Read the selected source post and its bounded public conversation; never publishes or imports it. */
221
+ /**
222
+ * Read the selected source post and its bounded public conversation; never publishes or imports it. `replies: 'none'`
223
+ * (0.7.78) reads the post alone, for a card (a post shared into a chat): no parents or replies are read, the answer
224
+ * has `repliesStatus: 'not_requested'`, and a Nostr note answers in one relay read instead of a whole thread's. An API
225
+ * older than 0.7.78 refuses it with a 400 `invalid_input`: read the conversation without it.
226
+ */
213
227
  getPublicConversation(params) {
214
- return this.client.get('/protocols/social/conversation', { protocol: params.protocol, post: params.post, readRef: params.readRef });
228
+ return this.client.get('/protocols/social/conversation', { protocol: params.protocol, post: params.post, readRef: params.readRef,
229
+ ...(params.replies !== undefined ? { replies: params.replies } : {}) });
215
230
  }
216
231
  /**
217
232
  * Current provider-ranked topics or posts, with explicit source and coverage. Does not ingest content.
@@ -70,7 +70,7 @@ export type SearchTopic = {
70
70
  name: string;
71
71
  };
72
72
  export interface SearchStory {
73
- /** Radar cluster id: open it with `protocols` radar story reads. */
73
+ /** Radar story id: open it with `protocols` radar story reads (an `article` opens as a one-article story). */
74
74
  id: string;
75
75
  label: string;
76
76
  kind: 'breaking' | 'viral' | 'story';
@@ -86,8 +86,17 @@ export interface SearchStory {
86
86
  width?: number;
87
87
  height?: number;
88
88
  } | null;
89
- /** Where the words were found: the story's headline, or only one of its members'. */
90
- matched: 'label' | 'member';
89
+ /**
90
+ * Where the words were found: the story's headline, or only its members'. `article` (a server newer than 0.7.74): no
91
+ * story is about the query, and this is one outlet's article whose headline carries its words.
92
+ */
93
+ matched: 'label' | 'member' | 'article';
94
+ /** An `article`'s link and outlet. */
95
+ article?: {
96
+ url: string;
97
+ outlet: string;
98
+ lane: 'mainstream' | 'independent';
99
+ };
91
100
  }
92
101
  export interface SearchRelated {
93
102
  categories: Array<{
package/llm.md CHANGED
@@ -630,6 +630,8 @@ const { data: results } = await r.posts.search({ query: 'shipped', community_id:
630
630
 
631
631
  Methods: `list`, `get`, `create`, `update`, `delete`, `search`, `replies(id, { limit, cursor })` (after `replies_next_cursor` on `get`), `react`, `unreact`, `liked`, `bookmarked`, `bookmark`, `unbookmark`
632
632
 
633
+ Several posts by id (0.7.77): `const found = await r.posts.getMany(ids)` reads up to `POSTS_BATCH_MAX` (25) per request (`GET /posts/batch`) and returns a `Map` keyed by id: `found.get(id)?.data` is the post as `get` serves it to the member (`viewer`, `user_reaction`, `reposted_by_viewer_id`), without its thread (no `replies`; no thread is read, so a story page's six posts come back in about the time of one), and `found.get(id)?.error` is a `NotFoundError` for one that is gone or not for this member. Each id is asked once. The call throws when the request fails as a whole, as on an API without the route (404): read those ids one by one with `get`.
634
+
633
635
  Feed definitions (types only; no route serves one yet): `FeedDefinition` is `{ v: 1, sources, ranking: { mode, range? }, filters?, exclude? }`, with 1-`FEED_SOURCES_MAX` (50) sources read as one union (`FEED_SOURCE_KINDS`; `FEED_SOURCE_KEY[kind]` names each kind's one field, e.g. `{ kind: 'tag', slug: 'rust' }`, `{ kind: 'account', ref: 'did:plc:…' }`). `filters.politics` stores only `'hide'`; viewer settings, blocks, mutes and compass Mix/Sides never go in a definition. Built-in feeds: `builtinCategoryFeedRef('science')` → `'builtin:category:science'`, plus `'builtin:all'` and `'builtin:for_you'`.
634
636
 
635
637
  `search` ranks relevance x recency x engagement (recency falls as a power of age: this month outranks years ago) through the promotion safety floor; `sort: 'new'` lists newest first; `media: 'video'` (0.7.69) keeps only posts with a video that plays (relevance only), in the page shape Watch reads, so a player can play a query. Page with `meta.next_cursor` as `cursor`: later relevance pages are slices of the ranking the first page kept for ten minutes (nothing skipped or repeated while it lives; after that, the page continues past the last post's score boundary), newest-first pages continue by (created_at, id). A cursor is bound to its search and sort: another search's, or the other sort's, is 400 `invalid_cursor`. The first relevance page carries `latest` (up to three of the newest matches not on the page, one per author, for a Latest strip); the last page carries `fall_over.lanes` (`new`, `open_network`, `tag`): draw a way on, not a wall.
@@ -827,10 +829,10 @@ await r.appSubscriptions.changeTier({ tier: 'pro', proration_date: quote.prorati
827
829
  - Native follows: `nativeFollowState({ accountId, protocol, nativeId })`, `prepareNativeFollow({ clientMutationId, accountId, protocol, nativeId, action: "follow" | "unfollow" })`, `submitNativeFollow(id, { approve: true, event? })`, `nativeFollowStatus(id)`. Show the acting identity and native following/requested/unfollowed state separately from the Minds feed subscription. Nostr must sign the complete review event without dropping other contacts; stale lists require a fresh review. Keep mutation IDs and reconcile ambiguous outcomes without replay. Requires current follow scopes and API `NATIVE_SOCIAL_FOLLOW_PROJECTS` plus source controls.
828
830
  - Native reposts: `nativeRepostState({ accountId, protocol, nativeId })`, `prepareNativeRepost({ clientMutationId, accountId, protocol, nativeId, action: "repost" | "unrepost" })`, `submitNativeRepost(id, { approve: true, event? })`, `nativeRepostStatus(id)`. Requires `NATIVE_SOCIAL_REPOST_PROJECTS`, account capability and explicit approval; Nostr signs the exact review (kind 6 or own kind 5). Unknown attempts only reconcile. Bluesky connections need the new repost scope; ActivityPub uses write:statuses. Native likes: `nativeLikeState({ accountId, protocol, nativeId })`, `prepareNativeLike({ clientMutationId, accountId, protocol, nativeId, action: "like" | "unlike" })`, `submitNativeLike(id, { approve: true, event? })`, and `nativeLikeStatus(id)`. Display the selected account, source and action before approval. Only Nostr supplies `event`: sign the exact public `signing.event` in the review; never transmit a private key. Preserve mutation IDs on ambiguous responses and refresh unknown results instead of resending. Read capabilities before offering actions; old reply-only connections need reconnecting for likes. Nostr results describe the connected relay, not every public relay. The app-specific `NATIVE_SOCIAL_LIKE_PROJECTS` switch and source controls must be enabled on the API.
829
831
  - Delegated social accounts: `r.protocols.nativeSocialAccounts()`, `startNativeSocialConnection({ protocol, identifier })`, `finishNativeSocialConnection({ protocol, state, code?, iss?, error? })`, `disconnectNativeSocialAccount(id)`. Example: `const { data } = await r.protocols.startNativeSocialConnection({ protocol: "bluesky", identifier: "alice.bsky.social" })`; open `data.authorizationUrl`, then complete with the same signed-in app user. Never log callback codes, send passwords, or auto-retry OAuth callbacks. Shared replies (off until schema/API deployment and live verification): `prepareNativeReply({ clientMutationId, text, parent: { protocol, nativeId, readRef? }, accountIds, media?, linkedPostAccountIds?, recoverFrom?: { batchId, deliveryIds } })`, `submitNativeReply(id, { approve: true, deliveryIds })`, `nativeReplyStatus(id)`, `nativeReplyHistory({ limit?, cursor? })`, `prepareNativeReplyRemoval(id, { deliveryIds })`, `removeNativeReply(id, { approve: true, removals })`. Review account, destination, `kind`, source, exact `wireText` and every reviewed `media` item with its alt text; connection is not publication approval. `media` takes the same `NativeMediaInput[]` as `prepareNativePost` and needs the account's `publish_media` capability. `linkedPostAccountIds` explicitly selects different-network standalone posts with a source link, never matching threaded replies or a same-network denial fallback. Preserve mutation IDs, show per-destination results and reconcile uncertain attempts without reposting. To review selected unsent or definitively rejected deliveries again, use recoverFrom with a new mutation ID, the same source/accounts and freshly reviewed text; preserve that ID on uncertain preparation. Only saved recoverable destinations qualify, current gates still apply, and replacedBy disables old sends atomically. Never restart unknown, accepted or published deliveries. The new preview still needs explicit approval. Saved delegated drafts/results: `const { data: history } = await r.protocols.nativeReplyHistory({ limit: 20 })`; use its opaque cursor for the next page and `nativeReplyStatus(item.id)` for details. This is a private read, not a write or provider refresh, and includes Nostr results marked type=nostr. Use nativeNostrReplyResult(item.id) to check that recorded event on its relay without signing or resending. Discard page state on owner/app changes. See README for examples and the callback, revocation and removal contracts.
830
- - `r.protocols` — Private app safety: `sourceControls` returns current `enabled` plus paged saved choices; `setSourceControl`, `onSourceControlChange` and `reportSource` share app scope. Example: `const choices = new Map(); const stop = r.protocols.onSourceControlChange(change => choices.set(JSON.stringify([change.source, change.control]), change.enabled));` then `await r.protocols.setSourceControl({ source: { protocol: "bluesky", kind: "actor", nativeId: "did:plc:aaaaaaaaaaaaaaaaaaaaaaaa" }, control: "mute", enabled: true, scope: "minds" });` and call `stop(); choices.clear()` on account change/unmount. Notifications follow an exact server confirmation, never an uncertain response. These preferences and reports apply in Minds, not on the remote network. Keyless post links: `const visitor = new Recursiv({ anonymous: true, inheritEnvironmentApiKey: false }); const { data: thread } = await visitor.protocols.getSharedConversation(projectId, { protocol: 'nostr', post: publicEventId })`; `sharedNetworks(projectId)` lists public conversation capabilities. Only configured apps; app moderation and rate limits apply, no member state or write authority. Member public reading: `publicNetworks`, `searchPublicProfiles`, `getPublicProfile`, `getPublicAuthorFeed`, `getFollowingFeed`, `getPublicConversation`, `getPublicTrends`, `getPublicTrendFeed`; private app subscriptions: `listSubscriptions`, `subscribeToProfile`, `unsubscribeFromProfile`, `onSubscriptionChange`. Example after selecting a returned source post: `const { data: thread } = await r.protocols.getPublicConversation({ protocol: post.protocol, post: post.nativeId, readRef: post.readRef })`. Render `focus`, oldest-first `ancestors` and direct `replies`; `public_provider_window` never promises every reply, and `limited` marks known omissions. Requires both `users:read` and `posts:read` on the current human app key. Protocol discovery/settings: `const { data: adapters } = await r.protocols.list()` returns bounded `{ id, name, icon, color, maturity, capabilities, limitations }` metadata; `supported` means implemented, not configured, healthy, authorized, or enabled. Methods: `list`, `status`, `listCandidates`, `getCandidate`, `search`, `getSettings`, `refresh`, `updateSettings`.
832
+ - `r.protocols` — Private app safety: `sourceControls` returns current `enabled` plus paged saved choices; `setSourceControl`, `onSourceControlChange` and `reportSource` share app scope. Example: `const choices = new Map(); const stop = r.protocols.onSourceControlChange(change => choices.set(JSON.stringify([change.source, change.control]), change.enabled));` then `await r.protocols.setSourceControl({ source: { protocol: "bluesky", kind: "actor", nativeId: "did:plc:aaaaaaaaaaaaaaaaaaaaaaaa" }, control: "mute", enabled: true, scope: "minds" });` and call `stop(); choices.clear()` on account change/unmount. Notifications follow an exact server confirmation, never an uncertain response. These preferences and reports apply in Minds, not on the remote network. Keyless post links: `const visitor = new Recursiv({ anonymous: true, inheritEnvironmentApiKey: false }); const { data: thread } = await visitor.protocols.getSharedConversation(projectId, { protocol: 'nostr', post: publicEventId })`; `sharedNetworks(projectId)` lists public conversation capabilities. Only configured apps; app moderation and rate limits apply, no member state or write authority. Member public reading: `publicNetworks`, `searchPublicProfiles`, `getPublicProfile`, `getPublicAuthorFeed`, `getFollowingFeed`, `getPublicConversation`, `getPublicTrends`, `getPublicTrendFeed`; private app subscriptions: `listSubscriptions`, `subscribeToProfile`, `unsubscribeFromProfile`, `onSubscriptionChange`. Example after selecting a returned source post: `const { data: thread } = await r.protocols.getPublicConversation({ protocol: post.protocol, post: post.nativeId, readRef: post.readRef })`. Render `focus`, oldest-first `ancestors` and direct `replies`; `public_provider_window` never promises every reply, and `limited` marks known omissions. A card (a post shared into a chat, a link preview) asks for the post alone, 0.7.78: `getPublicConversation({ protocol, post, replies: 'none' })` (also `getSharedConversation` and `getConnectedSocialConversation`) answers `focus` with no `ancestors` or `replies` and `repliesStatus: 'not_requested'`; a Nostr note answers from one relay read instead of a whole thread's. An older API answers 400 `invalid_input`: read without it. Requires both `users:read` and `posts:read` on the current human app key. Protocol discovery/settings: `const { data: adapters } = await r.protocols.list()` returns bounded `{ id, name, icon, color, maturity, capabilities, limitations }` metadata; `supported` means implemented, not configured, healthy, authorized, or enabled. Methods: `list`, `status`, `listCandidates`, `getCandidate`, `search`, `getSettings`, `refresh`, `updateSettings`.
831
833
  - Native Nostr: `const { data: preview } = await r.protocols.prepareNativeNostr(identityId, { action: "publish", content: draft })`; review and sign the exact public event, then `submitNativeNostr(identityId, { intent: preview.intent, event, approve: true })`. Preparation/submission never auto-retry. After uncertainty, use `getNativeNostrPost(identityId, preview.postId ?? preview.eventId)` before a user-selected retry of the identical intent/event: deletion checks the original note's `postId`, publication checks `eventId`. Check `nativeNostrAccount()` enablement first; keys stay with the signer. For replies, after `nativeNostrAccount()` advertises `reply_text_note`, use `prepareNativeNostr(identityId, { action: "reply", content: draft, parentId: post.nativeId })`. Review returned parent/root/participants/relay and exact event, sign with the connected client signer, then `submitNativeNostr(identityId, { intent, event, approve: true })`. Requires users:read, posts:read and posts:write. Kind1 public notes only; reply enablement and shared UI/live proof remain pending. Do not inherit arbitrary notification tags or relay hints. Signing mutations never auto-retry; check `getNativeNostrPost(identityId, eventId)` after uncertainty before a user-selected identical-event retry.
832
834
  - Hosted Bluesky account on the app's own PDS: `const { data } = await r.protocols.createHostedIdentity({ protocol: 'atproto', handle: 'alice', email })`. Only the label is sent; `data.handle` carries the domain and `data.password` is returned this once, never stored. Never auto-retried. Either the account is finished or it is removed again. 409 `handle_unavailable` = choose another label; 503 `handle_dns_unavailable`/`handle_unresolvable`/`handle_unverified` = operator setup, retry later unchanged.
833
- - 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`.
835
+ - 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`. With no category about one row in four is an open-network clip (0.7.78): `isWatchNetworkClip(row)` tells `{ kind: 'social', id, post: SocialPost }` from a `Post`; open and react to it as the network's post (its `id` is never a Minds id).
834
836
  - 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.
835
837
  - 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.
836
838
  - 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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@recursiv/sdk",
3
- "version": "0.7.74",
3
+ "version": "0.7.78",
4
4
  "description": "TypeScript SDK for the Recursiv API",
5
5
  "license": "FSL-1.1-ALv2",
6
6
  "type": "module",