@recursiv/sdk 0.7.63 → 0.7.66

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.
@@ -0,0 +1,228 @@
1
+ import type { HttpClient } from '../client.js';
2
+ import type { CompassFilterMeta } from '../types.js';
3
+ /**
4
+ * The top of a feed: what the member (or a visitor) is most likely to engage with, rotated by what they have seen.
5
+ * `feeds.top` and `feeds.stories` read it for a member with their own key of the app; `feeds.publicTop` reads a
6
+ * visitor's top keylessly. A feed session (`discovery.createFeed({ top })`) opens its first page with it.
7
+ *
8
+ * Every tap target is in the app: a Minds post by id, an open-network post by `/social/post/<protocol>/<id>`, a story
9
+ * by id. Nothing here is a URL on another network.
10
+ */
11
+ /** The open networks whose lanes a top carries. */
12
+ export type FeedTopNetwork = 'bluesky' | 'activitypub' | 'nostr';
13
+ export declare const FEED_TOP_NETWORKS: readonly FeedTopNetwork[];
14
+ /** Where a tap on an item goes, always in the app. `path` is the app's own page of a network post, never its URL there. */
15
+ export type FeedTopTarget = {
16
+ kind: 'post';
17
+ id: string;
18
+ } | {
19
+ kind: 'network_post';
20
+ protocol: string;
21
+ nativeId: string;
22
+ path: string;
23
+ readRef?: string;
24
+ };
25
+ /** Why an item is where it is. */
26
+ export interface FeedTopExplain {
27
+ score: number;
28
+ engagement: {
29
+ source: string;
30
+ rate: number;
31
+ };
32
+ media: string;
33
+ origin: string;
34
+ /** Where the item came from: the Minds ranking, Watch's list, a radar story, the member's follows, or a network's trends. */
35
+ source: 'minds' | 'watch' | 'radar' | 'following' | 'trends';
36
+ /** 'safe' for everyone; 'open' only for a member who turned safe defaults off. */
37
+ label: 'safe' | 'open';
38
+ cluster: string | null;
39
+ topic: string | null;
40
+ /** 'demoted': the member viewed it in the last 6 hours (it sits below rank 20). A visitor's items are all 'fresh'. */
41
+ impression: 'fresh' | 'demoted';
42
+ }
43
+ /** One item of the top: a Minds post (`ref` is its id) or an open-network post (`ref` is '<protocol>/<nativeId>'). */
44
+ export interface FeedTopItem {
45
+ ref: string;
46
+ kind: 'post' | 'network_post';
47
+ authorId: string;
48
+ target: FeedTopTarget;
49
+ position: number;
50
+ /** 'top' for the first 10: send `block: 'top'` and the position with a visible view of one (an impression). */
51
+ block: 'top' | 'below';
52
+ explain: FeedTopExplain;
53
+ }
54
+ /** What an app draws for an open-network lane entry. No URL on the post's network: the app opens it by `target.path`. */
55
+ export interface FeedTopNetworkCard {
56
+ text: string;
57
+ createdAt: string;
58
+ author: {
59
+ nativeId: string;
60
+ handle: string;
61
+ displayName: string;
62
+ avatarUrl: string | null;
63
+ };
64
+ media: Array<{
65
+ type: 'image' | 'video';
66
+ url: string;
67
+ alt: string;
68
+ poster?: string;
69
+ width?: number;
70
+ height?: number;
71
+ }>;
72
+ counts: {
73
+ likes: number | null;
74
+ replies: number | null;
75
+ reposts: number | null;
76
+ };
77
+ sensitive: boolean;
78
+ /** The post quotes another; read the quote when the post is opened. */
79
+ hasQuote: boolean;
80
+ }
81
+ /** One entry of an open-network lane: up to six per network, ranked by engagement and rotated by impressions. */
82
+ export interface FeedTopNetworkEntry extends FeedTopItem {
83
+ kind: 'network_post';
84
+ protocol: FeedTopNetwork;
85
+ card: FeedTopNetworkCard;
86
+ }
87
+ /** A story as the top names it: its id and in-app target, never an outlet's or a network's URL. */
88
+ export interface FeedStory {
89
+ id: string;
90
+ /** 'cluster:<id>': what an impression of the story names. */
91
+ ref: string;
92
+ target: {
93
+ kind: 'story';
94
+ id: string;
95
+ };
96
+ label: string;
97
+ kind: 'breaking' | 'viral' | 'story';
98
+ breaking: boolean;
99
+ theme: string | null;
100
+ sources: Partial<Record<'outlet' | 'hackernews' | 'bluesky' | 'activitypub' | 'nostr' | 'minds', number>>;
101
+ media: {
102
+ images: number;
103
+ videos: number;
104
+ };
105
+ freshness: {
106
+ newestAt: string;
107
+ oldestAt: string;
108
+ };
109
+ explain: {
110
+ score: number;
111
+ parts: {
112
+ ageHours: number;
113
+ engagement: number;
114
+ media: number;
115
+ kind: number;
116
+ freshness: number;
117
+ };
118
+ impression: 'fresh' | 'demoted';
119
+ };
120
+ }
121
+ export interface FeedTopMeta {
122
+ ref: string;
123
+ poolBuiltAt: string;
124
+ poolSize: number;
125
+ /** Fewer items than asked for (a thin pool, the caps, or only items the member saw recently). */
126
+ short: boolean;
127
+ /** `impressions`: 'on' when the order uses what the member saw; otherwise the order is the pool's, jittered per session. */
128
+ flag: {
129
+ top: 'on';
130
+ impressions: 'on' | 'off' | 'unavailable';
131
+ };
132
+ stoppedBy: 'pool' | 'caps' | 'seen' | null;
133
+ /** 'safe' for a member on safe defaults, 'open' for one who turned them off. */
134
+ safety: 'safe' | 'open';
135
+ mindsSource: 'member' | 'anonymous' | 'none' | 'following';
136
+ networks: Record<FeedTopNetwork, 'ok' | 'unavailable'> | null;
137
+ compass_filter?: CompassFilterMeta;
138
+ }
139
+ /** GET /feeds/top. `networks` is null on a feed with no lanes (Following, a story); `lead` is null when the feed has no story. */
140
+ export interface FeedTopResponse {
141
+ items: FeedTopItem[];
142
+ networks: Record<FeedTopNetwork, FeedTopNetworkEntry[]> | null;
143
+ lead: FeedStory | null;
144
+ meta: FeedTopMeta;
145
+ }
146
+ export interface FeedStoriesResponse {
147
+ data: FeedStory[];
148
+ meta: {
149
+ after: string | null;
150
+ limit: number;
151
+ short: boolean;
152
+ flag: {
153
+ top: 'on';
154
+ impressions: 'on' | 'off' | 'unavailable';
155
+ };
156
+ compass_filter?: CompassFilterMeta;
157
+ };
158
+ }
159
+ export interface PublicFeedTopMeta {
160
+ ref: string;
161
+ /** The 15-minute bucket the page was drawn for: the same for every visitor in it. */
162
+ bucket: number;
163
+ poolBuiltAt: string | null;
164
+ poolSize: number;
165
+ short: boolean;
166
+ flag: {
167
+ top: 'on';
168
+ };
169
+ compass_filter?: CompassFilterMeta;
170
+ }
171
+ /** A visitor's top (`feeds.publicTop`): safe items only, the same for every visitor in one 15-minute bucket. */
172
+ export interface PublicFeedTopResponse {
173
+ items: FeedTopItem[];
174
+ networks: Record<FeedTopNetwork, FeedTopNetworkEntry[]> | null;
175
+ lead: FeedStory | null;
176
+ meta: PublicFeedTopMeta;
177
+ }
178
+ export interface FeedTopParams {
179
+ /** `builtin:all`, `category:<a browse category id>`, `following`, `watch` or `story:<a 64-character cluster id>`. */
180
+ ref: string;
181
+ /** 1..25, default 25. */
182
+ limit?: number;
183
+ /** Opaque per app session ([A-Za-z0-9_-], up to 64): the same token keeps one session's order stable across reads. */
184
+ session?: string;
185
+ /** A Politics choice, as `posts.list` takes it (refused on `following`). */
186
+ compass?: string;
187
+ }
188
+ export interface FeedStoriesParams {
189
+ /** The story the member is on (a 64-character cluster id): left out of the next ones. */
190
+ after?: string;
191
+ /** 1..10, default 10. */
192
+ limit?: number;
193
+ compass?: string;
194
+ }
195
+ export interface PublicFeedTopParams {
196
+ /** The app's project id. */
197
+ projectId: string;
198
+ /** `all` (default), `watch` or `category:<a browse category id>`. */
199
+ ref?: string;
200
+ /** 1..25 (the server's default is 25). */
201
+ limit?: number;
202
+ /** `all` or `hide`: a visitor's Politics choice. */
203
+ compass?: string;
204
+ }
205
+ export declare class FeedsResource {
206
+ private readonly http;
207
+ constructor(http: HttpClient);
208
+ /**
209
+ * The top of a feed for the signed-in member, with their own key of the app: the items they are most likely to engage
210
+ * with, rotated by what they have seen, the open-network lanes and the lead story. Off (404) until the app's operator
211
+ * turns it on; a 503 `feed_top_unavailable` carries Retry-After. Always private: never cached for anyone else.
212
+ */
213
+ top(params: FeedTopParams, options?: {
214
+ retry?: boolean;
215
+ }): Promise<FeedTopResponse>;
216
+ /** The stories the member should meet next (the story tail), by the same ranker as the top's lead. */
217
+ stories(params?: FeedStoriesParams, options?: {
218
+ retry?: boolean;
219
+ }): Promise<FeedStoriesResponse>;
220
+ /**
221
+ * A visitor's top of All, a category or Watch (tRPC `publicDiscovery.feedTop`): safe items only, the same for every
222
+ * visitor in one 15-minute bucket. Read with no credential at all (no key, no cookie: `credentials: 'omit'`), so the
223
+ * edge can hold it; a credentialed read would be private and uncached. Off (404) until the app's operator turns it on.
224
+ */
225
+ publicTop(params: PublicFeedTopParams, options?: {
226
+ timeoutMs?: number;
227
+ }): Promise<PublicFeedTopResponse>;
228
+ }
@@ -0,0 +1,61 @@
1
+ import { NotFoundError, RecursivError } from '../errors.js';
2
+ export const FEED_TOP_NETWORKS = ['bluesky', 'activitypub', 'nostr'];
3
+ export class FeedsResource {
4
+ http;
5
+ constructor(http) {
6
+ this.http = http;
7
+ }
8
+ /**
9
+ * The top of a feed for the signed-in member, with their own key of the app: the items they are most likely to engage
10
+ * with, rotated by what they have seen, the open-network lanes and the lead story. Off (404) until the app's operator
11
+ * turns it on; a 503 `feed_top_unavailable` carries Retry-After. Always private: never cached for anyone else.
12
+ */
13
+ top(params, options) {
14
+ return this.http.get('/feeds/top', { ref: params.ref, limit: params.limit, session: params.session, compass: params.compass }, options);
15
+ }
16
+ /** The stories the member should meet next (the story tail), by the same ranker as the top's lead. */
17
+ stories(params = {}, options) {
18
+ return this.http.get('/feeds/stories', { after: params.after, limit: params.limit, compass: params.compass }, options);
19
+ }
20
+ /**
21
+ * A visitor's top of All, a category or Watch (tRPC `publicDiscovery.feedTop`): safe items only, the same for every
22
+ * visitor in one 15-minute bucket. Read with no credential at all (no key, no cookie: `credentials: 'omit'`), so the
23
+ * edge can hold it; a credentialed read would be private and uncached. Off (404) until the app's operator turns it on.
24
+ */
25
+ async publicTop(params, options) {
26
+ // One fixed key order, so equal reads are one edge-cached URL.
27
+ const input = { projectId: params.projectId, ref: params.ref ?? 'all', ...(params.limit !== undefined ? { limit: params.limit } : {}),
28
+ ...(params.compass ? { compass: params.compass } : {}) };
29
+ const url = `${this.http.baseOrigin}/api/trpc/publicDiscovery.feedTop?input=${encodeURIComponent(JSON.stringify(input))}`;
30
+ const controller = new AbortController();
31
+ const timer = setTimeout(() => controller.abort(), options?.timeoutMs ?? this.http.requestTimeoutMs);
32
+ try {
33
+ let response;
34
+ try {
35
+ response = await fetch(url, { method: 'GET', headers: { Accept: 'application/json' }, credentials: 'omit', signal: controller.signal });
36
+ }
37
+ catch (error) {
38
+ throw new RecursivError(0, { type: 'api_error', code: controller.signal.aborted ? 'timeout' : 'network_error',
39
+ message: controller.signal.aborted ? 'The visitor top did not answer in time.' : `The visitor top could not be reached (${error instanceof Error ? error.message : String(error)}).` });
40
+ }
41
+ let body = null;
42
+ try {
43
+ body = await response.json();
44
+ }
45
+ catch {
46
+ body = null;
47
+ }
48
+ const data = body?.result?.data;
49
+ if (response.ok && data && typeof data === 'object')
50
+ return data;
51
+ const error = body?.error && typeof body.error === 'object' ? body.error : undefined;
52
+ const said = [error?.json?.message, error?.message].find((value) => typeof value === 'string' && value.length > 0);
53
+ const code = typeof error?.data?.code === 'string' ? error.data.code.toLowerCase() : 'unknown_error';
54
+ const failure = { type: response.status === 404 ? 'not_found_error' : 'api_error', code, message: said ?? `HTTP ${response.status}` };
55
+ throw response.status === 404 ? new NotFoundError(failure) : new RecursivError(response.ok ? 502 : response.status, failure);
56
+ }
57
+ finally {
58
+ clearTimeout(timer);
59
+ }
60
+ }
61
+ }
@@ -3,7 +3,10 @@ import type { PaginatedResponse, SingleResponse, SuccessResponse, Profile, Profi
3
3
  export declare class ProfilesResource {
4
4
  private client;
5
5
  constructor(client: HttpClient);
6
- /** List users in the network */
6
+ /**
7
+ * List users in the network, newest first. Automated accounts are left out (a personal agent stays, `is_ai`), and
8
+ * so is an account wearing an official account's name; a page can come back shorter than `limit`.
9
+ */
7
10
  list(params?: {
8
11
  limit?: number;
9
12
  offset?: number;
@@ -3,7 +3,10 @@ export class ProfilesResource {
3
3
  constructor(client) {
4
4
  this.client = client;
5
5
  }
6
- /** List users in the network */
6
+ /**
7
+ * List users in the network, newest first. Automated accounts are left out (a personal agent stays, `is_ai`), and
8
+ * so is an account wearing an official account's name; a page can come back shorter than `limit`.
9
+ */
7
10
  list(params) {
8
11
  return this.client.get('/profiles', params);
9
12
  }
@@ -1043,11 +1043,10 @@ export interface SocialPost {
1043
1043
  };
1044
1044
  /**
1045
1045
  * Present, and only then, when the ActivityPub author opted out of indexing (`noindex`,
1046
- * `indexable: false`, or #nobot, #noindex or #noai in their bio). The keyless trends, trend-feed and
1047
- * radar reads leave such posts out, and no post served from the source index is one. The keyless
1048
- * author-feed and conversation reads (a profile or a post someone opened) and member reads of a
1049
- * provider window still return them with this mark, so an app should never re-share or promote one
1050
- * to signed-out visitors.
1046
+ * `indexable: false`, or #nobot, #noindex or #noai in their bio). The trends, trend-feed and radar
1047
+ * reads leave such posts out, keyless and for a member alike, and no post served from the source index
1048
+ * is one. The author-feed and conversation reads (a profile or a post someone opened), keyless or a
1049
+ * member's, still return them with this mark, so an app should never re-share or promote one.
1051
1050
  */
1052
1051
  authorOptOut?: true;
1053
1052
  /** A bridge (Bridgy Fed, Mostr, a NIP-48 proxy tag) made this post from one on another network (SocialBridge). Present only on a copy read live. */
@@ -35,8 +35,9 @@ export interface SearchPerson {
35
35
  pro: boolean;
36
36
  founder: boolean;
37
37
  /**
38
- * The account wears the display name of a verified or founder account it is not: it is listed after every account
39
- * that is not such a copy. Label it as not the account at `name_matches_verified_handle`. Absent from an older API.
38
+ * The account wears the display name of a verified or founder account it is not. Such an account is listed only when
39
+ * the query asks for its handle ("@handle"), after every account that is not such a copy (an older API listed it
40
+ * last for any query). Label it as not the account at `name_matches_verified_handle`. Absent from an older API.
40
41
  */
41
42
  name_matches_verified?: boolean;
42
43
  name_matches_verified_handle?: string | null;
package/dist/types.d.ts CHANGED
@@ -1261,8 +1261,9 @@ export interface ProfileSearchResult extends Profile {
1261
1261
  pro: boolean;
1262
1262
  founder: boolean;
1263
1263
  /**
1264
- * The account wears the display name of a verified or founder account it is not: it is listed after every account
1265
- * that is not such a copy. Label it as not the account at `name_matches_verified_handle`. Absent from an older API.
1264
+ * The account wears the display name of a verified or founder account it is not. Such an account is listed only when
1265
+ * the query asks for its handle ("@handle"), after every account that is not such a copy (an older API listed it
1266
+ * last for any query). Label it as not the account at `name_matches_verified_handle`. Absent from an older API.
1266
1267
  */
1267
1268
  name_matches_verified?: boolean;
1268
1269
  name_matches_verified_handle?: string | null;
package/llm.md CHANGED
@@ -639,6 +639,31 @@ await r.bookmarks.unsave(key) // or uns
639
639
 
640
640
  Newest saved first. Pages can be short while they skip posts the member can no longer read: page on `meta.next_cursor`, not length. Needs the member's own app key (else 403 `project_binding_required`). 503 `bookmarks_unavailable` means the server has no synced bookmarks yet: keep saves on the device (pass `{ retry: false }` to the probe so the 503 is not retried).
641
641
 
642
+ ### r.curator.forYouWorld — For You as the world by the member's interests
643
+
644
+ ```typescript
645
+ const head = r.curator.forYouWorld({ part: 'stories', session }) // member's own app key; read the two halves at once
646
+ const page = await r.curator.forYouWorld({ part: 'items', session, snapshot }) // items + blocks; snapshot = a previous meta.snapshot
647
+ // stories: { ref, id, source: today|radar|viral, target, label, kind, breaking, general, theme, counts, reasons, ... }
648
+ // items: { ref, kind, target, position, block: 'top' | 'below', media, topic, reasons, explain }
649
+ // blocks: [{ id, type: stories|lead_video|clips|posts|people|groups|topics|viral|categories, refs, deferred?, entities?, categories?, viral? }]
650
+ ```
651
+
652
+ 404 while the app's operator has it off; always private (`Cache-Control: private, no-store`). A story's or item's `target` is in the app (a radar story by cluster id, a Today story as `today_story`, a post id, `/social/post/<protocol>/<id>`), never a URL out. Send `block: 'top'` with a visible view of a story in the rail or one of the first 10 items: three such views without an open rest it for a week. `meta.partial` names a source that missed its deadline; `meta.interests` are category ids for chips, never for a URL; `meta.foryou_cursor` continues on `r.curator.forYou({ cursor })`.
653
+
654
+ ### r.feeds — The top of a feed (member and visitor)
655
+
656
+ ```typescript
657
+ const top = await r.feeds.top({ ref: 'builtin:all', limit: 10, session }) // member's own app key; category:<id>, following, watch, story:<id>
658
+ // top.items: { ref, kind, target, position, block: 'top' | 'below', explain }; top.networks: { bluesky, activitypub, nostr } lanes with a card each
659
+ // top.lead: the story to open with; every target is in the app (post id, /social/post/<protocol>/<id>, story id), never a URL out
660
+ const { data: next } = await r.feeds.stories({ after: storyId }) // the story tail, same ranker as the lead
661
+ const page = await r.feeds.publicTop({ projectId, ref: 'all' }) // a visitor's top: safe items only, keyless, edge-cacheable
662
+ const feed = r.discovery.createFeed({ networks, top: { ref: 'builtin:all', session }, seen: seenRefs })
663
+ ```
664
+
665
+ Each answers 404 while the app's operator has it off; `feeds.top` is private to the member and 503 `feed_top_unavailable` carries Retry-After. With a visible view of a top item, send `block: 'top'` and its position. In a feed session the top's read starts with page 0's reads and the top holds the page at most 1.5 s from its start, the by-id reads of its posts included; off, failed or late, the session is unchanged, and the radar's story reads its posts as soon as the radar answers either way. Page 0 then opens with the top's lead story and items (each item carries `top: { ref, position, block }`), its lanes replace the networks' first reads, and no hot page repeats a top item. `seen` (Minds ids and `<protocol>/<nativeId>` refs) leaves seen posts out of the hot pages, walking on past a page left under five posts (up to three more). `seenClusters` (`Map<id, { n, clicked }>`) and a visitor's `bucket` rotate the story when the top names none (`pickRadarCluster`).
666
+
642
667
  ### r.search
643
668
 
644
669
  ```typescript
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@recursiv/sdk",
3
- "version": "0.7.63",
3
+ "version": "0.7.66",
4
4
  "description": "TypeScript SDK for the Recursiv API",
5
5
  "license": "FSL-1.1-ALv2",
6
6
  "type": "module",