@recursiv/sdk 0.7.53 → 0.7.54
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +29 -0
- package/dist/index.d.ts +1 -1
- package/dist/resources/auth.d.ts +17 -3
- package/dist/resources/auth.js +7 -2
- package/dist/resources/curator.d.ts +1 -1
- package/dist/resources/discovery.d.ts +78 -1
- package/dist/resources/discovery.js +55 -6
- package/dist/resources/human-input.js +2 -1
- package/dist/resources/profiles.d.ts +7 -3
- package/dist/resources/profiles.js +5 -1
- package/dist/resources/protocols.d.ts +16 -0
- package/dist/types.d.ts +48 -0
- package/llm.md +5 -3
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -675,6 +675,35 @@ what the viewer follows, joined, blocked or muted, and says why a list is empty
|
|
|
675
675
|
showings without a tap rest a suggestion for fourteen days, and engaging clears it.
|
|
676
676
|
Keep one `session` token per app session so a rail does not reshuffle on every render.
|
|
677
677
|
|
|
678
|
+
A category feed never ends at an empty page. `sdk.posts.list({ category, sort: 'hot' })`
|
|
679
|
+
(or `score`, or the default chronological order) pages the window of the category's
|
|
680
|
+
newest judged posts, then continues by keyset through the whole indexed
|
|
681
|
+
history: every page's `meta.next` is `{ offset }`, `{ cursor }` or `null`, and when a
|
|
682
|
+
stage ends `meta.fallover` lists where to go, in order: `category_older` (the keyset
|
|
683
|
+
cursor `next` also carries), `category_network` (the same category on the open
|
|
684
|
+
networks), the member's next categories (`via` names the order they came from), `for_you`
|
|
685
|
+
and `all`. A `DiscoveryFeedSession` follows the cursor itself and reports the Minds
|
|
686
|
+
feed's `fallover` on its pages. `sdk.discovery.category({ category, session? })` answers a
|
|
687
|
+
category's surface in one call: `stories`, `network` and `clips` (each shared per app and
|
|
688
|
+
category, refreshed after 30 s, kept up to 300 s and warmed as the warmer reaches the category, then filtered for the viewer on every call: the cached safety verdicts of story members, network posts and clips again, the term list, the viewer's blocks and mutes over clips, and the member's own hidden sources over network posts and story members) and the
|
|
689
|
+
viewer's `people` and `groups`; a section that timed out, failed or is off is empty with a
|
|
690
|
+
`reason` for that request only (a failure is never cached, so asking again retries it), and one this
|
|
691
|
+
key may not read is empty with `refused` (`stories` and `network` need the member's own app
|
|
692
|
+
key with `users:read`; `people` and `groups` need `users:read`). In a category, a Bluesky
|
|
693
|
+
topic the provider no longer lists, or whose feed holds nothing in the category, is served
|
|
694
|
+
the network's indexed posts in it (`sourceIndex.reason` of `gone` or `empty`). While the
|
|
695
|
+
provider has more of the topic the page's cursor is the provider's own, so the topic's
|
|
696
|
+
deeper pages are still read; once it has none, or the topic is gone, the cursor is an
|
|
697
|
+
`idx:tf:` one that continues in the index. When the index cannot answer (busy, or its safety
|
|
698
|
+
cache unreadable), the page is what the provider answered: the empty page with its own
|
|
699
|
+
cursor, or the gone topic's 404; only an `idx:tf:` page answers 503 `unavailable`, and that
|
|
700
|
+
ends the index continuation, not the network. The app contract: a `trend_unavailable` 404,
|
|
701
|
+
an empty topic page or a failed `idx:tf:` page never disables a network for the session;
|
|
702
|
+
move to the next topic, keep every provider continuation, start the `idx:tf:` stream once
|
|
703
|
+
per network and session (every such topic answers the same index stream; after a failed
|
|
704
|
+
page it may start again), and pass cursors back unchanged. `DiscoveryFeedSession` does
|
|
705
|
+
this for you.
|
|
706
|
+
|
|
678
707
|
`sdk.posts.categories()` answers the app's categories in the order this member
|
|
679
708
|
should see them. `meta.order` is `personal`, `popular` or `static`; `meta.reason`
|
|
680
709
|
says why an answer is not personal (`low_signal`, `deadline`, `requested`,
|
package/dist/index.d.ts
CHANGED
|
@@ -147,7 +147,7 @@ export { HttpClient } from './client.js';
|
|
|
147
147
|
export { RealtimeClient } from './realtime.js';
|
|
148
148
|
export type { RealtimeConnectOptions } from './realtime.js';
|
|
149
149
|
export { AuthResource } from './resources/auth.js';
|
|
150
|
-
export type { AuthSession, SignUpInput, SignInInput, UsernameSignInInput, AppContext, ResetPasswordInput, TwoFactorConfirmation, TwoFactorEnrollment, TwoFactorChallenge, CompleteTwoFactorSignInInput, OAuthClientInfo, SendOtpInput, VerifyOtpInput, CreateApiKeyInput, ApiKeyResult, } from './resources/auth.js';
|
|
150
|
+
export type { AuthSession, CurrentSession, SignUpInput, SignInInput, UsernameSignInInput, AppContext, ResetPasswordInput, TwoFactorConfirmation, TwoFactorEnrollment, TwoFactorChallenge, CompleteTwoFactorSignInInput, OAuthClientInfo, SendOtpInput, VerifyOtpInput, CreateApiKeyInput, ApiKeyResult, } from './resources/auth.js';
|
|
151
151
|
export { PostsResource } from './resources/posts.js';
|
|
152
152
|
export type { CategoryOrder, CategoryOrderMeta, CategoryOrderReason } from './resources/posts.js';
|
|
153
153
|
export { UsersResource } from './resources/users.js';
|
package/dist/resources/auth.d.ts
CHANGED
|
@@ -8,6 +8,20 @@ export interface AuthSession {
|
|
|
8
8
|
image: string | null;
|
|
9
9
|
};
|
|
10
10
|
}
|
|
11
|
+
/**
|
|
12
|
+
* What `getSession` answers: the account, and which of its sessions the token
|
|
13
|
+
* is. `session.id` is the id `settings.listSessions()` lists that session
|
|
14
|
+
* under. A client that authenticates with an API key gets `is_current: false`
|
|
15
|
+
* on every listed row (a key belongs to no session), so it matches this id to
|
|
16
|
+
* mark its own device.
|
|
17
|
+
*/
|
|
18
|
+
export interface CurrentSession extends AuthSession {
|
|
19
|
+
/** Null only when the server's answer names no session row. */
|
|
20
|
+
session: {
|
|
21
|
+
id: string;
|
|
22
|
+
expiresAt: string | null;
|
|
23
|
+
} | null;
|
|
24
|
+
}
|
|
11
25
|
export interface SignUpInput {
|
|
12
26
|
email: string;
|
|
13
27
|
password: string;
|
|
@@ -275,10 +289,10 @@ export declare class AuthResource {
|
|
|
275
289
|
success: true;
|
|
276
290
|
}>;
|
|
277
291
|
/**
|
|
278
|
-
* Get current session info using a session token
|
|
279
|
-
* Returns null if the session is invalid or expired.
|
|
292
|
+
* Get current session info using a session token: the account, and the
|
|
293
|
+
* session's id and expiry. Returns null if the session is invalid or expired.
|
|
280
294
|
*/
|
|
281
|
-
getSession(sessionToken: string): Promise<
|
|
295
|
+
getSession(sessionToken: string): Promise<CurrentSession | null>;
|
|
282
296
|
/**
|
|
283
297
|
* Sign out (invalidate session).
|
|
284
298
|
*/
|
package/dist/resources/auth.js
CHANGED
|
@@ -596,8 +596,8 @@ export class AuthResource {
|
|
|
596
596
|
return { success: true };
|
|
597
597
|
}
|
|
598
598
|
/**
|
|
599
|
-
* Get current session info using a session token
|
|
600
|
-
* Returns null if the session is invalid or expired.
|
|
599
|
+
* Get current session info using a session token: the account, and the
|
|
600
|
+
* session's id and expiry. Returns null if the session is invalid or expired.
|
|
601
601
|
*/
|
|
602
602
|
async getSession(sessionToken) {
|
|
603
603
|
const res = await this.fetchWithTimeout(`${this.baseOrigin}/api/auth/get-session`, {
|
|
@@ -612,6 +612,8 @@ export class AuthResource {
|
|
|
612
612
|
const data = await res.json();
|
|
613
613
|
if (!data?.user)
|
|
614
614
|
return null;
|
|
615
|
+
const sessionId = data.session?.id;
|
|
616
|
+
const expiresAt = data.session?.expiresAt;
|
|
615
617
|
return {
|
|
616
618
|
token: sessionToken,
|
|
617
619
|
user: {
|
|
@@ -620,6 +622,9 @@ export class AuthResource {
|
|
|
620
622
|
email: data.user.email,
|
|
621
623
|
image: data.user.image ?? null,
|
|
622
624
|
},
|
|
625
|
+
session: typeof sessionId === 'string' && sessionId
|
|
626
|
+
? { id: sessionId, expiresAt: typeof expiresAt === 'string' ? expiresAt : null }
|
|
627
|
+
: null,
|
|
623
628
|
};
|
|
624
629
|
}
|
|
625
630
|
/**
|
|
@@ -7,7 +7,7 @@ export type ForYouParams = {
|
|
|
7
7
|
offset?: number;
|
|
8
8
|
/** The previous page's `meta.next_cursor`: pages a stored ranking, so posts read meanwhile never shift the next page. */
|
|
9
9
|
cursor?: string;
|
|
10
|
-
/** Adds `meta.discovery` (layouts)
|
|
10
|
+
/** Adds `meta.discovery` (layouts); limit <= 25. Every page carries the promotion safety floor, with or without it. */
|
|
11
11
|
discovery?: boolean;
|
|
12
12
|
/** `discover` ranks for Discover: fresher, beyond the member's follows, with their category order. */
|
|
13
13
|
surface?: ForYouSurface;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { HttpClient } from '../client.js';
|
|
2
|
-
import type { DiscoveryLayout, Post } from '../types.js';
|
|
2
|
+
import type { DiscoveryLayout, FalloverStep, Post } from '../types.js';
|
|
3
3
|
import type { CuratorResource } from './curator.js';
|
|
4
4
|
import type { DiscoveryLearning } from './discovery-learning.js';
|
|
5
5
|
import type { PostsResource } from './posts.js';
|
|
@@ -66,6 +66,61 @@ export interface EntityImpressionsInput {
|
|
|
66
66
|
/** 'shown' when a suggestion was on screen; 'engaged' on a tap, follow or join. Three showings without a tap rest a suggestion for fourteen days; engaging clears it. */
|
|
67
67
|
event: 'shown' | 'engaged';
|
|
68
68
|
}
|
|
69
|
+
/** One section of a category surface: bounded, read under its own deadline; empty with a `reason` when it timed out, failed, is off, or the recommender said why. */
|
|
70
|
+
export interface CategorySurfaceSection<T> {
|
|
71
|
+
items: T[];
|
|
72
|
+
ms: number;
|
|
73
|
+
reason?: string;
|
|
74
|
+
}
|
|
75
|
+
/** A radar cluster themed into the category, with up to six members (headline or excerpt and link only). */
|
|
76
|
+
export type CategoryStory = Pick<SocialRadar['clusters'][number], 'id' | 'label' | 'kind' | 'theme' | 'sources' | 'corroboration' | 'laneMix' | 'velocity' | 'media' | 'freshness' | 'score' | 'members'>;
|
|
77
|
+
export interface CategoryClip {
|
|
78
|
+
id: string;
|
|
79
|
+
title: string | null;
|
|
80
|
+
excerpt: string;
|
|
81
|
+
created_at: string;
|
|
82
|
+
author: {
|
|
83
|
+
id: string;
|
|
84
|
+
username: string | null;
|
|
85
|
+
name: string;
|
|
86
|
+
image: string | null;
|
|
87
|
+
};
|
|
88
|
+
media: Array<{
|
|
89
|
+
id: string;
|
|
90
|
+
url: string;
|
|
91
|
+
type: string;
|
|
92
|
+
thumbnail_url: string | null;
|
|
93
|
+
unplayable?: boolean;
|
|
94
|
+
}>;
|
|
95
|
+
engagement: {
|
|
96
|
+
score: number;
|
|
97
|
+
like_count: number;
|
|
98
|
+
reply_count: number;
|
|
99
|
+
repost_count: number;
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
export interface CategorySurface {
|
|
103
|
+
category: string;
|
|
104
|
+
/** When the oldest cached section served (stories, network, clips) was built. */
|
|
105
|
+
as_of: string;
|
|
106
|
+
stories: CategorySurfaceSection<CategoryStory>;
|
|
107
|
+
network: CategorySurfaceSection<SocialPost> & {
|
|
108
|
+
by_protocol: Record<string, number>;
|
|
109
|
+
};
|
|
110
|
+
clips: CategorySurfaceSection<CategoryClip>;
|
|
111
|
+
people: CategorySurfaceSection<EntityRecommendation>;
|
|
112
|
+
groups: CategorySurfaceSection<EntityRecommendation>;
|
|
113
|
+
}
|
|
114
|
+
export interface CategorySurfaceResponse {
|
|
115
|
+
/** Null when the key has no app (meta.reason `no_app`). */
|
|
116
|
+
data: CategorySurface | null;
|
|
117
|
+
meta: {
|
|
118
|
+
category: string;
|
|
119
|
+
as_of?: string;
|
|
120
|
+
reason?: 'no_app';
|
|
121
|
+
personalized: boolean;
|
|
122
|
+
};
|
|
123
|
+
}
|
|
69
124
|
export interface DiscoveryGroupCandidate {
|
|
70
125
|
id: string;
|
|
71
126
|
name: string;
|
|
@@ -219,6 +274,12 @@ export interface DiscoveryFeedPage {
|
|
|
219
274
|
hasMore: boolean;
|
|
220
275
|
failedSources: string[];
|
|
221
276
|
partialCategories: boolean;
|
|
277
|
+
/**
|
|
278
|
+
* The Minds feed's fall-through, once its category feed has ended a stage (the server's
|
|
279
|
+
* `meta.fallover`): where to read next when this session has no more. Absent until then, and
|
|
280
|
+
* on servers that predate it.
|
|
281
|
+
*/
|
|
282
|
+
fallover?: FalloverStep[];
|
|
222
283
|
}
|
|
223
284
|
/** A gallery strip holds up to twelve image posts gathered from the whole page. */
|
|
224
285
|
export declare const GALLERY_MAX_ITEMS = 12;
|
|
@@ -292,6 +353,22 @@ export declare class DiscoveryResource {
|
|
|
292
353
|
reason?: 'disabled' | 'other_app' | 'unavailable' | 'no_app';
|
|
293
354
|
};
|
|
294
355
|
}>;
|
|
356
|
+
/**
|
|
357
|
+
* A category's surface in one call: the modules All has, scoped to the category. `stories` (radar
|
|
358
|
+
* clusters themed into it), `network` (open-network posts judged into it, newest first) and `clips`
|
|
359
|
+
* (public video posts among its hot window) are shared per app and category (refreshed after 30 s, kept up to 300 s, so a copy
|
|
360
|
+
* may be that old) and filtered for the viewer on every call: the cached safety verdicts and the promotion term list again, the
|
|
361
|
+
* viewer's blocks and mutes, and for stories and network the member's own hidden sources. `people` and `groups` are the
|
|
362
|
+
* viewer's suggestions in it. A section that timed out, failed or is off is empty
|
|
363
|
+
* with a `reason` and never delays the others. The posts feed itself is `posts.list({ category })`,
|
|
364
|
+
* whose pages carry `meta.next` and `meta.fallover`.
|
|
365
|
+
*/
|
|
366
|
+
category(params: {
|
|
367
|
+
category: string;
|
|
368
|
+
session?: string;
|
|
369
|
+
}, options?: {
|
|
370
|
+
retry?: boolean;
|
|
371
|
+
}): Promise<CategorySurfaceResponse>;
|
|
295
372
|
/**
|
|
296
373
|
* A For you Discover feed: the member's For You ranking (asked for Discover's surface, with layouts and a
|
|
297
374
|
* snapshot cursor) in a session's Minds slot. With a category, the session also reads open-network posts
|
|
@@ -22,6 +22,16 @@ const isGone = (error) => typeof error === 'object' && error !== null && error.c
|
|
|
22
22
|
const TOPIC_WALK = 5;
|
|
23
23
|
/** The walk starts no topic read after this long: well inside the page deadline, so it commits its progress rather than timing the network out. */
|
|
24
24
|
const TOPIC_WALK_MS = 8000;
|
|
25
|
+
/**
|
|
26
|
+
* A topic page the server answered from its open-network source index (`sourceIndex`): in a category, a
|
|
27
|
+
* topic that is gone or holds nothing in it is served the network's indexed posts in the category, which
|
|
28
|
+
* are the same for every such topic. While the provider has more of the topic the page carries the
|
|
29
|
+
* provider's own cursor, which the walk keeps like any other; a topic that is gone or ended carries an
|
|
30
|
+
* `idx:tf:` cursor, the one index stream of the network. So a walk starts that stream at most once per
|
|
31
|
+
* session (a second start would re-read what it already showed), and an index page that shows nothing
|
|
32
|
+
* new counts as an empty topic.
|
|
33
|
+
*/
|
|
34
|
+
const INDEX_CURSOR = 'idx:tf:';
|
|
25
35
|
const RECOMMENDATION_KINDS = ['groups', 'people', 'tags'];
|
|
26
36
|
const nonEmpty = (value) => typeof value === 'string' && value.trim().length > 0 && value.length <= 512;
|
|
27
37
|
function recommendationItem(kind, candidate) {
|
|
@@ -188,6 +198,9 @@ export class DiscoveryFeedSession {
|
|
|
188
198
|
if (stream.cursor)
|
|
189
199
|
stream.used = [...stream.used, stream.cursor].slice(-32);
|
|
190
200
|
stream.cursor = next;
|
|
201
|
+
// A category feed that ended a stage says where to go next; kept on the stream and reported on every page from here.
|
|
202
|
+
if (Array.isArray(response.meta.fallover))
|
|
203
|
+
stream.fallover = response.meta.fallover;
|
|
191
204
|
return { items: response.data.map(post => ({ key: key('minds', post.id), kind: 'minds', post })),
|
|
192
205
|
layout: response.meta.discovery ?? ordinary, partial: response.meta.category_filter?.status === 'partial' };
|
|
193
206
|
}
|
|
@@ -237,9 +250,20 @@ export class DiscoveryFeedSession {
|
|
|
237
250
|
const topic = topics[0];
|
|
238
251
|
if (!topic)
|
|
239
252
|
break;
|
|
253
|
+
// A failed read of the network's index stream (the server's index busy, or its safety cache down) ends that
|
|
254
|
+
// continuation, not the network: the stream may start again from a later topic's page. A provider topic
|
|
255
|
+
// read that fails any other way than gone is still the network's failure.
|
|
256
|
+
const indexContinuation = !!topic.cursor?.startsWith(INDEX_CURSOR);
|
|
240
257
|
const response = await this.protocols.getPublicTrendFeed({ ...params, protocol: stream.protocol, topic: topic.topic, cursor: topic.cursor }, NO_RETRY)
|
|
241
|
-
.catch((error) => {
|
|
242
|
-
|
|
258
|
+
.catch((error) => {
|
|
259
|
+
if (isGone(error))
|
|
260
|
+
return null;
|
|
261
|
+
if (indexContinuation) {
|
|
262
|
+
stream.indexStarted = false;
|
|
263
|
+
return null;
|
|
264
|
+
}
|
|
265
|
+
throw error;
|
|
266
|
+
});
|
|
243
267
|
topics.shift();
|
|
244
268
|
if (!response)
|
|
245
269
|
continue;
|
|
@@ -248,12 +272,20 @@ export class DiscoveryFeedSession {
|
|
|
248
272
|
if (data.trend.protocol !== stream.protocol || data.trend.nativeId !== topic.topic)
|
|
249
273
|
throw new Error('Invalid discovery topic');
|
|
250
274
|
const next = data.cursor;
|
|
251
|
-
|
|
275
|
+
// A provider cursor is always kept. An index cursor from a provider page starts the network's one index
|
|
276
|
+
// stream: queued the first time only, since every later start would re-read the same stream from its top.
|
|
277
|
+
const indexed = !!data.sourceIndex;
|
|
278
|
+
const startsIndex = !!next?.startsWith(INDEX_CURSOR) && !topic.cursor?.startsWith(INDEX_CURSOR);
|
|
279
|
+
if (next && next !== topic.cursor && !topic.used.includes(next) && !(startsIndex && stream.indexStarted)) {
|
|
252
280
|
topics.push({ topic: topic.topic, cursor: next, used: [...topic.used, ...(topic.cursor ? [topic.cursor] : [])].slice(-32) });
|
|
281
|
+
if (startsIndex)
|
|
282
|
+
stream.indexStarted = true;
|
|
283
|
+
}
|
|
253
284
|
partial ||= data.categoryFilter?.status === 'partial';
|
|
254
|
-
|
|
285
|
+
const items = indexed ? data.items.filter(post => !this.seen.has(key(post.protocol, post.nativeId))) : data.items;
|
|
286
|
+
if (items.length) {
|
|
255
287
|
stream.done = !topics.length;
|
|
256
|
-
return { items:
|
|
288
|
+
return { items: items.map(post => ({ key: key(post.protocol, post.nativeId), kind: 'social', post })), layout: data.discovery ?? ordinary, partial };
|
|
257
289
|
}
|
|
258
290
|
}
|
|
259
291
|
stream.done = !topics.length;
|
|
@@ -369,8 +401,10 @@ export class DiscoveryFeedSession {
|
|
|
369
401
|
modules.splice(Math.min(2, modules.length), 0, recommendation);
|
|
370
402
|
if (cluster)
|
|
371
403
|
modules.splice(page === 0 ? 0 : Math.min(2, modules.length), 0, this.radarModule(page, cluster, clusterItems));
|
|
404
|
+
const fallover = this.streams.find(stream => stream.protocol === 'minds')?.fallover;
|
|
372
405
|
return { modules, hasMore: this.streams.some(stream => !stream.done && !stream.failed),
|
|
373
|
-
failedSources: this.streams.filter(stream => stream.failed).map(stream => stream.protocol), partialCategories: reads.some(read => read.partial)
|
|
406
|
+
failedSources: this.streams.filter(stream => stream.failed).map(stream => stream.protocol), partialCategories: reads.some(read => read.partial),
|
|
407
|
+
...(fallover ? { fallover } : {}) };
|
|
374
408
|
}
|
|
375
409
|
/** Page 0: the top breaking cluster, else the top cluster. Every second later page: the next unused story or viral cluster. */
|
|
376
410
|
async radarCluster(page) {
|
|
@@ -534,6 +568,21 @@ export class DiscoveryResource {
|
|
|
534
568
|
throw new Error('recordImpressions needs the HTTP client');
|
|
535
569
|
return this.http.post('/discover/impressions', input);
|
|
536
570
|
}
|
|
571
|
+
/**
|
|
572
|
+
* A category's surface in one call: the modules All has, scoped to the category. `stories` (radar
|
|
573
|
+
* clusters themed into it), `network` (open-network posts judged into it, newest first) and `clips`
|
|
574
|
+
* (public video posts among its hot window) are shared per app and category (refreshed after 30 s, kept up to 300 s, so a copy
|
|
575
|
+
* may be that old) and filtered for the viewer on every call: the cached safety verdicts and the promotion term list again, the
|
|
576
|
+
* viewer's blocks and mutes, and for stories and network the member's own hidden sources. `people` and `groups` are the
|
|
577
|
+
* viewer's suggestions in it. A section that timed out, failed or is off is empty
|
|
578
|
+
* with a `reason` and never delays the others. The posts feed itself is `posts.list({ category })`,
|
|
579
|
+
* whose pages carry `meta.next` and `meta.fallover`.
|
|
580
|
+
*/
|
|
581
|
+
category(params, options) {
|
|
582
|
+
if (!this.http)
|
|
583
|
+
throw new Error('category needs the HTTP client');
|
|
584
|
+
return this.http.get('/discover/category', params, options);
|
|
585
|
+
}
|
|
537
586
|
/**
|
|
538
587
|
* A For you Discover feed: the member's For You ranking (asked for Discover's surface, with layouts and a
|
|
539
588
|
* snapshot cursor) in a session's Minds slot. With a category, the session also reads open-network posts
|
|
@@ -18,7 +18,8 @@ export class HumanInputResource {
|
|
|
18
18
|
}
|
|
19
19
|
/** Raise a blocking question and notify the human wherever they are. */
|
|
20
20
|
request(input) {
|
|
21
|
-
|
|
21
|
+
// This write is not idempotent. A lost response must not replay the ask.
|
|
22
|
+
return this.client.post('/human-input', input, { retry: false });
|
|
22
23
|
}
|
|
23
24
|
/** Has the human answered yet? */
|
|
24
25
|
answers(conversationId) {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { HttpClient } from '../client.js';
|
|
2
|
-
import type { PaginatedResponse, SingleResponse, SuccessResponse, Profile, ProfileDetail, ProfileMe, LeaderboardEntry, UpdateProfileInput, SearchProfilesParams, ProfileSearchResult, SearchMentionsParams, MentionCandidate, FollowListParams, FollowListProfile, BlockedProfile, MutedProfile, ModerationListParams,
|
|
2
|
+
import type { PaginatedResponse, SingleResponse, SuccessResponse, Profile, ProfileDetail, ProfileMe, LeaderboardEntry, UpdateProfileInput, SearchProfilesParams, ProfileSearchResult, SearchMentionsParams, MentionCandidate, FollowListParams, FollowListProfile, BlockedProfile, MutedProfile, ModerationListParams, ProfileRelationship } from '../types.js';
|
|
3
3
|
export declare class ProfilesResource {
|
|
4
4
|
private client;
|
|
5
5
|
constructor(client: HttpClient);
|
|
@@ -60,8 +60,12 @@ export declare class ProfilesResource {
|
|
|
60
60
|
cursor?: string;
|
|
61
61
|
limit?: number;
|
|
62
62
|
}): Promise<PaginatedResponse<string>>;
|
|
63
|
-
/**
|
|
64
|
-
|
|
63
|
+
/**
|
|
64
|
+
* The caller's relationship with another user: follows in both directions
|
|
65
|
+
* (mutual when both are true), and whether the caller blocks (`is_blocking`)
|
|
66
|
+
* or mutes (`is_muting`) them, left out when the server cannot say.
|
|
67
|
+
*/
|
|
68
|
+
isFollowing(userId: string): Promise<SingleResponse<ProfileRelationship>>;
|
|
65
69
|
/**
|
|
66
70
|
* Block a user. A block is a two-way wall: it severs follows in both
|
|
67
71
|
* directions and hides each side's posts from the other.
|
|
@@ -76,7 +76,11 @@ export class ProfilesResource {
|
|
|
76
76
|
followingIds(params) {
|
|
77
77
|
return this.client.get('/profiles/me/following/ids', params);
|
|
78
78
|
}
|
|
79
|
-
/**
|
|
79
|
+
/**
|
|
80
|
+
* The caller's relationship with another user: follows in both directions
|
|
81
|
+
* (mutual when both are true), and whether the caller blocks (`is_blocking`)
|
|
82
|
+
* or mutes (`is_muting`) them, left out when the server cannot say.
|
|
83
|
+
*/
|
|
80
84
|
isFollowing(userId) {
|
|
81
85
|
return this.client.get(`/profiles/${userId}/is-following`);
|
|
82
86
|
}
|
|
@@ -928,6 +928,8 @@ export interface SocialProfile {
|
|
|
928
928
|
followersCount: number | null;
|
|
929
929
|
followingCount: number | null;
|
|
930
930
|
postsCount: number | null;
|
|
931
|
+
/** Nostr only: the NIP-05 name its domain has confirmed for this key ("lyn@primal.net"); `handle` stays the key. */
|
|
932
|
+
nip05?: string;
|
|
931
933
|
subscription: SocialSubscription | null;
|
|
932
934
|
}
|
|
933
935
|
export interface SocialSubscription {
|
|
@@ -1073,6 +1075,20 @@ export interface SocialTrendFeed extends SocialTrendProvenance {
|
|
|
1073
1075
|
trend: SocialTrend;
|
|
1074
1076
|
items: SocialPost[];
|
|
1075
1077
|
cursor: string | null;
|
|
1078
|
+
/**
|
|
1079
|
+
* Present when the page came from the app's open-network source index instead of the provider:
|
|
1080
|
+
* with a category, a topic the provider no longer lists (`gone`) or whose feed held nothing in the
|
|
1081
|
+
* category (`empty`) is served the network's indexed posts in the category. While the provider has
|
|
1082
|
+
* more of the topic, `cursor` is the provider's own continuation (the topic's deeper pages may hold
|
|
1083
|
+
* the category); once it has none, or the topic is gone, it is an `idx:tf:` cursor that continues in
|
|
1084
|
+
* the index (`cursor`), the same stream for every topic of the network. The `trend` of a gone topic
|
|
1085
|
+
* carries its id as its title. Pass the cursor back unchanged like any other. When the index cannot
|
|
1086
|
+
* answer, the page is what the provider answered (the empty page with its cursor, or the gone topic's
|
|
1087
|
+
* 404); only an `idx:tf:` page is 503 `unavailable`, which ends that continuation, not the network.
|
|
1088
|
+
*/
|
|
1089
|
+
sourceIndex?: SocialTrendSourceIndex & {
|
|
1090
|
+
reason: 'gone' | 'empty' | 'cursor';
|
|
1091
|
+
};
|
|
1076
1092
|
}
|
|
1077
1093
|
/** A member of a radar cluster: a headline or excerpt and a link only, never a body. */
|
|
1078
1094
|
export type SocialRadarSourceKind = 'outlet' | 'hackernews' | 'bluesky' | 'activitypub' | 'nostr' | 'minds';
|
package/dist/types.d.ts
CHANGED
|
@@ -22,6 +22,38 @@ export interface DiscoveryLayout {
|
|
|
22
22
|
theme?: string;
|
|
23
23
|
windows?: DiscoveryLayoutWindow[];
|
|
24
24
|
}
|
|
25
|
+
/**
|
|
26
|
+
* How to ask for the next page of the same feed: the next offset while the page order is a ranking,
|
|
27
|
+
* the cursor once the feed pages by keyset (a category feed past its window of newest judged posts), or null when the
|
|
28
|
+
* feed has no more. On a category feed, `fallover` then says where to go.
|
|
29
|
+
*/
|
|
30
|
+
export type FeedNext = {
|
|
31
|
+
offset: number;
|
|
32
|
+
} | {
|
|
33
|
+
cursor: string;
|
|
34
|
+
} | null;
|
|
35
|
+
/**
|
|
36
|
+
* One step of a category feed's fall-through, in order: the same category's older posts (the keyset
|
|
37
|
+
* cursor; the same continuation `next` carries), the same category on the open networks, the member's
|
|
38
|
+
* next categories (`via`: the order they came from, the one `posts.categories()` serves), For you
|
|
39
|
+
* (signed in), All.
|
|
40
|
+
*/
|
|
41
|
+
export type FalloverStep = {
|
|
42
|
+
kind: 'category_older';
|
|
43
|
+
category: string;
|
|
44
|
+
cursor: string;
|
|
45
|
+
} | {
|
|
46
|
+
kind: 'category_network';
|
|
47
|
+
category: string;
|
|
48
|
+
} | {
|
|
49
|
+
kind: 'category';
|
|
50
|
+
category: string;
|
|
51
|
+
via: 'personal' | 'popular' | 'static';
|
|
52
|
+
} | {
|
|
53
|
+
kind: 'for_you';
|
|
54
|
+
} | {
|
|
55
|
+
kind: 'all';
|
|
56
|
+
};
|
|
25
57
|
export interface PaginationMeta {
|
|
26
58
|
discovery?: DiscoveryLayout;
|
|
27
59
|
scanned?: number;
|
|
@@ -36,6 +68,10 @@ export interface PaginationMeta {
|
|
|
36
68
|
has_more: boolean;
|
|
37
69
|
/** Pass this as `cursor` for the next page. Absent on offset-only endpoints. */
|
|
38
70
|
next_cursor?: string | null;
|
|
71
|
+
/** The next page of this feed (offset or cursor), or null at its end. Absent on older servers. */
|
|
72
|
+
next?: FeedNext;
|
|
73
|
+
/** Present when a stage of a category feed ends: the ordered steps to follow so scrolling never stops. */
|
|
74
|
+
fallover?: FalloverStep[];
|
|
39
75
|
}
|
|
40
76
|
export interface PaginatedResponse<T> {
|
|
41
77
|
data: T[];
|
|
@@ -99,6 +135,18 @@ export interface ViewerRelationship {
|
|
|
99
135
|
/** This account follows the caller. */
|
|
100
136
|
follows_you: boolean;
|
|
101
137
|
}
|
|
138
|
+
/**
|
|
139
|
+
* The caller's relationship with one account (`profiles.isFollowing`): follows
|
|
140
|
+
* in both directions, and whether the caller blocks or mutes it. A server
|
|
141
|
+
* from before the moderation fields, or one whose read of them failed, leaves
|
|
142
|
+
* them out: absent means unknown, never "not blocked".
|
|
143
|
+
*/
|
|
144
|
+
export interface ProfileRelationship extends ViewerRelationship {
|
|
145
|
+
/** The caller blocks this account. */
|
|
146
|
+
is_blocking?: boolean;
|
|
147
|
+
/** The caller mutes this account. */
|
|
148
|
+
is_muting?: boolean;
|
|
149
|
+
}
|
|
102
150
|
export interface FollowUser extends ViewerRelationship {
|
|
103
151
|
id: string;
|
|
104
152
|
name: string;
|
package/llm.md
CHANGED
|
@@ -367,7 +367,9 @@ await r.auth.signOut(session.token)
|
|
|
367
367
|
```
|
|
368
368
|
|
|
369
369
|
`getSession(token)` and `signOut(token)` send the explicit session token as
|
|
370
|
-
Bearer auth, with a Cookie fallback for native/server runtimes.
|
|
370
|
+
Bearer auth, with a Cookie fallback for native/server runtimes. `getSession`
|
|
371
|
+
also answers `session: { id, expiresAt }`; an API-key client lists sessions with
|
|
372
|
+
`is_current: false`, so it matches `session.id` to mark its own device.
|
|
371
373
|
|
|
372
374
|
Methods: `signUp`, `signIn`, `getSession`, `signOut`, `createApiKey`, `signInAndCreateKey`, `signUpAndCreateKey`, `verifyOtpAndCreateKey`
|
|
373
375
|
|
|
@@ -479,7 +481,7 @@ const { data: rows } = await r.profiles.followers('user_xxx') // each row: is_fo
|
|
|
479
481
|
await r.profiles.mute('user_xxx') // server hides their posts and reposts of them from your feeds
|
|
480
482
|
```
|
|
481
483
|
|
|
482
|
-
Methods: `list`, `me`, `get`, `getByUsername`, `search` (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
|
|
484
|
+
Methods: `list`, `me`, `get`, `getByUsername`, `search` (ranked by match x ln(1 + followers + min(posts, 100)) over one pool of 200, the same for every page; each row (`ProfileSearchResult`) carries `followers_count`, `posts_count`, `empty` (fold empties under "more handles") and `verified` (the accounts the operator names)), `mentions` (people + agents for an @-picker, follows first), `update`, `follow`, `unfollow`, `followers`, `following`, `followingIds` (your follow set as ids, cursor-paged), `isFollowing` (follows both ways, plus `is_blocking`/`is_muting` when the server knows), `related`, `leaderboard`, `block`, `unblock`, `blocked`, `mute`, `unmute`, `muted`
|
|
483
485
|
|
|
484
486
|
---
|
|
485
487
|
|
|
@@ -708,7 +710,7 @@ await r.appSubscriptions.changeTier({ tier: 'pro', proration_date: quote.prorati
|
|
|
708
710
|
- `r.protocols` — Private app safety: `sourceControls` returns current `enabled` plus paged saved choices; `setSourceControl`, `onSourceControlChange` and `reportSource` share app scope. Example: `const choices = new Map(); const stop = r.protocols.onSourceControlChange(change => choices.set(JSON.stringify([change.source, change.control]), change.enabled));` then `await r.protocols.setSourceControl({ source: { protocol: "bluesky", kind: "actor", nativeId: "did:plc:aaaaaaaaaaaaaaaaaaaaaaaa" }, control: "mute", enabled: true, scope: "minds" });` and call `stop(); choices.clear()` on account change/unmount. Notifications follow an exact server confirmation, never an uncertain response. These preferences and reports apply in Minds, not on the remote network. Keyless post links: `const visitor = new Recursiv({ anonymous: true, inheritEnvironmentApiKey: false }); const { data: thread } = await visitor.protocols.getSharedConversation(projectId, { protocol: 'nostr', post: publicEventId })`; `sharedNetworks(projectId)` lists public conversation capabilities. Only configured apps; app moderation and rate limits apply, no member state or write authority. Member public reading: `publicNetworks`, `searchPublicProfiles`, `getPublicProfile`, `getPublicAuthorFeed`, `getFollowingFeed`, `getPublicConversation`, `getPublicTrends`, `getPublicTrendFeed`; private app subscriptions: `listSubscriptions`, `subscribeToProfile`, `unsubscribeFromProfile`, `onSubscriptionChange`. Example after selecting a returned source post: `const { data: thread } = await r.protocols.getPublicConversation({ protocol: post.protocol, post: post.nativeId, readRef: post.readRef })`. Render `focus`, oldest-first `ancestors` and direct `replies`; `public_provider_window` never promises every reply, and `limited` marks known omissions. Requires both `users:read` and `posts:read` on the current human app key. Protocol discovery/settings: `const { data: adapters } = await r.protocols.list()` returns bounded `{ id, name, icon, color, maturity, capabilities, limitations }` metadata; `supported` means implemented, not configured, healthy, authorized, or enabled. Methods: `list`, `status`, `listCandidates`, `getCandidate`, `search`, `getSettings`, `refresh`, `updateSettings`.
|
|
709
711
|
- Native Nostr: `const { data: preview } = await r.protocols.prepareNativeNostr(identityId, { action: "publish", content: draft })`; review and sign the exact public event, then `submitNativeNostr(identityId, { intent: preview.intent, event, approve: true })`. Preparation/submission never auto-retry. After uncertainty, use `getNativeNostrPost(identityId, preview.postId ?? preview.eventId)` before a user-selected retry of the identical intent/event: deletion checks the original note's `postId`, publication checks `eventId`. Check `nativeNostrAccount()` enablement first; keys stay with the signer. For replies, after `nativeNostrAccount()` advertises `reply_text_note`, use `prepareNativeNostr(identityId, { action: "reply", content: draft, parentId: post.nativeId })`. Review returned parent/root/participants/relay and exact event, sign with the connected client signer, then `submitNativeNostr(identityId, { intent, event, approve: true })`. Requires users:read, posts:read and posts:write. Kind1 public notes only; reply enablement and shared UI/live proof remain pending. Do not inherit arbitrary notification tags or relay hints. Signing mutations never auto-retry; check `getNativeNostrPost(identityId, eventId)` after uncertainty before a user-selected identical-event retry.
|
|
710
712
|
- Hosted Bluesky account on the app's own PDS: `const { data } = await r.protocols.createHostedIdentity({ protocol: 'atproto', handle: 'alice', email })`. Only the label is sent; `data.handle` carries the domain and `data.password` is returned this once, never stored. Never auto-retried. Either the account is finished or it is removed again. 409 `handle_unavailable` = choose another label; 503 `handle_dns_unavailable`/`handle_unresolvable`/`handle_unverified` = operator setup, retry later unchanged.
|
|
711
|
-
- Anonymous discovery: `const visitor = new Recursiv({ anonymous: true, inheritEnvironmentApiKey: false }); const { data } = await visitor.protocols.getSharedTrends(projectId, { protocol: 'nostr', limit: 20, discovery: true })`. Use `sharedNetworks(projectId)` capabilities; open a returned topic with `getSharedTrendFeed(projectId, { protocol: 'bluesky', topic: trend.nativeId, cursor })`. Configured apps only; no account authority or writes. App moderation and per-IP/app rate limits apply. Categories and Jev layouts reuse cached judgments without initiating paid inference; handle partial categories and finite provider windows.
|
|
713
|
+
- Anonymous discovery: `const visitor = new Recursiv({ anonymous: true, inheritEnvironmentApiKey: false }); const { data } = await visitor.protocols.getSharedTrends(projectId, { protocol: 'nostr', limit: 20, discovery: true })`. Use `sharedNetworks(projectId)` capabilities; open a returned topic with `getSharedTrendFeed(projectId, { protocol: 'bluesky', topic: trend.nativeId, cursor })`. Configured apps only; no account authority or writes. App moderation and per-IP/app rate limits apply. Categories and Jev layouts reuse cached judgments without initiating paid inference; handle partial categories and finite provider windows. Category depth: `r.posts.list({ category: 'science', sort: 'hot', discovery: true })` pages the hot window by offset and then by keyset (`meta.next` is `{ offset }`, `{ cursor }` or `null`; pass a cursor back as `cursor`); when a stage ends, `meta.fallover` lists the next feeds in order (`category_older`, `category_network`, `category` with `via`, `for_you`, `all`). One call for a category's modules: `const { data } = await r.discovery.category({ category: 'science' })` returns `stories`, `network`, `clips`, `people`, `groups`, each `{ items, ms, reason? }`.
|
|
712
714
|
- Public trends: require `public_trends` from `publicNetworks()`, then `getPublicTrends({ protocol, limit?, cursor? })`; render `kind: topics` as source topics and `kind: posts` as source posts. With `trend_feed`, open only a returned current topic using `getPublicTrendFeed({ protocol, topic: trend.nativeId, limit?, cursor? })`. Show provider, observedAt and provider_ranked_window coverage; null rankingWindow is unknown, not a daily/global ranking. Preserve source order, canonical IDs and readRef, and opaque cursors. Topic catalogs do not paginate; Mastodon trends stop at 200 source positions. Both methods require users:read plus posts:read on the current human app key; Nostr trends are unsupported. Propagate errors; trend_unavailable means refresh the topic list. No ingestion or native writes.
|
|
713
715
|
- `listCandidates({ protocol?, limit?, cursor? })` and `getCandidate(id)` are read-only quarantine review, not published-post search. Both require `admin` scope, a project-bound key, a live admin role, and existing project-admin access. The key supplies app/network scope; neither method accepts another project or network. They never invoke refresh, change settings, or publish.
|
|
714
716
|
- Lists return `{ data: ProtocolQuarantinedCandidate[], meta: { limit, has_more, next_cursor } }`; use the opaque `next_cursor`, not offset pagination. The API defaults `limit` to 20 and permits at most 50. Details return `{ data: ProtocolQuarantinedCandidate }`. Every candidate has `state: 'quarantined'` and `content_format: 'plain'`; render its bounded `content` as text, respect `content_truncated`, and distinguish upstream `published_at` from local `observed_at` / `created_at`.
|