@recursiv/sdk 0.7.57 → 0.7.59
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 +16 -4
- package/dist/errors.d.ts +13 -0
- package/dist/errors.js +12 -0
- package/dist/index.d.ts +7 -6
- package/dist/index.js +2 -2
- package/dist/resources/agents.d.ts +70 -0
- package/dist/resources/agents.js +10 -0
- package/dist/resources/curator.d.ts +5 -1
- package/dist/resources/curator.js +6 -2
- package/dist/resources/discovery.d.ts +83 -1
- package/dist/resources/discovery.js +36 -6
- package/dist/resources/e2ee.d.ts +115 -2
- package/dist/resources/e2ee.js +41 -3
- package/dist/resources/email.d.ts +27 -3
- package/dist/resources/email.js +18 -3
- package/dist/resources/env-vars.d.ts +25 -0
- package/dist/resources/env-vars.js +8 -0
- package/dist/resources/moderation.d.ts +22 -0
- package/dist/resources/moderation.js +16 -0
- package/dist/resources/posts.d.ts +1 -1
- package/dist/resources/posts.js +6 -2
- package/dist/resources/protocols.d.ts +237 -5
- package/dist/resources/protocols.js +57 -1
- package/dist/resources/search.d.ts +9 -1
- package/dist/types.d.ts +89 -0
- package/llm.md +17 -4
- package/package.json +1 -1
package/llm.md
CHANGED
|
@@ -70,7 +70,9 @@ const { data } = await r.envVars.list({ project_id: 'proj_xxx' })
|
|
|
70
70
|
// [{ key: 'STRIPE_SECRET_KEY', value: '••••••••', is_secret: true, ... }]
|
|
71
71
|
```
|
|
72
72
|
|
|
73
|
-
Methods: `list({ project_id, environment? })`, `set({ project_id, key, value, is_secret?, environment? })`, `delete({ project_id, key, environment? })`
|
|
73
|
+
Methods: `list({ project_id, environment? })`, `set({ project_id, key, value, is_secret?, environment? })`, `createProjectApiKey({ project_id, key, scopes?, environment? })`, `delete({ project_id, key, environment? })`
|
|
74
|
+
|
|
75
|
+
`createProjectApiKey` mints a project-bound Recursiv key straight into the env var (e.g. `RECURSIV_API_KEY` for a server-side form) and returns only `api_key_id` and `api_key_prefix`, never the key. Scopes default to `databases:read` + `databases:write`, can't exceed the calling key's, and can't include `admin`. Calling it again rotates: the previous key in that variable is revoked.
|
|
74
76
|
|
|
75
77
|
Keys are UPPER_SNAKE_CASE. `environment` is `'preview' | 'production' | 'both'`, default `'both'`.
|
|
76
78
|
|
|
@@ -534,6 +536,16 @@ const { data: ids, next } = await r.e2ee.conversations({ limit: 200 });
|
|
|
534
536
|
r.realtime.onE2eeDevices(({ deviceId, reason }) => {}); // 'packages': it can be claimed now; 'revoked'
|
|
535
537
|
```
|
|
536
538
|
|
|
539
|
+
Revoking a device other active devices were approved through, or the account's root (main) device, throws a 409 `ConflictError` with code `has_dependents` and `details: { dependents: string[], root: boolean }`; nothing changes (0.7.58). `revokeDevice(id, { cascade: true })` revokes it and every device chained through it, answering `revoked` (ids). Once the root is revoked the account can link a new device only after it resets encrypted chat.
|
|
540
|
+
|
|
541
|
+
Reset encrypted chat (D2; only where `status()` reports `accountReset: true`): for someone who lost every device, or who starts over on this one. The code is eight digits, emailed to the account's address; the relay warns the owner. One call then revokes every device, signs the account out of every other session, API key and MCP-connected AI client on the network (closing their sockets), and registers this device as the new main device; contacts see that the security code changed. With `accountReset` on, `revokeDevice` on the main device answers 409 `reset_required`, and so does `createLink` when the main device is gone with nothing left to approve. Errors: 403 `confirmation_failed` (wrong, expired or spent code; nothing changed), 409 `code_limit` (`details.retryAfterSeconds`, never 429), 400 `no_email_on_file`, 503 `email_delivery_unavailable`, 409 `conflict` (name or key used before), 403 `forbidden` (an account that is not this network's own, such as a platform owner's; `status()` never reports `accountReset` to such an account, and its main device and links answer as with the feature off).
|
|
542
|
+
|
|
543
|
+
```ts
|
|
544
|
+
const { data: sent } = await r.e2ee.requestResetCode(); // { expires_at, sent_to: 'b***@minds.com' }
|
|
545
|
+
const { data: reset } = await r.e2ee.resetAccount({ email_code, device_name, realm, signing_key, credential, keep_session_token });
|
|
546
|
+
// → { reset, resetAt, retiredRoot, revokedDeviceIds, device (the new main device), signedOut: { sessions, keys, connectedApps } }; a retry after a lost answer gets the same result
|
|
547
|
+
```
|
|
548
|
+
|
|
537
549
|
### r.realtime — WebSocket Chat Client
|
|
538
550
|
|
|
539
551
|
Provides persistent real-time streaming and chat presence, useful for building frontends (e.g. React Native, Web).
|
|
@@ -705,7 +717,7 @@ Methods: `list`, `get`, `create`, `delete`
|
|
|
705
717
|
- `r.github` — GitHub integration (getRepository, getTree, getFileContent, getCommits, createFromTemplate)
|
|
706
718
|
- `r.deployments` — Deployment management (prefer `r.projects.deploy()` for new projects)
|
|
707
719
|
- `r.integrations` — External service integrations and agent tool connections. Connections: `listConnections(orgId)`, `connect()`, `confirmConnection()`. Deterministic tool execution (no agent/LLM in the loop): `executeTool({ connection_id, tool, arguments })` — run a tool yourself with explicit args + pagination and get the raw result back (e.g. `{ tool: 'GMAIL_FETCH_EMAILS', arguments: { query, max_results } }`); use for reliable batch coverage, vs `agents.chat` for autonomous behavior. Tool approvals: `listPendingExecutions(conversationId)`, `approveExecution(id)`, `rejectExecution(id)` — handles both integration tools and brain/platform tools (provision_sandbox etc.); use these to unblock a permission-mode agent waiting on tool approval
|
|
708
|
-
- `r.email` — Email campaigns, batch sending, and transactional email
|
|
720
|
+
- `r.email` — Email campaigns, batch sending, and transactional email. **Pass `category: 'transactional'`** for receipts, invites, confirmations and alerts: `r.email.send({ to, subject, html, category: 'transactional' })`. Without it the send is treated as marketing and held for anyone who has not given marketing consent. A send where every recipient was held throws `EmailNotSentError` (its message carries the reason). A `status: 'partial'` result lists each recipient's `status` and `reason` in `messages`.
|
|
709
721
|
- `r.webhooks` — Register inbound webhook endpoints: `register()`, `list()`, `delete()`. Webhook events execute handler code in project sandbox.
|
|
710
722
|
- `r.jobs` — Scheduled cron jobs: `create({ name, cron, handler_code })`, `list()`, `update()`, `delete()`. Handlers execute in project sandbox on schedule.
|
|
711
723
|
- `r.wallet` — Wallet, MINDS tokens (`getTokenBalance`, `getTokenLedger`; wei stays a string), USD payouts (`getPayouts`, `startPayoutOnboarding`, `payoutLoginLink`)
|
|
@@ -730,7 +742,8 @@ await r.appSubscriptions.changeTier({ tier: 'pro', proration_date: quote.prorati
|
|
|
730
742
|
- `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`.
|
|
731
743
|
- 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.
|
|
732
744
|
- 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.
|
|
733
|
-
- 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? }`.
|
|
745
|
+
- 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? }`. 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`.
|
|
746
|
+
- 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.
|
|
734
747
|
- 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.
|
|
735
748
|
- `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.
|
|
736
749
|
- 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`.
|
|
@@ -828,7 +841,7 @@ Pagination: pass `limit` and `offset`. Check `meta.has_more`.
|
|
|
828
841
|
| `RateLimitError` | 429 | Rate limit hit (`retryAfter` seconds, `resetAt` Date, `scope`) |
|
|
829
842
|
| `ConflictError` | 409 | Resource already exists |
|
|
830
843
|
|
|
831
|
-
Auto-retry on 429/5xx built in (exponential backoff, default `maxRetries: 2`), except a 429 with `X-RateLimit-Scope: action` (one member action past its budget): never retried, `RateLimitError.scope === 'action'`, `resetAt` = when it reopens; show it on that action, do not pause the app. Member actions with a per-member budget are sent once and never auto-retried: `reports.create`, `moderation.appeal` and `protocols.reportSource` throw the 429 at once (`RateLimitError`, `message` is member-facing, `retryAfter` = seconds until the window opens; a budget refusal carries `X-RateLimit-Scope: action` and no `Retry-After` header).
|
|
844
|
+
Auto-retry on 429/5xx built in (exponential backoff, default `maxRetries: 2`), except a 429 with `X-RateLimit-Scope: action` (one member action past its budget): never retried, `RateLimitError.scope === 'action'`, `resetAt` = when it reopens; show it on that action, do not pause the app. A read that holds its own deadline can ask once: `r.posts.list({ ...params, retry: false })` and `r.curator.forYou({ ...params, retry: false })` (0.7.59) throw the first 5xx or 429 with no retry and no `Retry-After` sleep; `retry` is never sent. It rides in the params so a reader that wraps `posts.list` and forwards its params keeps it. Discover's feed sessions (`discovery.createFeed`, `createPersonalFeed`, a `DiscoveryFeedSession` over your own `posts.list` wrapper) read Minds posts this way, as they read the open networks, and report a busy read in `failedSources`; a wrapper that rebuilds the params from chosen fields must copy `retry` too. Member actions with a per-member budget are sent once and never auto-retried: `reports.create`, `moderation.appeal` and `protocols.reportSource` throw the 429 at once (`RateLimitError`, `message` is member-facing, `retryAfter` = seconds until the window opens; a budget refusal carries `X-RateLimit-Scope: action` and no `Retry-After` header).
|
|
832
845
|
|
|
833
846
|
### Idempotency
|
|
834
847
|
|