@recursiv/sdk 0.7.51 → 0.7.53

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.
Files changed (37) hide show
  1. package/README.md +43 -5
  2. package/dist/client.d.ts +6 -2
  3. package/dist/client.js +10 -9
  4. package/dist/index.d.ts +15 -4
  5. package/dist/index.js +11 -1
  6. package/dist/resources/admin.d.ts +13 -0
  7. package/dist/resources/agents.d.ts +13 -2
  8. package/dist/resources/agents.js +7 -1
  9. package/dist/resources/app-subscriptions.d.ts +48 -2
  10. package/dist/resources/app-subscriptions.js +18 -2
  11. package/dist/resources/bookmarks.d.ts +172 -0
  12. package/dist/resources/bookmarks.js +44 -0
  13. package/dist/resources/boosts.d.ts +23 -1
  14. package/dist/resources/boosts.js +6 -1
  15. package/dist/resources/communities.d.ts +12 -2
  16. package/dist/resources/communities.js +15 -1
  17. package/dist/resources/curator.d.ts +24 -6
  18. package/dist/resources/curator.js +2 -1
  19. package/dist/resources/discovery.d.ts +104 -2
  20. package/dist/resources/discovery.js +111 -20
  21. package/dist/resources/invite-codes-admin.d.ts +13 -0
  22. package/dist/resources/invite-codes.d.ts +10 -0
  23. package/dist/resources/invite-codes.js +6 -6
  24. package/dist/resources/link-preview.d.ts +11 -4
  25. package/dist/resources/link-preview.js +4 -3
  26. package/dist/resources/notifications.d.ts +26 -0
  27. package/dist/resources/posts.d.ts +45 -6
  28. package/dist/resources/posts.js +29 -4
  29. package/dist/resources/profiles.d.ts +23 -14
  30. package/dist/resources/profiles.js +18 -5
  31. package/dist/resources/protocols.d.ts +54 -7
  32. package/dist/resources/protocols.js +10 -4
  33. package/dist/resources/search.d.ts +159 -0
  34. package/dist/resources/search.js +21 -0
  35. package/dist/types.d.ts +146 -5
  36. package/llm.md +41 -8
  37. package/package.json +2 -2
@@ -8,6 +8,20 @@ const RADAR_MAX_MEMBERS = 12;
8
8
  const RADAR_MAX_MINDS_READS = 4;
9
9
  /** A discovery page holds its own deadline: a retried 5xx from one provider only makes the whole page wait. */
10
10
  const NO_RETRY = { retry: false };
11
+ /**
12
+ * The provider no longer lists the topic (`trend_unavailable`): that topic's end, never a network
13
+ * failure. Matched on the server's code, which every SDK error carries: any other 404 on the route
14
+ * (`unsupported_protocol`, a missing route) is the network failing, and still fails the source.
15
+ */
16
+ const isGone = (error) => typeof error === 'object' && error !== null && error.code === 'trend_unavailable';
17
+ /**
18
+ * Topic pages one read of a topics network walks past when each is gone or carries nothing in the
19
+ * category, so a page reaches a topic that carries posts. Live (2026-09-26, Bluesky, news) 21 of 25
20
+ * topics answered no posts in the category; one topic a read left the network out of most pages.
21
+ */
22
+ const TOPIC_WALK = 5;
23
+ /** The walk starts no topic read after this long: well inside the page deadline, so it commits its progress rather than timing the network out. */
24
+ const TOPIC_WALK_MS = 8000;
11
25
  const RECOMMENDATION_KINDS = ['groups', 'people', 'tags'];
12
26
  const nonEmpty = (value) => typeof value === 'string' && value.trim().length > 0 && value.length <= 512;
13
27
  function recommendationItem(kind, candidate) {
@@ -116,7 +130,7 @@ function windowLayout(layout, index) {
116
130
  }
117
131
  /** Per-viewer feed session. Create a new session when user, app, category or
118
132
  * source changes. All reads retain existing server authorization/moderation.
119
- * At most four requests per page; providers fail independently. */
133
+ * At most four sources per page (a topics network's read walks up to TOPIC_WALK topic pages); providers fail independently. */
120
134
  export class DiscoveryFeedSession {
121
135
  posts;
122
136
  protocols;
@@ -160,6 +174,9 @@ export class DiscoveryFeedSession {
160
174
  return this.pending;
161
175
  }
162
176
  async read(stream) {
177
+ // `discovery` asks only for the layout judgment (and, for Minds posts, the public-only floor). The
178
+ // server's safety floor does not depend on it, so a reader that drops it to skip layout work (a clip
179
+ // pool) is screened exactly like this session.
163
180
  const params = { limit: 25, ...(this.category ? { category: this.category } : {}), discovery: true };
164
181
  if (stream.protocol === 'minds') {
165
182
  // Discover leads with what is moving now: the REST hot order (engagement
@@ -178,23 +195,9 @@ export class DiscoveryFeedSession {
178
195
  if (data.coverage !== 'provider_ranked_window' || data.items.length > 25 || data.items.some(post => post.protocol !== stream.protocol || post.author.protocol !== stream.protocol))
179
196
  throw new Error('Invalid discovery source response');
180
197
  };
181
- if (stream.topics) {
182
- const topic = stream.topics[0];
183
- if (!topic) {
184
- stream.done = true;
185
- return { items: [], layout: ordinary, partial: false };
186
- }
187
- const { data } = await this.protocols.getPublicTrendFeed({ ...params, protocol: stream.protocol, topic: topic.topic, cursor: topic.cursor }, NO_RETRY);
188
- validatePosts(data);
189
- if (data.trend.protocol !== stream.protocol || data.trend.nativeId !== topic.topic)
190
- throw new Error('Invalid discovery topic');
191
- const next = data.cursor;
192
- stream.topics.shift();
193
- if (next && next !== topic.cursor && !topic.used.includes(next))
194
- stream.topics.push({ topic: topic.topic, cursor: next, used: [...topic.used, ...(topic.cursor ? [topic.cursor] : [])].slice(-32) });
195
- stream.done = !stream.topics.length;
196
- return { items: data.items.map(post => ({ key: key(post.protocol, post.nativeId), kind: 'social', post })), layout: data.discovery ?? ordinary, partial: data.categoryFilter?.status === 'partial' };
197
- }
198
+ const started = Date.now();
199
+ if (stream.topics)
200
+ return this.walkTopics(stream, params, validatePosts, false, started);
198
201
  const { data } = await this.protocols.getPublicTrends({ ...params, protocol: stream.protocol, cursor: stream.cursor, includePreviews: true }, NO_RETRY);
199
202
  if (data.coverage !== 'provider_ranked_window' || data.items.length > 25 || data.items.some(item => item.protocol !== stream.protocol))
200
203
  throw new Error('Invalid discovery source response');
@@ -203,7 +206,14 @@ export class DiscoveryFeedSession {
203
206
  throw new Error('Invalid discovery preview');
204
207
  stream.topics = [...new Set(data.items.map(topic => topic.nativeId))].map(topic => ({ topic, used: [] }));
205
208
  stream.done = !stream.topics.length;
206
- return { items: data.items.flatMap(topic => !this.category && topic.preview ? [{ key: key(topic.preview.protocol, topic.preview.nativeId), kind: 'social', post: topic.preview }] : []), layout: ordinary, partial: data.categoryFilter?.status === 'partial' };
209
+ const partial = data.categoryFilter?.status === 'partial';
210
+ // Outside a category the catalog's previews are this page. In a category a preview is not
211
+ // judged to be in it, so the catalog carries no card: the read walks into its topics now,
212
+ // not a round later, so the network's first page in a category is not empty by design.
213
+ const previews = data.items.flatMap(topic => !this.category && topic.preview ? [{ key: key(topic.preview.protocol, topic.preview.nativeId), kind: 'social', post: topic.preview }] : []);
214
+ if (previews.length || stream.done)
215
+ return { items: previews, layout: ordinary, partial };
216
+ return this.walkTopics(stream, params, validatePosts, partial, started);
207
217
  }
208
218
  validatePosts(data);
209
219
  const next = data.cursor ?? undefined;
@@ -213,6 +223,42 @@ export class DiscoveryFeedSession {
213
223
  stream.cursor = next;
214
224
  return { items: data.items.map(post => ({ key: key(post.protocol, post.nativeId), kind: 'social', post })), layout: data.discovery ?? ordinary, partial: data.categoryFilter?.status === 'partial' };
215
225
  }
226
+ /**
227
+ * One read of a topics network: the next topic page that carries posts. A topic the provider no
228
+ * longer lists (Bluesky rotates its trends between the catalog read and the walk through it) is
229
+ * that topic's end, not the network's failure; a topic page with nothing in the category is
230
+ * walked past too, and its continuation kept, so the page reaches a topic that carries posts
231
+ * instead of spending a round on each empty one. Bounded by TOPIC_WALK reads and TOPIC_WALK_MS;
232
+ * a walk that stops short has moved the stream on, and the next read continues from there.
233
+ */
234
+ async walkTopics(stream, params, validatePosts, partial, started) {
235
+ const topics = stream.topics;
236
+ for (let reads = 0; reads < TOPIC_WALK && Date.now() - started < TOPIC_WALK_MS; reads++) {
237
+ const topic = topics[0];
238
+ if (!topic)
239
+ break;
240
+ const response = await this.protocols.getPublicTrendFeed({ ...params, protocol: stream.protocol, topic: topic.topic, cursor: topic.cursor }, NO_RETRY)
241
+ .catch((error) => { if (isGone(error))
242
+ return null; throw error; });
243
+ topics.shift();
244
+ if (!response)
245
+ continue;
246
+ const { data } = response;
247
+ validatePosts(data);
248
+ if (data.trend.protocol !== stream.protocol || data.trend.nativeId !== topic.topic)
249
+ throw new Error('Invalid discovery topic');
250
+ const next = data.cursor;
251
+ if (next && next !== topic.cursor && !topic.used.includes(next))
252
+ topics.push({ topic: topic.topic, cursor: next, used: [...topic.used, ...(topic.cursor ? [topic.cursor] : [])].slice(-32) });
253
+ partial ||= data.categoryFilter?.status === 'partial';
254
+ if (data.items.length) {
255
+ stream.done = !topics.length;
256
+ return { items: data.items.map(post => ({ key: key(post.protocol, post.nativeId), kind: 'social', post })), layout: data.discovery ?? ordinary, partial };
257
+ }
258
+ }
259
+ stream.done = !topics.length;
260
+ return { items: [], layout: ordinary, partial };
261
+ }
216
262
  async readPage() {
217
263
  // The radar is one small snapshot read: start it with the page, not after the slowest provider,
218
264
  // and start the lead story's member reads the moment the snapshot names it.
@@ -462,9 +508,54 @@ export class DiscoveryFeedSession {
462
508
  export class DiscoveryResource {
463
509
  posts;
464
510
  protocols;
465
- constructor(posts, protocols) {
511
+ curator;
512
+ http;
513
+ constructor(posts, protocols, curator, http) {
466
514
  this.posts = posts;
467
515
  this.protocols = protocols;
516
+ this.curator = curator;
517
+ this.http = http;
518
+ }
519
+ /**
520
+ * Suggestions for the signed-in viewer from the app's one recommender: active or warm members, groups
521
+ * with several posters, tags used by several people, open-network accounts or stories, overall or in one
522
+ * browse category. What the viewer follows, joined, blocked or muted is left out, and so is anything
523
+ * resting for them. Call `recordImpressions` when suggestions are actually on screen and when one is tapped.
524
+ */
525
+ recommendations(params, options) {
526
+ if (!this.http)
527
+ throw new Error('recommendations needs the HTTP client');
528
+ const { exclude, ...rest } = params;
529
+ return this.http.get('/discover/recommendations', { ...rest, exclude: exclude?.length ? exclude.join(',') : undefined }, options);
530
+ }
531
+ /** Records that suggestions were shown (at most once per half hour counts) or engaged with. */
532
+ recordImpressions(input) {
533
+ if (!this.http)
534
+ throw new Error('recordImpressions needs the HTTP client');
535
+ return this.http.post('/discover/impressions', input);
536
+ }
537
+ /**
538
+ * A For you Discover feed: the member's For You ranking (asked for Discover's surface, with layouts and a
539
+ * snapshot cursor) in a session's Minds slot. With a category, the session also reads open-network posts
540
+ * from that category (the member's top one); without, it is Minds only. Never the radar. The page-0
541
+ * arrangement and when to use which are the app's choice.
542
+ */
543
+ createPersonalFeed(options) {
544
+ const curator = this.curator;
545
+ if (!curator)
546
+ throw new Error('createPersonalFeed needs the curator resource');
547
+ const list = {
548
+ // The session's `sort` and `category` are Discover's hot-order knobs; the ranker ignores them.
549
+ list: async (params) => {
550
+ const response = await curator.forYou({ limit: params?.limit, offset: params?.offset, cursor: params?.cursor, discovery: true, surface: 'discover' });
551
+ // A server that predates the cursor says has_more: false on a partial ranked page although its
552
+ // continuation serves the next offset. A cursor-aware server always says `personalized`, and its
553
+ // has_more is the end of the feed.
554
+ const legacy = typeof response.meta.personalized !== 'boolean';
555
+ return { ...response, meta: { ...response.meta, has_more: response.meta.has_more || (legacy && response.data.length > 0) } };
556
+ },
557
+ };
558
+ return new DiscoveryFeedSession(list, this.protocols, options.category ? options.networks : [], options.category, options.category ? 'all' : 'minds', options.learning, options.recommend);
468
559
  }
469
560
  createFeed(options) {
470
561
  // The radar rides along by default; an older server answers 404 and the session simply shows no story module.
@@ -18,12 +18,25 @@ export interface InviteCodeStats {
18
18
  used: number;
19
19
  expired: number;
20
20
  revoked: number;
21
+ /** @deprecated Use `total`. */
22
+ total_codes: number;
23
+ /** @deprecated Use `active`. */
24
+ active_codes: number;
25
+ /** @deprecated Use `used`. */
26
+ used_codes: number;
27
+ /** @deprecated Use `expired`. */
28
+ expired_codes: number;
29
+ /** @deprecated Use `revoked`. */
30
+ revoked_codes: number;
21
31
  };
22
32
  waitlist: Record<string, number>;
23
33
  current_wave: {
24
34
  id: string;
25
35
  wave: number;
26
36
  name: string;
37
+ max_codes: number;
38
+ invites_per_user: number;
39
+ reward_tokens: number;
27
40
  is_active: boolean;
28
41
  } | null;
29
42
  }
@@ -90,10 +90,20 @@ export interface BulkGenerateResult {
90
90
  }
91
91
  export interface InviteStats {
92
92
  codes: {
93
+ total: number;
94
+ active: number;
95
+ used: number;
96
+ expired: number;
97
+ revoked: number;
98
+ /** @deprecated Use `total`. Retained for legacy admin clients. */
93
99
  total_codes: number;
100
+ /** @deprecated Use `active`. Retained for legacy admin clients. */
94
101
  active_codes: number;
102
+ /** @deprecated Use `used`. Retained for legacy admin clients. */
95
103
  used_codes: number;
104
+ /** @deprecated Use `expired`. Retained for legacy admin clients. */
96
105
  expired_codes: number;
106
+ /** @deprecated Use `revoked`. Retained for legacy admin clients. */
97
107
  revoked_codes: number;
98
108
  };
99
109
  waitlist: Record<string, number>;
@@ -43,26 +43,26 @@ export class InviteCodesResource {
43
43
  // ── Admin Methods ──
44
44
  /** Admin: Initialize wave configs for the network */
45
45
  initializeWaves() {
46
- return this.client.post('/invite-codes/admin/initialize-waves');
46
+ return this.client.post('/invite-codes-admin/waves/initialize');
47
47
  }
48
48
  /** Admin: Get all wave configs */
49
49
  getWaves() {
50
- return this.client.get('/invite-codes/admin/waves');
50
+ return this.client.get('/invite-codes-admin/waves');
51
51
  }
52
52
  /** Admin: Activate a specific wave */
53
53
  activateWave(wave) {
54
- return this.client.post('/invite-codes/admin/activate-wave', { wave });
54
+ return this.client.post('/invite-codes-admin/waves/activate', { wave });
55
55
  }
56
56
  /** Admin: Bulk generate invite codes */
57
57
  bulkGenerate(input) {
58
- return this.client.post('/invite-codes/admin/bulk-generate', input);
58
+ return this.client.post('/invite-codes-admin/bulk-generate', input);
59
59
  }
60
60
  /** Admin: Get invite system stats */
61
61
  getStats() {
62
- return this.client.get('/invite-codes/admin/stats');
62
+ return this.client.get('/invite-codes-admin/stats');
63
63
  }
64
64
  /** Admin: Get waitlist entries */
65
65
  getWaitlist(params) {
66
- return this.client.get('/invite-codes/admin/waitlist', params);
66
+ return this.client.get('/invite-codes-admin/waitlist', params);
67
67
  }
68
68
  }
@@ -9,7 +9,13 @@ export interface LinkPreview {
9
9
  siteName: string | null;
10
10
  }
11
11
  export interface LinkPreviewResponse {
12
- data: LinkPreview;
12
+ /**
13
+ * The preview, or null when the link cannot be previewed: the page refused a
14
+ * server (a private repository, a paywall or bot wall), is not HTML, or could
15
+ * not be reached. Show the plain link. Servers before this change answered
16
+ * 422 for these links instead, so treat that rejection the same way.
17
+ */
18
+ data: LinkPreview | null;
13
19
  cached?: boolean;
14
20
  }
15
21
  export declare class LinkPreviewResource {
@@ -17,9 +23,10 @@ export declare class LinkPreviewResource {
17
23
  constructor(client: HttpClient);
18
24
  /**
19
25
  * Fetch Open Graph / Twitter card metadata for a public URL. Cached
20
- * server-side so repeated lookups are cheap. No auth required — the
21
- * endpoint is intentionally public so unauthenticated UIs can render
22
- * link previews.
26
+ * server-side so repeated lookups are cheap, including links that have no
27
+ * preview (`data: null`). No auth required — the endpoint is intentionally
28
+ * public so unauthenticated UIs can render link previews. A URL that is not
29
+ * http(s), or points at a private or internal address, still rejects (422).
23
30
  */
24
31
  get(url: string): Promise<LinkPreviewResponse>;
25
32
  }
@@ -5,9 +5,10 @@ export class LinkPreviewResource {
5
5
  }
6
6
  /**
7
7
  * Fetch Open Graph / Twitter card metadata for a public URL. Cached
8
- * server-side so repeated lookups are cheap. No auth required — the
9
- * endpoint is intentionally public so unauthenticated UIs can render
10
- * link previews.
8
+ * server-side so repeated lookups are cheap, including links that have no
9
+ * preview (`data: null`). No auth required — the endpoint is intentionally
10
+ * public so unauthenticated UIs can render link previews. A URL that is not
11
+ * http(s), or points at a private or internal address, still rejects (422).
11
12
  */
12
13
  get(url) {
13
14
  return this.client.get('/link-preview', { url });
@@ -7,6 +7,12 @@ export interface NotificationActor {
7
7
  username: string | null;
8
8
  image: string | null;
9
9
  is_ai: boolean | null;
10
+ /**
11
+ * Follow rows only: whether the viewer already follows this person, so
12
+ * Follow back can render without reading the follow set. Absent on other
13
+ * rows and from servers older than this field.
14
+ */
15
+ viewer_follows?: boolean;
10
16
  }
11
17
  /**
12
18
  * The post a notification references, as it reads NOW, with the viewer's own
@@ -15,7 +21,18 @@ export interface NotificationActor {
15
21
  */
16
22
  export interface NotificationPostPreview {
17
23
  id: string;
24
+ /**
25
+ * The post's words as plain text (no Markdown or HTML marks; a link reads as
26
+ * its label and a bare URL as its site), up to 140 characters. A longer post
27
+ * is cut at the last whole word and ends in "…". Show it as it is.
28
+ */
18
29
  excerpt: string | null;
30
+ /**
31
+ * True when `excerpt` is a cut of a longer post. Use this rather than the
32
+ * excerpt's length to decide whether there is more to read. Absent from
33
+ * servers older than this field.
34
+ */
35
+ excerpt_truncated?: boolean;
19
36
  media_url: string | null;
20
37
  media_type: string | null;
21
38
  reactions_count?: number;
@@ -24,7 +41,10 @@ export interface NotificationPostPreview {
24
41
  downvote_count?: number;
25
42
  /** The viewer's own reaction on this post ('upvote', 'downvote', ...), if any. */
26
43
  user_reaction?: string | null;
44
+ /** Stored replies a reader can open. */
27
45
  reply_count?: number;
46
+ /** A legacy import's comment count; none of those comments are stored as replies. */
47
+ reply_count_external?: number;
28
48
  unique_commenters?: number;
29
49
  reposts_count?: number;
30
50
  /** Id of the viewer's own repost of this post; delete it to undo the repost. */
@@ -43,6 +63,12 @@ export interface Notification {
43
63
  read_at: string | null;
44
64
  /** For `post_reaction` rows: which reaction ('upvote', 'downvote', ...). Null otherwise or on older rows. */
45
65
  reaction_type?: string | null;
66
+ /**
67
+ * For `agent_needs_input` rows: whether you have written in the agent's
68
+ * thread since it asked, which is what answers an ask. The title then reads
69
+ * "You answered <agent>". Null on other rows; absent from older servers.
70
+ */
71
+ answered?: boolean | null;
46
72
  actor?: NotificationActor | null;
47
73
  post_preview?: NotificationPostPreview | null;
48
74
  }
@@ -1,17 +1,52 @@
1
1
  import type { HttpClient } from '../client.js';
2
- import type { PaginatedResponse, SingleResponse, DeleteResponse, Post, PostDetail, CreatePostInput, CreatedPost, UpdatePostInput, ListPostsParams, ListPostRepliesParams, PostRepliesPage, SearchPostsParams, ReactionType } from '../types.js';
2
+ import type { PaginatedResponse, SingleResponse, DeleteResponse, Post, PostDetail, CreatePostInput, CreatedPost, UpdatePostInput, ListPostsParams, ListPostRepliesParams, PostRepliesPage, SearchPostsParams, SearchPostsResponse, ReactionType } from '../types.js';
3
+ import type { BookmarkedPost, BookmarksPage, SavedBookmark, UnsavedBookmark } from './bookmarks.js';
4
+ /** How GET /posts/categories ordered its answer: the member's own order, the app's popularity order, or the fixed order. */
5
+ export type CategoryOrder = 'personal' | 'popular' | 'static';
6
+ /** Why an answer is not `personal`. */
7
+ export type CategoryOrderReason = 'low_signal' | 'deadline' | 'requested' | 'experiment_off';
8
+ export interface CategoryOrderMeta {
9
+ order: CategoryOrder;
10
+ /** 16 hex; equal ids mean the same order. */
11
+ order_id: string;
12
+ as_of: string;
13
+ reason?: CategoryOrderReason;
14
+ /** Whether For You can personalize for this member yet; absent when the member's order was not read. */
15
+ personalized?: boolean;
16
+ }
3
17
  export declare class PostsResource {
4
18
  private client;
5
19
  constructor(client: HttpClient);
6
20
  /** List posts with optional filters */
7
21
  list(params?: ListPostsParams): Promise<PaginatedResponse<Post>>;
8
- /** Broad categories currently enabled for public posts in this app. */
9
- categories(): Promise<SingleResponse<Array<{
22
+ /**
23
+ * Broad categories currently enabled for public posts in this app, in the order this member should see
24
+ * them: their own order once the app serves personal orders, otherwise the app's popularity order. The
25
+ * order is private to the member. `order` asks for a specific one (default `personal`).
26
+ */
27
+ categories(params?: {
28
+ order?: CategoryOrder;
29
+ }): Promise<SingleResponse<Array<{
10
30
  id: string;
11
31
  name: string;
12
- }>>>;
32
+ }>> & {
33
+ meta?: CategoryOrderMeta;
34
+ }>;
13
35
  /** List posts the authenticated caller has upvoted. */
14
36
  liked(params?: Pick<ListPostsParams, 'limit' | 'offset' | 'cursor'>): Promise<PaginatedResponse<Post>>;
37
+ /**
38
+ * The caller's saved posts in this app, newest saved first, each with `bookmarked_at`. Page on
39
+ * `meta.next_cursor` (a page can be short while it skips posts the member can no longer read).
40
+ * Same as `bookmarks.posts`; 503 `bookmarks_unavailable` where synced bookmarks are not available.
41
+ */
42
+ bookmarked(params?: {
43
+ limit?: number;
44
+ cursor?: string;
45
+ }): Promise<BookmarksPage<BookmarkedPost>>;
46
+ /** Save a post to the caller's synced bookmarks (idempotent). A bare repost is saved as its original: see `post_id`. */
47
+ bookmark(postId: string): Promise<SingleResponse<SavedBookmark>>;
48
+ /** Remove a post from the caller's synced bookmarks (idempotent). Pass the `post_id` its save returned. */
49
+ unbookmark(postId: string): Promise<SingleResponse<UnsavedBookmark>>;
15
50
  /** Posts similar to this one (embedding neighbours), in the app's scope. */
16
51
  similar(id: string, params?: {
17
52
  limit?: number;
@@ -27,8 +62,12 @@ export declare class PostsResource {
27
62
  update(id: string, input: UpdatePostInput): Promise<SingleResponse<CreatedPost>>;
28
63
  /** Delete a post */
29
64
  delete(id: string): Promise<DeleteResponse>;
30
- /** Search posts by text query */
31
- search(params: SearchPostsParams): Promise<PaginatedResponse<Post>>;
65
+ /**
66
+ * Search posts by text query. Relevance (the default) ranks relevance x recency x engagement through
67
+ * the promotion safety floor; `sort: 'new'` lists newest first. Page with `meta.next_cursor` as
68
+ * `cursor`; the first relevance page carries `latest`, the last page `fall_over` (SearchPostsResponse).
69
+ */
70
+ search(params: SearchPostsParams): Promise<SearchPostsResponse>;
32
71
  /** Add or update a reaction on a post */
33
72
  react(postId: string, type: ReactionType): Promise<SingleResponse<{
34
73
  success: true;
@@ -7,14 +7,35 @@ export class PostsResource {
7
7
  list(params) {
8
8
  return this.client.get('/posts', params);
9
9
  }
10
- /** Broad categories currently enabled for public posts in this app. */
11
- categories() {
12
- return this.client.get('/posts/categories');
10
+ /**
11
+ * Broad categories currently enabled for public posts in this app, in the order this member should see
12
+ * them: their own order once the app serves personal orders, otherwise the app's popularity order. The
13
+ * order is private to the member. `order` asks for a specific one (default `personal`).
14
+ */
15
+ categories(params) {
16
+ // No params, no query: the same request older SDKs make.
17
+ return params ? this.client.get('/posts/categories', params) : this.client.get('/posts/categories');
13
18
  }
14
19
  /** List posts the authenticated caller has upvoted. */
15
20
  liked(params) {
16
21
  return this.list({ ...params, liked: true });
17
22
  }
23
+ /**
24
+ * The caller's saved posts in this app, newest saved first, each with `bookmarked_at`. Page on
25
+ * `meta.next_cursor` (a page can be short while it skips posts the member can no longer read).
26
+ * Same as `bookmarks.posts`; 503 `bookmarks_unavailable` where synced bookmarks are not available.
27
+ */
28
+ bookmarked(params) {
29
+ return this.client.get('/posts', { bookmarked: true, ...params });
30
+ }
31
+ /** Save a post to the caller's synced bookmarks (idempotent). A bare repost is saved as its original: see `post_id`. */
32
+ bookmark(postId) {
33
+ return this.client.put('/bookmarks', { kind: 'post', post_id: postId });
34
+ }
35
+ /** Remove a post from the caller's synced bookmarks (idempotent). Pass the `post_id` its save returned. */
36
+ unbookmark(postId) {
37
+ return this.client.delete(`/bookmarks/${encodeURIComponent(`post:${postId}`)}`);
38
+ }
18
39
  /** Posts similar to this one (embedding neighbours), in the app's scope. */
19
40
  similar(id, params) {
20
41
  return this.client.get(`/posts/${encodeURIComponent(id)}/similar`, params);
@@ -39,7 +60,11 @@ export class PostsResource {
39
60
  delete(id) {
40
61
  return this.client.delete(`/posts/${id}`);
41
62
  }
42
- /** Search posts by text query */
63
+ /**
64
+ * Search posts by text query. Relevance (the default) ranks relevance x recency x engagement through
65
+ * the promotion safety floor; `sort: 'new'` lists newest first. Page with `meta.next_cursor` as
66
+ * `cursor`; the first relevance page carries `latest`, the last page `fall_over` (SearchPostsResponse).
67
+ */
43
68
  search(params) {
44
69
  return this.client.get('/posts/search', params);
45
70
  }
@@ -1,5 +1,5 @@
1
1
  import type { HttpClient } from '../client.js';
2
- import type { PaginatedResponse, SingleResponse, SuccessResponse, Profile, ProfileDetail, ProfileMe, LeaderboardEntry, UpdateProfileInput, SearchProfilesParams, SearchMentionsParams, MentionCandidate, FollowListProfile, BlockedProfile, MutedProfile, ModerationListParams, ViewerRelationship } from '../types.js';
2
+ import type { PaginatedResponse, SingleResponse, SuccessResponse, Profile, ProfileDetail, ProfileMe, LeaderboardEntry, UpdateProfileInput, SearchProfilesParams, ProfileSearchResult, SearchMentionsParams, MentionCandidate, FollowListParams, FollowListProfile, BlockedProfile, MutedProfile, ModerationListParams, ViewerRelationship } from '../types.js';
3
3
  export declare class ProfilesResource {
4
4
  private client;
5
5
  constructor(client: HttpClient);
@@ -15,14 +15,25 @@ export declare class ProfilesResource {
15
15
  /** Get a user by username */
16
16
  getByUsername(username: string): Promise<SingleResponse<ProfileDetail>>;
17
17
  /** Search users by name or username */
18
- search(params: SearchProfilesParams): Promise<PaginatedResponse<Profile>>;
18
+ /**
19
+ * Search accounts by name or handle, ranked by match x ln(1 + followers + min(posts, 100)) (doubled for an
20
+ * official account): empties last and marked `empty`, the official ones `verified`. Every page is
21
+ * a slice of one ranked pool of 200; past it `has_more` is false.
22
+ */
23
+ search(params: SearchProfilesParams): Promise<PaginatedResponse<ProfileSearchResult>>;
19
24
  /**
20
25
  * Accounts to @mention: people and agents on the caller's network, the
21
26
  * caller's follows first. Unlike `search`, this includes agents the app may
22
27
  * discover (an agent answers when mentioned); another member's personal AI
23
28
  * is never offered.
29
+ *
30
+ * A search that runs over its 2 s budget answers 503 with Retry-After. A
31
+ * type-ahead has its own deadline, so pass `{ retry: false }` to get that
32
+ * answer at once instead of waiting through retries.
24
33
  */
25
- mentions(params: SearchMentionsParams): Promise<PaginatedResponse<MentionCandidate>>;
34
+ mentions(params: SearchMentionsParams, options?: {
35
+ retry?: boolean;
36
+ }): Promise<PaginatedResponse<MentionCandidate>>;
26
37
  /** Update own profile */
27
38
  update(input: UpdateProfileInput): Promise<SingleResponse<Profile>>;
28
39
  /** Follow a user */
@@ -30,18 +41,16 @@ export declare class ProfilesResource {
30
41
  /** Unfollow a user */
31
42
  unfollow(userId: string): Promise<SuccessResponse>;
32
43
  /**
33
- * List a user's followers. Each row carries the caller's relationship with
34
- * it (`is_following`, `follows_you`), so a Follow button needs no second read.
44
+ * List a user's followers, newest follow first once the server's database
45
+ * has the follow-list order indexes (until then, in the order it reads
46
+ * them). Each row carries the caller's relationship with it (`is_following`,
47
+ * `follows_you`), so a Follow button needs no second read. `search` narrows
48
+ * the whole list to accounts whose name or handle contains it (at most 200
49
+ * characters); page the matches with `limit` and `offset`.
35
50
  */
36
- followers(userId: string, params?: {
37
- limit?: number;
38
- offset?: number;
39
- }): Promise<PaginatedResponse<FollowListProfile>>;
40
- /** List who a user follows, each row with the caller's relationship with it. */
41
- following(userId: string, params?: {
42
- limit?: number;
43
- offset?: number;
44
- }): Promise<PaginatedResponse<FollowListProfile>>;
51
+ followers(userId: string, params?: FollowListParams): Promise<PaginatedResponse<FollowListProfile>>;
52
+ /** List who a user follows, in the order `followers` uses, each row with the caller's relationship with it; `search` as on `followers`. */
53
+ following(userId: string, params?: FollowListParams): Promise<PaginatedResponse<FollowListProfile>>;
45
54
  /**
46
55
  * The caller's whole follow set as ids, up to 5000 per page (default 1000).
47
56
  * Answers what `isFollowing` answers, for every account at once. Pass
@@ -20,6 +20,11 @@ export class ProfilesResource {
20
20
  return this.client.get(`/profiles/by-username/${username}`);
21
21
  }
22
22
  /** Search users by name or username */
23
+ /**
24
+ * Search accounts by name or handle, ranked by match x ln(1 + followers + min(posts, 100)) (doubled for an
25
+ * official account): empties last and marked `empty`, the official ones `verified`. Every page is
26
+ * a slice of one ranked pool of 200; past it `has_more` is false.
27
+ */
23
28
  search(params) {
24
29
  return this.client.get('/profiles/search', params);
25
30
  }
@@ -28,9 +33,13 @@ export class ProfilesResource {
28
33
  * caller's follows first. Unlike `search`, this includes agents the app may
29
34
  * discover (an agent answers when mentioned); another member's personal AI
30
35
  * is never offered.
36
+ *
37
+ * A search that runs over its 2 s budget answers 503 with Retry-After. A
38
+ * type-ahead has its own deadline, so pass `{ retry: false }` to get that
39
+ * answer at once instead of waiting through retries.
31
40
  */
32
- mentions(params) {
33
- return this.client.get('/profiles/mentions', params);
41
+ mentions(params, options) {
42
+ return this.client.get('/profiles/mentions', params, options);
34
43
  }
35
44
  /** Update own profile */
36
45
  update(input) {
@@ -45,13 +54,17 @@ export class ProfilesResource {
45
54
  return this.client.delete(`/profiles/${userId}/follow`);
46
55
  }
47
56
  /**
48
- * List a user's followers. Each row carries the caller's relationship with
49
- * it (`is_following`, `follows_you`), so a Follow button needs no second read.
57
+ * List a user's followers, newest follow first once the server's database
58
+ * has the follow-list order indexes (until then, in the order it reads
59
+ * them). Each row carries the caller's relationship with it (`is_following`,
60
+ * `follows_you`), so a Follow button needs no second read. `search` narrows
61
+ * the whole list to accounts whose name or handle contains it (at most 200
62
+ * characters); page the matches with `limit` and `offset`.
50
63
  */
51
64
  followers(userId, params) {
52
65
  return this.client.get(`/profiles/${userId}/followers`, params);
53
66
  }
54
- /** List who a user follows, each row with the caller's relationship with it. */
67
+ /** List who a user follows, in the order `followers` uses, each row with the caller's relationship with it; `search` as on `followers`. */
55
68
  following(userId, params) {
56
69
  return this.client.get(`/profiles/${userId}/following`, params);
57
70
  }