@recursiv/sdk 0.7.62 → 0.7.63

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -99,7 +99,7 @@ for await (const chunk of r.agents.chatStream(agent.id, { message: 'Hello!' }))
99
99
  | `r.chat` | `send`, `dm`, `createGroup` (`encrypted: true` for an end-to-end encrypted group, 0.7.59), `messages` (cursor paging), `acceptRequest`, `declineRequest`, `leave`, `deleteConversation`; a private group's admin: `addMembers`, `removeMember`, `updateGroup` (0.7.59) |
100
100
  | `r.tags` | `create`, `list` |
101
101
  | `r.uploads` | `uploadMedia` — upload post/chat bytes end to end |
102
- | `r.video` | `entitlement` (may the caller publish video, and how much is left), `createUpload`, `getStatus`, `getPlaybackToken` |
102
+ | `r.video` | `entitlement` (may the caller publish video, and how much is left), `createUpload`, `renewUpload` (the creator's fresh TUS credentials for the same video, with `createUpload`'s `resumeGrant`, so a resume past the hour continues it; 0.7.63), `getStatus`, `getPlaybackToken` |
103
103
 
104
104
  ### Platform
105
105
  | Resource | Purpose |
package/dist/index.d.ts CHANGED
@@ -235,7 +235,7 @@ export type { Brief, BriefEmail, MyBrief, MyBriefEntry, BriefParams, BriefShippe
235
235
  export type { HumanAsk, HumanAskStatus, CreateHumanAskInput, ListHumanAsksParams } from './resources/human-asks.js';
236
236
  export { HumanInputResource } from './resources/human-input.js';
237
237
  export type { Goal, GoalTask, CreateGoalInput, UpdateGoalInput, ListGoalsParams, ReorderGoalTasksInput } from './resources/goals.js';
238
- export type { DispatcherTask, TaskClaim, ClaimRecord, DispatcherStats, CreateTaskInput, UpdateTaskInput, ListTasksParams, SignalRecord, OutcomeRecord, PauseState, PauseInput, SignalsParams, OutcomesParams, DiscoveryParams, StaleStuckParams } from './resources/dispatcher.js';
238
+ export type { DispatcherTask, TaskClaim, ClaimRecord, DispatcherStats, CreateTaskInput, UpdateTaskInput, ListTasksParams, SignalRecord, OutcomeRecord, PauseState, PauseInput, SignalsParams, OutcomesParams, DiscoveryParams, StaleStuckParams, TaskPlan, TaskPlanStep, SetTaskPlanInput, UpdateTaskPlanStepInput } from './resources/dispatcher.js';
239
239
  export { SwarmsResource } from './resources/swarms.js';
240
240
  export type { Swarm, SwarmMember, CreateSwarmInput, ListSwarmsParams, ScheduledTask, ScheduleTaskInput } from './resources/swarms.js';
241
241
  export { TemplatesResource } from './resources/templates.js';
@@ -1,5 +1,54 @@
1
1
  import type { HttpClient } from '../client.js';
2
2
  import type { SingleResponse, DeleteResponse, PaginationParams } from '../types.js';
3
+ /** One step of a task's plan, as the checklist card shows it. */
4
+ export interface TaskPlanStep {
5
+ position: number;
6
+ title: string;
7
+ status: 'pending' | 'in_progress' | 'done' | 'skipped' | 'blocked';
8
+ /** Why a step is blocked or was skipped. */
9
+ reason: string | null;
10
+ /** The specialist agent doing the step. */
11
+ agent: {
12
+ id: string;
13
+ name: string;
14
+ } | null;
15
+ /** The approval or question a blocked step waits on, in the plan's conversation. */
16
+ waitingOn: {
17
+ executionId: string | null;
18
+ messageId: string | null;
19
+ } | null;
20
+ startedAt: string | null;
21
+ finishedAt: string | null;
22
+ }
23
+ export interface TaskPlan {
24
+ taskId: string;
25
+ title: string;
26
+ /** The chat the plan's card lives in. */
27
+ conversationId: string | null;
28
+ steps: TaskPlanStep[];
29
+ }
30
+ export interface SetTaskPlanInput {
31
+ agent?: string;
32
+ /** The chat to post the card in. Only the first plan that names one posts it. */
33
+ conversation_id?: string | null;
34
+ steps: Array<{
35
+ title: string;
36
+ agent_id?: string | null;
37
+ }>;
38
+ organization_id?: string;
39
+ }
40
+ export interface UpdateTaskPlanStepInput {
41
+ agent?: string;
42
+ status: TaskPlanStep['status'];
43
+ title?: string;
44
+ /** Kept only for 'blocked' and 'skipped'. */
45
+ reason?: string | null;
46
+ /** Only for 'blocked': the approval it waits on. */
47
+ waiting_on_execution_id?: string | null;
48
+ /** Only for 'blocked': the question message it waits on. */
49
+ waiting_on_message_id?: string | null;
50
+ organization_id?: string;
51
+ }
3
52
  export interface DispatcherTask {
4
53
  id: string;
5
54
  title: string;
@@ -275,6 +324,13 @@ export declare class DispatcherResource {
275
324
  }): Promise<SingleResponse<{
276
325
  ok: true;
277
326
  }>>;
327
+ /**
328
+ * Write a claimed task's step plan (the checklist card). Replaces every step
329
+ * that has not finished; done and skipped steps stay. Requires the claim.
330
+ */
331
+ setPlan(taskId: string, input: SetTaskPlanInput): Promise<SingleResponse<TaskPlan>>;
332
+ /** Move one step of a claimed task's plan. Started and finished times are stamped server-side. */
333
+ updatePlanStep(taskId: string, position: number, input: UpdateTaskPlanStepInput): Promise<SingleResponse<TaskPlan>>;
278
334
  /**
279
335
  * Mark a task as done. A task with a thresholded check that has no passing
280
336
  * run refuses with 409 and the failing check names — run
@@ -28,6 +28,19 @@ export class DispatcherResource {
28
28
  const { organization_id, ...body } = input;
29
29
  return this.client.post(`/dispatcher/heartbeat/${encodeURIComponent(taskId)}${orgQuery(organization_id)}`, body);
30
30
  }
31
+ /**
32
+ * Write a claimed task's step plan (the checklist card). Replaces every step
33
+ * that has not finished; done and skipped steps stay. Requires the claim.
34
+ */
35
+ setPlan(taskId, input) {
36
+ const { organization_id, ...body } = input;
37
+ return this.client.put(`/dispatcher/tasks/${encodeURIComponent(taskId)}/plan${orgQuery(organization_id)}`, body);
38
+ }
39
+ /** Move one step of a claimed task's plan. Started and finished times are stamped server-side. */
40
+ updatePlanStep(taskId, position, input) {
41
+ const { organization_id, ...body } = input;
42
+ return this.client.patch(`/dispatcher/tasks/${encodeURIComponent(taskId)}/plan/steps/${position}${orgQuery(organization_id)}`, body);
43
+ }
31
44
  /**
32
45
  * Mark a task as done. A task with a thresholded check that has no passing
33
46
  * run refuses with 409 and the failing check names — run
@@ -95,9 +95,13 @@ export interface E2eeEvent {
95
95
  * quiet one; never a commit, never a Welcome, never one whose envelope is over
96
96
  * 16 KB), and only on a socket whose credential could read it with `events()`:
97
97
  * one authenticated by the account owner's session or by an owner key whose
98
- * app or organization binding reaches the conversation. A socket opened with
99
- * an agent-scoped key gets the content-free form only. When present it is the
100
- * stored event exactly as `events()` returns it, JSON byte for byte.
98
+ * app or organization binding reaches the conversation, once that sign-in has
99
+ * called `registerDevice`, `publishKeyPackages`, `keyPackageStatus`,
100
+ * `events`, `acknowledge` or `appendEvent` for one of the account's active
101
+ * devices since the socket connected, as a sync after connecting does. A
102
+ * socket opened with an agent-scoped key, or whose sign-in spoke only for a
103
+ * device since revoked, gets the content-free form only. When present it is
104
+ * the stored event exactly as `events()` returns it, JSON byte for byte.
101
105
  *
102
106
  * With the shared engine, hand `event` to `applyPushed(conversationId, event)`
103
107
  * on the device session: it applies the event without a relay read when it is
@@ -11,6 +11,10 @@ export interface RequestHumanInputInput {
11
11
  steps: string;
12
12
  /** One-tap answers (2-4 short words) — rendered as buttons on the phone. */
13
13
  options?: string[];
14
+ /** The option you recommend; must be one of `options`. Marked "Recommended" on the card. */
15
+ recommended?: string;
16
+ /** You keep working on `recommended` unless the human answers otherwise. Needs `recommended`. */
17
+ continuing_with_default?: boolean;
14
18
  }
15
19
  export interface HumanInputRequest {
16
20
  conversation_id: string;
@@ -982,6 +982,13 @@ export interface SocialPost {
982
982
  author: Omit<SocialProfile, 'subscription'>;
983
983
  text: string;
984
984
  createdAt: string;
985
+ /**
986
+ * The language the post declares, as the lower-case primary subtag of its BCP-47 tag ("de", "en", "pt"): Bluesky's
987
+ * record `langs[0]`, a Mastodon status's `language`, a Nostr note's NIP-32 `ISO-639-1` self-label. Absent when the post
988
+ * declares none (most Nostr notes) or a malformed one, and on posts served from the open-network source index; the
989
+ * server never guesses it from the text and never drops a post for it. Use it to order the viewer's language first.
990
+ */
991
+ language?: string;
985
992
  replyCount: number | null;
986
993
  likeCount: number | null;
987
994
  repostCount: number | null;
@@ -13,6 +13,12 @@ export interface CreateVideoUploadResult {
13
13
  upload: VideoUploadCreds;
14
14
  /** The HLS URL the video will play from once encoding completes. */
15
15
  hlsUrl: string;
16
+ /**
17
+ * The creator's proof for `renewUpload` (valid 24 h): pass it back to get
18
+ * fresh credentials for this same video when a resume outlives the hour
19
+ * `upload` is signed for. Absent from servers older than 0.7.63's API.
20
+ */
21
+ resumeGrant?: string;
16
22
  }
17
23
  export interface VideoStatusInfo {
18
24
  status: string | null;
@@ -59,6 +65,20 @@ export declare class VideoResource {
59
65
  }): Promise<{
60
66
  data: CreateVideoUploadResult;
61
67
  }>;
68
+ /**
69
+ * Fresh upload credentials for a video this member started, so a TUS resume
70
+ * that outlives the hour `upload` is signed for continues the same video.
71
+ * Pass the `resumeGrant` from `createUpload`. 404 for anyone else's video, an
72
+ * expired grant or an older API without the route; 409 `upload_finished` once
73
+ * a post carries the video or Bunny holds the whole file; 402 as `createUpload`;
74
+ * 503 when the video provider could not answer: keep the partial upload and
75
+ * retry (it never means the video is gone).
76
+ */
77
+ renewUpload(playbackId: string, input: {
78
+ grant: string;
79
+ }): Promise<{
80
+ data: CreateVideoUploadResult;
81
+ }>;
62
82
  /** Encode status + metadata for a video. */
63
83
  getStatus(playbackId: string): Promise<{
64
84
  data: VideoStatusInfo;
@@ -19,6 +19,18 @@ export class VideoResource {
19
19
  createUpload(input) {
20
20
  return this.client.post('/video/create-upload', input ?? {});
21
21
  }
22
+ /**
23
+ * Fresh upload credentials for a video this member started, so a TUS resume
24
+ * that outlives the hour `upload` is signed for continues the same video.
25
+ * Pass the `resumeGrant` from `createUpload`. 404 for anyone else's video, an
26
+ * expired grant or an older API without the route; 409 `upload_finished` once
27
+ * a post carries the video or Bunny holds the whole file; 402 as `createUpload`;
28
+ * 503 when the video provider could not answer: keep the partial upload and
29
+ * retry (it never means the video is gone).
30
+ */
31
+ renewUpload(playbackId, input) {
32
+ return this.client.post(`/video/${encodeURIComponent(playbackId)}/renew-upload`, input);
33
+ }
22
34
  /** Encode status + metadata for a video. */
23
35
  getStatus(playbackId) {
24
36
  return this.client.get(`/video/${playbackId}/status`);
package/dist/types.d.ts CHANGED
@@ -649,9 +649,15 @@ export interface ConversationMember {
649
649
  */
650
650
  role?: ChatMemberRole;
651
651
  /**
652
- * In a private group, every member's request: `pending` until an invitee
653
- * accepts. Declining stays private, so to anyone else a member who declined
654
- * still reads `pending`. Omitted outside private groups and by older APIs.
652
+ * Each member's message request, in a private group or a one-on-one chat.
653
+ * Someone who follows the person who wrote (or who made the group), and an
654
+ * agent, start `accepted`, as the person who wrote does; a chat started
655
+ * without a request reads `accepted` for both. Anyone else starts `pending`
656
+ * and reads `accepted` once they accept or reply, or, in a one-on-one chat,
657
+ * start a chat back. So `accepted` alone does not mean they accepted
658
+ * anything. Declining stays private: to anyone else a member who declined
659
+ * still reads `pending`. Omitted in community rooms and by older APIs
660
+ * (one-on-one chats: before this field was served for them).
655
661
  */
656
662
  request_status?: ChatRequestStatus;
657
663
  }
package/llm.md CHANGED
@@ -502,7 +502,7 @@ Methods: `conversations`, `communityConversation`, `conversation`, `messages`, `
502
502
 
503
503
  Scroll back with `messages(id, { cursor: page.meta.next_cursor })`, not a growing `offset`. Conversations carry `can_delete` / `can_leave`; check them before offering Delete or Leave.
504
504
 
505
- Private groups (0.7.59): each member has `role` (`admin` or `member`) and, in a group, `request_status`. Only the admin (the creator, then the longest-standing person left) may `addMembers(id, { member_ids })`, `removeMember(id, userId)` and `updateGroup(id, { name })` (else 403 `not_admin`). `createGroup({ name, member_ids, encrypted: true, device_id? })` makes an end-to-end encrypted group of people in the pilot, at most 8 counting pending invitees, or throws 409 `conversation_not_eligible` with `details.reason` and creates nothing. `r.realtime.onChatMembers` / `onChatConversation` tell you when people or the name change.
505
+ Private groups (0.7.59): each member has `role` (`admin` or `member`) and, in a group or a one-on-one chat, `request_status`: `accepted` from the start for the person who wrote, anyone who follows them and agents, else `pending` until that person accepts or replies (a decline reads `pending` to anyone else), so `accepted` alone does not mean they accepted anything. Only the admin (the creator, then the longest-standing person left) may `addMembers(id, { member_ids })`, `removeMember(id, userId)` and `updateGroup(id, { name })` (else 403 `not_admin`). `createGroup({ name, member_ids, encrypted: true, device_id? })` makes an end-to-end encrypted group of people in the pilot, at most 8 counting pending invitees, or throws 409 `conversation_not_eligible` with `details.reason` and creates nothing. `r.realtime.onChatMembers` / `onChatConversation` tell you when people or the name change.
506
506
 
507
507
  ### r.e2ee — End-to-end encryption transport (flag-gated)
508
508
 
@@ -685,7 +685,13 @@ the presign/PUT sequence in application code.
685
685
 
686
686
  Video: check `const { data } = await r.video.entitlement()` before offering the
687
687
  picker (`data.allowed`, `data.reason` 'tier' | 'quota', `data.remainingSeconds`),
688
- then `r.video.createUpload()`, which runs the same check.
688
+ then `r.video.createUpload()`, which runs the same check. Upload in chunks
689
+ (tus `chunkSize`) and keep the result for the attached file: a retry resumes the
690
+ same video. The TUS signature lasts an hour; past it, call
691
+ `r.video.renewUpload(playbackId, { grant: result.resumeGrant })` (0.7.63) for fresh
692
+ credentials for the same video. 404 (not yours, expired grant, older API) or 409
693
+ `upload_finished` means start a new upload instead. 503 means the video provider
694
+ could not answer: keep the partial upload and ask again later (`Retry-After`).
689
695
 
690
696
  ### r.chat — Messaging
691
697
 
@@ -700,7 +706,7 @@ Methods: `conversations`, `communityConversation`, `conversation`, `messages`, `
700
706
 
701
707
  Scroll back with `messages(id, { cursor: page.meta.next_cursor })`, not a growing `offset`. Conversations carry `can_delete` / `can_leave`; check them before offering Delete or Leave.
702
708
 
703
- Private groups (0.7.59): each member has `role` (`admin` or `member`) and, in a group, `request_status`. Only the admin (the creator, then the longest-standing person left) may `addMembers(id, { member_ids })`, `removeMember(id, userId)` and `updateGroup(id, { name })` (else 403 `not_admin`). `createGroup({ name, member_ids, encrypted: true, device_id? })` makes an end-to-end encrypted group of people in the pilot, at most 8 counting pending invitees, or throws 409 `conversation_not_eligible` with `details.reason` and creates nothing. `r.realtime.onChatMembers` / `onChatConversation` tell you when people or the name change.
709
+ Private groups (0.7.59): each member has `role` (`admin` or `member`) and, in a group or a one-on-one chat, `request_status`: `accepted` from the start for the person who wrote, anyone who follows them and agents, else `pending` until that person accepts or replies (a decline reads `pending` to anyone else), so `accepted` alone does not mean they accepted anything. Only the admin (the creator, then the longest-standing person left) may `addMembers(id, { member_ids })`, `removeMember(id, userId)` and `updateGroup(id, { name })` (else 403 `not_admin`). `createGroup({ name, member_ids, encrypted: true, device_id? })` makes an end-to-end encrypted group of people in the pilot, at most 8 counting pending invitees, or throws 409 `conversation_not_eligible` with `details.reason` and creates nothing. `r.realtime.onChatMembers` / `onChatConversation` tell you when people or the name change.
704
710
 
705
711
  ### r.tags
706
712
 
@@ -752,6 +758,7 @@ await r.appSubscriptions.changeTier({ tier: 'pro', proration_date: quote.prorati
752
758
  - 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`.
753
759
  - 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.
754
760
  - 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.
761
+ - 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.
755
762
  - `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.
756
763
  - 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`.
757
764
  - 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`.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@recursiv/sdk",
3
- "version": "0.7.62",
3
+ "version": "0.7.63",
4
4
  "description": "TypeScript SDK for the Recursiv API",
5
5
  "license": "FSL-1.1-ALv2",
6
6
  "type": "module",