@recursiv/sdk 0.7.77 → 0.7.79
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 +5 -2
- package/dist/index.d.ts +1 -1
- package/dist/resources/discovery.d.ts +30 -2
- package/dist/resources/discovery.js +11 -1
- package/dist/resources/protocols.d.ts +25 -3
- package/dist/resources/protocols.js +20 -5
- package/dist/resources/search.d.ts +71 -6
- package/llm.md +3 -3
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -94,6 +94,7 @@ for await (const chunk of r.agents.chatStream(agent.id, { message: 'Hello!' }))
|
|
|
94
94
|
| Resource | Key methods |
|
|
95
95
|
|----------|------------|
|
|
96
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
|
+
| `r.search` | `query` (blended search in sections; a thin answer steps down the search ladder, each item labelled `via`, the answer's `ladder` says how) |
|
|
97
98
|
| `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
99
|
| `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
100
|
| `r.communities` | `create`, `join`, `search`, `members` |
|
|
@@ -120,7 +121,7 @@ for await (const chunk of r.agents.chatStream(agent.id, { message: 'Hello!' }))
|
|
|
120
121
|
| `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
122
|
| `r.dispatcher` | Task queue, claims, signals, outcomes, webhooks |
|
|
122
123
|
| `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
|
|
124
|
+
| `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
125
|
|
|
125
126
|
Full reference: see [llm.md](./llm.md) (ships with this package) or the type definitions in `dist/`.
|
|
126
127
|
|
|
@@ -875,7 +876,9 @@ key with `users:read`; `people` and `groups` need `users:read`). `sdk.discovery.
|
|
|
875
876
|
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
877
|
the app and rebuilds about once a minute, each post checked again on every call under the public floors and the viewer's
|
|
877
878
|
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`).
|
|
879
|
+
`meta.next_cursor`; a feed session's Minds slot can read it as its `list`). With no category, about one row in four is
|
|
880
|
+
an open-network clip (0.7.78): `{ kind: 'social', id, post }` (`WatchNetworkClip`, `isWatchNetworkClip(row)`), its `post`
|
|
881
|
+
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
882
|
(`posts.list({ category })` with no other filter) reads on past a slice the safety floors
|
|
880
883
|
thinned, up to six reads, until it holds `limit` posts or its candidates end: `meta.scanned`
|
|
881
884
|
is how many it examined (the next offset is `offset + scanned`), and the stage ends on the
|
package/dist/index.d.ts
CHANGED
|
@@ -211,7 +211,7 @@ export { ModerationResource } from './resources/moderation.js';
|
|
|
211
211
|
export type { ModerationTargetType, ModerationActionType, ModerationAppealStatus, ModerationPrinciple, PublicModerationAction, PublicModerationLog, OwnModerationAppeal, OwnModerationAction, OwnModerationLog, ApplyModerationActionInput, ModerationAction, ListModerationActionsParams, RecordedModerationTargetType, AgentModerationMode, AgentModerationGrant, } from './resources/moderation.js';
|
|
212
212
|
export { LinkPreviewResource } from './resources/link-preview.js';
|
|
213
213
|
export { SEARCH_SECTIONS, SearchResource } from './resources/search.js';
|
|
214
|
-
export type { SearchAnswer, SearchGroup, SearchOmitReason, SearchParams, SearchPerson, SearchRelated, SearchResponse, SearchSection, SearchStory, SearchTopic } from './resources/search.js';
|
|
214
|
+
export type { PublicSearchPage, PublicSearchTab, SearchAnswer, SearchGroup, SearchLadder, SearchOmitReason, SearchParams, SearchPerson, SearchPost, SearchRelated, SearchResponse, SearchSection, SearchStage, SearchStory, SearchTopic, SearchVia, } from './resources/search.js';
|
|
215
215
|
export { BOOKMARK_MERGE_MAX_ITEMS, BookmarkCollectionsResource, BookmarksResource } from './resources/bookmarks.js';
|
|
216
216
|
export { FEED_TOP_NETWORKS, FeedsResource } from './resources/feeds.js';
|
|
217
217
|
export type { FeedStoriesParams, FeedStoriesResponse, FeedStory, FeedTopExplain, FeedTopItem, FeedTopMeta, FeedTopNetwork, FeedTopNetworkCard, FeedTopNetworkEntry, FeedTopParams, FeedTopResponse, FeedTopTarget, PublicFeedTopMeta, PublicFeedTopParams, PublicFeedTopResponse, } from './resources/feeds.js';
|
|
@@ -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
|
-
/**
|
|
65
|
-
|
|
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
|
-
|
|
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)
|
|
@@ -1725,6 +1725,13 @@ export interface SocialConversation {
|
|
|
1725
1725
|
replies: SocialPost[];
|
|
1726
1726
|
coverage: 'public_provider_window';
|
|
1727
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;
|
|
1728
1735
|
/**
|
|
1729
1736
|
* The member hid this post, or blocks or mutes its author: `focus` is the post, so the app can show it on request,
|
|
1730
1737
|
* `replies` is empty, and `viewerControls` are the member's controls to undo. Absent otherwise (a post the app's
|
|
@@ -1762,11 +1769,16 @@ export declare class ProtocolsResource {
|
|
|
1762
1769
|
onSourceControlChange(listener: (change: SocialSourceControlChange) => void): () => void;
|
|
1763
1770
|
/** Public-only conversation catalog for a configured app. Works with an anonymous client. */
|
|
1764
1771
|
sharedNetworks(projectId: string): Promise<SingleResponse<SocialNetwork[]>>;
|
|
1765
|
-
/**
|
|
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
|
+
*/
|
|
1766
1777
|
getSharedConversation(projectId: string, params: {
|
|
1767
1778
|
protocol: string;
|
|
1768
1779
|
post: string;
|
|
1769
1780
|
readRef?: string;
|
|
1781
|
+
replies?: 'none';
|
|
1770
1782
|
}): Promise<SingleResponse<SocialConversation>>;
|
|
1771
1783
|
/** Public, app-moderated actor identity without member subscription/account state. */
|
|
1772
1784
|
getSharedProfile(projectId: string, params: {
|
|
@@ -1836,12 +1848,16 @@ export declare class ProtocolsResource {
|
|
|
1836
1848
|
publicNetworks(): Promise<SingleResponse<SocialNetwork[]>>;
|
|
1837
1849
|
/** Query the live public network. It does not import users or posts. */
|
|
1838
1850
|
searchPublicPosts(params: SearchPublicPostsParams): Promise<SingleResponse<SocialPostSearch>>;
|
|
1839
|
-
/**
|
|
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
|
+
*/
|
|
1840
1855
|
getConnectedSocialConversation(params: {
|
|
1841
1856
|
protocol: 'bluesky' | 'activitypub';
|
|
1842
1857
|
accountId: string;
|
|
1843
1858
|
post: string;
|
|
1844
1859
|
readRef?: string;
|
|
1860
|
+
replies?: 'none';
|
|
1845
1861
|
}): Promise<SingleResponse<SocialConversation>>;
|
|
1846
1862
|
/** Search public profiles from a network's configured provider. */
|
|
1847
1863
|
searchPublicProfiles(params: SearchPublicProfilesParams): Promise<SingleResponse<SocialProfilePage>>;
|
|
@@ -1858,11 +1874,17 @@ export declare class ProtocolsResource {
|
|
|
1858
1874
|
protocol?: string;
|
|
1859
1875
|
cursor?: string;
|
|
1860
1876
|
}): Promise<SingleResponse<SocialFollowingFeed>>;
|
|
1861
|
-
/**
|
|
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
|
+
*/
|
|
1862
1883
|
getPublicConversation(params: {
|
|
1863
1884
|
protocol: string;
|
|
1864
1885
|
post: string;
|
|
1865
1886
|
readRef?: string;
|
|
1887
|
+
replies?: 'none';
|
|
1866
1888
|
}): Promise<SingleResponse<SocialConversation>>;
|
|
1867
1889
|
/**
|
|
1868
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
|
-
/**
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
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.
|
|
@@ -6,6 +6,58 @@ export declare const SEARCH_SECTIONS: readonly ["top_posts", "latest_posts", "op
|
|
|
6
6
|
export type SearchSection = (typeof SEARCH_SECTIONS)[number];
|
|
7
7
|
/** Why a section is absent: out of time, the server was busy, not available for this app, not allowed for this key, or it failed. */
|
|
8
8
|
export type SearchOmitReason = 'timeout' | 'saturated' | 'unavailable' | 'scope' | 'error';
|
|
9
|
+
/**
|
|
10
|
+
* Where an item of a search came from (the search ladder, server 0.7.79): the words as written, the word a typo meant,
|
|
11
|
+
* the known words with the rest dropped, and what is popular in the category the words are about or on the app. The
|
|
12
|
+
* deep rungs (`semantic`, `related_topic`, `open_network`) are named ahead of their servers.
|
|
13
|
+
*/
|
|
14
|
+
export type SearchVia = 'exact' | 'did_you_mean' | 'relaxed' | 'semantic' | 'related_topic' | 'open_network' | 'trending_category' | 'trending';
|
|
15
|
+
/**
|
|
16
|
+
* The search ladder's account of an answer, so an app labels it honestly: "No exact matches for X" (`exactCount` 0),
|
|
17
|
+
* "Did you mean Y?" (`didYouMean`), "Showing posts about memes" (`keptTerms`, `droppedTerms`), "Related: Comedy"
|
|
18
|
+
* (`inferredCategories`), "Popular in Comedy right now". `steps` names the rungs that answered, in order (`['guarded']`
|
|
19
|
+
* alone: the query gets what is popular and no suggestion). `withheld` and `pending` count posts a visitor's floors held
|
|
20
|
+
* back. `unavailable` names sections this key cannot read: say so, never fill them.
|
|
21
|
+
*/
|
|
22
|
+
export interface SearchLadder {
|
|
23
|
+
exactCount: number;
|
|
24
|
+
didYouMean: string | null;
|
|
25
|
+
keptTerms: string[];
|
|
26
|
+
droppedTerms: string[];
|
|
27
|
+
inferredCategories: Array<{
|
|
28
|
+
id: string;
|
|
29
|
+
name: string;
|
|
30
|
+
via: 'name' | 'seed' | 'alias' | 'emoji';
|
|
31
|
+
}>;
|
|
32
|
+
expandedTags: string[];
|
|
33
|
+
steps: string[];
|
|
34
|
+
withheld: number;
|
|
35
|
+
pending: number;
|
|
36
|
+
unavailable: string[];
|
|
37
|
+
}
|
|
38
|
+
/** A post as a search lists it: `via` says how the ladder found it (absent from servers before 0.7.79). */
|
|
39
|
+
export type SearchPost = Post & {
|
|
40
|
+
via?: SearchVia;
|
|
41
|
+
};
|
|
42
|
+
/** A visitor's search tabs (tRPC `publicDiscovery.search`, read keyless through the app's public client). */
|
|
43
|
+
export type PublicSearchTab = 'top' | 'latest' | 'people' | 'groups' | 'videos' | 'news' | 'tag';
|
|
44
|
+
/** `fast` answers within about a second; `deep` waits for the reads `fast` left running. */
|
|
45
|
+
export type SearchStage = 'fast' | 'deep';
|
|
46
|
+
/** One page of a visitor's search: every item labelled `via`, never empty while the app has anything popular. */
|
|
47
|
+
export interface PublicSearchPage<T = Record<string, unknown>> {
|
|
48
|
+
tab: PublicSearchTab;
|
|
49
|
+
q: string;
|
|
50
|
+
items: Array<T & {
|
|
51
|
+
via: SearchVia;
|
|
52
|
+
}>;
|
|
53
|
+
ladder: SearchLadder;
|
|
54
|
+
meta: {
|
|
55
|
+
limit: number;
|
|
56
|
+
stage: SearchStage;
|
|
57
|
+
next_cursor: string | null;
|
|
58
|
+
has_more: boolean;
|
|
59
|
+
};
|
|
60
|
+
}
|
|
9
61
|
export interface SearchParams {
|
|
10
62
|
q: string;
|
|
11
63
|
/** Items per section, 1-20 (default 6). */
|
|
@@ -70,7 +122,7 @@ export type SearchTopic = {
|
|
|
70
122
|
name: string;
|
|
71
123
|
};
|
|
72
124
|
export interface SearchStory {
|
|
73
|
-
/** Radar
|
|
125
|
+
/** Radar story id: open it with `protocols` radar story reads (an `article` opens as a one-article story). */
|
|
74
126
|
id: string;
|
|
75
127
|
label: string;
|
|
76
128
|
kind: 'breaking' | 'viral' | 'story';
|
|
@@ -86,8 +138,18 @@ export interface SearchStory {
|
|
|
86
138
|
width?: number;
|
|
87
139
|
height?: number;
|
|
88
140
|
} | null;
|
|
89
|
-
/**
|
|
90
|
-
|
|
141
|
+
/**
|
|
142
|
+
* Where the words were found: the story's headline, or only its members'. `article` (a server newer than 0.7.74): no
|
|
143
|
+
* story is about the query, and this is one outlet's article whose headline carries its words. `related` (a server
|
|
144
|
+
* newer than 0.7.78): neither, and a visitor's search ladder served it as a story about what the words are about.
|
|
145
|
+
*/
|
|
146
|
+
matched: 'label' | 'member' | 'article' | 'related';
|
|
147
|
+
/** An `article`'s link and outlet. */
|
|
148
|
+
article?: {
|
|
149
|
+
url: string;
|
|
150
|
+
outlet: string;
|
|
151
|
+
lane: 'mainstream' | 'independent';
|
|
152
|
+
};
|
|
91
153
|
}
|
|
92
154
|
export interface SearchRelated {
|
|
93
155
|
categories: Array<{
|
|
@@ -108,12 +170,12 @@ export interface SearchAnswer {
|
|
|
108
170
|
sections: {
|
|
109
171
|
/** Relevance x recency x engagement, through the promotion safety floor. `has_more`: page further with `posts.search`. */
|
|
110
172
|
top_posts?: {
|
|
111
|
-
items:
|
|
173
|
+
items: SearchPost[];
|
|
112
174
|
has_more: boolean;
|
|
113
175
|
};
|
|
114
176
|
/** Newest first, never repeating a top post. `partial`: these are the newest of what was read in time. */
|
|
115
177
|
latest_posts?: {
|
|
116
|
-
items:
|
|
178
|
+
items: SearchPost[];
|
|
117
179
|
partial: boolean;
|
|
118
180
|
};
|
|
119
181
|
open_network?: {
|
|
@@ -135,12 +197,13 @@ export interface SearchAnswer {
|
|
|
135
197
|
/** Categories and topics around the matches. */
|
|
136
198
|
related?: SearchRelated;
|
|
137
199
|
/** Present when the answer is thin or old: what is moving in the nearest category. */
|
|
200
|
+
/** `category` null with the ladder: what is popular on the app right now. */
|
|
138
201
|
trending?: {
|
|
139
202
|
category: {
|
|
140
203
|
id: string;
|
|
141
204
|
name: string;
|
|
142
205
|
} | null;
|
|
143
|
-
items:
|
|
206
|
+
items: SearchPost[];
|
|
144
207
|
};
|
|
145
208
|
};
|
|
146
209
|
/** Answered, non-empty sections in the order to render them. */
|
|
@@ -152,6 +215,8 @@ export interface SearchAnswer {
|
|
|
152
215
|
}>;
|
|
153
216
|
/** Fewer than five matches across Minds and the open networks. */
|
|
154
217
|
few_results: boolean;
|
|
218
|
+
/** The search ladder's account of the answer (server 0.7.79): present when the server ran the ladder. */
|
|
219
|
+
ladder?: SearchLadder;
|
|
155
220
|
}
|
|
156
221
|
export interface SearchResponse {
|
|
157
222
|
data: SearchAnswer;
|
package/llm.md
CHANGED
|
@@ -703,7 +703,7 @@ for (const section of answer.order) render(section, answer.sections[section])
|
|
|
703
703
|
// top_posts, latest_posts, open_network, people, groups, topics, stories, related, trending
|
|
704
704
|
```
|
|
705
705
|
|
|
706
|
-
`trending` joins when the answer is thin or old, so a search never dead-ends. `top_posts` is floored as `r.posts.search`'s relevance page is; `people` rows carry `followers_count`, `posts_count`, `empty` and `verified` (see `r.profiles.search`); `groups` rows carry `description` (one line) and `last_active_at`, to tell groups of one name apart. Page further with `r.posts.search`. Needs `posts:read`; `people` needs `users:read`, `groups` `communities:read`; `open_network` and `stories` need `users:read` plus the caller's own app key on its network, `trending` that app key. What the key cannot have is in `omitted` (`scope` / `unavailable`).
|
|
706
|
+
`trending` joins when the answer is thin or old, so a search never dead-ends. A thin answer steps down the search ladder (0.7.79): the word a typo meant, the known words with the rest dropped ("wiggly memes" searches "memes"), what is popular in the category the words are about, then on the app. Every item says how in `via` (`SearchVia`) and `answer.ladder` (`SearchLadder`) says what happened: label it honestly ("No exact matches for X", "Did you mean Y?", "Popular in Comedy right now"); `ladder.unavailable` names sections the key cannot read. Visitors read the same ladder keyless through tRPC `publicDiscovery.search` (`PublicSearchPage`). `top_posts` is floored as `r.posts.search`'s relevance page is; `people` rows carry `followers_count`, `posts_count`, `empty` and `verified` (see `r.profiles.search`); `groups` rows carry `description` (one line) and `last_active_at`, to tell groups of one name apart. Page further with `r.posts.search`. Needs `posts:read`; `people` needs `users:read`, `groups` `communities:read`; `open_network` and `stories` need `users:read` plus the caller's own app key on its network, `trending` that app key. What the key cannot have is in `omitted` (`scope` / `unavailable`).
|
|
707
707
|
|
|
708
708
|
### r.curator — World Today and its story pages
|
|
709
709
|
|
|
@@ -829,10 +829,10 @@ await r.appSubscriptions.changeTier({ tier: 'pro', proration_date: quote.prorati
|
|
|
829
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.
|
|
830
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.
|
|
831
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.
|
|
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. 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`.
|
|
833
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.
|
|
834
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.
|
|
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`.
|
|
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).
|
|
836
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.
|
|
837
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.
|
|
838
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.
|