@recursiv/sdk 0.7.66 → 0.7.68

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 CHANGED
@@ -94,7 +94,7 @@ for await (const chunk of r.agents.chatStream(agent.id, { message: 'Hello!' }))
94
94
  | Resource | Key methods |
95
95
  |----------|------------|
96
96
  | `r.posts` | `create`, `update`, `list`, `liked`, `bookmarked`, `bookmark`, `unbookmark`, `search`, `replies`, `react` |
97
- | `r.bookmarks` | `save`, `unsave`, `posts`, `list`, `keys`, `merge` — the member's saved posts, open-network posts and articles, synced across devices |
97
+ | `r.bookmarks` | `save`, `unsave`, `posts`, `list`, `keys`, `merge` — the member's saved posts, open-network posts and articles, synced across devices; `collections`: `list`, `create`, `update`, `remove`, `add`, `removeItem`, `items`, `posts` — private named collections of saves (0.7.68, where `network.getConfig()` advertises `capabilities.bookmark_collections`) |
98
98
  | `r.feeds` | `top` (what the member is most likely to engage with, rotated by what they saw, with open-network lanes and the lead story), `stories` (the story tail), `publicTop` (a visitor's top, keyless and edge-cacheable) |
99
99
  | `r.communities` | `create`, `join`, `search`, `members` |
100
100
  | `r.chat` | `send`, `dm`, `createGroup` (`encrypted: true` for an end-to-end encrypted group, 0.7.59), `messages` (cursor paging), `acceptRequest`, `declineRequest`, `leave`, `deleteConversation`; a private group's admin: `addMembers`, `removeMember`, `updateGroup` (0.7.59) |
@@ -732,7 +732,13 @@ The catalog lists `compass_filter` on every network when the filter is served to
732
732
  Hide politics (`compass: 'hide'`) also applies on its own, with or without compass judgments on the server, to
733
733
  `sdk.discovery.videos`, `protocols.getPublicTrends` and the radar (`getPublicRadar`, keyless `getSharedRadar`; 0.7.62):
734
734
  it leaves out what is categorised politics (a story not themed yet, or `mixed`, too) and says what it did in
735
- `meta.compass_filter`. These reads answer a side or Mix unfiltered, marked `unavailable`.
735
+ `meta.compass_filter`. These reads answer a side or Mix unfiltered, marked `unavailable`. Every radar story and every
736
+ World Today story (`curator.today()`) carries `political`: true when a quarter of its judged posts are politics, its
737
+ theme or topic is politics, or its headline or summary names an election, a war or troop deployment, a court or justice,
738
+ a legislature, or a party or politician. Hide politics leaves those out whatever their theme, on the radar and on
739
+ `curator.today({ compass: 'hide' })`, where the next story then leads. The stories of `feeds.stories()` and the lead of
740
+ `feeds.top()` carry `political` too (0.7.67), and those reads and `curator.forYouWorld()` leave political stories out
741
+ under Hide politics.
736
742
 
737
743
  `sdk.discovery.recommendations({ kind, category?, limit?, session?, exclude? })` is
738
744
  the app's one recommender for suggestion rails: `kind` is `member`, `group`, `tag`,
package/dist/index.d.ts CHANGED
@@ -195,7 +195,7 @@ export { VideoResource } from './resources/video.js';
195
195
  export type { VideoUploadCreds, CreateVideoUploadResult, VideoStatusInfo, VideoPlaybackToken, VideoEntitlement, } from './resources/video.js';
196
196
  export { AppSubscriptionsResource } from './resources/app-subscriptions.js';
197
197
  export type { AppSubscriptionBoostCredit, AppSubscriptionCheckoutResult, AppSubscriptionPortalResult, AppSubscriptionStatusResult, AppSubscriptionTierChangePreview, AppSubscriptionTierChangeResult, } from './resources/app-subscriptions.js';
198
- export type { CuratorSource, CuratorPrompt, CuratorRunInput, CuratorRunResult, CuratorTodayEdition, CuratorTodayExcerpt, CuratorTodayLeadImage, CuratorTodayRadar, CuratorTodayReaction, CuratorTodayResponse, CuratorTodaySource, CuratorTodayStory, ForYouMeta, ForYouParams, ForYouSurface, ForYouWorldBlock, ForYouWorldBlockType, ForYouWorldCategoryShortcut, ForYouWorldItem, ForYouWorldMeta, ForYouWorldParams, ForYouWorldPart, ForYouWorldResponse, ForYouWorldStory, ForYouWorldStoryTarget, ForYouWorldViral, } from './resources/curator.js';
198
+ export type { CuratorSource, CuratorPrompt, CuratorRunInput, CuratorRunResult, CuratorTodayEdition, CuratorTodayExcerpt, CuratorTodayLeadImage, CuratorTodayParams, CuratorTodayRadar, CuratorTodayReaction, CuratorTodayResponse, CuratorTodaySource, CuratorTodayStory, ForYouMeta, ForYouParams, ForYouSurface, ForYouWorldBlock, ForYouWorldBlockType, ForYouWorldCategoryShortcut, ForYouWorldItem, ForYouWorldMeta, ForYouWorldParams, ForYouWorldPart, ForYouWorldResponse, ForYouWorldStory, ForYouWorldStoryTarget, ForYouWorldViral, } from './resources/curator.js';
199
199
  export { ReportsResource } from './resources/reports.js';
200
200
  export type { ReportTargetType, ReportStatus, CreateReportInput, ReportRecord, ResolveReportInput, } from './resources/reports.js';
201
201
  export { ModerationResource } from './resources/moderation.js';
@@ -203,10 +203,10 @@ export type { ModerationTargetType, ModerationActionType, ModerationAppealStatus
203
203
  export { LinkPreviewResource } from './resources/link-preview.js';
204
204
  export { SEARCH_SECTIONS, SearchResource } from './resources/search.js';
205
205
  export type { SearchAnswer, SearchGroup, SearchOmitReason, SearchParams, SearchPerson, SearchRelated, SearchResponse, SearchSection, SearchStory, SearchTopic } from './resources/search.js';
206
- export { BOOKMARK_MERGE_MAX_ITEMS, BookmarksResource } from './resources/bookmarks.js';
206
+ export { BOOKMARK_MERGE_MAX_ITEMS, BookmarkCollectionsResource, BookmarksResource } from './resources/bookmarks.js';
207
207
  export { FEED_TOP_NETWORKS, FeedsResource } from './resources/feeds.js';
208
208
  export type { FeedStoriesParams, FeedStoriesResponse, FeedStory, FeedTopExplain, FeedTopItem, FeedTopMeta, FeedTopNetwork, FeedTopNetworkCard, FeedTopNetworkEntry, FeedTopParams, FeedTopResponse, FeedTopTarget, PublicFeedTopMeta, PublicFeedTopParams, PublicFeedTopResponse, } from './resources/feeds.js';
209
- export type { BookmarkedPost, BookmarkInput, BookmarkItem, BookmarkKey, BookmarkKeyRow, BookmarkKind, BookmarkMergeItem, BookmarkMergeResult, BookmarkProtocol, BookmarkRequestOptions, BookmarksPage, SavedBookmark, UnsavedBookmark, } from './resources/bookmarks.js';
209
+ export type { BookmarkCollection, BookmarkCollectionInput, BookmarkCollectionKind, BookmarkCollectionPatch, BookmarkCollectionsList, BookmarkCollectionTarget, BookmarkCollectionVisibility, BookmarkedPost, BookmarkInput, BookmarkItem, BookmarkKey, BookmarkKeyRow, BookmarkKeysPage, BookmarkKind, BookmarkMergeItem, BookmarkMergeResult, BookmarkProtocol, BookmarkRequestOptions, BookmarksPage, FiledBookmark, RemovedBookmarkCollection, SavedBookmark, UnfiledBookmark, UnsavedBookmark, } from './resources/bookmarks.js';
210
210
  export { AccountsResource } from './resources/accounts.js';
211
211
  export type { SiblingAccount, SiblingAccountsResponse, SwitchAccountResult, } from './resources/accounts.js';
212
212
  export type { LinkPreview, LinkPreviewResponse, } from './resources/link-preview.js';
package/dist/index.js CHANGED
@@ -257,7 +257,7 @@ export { ReportsResource } from './resources/reports.js';
257
257
  export { ModerationResource } from './resources/moderation.js';
258
258
  export { LinkPreviewResource } from './resources/link-preview.js';
259
259
  export { SEARCH_SECTIONS, SearchResource } from './resources/search.js';
260
- export { BOOKMARK_MERGE_MAX_ITEMS, BookmarksResource } from './resources/bookmarks.js';
260
+ export { BOOKMARK_MERGE_MAX_ITEMS, BookmarkCollectionsResource, BookmarksResource } from './resources/bookmarks.js';
261
261
  export { FEED_TOP_NETWORKS, FeedsResource } from './resources/feeds.js';
262
262
  export { AccountsResource } from './resources/accounts.js';
263
263
  export { OrganizationSettingsResource } from './resources/organization-settings.js';
@@ -122,6 +122,83 @@ export interface BookmarksPage<T> extends PaginatedResponse<T> {
122
122
  next_cursor: string | null;
123
123
  };
124
124
  }
125
+ /** A page of saved keys. `collections` (0.7.68) is there only where the server serves collections. */
126
+ export interface BookmarkKeysPage extends BookmarksPage<BookmarkKeyRow> {
127
+ /**
128
+ * For this page's keys that are filed somewhere: the ids of the collections each sits in, oldest
129
+ * collection first. A key filed nowhere is absent. The whole field is absent while collections are
130
+ * off on the server, so its presence also says collections can be used.
131
+ */
132
+ collections?: Record<BookmarkKey, string[]>;
133
+ }
134
+ /** What a collection holds. Before the cutover only `posts` collections are made (any saved thing: posts, open-network posts, articles). */
135
+ export type BookmarkCollectionKind = 'posts' | 'accounts' | 'tags';
136
+ /** Who may see a collection. Before the cutover every collection is `private`. */
137
+ export type BookmarkCollectionVisibility = 'private' | 'unlisted' | 'public';
138
+ /** A member's named collection of saves in this app (0.7.68). Collections are labels on saves. */
139
+ export interface BookmarkCollection {
140
+ id: string;
141
+ kind: BookmarkCollectionKind;
142
+ /** Unique per member and app: lowercase letters and digits joined by hyphens, at most 64 characters. */
143
+ slug: string;
144
+ /** At most 120 characters. */
145
+ title: string;
146
+ /** At most 500 characters. */
147
+ description: string | null;
148
+ visibility: BookmarkCollectionVisibility;
149
+ item_count: number;
150
+ created_at: string;
151
+ updated_at: string;
152
+ }
153
+ export interface BookmarkCollectionsList {
154
+ /** Oldest first. */
155
+ data: BookmarkCollection[];
156
+ /** The most collections one member keeps in this app, and the most things one collection holds. */
157
+ meta: {
158
+ max_collections: number;
159
+ max_items: number;
160
+ };
161
+ }
162
+ /** A new collection. Without `slug` one is made from the title (made unique with -2, -3, ...). */
163
+ export interface BookmarkCollectionInput {
164
+ title: string;
165
+ description?: string | null;
166
+ slug?: string;
167
+ }
168
+ /** Any of a collection's title, description (null clears it) and slug. */
169
+ export interface BookmarkCollectionPatch {
170
+ title?: string;
171
+ description?: string | null;
172
+ slug?: string;
173
+ }
174
+ export interface RemovedBookmarkCollection {
175
+ id: string;
176
+ removed: boolean;
177
+ }
178
+ export interface FiledBookmark {
179
+ collection_id: string;
180
+ /** The saved thing's key. A bare repost is filed as its original, as saving one is: use this key and `post_id`. */
181
+ key: BookmarkKey;
182
+ filed: true;
183
+ /** False when it was already in the collection: nothing changed. */
184
+ created: boolean;
185
+ /** True when filing it saved it too. */
186
+ saved_now: boolean;
187
+ added_at: string;
188
+ item_count: number;
189
+ post_id?: string;
190
+ }
191
+ export interface UnfiledBookmark {
192
+ collection_id: string;
193
+ key: BookmarkKey;
194
+ filed: false;
195
+ /** False when it was not in the collection. It stays saved either way. */
196
+ removed: boolean;
197
+ }
198
+ /** What to file: a saved key, a Minds post, or an item (saved first if it is not saved yet). */
199
+ export type BookmarkCollectionTarget = BookmarkKey | {
200
+ postId: string;
201
+ } | BookmarkInput;
125
202
  /** Most items one `merge` call takes. */
126
203
  export declare const BOOKMARK_MERGE_MAX_ITEMS = 200;
127
204
  /**
@@ -131,6 +208,56 @@ export declare const BOOKMARK_MERGE_MAX_ITEMS = 200;
131
208
  export interface BookmarkRequestOptions {
132
209
  retry?: boolean;
133
210
  }
211
+ /**
212
+ * Bookmark collections (0.7.68): the member's private, named labels on their saves in this app.
213
+ * Filing something saves it first; one saved thing can sit in several collections; unsaving it takes it
214
+ * out of every one; deleting a collection leaves everything it held saved.
215
+ *
216
+ * Off on a server until it advertises `capabilities.bookmark_collections` (`network.getConfig()`): until
217
+ * then, and until its tables are applied, every call rejects with a 503 whose code is `lists_unavailable`
218
+ * (saving still works). A collection that is not the member's is 404 `collection_not_found`. Caps: 409
219
+ * `collection_limit` (collections per member) and `collection_item_limit` (things per collection).
220
+ */
221
+ export declare class BookmarkCollectionsResource {
222
+ private client;
223
+ constructor(client: HttpClient);
224
+ /** The member's collections in this app, oldest first, with the caps. */
225
+ list(options?: BookmarkRequestOptions): Promise<BookmarkCollectionsList>;
226
+ /**
227
+ * Make a private collection. 409 `slug_taken` for a slug the member already uses. Retried on a 5xx like any
228
+ * POST: pass an `idempotencyKey` so a retry cannot make a second collection.
229
+ */
230
+ create(input: BookmarkCollectionInput, options?: BookmarkRequestOptions & {
231
+ idempotencyKey?: string;
232
+ }): Promise<SingleResponse<BookmarkCollection>>;
233
+ /** Rename, re-describe or re-slug a collection. */
234
+ update(collectionId: string, patch: BookmarkCollectionPatch): Promise<SingleResponse<BookmarkCollection>>;
235
+ /** Delete a collection. Idempotent; what it held stays saved. */
236
+ remove(collectionId: string, options?: BookmarkRequestOptions): Promise<SingleResponse<RemovedBookmarkCollection>>;
237
+ /**
238
+ * File something in a collection; idempotent. A key or `{ postId }` files what is saved, and a Minds post is
239
+ * saved first when it is not (404 `post_not_found` if the member cannot read it); a key alone that is not saved
240
+ * is 409 `not_saved`. An item (a Minds post, open-network post or article) goes in one request that saves it if
241
+ * it is not saved and files it together: when the collection is missing (404), full (409 `collection_item_limit`)
242
+ * or unavailable (503), or the member's saves are at their cap (409 `bookmark_limit`), nothing is saved.
243
+ */
244
+ add(collectionId: string, target: BookmarkCollectionTarget, options?: BookmarkRequestOptions): Promise<SingleResponse<FiledBookmark>>;
245
+ /** Take something out of a collection. Idempotent; it stays saved, and in any other collection. */
246
+ removeItem(collectionId: string, target: BookmarkKey | {
247
+ postId: string;
248
+ }, options?: BookmarkRequestOptions): Promise<SingleResponse<UnfiledBookmark>>;
249
+ /** The open-network posts and articles filed in a collection, newest saved first. Page on `meta.next_cursor`. */
250
+ items(collectionId: string, params?: {
251
+ kind?: 'social' | 'article';
252
+ limit?: number;
253
+ cursor?: string;
254
+ }, options?: BookmarkRequestOptions): Promise<BookmarksPage<BookmarkItem>>;
255
+ /** The Minds posts filed in a collection, newest saved first, hydrated like the feed. Page on `meta.next_cursor`. */
256
+ posts(collectionId: string, params?: {
257
+ limit?: number;
258
+ cursor?: string;
259
+ }, options?: BookmarkRequestOptions): Promise<BookmarksPage<BookmarkedPost>>;
260
+ }
134
261
  /**
135
262
  * Synced bookmarks: the signed-in member's saved Minds posts, open-network posts and articles in this app,
136
263
  * across every device they sign in on. Private to the member; needs the member's own app key.
@@ -140,6 +267,8 @@ export interface BookmarkRequestOptions {
140
267
  */
141
268
  export declare class BookmarksResource {
142
269
  private client;
270
+ /** Named collections of the member's saves (0.7.68). */
271
+ readonly collections: BookmarkCollectionsResource;
143
272
  constructor(client: HttpClient);
144
273
  /** Save one item. Idempotent: saving what is saved changes nothing. 409 `bookmark_limit` past the member's limit. */
145
274
  save(item: BookmarkInput, options?: BookmarkRequestOptions): Promise<SingleResponse<SavedBookmark>>;
@@ -147,22 +276,27 @@ export declare class BookmarksResource {
147
276
  unsave(target: BookmarkKey | {
148
277
  postId: string;
149
278
  }, options?: BookmarkRequestOptions): Promise<SingleResponse<UnsavedBookmark>>;
150
- /** Saved Minds posts, newest saved first, hydrated like the feed. Page on `meta.next_cursor`. */
279
+ /** Saved Minds posts, newest saved first, hydrated like the feed; `collection` narrows to one collection (0.7.68). Page on `meta.next_cursor`. */
151
280
  posts(params?: {
152
281
  limit?: number;
153
282
  cursor?: string;
283
+ collection?: string;
154
284
  }, options?: BookmarkRequestOptions): Promise<BookmarksPage<BookmarkedPost>>;
155
- /** Saved open-network posts and articles, newest saved first. Page on `meta.next_cursor`. */
285
+ /** Saved open-network posts and articles, newest saved first; `collection` narrows to one collection (0.7.68). Page on `meta.next_cursor`. */
156
286
  list(params?: {
157
287
  kind?: 'social' | 'article';
158
288
  limit?: number;
159
289
  cursor?: string;
290
+ collection?: string;
160
291
  }, options?: BookmarkRequestOptions): Promise<BookmarksPage<BookmarkItem>>;
161
- /** Every saved key (hidden posts included), newest saved first, up to 500 a page. For a saved-state cache. */
292
+ /**
293
+ * Every saved key (hidden posts included), newest saved first, up to 500 a page. For a saved-state cache.
294
+ * Where collections are served, `collections` says which collections this page's keys are filed in.
295
+ */
162
296
  keys(params?: {
163
297
  limit?: number;
164
298
  cursor?: string;
165
- }, options?: BookmarkRequestOptions): Promise<BookmarksPage<BookmarkKeyRow>>;
299
+ }, options?: BookmarkRequestOptions): Promise<BookmarkKeysPage>;
166
300
  /**
167
301
  * Upload a device's saves, e.g. when its member signs in: at most 200 items a call. Idempotent (a second
168
302
  * merge of the same items is a no-op); invalid items and posts the member cannot read are `skipped`, and
@@ -1,5 +1,68 @@
1
1
  /** Most items one `merge` call takes. */
2
2
  export const BOOKMARK_MERGE_MAX_ITEMS = 200;
3
+ const itemPath = (collectionId, key) => `/bookmarks/collections/${encodeURIComponent(collectionId)}/items/${encodeURIComponent(key)}`;
4
+ /**
5
+ * Bookmark collections (0.7.68): the member's private, named labels on their saves in this app.
6
+ * Filing something saves it first; one saved thing can sit in several collections; unsaving it takes it
7
+ * out of every one; deleting a collection leaves everything it held saved.
8
+ *
9
+ * Off on a server until it advertises `capabilities.bookmark_collections` (`network.getConfig()`): until
10
+ * then, and until its tables are applied, every call rejects with a 503 whose code is `lists_unavailable`
11
+ * (saving still works). A collection that is not the member's is 404 `collection_not_found`. Caps: 409
12
+ * `collection_limit` (collections per member) and `collection_item_limit` (things per collection).
13
+ */
14
+ export class BookmarkCollectionsResource {
15
+ client;
16
+ constructor(client) {
17
+ this.client = client;
18
+ }
19
+ /** The member's collections in this app, oldest first, with the caps. */
20
+ list(options) {
21
+ return this.client.get('/bookmarks/collections', undefined, options);
22
+ }
23
+ /**
24
+ * Make a private collection. 409 `slug_taken` for a slug the member already uses. Retried on a 5xx like any
25
+ * POST: pass an `idempotencyKey` so a retry cannot make a second collection.
26
+ */
27
+ create(input, options) {
28
+ return this.client.post('/bookmarks/collections', input, options);
29
+ }
30
+ /** Rename, re-describe or re-slug a collection. */
31
+ update(collectionId, patch) {
32
+ return this.client.patch(`/bookmarks/collections/${encodeURIComponent(collectionId)}`, patch);
33
+ }
34
+ /** Delete a collection. Idempotent; what it held stays saved. */
35
+ remove(collectionId, options) {
36
+ return this.client.delete(`/bookmarks/collections/${encodeURIComponent(collectionId)}`, options);
37
+ }
38
+ /**
39
+ * File something in a collection; idempotent. A key or `{ postId }` files what is saved, and a Minds post is
40
+ * saved first when it is not (404 `post_not_found` if the member cannot read it); a key alone that is not saved
41
+ * is 409 `not_saved`. An item (a Minds post, open-network post or article) goes in one request that saves it if
42
+ * it is not saved and files it together: when the collection is missing (404), full (409 `collection_item_limit`)
43
+ * or unavailable (503), or the member's saves are at their cap (409 `bookmark_limit`), nothing is saved.
44
+ */
45
+ add(collectionId, target, options) {
46
+ if (typeof target === 'string')
47
+ return this.client.put(itemPath(collectionId, target), undefined, options);
48
+ if ('postId' in target)
49
+ return this.client.put(itemPath(collectionId, `post:${target.postId}`), undefined, options);
50
+ return this.client.post(`/bookmarks/collections/${encodeURIComponent(collectionId)}/items`, target, options);
51
+ }
52
+ /** Take something out of a collection. Idempotent; it stays saved, and in any other collection. */
53
+ removeItem(collectionId, target, options) {
54
+ const key = typeof target === 'string' ? target : `post:${target.postId}`;
55
+ return this.client.delete(itemPath(collectionId, key), options);
56
+ }
57
+ /** The open-network posts and articles filed in a collection, newest saved first. Page on `meta.next_cursor`. */
58
+ items(collectionId, params, options) {
59
+ return this.client.get('/bookmarks', { ...params, collection: collectionId }, options);
60
+ }
61
+ /** The Minds posts filed in a collection, newest saved first, hydrated like the feed. Page on `meta.next_cursor`. */
62
+ posts(collectionId, params, options) {
63
+ return this.client.get('/posts', { bookmarked: true, ...params, collection: collectionId }, options);
64
+ }
65
+ }
3
66
  /**
4
67
  * Synced bookmarks: the signed-in member's saved Minds posts, open-network posts and articles in this app,
5
68
  * across every device they sign in on. Private to the member; needs the member's own app key.
@@ -9,8 +72,11 @@ export const BOOKMARK_MERGE_MAX_ITEMS = 200;
9
72
  */
10
73
  export class BookmarksResource {
11
74
  client;
75
+ /** Named collections of the member's saves (0.7.68). */
76
+ collections;
12
77
  constructor(client) {
13
78
  this.client = client;
79
+ this.collections = new BookmarkCollectionsResource(client);
14
80
  }
15
81
  /** Save one item. Idempotent: saving what is saved changes nothing. 409 `bookmark_limit` past the member's limit. */
16
82
  save(item, options) {
@@ -21,15 +87,18 @@ export class BookmarksResource {
21
87
  const key = typeof target === 'string' ? target : `post:${target.postId}`;
22
88
  return this.client.delete(`/bookmarks/${encodeURIComponent(key)}`, options);
23
89
  }
24
- /** Saved Minds posts, newest saved first, hydrated like the feed. Page on `meta.next_cursor`. */
90
+ /** Saved Minds posts, newest saved first, hydrated like the feed; `collection` narrows to one collection (0.7.68). Page on `meta.next_cursor`. */
25
91
  posts(params, options) {
26
92
  return this.client.get('/posts', { bookmarked: true, ...params }, options);
27
93
  }
28
- /** Saved open-network posts and articles, newest saved first. Page on `meta.next_cursor`. */
94
+ /** Saved open-network posts and articles, newest saved first; `collection` narrows to one collection (0.7.68). Page on `meta.next_cursor`. */
29
95
  list(params, options) {
30
96
  return this.client.get('/bookmarks', params, options);
31
97
  }
32
- /** Every saved key (hidden posts included), newest saved first, up to 500 a page. For a saved-state cache. */
98
+ /**
99
+ * Every saved key (hidden posts included), newest saved first, up to 500 a page. For a saved-state cache.
100
+ * Where collections are served, `collections` says which collections this page's keys are filed in.
101
+ */
33
102
  keys(params, options) {
34
103
  return this.client.get('/bookmarks/keys', params, options);
35
104
  }
@@ -344,6 +344,12 @@ export interface CuratorTodayStory {
344
344
  headline: string;
345
345
  dek: string | null;
346
346
  topic: string | null;
347
+ /**
348
+ * Whether the story is political: its headline, dek or topic names an election, a war or troop deployment, a court or
349
+ * justice, a legislature, or a party or politician, or the radar story it seeded is political. Hide politics
350
+ * (`today({ compass: 'hide' })`) leaves these out. Absent from a server before it was served.
351
+ */
352
+ political?: boolean;
347
353
  /** Outlet links for a world story; null for a Minds story. */
348
354
  sources: CuratorTodaySource[] | null;
349
355
  /** Distinct outlets behind a world story; null for a Minds story. */
@@ -380,10 +386,22 @@ export interface CuratorTodayEdition {
380
386
  export interface CuratorTodayResponse {
381
387
  /** null when the network has no published edition yet. */
382
388
  data: CuratorTodayEdition | null;
389
+ /**
390
+ * `compass_filter`: present when the read named a filtering `compass`: what Hide politics did to the stories (counts
391
+ * only), or status 'unavailable' when it was not applied (not served to this reader yet, or a side or Mix).
392
+ */
383
393
  meta: {
384
394
  generating: boolean;
395
+ compass_filter?: CompassFilterMeta;
385
396
  };
386
397
  }
398
+ export interface CuratorTodayParams {
399
+ /**
400
+ * A Politics choice. Hide politics (`hide`) leaves out the political stories (CuratorTodayStory.political), and the
401
+ * next story leads. Any other choice is answered unfiltered, marked `unavailable`.
402
+ */
403
+ compass?: string;
404
+ }
387
405
  export declare class CuratorResource {
388
406
  private client;
389
407
  constructor(client: HttpClient);
@@ -392,7 +410,7 @@ export declare class CuratorResource {
392
410
  * (set by the open web, ranked by outlet count) come first; Minds
393
411
  * conversation stories follow. Requires `posts:read`.
394
412
  */
395
- today(): Promise<CuratorTodayResponse>;
413
+ today(params?: CuratorTodayParams): Promise<CuratorTodayResponse>;
396
414
  /**
397
415
  * Fetch the supplied sources in parallel, annotate each top item with
398
416
  * the supplied prompt template, and insert audience-scoped posts into
@@ -8,8 +8,8 @@ export class CuratorResource {
8
8
  * (set by the open web, ranked by outlet count) come first; Minds
9
9
  * conversation stories follow. Requires `posts:read`.
10
10
  */
11
- today() {
12
- return this.client.get('/curator/today');
11
+ today(params = {}) {
12
+ return params.compass ? this.client.get('/curator/today', { compass: params.compass }) : this.client.get('/curator/today');
13
13
  }
14
14
  /**
15
15
  * Fetch the supplied sources in parallel, annotate each top item with
@@ -97,6 +97,12 @@ export interface FeedStory {
97
97
  kind: 'breaking' | 'viral' | 'story';
98
98
  breaking: boolean;
99
99
  theme: string | null;
100
+ /**
101
+ * Whether the story is political whatever its theme: its headline names an election, a war or troop deployment, a
102
+ * court or justice, a legislature, or a party or politician, its theme is politics, or a quarter of its judged posts
103
+ * are politics. Hide politics (`compass: 'hide'`) leaves such a story out. Absent from a server older than 0.7.67.
104
+ */
105
+ political?: boolean;
100
106
  sources: Partial<Record<'outlet' | 'hackernews' | 'bluesky' | 'activitypub' | 'nostr' | 'minds', number>>;
101
107
  media: {
102
108
  images: number;
@@ -7,8 +7,14 @@ interface NetworkConfig {
7
7
  primary_color: string | null;
8
8
  theme: string | null;
9
9
  features: Record<string, unknown>;
10
+ /**
11
+ * `bookmark_collections` (0.7.68; absent on older servers): this server serves bookmark collections
12
+ * (`bookmarks.collections`). Show collections only when it is true, and still treat a 503
13
+ * `lists_unavailable` as off.
14
+ */
10
15
  capabilities: {
11
16
  ws_token: boolean;
17
+ bookmark_collections?: boolean;
12
18
  };
13
19
  auth_methods: string;
14
20
  default_template_name: string | null;
@@ -1206,6 +1206,12 @@ export interface SocialRadarCluster {
1206
1206
  };
1207
1207
  /** A browse-category id or `mixed`, from cached judgments only. */
1208
1208
  theme?: string;
1209
+ /**
1210
+ * Whether the story is political, whatever its theme: a quarter or more of its judged posts are politics, its theme is
1211
+ * politics, or its headline or summary names an election, a war or troop deployment, a court or justice, a legislature,
1212
+ * or a party or politician. Hide politics (`compass: 'hide'`) leaves these out. Absent from a server before it was served.
1213
+ */
1214
+ political?: boolean;
1209
1215
  sources: Partial<Record<SocialRadarSourceKind, number>>;
1210
1216
  corroboration: number;
1211
1217
  laneMix: {
@@ -1266,8 +1272,9 @@ export interface SocialRadarParams {
1266
1272
  category?: string;
1267
1273
  limit?: number;
1268
1274
  /**
1269
- * A Politics choice. Hide politics (`hide`) leaves out the stories whose theme is politics, and the ones not themed yet
1270
- * or themed `mixed` (a story has no compass placement, so it fails closed); the stories after them fill the list.
1275
+ * A Politics choice. Hide politics (`hide`) leaves out the political stories (SocialRadarCluster.political) whatever
1276
+ * their theme, and the ones not themed yet or themed `mixed` (a story has no compass placement, so it fails closed);
1277
+ * the stories after them fill the list.
1271
1278
  * A side or Mix is answered unfiltered, marked `unavailable`; the keyless `getSharedRadar` takes only `all` or `hide`.
1272
1279
  */
1273
1280
  compass?: string;
@@ -1281,7 +1288,7 @@ export interface SocialRadarPosts {
1281
1288
  /** 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). */
1282
1289
  export interface SocialRadarStoryArchive {
1283
1290
  /** The story as the radar last published it. Articles keep their headline and link; a post is a member by reference only (no `title`). */
1284
- cluster: Pick<SocialRadarCluster, 'id' | 'label' | 'kind' | 'seed' | 'theme' | 'sources' | 'laneMix'> & {
1291
+ cluster: Pick<SocialRadarCluster, 'id' | 'label' | 'kind' | 'seed' | 'theme' | 'political' | 'sources' | 'laneMix'> & {
1285
1292
  members: Array<Pick<SocialRadarMember, 'kind' | 'key' | 'url' | 'publishedAt' | 'outlet'> & {
1286
1293
  title?: string;
1287
1294
  }>;
package/dist/types.d.ts CHANGED
@@ -698,6 +698,20 @@ export interface Conversation {
698
698
  /** Whether `chat.leave()` is allowed for the caller. */
699
699
  can_leave?: boolean;
700
700
  created_at: string;
701
+ /**
702
+ * When the conversation last had activity, the time `conversations()` sorts
703
+ * by (0.7.67): its newest message, or, in an end-to-end encrypted
704
+ * (`mls_v2`) conversation, the relay's receive time of its newest encrypted
705
+ * message when that is later; its creation when it has no messages.
706
+ * In an `mls_v2` conversation, membership and key changes and quiet events
707
+ * (a reaction, an edit, a delete) do not move it. In any other
708
+ * conversation it is read from the messages there now, so deleting the
709
+ * newest message moves it back, to the previous message or to the
710
+ * creation: do not keep the later of a cached and a served value. A time
711
+ * only, never content, so a device that cannot read an encrypted chat can
712
+ * still show when it last moved. Omitted by older APIs.
713
+ */
714
+ last_activity_at?: string;
701
715
  }
702
716
  export interface ConversationRequestResult {
703
717
  id: string;
@@ -733,6 +747,12 @@ export interface ConversationDetail {
733
747
  /** Whether `chat.leave()` is allowed for the caller. */
734
748
  can_leave?: boolean;
735
749
  created_at: string;
750
+ /**
751
+ * As `Conversation.last_activity_at` (0.7.67). Served by `conversation()`;
752
+ * omitted by `createGroup()`, to a member who declined the request, and by
753
+ * older APIs.
754
+ */
755
+ last_activity_at?: string;
736
756
  }
737
757
  export type ChatMemberRole = 'admin' | 'member';
738
758
  /**
package/llm.md CHANGED
@@ -504,6 +504,8 @@ Scroll back with `messages(id, { cursor: page.meta.next_cursor })`, not a growin
504
504
 
505
505
  Private groups (0.7.59): each member has `role` (`admin` or `member`) and, in a group or a one-on-one chat, `request_status`: `accepted` from the start for the person who wrote, anyone who follows them and agents, else `pending` until that person accepts or replies (a decline reads `pending` to anyone else), so `accepted` alone does not mean they accepted anything. Only the admin (the creator, then the longest-standing person left) may `addMembers(id, { member_ids })`, `removeMember(id, userId)` and `updateGroup(id, { name })` (else 403 `not_admin`). `createGroup({ name, member_ids, encrypted: true, device_id? })` makes an end-to-end encrypted group of people in the pilot, at most 8 counting pending invitees, or throws 409 `conversation_not_eligible` with `details.reason` and creates nothing. `r.realtime.onChatMembers` / `onChatConversation` tell you when people or the name change.
506
506
 
507
+ Activity time (0.7.67): conversation rows, `communityConversation()` and `conversation(id)` carry `last_activity_at`, the time `conversations()` sorts by: the newest message, or, in an end-to-end encrypted (`mls_v2`) conversation, the relay's receive time of its newest encrypted message when that is later (membership and key changes and quiet events such as reactions do not move it); its creation when it has no messages. In any other conversation, deleting the newest message moves it back to the previous message (or the creation), so do not keep the later of a cached and a served value. A time only, never content: show it on a row the device cannot decrypt. Omitted by older APIs, and by `conversation(id)` to a member who declined the request.
508
+
507
509
  ### r.e2ee — End-to-end encryption transport (flag-gated)
508
510
 
509
511
  Relays opaque MLS envelopes, public device credentials and one-time key packages for the shared E2EE core; the server never sees keys or plaintext. Requires the `e2eeV2` network feature flag (otherwise `feature_disabled`) and a key acting as the account owner (agent-scoped keys get `agent_key_not_allowed`). The realm is the network id. Only a direct conversation between two people with an accepted request can start encryption (`conversation_not_eligible` otherwise), and every later append to it is checked the same way again (both people still accepted, still in the pilot, still only the two of them), so a client need not read the policy before each send; a resend of an event already stored still returns `duplicate: true`, even once a Welcome's target device is gone; blocks apply (`blocked`), to resends too; commits must send `expected_commit_seq`.
@@ -639,6 +641,23 @@ await r.bookmarks.unsave(key) // or uns
639
641
 
640
642
  Newest saved first. Pages can be short while they skip posts the member can no longer read: page on `meta.next_cursor`, not length. Needs the member's own app key (else 403 `project_binding_required`). 503 `bookmarks_unavailable` means the server has no synced bookmarks yet: keep saves on the device (pass `{ retry: false }` to the probe so the 503 is not retried).
641
643
 
644
+ Collections (0.7.68) are private, named labels on saves: filing saves first, one thing can sit in several collections, unsaving takes it out of every one, and deleting a collection leaves what it held saved. Use them only when `(await r.network.getConfig()).data.capabilities.bookmark_collections` is true; still treat 503 `lists_unavailable` as off.
645
+
646
+ ```typescript
647
+ const { data: lists, meta: caps } = await r.bookmarks.collections.list() // oldest first; caps.max_collections (25), caps.max_items (1000)
648
+ const { data: list } = await r.bookmarks.collections.create({ title: 'Read later' }, { idempotencyKey }) // private; slug from the title
649
+ await r.bookmarks.collections.add(list.id, { postId: post.id }) // saves it first if needed; idempotent
650
+ await r.bookmarks.collections.add(list.id, socialOrArticleItem) // one request: saved and filed together, or neither
651
+ await r.bookmarks.collections.removeItem(list.id, key) // it stays saved
652
+ const { data: posts } = await r.bookmarks.collections.posts(list.id) // = r.bookmarks.posts({ collection: list.id })
653
+ const { data: items } = await r.bookmarks.collections.items(list.id) // open-network posts and articles in it
654
+ const { collections: filedIn } = await r.bookmarks.keys() // { [key]: [collection ids] }; absent while collections are off
655
+ await r.bookmarks.collections.update(list.id, { title: 'Later' })
656
+ await r.bookmarks.collections.remove(list.id)
657
+ ```
658
+
659
+ 404 `collection_not_found` for a collection that is not the member's; 409 `collection_limit`, `collection_item_limit`, `slug_taken`, `bookmark_limit` (filing that would save past the saves cap), and `not_saved` (a key that is not saved, sent without its item); 400 `not_supported_yet` for a kind other than posts or a visibility other than private.
660
+
642
661
  ### r.curator.forYouWorld — For You as the world by the member's interests
643
662
 
644
663
  ```typescript
@@ -733,6 +752,8 @@ Scroll back with `messages(id, { cursor: page.meta.next_cursor })`, not a growin
733
752
 
734
753
  Private groups (0.7.59): each member has `role` (`admin` or `member`) and, in a group or a one-on-one chat, `request_status`: `accepted` from the start for the person who wrote, anyone who follows them and agents, else `pending` until that person accepts or replies (a decline reads `pending` to anyone else), so `accepted` alone does not mean they accepted anything. Only the admin (the creator, then the longest-standing person left) may `addMembers(id, { member_ids })`, `removeMember(id, userId)` and `updateGroup(id, { name })` (else 403 `not_admin`). `createGroup({ name, member_ids, encrypted: true, device_id? })` makes an end-to-end encrypted group of people in the pilot, at most 8 counting pending invitees, or throws 409 `conversation_not_eligible` with `details.reason` and creates nothing. `r.realtime.onChatMembers` / `onChatConversation` tell you when people or the name change.
735
754
 
755
+ Activity time (0.7.67): conversation rows, `communityConversation()` and `conversation(id)` carry `last_activity_at`, the time `conversations()` sorts by: the newest message, or, in an end-to-end encrypted (`mls_v2`) conversation, the relay's receive time of its newest encrypted message when that is later (membership and key changes and quiet events such as reactions do not move it); its creation when it has no messages. In any other conversation, deleting the newest message moves it back to the previous message (or the creation), so do not keep the later of a cached and a served value. A time only, never content: show it on a row the device cannot decrypt. Omitted by older APIs, and by `conversation(id)` to a member who declined the request.
756
+
736
757
  ### r.tags
737
758
 
738
759
  ```typescript
@@ -783,6 +804,7 @@ await r.appSubscriptions.changeTier({ tier: 'pro', proration_date: quote.prorati
783
804
  - 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`; `feed` with `ref` is declared in 0.7.60 but not sent yet, so skip kinds you cannot open). 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? }`. Watch's Minds videos: `const { data, meta } = await r.discovery.videos({ limit: 12 })` returns public video posts in hot order from a list shared per app (rebuilt about once a minute), each re-checked per call for the viewer; page on `meta.next_cursor`.
784
805
  - Viral read (0.7.59; off until the server enables it for the app): `const { data } = await r.protocols.getPublicViral({ media: 'video', category: 'science', limit: 20 })` (member key, users:read + posts:read, never cached) or keyless `visitor.protocols.getSharedViral(projectId, { media: 'video', compass: 'hide' })` (visitors send compass `all` or `hide` only). Params, all optional: `network` (all, minds, bluesky, activitypub, nostr), `category`, `media: 'video'`, `limit` 5/10/20/40, `cursor` (the last page's; a cursor whose 30-minute build expired ends the list), `compass`. Each item: `key` (stable; drop ones already shown), `protocol`, `post` (checking `protocol` narrows it: `ViralMindsCard` for minds, open at /post/:id; otherwise a `SocialPost`), `label` viral/rising/hot, `score`, `rank`, `category`, `velocity` (show counts with `countsReportedBy` as reported, never verified), `spread`. A visitor's read serves media only where the server cleared it, so even with `media: 'video'` some items arrive with the video withheld (empty `media`): `import { viralItemHasVideo } from '@recursiv/sdk'` and keep `data.items.filter(viralItemHasVideo)` before a vertical video player. `status` ok/building/screening/off: off resolves with no items, and `getSharedViral` also resolves an older API's 401 `missing_api_key` as off; `getPublicViral` throws every 401/403 (never off), so catch it and skip that step.
785
806
  - Public trends: require `public_trends` from `publicNetworks()`, then `getPublicTrends({ protocol, limit?, cursor?, compass? })` (`compass: 'hide'` leaves out politics, 0.7.62); 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.
807
+ - Political stories (0.7.67): every radar story (`getPublicRadar`, `getSharedRadar`, a story read) and every World Today story (`r.curator.today()`) carries `political`: true when a quarter of its judged posts are politics, its theme or topic is politics, or its headline or summary names an election, a war or troop deployment, a court or justice, a legislature, or a party or politician. `compass: 'hide'` leaves those out whatever their theme; `r.curator.today({ compass: 'hide' })` does the same to the front page, the next story leads, and `meta.compass_filter` counts what was left out. A story from `r.feeds.stories()` or a `r.feeds.top()` lead carries `political` too, and both reads, and `r.curator.forYouWorld()`, leave political stories out under `compass: 'hide'` whatever their theme. An older server omits `political`: decide by `theme` as before.
786
808
  - Post language: a `SocialPost` may carry `language`, the lower-case primary subtag the post declares (`'de'`, `'en'`; Bluesky `langs[0]`, Mastodon `language`, a Nostr NIP-32 `ISO-639-1` label). It is absent when the post declares none (most Nostr notes) and on posts from the source index; it is never guessed and no post is dropped for it. Order by it (the viewer's language first, undeclared next), never filter on it.
787
809
  - `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.
788
810
  - 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`.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@recursiv/sdk",
3
- "version": "0.7.66",
3
+ "version": "0.7.68",
4
4
  "description": "TypeScript SDK for the Recursiv API",
5
5
  "license": "FSL-1.1-ALv2",
6
6
  "type": "module",