@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
@@ -305,6 +305,8 @@ export interface SocialSourceControlDisplay {
305
305
  avatarUrl: string | null;
306
306
  /** A hidden post's text, at most 140 characters; null for accounts, deleted posts, and moderated posts or authors. */
307
307
  excerpt: string | null;
308
+ /** A hidden ActivityPub post's public reader id, for the conversation read; absent for accounts, other networks and posts observed without one. */
309
+ readRef?: string;
308
310
  observedAt: string;
309
311
  }
310
312
  export interface SocialSourceControl {
@@ -641,6 +643,8 @@ export interface NativeReplyBatch {
641
643
  authorHandle: string;
642
644
  text: string;
643
645
  textTruncated?: boolean;
646
+ /** An ActivityPub parent's public reader id, when the reply was prepared with one: pass it to the conversation read. */
647
+ readRef?: string;
644
648
  };
645
649
  text: string;
646
650
  wireText: string;
@@ -742,7 +746,7 @@ export interface NativeDelegatedReplyHistoryItem {
742
746
  createdAt: string;
743
747
  text: string;
744
748
  textTruncated: boolean;
745
- source: Pick<NativeReplyBatch['deliveries'][number]['parent'], 'protocol' | 'nativeId' | 'sourceUrl'>;
749
+ source: Pick<NativeReplyBatch['deliveries'][number]['parent'], 'protocol' | 'nativeId' | 'sourceUrl' | 'readRef'>;
746
750
  deliveries: Array<Pick<NativeReplyBatch['deliveries'][number], 'id' | 'protocol' | 'kind' | 'state' | 'attemptedAt' | 'account' | 'recoverable' | 'replacedBy'> & {
747
751
  removalReceipt: NativeReplyRemovalReceipt | null;
748
752
  }>;
@@ -751,10 +755,12 @@ export interface NativeNostrReplyResult {
751
755
  id: string;
752
756
  createdAt: string;
753
757
  text: string;
758
+ /** `readRef`: a linked ActivityPub source's public reader id, for the conversation read (replies saved before it existed have none). */
754
759
  source: {
755
760
  protocol: SocialSourceRef['protocol'];
756
761
  nativeId: string;
757
762
  sourceUrl: string;
763
+ readRef?: string;
758
764
  };
759
765
  identityId: string;
760
766
  pubkey: string;
@@ -962,6 +968,12 @@ export interface SocialPost {
962
968
  replyCount: number | null;
963
969
  likeCount: number | null;
964
970
  repostCount: number | null;
971
+ /**
972
+ * When the counts were read from the source network. Present only on a post a category feed served
973
+ * from the open-network index, whose copy can be older than the page (the server refreshes counts
974
+ * older than 15 minutes when the provider answers in time). Absent: the counts are from this read.
975
+ */
976
+ countsObservedAt?: string;
965
977
  /** Pixel dimensions when the source declares them (Bluesky aspect ratio, Mastodon attachment meta), so a card can take the clip's shape before it loads. */
966
978
  media: Array<{
967
979
  type: 'image' | 'video';
@@ -1165,17 +1177,46 @@ export interface SocialRadarPosts {
1165
1177
  builtAt: string | null;
1166
1178
  items: SocialPost[];
1167
1179
  }
1180
+ /** A story that has left the radar, as the radar's story archive keeps it (up to 30 days after it was last on the radar). */
1181
+ export interface SocialRadarStoryArchive {
1182
+ /** The story as the radar last published it. Articles keep their headline and link; a post is a member by reference only (no `title`). */
1183
+ cluster: Pick<SocialRadarCluster, 'id' | 'label' | 'kind' | 'seed' | 'theme' | 'sources' | 'laneMix'> & {
1184
+ members: Array<Pick<SocialRadarMember, 'kind' | 'key' | 'url' | 'publishedAt' | 'outlet'> & {
1185
+ title?: string;
1186
+ }>;
1187
+ };
1188
+ /** Its open-network posts the app's source index still holds and its source controls and safety floor admit today, in member order. */
1189
+ items: SocialPost[];
1190
+ /** A World Today story's dek: an outlet's own sentence, or the editor model's. */
1191
+ summary: {
1192
+ text: string;
1193
+ origin: 'outlet' | 'editor';
1194
+ } | null;
1195
+ /** The first and the last radar build that published it. */
1196
+ firstSeenAt: string;
1197
+ lastSeenAt: string;
1198
+ /** The index holds none of its open-network posts any more (retention or deletion), so none are listed. */
1199
+ postsExpired: boolean;
1200
+ }
1168
1201
  /**
1169
1202
  * One radar story by id, with its social member posts.
1170
- * `live`: on the current radar (`cluster` and `items` present). `aged_out`: on one of the last six
1171
- * radars but not this one. `unknown`: not seen in that window (never existed, or aged out long ago).
1203
+ * `live`: on the current radar (`cluster` and `items` present). `aged_out`: not on this radar, but
1204
+ * held by the story archive (`archive`, and the live story it continues as in `successor`) or seen on
1205
+ * one of the last six radars (`archive` null). `unknown`: in neither (never existed, or left longer ago
1206
+ * than the archive keeps stories). A server without the story archive leaves `archive` and `successor` out.
1172
1207
  */
1173
1208
  export interface SocialRadarStory {
1174
1209
  id: string;
1175
1210
  builtAt: string | null;
1176
1211
  status: 'live' | 'aged_out' | 'unknown';
1177
1212
  cluster: SocialRadarCluster | null;
1213
+ /** The live story's posts; empty otherwise (an archived story's are in `archive.items`). */
1178
1214
  items: SocialPost[];
1215
+ archive?: SocialRadarStoryArchive | null;
1216
+ successor?: {
1217
+ id: string;
1218
+ label: string;
1219
+ } | null;
1179
1220
  }
1180
1221
  /** Read options for callers that hold their own deadline, such as a discovery page. */
1181
1222
  export interface SocialReadOptions {
@@ -1255,7 +1296,10 @@ export declare class ProtocolsResource {
1255
1296
  limit?: number;
1256
1297
  cursor?: string;
1257
1298
  }): Promise<SingleResponse<SocialAuthorFeed>>;
1258
- /** Public, app-moderated discovery without account authority. Enrichment uses cached judgments only. */
1299
+ /**
1300
+ * Public, app-moderated discovery without account authority. Enrichment uses cached judgments only.
1301
+ * Every page, previews included, gets the safety floor; `discovery` only asks for a layout.
1302
+ */
1259
1303
  getSharedTrends(projectId: string, params: {
1260
1304
  protocol: string;
1261
1305
  limit?: number;
@@ -1264,7 +1308,7 @@ export declare class ProtocolsResource {
1264
1308
  category?: string;
1265
1309
  discovery?: boolean;
1266
1310
  }): Promise<SingleResponse<SocialTrends>>;
1267
- /** Open a provider-listed topic through the same public-only app boundary. */
1311
+ /** Open a provider-listed topic through the same public-only app boundary, with the same safety floor. */
1268
1312
  getSharedTrendFeed(projectId: string, params: {
1269
1313
  protocol: string;
1270
1314
  topic: string;
@@ -1314,7 +1358,10 @@ export declare class ProtocolsResource {
1314
1358
  post: string;
1315
1359
  readRef?: string;
1316
1360
  }): Promise<SingleResponse<SocialConversation>>;
1317
- /** Current provider-ranked topics or posts, with explicit source and coverage. Does not ingest content. */
1361
+ /**
1362
+ * Current provider-ranked topics or posts, with explicit source and coverage. Does not ingest content.
1363
+ * Every page, previews included, gets the safety floor; `discovery` only asks for a layout.
1364
+ */
1318
1365
  getPublicTrends(params: {
1319
1366
  protocol: string;
1320
1367
  limit?: number;
@@ -1323,7 +1370,7 @@ export declare class ProtocolsResource {
1323
1370
  category?: string;
1324
1371
  discovery?: boolean;
1325
1372
  }, options?: SocialReadOptions): Promise<SingleResponse<SocialTrends>>;
1326
- /** Open a topic currently listed by the provider. Use its nativeId as topic and preserve the opaque cursor. */
1373
+ /** Open a topic currently listed by the provider. Use its nativeId as topic and preserve the opaque cursor. Screened like `getPublicTrends`. */
1327
1374
  getPublicTrendFeed(params: {
1328
1375
  protocol: string;
1329
1376
  topic: string;
@@ -89,12 +89,15 @@ export class ProtocolsResource {
89
89
  return this.client.get(`/protocols/public/${encodeURIComponent(projectId)}/author-feed`, { protocol: params.protocol, actor: params.actor,
90
90
  limit: params.limit, cursor: params.cursor });
91
91
  }
92
- /** Public, app-moderated discovery without account authority. Enrichment uses cached judgments only. */
92
+ /**
93
+ * Public, app-moderated discovery without account authority. Enrichment uses cached judgments only.
94
+ * Every page, previews included, gets the safety floor; `discovery` only asks for a layout.
95
+ */
93
96
  getSharedTrends(projectId, params) {
94
97
  return this.client.get(`/protocols/public/${encodeURIComponent(projectId)}/trends`, { protocol: params.protocol, limit: params.limit, cursor: params.cursor,
95
98
  includePreviews: params.includePreviews, category: params.category, discovery: params.discovery });
96
99
  }
97
- /** Open a provider-listed topic through the same public-only app boundary. */
100
+ /** Open a provider-listed topic through the same public-only app boundary, with the same safety floor. */
98
101
  getSharedTrendFeed(projectId, params) {
99
102
  return this.client.get(`/protocols/public/${encodeURIComponent(projectId)}/trend-feed`, { protocol: params.protocol, topic: params.topic,
100
103
  limit: params.limit, cursor: params.cursor, category: params.category, discovery: params.discovery });
@@ -148,14 +151,17 @@ export class ProtocolsResource {
148
151
  getPublicConversation(params) {
149
152
  return this.client.get('/protocols/social/conversation', { protocol: params.protocol, post: params.post, readRef: params.readRef });
150
153
  }
151
- /** Current provider-ranked topics or posts, with explicit source and coverage. Does not ingest content. */
154
+ /**
155
+ * Current provider-ranked topics or posts, with explicit source and coverage. Does not ingest content.
156
+ * Every page, previews included, gets the safety floor; `discovery` only asks for a layout.
157
+ */
152
158
  getPublicTrends(params, options) {
153
159
  return this.client.get('/protocols/social/trends', { protocol: params.protocol, limit: params.limit, cursor: params.cursor,
154
160
  ...(params.includePreviews !== undefined ? { includePreviews: params.includePreviews } : {}),
155
161
  ...(params.category !== undefined ? { category: params.category } : {}),
156
162
  ...(params.discovery !== undefined ? { discovery: params.discovery } : {}) }, ...readOptions(options));
157
163
  }
158
- /** Open a topic currently listed by the provider. Use its nativeId as topic and preserve the opaque cursor. */
164
+ /** Open a topic currently listed by the provider. Use its nativeId as topic and preserve the opaque cursor. Screened like `getPublicTrends`. */
159
165
  getPublicTrendFeed(params, options) {
160
166
  return this.client.get('/protocols/social/trend-feed', { protocol: params.protocol, topic: params.topic, limit: params.limit, cursor: params.cursor,
161
167
  ...(params.category !== undefined ? { category: params.category } : {}),
@@ -0,0 +1,159 @@
1
+ import type { HttpClient } from '../client.js';
2
+ import type { Post } from '../types.js';
3
+ import type { SocialPost } from './protocols.js';
4
+ /** The sections a blended search can answer, in no particular order (see `SearchAnswer.order`). */
5
+ export declare const SEARCH_SECTIONS: readonly ["top_posts", "latest_posts", "open_network", "people", "groups", "topics", "stories", "related", "trending"];
6
+ export type SearchSection = (typeof SEARCH_SECTIONS)[number];
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
+ export type SearchOmitReason = 'timeout' | 'saturated' | 'unavailable' | 'scope' | 'error';
9
+ export interface SearchParams {
10
+ q: string;
11
+ /** Items per section, 1-20 (default 6). */
12
+ limit?: number;
13
+ /** Only these sections (default: all). */
14
+ sections?: readonly SearchSection[];
15
+ /** For organization-bound keys, as on `posts.search`. */
16
+ organization_id?: string;
17
+ }
18
+ export interface SearchPerson {
19
+ id: string;
20
+ name: string | null;
21
+ username: string | null;
22
+ image: string | null;
23
+ is_ai: boolean;
24
+ /** The query is this account's handle. */
25
+ exact: boolean;
26
+ /** The account's public life, what the ranking read: match x ln(1 + followers + min(posts, 100)). */
27
+ followers_count: number;
28
+ posts_count: number;
29
+ /** Nobody follows it and it never posted: listed after every account with a public life, so a client can fold these under "more handles". */
30
+ empty: boolean;
31
+ /** An account the operator vouches for: draw the verified badge. */
32
+ verified: boolean;
33
+ /** Membership badges, as every embedded author carries them. */
34
+ plus: boolean;
35
+ pro: boolean;
36
+ founder: boolean;
37
+ }
38
+ export interface SearchGroup {
39
+ id: string;
40
+ name: string;
41
+ slug: string;
42
+ image: string | null;
43
+ member_count: number;
44
+ post_count: number;
45
+ is_member: boolean;
46
+ }
47
+ export type SearchTopic = {
48
+ kind: 'tag';
49
+ id: string;
50
+ name: string;
51
+ slug: string;
52
+ } | {
53
+ kind: 'category';
54
+ id: string;
55
+ name: string;
56
+ };
57
+ export interface SearchStory {
58
+ /** Radar cluster id: open it with `protocols` radar story reads. */
59
+ id: string;
60
+ label: string;
61
+ kind: 'breaking' | 'viral' | 'story';
62
+ theme: string | null;
63
+ newest_at: string;
64
+ sources: Partial<Record<'outlet' | 'hackernews' | 'bluesky' | 'activitypub' | 'nostr' | 'minds', number>>;
65
+ lead: {
66
+ type: 'image' | 'video';
67
+ url: string;
68
+ poster?: string;
69
+ width?: number;
70
+ height?: number;
71
+ } | null;
72
+ /** Where the words were found: the story's headline, or only one of its members'. */
73
+ matched: 'label' | 'member';
74
+ }
75
+ export interface SearchRelated {
76
+ categories: Array<{
77
+ id: string;
78
+ name: string;
79
+ count: number;
80
+ }>;
81
+ tags: Array<{
82
+ id: string;
83
+ name: string;
84
+ slug: string;
85
+ count: number;
86
+ }>;
87
+ }
88
+ /** One blended search answer. Only answered sections are present; an answered section may be empty. */
89
+ export interface SearchAnswer {
90
+ query: string;
91
+ sections: {
92
+ /** Relevance x recency x engagement, through the promotion safety floor. `has_more`: page further with `posts.search`. */
93
+ top_posts?: {
94
+ items: Post[];
95
+ has_more: boolean;
96
+ };
97
+ /** Newest first, never repeating a top post. `partial`: these are the newest of what was read in time. */
98
+ latest_posts?: {
99
+ items: Post[];
100
+ partial: boolean;
101
+ };
102
+ open_network?: {
103
+ items: SocialPost[];
104
+ withheld: number;
105
+ };
106
+ people?: {
107
+ items: SearchPerson[];
108
+ };
109
+ groups?: {
110
+ items: SearchGroup[];
111
+ };
112
+ topics?: {
113
+ items: SearchTopic[];
114
+ };
115
+ stories?: {
116
+ items: SearchStory[];
117
+ };
118
+ /** Categories and topics around the matches. */
119
+ related?: SearchRelated;
120
+ /** Present when the answer is thin or old: what is moving in the nearest category. */
121
+ trending?: {
122
+ category: {
123
+ id: string;
124
+ name: string;
125
+ } | null;
126
+ items: Post[];
127
+ };
128
+ };
129
+ /** Answered, non-empty sections in the order to render them. */
130
+ order: SearchSection[];
131
+ /** Sections the server left out, and why; the rest of the answer stands. */
132
+ omitted: Array<{
133
+ section: SearchSection;
134
+ reason: SearchOmitReason;
135
+ }>;
136
+ /** Fewer than five matches across Minds and the open networks. */
137
+ few_results: boolean;
138
+ }
139
+ export interface SearchResponse {
140
+ data: SearchAnswer;
141
+ meta: {
142
+ limit: number;
143
+ elapsed_ms: number | null;
144
+ };
145
+ }
146
+ export declare class SearchResource {
147
+ private client;
148
+ constructor(client: HttpClient);
149
+ /**
150
+ * Blended search (GET /search): the best and newest posts, open-network posts, people, groups, topics,
151
+ * news stories, and, so a search never dead-ends, related categories and trending posts. Each section
152
+ * has its own time budget; one that runs out is listed in `omitted` instead of failing the answer.
153
+ * Needs `posts:read`. People need `users:read`, groups `communities:read`; open network and stories need
154
+ * `users:read` and the caller's own app key bound to its network, trending that app key. A section the
155
+ * key cannot have is listed in `omitted` with reason `scope` or `unavailable`.
156
+ * Servers before this endpoint answer 404: fall back to `posts.search`.
157
+ */
158
+ query(params: SearchParams): Promise<SearchResponse>;
159
+ }
@@ -0,0 +1,21 @@
1
+ /** The sections a blended search can answer, in no particular order (see `SearchAnswer.order`). */
2
+ export const SEARCH_SECTIONS = ['top_posts', 'latest_posts', 'open_network', 'people', 'groups', 'topics', 'stories', 'related', 'trending'];
3
+ export class SearchResource {
4
+ client;
5
+ constructor(client) {
6
+ this.client = client;
7
+ }
8
+ /**
9
+ * Blended search (GET /search): the best and newest posts, open-network posts, people, groups, topics,
10
+ * news stories, and, so a search never dead-ends, related categories and trending posts. Each section
11
+ * has its own time budget; one that runs out is listed in `omitted` instead of failing the answer.
12
+ * Needs `posts:read`. People need `users:read`, groups `communities:read`; open network and stories need
13
+ * `users:read` and the caller's own app key bound to its network, trending that app key. A section the
14
+ * key cannot have is listed in `omitted` with reason `scope` or `unavailable`.
15
+ * Servers before this endpoint answer 404: fall back to `posts.search`.
16
+ */
17
+ query(params) {
18
+ const { sections, ...rest } = params;
19
+ return this.client.get('/search', { ...rest, ...(sections && sections.length > 0 ? { sections: sections.join(',') } : {}) });
20
+ }
21
+ }
package/dist/types.d.ts CHANGED
@@ -123,9 +123,31 @@ export interface MediaItem {
123
123
  id: string;
124
124
  url: string;
125
125
  type: string;
126
+ /**
127
+ * Set on a legacy Minds video whose file is known to be gone (the server probes it): leave it out
128
+ * of Watch and clip queues and show the poster instead. Absent when the file plays or is unjudged.
129
+ */
130
+ unplayable?: boolean;
131
+ }
132
+ /** The group a post is in, as every post payload carries it. */
133
+ export interface PostCommunity {
134
+ id: string;
135
+ name: string;
136
+ slug: string;
137
+ /**
138
+ * A post in a group that is not public is for the group's members, and is
139
+ * never boosted: offer Boost only when this is 'public'.
140
+ */
141
+ privacy: 'public' | 'private' | 'hidden';
126
142
  }
127
143
  export interface Post {
144
+ /** Stored replies a reader can open. */
128
145
  reply_count?: number;
146
+ /**
147
+ * The comment count a legacy import brought with the post (minds.com). None of those comments are
148
+ * stored as replies, so it is not in `reply_count`.
149
+ */
150
+ reply_count_external?: number;
129
151
  id: string;
130
152
  /** Original Minds numeric GUID when this row came from the legacy import. */
131
153
  legacy_guid?: string | null;
@@ -133,6 +155,8 @@ export interface Post {
133
155
  content_format: 'plain' | 'markdown';
134
156
  author: UserSummary;
135
157
  community_id: string | null;
158
+ /** The group the post is in (null when it is in none). */
159
+ community?: PostCommunity | null;
136
160
  organization_id: string | null;
137
161
  reply_to_id?: string | null;
138
162
  /** Present when this post is an X-style repost — the original post is nested. */
@@ -154,10 +178,25 @@ export interface RepostedPost {
154
178
  content_format: 'plain' | 'markdown';
155
179
  author: UserSummary | null;
156
180
  community_id: string | null;
181
+ /** The group the original is in (null when it is in none): it decides what the repost card offers. */
182
+ community?: PostCommunity | null;
157
183
  organization_id: string | null;
158
184
  tags: Tag[];
159
185
  reactions_count: number;
160
186
  score: number;
187
+ /** The original's engagement, as the REST payload sends it for the original itself. */
188
+ upvote_count?: number;
189
+ downvote_count?: number;
190
+ /** The viewer's own reaction on the original ('upvote', 'downvote', ...), if any. */
191
+ user_reaction?: string | null;
192
+ /** Stored replies a reader can open. */
193
+ reply_count?: number;
194
+ /** A legacy import's comment count; none of those comments are stored as replies, so it is not in `reply_count`. */
195
+ reply_count_external?: number;
196
+ unique_commenters?: number;
197
+ reposts_count?: number;
198
+ /** Id of the viewer's own repost of the original; delete it to undo the repost. */
199
+ reposted_by_viewer_id?: string | null;
161
200
  media: MediaItem[];
162
201
  created_at: string;
163
202
  }
@@ -178,6 +217,8 @@ export interface PostReply {
178
217
  user_reaction?: string | null;
179
218
  /** This reply's own reply count, so its sub-thread can be opened. */
180
219
  reply_count?: number;
220
+ /** A legacy import's comment count for this reply; not stored as replies, so not in `reply_count`. */
221
+ reply_count_external?: number;
181
222
  created_at: string;
182
223
  }
183
224
  export interface ListPostRepliesParams {
@@ -226,7 +267,11 @@ export interface UpdatePostInput {
226
267
  }
227
268
  export type ReactionType = 'like' | 'heart' | 'fire' | 'laugh' | 'sad' | 'angry' | 'upvote' | 'downvote';
228
269
  export interface ListPostsParams extends PaginationParams {
229
- /** Public-only feed with a bounded Jev presentation hint. Limit <=25. */
270
+ /**
271
+ * Public-only feed with a bounded Jev presentation hint. Limit <=25.
272
+ * The safety floor does not depend on it: a category page and every app-wide
273
+ * `sort: 'hot'` or `'score'` read are screened with or without a layout.
274
+ */
230
275
  discovery?: boolean;
231
276
  /** Public-only broad category. Requires an enabled app experiment; limit <=25. */
232
277
  category?: string;
@@ -239,6 +284,11 @@ export interface ListPostsParams extends PaginationParams {
239
284
  audience_user_id?: 'me' | string;
240
285
  /** Return only posts the authenticated caller has upvoted. */
241
286
  liked?: boolean;
287
+ /**
288
+ * Return only the caller's saved posts, newest saved first, each with `bookmarked_at`. Takes only `limit`
289
+ * (at most 50) and `cursor`; see `posts.bookmarked` / `bookmarks.posts`.
290
+ */
291
+ bookmarked?: boolean;
242
292
  }
243
293
  export interface SearchPostsParams extends PaginationParams {
244
294
  q: string;
@@ -247,6 +297,42 @@ export interface SearchPostsParams extends PaginationParams {
247
297
  sort?: 'relevance' | 'new';
248
298
  since?: string;
249
299
  until?: string;
300
+ /**
301
+ * The `meta.next_cursor` of the previous page. Later relevance pages are slices of the ranking the
302
+ * first page kept (ten minutes), so nothing is skipped or repeated while it lives; after that the page
303
+ * continues past the last post's score boundary. Newest-first pages continue by (created_at, id). A
304
+ * cursor is bound to its search and sort: another's is refused (400 `invalid_cursor`). Without one,
305
+ * `offset` counts.
306
+ */
307
+ cursor?: string;
308
+ }
309
+ /** A way on from the end of a search: newest first, the open networks, or a topic the matches share. */
310
+ export type SearchFallOverLane = {
311
+ kind: 'new';
312
+ } | {
313
+ kind: 'open_network';
314
+ } | {
315
+ kind: 'tag';
316
+ id: string;
317
+ name: string;
318
+ slug: string;
319
+ };
320
+ /** One page of `posts.search`. */
321
+ export interface SearchPostsResponse extends PaginatedResponse<Post> {
322
+ /**
323
+ * On the first relevance page: up to three of the newest matches that are not on the page, newest
324
+ * first, one per author, for a "Latest" strip above the best matches. Absent when there are none, and on later pages.
325
+ */
326
+ latest?: Post[];
327
+ /**
328
+ * On the last page (`meta.has_more` false): what exists past the end of this search, so a client
329
+ * can offer a way on instead of a wall. `new` when newest-first goes deeper than the ranking did;
330
+ * `open_network` when the caller's app can search the open networks; `tag` for the topic the
331
+ * page's posts share most. May be empty.
332
+ */
333
+ fall_over?: {
334
+ lanes: SearchFallOverLane[];
335
+ };
250
336
  }
251
337
  export type QueryParams = Record<string, unknown>;
252
338
  export interface CommunityCreator {
@@ -269,6 +355,8 @@ export interface Community {
269
355
  post_count?: number;
270
356
  /** Whether the caller is an accepted member. Returned by `list` and `search`. */
271
357
  is_member?: boolean;
358
+ /** The caller's role in the group, or null outside it. Returned by `list` and the detail read; a group's creator reads as its owner. */
359
+ viewer_role?: 'owner' | 'admin' | 'moderator' | 'member' | null;
272
360
  created_at: string;
273
361
  }
274
362
  /** Filters accepted by the communities directory. */
@@ -277,14 +365,36 @@ export interface ListCommunitiesParams extends PaginationParams {
277
365
  member?: boolean;
278
366
  /** Restrict personal keys to one organization. Project-bound keys are already scoped. */
279
367
  organization_id?: string;
280
- /** Server-side directory ordering. With `q`, name matches come first and this breaks ties. */
281
- sort?: 'members' | 'active' | 'newest';
368
+ /**
369
+ * Server-side ordering. With `q`, name matches come first and this breaks ties. `joined` (with
370
+ * `member: true`) lists the caller's groups by when they joined, newest first.
371
+ */
372
+ sort?: 'members' | 'active' | 'newest' | 'joined';
282
373
  /**
283
374
  * Case-insensitive text search over name and description, up to 200
284
375
  * characters. With `member: true` it searches only the caller's own groups.
285
376
  */
286
377
  q?: string;
287
378
  }
379
+ /** Which join requests to answer at once: named people (at most 100), or the oldest 100 waiting. */
380
+ export type AnswerJoinRequestsInput = {
381
+ user_ids: string[];
382
+ } | {
383
+ all: true;
384
+ };
385
+ /** What a many-at-once answer did: how many it answered, who it skipped, and how many still wait. */
386
+ export interface ApprovedJoinRequests {
387
+ approved: number;
388
+ /** Named people with no pending request here: already in, already answered, banned, or never asked. */
389
+ skipped: string[];
390
+ /** Pending requests left in the group (bans not counted). */
391
+ remaining: number;
392
+ }
393
+ export interface DeclinedJoinRequests {
394
+ declined: number;
395
+ skipped: string[];
396
+ remaining: number;
397
+ }
288
398
  /** The caller's own membership row in a group, if any. */
289
399
  export type CommunityMembershipStatus = 'accepted' | 'pending' | 'invited' | 'banned';
290
400
  export interface CommunityDetail extends Community {
@@ -366,7 +476,8 @@ export interface CommunityBannedMember {
366
476
  }
367
477
  export interface CreateCommunityInput {
368
478
  name: string;
369
- slug: string;
479
+ /** Optional vanity slug. Omit it for a server-generated collision-resistant default. */
480
+ slug?: string;
370
481
  description?: string;
371
482
  privacy?: 'public' | 'private' | 'hidden';
372
483
  }
@@ -719,7 +830,8 @@ export interface AgentDetail extends Agent {
719
830
  }
720
831
  export interface CreateAgentInput {
721
832
  name: string;
722
- username: string;
833
+ /** Optional public handle. Omit it for a server-generated collision-resistant default. */
834
+ username?: string;
723
835
  bio?: string;
724
836
  model?: string;
725
837
  system_prompt?: string;
@@ -856,6 +968,14 @@ export interface Profile {
856
968
  /** A row of a followers/following list: the profile plus the caller's relationship with it. */
857
969
  export interface FollowListProfile extends Profile, ViewerRelationship {
858
970
  }
971
+ /** Paging and search for a follower or following list (profiles.followers / following). */
972
+ export interface FollowListParams {
973
+ limit?: number;
974
+ offset?: number;
975
+ /** Name or handle, matched anywhere in either (at most 200 characters); a blank term lists everyone. */
976
+ search?: string;
977
+ [key: string]: unknown;
978
+ }
859
979
  /** An account the caller blocked. */
860
980
  export interface BlockedProfile extends Profile {
861
981
  blocked_at: string;
@@ -895,10 +1015,24 @@ export interface UpdateProfileInput {
895
1015
  username?: string;
896
1016
  bio?: string | null;
897
1017
  image?: string | null;
1018
+ /** The profile banner as an absolute image URL, or null to remove it (PUT /profiles/me has accepted it since #1867). */
1019
+ banner?: string | null;
898
1020
  }
899
1021
  export interface SearchProfilesParams extends PaginationParams {
900
1022
  q: string;
901
1023
  }
1024
+ /** A row of `profiles.search`: the profile plus what the ranking read (match x ln(1 + followers + min(posts, 100))). */
1025
+ export interface ProfileSearchResult extends Profile {
1026
+ followers_count: number;
1027
+ posts_count: number;
1028
+ /** Nobody follows it and it never posted: listed after every account with a public life, for a client to fold under "more handles". */
1029
+ empty: boolean;
1030
+ /** An account the operator vouches for: draw the verified badge. */
1031
+ verified: boolean;
1032
+ plus: boolean;
1033
+ pro: boolean;
1034
+ founder: boolean;
1035
+ }
902
1036
  export interface SearchMentionsParams {
903
1037
  /** What the member typed after `@` (a leading `@` is ignored). */
904
1038
  q: string;
@@ -1082,6 +1216,13 @@ export interface LoginHistoryEntry {
1082
1216
  }
1083
1217
  export interface RecursivConfig {
1084
1218
  apiKey?: string;
1219
+ /**
1220
+ * Whether to fall back to `RECURSIV_API_KEY` when `apiKey` is omitted.
1221
+ * Defaults to true for backwards compatibility and branded pre-auth flows.
1222
+ * Set to false for public clients that must remain keyless even in a
1223
+ * credentialed server process.
1224
+ */
1225
+ inheritEnvironmentApiKey?: boolean;
1085
1226
  baseUrl?: string;
1086
1227
  timeout?: number;
1087
1228
  maxRetries?: number;