@recursiv/sdk 0.7.69 → 0.7.71

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. package/README.md +78 -10
  2. package/dist/client.d.ts +5 -0
  3. package/dist/client.js +7 -1
  4. package/dist/index.d.ts +14 -4
  5. package/dist/index.js +11 -1
  6. package/dist/realtime.d.ts +12 -3
  7. package/dist/realtime.js +10 -3
  8. package/dist/resources/agents.d.ts +48 -0
  9. package/dist/resources/agents.js +22 -0
  10. package/dist/resources/chat.d.ts +4 -1
  11. package/dist/resources/chat.js +4 -1
  12. package/dist/resources/curator.d.ts +9 -3
  13. package/dist/resources/discovery-fallover.d.ts +8 -0
  14. package/dist/resources/discovery-fallover.js +26 -0
  15. package/dist/resources/discovery-first-screen.d.ts +30 -0
  16. package/dist/resources/discovery-first-screen.js +149 -0
  17. package/dist/resources/discovery-merge.d.ts +144 -0
  18. package/dist/resources/discovery-merge.js +479 -0
  19. package/dist/resources/discovery-progressive.d.ts +117 -0
  20. package/dist/resources/discovery-progressive.js +414 -0
  21. package/dist/resources/discovery-safety.d.ts +32 -0
  22. package/dist/resources/discovery-safety.js +521 -0
  23. package/dist/resources/discovery.d.ts +59 -16
  24. package/dist/resources/discovery.js +147 -47
  25. package/dist/resources/e2ee.d.ts +41 -0
  26. package/dist/resources/e2ee.js +33 -0
  27. package/dist/resources/feeds.d.ts +3 -1
  28. package/dist/resources/judgments.d.ts +252 -0
  29. package/dist/resources/judgments.js +79 -0
  30. package/dist/resources/protocols.d.ts +196 -15
  31. package/dist/resources/protocols.js +16 -2
  32. package/dist/resources/search.d.ts +9 -1
  33. package/dist/resources/settings.d.ts +8 -1
  34. package/dist/resources/settings.js +9 -0
  35. package/dist/resources/signals.d.ts +58 -0
  36. package/dist/resources/signals.js +32 -0
  37. package/dist/types.d.ts +90 -5
  38. package/llm.md +25 -6
  39. package/package.json +1 -1
@@ -0,0 +1,252 @@
1
+ import type { HttpClient } from '../client.js';
2
+ import { ConflictError } from '../errors.js';
3
+ import type { SingleResponse } from '../types.js';
4
+ /** A Minds post (`post`, its uuid) or an open-network post (`network_post`, its subject id). */
5
+ export type JudgmentSubjectKind = 'post' | 'network_post';
6
+ /** Who wrote the label: Jev (`model`), or staff who overrode it (`review`). */
7
+ export type JudgmentSource = 'model' | 'review';
8
+ /** Whether the server's judgment store answered. `'unavailable'`: only the category index's label is known; hide label actions. */
9
+ export type PostLabelsStatus = 'ok' | 'unavailable';
10
+ /** How an override met its quorum: one reviewer (a safety appeal), two agreeing reviewers, or one beside an agreeing Jev re-judge. */
11
+ export type JudgmentOverrideMethod = 'reviewer' | 'two_reviewers' | 'reviewer_and_rejudge';
12
+ export interface PostLabelsQuery {
13
+ kind: JudgmentSubjectKind;
14
+ /** A Minds post id, or an open-network post's subject id. */
15
+ id: string;
16
+ }
17
+ export interface PostLabelsSubject {
18
+ kind: JudgmentSubjectKind;
19
+ id: string;
20
+ }
21
+ export interface PostLabelsCategory {
22
+ id: string;
23
+ name: string;
24
+ }
25
+ /** The compass band in words. Never a probability or a cell code. */
26
+ export interface PostLabelsPolitics {
27
+ /** The judgment a member disputes with `dispute(judgment_id, { question_key: 'placement', proposed })`. */
28
+ judgment_id: string;
29
+ politics: 'Takes a side' | 'Maybe' | 'No';
30
+ /** Each placed axis in words, e.g. `{ axis: 'Liberty ↔ Order', band: 'Order' }`. Empty unless the post takes a side. */
31
+ axes: Array<{
32
+ axis: string;
33
+ band: string;
34
+ }>;
35
+ model: string;
36
+ rubric: string;
37
+ source: JudgmentSource;
38
+ judged_at: string | null;
39
+ }
40
+ /** One stored judgment, as the post's author and staff see it. */
41
+ export interface PostJudgmentRecord {
42
+ id: string;
43
+ rubric: string;
44
+ model: string;
45
+ source: JudgmentSource;
46
+ judged_at: string | null;
47
+ /** The typed answers, every probability included. */
48
+ answers: Record<string, unknown>;
49
+ political: number | null;
50
+ cell: string | null;
51
+ reason: 'hate' | 'spam' | null;
52
+ }
53
+ /** A rubric's current judgment in the full view. */
54
+ export interface PostJudgment extends PostJudgmentRecord {
55
+ /** How many staff reviewers wrote it (0 for Jev's own). */
56
+ reviewers: number;
57
+ /** Staff only. */
58
+ reviewer_ids?: string[];
59
+ /** Staff only. */
60
+ review_note?: string | null;
61
+ /** Whether `dispute()` takes it: political-compass and safety labels (safety: the author only). */
62
+ disputable: boolean;
63
+ /** Jev's judgment a staff review replaced, for the same words. */
64
+ superseded: PostJudgmentRecord | null;
65
+ }
66
+ /** The safety verdict the Discover floor applies to the post now, with who judged it (full view, Minds posts). */
67
+ export type PostSafetyAttribution = {
68
+ status: 'judged';
69
+ reason: 'hate' | 'spam' | null;
70
+ unsafe: boolean;
71
+ hate: number;
72
+ spam: number;
73
+ rubric: string;
74
+ /** The judging model, `'review'` for a staff review; null when only the cache holds the verdict. */
75
+ model: string | null;
76
+ source: JudgmentSource | 'cache';
77
+ judgedAt: string | null;
78
+ /** A staff review the floor does not apply yet (the cache still holds another verdict). */
79
+ unappliedReview?: {
80
+ reason: 'hate' | 'spam' | null;
81
+ unsafe: boolean;
82
+ hate: number;
83
+ spam: number;
84
+ judgedAt: string;
85
+ };
86
+ } | {
87
+ status: 'pending';
88
+ } | {
89
+ status: 'not_judgeable';
90
+ };
91
+ /** A visitor's view (`getPublic`): the category only. */
92
+ export interface PostLabelsCategoryView {
93
+ subject: PostLabelsSubject;
94
+ view: 'category';
95
+ category: PostLabelsCategory | null;
96
+ status: PostLabelsStatus;
97
+ }
98
+ /** Any other member's view: the category and the compass band in words, with the id to dispute it. */
99
+ export interface PostLabelsSummaryView {
100
+ subject: PostLabelsSubject;
101
+ view: 'summary';
102
+ category: PostLabelsCategory | null;
103
+ politics: PostLabelsPolitics | null;
104
+ status: PostLabelsStatus;
105
+ }
106
+ /** The post's author's and staff's view: every rubric's current judgment, the safety verdict and Viral. */
107
+ export interface PostLabelsFullView {
108
+ subject: PostLabelsSubject;
109
+ view: 'full';
110
+ category: PostLabelsCategory | null;
111
+ politics: PostLabelsPolitics | null;
112
+ judgments: PostJudgment[];
113
+ /** null for an open-network post. */
114
+ safety: PostSafetyAttribution | null;
115
+ /** Why the post is withheld from Viral ("Withheld from Viral: <reason>"), or null. */
116
+ viral: {
117
+ withheld: string | null;
118
+ };
119
+ status: PostLabelsStatus;
120
+ }
121
+ /** One post's labels, in the view the caller may see: switch on `view`. */
122
+ export type PostLabels = PostLabelsFullView | PostLabelsSummaryView | PostLabelsCategoryView;
123
+ export interface DisputeJudgmentInput {
124
+ /** The label's question: `'placement'` for a political-compass label, `'safety'` for a safety label (its author only). */
125
+ question_key: string;
126
+ /** The answer it should be: a compass cell (`'lo'`, `'cm'`, …) or `'not_political'`; for safety `'none'`, `'hate'` or `'spam'`. */
127
+ proposed: string;
128
+ /** At most 500 characters. */
129
+ note?: string;
130
+ }
131
+ /** A filed dispute. The author's is an appeal and goes to the moderation queue; anyone else's feeds the evaluation. */
132
+ export interface JudgmentDispute {
133
+ id: string;
134
+ judgment_id: string;
135
+ /** True when the caller wrote the post: an appeal. */
136
+ appeal: boolean;
137
+ /** Whether it waits in the moderation queue now. */
138
+ queued: boolean;
139
+ /** Present on a repeat: the member's first dispute of this judgment, unchanged. */
140
+ existing?: true;
141
+ }
142
+ export type ReviewJudgmentInput = {
143
+ outcome: 'uphold';
144
+ note?: string;
145
+ } | {
146
+ outcome: 'override';
147
+ /** The answer the label becomes. */
148
+ proposed: string;
149
+ /** At most 2,000 characters. */
150
+ note?: string;
151
+ /** A safety override to `'none'` that frees other posts with exactly these words: send true after a 409 `shared_words`. */
152
+ apply_to_shared?: boolean;
153
+ };
154
+ /** 200: the label stands, and its queue items are closed. */
155
+ export interface JudgmentReviewUpheld {
156
+ status: 'upheld';
157
+ resolved: number;
158
+ }
159
+ /** 202: this reviewer's vote is recorded; a compass label needs another reviewer (or an agreeing Jev re-judge). */
160
+ export interface JudgmentReviewAwaitingReviewer {
161
+ status: 'awaiting_reviewer';
162
+ vote_id: string;
163
+ agreeing: number;
164
+ needed: number;
165
+ }
166
+ /** 200: the override is written and in the public moderation log. */
167
+ export interface JudgmentReviewOverridden {
168
+ status: 'overridden';
169
+ review_id: string;
170
+ moderation_action_id: string;
171
+ method: JudgmentOverrideMethod;
172
+ reviewers: number;
173
+ resolved: number;
174
+ trust_log: 'appended' | 'disabled' | 'duplicate';
175
+ /** A safety review: whether the Discover floor applies it now; null for a compass label. */
176
+ applied_to_discover: boolean | null;
177
+ /** A safety review of words the post no longer has: recorded, never applied. */
178
+ words_changed: boolean | null;
179
+ shared_posts: number | null;
180
+ }
181
+ export type JudgmentReview = JudgmentReviewUpheld | JudgmentReviewAwaitingReviewer | JudgmentReviewOverridden;
182
+ /** The `details` of a 409 `shared_words` from `review()`. */
183
+ export interface JudgmentSharedWordsDetails {
184
+ shared_posts: number;
185
+ }
186
+ /** The 409 `review()` throws before a safety override frees other posts with exactly these words. */
187
+ export type JudgmentSharedWordsError = ConflictError & {
188
+ code: 'shared_words';
189
+ details: JudgmentSharedWordsDetails;
190
+ };
191
+ /**
192
+ * Whether `error` is the 409 `shared_words` of `review()`: nothing was recorded, and
193
+ * `error.details.shared_posts` other posts have exactly these words. Resend with
194
+ * `apply_to_shared: true` once the reviewer agrees to change them all.
195
+ */
196
+ export declare function isJudgmentSharedWordsError(error: unknown): error is JudgmentSharedWordsError;
197
+ /**
198
+ * Jev's labels of one post: read them, dispute or appeal one, and (staff)
199
+ * decide a dispute.
200
+ *
201
+ * Budgets: 60 label reads per member an hour (25 of one author's posts) and
202
+ * 20 disputes a day. Past one, the server answers 429 with
203
+ * `X-RateLimit-Scope: action`: the SDK throws `RateLimitError` at once
204
+ * (`scope === 'action'`, `resetAt`, a member-facing `message`) and never
205
+ * retries it. Disputes and reviews answer 503 until the server's judgment
206
+ * store is applied; reads answer `status: 'unavailable'` instead.
207
+ */
208
+ export declare class JudgmentsResource {
209
+ private client;
210
+ constructor(client: HttpClient);
211
+ /**
212
+ * One post's labels as the caller may see them. The post's author and staff
213
+ * get `view: 'full'` (every judgment with its answers, the safety verdict,
214
+ * Viral); any other member who can see the post gets `view: 'summary'` (the
215
+ * category and the compass band in words). `status: 'unavailable'` is
216
+ * returned, not thrown. A post the caller cannot see throws `NotFoundError`.
217
+ * Open-network posts need a key bound to an app (400 `app_required`).
218
+ *
219
+ * @example
220
+ * const { data } = await r.judgments.get({ kind: 'post', id: postId });
221
+ * if (data.status === 'ok' && data.view !== 'category' && data.politics) showBand(data.politics);
222
+ */
223
+ get(query: PostLabelsQuery): Promise<SingleResponse<PostLabels>>;
224
+ /**
225
+ * A signed-out visitor's view of one post's labels: the category only.
226
+ * Keyless: use an anonymous client (`new Recursiv({ anonymous: true })`).
227
+ */
228
+ getPublic(projectId: string, query: PostLabelsQuery): Promise<SingleResponse<PostLabelsCategoryView>>;
229
+ /**
230
+ * Disagree with a label. The post's author's dispute is an appeal and goes
231
+ * to the moderation queue; anyone else's feeds the evaluation and changes
232
+ * nothing on its own. One per member and judgment: a repeat returns the
233
+ * first with `existing: true`. Sent once, never retried automatically.
234
+ *
235
+ * @example
236
+ * await r.judgments.dispute(labels.politics.judgment_id, { question_key: 'placement', proposed: 'not_political' });
237
+ */
238
+ dispute(judgmentId: string, input: DisputeJudgmentInput): Promise<SingleResponse<JudgmentDispute>>;
239
+ /**
240
+ * Staff: uphold a disputed label, or vote to override it. A compass label
241
+ * changes when two reviewers propose the same answer (or one, beside an
242
+ * agreeing Jev re-judge): the first gets `status: 'awaiting_reviewer'`
243
+ * (HTTP 202). Every override is in the public moderation log.
244
+ *
245
+ * Sent once, never retried: an override that met its quorum but lost its
246
+ * response would otherwise be recorded again as a fresh vote. A safety
247
+ * override to `'none'` that would free other posts with exactly these words
248
+ * throws a 409 (`isJudgmentSharedWordsError`, `details.shared_posts`) until
249
+ * resent with `apply_to_shared: true`.
250
+ */
251
+ review(judgmentId: string, input: ReviewJudgmentInput): Promise<SingleResponse<JudgmentReview>>;
252
+ }
@@ -0,0 +1,79 @@
1
+ import { ConflictError } from '../errors.js';
2
+ /**
3
+ * Whether `error` is the 409 `shared_words` of `review()`: nothing was recorded, and
4
+ * `error.details.shared_posts` other posts have exactly these words. Resend with
5
+ * `apply_to_shared: true` once the reviewer agrees to change them all.
6
+ */
7
+ export function isJudgmentSharedWordsError(error) {
8
+ if (!(error instanceof ConflictError) || error.code !== 'shared_words')
9
+ return false;
10
+ const count = error.details?.shared_posts;
11
+ return typeof count === 'number' && Number.isFinite(count);
12
+ }
13
+ const path = (judgmentId, action) => `/content-judgments/${encodeURIComponent(judgmentId)}/${action}`;
14
+ /**
15
+ * Jev's labels of one post: read them, dispute or appeal one, and (staff)
16
+ * decide a dispute.
17
+ *
18
+ * Budgets: 60 label reads per member an hour (25 of one author's posts) and
19
+ * 20 disputes a day. Past one, the server answers 429 with
20
+ * `X-RateLimit-Scope: action`: the SDK throws `RateLimitError` at once
21
+ * (`scope === 'action'`, `resetAt`, a member-facing `message`) and never
22
+ * retries it. Disputes and reviews answer 503 until the server's judgment
23
+ * store is applied; reads answer `status: 'unavailable'` instead.
24
+ */
25
+ export class JudgmentsResource {
26
+ client;
27
+ constructor(client) {
28
+ this.client = client;
29
+ }
30
+ /**
31
+ * One post's labels as the caller may see them. The post's author and staff
32
+ * get `view: 'full'` (every judgment with its answers, the safety verdict,
33
+ * Viral); any other member who can see the post gets `view: 'summary'` (the
34
+ * category and the compass band in words). `status: 'unavailable'` is
35
+ * returned, not thrown. A post the caller cannot see throws `NotFoundError`.
36
+ * Open-network posts need a key bound to an app (400 `app_required`).
37
+ *
38
+ * @example
39
+ * const { data } = await r.judgments.get({ kind: 'post', id: postId });
40
+ * if (data.status === 'ok' && data.view !== 'category' && data.politics) showBand(data.politics);
41
+ */
42
+ get(query) {
43
+ return this.client.get('/content-judgments', { kind: query.kind, id: query.id });
44
+ }
45
+ /**
46
+ * A signed-out visitor's view of one post's labels: the category only.
47
+ * Keyless: use an anonymous client (`new Recursiv({ anonymous: true })`).
48
+ */
49
+ getPublic(projectId, query) {
50
+ return this.client.get(`/protocols/public/${encodeURIComponent(projectId)}/content-judgments`, { kind: query.kind, id: query.id });
51
+ }
52
+ /**
53
+ * Disagree with a label. The post's author's dispute is an appeal and goes
54
+ * to the moderation queue; anyone else's feeds the evaluation and changes
55
+ * nothing on its own. One per member and judgment: a repeat returns the
56
+ * first with `existing: true`. Sent once, never retried automatically.
57
+ *
58
+ * @example
59
+ * await r.judgments.dispute(labels.politics.judgment_id, { question_key: 'placement', proposed: 'not_political' });
60
+ */
61
+ dispute(judgmentId, input) {
62
+ return this.client.post(path(judgmentId, 'disputes'), input, { retry: false });
63
+ }
64
+ /**
65
+ * Staff: uphold a disputed label, or vote to override it. A compass label
66
+ * changes when two reviewers propose the same answer (or one, beside an
67
+ * agreeing Jev re-judge): the first gets `status: 'awaiting_reviewer'`
68
+ * (HTTP 202). Every override is in the public moderation log.
69
+ *
70
+ * Sent once, never retried: an override that met its quorum but lost its
71
+ * response would otherwise be recorded again as a fresh vote. A safety
72
+ * override to `'none'` that would free other posts with exactly these words
73
+ * throws a 409 (`isJudgmentSharedWordsError`, `details.shared_posts`) until
74
+ * resent with `apply_to_shared: true`.
75
+ */
76
+ review(judgmentId, input) {
77
+ return this.client.post(path(judgmentId, 'reviews'), input, { retry: false });
78
+ }
79
+ }
@@ -1,5 +1,5 @@
1
1
  import type { HttpClient } from '../client.js';
2
- import type { CompassFilterMeta, DiscoveryLayout, PaginatedResponse, PaginationParams, SingleResponse } from '../types.js';
2
+ import type { CompassFilterMeta, DiscoveryLayout, PaginatedResponse, PaginationParams, SingleResponse, StoryOutlets } from '../types.js';
3
3
  /** Canonical protocol operation names returned by the adapter registry. */
4
4
  export declare const PROTOCOL_CAPABILITY_OPERATIONS: readonly ["discover", "stream", "backfill", "search", "resolveIdentity", "fetchProfile", "fetchThread", "publish", "reply", "react", "repost", "follow", "delete", "health", "checkpoint"];
5
5
  export declare const PROTOCOL_CAPABILITY_STATUSES: readonly ["supported", "planned", "unsupported"];
@@ -181,6 +181,134 @@ export interface ProtocolIngestionReplayResult {
181
181
  job_id: string;
182
182
  status: 'replayed';
183
183
  }
184
+ /** A section of the ingestion health report that can fail to answer within its budget. */
185
+ export type ProtocolIngestionHealthSection = 'activity' | 'circuits' | 'dead_letters' | 'jobs' | 'kill_switch' | 'lanes';
186
+ /**
187
+ * Where a job is in its lane, read from its checkpoint: RSS reads one feed per page (feed_index); Hacker News and
188
+ * Event Registry walk term_index and page. Null when the checkpoint holds no such index.
189
+ */
190
+ export interface ProtocolIngestionHealthPosition {
191
+ feed_index: number | null;
192
+ term_index: number | null;
193
+ page: number | null;
194
+ }
195
+ export interface ProtocolIngestionHealthRuntime {
196
+ mode: 'collector' | 'runtime';
197
+ enabled: boolean;
198
+ simulation: boolean;
199
+ global_kill_switch: boolean;
200
+ controls_valid: boolean;
201
+ project_allowed: boolean;
202
+ /** This app's operator kill switch; null when that read did not answer. */
203
+ kill_switch: boolean | null;
204
+ }
205
+ export interface ProtocolIngestionLaneJobs {
206
+ queued: number;
207
+ leased: number;
208
+ done: number;
209
+ dead: number;
210
+ }
211
+ /** The lane's open job (the worker keeps one open per lane). */
212
+ export interface ProtocolIngestionLaneCurrentJob {
213
+ status: 'queued' | 'leased';
214
+ position: ProtocolIngestionHealthPosition;
215
+ pages_committed: number;
216
+ consecutive_failures: number;
217
+ last_error: string | null;
218
+ retry_at: string | null;
219
+ /** A leased job's lease end; null while queued. */
220
+ lease_until: string | null;
221
+ /** Leased, but the lease ran out and no worker reclaimed it (the worker died mid-page). */
222
+ lease_expired: boolean;
223
+ /** Eligible to run since then; null while a live lease holds it or it waits out a retry. */
224
+ waiting_since: string | null;
225
+ /** waiting_since once the wait passes the worker's starvation threshold (30 minutes); otherwise null. */
226
+ starved_since: string | null;
227
+ }
228
+ export interface ProtocolIngestionActivityReason {
229
+ status: 'retry' | 'dead' | 'invalid_job' | 'deferred' | 'abandoned';
230
+ reason: string | null;
231
+ count: number;
232
+ }
233
+ /** Attempts over the last window_hours; last success, last failure and failing_since look back lookback_days. */
234
+ export interface ProtocolIngestionLaneActivity {
235
+ window_hours: number;
236
+ pages_committed: number;
237
+ /** Attempts that ended in a retry, a dead letter or an invalid job. */
238
+ failed: number;
239
+ deferred: number;
240
+ abandoned: number;
241
+ reasons: ProtocolIngestionActivityReason[];
242
+ lookback_days: number;
243
+ last_success_at: string | null;
244
+ last_failure_at: string | null;
245
+ /** The first failure after the last success; null when the newest finished attempt succeeded or none failed. */
246
+ failing_since: string | null;
247
+ }
248
+ export interface ProtocolIngestionDeadLetterGroup {
249
+ reason: string | null;
250
+ position: ProtocolIngestionHealthPosition;
251
+ count: number;
252
+ last_at: string | null;
253
+ }
254
+ /** Every dead job of the lane counted in total; the newest 20 (reason, position) groups listed. */
255
+ export interface ProtocolIngestionLaneDeadLetters {
256
+ total: number;
257
+ last_dead_at: string | null;
258
+ groups: ProtocolIngestionDeadLetterGroup[];
259
+ }
260
+ /** One ingestion lane. A section that did not answer reads as null here and is named in the report's `unavailable`. */
261
+ export interface ProtocolIngestionLaneHealth {
262
+ provider_id: string;
263
+ source_subscription_key: string;
264
+ active: boolean;
265
+ /** RSS only: the feeds this process reads, one page each, so a pass ends after feed_count pages. Null for other providers. */
266
+ feed_count: number | null;
267
+ jobs: ProtocolIngestionLaneJobs | null;
268
+ /** When a job of this lane last reached done (read every page of its pass). */
269
+ last_done_at: string | null;
270
+ /** Null when the lane has no open job or the jobs section is unavailable. */
271
+ current: ProtocolIngestionLaneCurrentJob | null;
272
+ activity: ProtocolIngestionLaneActivity | null;
273
+ dead_letters: ProtocolIngestionLaneDeadLetters | null;
274
+ }
275
+ /** A circuit row that is open, counting failures or killed: the provider's own, or (per-RSS-host circuit on) a fetch host's. */
276
+ export interface ProtocolIngestionCircuitHealth {
277
+ provider_id: string;
278
+ kind: 'provider' | 'host';
279
+ /**
280
+ * A host circuit's fetch host: read from a readable key, or for a hashed key the configured feed host (with any
281
+ * explicit port) it belongs to. Null for the provider's own circuit and for a hashed key no configured feed names.
282
+ */
283
+ host: string | null;
284
+ /** The quota row key: `rss`, `rss--h-feeds_bbci_co_uk`, `rss--x-<hash>`. */
285
+ domain: string;
286
+ open: boolean;
287
+ open_until: string | null;
288
+ /**
289
+ * When the circuit last opened, or re-opened on a failed probe (open_until minus the open window). Null when it has
290
+ * not opened since it last closed.
291
+ */
292
+ opened_at: string | null;
293
+ /** Failures in a row since the circuit last closed (a host circuit's: feeds whose host never accepted the connection). */
294
+ failures: number;
295
+ killed: boolean;
296
+ }
297
+ /**
298
+ * Counts-only ingestion health for the key's app (audit row 8a): per lane, job counts, the open job's position,
299
+ * the last done, success and failure, 24-hour attempts by reason and dead letters by reason and position; plus the
300
+ * open or counting circuits. Never a checkpoint, payload, candidate or feed body.
301
+ */
302
+ export interface ProtocolIngestionHealthReport {
303
+ generated_at: string;
304
+ runtime: ProtocolIngestionHealthRuntime;
305
+ /** Null when the lanes section is unavailable. */
306
+ lanes: ProtocolIngestionLaneHealth[] | null;
307
+ /** Open first. Null when the circuits section is unavailable. */
308
+ circuits: ProtocolIngestionCircuitHealth[] | null;
309
+ /** Sections that did not answer within their statement budget; their fields read as null. */
310
+ unavailable: ProtocolIngestionHealthSection[];
311
+ }
184
312
  /** App/network scope comes only from the authenticated project-bound key. */
185
313
  export interface ListProtocolCandidatesParams {
186
314
  protocol?: string;
@@ -250,6 +378,14 @@ export type NativeNostrPrepareInput = {
250
378
  content: string;
251
379
  /** Your own post in this app (sdk.posts) that this note copies, so deleting it can offer this copy. */
252
380
  crossPostOf?: string;
381
+ /**
382
+ * A NIP-18 quote of one public note (0.7.71; accounts advertising `quote`). The server reads the note and adds its
383
+ * `q` and `p` tags and a `nostr:nevent` line after your text; review `quote` and the exact event before signing.
384
+ */
385
+ quote?: {
386
+ protocol: 'nostr';
387
+ nativeId: string;
388
+ };
253
389
  } | {
254
390
  action: 'reply';
255
391
  content: string;
@@ -662,13 +798,22 @@ export interface NativeReplyBatch {
662
798
  /** Public text and Minds-hosted images; accounts are selected explicitly in the shared editor. */
663
799
  export interface NativePostPrepareInput {
664
800
  clientMutationId: string;
801
+ /**
802
+ * The exact public text. Pass '' for a photo-only post (no words), which needs at least one `media`
803
+ * item: Bluesky gets an empty text with the images and Mastodon a status with only the photos, each
804
+ * photo's `alt` as its description. '' without media, or only spaces, is refused. Replies always need text.
805
+ */
665
806
  text: string;
666
807
  accountIds: string[];
667
808
  /** Minds-hosted images attached to every selected destination. Omit for a text-only post. */
668
809
  media?: NativeMediaInput[];
669
- /** Native quote on Bluesky only. The server reads the source and permissions using each selected account. */
810
+ /**
811
+ * A native quote: a Bluesky record (`at://` URI), or a Mastodon status by its public URI (0.7.71) on a server that
812
+ * creates quote posts (Mastodon API version 7). The server reads the source and permissions using each selected account;
813
+ * a server without quote posts answers `quote_not_supported`, never a plain link. Nostr quotes use `prepareNativeNostr`.
814
+ */
670
815
  quote?: {
671
- protocol: 'bluesky';
816
+ protocol: 'bluesky' | 'activitypub';
672
817
  nativeId: string;
673
818
  };
674
819
  /**
@@ -685,17 +830,24 @@ export interface NativePostBatch {
685
830
  deliveries: Array<Omit<NativeReplyBatch['deliveries'][number], 'kind' | 'parent' | 'recoverable' | 'replacedBy'> & {
686
831
  audience: 'public';
687
832
  maxCharacters: number;
688
- quote?: {
689
- protocol: 'bluesky';
690
- nativeId: string;
691
- cid: string;
692
- authorId: string;
693
- authorHandle: string;
694
- sourceUrl: string;
695
- text: string;
696
- };
833
+ quote?: NativePostQuote;
697
834
  }>;
698
835
  }
836
+ /** The reviewed quote of a delivery: Bluesky binds the record's CID, Mastodon the status's id on the member's server and its revision. */
837
+ export type NativePostQuote = {
838
+ nativeId: string;
839
+ authorId: string;
840
+ authorHandle: string;
841
+ sourceUrl: string;
842
+ text: string;
843
+ } & ({
844
+ protocol: 'bluesky';
845
+ cid: string;
846
+ } | {
847
+ protocol: 'activitypub';
848
+ localId: string;
849
+ revision: string;
850
+ });
699
851
  export interface NativeDelegatedPostHistoryItem {
700
852
  id: string;
701
853
  createdAt: string;
@@ -822,11 +974,22 @@ export interface NativeNostrPrepared {
822
974
  text: string;
823
975
  textTruncated?: boolean;
824
976
  };
977
+ /** The quoted note (a publish with `quote`), and the relay hint its `q` tag and `nostr:nevent` carry. */
978
+ quote?: {
979
+ protocol: 'nostr';
980
+ nativeId: string;
981
+ authorId: string;
982
+ authorHandle: string;
983
+ sourceUrl: string;
984
+ text: string;
985
+ textTruncated: boolean;
986
+ relay: string;
987
+ };
825
988
  }
826
989
  export interface NativeNostrAccount {
827
990
  enabled: boolean;
828
991
  identity: HostedProtocolIdentity | null;
829
- capabilities: Array<'publish_original_text' | 'request_own_post_deletion' | 'reply_text_note' | 'publish_linked_text' | 'like' | 'unlike' | 'repost' | 'unrepost' | 'follow' | 'unfollow'>;
992
+ capabilities: Array<'publish_original_text' | 'request_own_post_deletion' | 'reply_text_note' | 'publish_linked_text' | 'quote' | 'like' | 'unlike' | 'repost' | 'unrepost' | 'follow' | 'unfollow'>;
830
993
  signing?: 'client';
831
994
  }
832
995
  export interface NativeNostrPostState {
@@ -1212,12 +1375,19 @@ export interface SocialRadarCluster {
1212
1375
  * or a party or politician. Hide politics (`compass: 'hide'`) leaves these out. Absent from a server before it was served.
1213
1376
  */
1214
1377
  political?: boolean;
1378
+ /** Members per source kind, counted before the member cap: `outlet` counts articles, not outlets (use `outlets`). */
1215
1379
  sources: Partial<Record<SocialRadarSourceKind, number>>;
1216
1380
  corroboration: number;
1381
+ /** Distinct outlets per lane; from 0.7.71's servers, `outlets`' two lanes. */
1217
1382
  laneMix: {
1218
1383
  mainstream: number;
1219
1384
  independent: number;
1220
1385
  };
1386
+ /**
1387
+ * How many outlets the story's coverage holds (StoryOutlets), from the members this read serves: the number its card,
1388
+ * page, the News section and World Today all name. Absent from a server older than 0.7.71.
1389
+ */
1390
+ outlets?: StoryOutlets;
1221
1391
  velocity: {
1222
1392
  perHour: number;
1223
1393
  normalized: number;
@@ -1288,7 +1458,7 @@ export interface SocialRadarPosts {
1288
1458
  /** 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). */
1289
1459
  export interface SocialRadarStoryArchive {
1290
1460
  /** The story as the radar last published it. Articles keep their headline and link; a post is a member by reference only (no `title`). */
1291
- cluster: Pick<SocialRadarCluster, 'id' | 'label' | 'kind' | 'seed' | 'theme' | 'political' | 'sources' | 'laneMix'> & {
1461
+ cluster: Pick<SocialRadarCluster, 'id' | 'label' | 'kind' | 'seed' | 'theme' | 'political' | 'sources' | 'laneMix' | 'outlets'> & {
1292
1462
  members: Array<Pick<SocialRadarMember, 'kind' | 'key' | 'url' | 'publishedAt' | 'outlet'> & {
1293
1463
  title?: string;
1294
1464
  }>;
@@ -1761,7 +1931,7 @@ export declare class ProtocolsResource {
1761
1931
  }): Promise<SingleResponse<NativeRepostReview>>;
1762
1932
  /** Reconcile the saved action without sending another native write. */
1763
1933
  nativeRepostStatus(id: string): Promise<SingleResponse<NativeRepostReview>>;
1764
- /** Prepare exact public text and reviewed images for selected connected Bluesky/ActivityPub accounts. */
1934
+ /** Prepare exact public text, reviewed images and an optional native quote for selected connected Bluesky/ActivityPub accounts. */
1765
1935
  prepareNativePost(input: NativePostPrepareInput): Promise<SingleResponse<NativePostBatch>>;
1766
1936
  /** Explicitly approve selected deliveries; never auto-retry an uncertain write. */
1767
1937
  submitNativePost(id: string, input: {
@@ -2005,6 +2175,17 @@ export declare class ProtocolsResource {
2005
2175
  * project-admin authorization as listCandidates.
2006
2176
  */
2007
2177
  ingestionStatus(): Promise<SingleResponse<ProtocolIngestionStatus>>;
2178
+ /**
2179
+ * Read counts-only ingestion health for the key's app: per lane, whether it
2180
+ * reaches done or dies part way (the open job's position, failures in a row,
2181
+ * starvation, last success and failure, attempts by reason, dead letters by
2182
+ * reason and position), plus open or counting circuits. Read-only. Needs a
2183
+ * project-bound admin key and current project-admin authority, but not the
2184
+ * content_intelligence_admin binding. Scope comes only from the key, so no
2185
+ * query is sent (the API rejects one). A section that does not answer in its
2186
+ * budget reads as null and is listed in `unavailable`.
2187
+ */
2188
+ ingestionHealth(): Promise<SingleResponse<ProtocolIngestionHealthReport>>;
2008
2189
  /** Turn the app's ingestion kill switch on: no reservation, screening or approval proceeds. */
2009
2190
  killIngestion(): Promise<SingleResponse<{
2010
2191
  kill_switch: true;
@@ -327,7 +327,7 @@ export class ProtocolsResource {
327
327
  hostedIdentityPath(id);
328
328
  return this.client.get(`/protocols/social/reposts/${encodeURIComponent(id)}`);
329
329
  }
330
- /** Prepare exact public text and reviewed images for selected connected Bluesky/ActivityPub accounts. */
330
+ /** Prepare exact public text, reviewed images and an optional native quote for selected connected Bluesky/ActivityPub accounts. */
331
331
  prepareNativePost(input) {
332
332
  return this.client.post('/protocols/social/posts/prepare', { clientMutationId: input.clientMutationId,
333
333
  text: input.text, accountIds: input.accountIds,
@@ -479,7 +479,8 @@ export class ProtocolsResource {
479
479
  prepareNativeNostr(id, input) {
480
480
  hostedIdentityPath(id);
481
481
  const body = input.action === 'publish' ? { action: input.action, content: input.content,
482
- ...(input.crossPostOf !== undefined ? { crossPostOf: input.crossPostOf } : {}) }
482
+ ...(input.crossPostOf !== undefined ? { crossPostOf: input.crossPostOf } : {}),
483
+ ...(input.quote ? { quote: { protocol: input.quote.protocol, nativeId: input.quote.nativeId } } : {}) }
483
484
  : input.action === 'reply' ? { action: input.action, content: input.content, parentId: input.parentId }
484
485
  : input.action === 'linked_post' ? { action: input.action, content: input.content,
485
486
  source: { protocol: input.source.protocol, nativeId: input.source.nativeId,
@@ -619,6 +620,19 @@ export class ProtocolsResource {
619
620
  ingestionStatus() {
620
621
  return this.client.get('/protocols/ingestion/status');
621
622
  }
623
+ /**
624
+ * Read counts-only ingestion health for the key's app: per lane, whether it
625
+ * reaches done or dies part way (the open job's position, failures in a row,
626
+ * starvation, last success and failure, attempts by reason, dead letters by
627
+ * reason and position), plus open or counting circuits. Read-only. Needs a
628
+ * project-bound admin key and current project-admin authority, but not the
629
+ * content_intelligence_admin binding. Scope comes only from the key, so no
630
+ * query is sent (the API rejects one). A section that does not answer in its
631
+ * budget reads as null and is listed in `unavailable`.
632
+ */
633
+ ingestionHealth() {
634
+ return this.client.get('/protocols/ingestion/health');
635
+ }
622
636
  /** Turn the app's ingestion kill switch on: no reservation, screening or approval proceeds. */
623
637
  killIngestion() {
624
638
  return this.client.post('/protocols/ingestion/kill', {});