@recursiv/sdk 0.7.52 → 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.
@@ -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 {
@@ -243,6 +284,11 @@ export interface ListPostsParams extends PaginationParams {
243
284
  audience_user_id?: 'me' | string;
244
285
  /** Return only posts the authenticated caller has upvoted. */
245
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;
246
292
  }
247
293
  export interface SearchPostsParams extends PaginationParams {
248
294
  q: string;
@@ -251,6 +297,42 @@ export interface SearchPostsParams extends PaginationParams {
251
297
  sort?: 'relevance' | 'new';
252
298
  since?: string;
253
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
+ };
254
336
  }
255
337
  export type QueryParams = Record<string, unknown>;
256
338
  export interface CommunityCreator {
@@ -273,6 +355,8 @@ export interface Community {
273
355
  post_count?: number;
274
356
  /** Whether the caller is an accepted member. Returned by `list` and `search`. */
275
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;
276
360
  created_at: string;
277
361
  }
278
362
  /** Filters accepted by the communities directory. */
@@ -281,14 +365,36 @@ export interface ListCommunitiesParams extends PaginationParams {
281
365
  member?: boolean;
282
366
  /** Restrict personal keys to one organization. Project-bound keys are already scoped. */
283
367
  organization_id?: string;
284
- /** Server-side directory ordering. With `q`, name matches come first and this breaks ties. */
285
- 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';
286
373
  /**
287
374
  * Case-insensitive text search over name and description, up to 200
288
375
  * characters. With `member: true` it searches only the caller's own groups.
289
376
  */
290
377
  q?: string;
291
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
+ }
292
398
  /** The caller's own membership row in a group, if any. */
293
399
  export type CommunityMembershipStatus = 'accepted' | 'pending' | 'invited' | 'banned';
294
400
  export interface CommunityDetail extends Community {
@@ -862,6 +968,14 @@ export interface Profile {
862
968
  /** A row of a followers/following list: the profile plus the caller's relationship with it. */
863
969
  export interface FollowListProfile extends Profile, ViewerRelationship {
864
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
+ }
865
979
  /** An account the caller blocked. */
866
980
  export interface BlockedProfile extends Profile {
867
981
  blocked_at: string;
@@ -901,10 +1015,24 @@ export interface UpdateProfileInput {
901
1015
  username?: string;
902
1016
  bio?: string | null;
903
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;
904
1020
  }
905
1021
  export interface SearchProfilesParams extends PaginationParams {
906
1022
  q: string;
907
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
+ }
908
1036
  export interface SearchMentionsParams {
909
1037
  /** What the member typed after `@` (a leading `@` is ignored). */
910
1038
  q: string;
package/llm.md CHANGED
@@ -479,7 +479,7 @@ const { data: rows } = await r.profiles.followers('user_xxx') // each row: is_fo
479
479
  await r.profiles.mute('user_xxx') // server hides their posts and reposts of them from your feeds
480
480
  ```
481
481
 
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`, `related`, `leaderboard`, `block`, `unblock`, `blocked`, `mute`, `unmute`, `muted`
482
+ 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`, `related`, `leaderboard`, `block`, `unblock`, `blocked`, `mute`, `unmute`, `muted`
483
483
 
484
484
  ---
485
485
 
@@ -580,7 +580,36 @@ await r.posts.react(post.id, { type: 'fire' })
580
580
  const { data: results } = await r.posts.search({ query: 'shipped', community_id: 'comm_xxx' })
581
581
  ```
582
582
 
583
- Methods: `list`, `get`, `create`, `update`, `delete`, `search` (`sort: 'new'` for newest first), `replies(id, { limit, cursor })` (after `replies_next_cursor` on `get`), `react`, `unreact`
583
+ Methods: `list`, `get`, `create`, `update`, `delete`, `search`, `replies(id, { limit, cursor })` (after `replies_next_cursor` on `get`), `react`, `unreact`, `liked`, `bookmarked`, `bookmark`, `unbookmark`
584
+
585
+ `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.
586
+
587
+ ### r.bookmarks — Synced saves (per member, per app)
588
+
589
+ ```typescript
590
+ await r.bookmarks.save({ kind: 'post', post_id: post.id }) // idempotent; a bare repost saves its original (see data.post_id)
591
+ await r.bookmarks.save({ kind: 'social', protocol: 'bluesky', native_id: 'at://did:plc:.../app.bsky.feed.post/...',
592
+ author: { display_name: 'Alice', handle: 'alice.bsky.social' }, snippet: 'hello' })
593
+ await r.bookmarks.save({ kind: 'article', url: 'https://news.example/a', title: 'Headline', outlet: 'Example' })
594
+ const { data: saved, meta } = await r.bookmarks.posts({ limit: 20 }) // = r.posts.bookmarked(); page on meta.next_cursor
595
+ const { data: items } = await r.bookmarks.list({ kind: 'social' }) // open-network posts and articles, as saved snapshots
596
+ const { data: keys } = await r.bookmarks.keys(undefined, { retry: false }) // every key, for a saved-state cache
597
+ const { data: merged } = await r.bookmarks.merge(deviceItems) // ≤200 per call, each with its own saved_at; idempotent
598
+ await r.bookmarks.unsave(key) // or unsave({ postId }); r.posts.bookmark/unbookmark too
599
+ ```
600
+
601
+ 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).
602
+
603
+ ### r.search
604
+
605
+ ```typescript
606
+ // One query, answered as sections. Each has its own time budget; a slow one is listed in `omitted`.
607
+ const { data: answer } = await r.search.query({ q: 'apple', limit: 6 })
608
+ for (const section of answer.order) render(section, answer.sections[section])
609
+ // top_posts, latest_posts, open_network, people, groups, topics, stories, related, trending
610
+ ```
611
+
612
+ `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
613
 
585
614
  ### r.communities
586
615
 
@@ -649,7 +678,7 @@ Methods: `list`, `get`, `create`, `delete`
649
678
  - `r.billing` — Usage tracking, checkout sessions, portal sessions, metered billing
650
679
  - `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
680
  - `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.
681
+ - `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
682
  - `r.uploads` — Legacy media upload URLs (prefer `r.storage` for new projects)
654
683
  - `r.github` — GitHub integration (getRepository, getTree, getFileContent, getCommits, createFromTemplate)
655
684
  - `r.deployments` — Deployment management (prefer `r.projects.deploy()` for new projects)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@recursiv/sdk",
3
- "version": "0.7.52",
3
+ "version": "0.7.53",
4
4
  "description": "TypeScript SDK for the Recursiv API",
5
5
  "license": "FSL-1.1-ALv2",
6
6
  "type": "module",