@recursiv/sdk 0.7.66 → 0.7.67

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
@@ -732,7 +732,13 @@ The catalog lists `compass_filter` on every network when the filter is served to
732
732
  Hide politics (`compass: 'hide'`) also applies on its own, with or without compass judgments on the server, to
733
733
  `sdk.discovery.videos`, `protocols.getPublicTrends` and the radar (`getPublicRadar`, keyless `getSharedRadar`; 0.7.62):
734
734
  it leaves out what is categorised politics (a story not themed yet, or `mixed`, too) and says what it did in
735
- `meta.compass_filter`. These reads answer a side or Mix unfiltered, marked `unavailable`.
735
+ `meta.compass_filter`. These reads answer a side or Mix unfiltered, marked `unavailable`. Every radar story and every
736
+ World Today story (`curator.today()`) carries `political`: true when a quarter of its judged posts are politics, its
737
+ theme or topic is politics, or its headline or summary names an election, a war or troop deployment, a court or justice,
738
+ a legislature, or a party or politician. Hide politics leaves those out whatever their theme, on the radar and on
739
+ `curator.today({ compass: 'hide' })`, where the next story then leads. The stories of `feeds.stories()` and the lead of
740
+ `feeds.top()` carry `political` too (0.7.67), and those reads and `curator.forYouWorld()` leave political stories out
741
+ under Hide politics.
736
742
 
737
743
  `sdk.discovery.recommendations({ kind, category?, limit?, session?, exclude? })` is
738
744
  the app's one recommender for suggestion rails: `kind` is `member`, `group`, `tag`,
package/dist/index.d.ts CHANGED
@@ -195,7 +195,7 @@ export { VideoResource } from './resources/video.js';
195
195
  export type { VideoUploadCreds, CreateVideoUploadResult, VideoStatusInfo, VideoPlaybackToken, VideoEntitlement, } from './resources/video.js';
196
196
  export { AppSubscriptionsResource } from './resources/app-subscriptions.js';
197
197
  export type { AppSubscriptionBoostCredit, AppSubscriptionCheckoutResult, AppSubscriptionPortalResult, AppSubscriptionStatusResult, AppSubscriptionTierChangePreview, AppSubscriptionTierChangeResult, } from './resources/app-subscriptions.js';
198
- export type { CuratorSource, CuratorPrompt, CuratorRunInput, CuratorRunResult, CuratorTodayEdition, CuratorTodayExcerpt, CuratorTodayLeadImage, CuratorTodayRadar, CuratorTodayReaction, CuratorTodayResponse, CuratorTodaySource, CuratorTodayStory, ForYouMeta, ForYouParams, ForYouSurface, ForYouWorldBlock, ForYouWorldBlockType, ForYouWorldCategoryShortcut, ForYouWorldItem, ForYouWorldMeta, ForYouWorldParams, ForYouWorldPart, ForYouWorldResponse, ForYouWorldStory, ForYouWorldStoryTarget, ForYouWorldViral, } from './resources/curator.js';
198
+ export type { CuratorSource, CuratorPrompt, CuratorRunInput, CuratorRunResult, CuratorTodayEdition, CuratorTodayExcerpt, CuratorTodayLeadImage, CuratorTodayParams, CuratorTodayRadar, CuratorTodayReaction, CuratorTodayResponse, CuratorTodaySource, CuratorTodayStory, ForYouMeta, ForYouParams, ForYouSurface, ForYouWorldBlock, ForYouWorldBlockType, ForYouWorldCategoryShortcut, ForYouWorldItem, ForYouWorldMeta, ForYouWorldParams, ForYouWorldPart, ForYouWorldResponse, ForYouWorldStory, ForYouWorldStoryTarget, ForYouWorldViral, } from './resources/curator.js';
199
199
  export { ReportsResource } from './resources/reports.js';
200
200
  export type { ReportTargetType, ReportStatus, CreateReportInput, ReportRecord, ResolveReportInput, } from './resources/reports.js';
201
201
  export { ModerationResource } from './resources/moderation.js';
@@ -344,6 +344,12 @@ export interface CuratorTodayStory {
344
344
  headline: string;
345
345
  dek: string | null;
346
346
  topic: string | null;
347
+ /**
348
+ * Whether the story is political: its headline, dek or topic names an election, a war or troop deployment, a court or
349
+ * justice, a legislature, or a party or politician, or the radar story it seeded is political. Hide politics
350
+ * (`today({ compass: 'hide' })`) leaves these out. Absent from a server before it was served.
351
+ */
352
+ political?: boolean;
347
353
  /** Outlet links for a world story; null for a Minds story. */
348
354
  sources: CuratorTodaySource[] | null;
349
355
  /** Distinct outlets behind a world story; null for a Minds story. */
@@ -380,10 +386,22 @@ export interface CuratorTodayEdition {
380
386
  export interface CuratorTodayResponse {
381
387
  /** null when the network has no published edition yet. */
382
388
  data: CuratorTodayEdition | null;
389
+ /**
390
+ * `compass_filter`: present when the read named a filtering `compass`: what Hide politics did to the stories (counts
391
+ * only), or status 'unavailable' when it was not applied (not served to this reader yet, or a side or Mix).
392
+ */
383
393
  meta: {
384
394
  generating: boolean;
395
+ compass_filter?: CompassFilterMeta;
385
396
  };
386
397
  }
398
+ export interface CuratorTodayParams {
399
+ /**
400
+ * A Politics choice. Hide politics (`hide`) leaves out the political stories (CuratorTodayStory.political), and the
401
+ * next story leads. Any other choice is answered unfiltered, marked `unavailable`.
402
+ */
403
+ compass?: string;
404
+ }
387
405
  export declare class CuratorResource {
388
406
  private client;
389
407
  constructor(client: HttpClient);
@@ -392,7 +410,7 @@ export declare class CuratorResource {
392
410
  * (set by the open web, ranked by outlet count) come first; Minds
393
411
  * conversation stories follow. Requires `posts:read`.
394
412
  */
395
- today(): Promise<CuratorTodayResponse>;
413
+ today(params?: CuratorTodayParams): Promise<CuratorTodayResponse>;
396
414
  /**
397
415
  * Fetch the supplied sources in parallel, annotate each top item with
398
416
  * the supplied prompt template, and insert audience-scoped posts into
@@ -8,8 +8,8 @@ export class CuratorResource {
8
8
  * (set by the open web, ranked by outlet count) come first; Minds
9
9
  * conversation stories follow. Requires `posts:read`.
10
10
  */
11
- today() {
12
- return this.client.get('/curator/today');
11
+ today(params = {}) {
12
+ return params.compass ? this.client.get('/curator/today', { compass: params.compass }) : this.client.get('/curator/today');
13
13
  }
14
14
  /**
15
15
  * Fetch the supplied sources in parallel, annotate each top item with
@@ -97,6 +97,12 @@ export interface FeedStory {
97
97
  kind: 'breaking' | 'viral' | 'story';
98
98
  breaking: boolean;
99
99
  theme: string | null;
100
+ /**
101
+ * Whether the story is political whatever its theme: its headline names an election, a war or troop deployment, a
102
+ * court or justice, a legislature, or a party or politician, its theme is politics, or a quarter of its judged posts
103
+ * are politics. Hide politics (`compass: 'hide'`) leaves such a story out. Absent from a server older than 0.7.67.
104
+ */
105
+ political?: boolean;
100
106
  sources: Partial<Record<'outlet' | 'hackernews' | 'bluesky' | 'activitypub' | 'nostr' | 'minds', number>>;
101
107
  media: {
102
108
  images: number;
@@ -1206,6 +1206,12 @@ export interface SocialRadarCluster {
1206
1206
  };
1207
1207
  /** A browse-category id or `mixed`, from cached judgments only. */
1208
1208
  theme?: string;
1209
+ /**
1210
+ * Whether the story is political, whatever its theme: a quarter or more of its judged posts are politics, its theme is
1211
+ * politics, or its headline or summary names an election, a war or troop deployment, a court or justice, a legislature,
1212
+ * or a party or politician. Hide politics (`compass: 'hide'`) leaves these out. Absent from a server before it was served.
1213
+ */
1214
+ political?: boolean;
1209
1215
  sources: Partial<Record<SocialRadarSourceKind, number>>;
1210
1216
  corroboration: number;
1211
1217
  laneMix: {
@@ -1266,8 +1272,9 @@ export interface SocialRadarParams {
1266
1272
  category?: string;
1267
1273
  limit?: number;
1268
1274
  /**
1269
- * A Politics choice. Hide politics (`hide`) leaves out the stories whose theme is politics, and the ones not themed yet
1270
- * or themed `mixed` (a story has no compass placement, so it fails closed); the stories after them fill the list.
1275
+ * A Politics choice. Hide politics (`hide`) leaves out the political stories (SocialRadarCluster.political) whatever
1276
+ * their theme, and the ones not themed yet or themed `mixed` (a story has no compass placement, so it fails closed);
1277
+ * the stories after them fill the list.
1271
1278
  * A side or Mix is answered unfiltered, marked `unavailable`; the keyless `getSharedRadar` takes only `all` or `hide`.
1272
1279
  */
1273
1280
  compass?: string;
@@ -1281,7 +1288,7 @@ export interface SocialRadarPosts {
1281
1288
  /** A story that has left the radar, as the radar's story archive keeps it (up to 30 days after it was last on the radar). */
1282
1289
  export interface SocialRadarStoryArchive {
1283
1290
  /** The story as the radar last published it. Articles keep their headline and link; a post is a member by reference only (no `title`). */
1284
- cluster: Pick<SocialRadarCluster, 'id' | 'label' | 'kind' | 'seed' | 'theme' | 'sources' | 'laneMix'> & {
1291
+ cluster: Pick<SocialRadarCluster, 'id' | 'label' | 'kind' | 'seed' | 'theme' | 'political' | 'sources' | 'laneMix'> & {
1285
1292
  members: Array<Pick<SocialRadarMember, 'kind' | 'key' | 'url' | 'publishedAt' | 'outlet'> & {
1286
1293
  title?: string;
1287
1294
  }>;
package/dist/types.d.ts CHANGED
@@ -698,6 +698,20 @@ export interface Conversation {
698
698
  /** Whether `chat.leave()` is allowed for the caller. */
699
699
  can_leave?: boolean;
700
700
  created_at: string;
701
+ /**
702
+ * When the conversation last had activity, the time `conversations()` sorts
703
+ * by (0.7.67): its newest message, or, in an end-to-end encrypted
704
+ * (`mls_v2`) conversation, the relay's receive time of its newest encrypted
705
+ * message when that is later; its creation when it has no messages.
706
+ * In an `mls_v2` conversation, membership and key changes and quiet events
707
+ * (a reaction, an edit, a delete) do not move it. In any other
708
+ * conversation it is read from the messages there now, so deleting the
709
+ * newest message moves it back, to the previous message or to the
710
+ * creation: do not keep the later of a cached and a served value. A time
711
+ * only, never content, so a device that cannot read an encrypted chat can
712
+ * still show when it last moved. Omitted by older APIs.
713
+ */
714
+ last_activity_at?: string;
701
715
  }
702
716
  export interface ConversationRequestResult {
703
717
  id: string;
@@ -733,6 +747,12 @@ export interface ConversationDetail {
733
747
  /** Whether `chat.leave()` is allowed for the caller. */
734
748
  can_leave?: boolean;
735
749
  created_at: string;
750
+ /**
751
+ * As `Conversation.last_activity_at` (0.7.67). Served by `conversation()`;
752
+ * omitted by `createGroup()`, to a member who declined the request, and by
753
+ * older APIs.
754
+ */
755
+ last_activity_at?: string;
736
756
  }
737
757
  export type ChatMemberRole = 'admin' | 'member';
738
758
  /**
package/llm.md CHANGED
@@ -504,6 +504,8 @@ Scroll back with `messages(id, { cursor: page.meta.next_cursor })`, not a growin
504
504
 
505
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
+ Activity time (0.7.67): conversation rows, `communityConversation()` and `conversation(id)` carry `last_activity_at`, the time `conversations()` sorts by: the newest message, or, in an end-to-end encrypted (`mls_v2`) conversation, the relay's receive time of its newest encrypted message when that is later (membership and key changes and quiet events such as reactions do not move it); its creation when it has no messages. In any other conversation, deleting the newest message moves it back to the previous message (or the creation), so do not keep the later of a cached and a served value. A time only, never content: show it on a row the device cannot decrypt. Omitted by older APIs, and by `conversation(id)` to a member who declined the request.
508
+
507
509
  ### r.e2ee — End-to-end encryption transport (flag-gated)
508
510
 
509
511
  Relays opaque MLS envelopes, public device credentials and one-time key packages for the shared E2EE core; the server never sees keys or plaintext. Requires the `e2eeV2` network feature flag (otherwise `feature_disabled`) and a key acting as the account owner (agent-scoped keys get `agent_key_not_allowed`). The realm is the network id. Only a direct conversation between two people with an accepted request can start encryption (`conversation_not_eligible` otherwise), and every later append to it is checked the same way again (both people still accepted, still in the pilot, still only the two of them), so a client need not read the policy before each send; a resend of an event already stored still returns `duplicate: true`, even once a Welcome's target device is gone; blocks apply (`blocked`), to resends too; commits must send `expected_commit_seq`.
@@ -733,6 +735,8 @@ Scroll back with `messages(id, { cursor: page.meta.next_cursor })`, not a growin
733
735
 
734
736
  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.
735
737
 
738
+ Activity time (0.7.67): conversation rows, `communityConversation()` and `conversation(id)` carry `last_activity_at`, the time `conversations()` sorts by: the newest message, or, in an end-to-end encrypted (`mls_v2`) conversation, the relay's receive time of its newest encrypted message when that is later (membership and key changes and quiet events such as reactions do not move it); its creation when it has no messages. In any other conversation, deleting the newest message moves it back to the previous message (or the creation), so do not keep the later of a cached and a served value. A time only, never content: show it on a row the device cannot decrypt. Omitted by older APIs, and by `conversation(id)` to a member who declined the request.
739
+
736
740
  ### r.tags
737
741
 
738
742
  ```typescript
@@ -783,6 +787,7 @@ await r.appSubscriptions.changeTier({ tier: 'pro', proration_date: quote.prorati
783
787
  - 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`.
784
788
  - 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.
785
789
  - 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.
790
+ - 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.
786
791
  - 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.
787
792
  - `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.
788
793
  - 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`.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@recursiv/sdk",
3
- "version": "0.7.66",
3
+ "version": "0.7.67",
4
4
  "description": "TypeScript SDK for the Recursiv API",
5
5
  "license": "FSL-1.1-ALv2",
6
6
  "type": "module",