@recursiv/sdk 0.7.69 → 0.7.71
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 +78 -10
- package/dist/client.d.ts +5 -0
- package/dist/client.js +7 -1
- package/dist/index.d.ts +14 -4
- package/dist/index.js +11 -1
- package/dist/realtime.d.ts +12 -3
- package/dist/realtime.js +10 -3
- package/dist/resources/agents.d.ts +48 -0
- package/dist/resources/agents.js +22 -0
- package/dist/resources/chat.d.ts +4 -1
- package/dist/resources/chat.js +4 -1
- package/dist/resources/curator.d.ts +9 -3
- package/dist/resources/discovery-fallover.d.ts +8 -0
- package/dist/resources/discovery-fallover.js +26 -0
- package/dist/resources/discovery-first-screen.d.ts +30 -0
- package/dist/resources/discovery-first-screen.js +149 -0
- package/dist/resources/discovery-merge.d.ts +144 -0
- package/dist/resources/discovery-merge.js +479 -0
- package/dist/resources/discovery-progressive.d.ts +117 -0
- package/dist/resources/discovery-progressive.js +414 -0
- package/dist/resources/discovery-safety.d.ts +32 -0
- package/dist/resources/discovery-safety.js +521 -0
- package/dist/resources/discovery.d.ts +59 -16
- package/dist/resources/discovery.js +147 -47
- package/dist/resources/e2ee.d.ts +41 -0
- package/dist/resources/e2ee.js +33 -0
- package/dist/resources/feeds.d.ts +3 -1
- package/dist/resources/judgments.d.ts +252 -0
- package/dist/resources/judgments.js +79 -0
- package/dist/resources/protocols.d.ts +196 -15
- package/dist/resources/protocols.js +16 -2
- package/dist/resources/search.d.ts +9 -1
- package/dist/resources/settings.d.ts +8 -1
- package/dist/resources/settings.js +9 -0
- package/dist/resources/signals.d.ts +58 -0
- package/dist/resources/signals.js +32 -0
- package/dist/types.d.ts +90 -5
- package/llm.md +25 -6
- package/package.json +1 -1
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { HttpClient } from '../client.js';
|
|
2
|
-
import type { Post, ViewerModeration } from '../types.js';
|
|
2
|
+
import type { Post, StoryOutlets, ViewerModeration } from '../types.js';
|
|
3
3
|
import type { SocialPost } from './protocols.js';
|
|
4
4
|
/** The sections a blended search can answer, in no particular order (see `SearchAnswer.order`). */
|
|
5
5
|
export declare const SEARCH_SECTIONS: readonly ["top_posts", "latest_posts", "open_network", "people", "groups", "topics", "stories", "related", "trending"];
|
|
@@ -52,6 +52,12 @@ export interface SearchGroup {
|
|
|
52
52
|
member_count: number;
|
|
53
53
|
post_count: number;
|
|
54
54
|
is_member: boolean;
|
|
55
|
+
/** The group's description as one line (legacy entities decoded, cut at a word with an ellipsis), null when it has none. Absent from an older API. */
|
|
56
|
+
description?: string | null;
|
|
57
|
+
/** When the group was last posted in (ISO 8601), null when nobody has or the API had no time to read it. Absent from an older API. */
|
|
58
|
+
last_active_at?: string | null;
|
|
59
|
+
/** The group's own words label it adult: cover its avatar. Absent from an older API. */
|
|
60
|
+
is_nsfw?: boolean;
|
|
55
61
|
}
|
|
56
62
|
export type SearchTopic = {
|
|
57
63
|
kind: 'tag';
|
|
@@ -71,6 +77,8 @@ export interface SearchStory {
|
|
|
71
77
|
theme: string | null;
|
|
72
78
|
newest_at: string;
|
|
73
79
|
sources: Partial<Record<'outlet' | 'hackernews' | 'bluesky' | 'activitypub' | 'nostr' | 'minds', number>>;
|
|
80
|
+
/** How many outlets reported it (StoryOutlets): the number every surface names for the story. Absent from a server older than 0.7.71. */
|
|
81
|
+
outlets?: StoryOutlets;
|
|
74
82
|
lead: {
|
|
75
83
|
type: 'image' | 'video';
|
|
76
84
|
url: string;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { HttpClient } from '../client.js';
|
|
2
|
-
import type { ChangePasswordInput, ConfirmationCodePurpose, ConfirmationCodeSent, DeletionStatus, LoginHistoryEntry, PaginatedResponse, RequestEmailChangeInput, RevokeOtherSessionsInput, RevokeOtherSessionsResult, Session, SingleResponse, SuccessResponse, UpdateAppearanceInput, UpdateContentPreferencesInput, UpdateNotificationChannelsInput, UpdatePrivacyInput, UserPreferences } from '../types.js';
|
|
2
|
+
import type { ChangePasswordInput, ConfirmationCodePurpose, ConfirmationCodeSent, DeletionStatus, DisableAccountInput, LoginHistoryEntry, PaginatedResponse, RequestEmailChangeInput, RevokeOtherSessionsInput, RevokeOtherSessionsResult, Session, SingleResponse, SuccessResponse, UpdateAppearanceInput, UpdateContentPreferencesInput, UpdateNotificationChannelsInput, UpdatePrivacyInput, UserPreferences } from '../types.js';
|
|
3
3
|
export declare class SettingsResource {
|
|
4
4
|
private client;
|
|
5
5
|
constructor(client: HttpClient);
|
|
@@ -66,6 +66,13 @@ export declare class SettingsResource {
|
|
|
66
66
|
success: true;
|
|
67
67
|
new_email: string;
|
|
68
68
|
}>>;
|
|
69
|
+
/**
|
|
70
|
+
* Deactivate the account (POST /settings/account/disable): it is hidden and nothing is deleted; signing back in
|
|
71
|
+
* restores it. Confirmed with the password and asked once (`retry: false`), so a retried 5xx never lands after the
|
|
72
|
+
* first attempt already went through. An API that does not serve deactivation answers 404 (`NotFoundError`): keep
|
|
73
|
+
* the control hidden until the app's API serves it.
|
|
74
|
+
*/
|
|
75
|
+
disableAccount(input: DisableAccountInput): Promise<SuccessResponse>;
|
|
69
76
|
/** Request account deletion */
|
|
70
77
|
requestDeletion(input?: {
|
|
71
78
|
password?: string;
|
|
@@ -102,6 +102,15 @@ export class SettingsResource {
|
|
|
102
102
|
verifyEmailChange(token) {
|
|
103
103
|
return this.client.post('/settings/email-change/verify', { token });
|
|
104
104
|
}
|
|
105
|
+
/**
|
|
106
|
+
* Deactivate the account (POST /settings/account/disable): it is hidden and nothing is deleted; signing back in
|
|
107
|
+
* restores it. Confirmed with the password and asked once (`retry: false`), so a retried 5xx never lands after the
|
|
108
|
+
* first attempt already went through. An API that does not serve deactivation answers 404 (`NotFoundError`): keep
|
|
109
|
+
* the control hidden until the app's API serves it.
|
|
110
|
+
*/
|
|
111
|
+
disableAccount(input) {
|
|
112
|
+
return this.client.post('/settings/account/disable', input, { retry: false });
|
|
113
|
+
}
|
|
105
114
|
/** Request account deletion */
|
|
106
115
|
requestDeletion(input) {
|
|
107
116
|
return this.client.post('/settings/delete-account', input);
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import type { HttpClient } from '../client.js';
|
|
2
|
+
import type { SingleResponse } from '../types.js';
|
|
3
|
+
/**
|
|
4
|
+
* What a member did with a post. `more_like` ("More like this") and `unhide` ("Show the post again" after "Less like
|
|
5
|
+
* this") are stored as marked clicks; an API before they were accepted answers 400 for the whole batch that carries
|
|
6
|
+
* one, so send them in a request of their own if the API may be older.
|
|
7
|
+
*/
|
|
8
|
+
export type SignalEventType = 'view' | 'click' | 'dwell' | 'react_up' | 'react_down' | 'save' | 'hide' | 'share' | 'mute_source' | 'open_chat' | 'ask_agent' | 'more_like' | 'unhide';
|
|
9
|
+
/** One engagement signal. An open-network post has no Minds id: name it in `metadata` (e.g. `{ ref }`) instead. */
|
|
10
|
+
export interface SignalEventInput {
|
|
11
|
+
event_type: SignalEventType;
|
|
12
|
+
/** The Minds post's id (a UUID). */
|
|
13
|
+
post_id?: string;
|
|
14
|
+
/** Milliseconds on screen, for `dwell` (0 to one hour). */
|
|
15
|
+
dwell_ms?: number;
|
|
16
|
+
metadata?: Record<string, unknown>;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Why nothing was stored, on a 200: `no_project_scope` (the key is not bound to an app, so there is no app database
|
|
20
|
+
* for the signal) or `project_db_unavailable` (the app database did not take it; signals are best-effort).
|
|
21
|
+
*/
|
|
22
|
+
export type SignalNotRecordedReason = 'no_project_scope' | 'project_db_unavailable';
|
|
23
|
+
export interface SignalBatchResult {
|
|
24
|
+
/** Events stored. */
|
|
25
|
+
recorded: number;
|
|
26
|
+
reason?: SignalNotRecordedReason;
|
|
27
|
+
}
|
|
28
|
+
export interface SignalRecordResult {
|
|
29
|
+
recorded: boolean;
|
|
30
|
+
reason?: SignalNotRecordedReason;
|
|
31
|
+
}
|
|
32
|
+
export interface SignalSendOptions {
|
|
33
|
+
/**
|
|
34
|
+
* The request outlives the page (`fetch` keepalive), for a flush as the page is hidden or unloads: it is on the wire
|
|
35
|
+
* before the call returns and is asked once. Browsers cap keepalive bodies (about 64 KB in flight).
|
|
36
|
+
*/
|
|
37
|
+
keepalive?: boolean;
|
|
38
|
+
/** `false` asks once: a 429 or 5xx is the call's failure at once, not retried after Retry-After. */
|
|
39
|
+
retry?: boolean;
|
|
40
|
+
}
|
|
41
|
+
/** The most events one `signals.batch` call may carry; the server answers 400 for a larger array. */
|
|
42
|
+
export declare const SIGNAL_BATCH_MAX = 100;
|
|
43
|
+
/**
|
|
44
|
+
* Engagement signals (views, dwell, hides, "More like this") for the member's own ranking, under the member's
|
|
45
|
+
* app-bound key (`agents:write`). They land in the app's database and feed For You. Best-effort: a signal the server
|
|
46
|
+
* cannot store still answers 200 with `recorded` 0 or false and a `reason`.
|
|
47
|
+
*/
|
|
48
|
+
export declare class SignalsResource {
|
|
49
|
+
private client;
|
|
50
|
+
constructor(client: HttpClient);
|
|
51
|
+
/**
|
|
52
|
+
* Store up to SIGNAL_BATCH_MAX events in one request (POST /signals/batch). The server reads a post's newest hide or
|
|
53
|
+
* unhide, so send a batch only after the one before it has landed when order matters. An empty array stores nothing.
|
|
54
|
+
*/
|
|
55
|
+
batch(events: SignalEventInput[], options?: SignalSendOptions): Promise<SingleResponse<SignalBatchResult>>;
|
|
56
|
+
/** Store one event (POST /signals/post). Prefer `batch`; this is for an API that predates it (404 there). */
|
|
57
|
+
record(event: SignalEventInput, options?: SignalSendOptions): Promise<SingleResponse<SignalRecordResult>>;
|
|
58
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/** The most events one `signals.batch` call may carry; the server answers 400 for a larger array. */
|
|
2
|
+
export const SIGNAL_BATCH_MAX = 100;
|
|
3
|
+
/**
|
|
4
|
+
* Engagement signals (views, dwell, hides, "More like this") for the member's own ranking, under the member's
|
|
5
|
+
* app-bound key (`agents:write`). They land in the app's database and feed For You. Best-effort: a signal the server
|
|
6
|
+
* cannot store still answers 200 with `recorded` 0 or false and a `reason`.
|
|
7
|
+
*/
|
|
8
|
+
export class SignalsResource {
|
|
9
|
+
client;
|
|
10
|
+
constructor(client) {
|
|
11
|
+
this.client = client;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Store up to SIGNAL_BATCH_MAX events in one request (POST /signals/batch). The server reads a post's newest hide or
|
|
15
|
+
* unhide, so send a batch only after the one before it has landed when order matters. An empty array stores nothing.
|
|
16
|
+
*/
|
|
17
|
+
batch(events, options) {
|
|
18
|
+
return this.client.post('/signals/batch', { events }, sendOptions(options));
|
|
19
|
+
}
|
|
20
|
+
/** Store one event (POST /signals/post). Prefer `batch`; this is for an API that predates it (404 there). */
|
|
21
|
+
record(event, options) {
|
|
22
|
+
return this.client.post('/signals/post', event, sendOptions(options));
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
function sendOptions(options) {
|
|
26
|
+
if (!options)
|
|
27
|
+
return undefined;
|
|
28
|
+
return {
|
|
29
|
+
...(options.retry === false ? { retry: false } : {}),
|
|
30
|
+
...(options.keepalive ? { keepalive: true } : {}),
|
|
31
|
+
};
|
|
32
|
+
}
|
package/dist/types.d.ts
CHANGED
|
@@ -32,6 +32,19 @@ export type FeedNext = {
|
|
|
32
32
|
} | {
|
|
33
33
|
cursor: string;
|
|
34
34
|
} | null;
|
|
35
|
+
/**
|
|
36
|
+
* How many outlets reported a story (0.7.71): the distinct outlets of the articles it lists, by lane. An allow-listed
|
|
37
|
+
* outlet is one outlet however many hosts it publishes on, in its lane; an article from anywhere else (a Hacker News
|
|
38
|
+
* story's link) is one outlet per host, in `other`. `mainstream + independent + other` is always `total`. The radar
|
|
39
|
+
* list, a radar story read, the News category section, World Today, the top of a feed, search and For you serve the
|
|
40
|
+
* same object for one story, so it is the one number to show; a story's `sources.outlet` counts articles, not outlets.
|
|
41
|
+
*/
|
|
42
|
+
export interface StoryOutlets {
|
|
43
|
+
total: number;
|
|
44
|
+
mainstream: number;
|
|
45
|
+
independent: number;
|
|
46
|
+
other: number;
|
|
47
|
+
}
|
|
35
48
|
/**
|
|
36
49
|
* One step of a category feed's fall-through, in order: the same category's older posts (the keyset
|
|
37
50
|
* cursor; the same continuation `next` carries), the same category on the open networks, the member's
|
|
@@ -261,9 +274,22 @@ export interface Post {
|
|
|
261
274
|
media: MediaItem[];
|
|
262
275
|
/** Set when this post is pinned in its profile or community container feed. */
|
|
263
276
|
pinned_at?: string | null;
|
|
277
|
+
/**
|
|
278
|
+
* A paid placement (`curator.forYou`): always show it labelled Boosted, and report its impression
|
|
279
|
+
* with `boosts.recordEvent(boost_campaign_id, ...)`. Absent on an organic post.
|
|
280
|
+
*/
|
|
281
|
+
boosted?: boolean;
|
|
282
|
+
boost_campaign_id?: string;
|
|
283
|
+
/** Who paid for a boosted slot. Absent on an organic post, and from a server before it said. */
|
|
284
|
+
boost_disclosure?: BoostDisclosure;
|
|
264
285
|
created_at: string;
|
|
265
286
|
updated_at: string;
|
|
266
287
|
}
|
|
288
|
+
/** Who paid for a boosted slot, read from its campaign (a boosted repost's byline is the original author's). */
|
|
289
|
+
export interface BoostDisclosure {
|
|
290
|
+
/** The paying member's handle; null when it could not be read or the account has none. */
|
|
291
|
+
payer_username: string | null;
|
|
292
|
+
}
|
|
267
293
|
export interface RepostedPost {
|
|
268
294
|
id: string;
|
|
269
295
|
/** Original Minds numeric GUID when this row came from the legacy import. */
|
|
@@ -760,6 +786,25 @@ export interface ConversationDetail {
|
|
|
760
786
|
* older APIs.
|
|
761
787
|
*/
|
|
762
788
|
last_activity_at?: string;
|
|
789
|
+
/**
|
|
790
|
+
* The live "Encrypt this chat" requests (`e2ee.requestStart`) that can
|
|
791
|
+
* still be answered, oldest first, at most one per person, however far
|
|
792
|
+
* back in the history their messages are. Served by `conversation()` on a
|
|
793
|
+
* plaintext one-on-one chat only: `[]` when there is none (or the chat is
|
|
794
|
+
* not accepted by both, or a block stands). A request is listed only while
|
|
795
|
+
* both people are inside the encryption pilot and its sender has an active
|
|
796
|
+
* device. Absent on other chats and from older APIs.
|
|
797
|
+
*/
|
|
798
|
+
encryption_requests?: ConversationEncryptionRequest[];
|
|
799
|
+
}
|
|
800
|
+
/** One entry of `ConversationDetail.encryption_requests`. */
|
|
801
|
+
export interface ConversationEncryptionRequest {
|
|
802
|
+
/** Who asked. The other person answers by starting encryption as usual, from their own device. */
|
|
803
|
+
sender_id: string;
|
|
804
|
+
/** The request's message (`metadata.kind` `e2ee_start_request`). */
|
|
805
|
+
message_id: string;
|
|
806
|
+
/** When it was asked, by the relay's clock. */
|
|
807
|
+
requested_at: string;
|
|
763
808
|
}
|
|
764
809
|
export type ChatMemberRole = 'admin' | 'member';
|
|
765
810
|
/**
|
|
@@ -807,21 +852,39 @@ export interface UpdateGroupResult {
|
|
|
807
852
|
* the conversation again (and, in an encrypted group, check its keys).
|
|
808
853
|
* `actorId` is the admin for `added` and `removed`, and the leaver for `left`
|
|
809
854
|
* and `invites_cancelled` (the invites an encrypted group's admin sent, which
|
|
810
|
-
* went with them).
|
|
855
|
+
* went with them). `accepted`: the person in `userIds` (and `actorId`)
|
|
856
|
+
* accepted their invite; everyone in the group is told. `device`: one of that
|
|
857
|
+
* person's devices can be added to the encrypted group now (it has key
|
|
858
|
+
* packages after it had none); only the group's admin is told, since only the
|
|
859
|
+
* admin's device adds someone it invited, and only on a connection that may
|
|
860
|
+
* read encrypted chat for the group (a session, or a key for the group's app;
|
|
861
|
+
* never an agent's key). Read `change` defensively: an older API never sends
|
|
862
|
+
* the last two, and a newer one may add more.
|
|
811
863
|
*/
|
|
812
864
|
export interface ChatMembersEvent {
|
|
813
865
|
conversationId: string;
|
|
814
|
-
change: 'added' | 'removed' | 'left' | 'invites_cancelled';
|
|
866
|
+
change: 'added' | 'accepted' | 'removed' | 'left' | 'invites_cancelled' | 'device';
|
|
815
867
|
userIds: string[];
|
|
816
868
|
actorId: string;
|
|
817
869
|
}
|
|
818
|
-
/**
|
|
819
|
-
|
|
870
|
+
/**
|
|
871
|
+
* Realtime `chat:conversation`. Ids only, except a group's new name:
|
|
872
|
+
* - `renamed`: a private group was renamed by its admin (`name`).
|
|
873
|
+
* - `encryption_request_withdrawn`: `actorId` withdrew their request to
|
|
874
|
+
* encrypt this one-on-one chat (`e2ee.cancelStartRequest`); it left the
|
|
875
|
+
* history. Read the conversation and its messages again.
|
|
876
|
+
* Treat any other `change` as "read the conversation again".
|
|
877
|
+
*/
|
|
878
|
+
export type ChatConversationEvent = {
|
|
820
879
|
conversationId: string;
|
|
821
880
|
change: 'renamed';
|
|
822
881
|
name: string;
|
|
823
882
|
actorId: string;
|
|
824
|
-
}
|
|
883
|
+
} | {
|
|
884
|
+
conversationId: string;
|
|
885
|
+
change: 'encryption_request_withdrawn';
|
|
886
|
+
actorId: string;
|
|
887
|
+
};
|
|
825
888
|
export interface MessageReaction {
|
|
826
889
|
type: string;
|
|
827
890
|
user_id: string;
|
|
@@ -834,6 +897,20 @@ export interface MessageMedia {
|
|
|
834
897
|
mime_type: string | null;
|
|
835
898
|
file_name: string | null;
|
|
836
899
|
}
|
|
900
|
+
/**
|
|
901
|
+
* What a message the relay wrote on someone's behalf is, by `kind`. Only such
|
|
902
|
+
* messages carry it.
|
|
903
|
+
* - `e2ee_start_request` (`v` 1): the sender asked to turn on end-to-end
|
|
904
|
+
* encryption for this one-on-one chat (`e2ee.requestStart`). Show it as
|
|
905
|
+
* that request, not as its `content`, which is a fallback for apps that do
|
|
906
|
+
* not know the kind. It is live while it is in the history and the chat is
|
|
907
|
+
* not encrypted; a withdrawn one leaves the history. The person who did not
|
|
908
|
+
* ask answers it by starting encryption as usual, from their own device.
|
|
909
|
+
*/
|
|
910
|
+
export interface MessageMetadata {
|
|
911
|
+
kind: 'e2ee_start_request';
|
|
912
|
+
v: number;
|
|
913
|
+
}
|
|
837
914
|
export interface Message {
|
|
838
915
|
id: string;
|
|
839
916
|
content: string;
|
|
@@ -842,6 +919,8 @@ export interface Message {
|
|
|
842
919
|
media: MessageMedia[];
|
|
843
920
|
reactions: MessageReaction[];
|
|
844
921
|
created_at: string;
|
|
922
|
+
/** Absent on a person's own message, and from older APIs: see MessageMetadata. */
|
|
923
|
+
metadata?: MessageMetadata;
|
|
845
924
|
}
|
|
846
925
|
export interface SendMessageInput {
|
|
847
926
|
conversation_id: string;
|
|
@@ -861,6 +940,8 @@ export interface ChatMessageEvent {
|
|
|
861
940
|
isAi?: boolean;
|
|
862
941
|
};
|
|
863
942
|
createdAt: string;
|
|
943
|
+
/** The message's metadata as stored; `kind: 'e2ee_start_request'` is an encryption request (MessageMetadata). */
|
|
944
|
+
metadata?: Record<string, unknown>;
|
|
864
945
|
}
|
|
865
946
|
export interface ChatMessageEditEvent {
|
|
866
947
|
id: string;
|
|
@@ -1466,6 +1547,10 @@ export interface RevokeOtherSessionsResult {
|
|
|
1466
1547
|
/** Whether keep_session_token named a session of this account, which was kept. */
|
|
1467
1548
|
kept_session: boolean;
|
|
1468
1549
|
}
|
|
1550
|
+
/** Confirms deactivation (`settings.disableAccount`) with the account's password. */
|
|
1551
|
+
export interface DisableAccountInput {
|
|
1552
|
+
password: string;
|
|
1553
|
+
}
|
|
1469
1554
|
export interface DeletionStatus {
|
|
1470
1555
|
requested: boolean;
|
|
1471
1556
|
scheduled_at: string | null;
|
package/llm.md
CHANGED
|
@@ -27,6 +27,7 @@ const r = new Recursiv() // reads RECURSIV_API_KEY from env
|
|
|
27
27
|
| Transactional email (receipts, alerts) | `r.email.send()` | Direct Resend/SendGrid calls |
|
|
28
28
|
| Receive external webhooks (Stripe, etc.) | `r.webhooks.register()` | Custom webhook server |
|
|
29
29
|
| Server-side cron / scheduled tasks | `r.jobs.create()` | External cron services |
|
|
30
|
+
| A member's engagement signals (views, dwell, hides) | `r.signals.batch()` | Raw fetch to `/signals/*`, or a resource's `client` |
|
|
30
31
|
|
|
31
32
|
**IMPORTANT:** Do NOT store structured app data (records, inventory, tasks, settings) as JSON inside `r.posts.content`. Posts are for social content. Use `r.databases` for application data.
|
|
32
33
|
|
|
@@ -157,6 +158,11 @@ const { content, conversationId } = await r.agents.chatStreamText(agent.id, {
|
|
|
157
158
|
conversation_id: existingConvId, // optional
|
|
158
159
|
})
|
|
159
160
|
|
|
161
|
+
// The app's agent directory, searched and sorted on the server (0.7.71). Keyless works for visitors:
|
|
162
|
+
// new Recursiv({ anonymous: true, inheritEnvironmentApiKey: false }) on the app's API host (public profiles only).
|
|
163
|
+
const { data: found, meta } = await r.agents.discover({ q: 'space', sort: 'popular', limit: 20 }) // sort: popular | recent | name
|
|
164
|
+
// found[]: { id, name, username, image, bio, follower_count, verified, created_at }; next page: discover({ ..., cursor: meta.next_cursor })
|
|
165
|
+
|
|
160
166
|
// Give agent access to project infrastructure
|
|
161
167
|
await r.agents.grantProjectAccess(agent.id, {
|
|
162
168
|
project_id: project.id,
|
|
@@ -164,7 +170,7 @@ await r.agents.grantProjectAccess(agent.id, {
|
|
|
164
170
|
})
|
|
165
171
|
```
|
|
166
172
|
|
|
167
|
-
Methods: `list`, `get`, `create`, `update`, `delete`, `chat`, `chatStream`, `chatStreamText`, `retryChatStream`, `retryChatStreamText` ("Try again": `{ conversation_id, retry_message_id }` re-answers your latest message without sending it twice), `conversations`, `listDiscoverable`, `leaderboard`, `grantProjectAccess`, `revokeProjectAccess`, `resetRequestCount`, `getByClaimToken`, `claim`, `orchestratorStatus`
|
|
173
|
+
Methods: `list`, `get`, `create`, `update`, `delete`, `chat`, `chatStream`, `chatStreamText`, `retryChatStream`, `retryChatStreamText` ("Try again": `{ conversation_id, retry_message_id }` re-answers your latest message without sending it twice), `conversations`, `discover` (search and sort the app's published agents, cursor paged, keyless-readable), `listDiscoverable`, `leaderboard`, `grantProjectAccess`, `revokeProjectAccess`, `resetRequestCount`, `getByClaimToken`, `claim`, `orchestratorStatus`
|
|
168
174
|
|
|
169
175
|
### r.brain — AI assistant messaging
|
|
170
176
|
|
|
@@ -522,6 +528,9 @@ await r.e2ee.appendEvent(conversationId, { device_id: device.id, envelope }); //
|
|
|
522
528
|
await r.e2ee.appendEvent(conversationId, { device_id: device.id, envelope: reaction, silent: true }); // no push, no move up the chat list; every member's doorbell still rings
|
|
523
529
|
const { data: page } = await r.e2ee.events(conversationId, { device_id: device.id, after_seq: 0 });
|
|
524
530
|
await r.e2ee.acknowledge(conversationId, { device_id: device.id, seq: page.headSeq });
|
|
531
|
+
// The other person has no device yet: ask them (0.7.70). The relay writes it into the chat as a message with metadata { kind: 'e2ee_start_request', v: 1 }; their start answers it.
|
|
532
|
+
await r.e2ee.requestStart(conversationId); // → { message_id, requested_at, created }; cancelStartRequest(conversationId) withdraws it
|
|
533
|
+
// The live requests, however far back: (await r.chat.conversation(conversationId)).data.encryption_requests → [{ sender_id, message_id, requested_at }]
|
|
525
534
|
// Doorbell after every append: { conversationId, seq, event? }. `event` (a small application message, as events() returns it) comes only to a socket of the owner's session or an owner key, never an agent-scoped key.
|
|
526
535
|
// The policy is required: policyForConversationType(conversation.type) from @recursiv/e2ee ('direct' or 'group').
|
|
527
536
|
r.realtime.onE2eeEvent(({ conversationId, event }) => (event ? controller.applyPushed(conversationId, event, policy) : controller.sync(conversationId, undefined, policy)));
|
|
@@ -692,7 +701,7 @@ for (const section of answer.order) render(section, answer.sections[section])
|
|
|
692
701
|
// top_posts, latest_posts, open_network, people, groups, topics, stories, related, trending
|
|
693
702
|
```
|
|
694
703
|
|
|
695
|
-
`trending` joins when the answer is thin or old, so a search never dead-ends. `top_posts` is floored as `r.posts.search`'s relevance page is; `people` rows carry `followers_count`, `posts_count`, `empty` and `verified` (see `r.profiles.search`). Page further with `r.posts.search`. Needs `posts:read`; `people` needs `users:read`, `groups` `communities:read`; `open_network` and `stories` need `users:read` plus the caller's own app key on its network, `trending` that app key. What the key cannot have is in `omitted` (`scope` / `unavailable`).
|
|
704
|
+
`trending` joins when the answer is thin or old, so a search never dead-ends. `top_posts` is floored as `r.posts.search`'s relevance page is; `people` rows carry `followers_count`, `posts_count`, `empty` and `verified` (see `r.profiles.search`); `groups` rows carry `description` (one line) and `last_active_at`, to tell groups of one name apart. Page further with `r.posts.search`. Needs `posts:read`; `people` needs `users:read`, `groups` `communities:read`; `open_network` and `stories` need `users:read` plus the caller's own app key on its network, `trending` that app key. What the key cannot have is in `omitted` (`scope` / `unavailable`).
|
|
696
705
|
|
|
697
706
|
### r.curator — World Today and its story pages
|
|
698
707
|
|
|
@@ -711,7 +720,7 @@ for await (const p of r.curator.storyPostPages({ storyKey: page.story_key }, { r
|
|
|
711
720
|
```typescript
|
|
712
721
|
const { data: c } = await r.communities.create({ name: 'Denver Devs', slug: 'denver-devs', privacy: 'public' })
|
|
713
722
|
await r.communities.join(c.id)
|
|
714
|
-
const { data: found } = await r.communities.search('denver', { limit: 10 }) // name matches first
|
|
723
|
+
const { data: found } = await r.communities.search('denver', { limit: 10 }) // name matches first, then size; no 1-member group unless named exactly or yours
|
|
715
724
|
const { data: mine } = await r.communities.search('book', { member: true }) // only my groups
|
|
716
725
|
const { data: people } = await r.communities.members(c.id, { q: 'ada' }) // name or username
|
|
717
726
|
```
|
|
@@ -779,7 +788,13 @@ Methods: `list`, `get`, `create`, `delete`
|
|
|
779
788
|
|
|
780
789
|
## Platform Resources
|
|
781
790
|
|
|
782
|
-
- `r.settings` — User preferences, sessions, password changes, account deletion. `revokeOtherSessions({ keep_session_token? })` signs out every other device and keeps this key (and the proven session); `revokeAllSessions()` also revokes this key. Accounts with no password confirm `requestEmailChange({ new_email, email_code })` with a code from `requestConfirmationCode('email_change')`. With a project-bound key, `getPreferences()` includes app-scoped `content`; persist changes with `updateContent({ show_nsfw?, autoplay_video?, ai_enabled?, default_feed? })`.
|
|
791
|
+
- `r.settings` — User preferences, sessions, password changes, account deletion. `revokeOtherSessions({ keep_session_token? })` signs out every other device and keeps this key (and the proven session); `revokeAllSessions()` also revokes this key. Accounts with no password confirm `requestEmailChange({ new_email, email_code })` with a code from `requestConfirmationCode('email_change')`. With a project-bound key, `getPreferences()` includes app-scoped `content`; persist changes with `updateContent({ show_nsfw?, autoplay_video?, ai_enabled?, default_feed? })`. `disableAccount({ password })` (0.7.71) deactivates the account (hidden, nothing deleted; signing back in restores it), asked once; an API that does not serve deactivation answers 404 `NotFoundError`, so keep the control hidden until yours does.
|
|
792
|
+
- `r.signals` (0.7.71) — The member's engagement signals for their own ranking (For You), under their app-bound key (`agents:write`): `batch(events, { keepalive?, retry? })` sends up to `SIGNAL_BATCH_MAX` (100) `{ event_type, post_id?, dwell_ms?, metadata? }` to POST /signals/batch; `record(event)` sends one to POST /signals/post (for an API before the batch route, which answers that one 404). `event_type`: `view`, `click`, `dwell`, `react_up`, `react_down`, `save`, `hide`, `share`, `mute_source`, `open_chat`, `ask_agent`, `more_like`, `unhide`. Best-effort: a signal the server cannot store answers 200 with `recorded` 0 (or false) and a `reason` (`no_project_scope`, `project_db_unavailable`). `keepalive: true` is for a flush as the page is hidden or unloads: the request is on the wire before the call returns and is asked once.
|
|
793
|
+
|
|
794
|
+
```typescript
|
|
795
|
+
await r.signals.batch([{ event_type: 'view', post_id: post.id }, { event_type: 'dwell', post_id: post.id, dwell_ms: 4200 }])
|
|
796
|
+
addEventListener('pagehide', () => { void r.signals.batch(pending.splice(0, SIGNAL_BATCH_MAX), { keepalive: true }).catch(() => {}) })
|
|
797
|
+
```
|
|
783
798
|
- `r.billing` — Usage tracking, checkout sessions, portal sessions, metered billing
|
|
784
799
|
- `r.appSubscriptions` — Project-scoped consumer plans: `checkout` (409 `subscription_exists` when the member already pays), `status`, `createPortalSession({ return_url, flow?: 'cancel' })` (`flow: 'cancel'` opens Stripe's cancel confirmation; 409 `no_subscription` when there is no plan to cancel), `previewTierChange({ tier })`, `changeTier({ tier, proration_date? })` (in-place, prorated, never a second subscription)
|
|
785
800
|
- `r.boosts` — Promoted posts (micro-USD): `create`, `mine`, `balance`, `pause`, `resume`, `cancel`, `recordEvent`; buy credit with `topup({ amount_cents, return_url })` then `confirmTopup(sessionId)` (credits once); admin `listPendingReview`, `review`, `grant`
|
|
@@ -805,6 +820,7 @@ const { data: quote } = await r.appSubscriptions.previewTierChange({ tier: 'pro'
|
|
|
805
820
|
await r.appSubscriptions.changeTier({ tier: 'pro', proration_date: quote.prorationDate })
|
|
806
821
|
```
|
|
807
822
|
- `r.admin` — Admin operations (user management, stats, network settings); content reports: `listReports({ status? })`, `resolveReport(id)`, `dismissReport(id)` (project-bound keys see only their app's reports)
|
|
823
|
+
- `r.judgments` (0.7.71) — Jev's labels of one post. `get({ kind: 'post' | 'network_post', id })`: switch on `data.view` — `'full'` for the post's author and staff (`judgments` with answers, `safety` verdict, `viral.withheld`; staff also `reviewer_ids`, `review_note`), `'summary'` for other members (`category`, `politics` band in words with its `judgment_id`; never a probability or cell). `data.status === 'unavailable'` (returned, not thrown): hide label actions. `getPublic(projectId, { kind, id })` on an anonymous client: `view: 'category'` only. `dispute(judgmentId, { question_key: 'placement', proposed: 'cm' | … | 'not_political', note? })` (safety: `question_key: 'safety'`, author only): the author's dispute is an appeal; a repeat answers `existing: true`. Staff: `review(judgmentId, { outcome: 'uphold' } | { outcome: 'override', proposed, note?, apply_to_shared? })`, sent once (never retried): a compass override answers 202 `{ status: 'awaiting_reviewer', agreeing, needed }` until a second reviewer (or an agreeing Jev re-judge) proposes the same; a safety override to `'none'` that would free other posts throws 409 `shared_words` (`isJudgmentSharedWordsError(e)`, `e.details.shared_posts`) until resent with `apply_to_shared: true`. Budgets (60 reads/hour, 25 of one author's posts, 20 disputes/day) throw `RateLimitError` with `scope: 'action'` at once. Example: `const { data } = await r.judgments.get({ kind: 'post', id }); if (data.status === 'ok' && data.view === 'summary' && data.politics) await r.judgments.dispute(data.politics.judgment_id, { question_key: 'placement', proposed: 'not_political' })`
|
|
808
824
|
- `r.inviteCodes` / `r.inviteCodesAdmin` — Invite code management
|
|
809
825
|
- `r.organizationSettings` / `r.organizationSecurity` — Org configuration
|
|
810
826
|
- Native follows: `nativeFollowState({ accountId, protocol, nativeId })`, `prepareNativeFollow({ clientMutationId, accountId, protocol, nativeId, action: "follow" | "unfollow" })`, `submitNativeFollow(id, { approve: true, event? })`, `nativeFollowStatus(id)`. Show the acting identity and native following/requested/unfollowed state separately from the Minds feed subscription. Nostr must sign the complete review event without dropping other contacts; stale lists require a fresh review. Keep mutation IDs and reconcile ambiguous outcomes without replay. Requires current follow scopes and API `NATIVE_SOCIAL_FOLLOW_PROJECTS` plus source controls.
|
|
@@ -814,14 +830,17 @@ await r.appSubscriptions.changeTier({ tier: 'pro', proration_date: quote.prorati
|
|
|
814
830
|
- 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.
|
|
815
831
|
- 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.
|
|
816
832
|
- 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`.
|
|
833
|
+
- Progressive Discover feed (0.7.71): `const feed = r.discovery.createProgressiveFeed({ networks, category, recommend, top: { ref: 'builtin:all' }, seen, seenClusters }); const round = await feed.next(part => draw(part))` opens one `createFeed` session per source (Minds with the radar and suggestions, one per network with `public_trends`) and hands each source's page on as a `DiscoveryPart` (`{ source, round, modules, composed?, mixed? }`) the moment it lands, instead of a page that waits for its slowest network (up to 15 s); a round waits `DISCOVERY_STRAGGLER_MS` past its first answer and rejects only when every source failed. The first load is held until Minds and one other network answered (or `FIRST_SCREEN_MS`, 2.5 s, after the first answer; `firstScreenMs: VISITOR_FIRST_SCREEN_MS` for a visitor) and arrives as one `composed: true` part from `composeFirstScreen`: the top's first post when it is a playable video, else a picture lead from the best 8 by engagement (never a story, rail, video strip, column, `isSensitivePost` post or `linksAdultSite` porn link), then every network's posts by engagement (`DiscoveryEngagementMerge`) with row 2 from another network than the lead's when one is left (`FIRST_SCREEN_NETWORK_ROWS`), a story as row 3 and a rail as row 5, nothing dropped. Merge later `mixed` parts with `mergeDiscoveryArrival(rows, arriving, reachedIds)`. `top`/`seen`/`seenClusters`/`bucket` reach every session; the top is read once per open. Also `retryFailed()`, `onStatus(listener)`, `setNetworks(catalog)`; settings `perSource`, `firstScreen`, `onLanded`. For you: `createProgressiveFeed({ createFeed: o => r.discovery.createPersonalFeed(o) }, options, { perSource: false })`. A session's `seen` now also leaves seen network posts out of trend pages, previews and topic pages; in a category a topic page with nothing new ends its topic and after `DISCOVERY_EMPTY_TOPIC_PAGES` (3) in a row the network's other topics are skipped.
|
|
817
834
|
- 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.
|
|
818
835
|
- 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.
|
|
836
|
+
- One outlet count per story (0.7.71): a radar story (list, story read, archive), a World Today story (`outlets`, with `outlet_count` its total and `lane_mix` its lanes), a category strip story, a `feeds.top`/`feeds.stories` story and a search story carry `outlets: { total, mainstream, independent, other }`, the distinct outlets of the articles the story lists (an allow-listed outlet once, an unlisted host in `other`; the buckets sum to `total`). Show `outlets.total`; `sources.outlet` counts articles. `DiscoveryCluster.outletCount` reads it. An older server omits it.
|
|
819
837
|
- 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.
|
|
820
838
|
- 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.
|
|
821
839
|
- `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.
|
|
822
840
|
- 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`.
|
|
823
841
|
- Safe source IDs are preserved byte-exact. `external_id: null` means unsafe or malformed provenance was withheld, not an invented ID; `author.native_id` may likewise be absent or withheld. Preserve nulls in both list and detail views, and use the candidate's own `id` for `getCandidate`.
|
|
824
842
|
- API authorization/validation/not-found/service errors propagate as normal SDK errors, including when the deployed API does not yet support these routes. Never substitute synthetic candidates or turn these failures into an empty queue. `getCandidate` rejects empty/whitespace and URL dot-segment IDs with `ValidationError` before sending; other IDs are encoded and validated by the server.
|
|
843
|
+
- `ingestionHealth()` (0.7.71) reads `GET /protocols/ingestion/health` with no query (the API rejects one; scope comes from the key): `{ data: ProtocolIngestionHealthReport }` with `runtime` switches, `lanes` (per lane `jobs`, `current` open job with `position` { feed_index, term_index, page }, `pages_committed`, `consecutive_failures`, `lease_expired`, `waiting_since`, `starved_since`; `last_done_at`; 24-hour `activity` with `reasons`; `dead_letters` grouped by reason and position) and `circuits` (provider or fetch-host rows that are open, counting or killed). Same key and authority as `listCandidates` but no `content_intelligence_admin` binding. A section that did not answer is null and listed in `unavailable`: show it as unknown, not as zero. Errors throw.
|
|
825
844
|
- `r.inbox` — Notification feed
|
|
826
845
|
- `r.network` — Federation/network config
|
|
827
846
|
- `r.simulator` — Simulation/testing
|
|
@@ -1008,8 +1027,8 @@ publication; history stores no signature, signing key or intent. Keep the acting
|
|
|
1008
1027
|
user/app-bound result IDs and discard UI state after an account switch. The last
|
|
1009
1028
|
read-back observation is separate from retained publication/deletion evidence.
|
|
1010
1029
|
|
|
1011
|
-
Standalone public text posts: `prepareNativePost({ clientMutationId, text, accountIds })` → review → `submitNativePost(id, { approve: true, deliveryIds })`. Use `nativePostStatus(id)` / `nativePostHistory({limit,cursor})` for private durable outcomes and `prepareNativePostRemoval(id,{deliveryIds})` → `removeNativePost(id,{approve:true,removals:[{deliveryId,removalId}]})` for reviewed own-post removal. Check `publish_text`; no reply parent or audience override. Attach already-uploaded Minds images with `media: [{ url, alt?, width?, height?, mimeType? }]` (at most four JPEG/PNG/WebP/GIF, Minds-hosted HTTPS URLs only, a Mastodon GIF must be the only attachment) on accounts that also advertise `publish_media` — grants made before image scopes existed keep working for text and must be reconnected first. Always pass the member's own `alt`; an omitted one is sent empty, never invented. The receipt reports `media: { count, alt_missing }`. Preserve mutation IDs, never create a new send to recover uncertainty.
|
|
1012
|
-
Bluesky quote: accounts must advertise `quote`; call `prepareNativePost({clientMutationId,text,accountIds,quote:{protocol:'bluesky',nativeId:sourceAtUri}})`, review each returned quote/account/comment, then approve via the same submit method. The server binds the source CID and rechecks permissions before sending. No cross-network quote conversion. Public reads expose one filtered `post.quote` (`available` with `post`, or `unavailable` without source details). 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.
|
|
1030
|
+
Standalone public text posts: `prepareNativePost({ clientMutationId, text, accountIds })` → review → `submitNativePost(id, { approve: true, deliveryIds })`. Use `nativePostStatus(id)` / `nativePostHistory({limit,cursor})` for private durable outcomes and `prepareNativePostRemoval(id,{deliveryIds})` → `removeNativePost(id,{approve:true,removals:[{deliveryId,removalId}]})` for reviewed own-post removal. Check `publish_text`; no reply parent or audience override. Attach already-uploaded Minds images with `media: [{ url, alt?, width?, height?, mimeType? }]` (at most four JPEG/PNG/WebP/GIF, Minds-hosted HTTPS URLs only, a Mastodon GIF must be the only attachment) on accounts that also advertise `publish_media` — grants made before image scopes existed keep working for text and must be reconnected first. Always pass the member's own `alt`; an omitted one is sent empty, never invented. The receipt reports `media: { count, alt_missing }`. A photo-only post (no words) passes `text: ''` with at least one `media` item: Bluesky gets an empty text with the images, Mastodon a status with only the photos, each `alt` as its description; `''` without media or text of only spaces is refused, and replies always need text. Preserve mutation IDs, never create a new send to recover uncertainty.
|
|
1031
|
+
Bluesky quote: accounts must advertise `quote`; call `prepareNativePost({clientMutationId,text,accountIds,quote:{protocol:'bluesky',nativeId:sourceAtUri}})`, review each returned quote/account/comment, then approve via the same submit method. The server binds the source CID and rechecks permissions before sending. Mastodon quote (0.7.71): `quote:{protocol:'activitypub',nativeId:statusUri}`; the member's server must support quote posts (API version 7) and allow it, else `quote_not_supported` / `reply_not_allowed`, never a link. Nostr quote: `prepareNativeNostr(id,{action:'publish',content,quote:{protocol:'nostr',nativeId:eventId}})` adds NIP-18 `q`/`p` tags and a `nostr:nevent` line; sign and submit as usual. No cross-network quote conversion. Public reads expose one filtered `post.quote` (`available` with `post`, or `unavailable` without source details). 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.
|
|
1013
1032
|
|
|
1014
1033
|
Public post search (API/SDK 0.7.35): `const { data: page } = await r.protocols.searchPublicPosts({ protocol: 'nostr', query: 'open social', limit: 10 })`. Bluesky/ActivityPub require a selected `accountId` from `nativeSocialAccounts()` with `post_search`; older Bluesky grants must reconnect with consent. Open their results with `getConnectedSocialConversation({ protocol, accountId: page.readerAccountId, post: item.nativeId, readRef: item.readRef })` and pass the same accountId to `reportSource` for post reports. Nostr uses `getPublicConversation`. Pages report provider/observedAt and bounded coverage; no invented exhaustive or global search. Keep cursors paired with the exact owner, query, account, limit and connection revision. No writes, credentials or new ingestion are part of search.
|
|
1015
1034
|
|