@recursiv/sdk 0.7.81 → 0.7.83

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.
Files changed (38) hide show
  1. package/README.md +45 -2
  2. package/dist/assistant/fixedCardData.d.ts +91 -0
  3. package/dist/assistant/fixedCardData.js +372 -0
  4. package/dist/assistant/fixedCards.d.ts +35 -0
  5. package/dist/assistant/fixedCards.js +904 -0
  6. package/dist/assistant/hiddenText.d.ts +24 -0
  7. package/dist/assistant/hiddenText.js +263 -0
  8. package/dist/assistant/index.d.ts +22 -0
  9. package/dist/assistant/index.js +22 -0
  10. package/dist/assistant/render.d.ts +8 -0
  11. package/dist/assistant/render.js +253 -0
  12. package/dist/assistant/subjectBundle.d.ts +30 -0
  13. package/dist/assistant/subjectBundle.js +518 -0
  14. package/dist/assistant/types.d.ts +353 -0
  15. package/dist/assistant/types.js +11 -0
  16. package/dist/assistant-surfaces.d.ts +42 -0
  17. package/dist/assistant-surfaces.js +113 -0
  18. package/dist/index.d.ts +8 -4
  19. package/dist/index.js +2 -1
  20. package/dist/realtime.d.ts +10 -3
  21. package/dist/realtime.js +11 -2
  22. package/dist/resources/agents.d.ts +6 -1
  23. package/dist/resources/agents.js +12 -2
  24. package/dist/resources/chat.d.ts +73 -1
  25. package/dist/resources/chat.js +83 -1
  26. package/dist/resources/e2ee.d.ts +56 -0
  27. package/dist/resources/e2ee.js +21 -0
  28. package/dist/resources/link-preview.d.ts +42 -1
  29. package/dist/resources/link-preview.js +36 -2
  30. package/dist/resources/network.d.ts +12 -0
  31. package/dist/resources/project-brain.d.ts +8 -0
  32. package/dist/resources/reports.d.ts +24 -0
  33. package/dist/resources/reports.js +12 -0
  34. package/dist/resources/users.d.ts +11 -2
  35. package/dist/resources/users.js +1 -1
  36. package/dist/types.d.ts +34 -0
  37. package/llm.md +35 -3
  38. package/package.json +6 -1
@@ -1,4 +1,4 @@
1
- import { RecursivError } from '../errors.js';
1
+ import { RateLimitError, rateLimitDetailsFromResponse, RecursivError } from '../errors.js';
2
2
  import { parseSSE } from '../lib/sse.js';
3
3
  // ── Agent directory (GET /agents/discover) ──
4
4
  /** The orders the agent directory serves (server: features/agents/agentDirectory.ts). */
@@ -83,7 +83,10 @@ export class AgentsResource {
83
83
  delete(id) {
84
84
  return this.client.delete(`/agents/${id}`);
85
85
  }
86
- /** Send a message to an agent (async — agent responds asynchronously) */
86
+ /**
87
+ * Send a message to an agent (async — agent responds asynchronously).
88
+ * `turn: { safe: true }` makes it a safe turn in the caller's Asks thread (see `AgentChatTurn`).
89
+ */
87
90
  chat(id, input) {
88
91
  return this.client.post(`/agents/${id}/chat`, input);
89
92
  }
@@ -224,6 +227,8 @@ export class AgentsResource {
224
227
  *
225
228
  * Requires `ReadableStream` support (Node.js, modern browsers).
226
229
  * For React Native, use `chatStreamText()` instead.
230
+ * `turn: { safe: true }` streams a safe turn in the caller's Asks thread (see `AgentChatTurn`); a refusal throws a
231
+ * `RecursivError` whose code is a `SafeTurnRefusalCode`, before anything streams.
227
232
  */
228
233
  async *chatStream(id, input) {
229
234
  yield* this.streamChunks(id, input);
@@ -286,6 +291,11 @@ export class AgentsResource {
286
291
  code: 'stream_error',
287
292
  });
288
293
  }
294
+ // A 429 is a RateLimitError, as on every other request: with its scope and reset (a safe turn's daily cap is
295
+ // `scope: 'action'`, see SafeTurnRefusalCode).
296
+ if (response.status === 429 && errorBody?.error && typeof errorBody.error === 'object') {
297
+ throw new RateLimitError(errorBody.error, rateLimitDetailsFromResponse(response.headers, errorBody.error));
298
+ }
289
299
  throw new RecursivError(response.status, errorBody.error);
290
300
  }
291
301
  return response;
@@ -116,7 +116,11 @@ export declare class ChatResource {
116
116
  type: string;
117
117
  };
118
118
  }>>;
119
- /** Edit a message (only the sender can edit) */
119
+ /**
120
+ * Edit a message's words (only the sender can edit). Keep each `chat-attachment:` reference it carries as it is: the
121
+ * server refuses an edit that drops, adds or swaps one (400 `attachment_refs_changed`, 0.7.82). To take an attachment
122
+ * back, delete the message.
123
+ */
120
124
  editMessage(messageId: string, input: {
121
125
  content: string;
122
126
  }): Promise<SingleResponse<Message>>;
@@ -130,4 +134,72 @@ export declare class ChatResource {
130
134
  unreadCount(conversationId: string): Promise<SingleResponse<{
131
135
  unread_count: number;
132
136
  }>>;
137
+ /**
138
+ * A PUT URL for one photo or voice note to send in a plaintext chat (0.7.82). With the server's private chat
139
+ * storage on, `private` is true, the URL writes into a private bucket and `ref` is `chat-attachment:<key>`, the text
140
+ * to send; with it off, `ref` is the upload's public URL. An end-to-end encrypted chat answers 409
141
+ * `conversation_encrypted`: its attachments go through `r.e2ee`. Most callers want `uploadAttachment`.
142
+ */
143
+ attachmentUploadUrl(conversationId: string, input: ChatAttachmentUploadUrlInput): Promise<SingleResponse<ChatAttachmentUploadUrl>>;
144
+ /**
145
+ * Upload one photo or voice note for a plaintext chat end to end and resolve the text to send for it (0.7.82): read
146
+ * the bytes, get the URL, PUT them with the matching Content-Type, and reject a failed transfer. Send `ref` as the
147
+ * message (or within it): a private reference renders through `signAttachments`, a public URL as before.
148
+ */
149
+ uploadAttachment(conversationId: string, input: ChatAttachmentUploadInput): Promise<ChatAttachmentUploadUrl>;
150
+ /**
151
+ * Short-lived URLs for a conversation's private attachments (0.7.82), at most 50 keys a call (the keys in its
152
+ * messages' `chat-attachment:` references; `chatAttachmentKeysIn` finds them). The server signs a key only for a
153
+ * member of the conversation, and only while a message its sender wrote there still carries it; any other key
154
+ * comes back with `url: null`. A URL works for about five minutes and is served `private, no-store`: ask again
155
+ * rather than keeping it.
156
+ */
157
+ signAttachments(conversationId: string, keys: string[]): Promise<SingleResponse<{
158
+ attachments: SignedChatAttachment[];
159
+ }>>;
160
+ }
161
+ export interface ChatAttachmentUploadUrlInput {
162
+ /** image/jpeg, image/png, image/webp, image/gif, audio/mpeg, audio/mp4, audio/wav, audio/ogg or audio/webm. */
163
+ content_type: string;
164
+ /** The exact byte count the PUT will send (signed into the URL; at most 50 MB). */
165
+ content_length: number;
166
+ }
167
+ export interface ChatAttachmentUploadUrl {
168
+ /** True when the attachment goes to the server's private chat storage. */
169
+ private: boolean;
170
+ /** PUT the bytes here with the same Content-Type. */
171
+ upload_url: string;
172
+ key: string;
173
+ /** The text to send for this attachment: `chat-attachment:<key>` when private, else the public URL. */
174
+ ref: string;
175
+ }
176
+ export interface ChatAttachmentUploadInput {
177
+ /** Caller-held bytes, such as a browser File or React Native Blob. */
178
+ blob?: Blob;
179
+ /** A local URI that the SDK should read into a Blob first. */
180
+ uri?: string;
181
+ /** Defaults to blob.type. */
182
+ contentType?: string;
183
+ /** Optional transport override for React Native adapters and focused tests. */
184
+ fetchImpl?: typeof fetch;
185
+ }
186
+ export interface SignedChatAttachment {
187
+ key: string;
188
+ /** A GET URL valid until `expires_at`, or null when this caller may not open the key. */
189
+ url: string | null;
190
+ expires_at: string | null;
191
+ }
192
+ /** The text a message carries before a private chat attachment's key. */
193
+ export declare const CHAT_ATTACHMENT_REF_PREFIX = "chat-attachment:";
194
+ /** One private attachment a message's text refers to. */
195
+ export interface ChatAttachmentRef {
196
+ /** The whole reference as it appears in the text (`chat-attachment:<key>`). */
197
+ ref: string;
198
+ /** The key to pass to `signAttachments`. */
199
+ key: string;
200
+ kind: 'image' | 'audio';
133
201
  }
202
+ /** The private attachments a message's text refers to, in order, each once (0.7.82). */
203
+ export declare function chatAttachmentRefsIn(content: string | null | undefined): ChatAttachmentRef[];
204
+ /** The private attachment keys a message's text refers to, for `signAttachments` (0.7.82). */
205
+ export declare function chatAttachmentKeysIn(content: string | null | undefined): string[];
@@ -1,3 +1,4 @@
1
+ import { RecursivError } from '../errors.js';
1
2
  export class ChatResource {
2
3
  client;
3
4
  constructor(client) {
@@ -127,7 +128,11 @@ export class ChatResource {
127
128
  reactToMessage(messageId, input) {
128
129
  return this.client.post(`/chat/messages/${messageId}/react`, input);
129
130
  }
130
- /** Edit a message (only the sender can edit) */
131
+ /**
132
+ * Edit a message's words (only the sender can edit). Keep each `chat-attachment:` reference it carries as it is: the
133
+ * server refuses an edit that drops, adds or swaps one (400 `attachment_refs_changed`, 0.7.82). To take an attachment
134
+ * back, delete the message.
135
+ */
131
136
  editMessage(messageId, input) {
132
137
  return this.client.put(`/chat/messages/${messageId}`, input);
133
138
  }
@@ -143,4 +148,81 @@ export class ChatResource {
143
148
  unreadCount(conversationId) {
144
149
  return this.client.get('/chat/unread-count', { conversation_id: conversationId });
145
150
  }
151
+ /**
152
+ * A PUT URL for one photo or voice note to send in a plaintext chat (0.7.82). With the server's private chat
153
+ * storage on, `private` is true, the URL writes into a private bucket and `ref` is `chat-attachment:<key>`, the text
154
+ * to send; with it off, `ref` is the upload's public URL. An end-to-end encrypted chat answers 409
155
+ * `conversation_encrypted`: its attachments go through `r.e2ee`. Most callers want `uploadAttachment`.
156
+ */
157
+ attachmentUploadUrl(conversationId, input) {
158
+ return this.client.post(`/chat/conversations/${conversationId}/attachments/upload-url`, input);
159
+ }
160
+ /**
161
+ * Upload one photo or voice note for a plaintext chat end to end and resolve the text to send for it (0.7.82): read
162
+ * the bytes, get the URL, PUT them with the matching Content-Type, and reject a failed transfer. Send `ref` as the
163
+ * message (or within it): a private reference renders through `signAttachments`, a public URL as before.
164
+ */
165
+ async uploadAttachment(conversationId, input) {
166
+ const hasBlob = input.blob !== undefined;
167
+ const hasUri = typeof input.uri === 'string' && input.uri.length > 0;
168
+ if (hasBlob === hasUri)
169
+ throw attachmentError(0, 'Provide exactly one of blob or uri.', 'invalid_upload_source');
170
+ const doFetch = input.fetchImpl ?? fetch;
171
+ let blob = input.blob;
172
+ if (!blob && input.uri) {
173
+ const source = await doFetch(input.uri);
174
+ if (!source.ok)
175
+ throw attachmentError(source.status, `Could not read upload source (HTTP ${source.status}).`, 'upload_source_failed');
176
+ blob = await source.blob();
177
+ }
178
+ if (!blob)
179
+ throw attachmentError(0, 'Upload source did not produce any bytes.', 'empty_upload_source');
180
+ const contentType = input.contentType?.trim() || blob.type?.trim();
181
+ if (!contentType)
182
+ throw attachmentError(0, 'A content type is required for chat attachments.', 'missing_content_type');
183
+ const minted = await this.attachmentUploadUrl(conversationId, { content_type: contentType, content_length: blob.size });
184
+ if (!minted?.data?.upload_url || !minted.data.ref) {
185
+ throw attachmentError(0, 'The API did not return a chat attachment upload URL.', 'missing_upload_url');
186
+ }
187
+ const response = await doFetch(minted.data.upload_url, { method: 'PUT', body: blob, headers: { 'Content-Type': contentType } });
188
+ if (!response.ok)
189
+ throw attachmentError(response.status, `Chat attachment upload failed (HTTP ${response.status}).`, 'chat_attachment_upload_failed');
190
+ return minted.data;
191
+ }
192
+ /**
193
+ * Short-lived URLs for a conversation's private attachments (0.7.82), at most 50 keys a call (the keys in its
194
+ * messages' `chat-attachment:` references; `chatAttachmentKeysIn` finds them). The server signs a key only for a
195
+ * member of the conversation, and only while a message its sender wrote there still carries it; any other key
196
+ * comes back with `url: null`. A URL works for about five minutes and is served `private, no-store`: ask again
197
+ * rather than keeping it.
198
+ */
199
+ signAttachments(conversationId, keys) {
200
+ return this.client.post(`/chat/conversations/${conversationId}/attachments/sign`, { keys });
201
+ }
202
+ }
203
+ function attachmentError(status, message, code) {
204
+ return new RecursivError(status, { type: 'upload_error', message, code });
205
+ }
206
+ /** The text a message carries before a private chat attachment's key. */
207
+ export const CHAT_ATTACHMENT_REF_PREFIX = 'chat-attachment:';
208
+ const CHAT_ATTACHMENT_REF_RE = /chat-attachment:(chat\/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\.([a-z0-9]{2,5}))/g;
209
+ /** The extensions the server mints a private attachment key with (and jpeg), as it parses one; anything else is text. */
210
+ const IMAGE_EXTENSIONS = new Set(['jpg', 'jpeg', 'png', 'webp', 'gif']);
211
+ const AUDIO_EXTENSIONS = new Set(['mp3', 'm4a', 'wav', 'ogg', 'weba']);
212
+ /** The private attachments a message's text refers to, in order, each once (0.7.82). */
213
+ export function chatAttachmentRefsIn(content) {
214
+ if (!content?.includes(CHAT_ATTACHMENT_REF_PREFIX))
215
+ return [];
216
+ const found = new Map();
217
+ for (const match of content.matchAll(CHAT_ATTACHMENT_REF_RE)) {
218
+ const extension = match[2];
219
+ const kind = AUDIO_EXTENSIONS.has(extension) ? 'audio' : IMAGE_EXTENSIONS.has(extension) ? 'image' : null;
220
+ if (kind && !found.has(match[1]))
221
+ found.set(match[1], { ref: match[0], key: match[1], kind });
222
+ }
223
+ return [...found.values()];
224
+ }
225
+ /** The private attachment keys a message's text refers to, for `signAttachments` (0.7.82). */
226
+ export function chatAttachmentKeysIn(content) {
227
+ return chatAttachmentRefsIn(content).map((attachment) => attachment.key);
146
228
  }
@@ -68,6 +68,38 @@ export interface E2eeAccountReset {
68
68
  * - `conflict` (409): the device name or key was used before; start the device again with fresh ones.
69
69
  */
70
70
  export type E2eeResetErrorCode = 'confirmation_failed' | 'code_limit' | 'no_email_on_file' | 'email_delivery_unavailable' | 'reset_required' | 'conflict';
71
+ /**
72
+ * One account whose main device a reset of encrypted chat replaced (`peerChanges`): `at` is when the new main
73
+ * device was registered (the relay's clock). Look at its devices again (`devices({ user_id })`).
74
+ */
75
+ export interface E2eePeerChange {
76
+ userId: string;
77
+ at: string;
78
+ }
79
+ /**
80
+ * The realtime `e2ee:peer` event (`r.realtime.onE2eePeer`), sent to each member of an encrypted conversation the
81
+ * account `userId` shares with them, once per such conversation, when that account reset encrypted chat
82
+ * (`reason: 'root_changed'`): its security code changed there. Only to sockets whose credential can read that
83
+ * conversation over the relay. A hint: look at the account's devices again. An app in the background catches up
84
+ * with `peerChanges` when it comes to the foreground.
85
+ */
86
+ export interface E2eePeerRealtimeEvent {
87
+ userId: string;
88
+ reason: 'root_changed';
89
+ conversationId: string;
90
+ }
91
+ /**
92
+ * Codes a message that declares `trust_change` can be refused with in a direct chat whose other person reset
93
+ * encrypted chat, where `status().accountReset` is on (409 `ConflictError` for both):
94
+ * - `peer_not_joined`: no device of this account has added the other person's new device to the chat yet, so the
95
+ * message would be unreadable on it. `details: { userId }` names the person. Add the device, then send again.
96
+ * - `stale_epoch`: encrypted under a key generation before the chat's last key update. Sync, then encrypt it again.
97
+ */
98
+ export type E2eePeerErrorCode = 'peer_not_joined' | 'stale_epoch';
99
+ /** The `details` of a `peer_not_joined` refusal. */
100
+ export interface E2eePeerNotJoinedDetails {
101
+ userId: string;
102
+ }
71
103
  /** Opaque `recursiv-mls-v2-draft1` envelope with base64 MLS bytes. */
72
104
  export interface E2eeEnvelope {
73
105
  profile: 'recursiv-mls-v2-draft1';
@@ -367,6 +399,15 @@ export declare class E2eeResource {
367
399
  * delete. A commit ignores the flag. The relay learns only that the event
368
400
  * is quiet; the flag is not stored. A relay that predates it drops the
369
401
  * key, so the event is simply not quiet there.
402
+ *
403
+ * `trust_change: true` says this device's engine can accept a contact's new
404
+ * main device after they reset encrypted chat. Where `status().accountReset`
405
+ * is on, such a message in a direct chat whose other person reset is refused
406
+ * 409 `peer_not_joined` (`details: { userId }`) until a device of this
407
+ * account added their new device, and 409 `stale_epoch` when encrypted under
408
+ * a key generation before the chat's last key update (E2eePeerErrorCode). A
409
+ * message without it is accepted as before. Never stored; relays without it
410
+ * ignore it.
370
411
  */
371
412
  appendEvent(conversationId: string, input: {
372
413
  device_id: string;
@@ -375,11 +416,26 @@ export declare class E2eeResource {
375
416
  welcome_for_device_id?: string;
376
417
  expected_commit_seq?: number;
377
418
  silent?: boolean;
419
+ trust_change?: boolean;
378
420
  }): Promise<SingleResponse<{
379
421
  seq: number;
380
422
  duplicate: boolean;
381
423
  commitSeq: number;
382
424
  }>>;
425
+ /**
426
+ * The accounts sharing an encrypted conversation with this account (a direct
427
+ * chat, or a group both accepted) whose main device a reset of encrypted chat
428
+ * replaced after `since` (ISO 8601; default and floor: 7 days ago), oldest
429
+ * first, at most 200 (ask again from the last `at` for more). Read it when the
430
+ * app comes to the foreground, then look at each account's devices again; the
431
+ * live form is `r.realtime.onE2eePeer`. Empty where `status().accountReset` is
432
+ * off. Relays without it answer 404 (`NotFoundError`): treat that as no changes.
433
+ */
434
+ peerChanges(params?: {
435
+ since?: string;
436
+ }): Promise<{
437
+ data: E2eePeerChange[];
438
+ }>;
383
439
  events(conversationId: string, params: {
384
440
  device_id: string;
385
441
  after_seq?: number;
@@ -96,11 +96,32 @@ export class E2eeResource {
96
96
  * delete. A commit ignores the flag. The relay learns only that the event
97
97
  * is quiet; the flag is not stored. A relay that predates it drops the
98
98
  * key, so the event is simply not quiet there.
99
+ *
100
+ * `trust_change: true` says this device's engine can accept a contact's new
101
+ * main device after they reset encrypted chat. Where `status().accountReset`
102
+ * is on, such a message in a direct chat whose other person reset is refused
103
+ * 409 `peer_not_joined` (`details: { userId }`) until a device of this
104
+ * account added their new device, and 409 `stale_epoch` when encrypted under
105
+ * a key generation before the chat's last key update (E2eePeerErrorCode). A
106
+ * message without it is accepted as before. Never stored; relays without it
107
+ * ignore it.
99
108
  */
100
109
  appendEvent(conversationId, input) {
101
110
  // Retries are safe: the server is idempotent on (conversation, device, event id).
102
111
  return this.client.post(`/e2ee/conversations/${encodeURIComponent(conversationId)}/events`, input, { retry: true });
103
112
  }
113
+ /**
114
+ * The accounts sharing an encrypted conversation with this account (a direct
115
+ * chat, or a group both accepted) whose main device a reset of encrypted chat
116
+ * replaced after `since` (ISO 8601; default and floor: 7 days ago), oldest
117
+ * first, at most 200 (ask again from the last `at` for more). Read it when the
118
+ * app comes to the foreground, then look at each account's devices again; the
119
+ * live form is `r.realtime.onE2eePeer`. Empty where `status().accountReset` is
120
+ * off. Relays without it answer 404 (`NotFoundError`): treat that as no changes.
121
+ */
122
+ peerChanges(params) {
123
+ return this.client.get('/e2ee/peer-changes', params);
124
+ }
104
125
  events(conversationId, params) {
105
126
  return this.client.get(`/e2ee/conversations/${encodeURIComponent(conversationId)}/events`, params);
106
127
  }
@@ -17,6 +17,30 @@ export interface LinkPreviewResponse {
17
17
  */
18
18
  data: LinkPreview | null;
19
19
  cached?: boolean;
20
+ /**
21
+ * `'ai_link'`: the link is not in the named message in a person's own words,
22
+ * and the caller did not write the same address in that conversation, so the
23
+ * server did not fetch it (it may be an AI's). Show the plain link, and do not
24
+ * ask again for this message. Only on a request that named its message or
25
+ * conversation (`source`).
26
+ */
27
+ refused?: 'ai_link';
28
+ }
29
+ /**
30
+ * Where a chat link came from. Name the message when you have its id; the
31
+ * conversation alone when you do not have it yet (an AI reply still being
32
+ * saved). With the conversation alone, a link is previewed only when the caller
33
+ * wrote it there, so another member's message not yet saved waits for its id.
34
+ * At least one of the two must be a non-empty id: `get` rejects otherwise and
35
+ * never falls back to a request without a source.
36
+ *
37
+ * Not for an encrypted conversation: the server has no readable messages there,
38
+ * so every link would be refused, the member's own included. There the app
39
+ * decides by its own rule (no preview of a link the member did not write).
40
+ */
41
+ export interface LinkPreviewSource {
42
+ messageId?: string;
43
+ conversationId?: string;
20
44
  }
21
45
  export declare class LinkPreviewResource {
22
46
  private client;
@@ -27,6 +51,23 @@ export declare class LinkPreviewResource {
27
51
  * preview (`data: null`). No auth required — the endpoint is intentionally
28
52
  * public so unauthenticated UIs can render link previews. A URL that is not
29
53
  * http(s), or points at a private or internal address, still rejects (422).
54
+ *
55
+ * For a link in a chat message, pass `source`: the server then previews the
56
+ * link only when it is in the named message and a person wrote that message,
57
+ * or when the caller wrote the same address in that conversation (in a
58
+ * message, or as the card link of a Minds post they shared there). Otherwise
59
+ * it answers `{ data: null, refused: 'ai_link' }` without fetching it. A
60
+ * request with `source` is the caller's own: make it on a signed-in client
61
+ * whose key has `chat:read` (a keyless one gets 401). A message or
62
+ * conversation the caller cannot read rejects with 404. A `source` with no
63
+ * id in it (an AI reply whose id is not known yet, say) rejects with a
64
+ * ValidationError and sends nothing. Do not pass `source` in an encrypted
65
+ * conversation (see LinkPreviewSource). Servers before this change ignore
66
+ * `source` and preview the link, so an app keeps its own rule too: no
67
+ * preview of an AI-written link the member did not write.
68
+ *
69
+ * @example
70
+ * const { data, refused } = await r.linkPreview.get(url, { messageId: message.id });
30
71
  */
31
- get(url: string): Promise<LinkPreviewResponse>;
72
+ get(url: string, source?: LinkPreviewSource | null): Promise<LinkPreviewResponse>;
32
73
  }
@@ -1,3 +1,4 @@
1
+ import { ValidationError } from '../errors.js';
1
2
  export class LinkPreviewResource {
2
3
  client;
3
4
  constructor(client) {
@@ -9,8 +10,41 @@ export class LinkPreviewResource {
9
10
  * preview (`data: null`). No auth required — the endpoint is intentionally
10
11
  * public so unauthenticated UIs can render link previews. A URL that is not
11
12
  * http(s), or points at a private or internal address, still rejects (422).
13
+ *
14
+ * For a link in a chat message, pass `source`: the server then previews the
15
+ * link only when it is in the named message and a person wrote that message,
16
+ * or when the caller wrote the same address in that conversation (in a
17
+ * message, or as the card link of a Minds post they shared there). Otherwise
18
+ * it answers `{ data: null, refused: 'ai_link' }` without fetching it. A
19
+ * request with `source` is the caller's own: make it on a signed-in client
20
+ * whose key has `chat:read` (a keyless one gets 401). A message or
21
+ * conversation the caller cannot read rejects with 404. A `source` with no
22
+ * id in it (an AI reply whose id is not known yet, say) rejects with a
23
+ * ValidationError and sends nothing. Do not pass `source` in an encrypted
24
+ * conversation (see LinkPreviewSource). Servers before this change ignore
25
+ * `source` and preview the link, so an app keeps its own rule too: no
26
+ * preview of an AI-written link the member did not write.
27
+ *
28
+ * @example
29
+ * const { data, refused } = await r.linkPreview.get(url, { messageId: message.id });
12
30
  */
13
- get(url) {
14
- return this.client.get('/link-preview', { url });
31
+ async get(url, source) {
32
+ const params = { url };
33
+ if (source != null) {
34
+ // A source with no id must not turn into a bare request: the server would
35
+ // then fetch the link with no check at all.
36
+ if (!source.messageId && !source.conversationId) {
37
+ throw new ValidationError({
38
+ type: 'validation_error',
39
+ code: 'link_preview_source_missing',
40
+ message: 'linkPreview.get: source needs a messageId or a conversationId. Wait for the id, or show the plain link.',
41
+ });
42
+ }
43
+ if (source.messageId)
44
+ params.message_id = source.messageId;
45
+ if (source.conversationId)
46
+ params.conversation_id = source.conversationId;
47
+ }
48
+ return this.client.get('/link-preview', params);
15
49
  }
16
50
  }
@@ -1,5 +1,6 @@
1
1
  import type { HttpClient } from '../client.js';
2
2
  import type { SingleResponse } from '../types.js';
3
+ import type { AssistantSettings } from '../assistant-surfaces.js';
3
4
  interface NetworkConfig {
4
5
  name: string;
5
6
  slug: string | null;
@@ -11,14 +12,25 @@ interface NetworkConfig {
11
12
  * `bookmark_collections` (0.7.68; absent on older servers): this server serves bookmark collections
12
13
  * (`bookmarks.collections`). Show collections only when it is true, and still treat a 503
13
14
  * `lists_unavailable` as off.
15
+ *
16
+ * `child_safety_report_category` (absent on older servers): this server files the child-safety report category's
17
+ * sub-types (`child_safety:minor_in_danger`, `:sexualised_minor`, `:grooming_enticement`, `:trafficking`) as
18
+ * urgent: top severity, an immediate staff alert, decided by a person. Offer them only when it is true; otherwise
19
+ * report child sexual abuse material as `csam`, which every server files as urgent.
14
20
  */
15
21
  capabilities: {
16
22
  ws_token: boolean;
17
23
  bookmark_collections?: boolean;
24
+ child_safety_report_category?: boolean;
18
25
  };
19
26
  auth_methods: string;
20
27
  default_template_name: string | null;
21
28
  locale: string;
29
+ /**
30
+ * The assistant surface switches for the app this key is bound to (absent on servers from before George plan G7a).
31
+ * Read them with `assistantSurfaceOn(config, id)`, which is off for anything but an explicit true.
32
+ */
33
+ assistant?: AssistantSettings;
22
34
  }
23
35
  export declare class NetworkResource {
24
36
  private client;
@@ -1,5 +1,6 @@
1
1
  import type { HttpClient } from '../client.js';
2
2
  import type { SingleResponse } from '../types.js';
3
+ import type { AssistantSettings, AssistantSettingsUpdate } from '../assistant-surfaces.js';
3
4
  export interface BrainTask {
4
5
  id: string;
5
6
  title: string;
@@ -153,6 +154,8 @@ export interface ProjectBrainSettings {
153
154
  maxStepsPerRun?: number;
154
155
  heartbeatTimeoutMinutes?: number;
155
156
  dailySpendingLimitUsd?: number;
157
+ /** The assistant surface switches: every surface id, off unless stored as true (absent on older servers). */
158
+ assistant?: AssistantSettings;
156
159
  [key: string]: unknown;
157
160
  }
158
161
  export interface UpdateProjectSettingsInput {
@@ -161,6 +164,11 @@ export interface UpdateProjectSettingsInput {
161
164
  max_steps_per_run?: number;
162
165
  heartbeat_timeout_minutes?: number;
163
166
  daily_spending_limit_usd?: number;
167
+ /**
168
+ * Turn assistant surfaces on or off for this app (George plan G7a): the ids given are set, the rest kept. The
169
+ * project's owner or an admin only (403 `project_admin_required` otherwise); an unknown id is a 400.
170
+ */
171
+ assistant?: AssistantSettingsUpdate;
164
172
  }
165
173
  export interface BrowserUseRunTaskInput {
166
174
  task: string;
@@ -57,4 +57,28 @@ export declare class ReportsResource {
57
57
  dismiss(reportId: string): Promise<SingleResponse<{
58
58
  success: true;
59
59
  }>>;
60
+ /**
61
+ * Short-lived URLs for the private chat photos and voice notes a reported message carries (0.7.82), for the staff
62
+ * person reviewing the report. A live admin's own key only: an AI account or an agent key gets 403
63
+ * `human_required`. Only the reported message's own attachments are signed; each URL works for about five minutes
64
+ * and is served `private, no-store`, so ask again rather than keeping it. A report of anything but a message is
65
+ * 400 `not_a_message_report`. Only a recipient's report opens anything: a report you filed yourself is 403
66
+ * `own_report`, and one not filed by a person in that chat (other than the message's sender) is 403
67
+ * `reporter_not_a_recipient`.
68
+ */
69
+ signAttachments(reportId: string): Promise<SingleResponse<ReportedMessageAttachments>>;
70
+ }
71
+ export interface ReportedMessageAttachment {
72
+ key: string;
73
+ kind: 'image' | 'audio';
74
+ content_type: string;
75
+ /** A GET URL valid until `expires_at`. */
76
+ url: string;
77
+ expires_at: string;
78
+ }
79
+ export interface ReportedMessageAttachments {
80
+ report_id: string;
81
+ message_id: string;
82
+ conversation_id: string;
83
+ attachments: ReportedMessageAttachment[];
60
84
  }
@@ -27,4 +27,16 @@ export class ReportsResource {
27
27
  dismiss(reportId) {
28
28
  return this.client.post(`/reports/${encodeURIComponent(reportId)}/dismiss`);
29
29
  }
30
+ /**
31
+ * Short-lived URLs for the private chat photos and voice notes a reported message carries (0.7.82), for the staff
32
+ * person reviewing the report. A live admin's own key only: an AI account or an agent key gets 403
33
+ * `human_required`. Only the reported message's own attachments are signed; each URL works for about five minutes
34
+ * and is served `private, no-store`, so ask again rather than keeping it. A report of anything but a message is
35
+ * 400 `not_a_message_report`. Only a recipient's report opens anything: a report you filed yourself is 403
36
+ * `own_report`, and one not filed by a person in that chat (other than the message's sender) is 403
37
+ * `reporter_not_a_recipient`.
38
+ */
39
+ signAttachments(reportId) {
40
+ return this.client.post(`/reports/${encodeURIComponent(reportId)}/attachments/sign`);
41
+ }
30
42
  }
@@ -1,10 +1,19 @@
1
1
  import type { HttpClient } from '../client.js';
2
2
  import type { PaginatedResponse, SingleResponse, User, FollowUser, PaginationParams } from '../types.js';
3
+ import type { AssistantSettings } from '../assistant-surfaces.js';
4
+ /** The signed-in user as `users.me()` returns it. */
5
+ export interface CurrentUser extends User {
6
+ /**
7
+ * The assistant surface switches for the app this key is bound to (absent on servers from before George plan G7a).
8
+ * Read them with `assistantSurfaceOn(me, id)`, which is off for anything but an explicit true.
9
+ */
10
+ assistant?: AssistantSettings;
11
+ }
3
12
  export declare class UsersResource {
4
13
  private client;
5
14
  constructor(client: HttpClient);
6
- /** Get the authenticated user's profile */
7
- me(): Promise<SingleResponse<User>>;
15
+ /** Get the authenticated user's profile, with the app's assistant surface switches */
16
+ me(): Promise<SingleResponse<CurrentUser>>;
8
17
  /** Get a user by ID */
9
18
  get(id: string): Promise<SingleResponse<User>>;
10
19
  /** List a user's followers */
@@ -3,7 +3,7 @@ export class UsersResource {
3
3
  constructor(client) {
4
4
  this.client = client;
5
5
  }
6
- /** Get the authenticated user's profile */
6
+ /** Get the authenticated user's profile, with the app's assistant surface switches */
7
7
  me() {
8
8
  return this.client.get('/users/me');
9
9
  }
package/dist/types.d.ts CHANGED
@@ -1316,7 +1316,35 @@ export interface AgentChatInput {
1316
1316
  message: string;
1317
1317
  conversation_id?: string;
1318
1318
  new_conversation?: boolean;
1319
+ /** Per-turn options. `{ safe: true }` makes this a safe turn (see {@link AgentChatTurn}). */
1320
+ turn?: AgentChatTurn;
1319
1321
  }
1322
+ /**
1323
+ * Per-turn options for an agent chat message.
1324
+ *
1325
+ * `safe: true` (George plan G6a) asks about something on the network, typically someone else's post, on the caller's
1326
+ * own personal agent, in their one Asks thread with it (found or made by the server; leave out `new_conversation`, and
1327
+ * send `conversation_id` only for that thread). The turn has no tools whatever the agent's tool mode, reads and saves
1328
+ * no memory, sends OpenRouter `data_collection: 'deny'` whatever the member chose, writes no audit copy of the prompt
1329
+ * or reply (`ai_usage_detail`), sends pictures as alt text and stored descriptions only, and counts against a small
1330
+ * daily allowance. Every later turn in the Asks thread is a safe turn too, however it is sent. The reply
1331
+ * carries `metadata.safe_turn: true`. Refusals answer with a fixed {@link SafeTurnRefusalCode}; none names a plan or
1332
+ * a price, and the server never creates an agent for an ask.
1333
+ */
1334
+ export interface AgentChatTurn {
1335
+ safe?: boolean;
1336
+ }
1337
+ /**
1338
+ * Why the server refused a safe turn (the `code` of the error):
1339
+ * - `safe_turn_limit` (429): today's asks are used up (an app shows its own "limit" words). Marked
1340
+ * `X-RateLimit-Scope: action`, so the SDK throws it at once as a `RateLimitError` with `scope: 'action'` and
1341
+ * `resetAt` (the next midnight UTC), and does not resend it.
1342
+ * - `safe_turn_needs_personal_agent` (409): the agent is not the caller's own personal agent.
1343
+ * - `safe_turn_needs_app` (403): the key is bound to no app (a personal key, say); an ask goes with the app's own key.
1344
+ * - `safe_turn_ai_off` / `safe_turn_standard_off` (403): the member's AI choices refuse it.
1345
+ * - `safe_turn_conversation` (400): `new_conversation` was sent, or `conversation_id` is not the Asks thread.
1346
+ */
1347
+ export type SafeTurnRefusalCode = 'safe_turn_limit' | 'safe_turn_needs_personal_agent' | 'safe_turn_needs_app' | 'safe_turn_ai_off' | 'safe_turn_standard_off' | 'safe_turn_conversation';
1320
1348
  /** Answer an existing message of yours again, without sending it a second time. */
1321
1349
  export interface AgentChatRetryInput {
1322
1350
  conversation_id: string;
@@ -1597,6 +1625,12 @@ export interface ContentPreferences {
1597
1625
  autoplay_video: boolean;
1598
1626
  ai_enabled: boolean;
1599
1627
  default_feed: 'foryou' | 'following';
1628
+ /**
1629
+ * True when the member picked `default_feed` themselves (any `updateContent({ default_feed })` sets it).
1630
+ * When false, `default_feed` is only the default and the app may choose for them. Absent from APIs older than
1631
+ * this field: treat absent as unknown, not as false.
1632
+ */
1633
+ default_feed_chosen?: boolean;
1600
1634
  }
1601
1635
  export interface UpdateNotificationChannelsInput {
1602
1636
  /** Master switch: sets in-app on every kind. */