@recursiv/sdk 0.7.37 → 0.7.40

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
@@ -595,3 +595,17 @@ Standalone public text publication uses `protocols.prepareNativePost({ clientMut
595
595
  For Bluesky accounts advertising `quote`, prepare the same publication with `quote: { protocol: 'bluesky', nativeId: sourceAtUri }`. The server resolves the exact source CID using each connected account, checks quote permission and Minds safety, and rechecks before writing. Review each delivery’s `quote` alongside the account and comment before submitting. Changed or unavailable sources cannot publish; uncertain deliveries reconcile the original record key. ActivityPub/Nostr quotes are not provided by this method. Public post reads include at most one filtered `quote` with `state: 'available'` and `post`, or `state: 'unavailable'` without source content. Available quotes may include `embeddedReferences`: exact standalone source-reference lines in the unchanged outer `text`. Omit them only when the quoted original is actually visible; retain them for unavailable or locally hidden quotes. Nostr references are decoded and matched by the server, without following relay hints.
596
596
 
597
597
  Topic browsing can request `getPublicTrends({ protocol: "bluesky", includePreviews: true })`. Optional `SocialTrend.preview` is a real image-bearing post from that topic, never a generated illustration. The server samples at most eight posts for each of ten topics, with three concurrent reads and a shared 2.5-second deadline; missing images leave usable text topics. App moderation and user source controls filter previews on every request. Bluesky CDN image attachments, quote-with-media images, and unlabeled link-preview thumbnails are supported; omitted content retains the source link.
598
+
599
+ ### Continuous public discovery
600
+
601
+ Create one `sdk.discovery.createFeed({ networks, category, source, learning })`
602
+ per signed-in viewer and filter. Pass the app-advertised network catalog, then call
603
+ `next()` as the reader approaches the end. Each page contains safe json-render
604
+ module specs and native or protocol post objects. Preserve their existing source
605
+ authority and interactive controls. `retryFailed()` explicitly retries unavailable
606
+ sources; create a new session to refresh exhausted source windows.
607
+
608
+ Optional `DiscoveryLearning` consumes real visible impressions and explicit
609
+ engagement/hide outcomes. Persist its count-only snapshot per viewer; never share
610
+ a session or learning state across accounts. It starts conservatively and does
611
+ not imply measured retention improvement.
package/dist/index.d.ts CHANGED
@@ -1,3 +1,6 @@
1
+ export * from './resources/discovery-learning.js';
2
+ import { DiscoveryResource } from './resources/discovery.js';
3
+ export * from './resources/discovery.js';
1
4
  import { AuthResource } from './resources/auth.js';
2
5
  import { PostsResource } from './resources/posts.js';
3
6
  import { UsersResource } from './resources/users.js';
@@ -113,6 +116,7 @@ export declare class Recursiv {
113
116
  readonly webhooks: WebhooksResource;
114
117
  readonly jobs: JobsResource;
115
118
  readonly curator: CuratorResource;
119
+ readonly discovery: DiscoveryResource;
116
120
  readonly video: VideoResource;
117
121
  readonly appSubscriptions: AppSubscriptionsResource;
118
122
  readonly reports: ReportsResource;
package/dist/index.js CHANGED
@@ -1,3 +1,6 @@
1
+ export * from './resources/discovery-learning.js';
2
+ import { DiscoveryResource } from './resources/discovery.js';
3
+ export * from './resources/discovery.js';
1
4
  import { HttpClient } from './client.js';
2
5
  import { AuthResource } from './resources/auth.js';
3
6
  import { PostsResource } from './resources/posts.js';
@@ -114,6 +117,7 @@ export class Recursiv {
114
117
  webhooks;
115
118
  jobs;
116
119
  curator;
120
+ discovery;
117
121
  video;
118
122
  appSubscriptions;
119
123
  reports;
@@ -175,6 +179,7 @@ export class Recursiv {
175
179
  this.webhooks = new WebhooksResource(client);
176
180
  this.jobs = new JobsResource(client);
177
181
  this.curator = new CuratorResource(client);
182
+ this.discovery = new DiscoveryResource(this.posts, this.protocols);
178
183
  this.video = new VideoResource(client);
179
184
  this.appSubscriptions = new AppSubscriptionsResource(client);
180
185
  this.reports = new ReportsResource(client);
@@ -0,0 +1,27 @@
1
+ import type { DiscoveryLayout } from '../types.js';
2
+ export type DiscoveryPresentation = DiscoveryLayout['presentation'];
3
+ type Counts = {
4
+ views: number;
5
+ engagements: number;
6
+ hides: number;
7
+ };
8
+ export interface DiscoveryLearningSnapshot {
9
+ version: 1;
10
+ startedAt: number;
11
+ layouts: Record<DiscoveryPresentation, Counts>;
12
+ }
13
+ /** Local, per-viewer experimental presentation preferences. Stores counts only,
14
+ * never source text, author IDs or inferred traits. It cannot promote content,
15
+ * bypass safety, add unsupported layouts or establish a causal lift claim. */
16
+ export declare class DiscoveryLearning {
17
+ private now;
18
+ private value;
19
+ private observed;
20
+ constructor(input?: unknown, now?: () => number);
21
+ observe(id: string, presentation: DiscoveryPresentation, event: 'view' | 'engage' | 'hide'): void;
22
+ /** Jev proposes a suitable layout. Sufficient recent feedback may restore full
23
+ * post cards; every tenth eligible batch explores the safe proposal again. */
24
+ choose(proposal: DiscoveryPresentation, page: number): DiscoveryPresentation;
25
+ snapshot(): DiscoveryLearningSnapshot;
26
+ }
27
+ export {};
@@ -0,0 +1,58 @@
1
+ const names = ['posts', 'visual', 'conversation', 'video'];
2
+ const fresh = (now) => ({ version: 1, startedAt: now, layouts: {
3
+ video: { views: 0, engagements: 0, hides: 0 }, posts: { views: 0, engagements: 0, hides: 0 }, visual: { views: 0, engagements: 0, hides: 0 }, conversation: { views: 0, engagements: 0, hides: 0 },
4
+ } });
5
+ /** Local, per-viewer experimental presentation preferences. Stores counts only,
6
+ * never source text, author IDs or inferred traits. It cannot promote content,
7
+ * bypass safety, add unsupported layouts or establish a causal lift claim. */
8
+ export class DiscoveryLearning {
9
+ now;
10
+ value;
11
+ observed = new Set();
12
+ constructor(input, now = Date.now) {
13
+ this.now = now;
14
+ const value = input;
15
+ const time = now();
16
+ this.value = value?.version === 1 && Number.isFinite(value.startedAt) && value.startedAt <= time
17
+ && time - value.startedAt < 14 * 86400_000 && names.every(name => {
18
+ const c = value.layouts?.[name];
19
+ return c && [c.views, c.engagements, c.hides].every(n => Number.isSafeInteger(n) && n >= 0 && n <= 10000)
20
+ && c.engagements <= c.views && c.hides <= c.views;
21
+ }) ? JSON.parse(JSON.stringify(value)) : fresh(time);
22
+ }
23
+ observe(id, presentation, event) {
24
+ if (!names.includes(presentation) || !id || id.length > 8192)
25
+ return;
26
+ const key = `${id}:${event}`;
27
+ if (this.observed.has(key))
28
+ return;
29
+ // Engagements/hides require a real visible impression in this session.
30
+ if (event !== 'view' && !this.observed.has(`${id}:view`))
31
+ return;
32
+ this.observed.add(key);
33
+ while (this.observed.size > 4096)
34
+ this.observed.delete(this.observed.values().next().value);
35
+ const counters = this.value.layouts[presentation];
36
+ counters[event === 'view' ? 'views' : event === 'engage' ? 'engagements' : 'hides']++;
37
+ if (counters.views >= 10000)
38
+ for (const counts of Object.values(this.value.layouts)) {
39
+ counts.views = Math.floor(counts.views / 2);
40
+ counts.engagements = Math.floor(counts.engagements / 2);
41
+ counts.hides = Math.floor(counts.hides / 2);
42
+ }
43
+ }
44
+ /** Jev proposes a suitable layout. Sufficient recent feedback may restore full
45
+ * post cards; every tenth eligible batch explores the safe proposal again. */
46
+ choose(proposal, page) {
47
+ if (proposal === 'posts' || page % 10 === 0)
48
+ return proposal;
49
+ const selected = this.value.layouts[proposal];
50
+ const baseline = this.value.layouts.posts;
51
+ if (selected.views < 50 || baseline.views < 50)
52
+ return proposal;
53
+ const value = (c) => (c.engagements + 2 - 3 * c.hides) / (c.views + 10);
54
+ // Require a material difference, rather than switching on a single click.
55
+ return value(selected) + 0.1 < value(baseline) ? 'posts' : proposal;
56
+ }
57
+ snapshot() { return JSON.parse(JSON.stringify(this.value)); }
58
+ }
@@ -0,0 +1,69 @@
1
+ import type { DiscoveryLearning } from './discovery-learning.js';
2
+ import type { DiscoveryLayout, Post } from '../types.js';
3
+ import type { PostsResource } from './posts.js';
4
+ import type { ProtocolsResource, SocialNetwork, SocialPost } from './protocols.js';
5
+ export type DiscoveryItem = {
6
+ key: string;
7
+ kind: 'minds';
8
+ post: Post;
9
+ } | {
10
+ key: string;
11
+ kind: 'social';
12
+ post: SocialPost;
13
+ };
14
+ /** json-render Spec: values are references to authenticated SDK results, never
15
+ * arbitrary executable components, links, generated text or provider markup. */
16
+ export interface DiscoveryModule {
17
+ id: string;
18
+ presentation: DiscoveryLayout['presentation'];
19
+ source: DiscoveryLayout['source'];
20
+ items: DiscoveryItem[];
21
+ spec: {
22
+ root: 'module';
23
+ elements: {
24
+ module: {
25
+ type: 'DiscoveryPosts' | 'DiscoveryGallery' | 'DiscoveryConversation' | 'DiscoveryVideo';
26
+ props: {
27
+ itemKeys: string[];
28
+ };
29
+ children: [];
30
+ };
31
+ };
32
+ };
33
+ }
34
+ export interface DiscoveryFeedPage {
35
+ modules: DiscoveryModule[];
36
+ hasMore: boolean;
37
+ failedSources: string[];
38
+ partialCategories: boolean;
39
+ }
40
+ /** Per-viewer feed session. Create a new session when user, app, category or
41
+ * source changes. All reads retain existing server authorization/moderation.
42
+ * At most four requests per page; providers fail independently. */
43
+ export declare class DiscoveryFeedSession {
44
+ private posts;
45
+ private protocols;
46
+ private category?;
47
+ private learning?;
48
+ private streams;
49
+ private pending;
50
+ private seen;
51
+ private page;
52
+ private nextSource;
53
+ constructor(posts: Pick<PostsResource, 'list'>, protocols: Pick<ProtocolsResource, 'getPublicTrends' | 'getPublicTrendFeed'>, networks: SocialNetwork[], category?: string | undefined, source?: string, learning?: DiscoveryLearning | undefined);
54
+ retryFailed(): void;
55
+ next(): Promise<DiscoveryFeedPage>;
56
+ private read;
57
+ private readPage;
58
+ }
59
+ export declare class DiscoveryResource {
60
+ private posts;
61
+ private protocols;
62
+ constructor(posts: PostsResource, protocols: ProtocolsResource);
63
+ createFeed(options: {
64
+ networks: SocialNetwork[];
65
+ category?: string;
66
+ source?: string;
67
+ learning?: DiscoveryLearning;
68
+ }): DiscoveryFeedSession;
69
+ }
@@ -0,0 +1,156 @@
1
+ const ordinary = { version: 'discovery-layout-v1', presentation: 'posts', source: 'fallback' };
2
+ const key = (protocol, id) => JSON.stringify([protocol, id]);
3
+ /** Per-viewer feed session. Create a new session when user, app, category or
4
+ * source changes. All reads retain existing server authorization/moderation.
5
+ * At most four requests per page; providers fail independently. */
6
+ export class DiscoveryFeedSession {
7
+ posts;
8
+ protocols;
9
+ category;
10
+ learning;
11
+ streams;
12
+ pending = null;
13
+ seen = new Set();
14
+ page = 0;
15
+ nextSource = 0;
16
+ constructor(posts, protocols, networks, category, source, learning) {
17
+ this.posts = posts;
18
+ this.protocols = protocols;
19
+ this.category = category;
20
+ this.learning = learning;
21
+ const sources = [...new Set(networks.filter(n => n.capabilities.includes('public_trends')).map(n => n.protocol))].slice(0, 16);
22
+ this.streams = ['minds', ...sources].filter(protocol => !source || source === 'all' || source === protocol)
23
+ .map(protocol => ({ protocol, offset: 0, done: false, failed: false, used: [] }));
24
+ }
25
+ retryFailed() { for (const stream of this.streams)
26
+ stream.failed = false; }
27
+ next() {
28
+ if (this.pending)
29
+ return this.pending;
30
+ this.pending = this.readPage().finally(() => { this.pending = null; });
31
+ return this.pending;
32
+ }
33
+ async read(stream) {
34
+ const params = { limit: 20, ...(this.category ? { category: this.category } : {}), discovery: true };
35
+ if (stream.protocol === 'minds') {
36
+ const response = await this.posts.list({ ...params, sort: 'new', ...(stream.cursor ? { cursor: stream.cursor } : { offset: stream.offset }) });
37
+ stream.offset += response.meta.scanned ?? response.meta.limit;
38
+ const next = response.meta.next_cursor ?? undefined;
39
+ stream.done = !response.meta.has_more || !!next && (next === stream.cursor || stream.used.includes(next));
40
+ if (stream.cursor)
41
+ stream.used = [...stream.used, stream.cursor].slice(-32);
42
+ stream.cursor = next;
43
+ return { items: response.data.map(post => ({ key: key('minds', post.id), kind: 'minds', post })),
44
+ layout: response.meta.discovery ?? ordinary, partial: response.meta.category_filter?.status === 'partial' };
45
+ }
46
+ const validatePosts = (data) => {
47
+ if (data.coverage !== 'provider_ranked_window' || data.items.length > 25 || data.items.some(post => post.protocol !== stream.protocol || post.author.protocol !== stream.protocol))
48
+ throw new Error('Invalid discovery source response');
49
+ };
50
+ if (stream.topics) {
51
+ const topic = stream.topics[0];
52
+ if (!topic) {
53
+ stream.done = true;
54
+ return { items: [], layout: ordinary, partial: false };
55
+ }
56
+ const { data } = await this.protocols.getPublicTrendFeed({ ...params, protocol: stream.protocol, topic: topic.topic, cursor: topic.cursor });
57
+ validatePosts(data);
58
+ if (data.trend.protocol !== stream.protocol || data.trend.nativeId !== topic.topic)
59
+ throw new Error('Invalid discovery topic');
60
+ const next = data.cursor;
61
+ stream.topics.shift();
62
+ if (next && next !== topic.cursor && !topic.used.includes(next))
63
+ stream.topics.push({ topic: topic.topic, cursor: next, used: [...topic.used, ...(topic.cursor ? [topic.cursor] : [])].slice(-32) });
64
+ stream.done = !stream.topics.length;
65
+ return { items: data.items.map(post => ({ key: key(post.protocol, post.nativeId), kind: 'social', post })), layout: data.discovery ?? ordinary, partial: data.categoryFilter?.status === 'partial' };
66
+ }
67
+ const { data } = await this.protocols.getPublicTrends({ ...params, protocol: stream.protocol, cursor: stream.cursor, includePreviews: true });
68
+ if (data.coverage !== 'provider_ranked_window' || data.items.length > 25 || data.items.some(item => item.protocol !== stream.protocol))
69
+ throw new Error('Invalid discovery source response');
70
+ if (data.kind === 'topics') {
71
+ if (data.items.some(topic => topic.preview && (topic.preview.protocol !== stream.protocol || topic.preview.author.protocol !== stream.protocol)))
72
+ throw new Error('Invalid discovery preview');
73
+ stream.topics = [...new Set(data.items.map(topic => topic.nativeId))].map(topic => ({ topic, used: [] }));
74
+ stream.done = !stream.topics.length;
75
+ return { items: data.items.flatMap(topic => !this.category && topic.preview ? [{ key: key(topic.preview.protocol, topic.preview.nativeId), kind: 'social', post: topic.preview }] : []), layout: ordinary, partial: data.categoryFilter?.status === 'partial' };
76
+ }
77
+ validatePosts(data);
78
+ const next = data.cursor ?? undefined;
79
+ stream.done = !next || next === stream.cursor || stream.used.includes(next);
80
+ if (stream.cursor)
81
+ stream.used = [...stream.used, stream.cursor].slice(-32);
82
+ stream.cursor = next;
83
+ return { items: data.items.map(post => ({ key: key(post.protocol, post.nativeId), kind: 'social', post })), layout: data.discovery ?? ordinary, partial: data.categoryFilter?.status === 'partial' };
84
+ }
85
+ async readPage() {
86
+ const active = this.streams.filter(stream => !stream.done && !stream.failed);
87
+ const native = active.find(stream => stream.protocol === 'minds');
88
+ const remote = active.filter(stream => stream.protocol !== 'minds');
89
+ const selected = [...(native ? [native] : []), ...Array.from({ length: Math.min(native ? 3 : 4, remote.length) }, (_, index) => remote[(this.nextSource + index) % remote.length])];
90
+ this.nextSource += 3;
91
+ const reads = await Promise.all(selected.map(async (stream) => {
92
+ // Commit continuation state only if this page finishes within its UI
93
+ // deadline. A late provider cannot move the cursor past unseen content.
94
+ const candidate = { ...stream, used: [...stream.used], topics: stream.topics?.map(topic => ({ ...topic, used: [...topic.used] })) };
95
+ let cancel = () => { };
96
+ try {
97
+ const read = await Promise.race([this.read(candidate), new Promise((_resolve, reject) => {
98
+ const timer = setTimeout(() => reject(new Error('Discovery source timed out')), 15000);
99
+ cancel = () => clearTimeout(timer);
100
+ })]);
101
+ Object.assign(stream, candidate);
102
+ return read;
103
+ }
104
+ catch {
105
+ stream.failed = true;
106
+ return { items: [], layout: ordinary, partial: false };
107
+ }
108
+ finally {
109
+ cancel();
110
+ }
111
+ }));
112
+ // Round-robin before grouping: the largest network cannot occupy every slot.
113
+ const mixed = [];
114
+ for (let index = 0; index < Math.max(0, ...reads.map(read => read.items.length)); index++)
115
+ for (const read of reads) {
116
+ const item = read.items[index];
117
+ if (!item || this.seen.has(item.key))
118
+ continue;
119
+ this.seen.add(item.key);
120
+ mixed.push({ item, layout: read.layout });
121
+ }
122
+ // Deduplication memory is bounded independently of the UI's virtual window.
123
+ while (this.seen.size > 4096)
124
+ this.seen.delete(this.seen.values().next().value);
125
+ const modules = [];
126
+ const page = this.page++;
127
+ for (let index = 0; index < mixed.length;) {
128
+ const first = mixed[index];
129
+ const proposal = first.layout.source === 'jev' ? first.layout.presentation : 'posts';
130
+ const preferred = this.learning?.choose(proposal, page) ?? proposal;
131
+ const group = mixed.slice(index, index + 4);
132
+ const video = preferred === 'video' && first.item.post.media?.some(media => media.type === 'video');
133
+ const visual = preferred === 'visual' && group.filter(row => row.item.post.media?.some(media => media.type === 'image')).length >= 2;
134
+ const discussion = preferred === 'conversation' && group.filter(row => row.item.kind === 'social' ? (row.item.post.replyCount ?? 0) > 0 : (row.item.post.reply_count ?? 0) > 0).length >= 2;
135
+ const presentation = video ? 'video' : visual ? 'visual' : discussion ? 'conversation' : 'posts';
136
+ const selected = presentation === 'posts' || video ? [first] : group;
137
+ const items = selected.map(row => row.item);
138
+ modules.push({ id: `discovery-${page}-${index}-${items[0].key}`, presentation, source: presentation === 'posts' ? 'fallback' : 'jev', items,
139
+ spec: { root: 'module', elements: { module: { type: video ? 'DiscoveryVideo' : visual ? 'DiscoveryGallery' : discussion ? 'DiscoveryConversation' : 'DiscoveryPosts', props: { itemKeys: items.map(item => item.key) }, children: [] } } } });
140
+ index += selected.length;
141
+ }
142
+ return { modules, hasMore: this.streams.some(stream => !stream.done && !stream.failed),
143
+ failedSources: this.streams.filter(stream => stream.failed).map(stream => stream.protocol), partialCategories: reads.some(read => read.partial) };
144
+ }
145
+ }
146
+ export class DiscoveryResource {
147
+ posts;
148
+ protocols;
149
+ constructor(posts, protocols) {
150
+ this.posts = posts;
151
+ this.protocols = protocols;
152
+ }
153
+ createFeed(options) {
154
+ return new DiscoveryFeedSession(this.posts, this.protocols, options.networks, options.category, options.source, options.learning);
155
+ }
156
+ }
@@ -5,6 +5,11 @@ export declare class PostsResource {
5
5
  constructor(client: HttpClient);
6
6
  /** List posts with optional filters */
7
7
  list(params?: ListPostsParams): Promise<PaginatedResponse<Post>>;
8
+ /** Broad categories currently enabled for public posts in this app. */
9
+ categories(): Promise<SingleResponse<Array<{
10
+ id: string;
11
+ name: string;
12
+ }>>>;
8
13
  /** List posts the authenticated caller has upvoted. */
9
14
  liked(params?: Pick<ListPostsParams, 'limit' | 'offset' | 'cursor'>): Promise<PaginatedResponse<Post>>;
10
15
  /** Get a single post with replies */
@@ -7,6 +7,10 @@ export class PostsResource {
7
7
  list(params) {
8
8
  return this.client.get('/posts', params);
9
9
  }
10
+ /** Broad categories currently enabled for public posts in this app. */
11
+ categories() {
12
+ return this.client.get('/posts/categories');
13
+ }
10
14
  /** List posts the authenticated caller has upvoted. */
11
15
  liked(params) {
12
16
  return this.list({ ...params, liked: true });
@@ -1,3 +1,4 @@
1
+ import type { DiscoveryLayout } from '../types.js';
1
2
  import type { HttpClient } from '../client.js';
2
3
  import type { PaginatedResponse, PaginationParams, SingleResponse } from '../types.js';
3
4
  /** Canonical protocol operation names returned by the adapter registry. */
@@ -784,6 +785,11 @@ export interface SocialNetwork {
784
785
  capabilities: Array<'profile_search' | 'profile_read' | 'original_author_posts' | 'minds_subscription' | 'following_feed' | 'conversation_read' | 'public_trends' | 'trend_feed' | 'popular_accounts' | 'post_search'>;
785
786
  profileSearchPagination: boolean;
786
787
  postSearchAccountRequired?: boolean;
788
+ /** Categories available from the current app configuration. */
789
+ categories?: Array<{
790
+ id: string;
791
+ name: string;
792
+ }>;
787
793
  conversationCoverage?: string;
788
794
  }
789
795
  /** Public source identity, independent of any local Minds user or handle. */
@@ -839,9 +845,10 @@ export interface SocialPost {
839
845
  likeCount: number | null;
840
846
  repostCount: number | null;
841
847
  media: Array<{
842
- type: 'image';
848
+ type: 'image' | 'video';
843
849
  url: string;
844
850
  alt: string;
851
+ poster?: string;
845
852
  }>;
846
853
  hasAdditionalContent: boolean;
847
854
  /** At most one independently filtered quoted post. */
@@ -879,6 +886,7 @@ export interface SocialAuthorFeed {
879
886
  coverage: 'original_posts';
880
887
  }
881
888
  export interface SocialTrendProvenance {
889
+ discovery?: DiscoveryLayout;
882
890
  provider: {
883
891
  name: string;
884
892
  url: string;
@@ -887,6 +895,12 @@ export interface SocialTrendProvenance {
887
895
  coverage: 'provider_ranked_window';
888
896
  /** Exact ranking interval is not supplied by these providers. */
889
897
  rankingWindow: null;
898
+ categoryFilter?: {
899
+ selected: string;
900
+ status: 'complete' | 'partial';
901
+ classified: number;
902
+ unclassified: number;
903
+ };
890
904
  }
891
905
  export interface SocialTrend {
892
906
  protocol: string;
@@ -1012,6 +1026,8 @@ export declare class ProtocolsResource {
1012
1026
  limit?: number;
1013
1027
  cursor?: string;
1014
1028
  includePreviews?: boolean;
1029
+ category?: string;
1030
+ discovery?: boolean;
1015
1031
  }): Promise<SingleResponse<SocialTrends>>;
1016
1032
  /** Open a topic currently listed by the provider. Use its nativeId as topic and preserve the opaque cursor. */
1017
1033
  getPublicTrendFeed(params: {
@@ -1019,6 +1035,8 @@ export declare class ProtocolsResource {
1019
1035
  topic: string;
1020
1036
  limit?: number;
1021
1037
  cursor?: string;
1038
+ category?: string;
1039
+ discovery?: boolean;
1022
1040
  }): Promise<SingleResponse<SocialTrendFeed>>;
1023
1041
  /** Provider-ranked public accounts with explicit provenance; served from the app's index when a fresh snapshot exists. */
1024
1042
  getPublicPopularProfiles(params: {
@@ -110,11 +110,15 @@ export class ProtocolsResource {
110
110
  /** Current provider-ranked topics or posts, with explicit source and coverage. Does not ingest content. */
111
111
  getPublicTrends(params) {
112
112
  return this.client.get('/protocols/social/trends', { protocol: params.protocol, limit: params.limit, cursor: params.cursor,
113
- ...(params.includePreviews !== undefined ? { includePreviews: params.includePreviews } : {}) });
113
+ ...(params.includePreviews !== undefined ? { includePreviews: params.includePreviews } : {}),
114
+ ...(params.category !== undefined ? { category: params.category } : {}),
115
+ ...(params.discovery !== undefined ? { discovery: params.discovery } : {}) });
114
116
  }
115
117
  /** Open a topic currently listed by the provider. Use its nativeId as topic and preserve the opaque cursor. */
116
118
  getPublicTrendFeed(params) {
117
- return this.client.get('/protocols/social/trend-feed', { protocol: params.protocol, topic: params.topic, limit: params.limit, cursor: params.cursor });
119
+ return this.client.get('/protocols/social/trend-feed', { protocol: params.protocol, topic: params.topic, limit: params.limit, cursor: params.cursor,
120
+ ...(params.category !== undefined ? { category: params.category } : {}),
121
+ ...(params.discovery !== undefined ? { discovery: params.discovery } : {}) });
118
122
  }
119
123
  /** Provider-ranked public accounts with explicit provenance; served from the app's index when a fresh snapshot exists. */
120
124
  getPublicPopularProfiles(params) {
package/dist/types.d.ts CHANGED
@@ -5,7 +5,20 @@ export interface PaginationParams {
5
5
  cursor?: string;
6
6
  [key: string]: unknown;
7
7
  }
8
+ export interface DiscoveryLayout {
9
+ version: 'discovery-layout-v1';
10
+ presentation: 'posts' | 'visual' | 'conversation' | 'video';
11
+ source: 'jev' | 'fallback';
12
+ }
8
13
  export interface PaginationMeta {
14
+ discovery?: DiscoveryLayout;
15
+ scanned?: number;
16
+ category_filter?: {
17
+ selected: string;
18
+ status: 'complete' | 'partial';
19
+ classified: number;
20
+ unclassified: number;
21
+ };
9
22
  limit: number;
10
23
  offset: number;
11
24
  has_more: boolean;
@@ -93,6 +106,7 @@ export interface MediaItem {
93
106
  type: string;
94
107
  }
95
108
  export interface Post {
109
+ reply_count?: number;
96
110
  id: string;
97
111
  /** Original Minds numeric GUID when this row came from the legacy import. */
98
112
  legacy_guid?: string | null;
@@ -171,6 +185,10 @@ export interface UpdatePostInput {
171
185
  }
172
186
  export type ReactionType = 'like' | 'heart' | 'fire' | 'laugh' | 'sad' | 'angry' | 'upvote' | 'downvote';
173
187
  export interface ListPostsParams extends PaginationParams {
188
+ /** Public-only feed with a bounded Jev presentation hint. Limit <=25. */
189
+ discovery?: boolean;
190
+ /** Public-only broad category. Requires an enabled app experiment; limit <=25. */
191
+ category?: string;
174
192
  community_id?: string;
175
193
  author_id?: string;
176
194
  organization_id?: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@recursiv/sdk",
3
- "version": "0.7.37",
3
+ "version": "0.7.40",
4
4
  "description": "TypeScript SDK for the Recursiv API",
5
5
  "license": "FSL-1.1-ALv2",
6
6
  "type": "module",