@recursiv/sdk 0.7.52 → 0.7.54
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 +49 -3
- package/dist/client.d.ts +6 -2
- package/dist/client.js +7 -8
- package/dist/index.d.ts +12 -2
- package/dist/index.js +11 -1
- package/dist/resources/auth.d.ts +17 -3
- package/dist/resources/auth.js +7 -2
- package/dist/resources/bookmarks.d.ts +172 -0
- package/dist/resources/bookmarks.js +44 -0
- package/dist/resources/communities.d.ts +11 -1
- package/dist/resources/communities.js +14 -0
- package/dist/resources/curator.d.ts +1 -1
- package/dist/resources/discovery.d.ts +168 -3
- package/dist/resources/discovery.js +133 -21
- package/dist/resources/human-input.js +2 -1
- package/dist/resources/link-preview.d.ts +11 -4
- package/dist/resources/link-preview.js +4 -3
- package/dist/resources/notifications.d.ts +26 -0
- package/dist/resources/posts.d.ts +21 -3
- package/dist/resources/posts.js +21 -1
- package/dist/resources/profiles.d.ts +29 -16
- package/dist/resources/profiles.js +23 -6
- package/dist/resources/protocols.d.ts +60 -3
- package/dist/resources/search.d.ts +159 -0
- package/dist/resources/search.js +21 -0
- package/dist/types.d.ts +178 -2
- package/llm.md +36 -5
- package/package.json +1 -1
package/dist/types.d.ts
CHANGED
|
@@ -22,6 +22,38 @@ export interface DiscoveryLayout {
|
|
|
22
22
|
theme?: string;
|
|
23
23
|
windows?: DiscoveryLayoutWindow[];
|
|
24
24
|
}
|
|
25
|
+
/**
|
|
26
|
+
* How to ask for the next page of the same feed: the next offset while the page order is a ranking,
|
|
27
|
+
* the cursor once the feed pages by keyset (a category feed past its window of newest judged posts), or null when the
|
|
28
|
+
* feed has no more. On a category feed, `fallover` then says where to go.
|
|
29
|
+
*/
|
|
30
|
+
export type FeedNext = {
|
|
31
|
+
offset: number;
|
|
32
|
+
} | {
|
|
33
|
+
cursor: string;
|
|
34
|
+
} | null;
|
|
35
|
+
/**
|
|
36
|
+
* One step of a category feed's fall-through, in order: the same category's older posts (the keyset
|
|
37
|
+
* cursor; the same continuation `next` carries), the same category on the open networks, the member's
|
|
38
|
+
* next categories (`via`: the order they came from, the one `posts.categories()` serves), For you
|
|
39
|
+
* (signed in), All.
|
|
40
|
+
*/
|
|
41
|
+
export type FalloverStep = {
|
|
42
|
+
kind: 'category_older';
|
|
43
|
+
category: string;
|
|
44
|
+
cursor: string;
|
|
45
|
+
} | {
|
|
46
|
+
kind: 'category_network';
|
|
47
|
+
category: string;
|
|
48
|
+
} | {
|
|
49
|
+
kind: 'category';
|
|
50
|
+
category: string;
|
|
51
|
+
via: 'personal' | 'popular' | 'static';
|
|
52
|
+
} | {
|
|
53
|
+
kind: 'for_you';
|
|
54
|
+
} | {
|
|
55
|
+
kind: 'all';
|
|
56
|
+
};
|
|
25
57
|
export interface PaginationMeta {
|
|
26
58
|
discovery?: DiscoveryLayout;
|
|
27
59
|
scanned?: number;
|
|
@@ -36,6 +68,10 @@ export interface PaginationMeta {
|
|
|
36
68
|
has_more: boolean;
|
|
37
69
|
/** Pass this as `cursor` for the next page. Absent on offset-only endpoints. */
|
|
38
70
|
next_cursor?: string | null;
|
|
71
|
+
/** The next page of this feed (offset or cursor), or null at its end. Absent on older servers. */
|
|
72
|
+
next?: FeedNext;
|
|
73
|
+
/** Present when a stage of a category feed ends: the ordered steps to follow so scrolling never stops. */
|
|
74
|
+
fallover?: FalloverStep[];
|
|
39
75
|
}
|
|
40
76
|
export interface PaginatedResponse<T> {
|
|
41
77
|
data: T[];
|
|
@@ -99,6 +135,18 @@ export interface ViewerRelationship {
|
|
|
99
135
|
/** This account follows the caller. */
|
|
100
136
|
follows_you: boolean;
|
|
101
137
|
}
|
|
138
|
+
/**
|
|
139
|
+
* The caller's relationship with one account (`profiles.isFollowing`): follows
|
|
140
|
+
* in both directions, and whether the caller blocks or mutes it. A server
|
|
141
|
+
* from before the moderation fields, or one whose read of them failed, leaves
|
|
142
|
+
* them out: absent means unknown, never "not blocked".
|
|
143
|
+
*/
|
|
144
|
+
export interface ProfileRelationship extends ViewerRelationship {
|
|
145
|
+
/** The caller blocks this account. */
|
|
146
|
+
is_blocking?: boolean;
|
|
147
|
+
/** The caller mutes this account. */
|
|
148
|
+
is_muting?: boolean;
|
|
149
|
+
}
|
|
102
150
|
export interface FollowUser extends ViewerRelationship {
|
|
103
151
|
id: string;
|
|
104
152
|
name: string;
|
|
@@ -123,9 +171,31 @@ export interface MediaItem {
|
|
|
123
171
|
id: string;
|
|
124
172
|
url: string;
|
|
125
173
|
type: string;
|
|
174
|
+
/**
|
|
175
|
+
* Set on a legacy Minds video whose file is known to be gone (the server probes it): leave it out
|
|
176
|
+
* of Watch and clip queues and show the poster instead. Absent when the file plays or is unjudged.
|
|
177
|
+
*/
|
|
178
|
+
unplayable?: boolean;
|
|
179
|
+
}
|
|
180
|
+
/** The group a post is in, as every post payload carries it. */
|
|
181
|
+
export interface PostCommunity {
|
|
182
|
+
id: string;
|
|
183
|
+
name: string;
|
|
184
|
+
slug: string;
|
|
185
|
+
/**
|
|
186
|
+
* A post in a group that is not public is for the group's members, and is
|
|
187
|
+
* never boosted: offer Boost only when this is 'public'.
|
|
188
|
+
*/
|
|
189
|
+
privacy: 'public' | 'private' | 'hidden';
|
|
126
190
|
}
|
|
127
191
|
export interface Post {
|
|
192
|
+
/** Stored replies a reader can open. */
|
|
128
193
|
reply_count?: number;
|
|
194
|
+
/**
|
|
195
|
+
* The comment count a legacy import brought with the post (minds.com). None of those comments are
|
|
196
|
+
* stored as replies, so it is not in `reply_count`.
|
|
197
|
+
*/
|
|
198
|
+
reply_count_external?: number;
|
|
129
199
|
id: string;
|
|
130
200
|
/** Original Minds numeric GUID when this row came from the legacy import. */
|
|
131
201
|
legacy_guid?: string | null;
|
|
@@ -133,6 +203,8 @@ export interface Post {
|
|
|
133
203
|
content_format: 'plain' | 'markdown';
|
|
134
204
|
author: UserSummary;
|
|
135
205
|
community_id: string | null;
|
|
206
|
+
/** The group the post is in (null when it is in none). */
|
|
207
|
+
community?: PostCommunity | null;
|
|
136
208
|
organization_id: string | null;
|
|
137
209
|
reply_to_id?: string | null;
|
|
138
210
|
/** Present when this post is an X-style repost — the original post is nested. */
|
|
@@ -154,10 +226,25 @@ export interface RepostedPost {
|
|
|
154
226
|
content_format: 'plain' | 'markdown';
|
|
155
227
|
author: UserSummary | null;
|
|
156
228
|
community_id: string | null;
|
|
229
|
+
/** The group the original is in (null when it is in none): it decides what the repost card offers. */
|
|
230
|
+
community?: PostCommunity | null;
|
|
157
231
|
organization_id: string | null;
|
|
158
232
|
tags: Tag[];
|
|
159
233
|
reactions_count: number;
|
|
160
234
|
score: number;
|
|
235
|
+
/** The original's engagement, as the REST payload sends it for the original itself. */
|
|
236
|
+
upvote_count?: number;
|
|
237
|
+
downvote_count?: number;
|
|
238
|
+
/** The viewer's own reaction on the original ('upvote', 'downvote', ...), if any. */
|
|
239
|
+
user_reaction?: string | null;
|
|
240
|
+
/** Stored replies a reader can open. */
|
|
241
|
+
reply_count?: number;
|
|
242
|
+
/** A legacy import's comment count; none of those comments are stored as replies, so it is not in `reply_count`. */
|
|
243
|
+
reply_count_external?: number;
|
|
244
|
+
unique_commenters?: number;
|
|
245
|
+
reposts_count?: number;
|
|
246
|
+
/** Id of the viewer's own repost of the original; delete it to undo the repost. */
|
|
247
|
+
reposted_by_viewer_id?: string | null;
|
|
161
248
|
media: MediaItem[];
|
|
162
249
|
created_at: string;
|
|
163
250
|
}
|
|
@@ -178,6 +265,8 @@ export interface PostReply {
|
|
|
178
265
|
user_reaction?: string | null;
|
|
179
266
|
/** This reply's own reply count, so its sub-thread can be opened. */
|
|
180
267
|
reply_count?: number;
|
|
268
|
+
/** A legacy import's comment count for this reply; not stored as replies, so not in `reply_count`. */
|
|
269
|
+
reply_count_external?: number;
|
|
181
270
|
created_at: string;
|
|
182
271
|
}
|
|
183
272
|
export interface ListPostRepliesParams {
|
|
@@ -243,6 +332,11 @@ export interface ListPostsParams extends PaginationParams {
|
|
|
243
332
|
audience_user_id?: 'me' | string;
|
|
244
333
|
/** Return only posts the authenticated caller has upvoted. */
|
|
245
334
|
liked?: boolean;
|
|
335
|
+
/**
|
|
336
|
+
* Return only the caller's saved posts, newest saved first, each with `bookmarked_at`. Takes only `limit`
|
|
337
|
+
* (at most 50) and `cursor`; see `posts.bookmarked` / `bookmarks.posts`.
|
|
338
|
+
*/
|
|
339
|
+
bookmarked?: boolean;
|
|
246
340
|
}
|
|
247
341
|
export interface SearchPostsParams extends PaginationParams {
|
|
248
342
|
q: string;
|
|
@@ -251,6 +345,42 @@ export interface SearchPostsParams extends PaginationParams {
|
|
|
251
345
|
sort?: 'relevance' | 'new';
|
|
252
346
|
since?: string;
|
|
253
347
|
until?: string;
|
|
348
|
+
/**
|
|
349
|
+
* The `meta.next_cursor` of the previous page. Later relevance pages are slices of the ranking the
|
|
350
|
+
* first page kept (ten minutes), so nothing is skipped or repeated while it lives; after that the page
|
|
351
|
+
* continues past the last post's score boundary. Newest-first pages continue by (created_at, id). A
|
|
352
|
+
* cursor is bound to its search and sort: another's is refused (400 `invalid_cursor`). Without one,
|
|
353
|
+
* `offset` counts.
|
|
354
|
+
*/
|
|
355
|
+
cursor?: string;
|
|
356
|
+
}
|
|
357
|
+
/** A way on from the end of a search: newest first, the open networks, or a topic the matches share. */
|
|
358
|
+
export type SearchFallOverLane = {
|
|
359
|
+
kind: 'new';
|
|
360
|
+
} | {
|
|
361
|
+
kind: 'open_network';
|
|
362
|
+
} | {
|
|
363
|
+
kind: 'tag';
|
|
364
|
+
id: string;
|
|
365
|
+
name: string;
|
|
366
|
+
slug: string;
|
|
367
|
+
};
|
|
368
|
+
/** One page of `posts.search`. */
|
|
369
|
+
export interface SearchPostsResponse extends PaginatedResponse<Post> {
|
|
370
|
+
/**
|
|
371
|
+
* On the first relevance page: up to three of the newest matches that are not on the page, newest
|
|
372
|
+
* first, one per author, for a "Latest" strip above the best matches. Absent when there are none, and on later pages.
|
|
373
|
+
*/
|
|
374
|
+
latest?: Post[];
|
|
375
|
+
/**
|
|
376
|
+
* On the last page (`meta.has_more` false): what exists past the end of this search, so a client
|
|
377
|
+
* can offer a way on instead of a wall. `new` when newest-first goes deeper than the ranking did;
|
|
378
|
+
* `open_network` when the caller's app can search the open networks; `tag` for the topic the
|
|
379
|
+
* page's posts share most. May be empty.
|
|
380
|
+
*/
|
|
381
|
+
fall_over?: {
|
|
382
|
+
lanes: SearchFallOverLane[];
|
|
383
|
+
};
|
|
254
384
|
}
|
|
255
385
|
export type QueryParams = Record<string, unknown>;
|
|
256
386
|
export interface CommunityCreator {
|
|
@@ -273,6 +403,8 @@ export interface Community {
|
|
|
273
403
|
post_count?: number;
|
|
274
404
|
/** Whether the caller is an accepted member. Returned by `list` and `search`. */
|
|
275
405
|
is_member?: boolean;
|
|
406
|
+
/** 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. */
|
|
407
|
+
viewer_role?: 'owner' | 'admin' | 'moderator' | 'member' | null;
|
|
276
408
|
created_at: string;
|
|
277
409
|
}
|
|
278
410
|
/** Filters accepted by the communities directory. */
|
|
@@ -281,14 +413,36 @@ export interface ListCommunitiesParams extends PaginationParams {
|
|
|
281
413
|
member?: boolean;
|
|
282
414
|
/** Restrict personal keys to one organization. Project-bound keys are already scoped. */
|
|
283
415
|
organization_id?: string;
|
|
284
|
-
/**
|
|
285
|
-
|
|
416
|
+
/**
|
|
417
|
+
* Server-side ordering. With `q`, name matches come first and this breaks ties. `joined` (with
|
|
418
|
+
* `member: true`) lists the caller's groups by when they joined, newest first.
|
|
419
|
+
*/
|
|
420
|
+
sort?: 'members' | 'active' | 'newest' | 'joined';
|
|
286
421
|
/**
|
|
287
422
|
* Case-insensitive text search over name and description, up to 200
|
|
288
423
|
* characters. With `member: true` it searches only the caller's own groups.
|
|
289
424
|
*/
|
|
290
425
|
q?: string;
|
|
291
426
|
}
|
|
427
|
+
/** Which join requests to answer at once: named people (at most 100), or the oldest 100 waiting. */
|
|
428
|
+
export type AnswerJoinRequestsInput = {
|
|
429
|
+
user_ids: string[];
|
|
430
|
+
} | {
|
|
431
|
+
all: true;
|
|
432
|
+
};
|
|
433
|
+
/** What a many-at-once answer did: how many it answered, who it skipped, and how many still wait. */
|
|
434
|
+
export interface ApprovedJoinRequests {
|
|
435
|
+
approved: number;
|
|
436
|
+
/** Named people with no pending request here: already in, already answered, banned, or never asked. */
|
|
437
|
+
skipped: string[];
|
|
438
|
+
/** Pending requests left in the group (bans not counted). */
|
|
439
|
+
remaining: number;
|
|
440
|
+
}
|
|
441
|
+
export interface DeclinedJoinRequests {
|
|
442
|
+
declined: number;
|
|
443
|
+
skipped: string[];
|
|
444
|
+
remaining: number;
|
|
445
|
+
}
|
|
292
446
|
/** The caller's own membership row in a group, if any. */
|
|
293
447
|
export type CommunityMembershipStatus = 'accepted' | 'pending' | 'invited' | 'banned';
|
|
294
448
|
export interface CommunityDetail extends Community {
|
|
@@ -862,6 +1016,14 @@ export interface Profile {
|
|
|
862
1016
|
/** A row of a followers/following list: the profile plus the caller's relationship with it. */
|
|
863
1017
|
export interface FollowListProfile extends Profile, ViewerRelationship {
|
|
864
1018
|
}
|
|
1019
|
+
/** Paging and search for a follower or following list (profiles.followers / following). */
|
|
1020
|
+
export interface FollowListParams {
|
|
1021
|
+
limit?: number;
|
|
1022
|
+
offset?: number;
|
|
1023
|
+
/** Name or handle, matched anywhere in either (at most 200 characters); a blank term lists everyone. */
|
|
1024
|
+
search?: string;
|
|
1025
|
+
[key: string]: unknown;
|
|
1026
|
+
}
|
|
865
1027
|
/** An account the caller blocked. */
|
|
866
1028
|
export interface BlockedProfile extends Profile {
|
|
867
1029
|
blocked_at: string;
|
|
@@ -901,10 +1063,24 @@ export interface UpdateProfileInput {
|
|
|
901
1063
|
username?: string;
|
|
902
1064
|
bio?: string | null;
|
|
903
1065
|
image?: string | null;
|
|
1066
|
+
/** The profile banner as an absolute image URL, or null to remove it (PUT /profiles/me has accepted it since #1867). */
|
|
1067
|
+
banner?: string | null;
|
|
904
1068
|
}
|
|
905
1069
|
export interface SearchProfilesParams extends PaginationParams {
|
|
906
1070
|
q: string;
|
|
907
1071
|
}
|
|
1072
|
+
/** A row of `profiles.search`: the profile plus what the ranking read (match x ln(1 + followers + min(posts, 100))). */
|
|
1073
|
+
export interface ProfileSearchResult extends Profile {
|
|
1074
|
+
followers_count: number;
|
|
1075
|
+
posts_count: number;
|
|
1076
|
+
/** Nobody follows it and it never posted: listed after every account with a public life, for a client to fold under "more handles". */
|
|
1077
|
+
empty: boolean;
|
|
1078
|
+
/** An account the operator vouches for: draw the verified badge. */
|
|
1079
|
+
verified: boolean;
|
|
1080
|
+
plus: boolean;
|
|
1081
|
+
pro: boolean;
|
|
1082
|
+
founder: boolean;
|
|
1083
|
+
}
|
|
908
1084
|
export interface SearchMentionsParams {
|
|
909
1085
|
/** What the member typed after `@` (a leading `@` is ignored). */
|
|
910
1086
|
q: string;
|
package/llm.md
CHANGED
|
@@ -367,7 +367,9 @@ await r.auth.signOut(session.token)
|
|
|
367
367
|
```
|
|
368
368
|
|
|
369
369
|
`getSession(token)` and `signOut(token)` send the explicit session token as
|
|
370
|
-
Bearer auth, with a Cookie fallback for native/server runtimes.
|
|
370
|
+
Bearer auth, with a Cookie fallback for native/server runtimes. `getSession`
|
|
371
|
+
also answers `session: { id, expiresAt }`; an API-key client lists sessions with
|
|
372
|
+
`is_current: false`, so it matches `session.id` to mark its own device.
|
|
371
373
|
|
|
372
374
|
Methods: `signUp`, `signIn`, `getSession`, `signOut`, `createApiKey`, `signInAndCreateKey`, `signUpAndCreateKey`, `verifyOtpAndCreateKey`
|
|
373
375
|
|
|
@@ -479,7 +481,7 @@ const { data: rows } = await r.profiles.followers('user_xxx') // each row: is_fo
|
|
|
479
481
|
await r.profiles.mute('user_xxx') // server hides their posts and reposts of them from your feeds
|
|
480
482
|
```
|
|
481
483
|
|
|
482
|
-
Methods: `list`, `me`, `get`, `getByUsername`, `search`, `mentions` (people + agents for an @-picker, follows first), `update`, `follow`, `unfollow`, `followers`, `following`, `followingIds` (your follow set as ids, cursor-paged), `isFollowing
|
|
484
|
+
Methods: `list`, `me`, `get`, `getByUsername`, `search` (ranked by match x ln(1 + followers + min(posts, 100)) over one pool of 200, the same for every page; each row (`ProfileSearchResult`) carries `followers_count`, `posts_count`, `empty` (fold empties under "more handles") and `verified` (the accounts the operator names)), `mentions` (people + agents for an @-picker, follows first), `update`, `follow`, `unfollow`, `followers`, `following`, `followingIds` (your follow set as ids, cursor-paged), `isFollowing` (follows both ways, plus `is_blocking`/`is_muting` when the server knows), `related`, `leaderboard`, `block`, `unblock`, `blocked`, `mute`, `unmute`, `muted`
|
|
483
485
|
|
|
484
486
|
---
|
|
485
487
|
|
|
@@ -580,7 +582,36 @@ await r.posts.react(post.id, { type: 'fire' })
|
|
|
580
582
|
const { data: results } = await r.posts.search({ query: 'shipped', community_id: 'comm_xxx' })
|
|
581
583
|
```
|
|
582
584
|
|
|
583
|
-
Methods: `list`, `get`, `create`, `update`, `delete`, `search
|
|
585
|
+
Methods: `list`, `get`, `create`, `update`, `delete`, `search`, `replies(id, { limit, cursor })` (after `replies_next_cursor` on `get`), `react`, `unreact`, `liked`, `bookmarked`, `bookmark`, `unbookmark`
|
|
586
|
+
|
|
587
|
+
`search` ranks relevance x recency x engagement (recency falls as a power of age: this month outranks years ago) through the promotion safety floor; `sort: 'new'` lists newest first. Page with `meta.next_cursor` as `cursor`: later relevance pages are slices of the ranking the first page kept for ten minutes (nothing skipped or repeated while it lives; after that, the page continues past the last post's score boundary), newest-first pages continue by (created_at, id). A cursor is bound to its search and sort: another search's, or the other sort's, is 400 `invalid_cursor`. The first relevance page carries `latest` (up to three of the newest matches not on the page, one per author, for a Latest strip); the last page carries `fall_over.lanes` (`new`, `open_network`, `tag`): draw a way on, not a wall.
|
|
588
|
+
|
|
589
|
+
### r.bookmarks — Synced saves (per member, per app)
|
|
590
|
+
|
|
591
|
+
```typescript
|
|
592
|
+
await r.bookmarks.save({ kind: 'post', post_id: post.id }) // idempotent; a bare repost saves its original (see data.post_id)
|
|
593
|
+
await r.bookmarks.save({ kind: 'social', protocol: 'bluesky', native_id: 'at://did:plc:.../app.bsky.feed.post/...',
|
|
594
|
+
author: { display_name: 'Alice', handle: 'alice.bsky.social' }, snippet: 'hello' })
|
|
595
|
+
await r.bookmarks.save({ kind: 'article', url: 'https://news.example/a', title: 'Headline', outlet: 'Example' })
|
|
596
|
+
const { data: saved, meta } = await r.bookmarks.posts({ limit: 20 }) // = r.posts.bookmarked(); page on meta.next_cursor
|
|
597
|
+
const { data: items } = await r.bookmarks.list({ kind: 'social' }) // open-network posts and articles, as saved snapshots
|
|
598
|
+
const { data: keys } = await r.bookmarks.keys(undefined, { retry: false }) // every key, for a saved-state cache
|
|
599
|
+
const { data: merged } = await r.bookmarks.merge(deviceItems) // ≤200 per call, each with its own saved_at; idempotent
|
|
600
|
+
await r.bookmarks.unsave(key) // or unsave({ postId }); r.posts.bookmark/unbookmark too
|
|
601
|
+
```
|
|
602
|
+
|
|
603
|
+
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).
|
|
604
|
+
|
|
605
|
+
### r.search
|
|
606
|
+
|
|
607
|
+
```typescript
|
|
608
|
+
// One query, answered as sections. Each has its own time budget; a slow one is listed in `omitted`.
|
|
609
|
+
const { data: answer } = await r.search.query({ q: 'apple', limit: 6 })
|
|
610
|
+
for (const section of answer.order) render(section, answer.sections[section])
|
|
611
|
+
// top_posts, latest_posts, open_network, people, groups, topics, stories, related, trending
|
|
612
|
+
```
|
|
613
|
+
|
|
614
|
+
`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`). 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`).
|
|
584
615
|
|
|
585
616
|
### r.communities
|
|
586
617
|
|
|
@@ -649,7 +680,7 @@ Methods: `list`, `get`, `create`, `delete`
|
|
|
649
680
|
- `r.billing` — Usage tracking, checkout sessions, portal sessions, metered billing
|
|
650
681
|
- `r.appSubscriptions` — Project-scoped consumer plans: `checkout` (409 `subscription_exists` when the member already pays), `status`, `createPortalSession({ return_url, flow?: 'cancel' })` (`flow: 'cancel'` opens Stripe's cancel confirmation; 409 `no_subscription` when there is no plan to cancel), `previewTierChange({ tier })`, `changeTier({ tier, proration_date? })` (in-place, prorated, never a second subscription)
|
|
651
682
|
- `r.boosts` — Promoted posts (micro-USD): `create`, `mine`, `balance`, `pause`, `resume`, `cancel`, `recordEvent`; buy credit with `topup({ amount_cents, return_url })` then `confirmTopup(sessionId)` (credits once); admin `listPendingReview`, `review`, `grant`
|
|
652
|
-
- `r.notifications` — The notification stream: `list({ category: 'social' })` (rows carry `actor`, `reaction_type` and a `post_preview` with the viewer's `user_reaction`/`reposted_by_viewer_id` and counts), `unreadCount({ category })` for badges (`const { data } = await r.notifications.unreadCount({ category: 'social' })`), `markAsRead(id)`, `archive(id)` to dismiss, `markAllAsRead(category)` or `markAllAsRead({ category, organization_id })`, plus push token `registerToken`/`unregisterToken`. A push's `data.notificationId` is the row to mark read on tap. DMs are not in the stream.
|
|
683
|
+
- `r.notifications` — The notification stream: `list({ category: 'social' })` (rows carry `actor`, `reaction_type` and a `post_preview` with the viewer's `user_reaction`/`reposted_by_viewer_id` and counts; an `agent_needs_input` row carries `answered`, true once you have written in the agent's thread since it asked, when its title reads "You answered <agent>"), `unreadCount({ category })` for badges (`const { data } = await r.notifications.unreadCount({ category: 'social' })`), `markAsRead(id)`, `archive(id)` to dismiss, `markAllAsRead(category)` or `markAllAsRead({ category, organization_id })`, plus push token `registerToken`/`unregisterToken`. A push's `data.notificationId` is the row to mark read on tap. DMs are not in the stream.
|
|
653
684
|
- `r.uploads` — Legacy media upload URLs (prefer `r.storage` for new projects)
|
|
654
685
|
- `r.github` — GitHub integration (getRepository, getTree, getFileContent, getCommits, createFromTemplate)
|
|
655
686
|
- `r.deployments` — Deployment management (prefer `r.projects.deploy()` for new projects)
|
|
@@ -679,7 +710,7 @@ await r.appSubscriptions.changeTier({ tier: 'pro', proration_date: quote.prorati
|
|
|
679
710
|
- `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`.
|
|
680
711
|
- 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.
|
|
681
712
|
- 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.
|
|
682
|
-
- 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.
|
|
713
|
+
- 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`). 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? }`.
|
|
683
714
|
- Public trends: require `public_trends` from `publicNetworks()`, then `getPublicTrends({ protocol, limit?, cursor? })`; 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.
|
|
684
715
|
- `listCandidates({ protocol?, limit?, cursor? })` and `getCandidate(id)` are read-only quarantine review, not published-post search. Both require `admin` scope, a project-bound key, a live admin role, and existing project-admin access. The key supplies app/network scope; neither method accepts another project or network. They never invoke refresh, change settings, or publish.
|
|
685
716
|
- Lists return `{ data: ProtocolQuarantinedCandidate[], meta: { limit, has_more, next_cursor } }`; use the opaque `next_cursor`, not offset pagination. The API defaults `limit` to 20 and permits at most 50. Details return `{ data: ProtocolQuarantinedCandidate }`. Every candidate has `state: 'quarantined'` and `content_format: 'plain'`; render its bounded `content` as text, respect `content_truncated`, and distinguish upstream `published_at` from local `observed_at` / `created_at`.
|