saturndocs 0.1.0

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 (45) hide show
  1. package/LICENSE +21 -0
  2. package/dist/auth-FRCFGNYT.js +14 -0
  3. package/dist/brokenLinks-TTOBP4P7.js +48 -0
  4. package/dist/build-F5PWGP57.js +39 -0
  5. package/dist/chunk-3P2OFTXR.js +71 -0
  6. package/dist/chunk-3V753MRI.js +100 -0
  7. package/dist/chunk-5KX4ZK5E.js +104 -0
  8. package/dist/chunk-CMNX5DLQ.js +119 -0
  9. package/dist/chunk-GYRVGWDM.js +3499 -0
  10. package/dist/chunk-I6U4V7B4.js +1552 -0
  11. package/dist/chunk-J6P3VGGE.js +21 -0
  12. package/dist/chunk-KGJESHJC.js +37 -0
  13. package/dist/chunk-M2BZEDAS.js +57 -0
  14. package/dist/chunk-N7QPZMLP.js +82 -0
  15. package/dist/chunk-OEAEX5YW.js +51 -0
  16. package/dist/chunk-OZWTXMO3.js +584 -0
  17. package/dist/chunk-QLFODLVN.js +46 -0
  18. package/dist/chunk-R3X2DPFF.js +568 -0
  19. package/dist/chunk-S45UWCML.js +588 -0
  20. package/dist/chunk-SABK3R2P.js +2452 -0
  21. package/dist/chunk-STCCGFKC.js +246 -0
  22. package/dist/chunk-TKISMN3P.js +39 -0
  23. package/dist/chunk-U7PKMSB3.js +465 -0
  24. package/dist/chunk-VNYDYHXM.js +22 -0
  25. package/dist/chunk-VXEUNRVA.js +26 -0
  26. package/dist/chunk-XO4CUC7V.js +99 -0
  27. package/dist/cli.d.ts +1 -0
  28. package/dist/cli.js +95 -0
  29. package/dist/deploy-QU7DCKWU.js +11 -0
  30. package/dist/dev-VDSDCBO2.js +43 -0
  31. package/dist/index.d.ts +382 -0
  32. package/dist/index.js +191 -0
  33. package/dist/init-7T5TEPBH.js +14 -0
  34. package/dist/manage-MKH27U3B.js +12 -0
  35. package/dist/openapiCheck-KGBQJTIQ.js +66 -0
  36. package/dist/pages-HIPXRLSY.js +15 -0
  37. package/dist/read-DMSJUEDA.js +160 -0
  38. package/dist/read-ZEVUZNNB.js +146 -0
  39. package/dist/requests-H5MIGBIS.js +13 -0
  40. package/dist/schema-XJUEZSIW.js +13 -0
  41. package/dist/sites-I4WI2MPG.js +14 -0
  42. package/dist/status-7R3NOI5H.js +14 -0
  43. package/dist/suggestions-4WFQ3APX.js +739 -0
  44. package/dist/validate-VXL4PL42.js +30 -0
  45. package/package.json +68 -0
@@ -0,0 +1,3499 @@
1
+ // ../management-client/dist/index.js
2
+ import { z as z3 } from "zod";
3
+ import { z as z22 } from "zod";
4
+ import { z as z32 } from "zod";
5
+
6
+ // ../owner-contract/dist/index.js
7
+ import { z as z2 } from "zod";
8
+
9
+ // ../control-abi/dist/index.js
10
+ var HOSTNAME_TRANSITIONS = Object.freeze({ unclaimed: Object.freeze(["claimed"]), claimed: Object.freeze(["unclaimed"]) });
11
+ var OWNER_NOTIFICATION_EVENTS = [
12
+ "suggestion_created",
13
+ "suggestion_published",
14
+ "analysis_failed",
15
+ "release_failed",
16
+ /** A revision the reviewer asked for, or a hand edit, is back in the queue (#151, #154). */
17
+ "revision_landed",
18
+ /** An open question the agent raised has gone unanswered for long enough that the session nudged (F-12.13). */
19
+ "question_unanswered"
20
+ ];
21
+ var OWNER_NOTIFICATION_MODES = ["immediate", "off"];
22
+ var MAX_QUESTION_ID_CHARS = 64;
23
+ var SUGGESTION_DISMISS_REASONS = ["not_needed", "wrong_pages", "inaccurate", "manual", "other"];
24
+ var REQUEST_THREAD_AUTHORS = ["owner", "agent", "platform"];
25
+ var REQUEST_THREAD_KINDS = ["request", "understood", "question", "answer", "draft", "no_change", "failed"];
26
+ var GIT_CONNECTION_TRANSITIONS = Object.freeze({ active: Object.freeze(["broken"]), broken: Object.freeze(["active"]) });
27
+ var GIT_CONNECTION_UNIQUENESS = Object.freeze({ activeSite: ["siteId"], activeSource: ["provider", "installationId", "repositoryId", "branch", "docsRoot"] });
28
+ var DEFAULT_SKIP_LABELS = Object.freeze(["no-docs", "skip-docs"]);
29
+ var DEFAULT_IGNORE_GLOBS = Object.freeze(["**/*.test.*", "**/__tests__/**", "**/*.lock", "**/node_modules/**", ".github/**"]);
30
+ var DEFAULT_SKIP_AUTHORS = Object.freeze(["*[bot]"]);
31
+ var ENFORCEMENT = Object.freeze({ ownership: "d1_transaction", heads: "d1_transaction", hostname: "d1_transaction", gitConnections: "d1_transaction", webhookDeliveries: "d1_transaction", tombstone: "d1_transaction", jobRelations: "d1_transaction", records: "d1_transaction", sourceBlobs: "r2_ordering", releaseObjects: "r2_ordering" });
32
+
33
+ // ../owner-contract/dist/index.js
34
+ import { z } from "zod";
35
+ var MANAGEMENT_SCOPES = ["manage:read", "manage:write", "manage:approve", "manage:access"];
36
+ var ManagementCredential = z.object({
37
+ token_id: z.string(),
38
+ site_id: z.string(),
39
+ label: z.string(),
40
+ scopes: z.array(z.enum(MANAGEMENT_SCOPES)),
41
+ status: z.enum(["active", "revoked", "expired", "inactive"]),
42
+ issuer: z.string(),
43
+ parent_token_id: z.string().nullable(),
44
+ created_at: z.string(),
45
+ expires_at: z.string(),
46
+ last_used_at: z.string().nullable()
47
+ });
48
+ var ManagementCredentials = z.object({ site_id: z.string(), credentials: z.array(ManagementCredential) });
49
+ var CreateManagementCredential = z.object({
50
+ label: z.string().trim().min(1).max(80),
51
+ scopes: z.array(z.enum(MANAGEMENT_SCOPES)).min(1).max(4).refine((scopes) => new Set(scopes).size === scopes.length, "Permissions must be unique"),
52
+ expires_at: z.iso.datetime()
53
+ }).strict();
54
+ var IssuedManagementCredential = z.object({ credential: ManagementCredential, token: z.string() });
55
+ var OWNER_PREFIX = "/__gs-portal";
56
+ var MAX_SESSION_EVIDENCE = 20;
57
+ var OWNER_PATHS = {
58
+ login: `${OWNER_PREFIX}/login`,
59
+ magicRequest: `${OWNER_PREFIX}/magic/request`,
60
+ magicVerify: `${OWNER_PREFIX}/magic/verify`,
61
+ logout: `${OWNER_PREFIX}/logout`,
62
+ switch: `${OWNER_PREFIX}/switch`,
63
+ site: `${OWNER_PREFIX}/site`,
64
+ sites: `${OWNER_PREFIX}/sites`,
65
+ suggestions: `${OWNER_PREFIX}/suggestions`,
66
+ suggestion: (id) => `${OWNER_PREFIX}/suggestions/${encodeURIComponent(id)}`,
67
+ suggestionAction: (id, action) => `${OWNER_PREFIX}/suggestions/${encodeURIComponent(id)}/${action}`,
68
+ suggestionSession: (id) => `${OWNER_PREFIX}/suggestions/${encodeURIComponent(id)}/session`,
69
+ suggestionSessionSocket: (id) => `${OWNER_PREFIX}/suggestions/${encodeURIComponent(id)}/session/ws`,
70
+ requests: `${OWNER_PREFIX}/requests`,
71
+ requestRetry: (jobId) => `${OWNER_PREFIX}/requests/${encodeURIComponent(jobId)}/retry`,
72
+ requestThread: (jobId) => `${OWNER_PREFIX}/requests/${encodeURIComponent(jobId)}/thread`,
73
+ suggestionPreview: (id) => `${OWNER_PREFIX}/suggestions/${encodeURIComponent(id)}/preview`,
74
+ activity: `${OWNER_PREFIX}/activity`,
75
+ activitySummary: `${OWNER_PREFIX}/activity/summary`,
76
+ analytics: `${OWNER_PREFIX}/analytics`,
77
+ gaps: `${OWNER_PREFIX}/gaps`,
78
+ gapWork: `${OWNER_PREFIX}/gaps/work`,
79
+ pages: `${OWNER_PREFIX}/pages`,
80
+ releases: `${OWNER_PREFIX}/releases`,
81
+ metrics: `${OWNER_PREFIX}/metrics`,
82
+ domains: `${OWNER_PREFIX}/domains`,
83
+ domain: (hostname) => `${OWNER_PREFIX}/domains/${encodeURIComponent(hostname)}`,
84
+ readers: `${OWNER_PREFIX}/readers`,
85
+ reader: (entry) => `${OWNER_PREFIX}/readers/${encodeURIComponent(entry)}`,
86
+ readersRotate: `${OWNER_PREFIX}/readers/rotate`,
87
+ buildRetry: `${OWNER_PREFIX}/builds/retry`,
88
+ build: `${OWNER_PREFIX}/builds`,
89
+ notifications: `${OWNER_PREFIX}/notifications`,
90
+ owners: `${OWNER_PREFIX}/owners`,
91
+ owner: (email) => `${OWNER_PREFIX}/owners/${encodeURIComponent(email)}`,
92
+ settingsProposal: `${OWNER_PREFIX}/settings/proposal`,
93
+ settingsApprove: `${OWNER_PREFIX}/settings/proposal/approve`,
94
+ settingsUpdate: `${OWNER_PREFIX}/settings/proposal/update`,
95
+ settingsGeneral: `${OWNER_PREFIX}/settings/general`,
96
+ settingsInstructions: `${OWNER_PREFIX}/settings/instructions`,
97
+ settingsAssistant: `${OWNER_PREFIX}/settings/assistant`,
98
+ docsRepo: `${OWNER_PREFIX}/docs-repo`,
99
+ docsRepoReconnect: `${OWNER_PREFIX}/docs-repo/reconnect`,
100
+ productConnections: `${OWNER_PREFIX}/product-connections`,
101
+ productConnection: (id) => `${OWNER_PREFIX}/product-connections/${encodeURIComponent(id)}`,
102
+ productConnectionAction: (id, action) => `${OWNER_PREFIX}/product-connections/${encodeURIComponent(id)}/${action}`,
103
+ productConnectionAnalyse: (id) => `${OWNER_PREFIX}/product-connections/${encodeURIComponent(id)}/analyse`,
104
+ githubInstall: `${OWNER_PREFIX}/github/install`,
105
+ githubSetup: `${OWNER_PREFIX}/github/setup`,
106
+ githubRepositories: `${OWNER_PREFIX}/github/repositories`,
107
+ slackInstall: `${OWNER_PREFIX}/slack/install`
108
+ };
109
+ var SESSION_TURN_ID = z2.string().min(1).max(256);
110
+ var SESSION_FRAME_SEQUENCE = z2.number().int().nonnegative();
111
+ var SessionSocketStart = z2.object({
112
+ v: z2.literal(1),
113
+ type: z2.literal("start"),
114
+ turnId: SESSION_TURN_ID,
115
+ message: z2.string().trim().min(1).max(2e3),
116
+ questionId: z2.string().trim().min(1).max(128).nullable().optional()
117
+ });
118
+ var SessionSocketResume = z2.object({
119
+ v: z2.literal(1),
120
+ type: z2.literal("resume"),
121
+ turnId: SESSION_TURN_ID,
122
+ after: SESSION_FRAME_SEQUENCE
123
+ });
124
+ var SessionSocketClientFrame = z2.discriminatedUnion("type", [SessionSocketStart, SessionSocketResume]);
125
+ var SessionEvidence = z2.object({
126
+ document: z2.string().min(1).max(1e3),
127
+ title: z2.string().min(1).max(1e3),
128
+ url: z2.string().min(1).max(4e3)
129
+ });
130
+ var SessionSocketDelta = z2.object({
131
+ v: z2.literal(1),
132
+ type: z2.literal("delta"),
133
+ turnId: SESSION_TURN_ID,
134
+ seq: SESSION_FRAME_SEQUENCE,
135
+ text: z2.string().min(1)
136
+ });
137
+ var SessionSocketReset = z2.object({
138
+ v: z2.literal(1),
139
+ type: z2.literal("reset"),
140
+ turnId: SESSION_TURN_ID,
141
+ seq: SESSION_FRAME_SEQUENCE
142
+ });
143
+ var SessionSocketDone = z2.object({
144
+ v: z2.literal(1),
145
+ type: z2.literal("done"),
146
+ turnId: SESSION_TURN_ID,
147
+ seq: SESSION_FRAME_SEQUENCE,
148
+ answer: z2.string().max(2e3),
149
+ evidence: z2.array(SessionEvidence).max(MAX_SESSION_EVIDENCE),
150
+ startedRevision: z2.boolean()
151
+ });
152
+ var SessionSocketError = z2.object({
153
+ v: z2.literal(1),
154
+ type: z2.literal("error"),
155
+ turnId: SESSION_TURN_ID,
156
+ code: z2.enum(["conflict", "invalid", "interrupted", "unavailable"]),
157
+ retryable: z2.boolean(),
158
+ message: z2.string()
159
+ });
160
+ var SessionSocketServerFrame = z2.discriminatedUnion("type", [SessionSocketDelta, SessionSocketReset, SessionSocketDone, SessionSocketError]);
161
+ var SUGGESTION_TABS = ["pending", "revising", "published", "dismissed", "failed"];
162
+ var SUGGESTION_ROW_STATES = ["pending_review", "changes_requested", "revising", "publishing", "published", "dismissed", "failed", "superseded"];
163
+ var MERGE_MODES = ["review", "slack_approval", "autonomous"];
164
+ var CONNECTION_STATES = ["active", "paused", "broken"];
165
+ var CONNECTION_BROKEN_REASONS = ["uninstalled", "repository_transferred", "branch_deleted"];
166
+ var INSTALLATION_HEALTH = ["healthy", "stale", "broken"];
167
+ var SKIP_REASONS = ["no_matching_connection", "branch_mismatch", "connection_paused", "connection_broken", "no_watched_paths", "deleted_ref", "bot_author", "skip_label", "invalid_sha"];
168
+ var ACTIVITY_OPERATIONS = ["product_push", "manual_request", "git_push", "rebuild", "bundle_upload"];
169
+ var CONFIDENCES = ["high", "medium", "low"];
170
+ var TRIAGES = ["user_facing", "internal", "unclear"];
171
+ var PAGE_CHANGE_OPS = ["create", "update", "delete"];
172
+ var MAX_REQUEST_CHANGES_NOTE_CHARS = 4e3;
173
+ var MAX_SESSION_MESSAGE_CHARS = 2e3;
174
+ var MAX_INSTRUCTIONS_CHARS = 8e3;
175
+ var isoString = z2.string();
176
+ var nullableString = z2.string().nullable();
177
+ var sha = z2.string().regex(/^[0-9a-f]{40}$/u);
178
+ var ErrorEnvelope = z2.object({
179
+ error: z2.string(),
180
+ message: z2.string().optional()
181
+ });
182
+ var ActiveRelease = z2.object({
183
+ release_id: z2.string(),
184
+ state: z2.string(),
185
+ generation: z2.number().int(),
186
+ activated_at: nullableString
187
+ });
188
+ var LatestJob = z2.object({
189
+ state: z2.string(),
190
+ error_code: nullableString,
191
+ error_message: nullableString,
192
+ updated_at: isoString
193
+ });
194
+ var UsageMeter = z2.object({ used: z2.number(), limit: z2.number() });
195
+ var PortalUsage = z2.object({
196
+ analyses_this_month: UsageMeter.optional(),
197
+ storage_bytes: UsageMeter.optional(),
198
+ concurrent_jobs: UsageMeter.optional()
199
+ });
200
+ var SlackConnection = z2.object({
201
+ team_id: z2.string(),
202
+ channel_id: z2.string(),
203
+ connected_at: nullableString,
204
+ // True for a connection installed before the app could read who clicks; every click is refused until Slack is reconnected.
205
+ needs_reconnect: z2.boolean().optional()
206
+ });
207
+ var Installation = z2.object({
208
+ installation_id: z2.number().int(),
209
+ account_login: nullableString,
210
+ state: z2.string(),
211
+ last_delivery_at: nullableString,
212
+ health: z2.enum(INSTALLATION_HEALTH),
213
+ linked: z2.boolean().optional(),
214
+ reinstall_url: nullableString
215
+ });
216
+ var DocsRepo = z2.object({
217
+ installation_id: z2.number().int(),
218
+ repository_id: z2.number().int(),
219
+ repository: nullableString,
220
+ branch: z2.string(),
221
+ docs_root: z2.string(),
222
+ state: z2.enum(["active", "broken"]),
223
+ broken_reason: nullableString
224
+ });
225
+ var ProductConnection = z2.object({
226
+ connection_id: z2.string(),
227
+ installation_id: z2.number().int(),
228
+ repository: nullableString,
229
+ branch: z2.string(),
230
+ watch_globs: z2.array(z2.string()),
231
+ state: z2.enum(CONNECTION_STATES),
232
+ broken_reason: z2.enum(CONNECTION_BROKEN_REASONS).nullable(),
233
+ merge_mode: z2.enum(MERGE_MODES),
234
+ last_event_at: nullableString
235
+ });
236
+ var ProductConnectionRules = ProductConnection.omit({ last_event_at: true }).extend({
237
+ last_event_at: nullableString.optional(),
238
+ repository_id: z2.number().int(),
239
+ ignore_globs: z2.array(z2.string()),
240
+ skip_authors: z2.array(z2.string()),
241
+ skip_labels: z2.array(z2.string()),
242
+ connection_generation: z2.number().int()
243
+ });
244
+ var OWNER_ROLES = ["owner", "viewer"];
245
+ var OwnerRole = z2.enum(OWNER_ROLES);
246
+ var PortalSite = z2.object({
247
+ site_id: z2.string(),
248
+ role: OwnerRole.optional(),
249
+ hostnames: z2.array(z2.string()),
250
+ active_release: ActiveRelease.nullable(),
251
+ latest_job: LatestJob.nullable(),
252
+ latest_authoring_job: LatestJob.nullable().optional(),
253
+ reader_status: z2.enum(["published", "unpublished", "unknown"]).optional(),
254
+ connections: z2.array(ProductConnection),
255
+ docs_repo: DocsRepo.nullable(),
256
+ installations: z2.array(Installation).optional(),
257
+ slack: SlackConnection.nullable().optional(),
258
+ usage: PortalUsage.optional()
259
+ });
260
+ var SiteSummary = z2.object({ site_id: z2.string(), primary_host: z2.string().nullable() });
261
+ var SitesResponse = z2.object({ current: z2.string(), sites: z2.array(SiteSummary) });
262
+ var RationaleEntry = z2.object({ change: z2.string(), affects: z2.array(z2.string()) });
263
+ var PreviewBlock = z2.object({
264
+ kind: z2.enum(["title", "heading", "text", "code", "callout", "unchanged"]),
265
+ op: z2.enum(["added", "modified", "removed"]).nullable().optional(),
266
+ text: z2.string().optional(),
267
+ subtitle: nullableString.optional(),
268
+ label: nullableString.optional(),
269
+ previously: nullableString.optional(),
270
+ count: z2.number().int().optional()
271
+ });
272
+ var DiffLine = z2.object({
273
+ op: z2.enum(["context", "add", "del", "skip"]),
274
+ before_line: z2.number().int().nullable().optional(),
275
+ after_line: z2.number().int().nullable().optional(),
276
+ text: z2.string().optional(),
277
+ count: z2.number().int().optional()
278
+ });
279
+ var PageChange = z2.object({
280
+ path: z2.string(),
281
+ op: z2.enum(PAGE_CHANGE_OPS),
282
+ reason: nullableString.optional(),
283
+ url: nullableString.optional(),
284
+ before: nullableString.optional(),
285
+ after: nullableString.optional(),
286
+ added_lines: z2.number().int().optional(),
287
+ removed_lines: z2.number().int().optional(),
288
+ blocks: z2.array(PreviewBlock).optional(),
289
+ diff: z2.array(DiffLine).optional(),
290
+ truncated: z2.boolean().optional()
291
+ });
292
+ var ReferencedPage = z2.object({
293
+ title: z2.string(),
294
+ url: z2.string(),
295
+ note: nullableString.optional(),
296
+ changed: z2.boolean().optional()
297
+ });
298
+ var ThreadEntry = z2.object({
299
+ entry_id: z2.string(),
300
+ kind: z2.enum(["note", "revision", "event"]),
301
+ author: nullableString.optional(),
302
+ body: nullableString.optional(),
303
+ revision: z2.number().int(),
304
+ created_at: isoString,
305
+ evidence: z2.array(SessionEvidence).max(MAX_SESSION_EVIDENCE).optional()
306
+ });
307
+ var RevisionEntry = z2.object({
308
+ n: z2.number().int(),
309
+ label: z2.string(),
310
+ sub: nullableString.optional(),
311
+ current: z2.boolean().optional()
312
+ });
313
+ var Provenance = z2.object({
314
+ outcome: z2.string(),
315
+ triage: z2.enum(TRIAGES).nullable().optional(),
316
+ confidence: z2.enum(CONFIDENCES).nullable().optional(),
317
+ reason: z2.string(),
318
+ model: nullableString.optional(),
319
+ prompt_tokens: z2.number().int().nullable().optional(),
320
+ completion_tokens: z2.number().int().nullable().optional(),
321
+ duration_ms: z2.number().int().nullable().optional(),
322
+ log: z2.array(z2.string()),
323
+ created_at: isoString,
324
+ analysis_job_id: nullableString.optional(),
325
+ analysis_revision: z2.number().int().nullable().optional()
326
+ });
327
+ var SuggestionPublication = z2.object({
328
+ job_id: z2.string(),
329
+ state: z2.string(),
330
+ error_code: nullableString,
331
+ error_message: nullableString,
332
+ release_id: nullableString,
333
+ generation: z2.number().int().nullable(),
334
+ activated_at: nullableString
335
+ });
336
+ var SuggestionRecord = z2.object({
337
+ publication: SuggestionPublication.nullable().optional(),
338
+ request_thread_id: nullableString.optional(),
339
+ suggestion_id: z2.string(),
340
+ pr_number: z2.number().int().nullable(),
341
+ head_branch: z2.string(),
342
+ base_sha: z2.string(),
343
+ state: z2.enum(SUGGESTION_ROW_STATES),
344
+ created_at: isoString,
345
+ updated_at: isoString,
346
+ title: nullableString.optional(),
347
+ summary: nullableString.optional(),
348
+ repository: nullableString.optional(),
349
+ repository_id: z2.number().int().nullable().optional(),
350
+ commit_sha: nullableString.optional(),
351
+ source_pr_number: z2.number().int().nullable().optional(),
352
+ request: nullableString.optional(),
353
+ requested_by: nullableString.optional(),
354
+ confidence: z2.enum(CONFIDENCES).nullable().optional(),
355
+ revision: z2.number().int().nullable().optional(),
356
+ pages_count: z2.number().int().nullable().optional(),
357
+ first_path: nullableString.optional(),
358
+ job_id: nullableString.optional(),
359
+ error_code: nullableString.optional(),
360
+ error_message: nullableString.optional(),
361
+ reviewer: nullableString.optional(),
362
+ head_sha: nullableString.optional(),
363
+ head_edited_by: nullableString.optional(),
364
+ files_changed_url: nullableString.optional(),
365
+ snapshots_available: z2.boolean().optional(),
366
+ merge_commit_sha: nullableString.optional(),
367
+ failure_reason: nullableString.optional(),
368
+ /** Which step failed. A `revision` failure must not offer a merge retry (#360). */
369
+ failure_kind: z2.enum(["merge", "rebase", "revision", "build"]).nullable().optional(),
370
+ dismiss_reason: nullableString.optional(),
371
+ dismiss_note: nullableString.optional(),
372
+ pages: z2.array(PageChange).optional(),
373
+ rationale: z2.array(RationaleEntry).optional(),
374
+ open_questions: z2.array(z2.string()).optional(),
375
+ questions: z2.array(z2.object({
376
+ question_id: z2.string(),
377
+ body: z2.string(),
378
+ origin: z2.enum(["agent", "verifier", "lint"]),
379
+ answered_at: isoString.nullable(),
380
+ answer_entry_id: nullableString,
381
+ outcome: z2.enum(["talk", "revision"]).nullable()
382
+ })).optional(),
383
+ references: z2.array(ReferencedPage).optional(),
384
+ thread: z2.array(ThreadEntry).optional(),
385
+ revisions: z2.array(RevisionEntry).optional(),
386
+ provenance: Provenance.nullable().optional(),
387
+ merge_mode: z2.enum(MERGE_MODES).nullable().optional()
388
+ });
389
+ var SuggestionCounts = z2.object({
390
+ pending: z2.number().int(),
391
+ revising: z2.number().int(),
392
+ published: z2.number().int(),
393
+ dismissed: z2.number().int(),
394
+ failed: z2.number().int()
395
+ });
396
+ var SuggestionsQuery = z2.object({
397
+ state: z2.enum(SUGGESTION_TABS).default("pending"),
398
+ cursor: z2.string().optional(),
399
+ limit: z2.coerce.number().int().positive().optional()
400
+ });
401
+ var SuggestionsPage = z2.object({
402
+ site_id: z2.string(),
403
+ state: z2.enum(SUGGESTION_TABS),
404
+ counts: SuggestionCounts,
405
+ suggestions: z2.array(SuggestionRecord),
406
+ next_cursor: nullableString
407
+ });
408
+ var SuggestionDetailResponse = z2.object({ site_id: z2.string(), suggestion: SuggestionRecord });
409
+ var ExpectedHashes = z2.object({
410
+ expected_base_sha: sha.optional(),
411
+ expected_head_sha: sha.optional()
412
+ });
413
+ var DismissBody = ExpectedHashes.extend({
414
+ reason: z2.enum(SUGGESTION_DISMISS_REASONS),
415
+ note: z2.string().max(MAX_REQUEST_CHANGES_NOTE_CHARS).nullable().optional()
416
+ });
417
+ var RequestChangesBody = z2.object({ note: z2.string().min(1).max(MAX_REQUEST_CHANGES_NOTE_CHARS) });
418
+ var RetryBody = z2.object({});
419
+ var SessionMessageBody = z2.object({
420
+ message: z2.string().min(1).max(MAX_SESSION_MESSAGE_CHARS),
421
+ question_id: z2.string().min(1).max(MAX_QUESTION_ID_CHARS).optional()
422
+ });
423
+ var SessionResponse = z2.object({
424
+ answer: z2.string().min(1).max(2e3),
425
+ evidence: z2.array(SessionEvidence).max(MAX_SESSION_EVIDENCE),
426
+ startedRevision: z2.boolean()
427
+ });
428
+ var SuggestionThreadEntry = z2.object({
429
+ entry_id: z2.string(),
430
+ kind: z2.string(),
431
+ author: nullableString,
432
+ body: z2.string(),
433
+ revision: z2.number().int(),
434
+ created_at: isoString,
435
+ evidence: z2.array(SessionEvidence).max(MAX_SESSION_EVIDENCE).optional()
436
+ });
437
+ var SuggestionThread = z2.object({ entries: z2.array(SuggestionThreadEntry) });
438
+ var ActivityRelease = z2.object({ release_id: z2.string(), generation: z2.number().int().nullable() });
439
+ var ActivityEvent = z2.object({
440
+ id: z2.string(),
441
+ operation: z2.enum(ACTIVITY_OPERATIONS),
442
+ state: z2.string(),
443
+ outcome_code: nullableString,
444
+ error_code: nullableString,
445
+ error_message: nullableString,
446
+ repository: nullableString,
447
+ commit_sha: nullableString,
448
+ commit_message: nullableString,
449
+ actor: nullableString,
450
+ suggestion_id: nullableString,
451
+ pr_number: z2.number().int().nullable(),
452
+ triage: z2.enum(TRIAGES).nullable().optional(),
453
+ pages_considered: nullableString.optional(),
454
+ skip_reason: z2.enum(SKIP_REASONS).nullable().optional(),
455
+ reason: nullableString.optional(),
456
+ release: ActivityRelease.nullable().optional(),
457
+ pusher: nullableString.optional(),
458
+ created_at: isoString,
459
+ updated_at: isoString
460
+ });
461
+ var ACTIVITY_OUTCOME_FILTERS = ["suggestion", "nochange", "skipped", "failed", "published"];
462
+ var ActivitySummary = z2.object({ site_id: z2.string(), since: isoString, until: isoString, total: z2.number().int().nonnegative(), counts: z2.record(z2.string(), z2.number().int().nonnegative()) });
463
+ var ActivityQuery = z2.object({
464
+ outcome: z2.enum(ACTIVITY_OUTCOME_FILTERS).optional(),
465
+ repo: z2.string().min(1).max(200).optional(),
466
+ from: z2.iso.date().optional(),
467
+ to: z2.iso.date().optional(),
468
+ cursor: z2.string().optional(),
469
+ limit: z2.coerce.number().int().positive().optional()
470
+ });
471
+ var AnalyticsTotals = z2.object({
472
+ page_views: z2.number().int().nonnegative(),
473
+ agent_reads: z2.number().int().nonnegative(),
474
+ assistant_questions: z2.number().int().nonnegative(),
475
+ not_found: z2.number().int().nonnegative()
476
+ });
477
+ var AnalyticsResponse = z2.object({
478
+ site_id: z2.string(),
479
+ days: z2.union([z2.literal(7), z2.literal(30), z2.literal(90)]),
480
+ totals: AnalyticsTotals,
481
+ previous: AnalyticsTotals.nullable(),
482
+ series: z2.array(z2.object({ day: z2.string(), page_views: z2.number().int().nonnegative(), agent_reads: z2.number().int().nonnegative(), assistant_questions: z2.number().int().nonnegative() })),
483
+ top_pages: z2.array(z2.object({ path: z2.string(), count: z2.number().int().nonnegative() })),
484
+ top_missing: z2.array(z2.object({ path: z2.string(), count: z2.number().int().nonnegative() })),
485
+ ratings: z2.object({ count: z2.number().int().nonnegative(), positive: z2.number().int().nonnegative() }),
486
+ assistant: z2.object({ answered: z2.number().int().nonnegative(), unanswered: z2.number().int().nonnegative() }).optional(),
487
+ top_unanswered: z2.array(z2.object({ path: z2.string(), count: z2.number().int().nonnegative() })).optional()
488
+ });
489
+ var GapWork = z2.object({
490
+ key: z2.string().regex(/^[a-f0-9]{64}$/u),
491
+ source_key: z2.string().min(1).max(8192),
492
+ state: z2.enum(["open", "in_progress", "resolved", "ignored"]),
493
+ classification: z2.enum(["auto", "actionable", "noise"]),
494
+ note: z2.string().max(2e3),
495
+ version: z2.number().int().nonnegative(),
496
+ request_epoch: z2.number().int().nonnegative(),
497
+ updated_by: z2.string().nullable(),
498
+ updated_at: z2.string().nullable(),
499
+ submission_id: z2.string(),
500
+ request_id: z2.string().nullable()
501
+ });
502
+ var GapWorkWrite = GapWork.pick({ key: true, source_key: true, state: true, classification: true, note: true }).extend({
503
+ expected_version: z2.number().int().nonnegative(),
504
+ restart_request: z2.boolean().optional()
505
+ });
506
+ var GapWorkPage = z2.object({ site_id: z2.string(), items: z2.array(GapWork), next_cursor: z2.string().nullable() });
507
+ var GapWorkResponse = z2.object({ site_id: z2.string(), item: GapWork });
508
+ var GapsResponse = z2.object({
509
+ site_id: z2.string(),
510
+ days: z2.union([z2.literal(7), z2.literal(30), z2.literal(90)]),
511
+ /** Whether the edge figures were read; the records below are present either way. */
512
+ traffic: z2.enum(["ready", "not_configured", "failed"]),
513
+ /** The most requested addresses first; `_truncated` says more were requested than are shown. */
514
+ /** @deprecated Missing-address discovery was removed; servers return an empty array. */
515
+ missing_pages: z2.array(z2.object({ path: z2.string(), hits: z2.number().int().nonnegative(), last_seen: z2.string() })),
516
+ missing_pages_truncated: z2.boolean(),
517
+ work: z2.array(GapWork).optional(),
518
+ /** @deprecated Missing-address classification was removed; servers return an empty array. */
519
+ scanner_noise: z2.array(z2.object({ path: z2.string(), reason: z2.string() })).optional(),
520
+ unanswered_next_cursor: z2.string().nullable().optional(),
521
+ feedback_next_cursor: z2.string().nullable().optional(),
522
+ ratings_truncated: z2.boolean().optional(),
523
+ /** `from` is the page of this site readers were observed coming from; a redirect may sit between it and `to`. */
524
+ broken_links: z2.array(z2.object({ from: z2.string(), to: z2.string(), hits: z2.number().int().nonnegative(), last_seen: z2.string() })),
525
+ broken_links_truncated: z2.boolean(),
526
+ unanswered: z2.array(z2.object({ id: z2.string(), path: z2.string(), question: z2.string(), asked_at: z2.string() })),
527
+ feedback: z2.array(z2.object({ id: z2.string(), path: z2.string(), score: z2.union([z2.literal(0), z2.literal(1)]), comment: nullableString, created_at: z2.string() })),
528
+ /** Every rating in the range by page, the pages readers found least helpful first. */
529
+ ratings: z2.array(z2.object({ path: z2.string(), helpful: z2.number().int().nonnegative(), not_helpful: z2.number().int().nonnegative() }))
530
+ });
531
+ var UpdateRequestEvidence = z2.object({
532
+ gap_source: z2.string().min(1).max(8192).optional(),
533
+ gap_key: z2.string().regex(/^[a-f0-9]{64}$/u).optional(),
534
+ gap_epoch: z2.number().int().nonnegative().optional(),
535
+ kind: z2.enum(["missing_page", "broken_link", "unanswered_question", "page_feedback"]),
536
+ text: z2.string().min(1).max(1e3),
537
+ path: z2.string().max(400).nullable()
538
+ });
539
+ var PagesResponse = z2.union([
540
+ z2.object({ site_id: z2.string(), days: z2.union([z2.literal(7), z2.literal(30), z2.literal(90)]), unavailable: z2.literal("no_release") }),
541
+ z2.object({
542
+ site_id: z2.string(),
543
+ days: z2.union([z2.literal(7), z2.literal(30), z2.literal(90)]),
544
+ /** `partial` when the edge had more address counts than one answer holds, so some reads are missing from the counts. */
545
+ traffic: z2.enum(["ready", "partial", "not_configured", "failed"]),
546
+ release_id: z2.string(),
547
+ pages: z2.array(z2.object({
548
+ /** The page's source path in the docs repository, under the pages directory. */
549
+ path: z2.string(),
550
+ /** Where readers read it on the site. */
551
+ url: z2.string(),
552
+ /** The source on GitHub at the commit the live release was built from; null when the site is not connected that way. */
553
+ source_url: nullableString,
554
+ views: z2.number().int().nonnegative(),
555
+ agent_reads: z2.number().int().nonnegative()
556
+ }))
557
+ })
558
+ ]);
559
+ var ReleaseRecord = z2.object({
560
+ release_id: z2.string(),
561
+ state: z2.string(),
562
+ built_at: z2.string(),
563
+ /** The digest of the release's object manifest; null until the build was verified. */
564
+ digest: nullableString,
565
+ source_revision_id: z2.string(),
566
+ git_commit: nullableString,
567
+ /** When readers last started seeing it, from the activation record; null when it never went live. */
568
+ activated_at: nullableString,
569
+ generation: z2.number().int().nullable(),
570
+ job: z2.object({ job_id: z2.string(), operation: nullableString, commit_sha: nullableString, requested_by: nullableString })
571
+ });
572
+ var ReleasesPage = z2.object({
573
+ site_id: z2.string(),
574
+ active_release_id: nullableString,
575
+ active_release_status: z2.enum(["known", "unknown"]).optional(),
576
+ releases: z2.array(ReleaseRecord),
577
+ next_cursor: nullableString
578
+ });
579
+ var UpdateRequestBody = z2.object({
580
+ submission_id: z2.string().uuid(),
581
+ request: z2.string().min(1).max(4e3),
582
+ page: z2.string().max(400).optional(),
583
+ /** What the site recorded a reader hitting, sent beside the request and kept apart from it: a report, never an instruction. */
584
+ evidence: UpdateRequestEvidence.optional(),
585
+ /** The root job of the thread these words answer. Absent starts a new request; a thread not waiting on an answer is refused with `thread_closed`. */
586
+ thread: z2.string().max(200).optional()
587
+ });
588
+ var UpdateRequestResponse = z2.object({
589
+ job_id: z2.string(),
590
+ state: z2.literal("queued")
591
+ });
592
+ var REQUEST_STATES = ["queued", "running", "succeeded", "failed", "timed_out", "cancelled"];
593
+ var RequestRecord = z2.object({
594
+ job_id: z2.string(),
595
+ /** The owner's own words, as they were sent. */
596
+ request: z2.string(),
597
+ page: nullableString,
598
+ requested_by: nullableString,
599
+ state: z2.enum(REQUEST_STATES),
600
+ outcome_code: nullableString,
601
+ error_code: nullableString,
602
+ error_message: nullableString,
603
+ /** The sentence the agent wrote about this request; null until it has concluded something. */
604
+ reason: nullableString,
605
+ suggestion_id: nullableString,
606
+ pr_number: z2.number().int().nullable(),
607
+ attempt: z2.number().int().nullable(),
608
+ /** The job this conversation started with; its own id when the request started one. Rows that share it are one thread. */
609
+ thread_id: z2.string(),
610
+ created_at: isoString,
611
+ updated_at: isoString
612
+ });
613
+ var RequestThreadEntry = z2.object({
614
+ entry_id: z2.string(),
615
+ job_id: z2.string(),
616
+ seq: z2.number().int(),
617
+ /** Which run of that job wrote the turn. A retry keeps the job's id, so this is the only thing telling two attempts apart. */
618
+ attempt: z2.number().int(),
619
+ author: z2.enum(REQUEST_THREAD_AUTHORS),
620
+ kind: z2.enum(REQUEST_THREAD_KINDS),
621
+ body: z2.string(),
622
+ /** Set on a `draft` entry: the suggestion the request produced. */
623
+ suggestion_id: nullableString,
624
+ created_at: isoString
625
+ });
626
+ var RequestThreadSummary = z2.object({
627
+ thread_id: z2.string(),
628
+ /** The job the conversation started with, whose words name it. */
629
+ root: RequestRecord,
630
+ /** The newest job of the thread, whose state and outcome the card reports. */
631
+ latest: RequestRecord,
632
+ /** True only while the last turn is a question, which is the one state an owner may reply in. */
633
+ open: z2.boolean(),
634
+ /** How many turns the conversation has, including any older than the ones carried here. */
635
+ turns: z2.number().int().nonnegative(),
636
+ /** The newest turns, oldest first, with the words that started the thread kept whatever else falls outside. */
637
+ entries: z2.array(RequestThreadEntry)
638
+ });
639
+ var RequestsPage = z2.object({
640
+ site_id: z2.string(),
641
+ /** Newest original request first; replies never move a conversation between pages. */
642
+ threads: z2.array(RequestThreadSummary),
643
+ page_cursor: nullableString,
644
+ next_cursor: nullableString,
645
+ has_newer: z2.boolean(),
646
+ /** Every request of this site that failed or timed out, and every conversation waiting on an answer. Read only for a caller that asked for them. */
647
+ failed: z2.number().int().nonnegative().optional(),
648
+ waiting: z2.number().int().nonnegative().optional()
649
+ });
650
+ var RequestThread = z2.object({
651
+ root: RequestRecord,
652
+ thread_id: z2.string(),
653
+ site_id: z2.string(),
654
+ /** Oldest first. */
655
+ entries: z2.array(RequestThreadEntry),
656
+ /** True only while the last entry is a question, which is the one state an owner may reply in. */
657
+ open: z2.boolean(),
658
+ /** The newest job of the thread, in the listing's own shape. */
659
+ latest_job: RequestRecord
660
+ });
661
+ var MAX_REQUEST_THREADS = 100;
662
+ var RequestRetryResponse = z2.object({
663
+ job_id: z2.string(),
664
+ state: z2.literal("queued"),
665
+ attempt: z2.number().int().nullable()
666
+ });
667
+ var SuggestionPreviewResponse = z2.object({
668
+ state: z2.enum(["none", "building", "ready", "failed"]),
669
+ /** The site's own address with the link's token; the browser that opens it sees the preview for a day. */
670
+ url: nullableString,
671
+ /** The address that ends the preview in that browser. */
672
+ leave_url: nullableString,
673
+ revision: z2.number().int().nullable(),
674
+ /** Whether the preview was built for the suggestion's current revision. */
675
+ current: z2.boolean(),
676
+ error: nullableString
677
+ });
678
+ var LoopMetricsResponse = z2.object({
679
+ site_id: z2.string(),
680
+ days: z2.union([z2.literal(7), z2.literal(30), z2.literal(90)]),
681
+ opened: z2.number().int().nonnegative(),
682
+ reviewed: z2.number().int().nonnegative(),
683
+ published: z2.number().int().nonnegative(),
684
+ dismissed: z2.number().int().nonnegative(),
685
+ awaiting_review: z2.number().int().nonnegative(),
686
+ oldest_awaiting_at: nullableString,
687
+ median_hours_to_review: z2.number().nonnegative().nullable(),
688
+ median_hours_to_publish: z2.number().nonnegative().nullable()
689
+ });
690
+ var ActivityPage = z2.object({
691
+ site_id: z2.string(),
692
+ events: z2.array(ActivityEvent),
693
+ next_cursor: nullableString
694
+ });
695
+ var NotificationsResponse = z2.object({
696
+ site_id: z2.string(),
697
+ notifications: z2.record(z2.string(), z2.string())
698
+ });
699
+ var NotificationPut = z2.object({
700
+ event: z2.enum(OWNER_NOTIFICATION_EVENTS),
701
+ mode: z2.enum(OWNER_NOTIFICATION_MODES)
702
+ });
703
+ var OwnerRow = z2.object({ email: z2.string(), role: OwnerRole.optional(), added_at: isoString, you: z2.boolean() });
704
+ var OwnersResponse = z2.object({ site_id: z2.string(), owners: z2.array(OwnerRow) });
705
+ var OwnerResponse = z2.object({ owner: OwnerRow });
706
+ var AddOwnerBody = z2.object({ email: z2.string().min(3).max(320), role: OwnerRole.optional() });
707
+ var OwnerRoleBody = z2.object({ role: OwnerRole });
708
+ var RemoveOwnerResponse = z2.object({ removed: z2.string(), signed_out: z2.boolean() });
709
+ var PendingSettingsPr = z2.object({ pr_number: z2.number().int(), html_url: z2.string() });
710
+ var LogoValue = z2.union([z2.string(), z2.object({ light: z2.string().optional(), dark: z2.string().optional() })]);
711
+ var RedirectEntry = z2.object({ source: z2.string(), destination: z2.string(), permanent: z2.boolean().optional() });
712
+ var MAX_REDIRECTS = 500;
713
+ var SettingsValues = z2.object({
714
+ name: nullableString.optional(),
715
+ colors: z2.object({ primary: nullableString }).optional(),
716
+ logo: LogoValue.nullable().optional(),
717
+ favicon: nullableString.optional(),
718
+ instructions: nullableString.optional(),
719
+ redirects: z2.array(RedirectEntry).optional(),
720
+ assistant: z2.object({ greeting: nullableString, suggested_questions: z2.array(z2.string()), instructions: nullableString }).optional()
721
+ });
722
+ var GeneralSettings = SettingsValues.extend({
723
+ site_id: z2.string(),
724
+ unavailable: z2.enum(["no_release", "no_repository"]).optional(),
725
+ revision: z2.string().nullable().optional(),
726
+ source: z2.enum(["repository", "proposal"]).nullable().optional(),
727
+ published: SettingsValues.nullable().optional(),
728
+ docs_repo: z2.object({ repository: nullableString, branch: z2.string(), docs_root: z2.string().optional() }).nullable().optional(),
729
+ pending_pr: PendingSettingsPr.nullable().optional()
730
+ });
731
+ var MAX_ASSISTANT_GREETING_CHARS = 200;
732
+ var MAX_ASSISTANT_SUGGESTED_QUESTIONS = 6;
733
+ var MAX_ASSISTANT_SUGGESTED_QUESTION_CHARS = 160;
734
+ var MAX_ASSISTANT_INSTRUCTIONS_CHARS = 4e3;
735
+ var BuildOutcome = z2.object({
736
+ job_id: z2.string(),
737
+ commit_sha: z2.string(),
738
+ state: z2.enum(["queued", "running", "retrying"])
739
+ });
740
+ var AnalyseOutcome = z2.object({
741
+ job_id: z2.string(),
742
+ commit_sha: z2.string(),
743
+ before_sha: z2.string(),
744
+ running: z2.boolean(),
745
+ /** Where the range starts: after the last analysis, the same range again under other rules, or the head's own parent. */
746
+ range: z2.enum(["since_last", "same_again", "parent"]).optional()
747
+ });
748
+ var AssistantSettingsBody = z2.object({
749
+ greeting: z2.string().max(MAX_ASSISTANT_GREETING_CHARS).nullable().optional(),
750
+ suggested_questions: z2.array(z2.string().max(MAX_ASSISTANT_SUGGESTED_QUESTION_CHARS).refine((question) => !/[\r\n]/u.test(question), { message: "a suggested question is one line" })).max(MAX_ASSISTANT_SUGGESTED_QUESTIONS).nullable().optional(),
751
+ instructions: z2.string().max(MAX_ASSISTANT_INSTRUCTIONS_CHARS).nullable().optional()
752
+ });
753
+ var GeneralSettingsJsonBody = z2.object({
754
+ name: z2.string().optional(),
755
+ primary_color: z2.string().optional(),
756
+ logo: LogoValue.optional(),
757
+ favicon: z2.string().optional(),
758
+ redirects: z2.array(RedirectEntry).max(MAX_REDIRECTS).optional()
759
+ });
760
+ var DOMAIN_STATUSES = ["active", "pending", "unknown"];
761
+ var DomainRecord = z2.object({
762
+ hostname: z2.string(),
763
+ kind: z2.enum(["platform", "custom"]),
764
+ status: z2.enum(DOMAIN_STATUSES),
765
+ ssl_status: nullableString,
766
+ errors: z2.array(z2.string())
767
+ });
768
+ var DomainsResponse = z2.object({ site_id: z2.string(), configured: z2.boolean(), cname_target: z2.string(), domains: z2.array(DomainRecord) });
769
+ var AddDomainBody = z2.object({ hostname: z2.string().min(3).max(253) });
770
+ var AddDomainResponse = z2.object({ domain: DomainRecord, cname_target: z2.string() });
771
+ var RemoveDomainResponse = z2.object({ hostname: z2.string(), deleted: z2.boolean() });
772
+ var READER_ROLES = ["reader", "admin"];
773
+ var ReaderRole = z2.enum(READER_ROLES);
774
+ var ReaderRecord = z2.object({ entry: z2.string(), groups: z2.array(z2.string()), role: ReaderRole, added_at: z2.string(), updated_at: z2.string() });
775
+ var ReaderPosture = z2.object({ release: z2.enum(["none", "unreadable", "known"]), require_sign_in: z2.boolean(), public_paths: z2.array(z2.string()), other_sign_in_methods: z2.boolean() });
776
+ var ReadersResponse = z2.object({ site_id: z2.string(), enabled: z2.boolean(), readers: z2.array(ReaderRecord), known_groups: z2.array(z2.string()), cap: z2.number().int(), revocation_pending: z2.boolean().optional(), posture: ReaderPosture.optional() });
777
+ var WriteReaderBody = z2.object({ entry: z2.string().min(3).max(320), groups: z2.array(z2.string()).optional(), role: ReaderRole.optional(), confirm: z2.boolean().optional() });
778
+ var WriteReaderResponse = z2.object({ reader: ReaderRecord, enabled: z2.boolean(), rotated: z2.boolean(), created: z2.boolean(), revocation_incomplete: z2.boolean().optional() });
779
+ var RemoveReaderResponse = z2.object({ entry: z2.string(), deleted: z2.boolean(), enabled: z2.boolean(), revocation_incomplete: z2.boolean().optional() });
780
+ var ClearReadersResponse = z2.object({ deleted: z2.number().int(), enabled: z2.boolean(), revocation_incomplete: z2.boolean().optional() });
781
+ var RotateReadersResponse = z2.object({ rotated: z2.boolean() });
782
+ var SettingsWriteResponse = z2.object({ revision: z2.string().optional(), pending_pr: PendingSettingsPr.nullable().optional() });
783
+ var InstructionsBody = z2.object({ instructions: z2.string().max(MAX_INSTRUCTIONS_CHARS) });
784
+ var DocsRepoSettings = z2.object({ docs_repo: DocsRepo.nullable(), pages_count: z2.number().int().nullable() });
785
+ var DocsRepoConnectBody = z2.object({
786
+ installation_id: z2.number().int(),
787
+ repository_id: z2.number().int(),
788
+ branch: z2.string().min(1),
789
+ docs_root: z2.string()
790
+ });
791
+ var ProductConnectionsResponse = z2.object({ site_id: z2.string().optional(), product_connections: z2.array(ProductConnectionRules) });
792
+ var ProductConnectionCreateBody = z2.object({
793
+ installation_id: z2.number().int(),
794
+ repository_id: z2.number().int(),
795
+ branch: z2.string().optional(),
796
+ watch_globs: z2.array(z2.string()),
797
+ ignore_globs: z2.array(z2.string()).optional(),
798
+ skip_authors: z2.array(z2.string()).optional(),
799
+ skip_labels: z2.array(z2.string()).optional(),
800
+ merge_mode: z2.enum(MERGE_MODES).optional()
801
+ });
802
+ var ProductConnectionPatchBody = z2.object({
803
+ watch_globs: z2.array(z2.string()).optional(),
804
+ ignore_globs: z2.array(z2.string()).optional(),
805
+ skip_authors: z2.array(z2.string()).optional(),
806
+ skip_labels: z2.array(z2.string()).optional(),
807
+ merge_mode: z2.enum(MERGE_MODES).optional()
808
+ });
809
+ var ProductConnectionWriteResponse = z2.object({
810
+ product_connection: ProductConnectionRules.optional(),
811
+ dry_run: z2.boolean().optional(),
812
+ stranded_jobs: z2.number().int().optional()
813
+ });
814
+ var GrantedRepository = z2.object({ id: z2.number().int(), full_name: z2.string(), default_branch: z2.string().optional() });
815
+ var RepositoriesQuery = z2.object({ installation_id: z2.coerce.number().int() });
816
+ var RepositoriesResponse = z2.object({ installation_id: z2.number().int(), repositories: z2.array(GrantedRepository) });
817
+ var SettingsApproval = z2.object({
818
+ revision: z2.string(),
819
+ pr_number: z2.number().int(),
820
+ head_sha: z2.string(),
821
+ repository_id: z2.number().int(),
822
+ actor: z2.string(),
823
+ state: z2.enum(["pending", "merged", "failed"]),
824
+ merge_commit_sha: z2.string().nullable(),
825
+ error: z2.string().nullable(),
826
+ created_at: z2.string()
827
+ });
828
+ var SettingsProposalResponse = z2.object({
829
+ site_id: z2.string(),
830
+ revision: z2.string(),
831
+ proposal: z2.object({
832
+ number: z2.number().int(),
833
+ url: z2.string(),
834
+ head_sha: z2.string(),
835
+ base_sha: z2.string(),
836
+ before: z2.record(z2.string(), z2.unknown()),
837
+ after: z2.record(z2.string(), z2.unknown()),
838
+ files: z2.array(z2.object({ path: z2.string(), status: z2.string(), sha: z2.string() })),
839
+ can_approve: z2.boolean(),
840
+ can_update: z2.boolean(),
841
+ blocked_reason: z2.string().nullable()
842
+ }).nullable(),
843
+ latest_approval: SettingsApproval.nullable(),
844
+ publication: SuggestionPublication.nullable().optional()
845
+ });
846
+ var SettingsApprovalResponse = z2.object({ site_id: z2.string(), approval: SettingsApproval });
847
+ var SettingsUpdateResponse = z2.object({ site_id: z2.string(), state: z2.literal("updating") });
848
+ var OperationFailure = z2.object({ error: z2.string().optional(), message: z2.string().optional(), reason: z2.string().optional() });
849
+ var ApproveOutcome = OperationFailure.extend({
850
+ state: z2.enum(["publishing", "revising", "failed"]).optional(),
851
+ pr_number: z2.number().int().nullable().optional(),
852
+ merge_commit_sha: z2.string().nullable().optional(),
853
+ failure: z2.string().optional(),
854
+ failure_reason: z2.string().optional()
855
+ });
856
+ var DismissOutcome = OperationFailure.extend({ state: z2.literal("dismissed").optional(), pr_number: z2.number().int().nullable().optional(), pull_request_closed: z2.boolean().optional() });
857
+ var RequestChangesOutcome = OperationFailure.extend({ state: z2.literal("revising").optional(), pr_number: z2.number().int().nullable().optional(), revision: z2.number().int().optional(), already_requested: z2.boolean().optional() });
858
+
859
+ // ../management-client/dist/index.js
860
+ import { z as z4 } from "zod";
861
+ import { z as z5 } from "zod";
862
+ var EXIT = {
863
+ success: 0,
864
+ invalid_input: 2,
865
+ unauthorized: 3,
866
+ conflict: 4,
867
+ service_failure: 5,
868
+ wait_timeout: 6,
869
+ interrupted: 130
870
+ };
871
+ var EXIT_CODES = Object.freeze(Object.values(EXIT));
872
+ var EXIT_MEANINGS = Object.freeze({
873
+ success: "The command did what it names, including a read of work that failed and a question the agent asked back.",
874
+ invalid_input: "The client refused the arguments before any request left the process.",
875
+ unauthorized: "The server refused the credential or the permission, without a guess at which.",
876
+ conflict: "State moved: a stale reviewed revision, a replayed submission with other words, or work that is not in a retryable state.",
877
+ service_failure: "Transport, the service, or the remote work itself failed, including a wait that ended in that failure.",
878
+ wait_timeout: "A wait reached its deadline with the work still running. The remote work continues.",
879
+ interrupted: "Local interruption. Nothing remote was cancelled."
880
+ });
881
+ var MUTATION_OUTCOMES = ["not_submitted", "known", "unknown"];
882
+ var CLIENT_ERROR_CODES = [
883
+ "invalid_input",
884
+ "unauthorized",
885
+ "forbidden",
886
+ "not_found",
887
+ "stale_review",
888
+ "conflict",
889
+ "rate_limited",
890
+ "service_unavailable",
891
+ "transport_failure",
892
+ "remote_failure",
893
+ "unknown_outcome",
894
+ "wait_timeout",
895
+ "interrupted"
896
+ ];
897
+ var CLIENT_ERROR_CATEGORIES = Object.freeze({
898
+ invalid_input: { exit: EXIT.invalid_input, retryable: false, mutation: "not_submitted", meaning: "The client or the server rejected the arguments. Nothing was written." },
899
+ unauthorized: { exit: EXIT.unauthorized, retryable: false, mutation: "not_submitted", meaning: "The server refused the credential. The refusal is opaque and is reported as given." },
900
+ forbidden: { exit: EXIT.unauthorized, retryable: false, mutation: "not_submitted", meaning: "The credential is known and this operation is not granted to it." },
901
+ not_found: { exit: EXIT.conflict, retryable: false, mutation: "not_submitted", meaning: "The named identity is not this site's, or does not exist." },
902
+ stale_review: { exit: EXIT.conflict, retryable: false, mutation: "not_submitted", meaning: "The reviewed base or head no longer matches the suggestion. Review again; never resend with current hashes." },
903
+ conflict: { exit: EXIT.conflict, retryable: false, mutation: "not_submitted", meaning: "State moved: a closed thread, a replayed submission with other words, or work that is not retryable." },
904
+ rate_limited: { exit: EXIT.service_failure, retryable: true, mutation: "not_submitted", meaning: "The site's allowance is spent. The server's retry-after is the only wait the client honours." },
905
+ service_unavailable: { exit: EXIT.service_failure, retryable: true, mutation: "not_submitted", meaning: "The service could not answer. A read may be sent again within the deadline." },
906
+ transport_failure: { exit: EXIT.service_failure, retryable: true, mutation: "unknown", meaning: "The request did not complete. A write in flight may or may not have been accepted." },
907
+ remote_failure: { exit: EXIT.service_failure, retryable: false, mutation: "known", meaning: "A wait followed remote work to a failure. The work itself failed; the call did not." },
908
+ unknown_outcome: { exit: EXIT.service_failure, retryable: false, mutation: "unknown", meaning: "A write was accepted but its result could not be read. Reconcile by reading before sending anything again." },
909
+ wait_timeout: { exit: EXIT.wait_timeout, retryable: false, mutation: "known", meaning: "The deadline passed with the work still running. Nothing remote was cancelled." },
910
+ interrupted: { exit: EXIT.interrupted, retryable: false, mutation: "unknown", meaning: "Local interruption. Nothing remote was cancelled." }
911
+ });
912
+ var OPAQUE_UNAUTHORIZED_MESSAGE = "The server refused this credential and did not say why. Check that SATURNDOCS_MANAGEMENT_TOKEN is set, names this site, and holds the permission this command needs.";
913
+ var ServerFault = z3.object({
914
+ status: z3.number().int().nullable(),
915
+ /** `error` as the server sent it; never invented. */
916
+ code: z3.string().nullable(),
917
+ message: z3.string().nullable(),
918
+ /** Seconds, from the server's retry-after. */
919
+ retry_after: z3.number().int().nonnegative().nullable(),
920
+ correlation_id: z3.string().nullable()
921
+ });
922
+ var ClientError = z3.object({
923
+ code: z3.enum(CLIENT_ERROR_CODES),
924
+ message: z3.string().min(1),
925
+ retryable: z3.boolean(),
926
+ mutation: z3.enum(MUTATION_OUTCOMES),
927
+ exit: z3.union([z3.literal(0), z3.literal(2), z3.literal(3), z3.literal(4), z3.literal(5), z3.literal(6), z3.literal(130)]),
928
+ server: ServerFault.nullable()
929
+ });
930
+ function serverCode(body) {
931
+ if (body === null || typeof body !== "object" || Array.isArray(body)) return null;
932
+ const value = body.error;
933
+ return typeof value === "string" && value !== "" ? value : null;
934
+ }
935
+ function classifyRefusal(refusal, context = {}) {
936
+ const { status } = refusal;
937
+ if (status === 401) return "unauthorized";
938
+ if (status === 403) return "forbidden";
939
+ if (status === 404) return context.exactReview === true ? "stale_review" : "not_found";
940
+ if (status === 429) return "rate_limited";
941
+ if (status === 400 || status === 422) return "invalid_input";
942
+ if (status === 409 || status === 412 || status === 428) return "conflict";
943
+ if (status >= 500) return "service_unavailable";
944
+ return "conflict";
945
+ }
946
+ function serverFault(refusal, retryAfter = null, correlationId = null) {
947
+ const body = refusal.body === null || typeof refusal.body !== "object" ? {} : refusal.body;
948
+ const message = body.message;
949
+ const reason = body.reason;
950
+ return {
951
+ status: refusal.status,
952
+ code: serverCode(refusal.body),
953
+ message: typeof message === "string" ? message : typeof reason === "string" ? reason : null,
954
+ retry_after: retryAfter,
955
+ correlation_id: correlationId
956
+ };
957
+ }
958
+ var CLIENT_SCHEMA_VERSION = "saturndocs.cli/v1";
959
+ var LIMITATION_CODES = [
960
+ "page_bounded",
961
+ "output_truncated",
962
+ "content_unavailable",
963
+ "review_incomplete",
964
+ "hand_edited",
965
+ "cleanup_incomplete",
966
+ "activation_unknown",
967
+ "section_unavailable"
968
+ ];
969
+ var ClientLimitation = z22.object({
970
+ code: z22.enum(LIMITATION_CODES),
971
+ message: z22.string().min(1),
972
+ /** The part of `data` the limitation applies to, as a dotted path. */
973
+ at: z22.string().nullable()
974
+ });
975
+ var ClientRecovery = z22.object({
976
+ /** A command line the caller can run, with no request text or credential in it. */
977
+ command: z22.string().nullable(),
978
+ /** The submission id and payload to replay under, when a write's outcome is uncertain. */
979
+ replay: z22.object({ submission_id: z22.string(), stdin: z22.boolean() }).nullable(),
980
+ message: z22.string().min(1)
981
+ });
982
+ var DurableIds = z22.object({
983
+ submission_id: z22.string().optional(),
984
+ thread_id: z22.string().optional(),
985
+ job_id: z22.string().optional(),
986
+ attempt: z22.number().int().nullable().optional(),
987
+ suggestion_id: z22.string().optional(),
988
+ base_sha: z22.string().optional(),
989
+ head_sha: z22.string().optional(),
990
+ release_id: z22.string().optional(),
991
+ pr_number: z22.number().int().nullable().optional()
992
+ });
993
+ var envelope = {
994
+ schema_version: z22.literal(CLIENT_SCHEMA_VERSION),
995
+ command: z22.string().min(1),
996
+ /** The site the operation was verified against; null only where no site applies, such as local schema. */
997
+ site_id: z22.string().nullable(),
998
+ ids: DurableIds,
999
+ limitations: z22.array(ClientLimitation),
1000
+ recovery: ClientRecovery.nullable()
1001
+ };
1002
+ var ClientSuccess = z22.object({ ...envelope, ok: z22.literal(true), data: z22.unknown() });
1003
+ var ClientFailure = z22.object({ ...envelope, ok: z22.literal(false), error: ClientError, data: z22.unknown().optional() });
1004
+ var ClientResult = z22.discriminatedUnion("ok", [ClientSuccess, ClientFailure]);
1005
+ function exitForResult(result) {
1006
+ return result.ok ? EXIT.success : result.error.exit;
1007
+ }
1008
+ var SitesResponse2 = SitesResponse.loose();
1009
+ var PortalSite2 = PortalSite.loose();
1010
+ var PagesResponse2 = PagesResponse;
1011
+ var ActivityPage2 = ActivityPage.loose();
1012
+ var ReleasesPage2 = ReleasesPage.loose();
1013
+ var RequestsPage2 = RequestsPage.loose();
1014
+ var RequestThread2 = RequestThread.loose();
1015
+ var UpdateRequestResponse2 = UpdateRequestResponse.loose();
1016
+ var RequestRetryResponse2 = RequestRetryResponse.loose();
1017
+ var SuggestionsPage2 = SuggestionsPage.loose();
1018
+ var SuggestionDetailResponse2 = SuggestionDetailResponse.loose();
1019
+ var SuggestionThread2 = SuggestionThread.loose();
1020
+ var SessionResponse2 = SessionResponse.loose();
1021
+ var SuggestionPreviewResponse2 = SuggestionPreviewResponse.loose();
1022
+ var ApproveOutcome2 = ApproveOutcome.loose();
1023
+ var DismissOutcome2 = DismissOutcome.loose();
1024
+ var RequestChangesOutcome2 = RequestChangesOutcome.loose();
1025
+ var ErrorEnvelope2 = ErrorEnvelope.loose();
1026
+ var DocsRepoSettings2 = DocsRepoSettings.loose();
1027
+ var ProductConnectionsResponse2 = ProductConnectionsResponse.loose();
1028
+ var MANAGEMENT_SCOPES2 = MANAGEMENT_SCOPES;
1029
+ var SHA = /^[0-9a-f]{40}$/u;
1030
+ var ExactReviewInput = z32.object({
1031
+ expected_base_sha: z32.string().regex(SHA),
1032
+ expected_head_sha: z32.string().regex(SHA)
1033
+ });
1034
+ var DismissInput = ExactReviewInput.extend({
1035
+ reason: DismissBody.shape.reason,
1036
+ note: z32.string().max(MAX_REQUEST_CHANGES_NOTE_CHARS).nullable().optional()
1037
+ });
1038
+ var AnalysisRetryAccepted = z32.object({
1039
+ retried: z32.literal(true),
1040
+ job_id: z32.string().min(1),
1041
+ attempt: z32.number().int().nullable(),
1042
+ budget_tokens: z32.number().int().optional()
1043
+ }).loose();
1044
+ var NotRetryable = z32.object({
1045
+ error: z32.literal("not_retryable"),
1046
+ /** The sentence that says what failed and what to do instead; the analysis path sends it as `reason`. */
1047
+ reason: z32.string().optional(),
1048
+ message: z32.string().optional()
1049
+ }).loose();
1050
+ var PublicationRetryOutcome = ApproveOutcome2;
1051
+ var SuggestionRetryResponse = z32.union([AnalysisRetryAccepted, NotRetryable, PublicationRetryOutcome]);
1052
+ var CURSOR = { name: "cursor", kind: "string", note: "The opaque next_cursor of the preceding page. A cursor the server did not mint is refused, not restarted from the top." };
1053
+ var P0_OPERATIONS = {
1054
+ "sites.list": {
1055
+ path: "/v1/sites",
1056
+ method: "GET",
1057
+ permission: "manage:read",
1058
+ mutation: false,
1059
+ query: [],
1060
+ response: SitesResponse2,
1061
+ note: "Discovery. A presented credential resolves to one site; primary_host is null on that answer."
1062
+ },
1063
+ "site.get": {
1064
+ path: "/site",
1065
+ method: "GET",
1066
+ permission: "manage:read",
1067
+ mutation: false,
1068
+ query: [],
1069
+ response: PortalSite2,
1070
+ note: "Served at /v1/sites/{site} with no suffix."
1071
+ },
1072
+ "docs-repo.get": { path: "/docs-repo", method: "GET", permission: "manage:read", mutation: false, query: [], response: DocsRepoSettings2, note: "Composed into site detail for connection information." },
1073
+ "product-connections.list": { path: "/product-connections", method: "GET", permission: "manage:read", mutation: false, query: [], response: ProductConnectionsResponse2, note: "Composed into site detail for connection information." },
1074
+ "pages.list": {
1075
+ path: "/pages",
1076
+ method: "GET",
1077
+ permission: "manage:read",
1078
+ mutation: false,
1079
+ query: [{ name: "days", kind: "enum", values: ["7", "30", "90"], note: "Traffic window." }],
1080
+ response: PagesResponse2,
1081
+ note: "Answers `unavailable: no_release` when the site has never published, which is not an empty page list."
1082
+ },
1083
+ "activity.list": {
1084
+ path: "/activity",
1085
+ method: "GET",
1086
+ permission: "manage:read",
1087
+ mutation: false,
1088
+ query: [
1089
+ CURSOR,
1090
+ { name: "job_id", kind: "string", max: 200, note: "Exact read. Handled by the activity route and absent from the published query metadata; a wrong length is 400 invalid_job_id." },
1091
+ { name: "outcome", kind: "enum", values: ["suggestion", "nochange", "skipped", "failed", "published"], note: "Filter." },
1092
+ { name: "repo", kind: "string", max: 200, note: "Full repository name." },
1093
+ { name: "from", kind: "string", note: "Inclusive UTC date; from later than to is 400." },
1094
+ { name: "to", kind: "string", note: "Inclusive UTC date." },
1095
+ { name: "limit", kind: "integer", max: 100, note: "Server ceiling; a larger value is clamped, not refused." }
1096
+ ],
1097
+ response: ActivityPage2,
1098
+ note: "An exact job read comes from the jobs alone, so a queued job is found however many events came after it."
1099
+ },
1100
+ "releases.list": {
1101
+ path: "/releases",
1102
+ method: "GET",
1103
+ permission: "manage:read",
1104
+ mutation: false,
1105
+ query: [
1106
+ CURSOR,
1107
+ { name: "release_id", kind: "string", max: 200, note: "Exact read, including a release older than the first page." },
1108
+ { name: "limit", kind: "integer", max: 50, note: "Outside 1..50 is 400 invalid_limit, not clamped." }
1109
+ ],
1110
+ response: ReleasesPage2,
1111
+ note: "active_release_status is `unknown` when the activation record could not be read; that is not 'not active'."
1112
+ },
1113
+ "requests.list": {
1114
+ path: "/requests",
1115
+ method: "GET",
1116
+ permission: "manage:read",
1117
+ mutation: false,
1118
+ query: [
1119
+ CURSOR,
1120
+ { name: "view", kind: "enum", values: ["threads"], note: "Required for conversations; without it the route answers the flat job listing." },
1121
+ { name: "limit", kind: "integer", max: 100, note: "Thread page size." },
1122
+ { name: "attention", kind: "enum", values: ["1"], note: "Adds the site's failed and waiting counts." }
1123
+ ],
1124
+ response: RequestsPage2,
1125
+ note: "Threads page by the original request, so a reply never moves a conversation between pages."
1126
+ },
1127
+ "requests.thread": { path: "/requests/{job}/thread", method: "GET", permission: "manage:read", mutation: false, query: [], response: RequestThread2, note: "Any job of the thread names it. Carries root, latest_job, and entries oldest first." },
1128
+ "requests.create": {
1129
+ path: "/requests",
1130
+ method: "POST",
1131
+ permission: "manage:write",
1132
+ mutation: true,
1133
+ query: [],
1134
+ response: UpdateRequestResponse2,
1135
+ note: "202 with the queued job. The same submission_id with the same payload recovers that job; with other words it is 409 submission_conflict."
1136
+ },
1137
+ "requests.retry": {
1138
+ path: "/requests/{job}/retry",
1139
+ method: "POST",
1140
+ permission: "manage:write",
1141
+ mutation: true,
1142
+ query: [],
1143
+ response: RequestRetryResponse2,
1144
+ note: "Targets the failed job and keeps its id, so `attempt` is the only thing telling two runs apart. It may be null."
1145
+ },
1146
+ "suggestions.list": {
1147
+ path: "/suggestions",
1148
+ method: "GET",
1149
+ permission: "manage:read",
1150
+ mutation: false,
1151
+ query: [CURSOR, { name: "state", kind: "enum", values: ["pending", "revising", "published", "dismissed", "failed"], note: "Tab; defaults to pending." }],
1152
+ response: SuggestionsPage2,
1153
+ note: "Counts come from the server; a bounded list is never a site total."
1154
+ },
1155
+ "suggestions.get": { path: "/suggestions/{suggestion}", method: "GET", permission: "manage:read", mutation: false, query: [], response: SuggestionDetailResponse2, note: "Carries base_sha, head_sha, revision, questions, publication, and page content when the server has it." },
1156
+ "suggestions.session.read": { path: "/suggestions/{suggestion}/session", method: "GET", permission: "manage:read", mutation: false, query: [], response: SuggestionThread2, note: "The conversation transcript." },
1157
+ "suggestions.session.post": { path: "/suggestions/{suggestion}/session", method: "POST", permission: "manage:write", mutation: true, query: [], response: SessionResponse2, note: "startedRevision says whether the answer queued a revision. question_id is a suggestion question, never a request thread." },
1158
+ "suggestions.request-changes": { path: "/suggestions/{suggestion}/request-changes", method: "POST", permission: "manage:write", mutation: true, query: [], response: RequestChangesOutcome2, note: "already_requested means the earlier revision is the live one; this call changed nothing." },
1159
+ "suggestions.preview.read": { path: "/suggestions/{suggestion}/preview", method: "GET", permission: "manage:read", mutation: false, query: [], response: SuggestionPreviewResponse2, note: "Read-only. `current` false means the preview is not of the suggestion's current revision." },
1160
+ "suggestions.preview.create": { path: "/suggestions/{suggestion}/preview", method: "POST", permission: "manage:write", mutation: true, query: [], response: SuggestionPreviewResponse2, note: "Builds a preview. Only an explicit --create reaches here." },
1161
+ "suggestions.approve": { path: "/suggestions/{suggestion}/approve", method: "POST", permission: "manage:approve", mutation: true, query: [], response: ApproveOutcome2, note: "A head or base that moved is answered 404 by the merge path, which is a stale review and not a missing suggestion." },
1162
+ "suggestions.dismiss": { path: "/suggestions/{suggestion}/dismiss", method: "POST", permission: "manage:write", mutation: true, query: [], response: DismissOutcome2, note: "pull_request_closed false is a completed dismissal with incomplete cleanup." },
1163
+ "suggestions.retry": { path: "/suggestions/{suggestion}/retry", method: "POST", permission: "manage:approve", mutation: true, query: [], response: SuggestionRetryResponse, note: "Analysis retry answers with the job and attempt; publication retry answers with the approval outcome; a failed revision is 409 not_retryable." }
1164
+ };
1165
+ var P0_OPERATION_KEYS = Object.keys(P0_OPERATIONS);
1166
+ function operationOf(key) {
1167
+ return P0_OPERATIONS[key];
1168
+ }
1169
+ function operationPath(key, siteId, params = {}) {
1170
+ const operation = P0_OPERATIONS[key];
1171
+ if (operation.path === "/v1/sites") return "/v1/sites";
1172
+ const rest = operation.path === "/site" ? "" : operation.path.replace(/\{([^}]+)\}/gu, (_all, name) => {
1173
+ const value = params[name];
1174
+ if (value === void 0 || value === "") throw new Error(`Missing path parameter ${name} for ${key}`);
1175
+ return encodeURIComponent(value);
1176
+ });
1177
+ return `/v1/sites/${encodeURIComponent(siteId)}${rest}`;
1178
+ }
1179
+ var REQUEST_WAIT_TABLE = Object.freeze([
1180
+ { state: "queued", entry: null, decision: "continue", outcome: "working", exit: EXIT.success, note: "Still admitted; nothing to report yet." },
1181
+ { state: "running", entry: null, decision: "continue", outcome: "working", exit: EXIT.success, note: "Drafting." },
1182
+ { state: "succeeded", entry: "draft", decision: "stop", outcome: "drafted", exit: EXIT.success, note: "The suggestion the request produced is named on the entry." },
1183
+ { state: "succeeded", entry: "question", decision: "stop", outcome: "question", exit: EXIT.success, note: "The agent asked back. Answering is a reply on the root thread, not a new request." },
1184
+ { state: "succeeded", entry: "no_change", decision: "stop", outcome: "no_change", exit: EXIT.success, note: "Concluded that nothing needed changing. This is a result, not a failure." },
1185
+ { state: "succeeded", entry: "other", decision: "stop", outcome: "outcome_unknown", exit: EXIT.service_failure, note: "Terminal with no entry that says what came of it; read the thread rather than assume." },
1186
+ { state: "succeeded", entry: null, decision: "stop", outcome: "outcome_unknown", exit: EXIT.service_failure, note: "Terminal with no matching entry at all. Never wait forever on this." },
1187
+ { state: "failed", entry: "failed", decision: "stop", outcome: "failed", exit: EXIT.service_failure, note: "The work failed. Reading it exits 0; waiting on it exits 5." },
1188
+ { state: "failed", entry: null, decision: "stop", outcome: "failed", exit: EXIT.service_failure, note: "Failed before it wrote an entry." },
1189
+ { state: "failed", entry: "other", decision: "stop", outcome: "failed", exit: EXIT.service_failure, note: "Failed after some other turn." },
1190
+ { state: "timed_out", entry: null, decision: "stop", outcome: "failed", exit: EXIT.service_failure, note: "Ran out of time server-side. Retryable as a failure is." },
1191
+ { state: "timed_out", entry: "other", decision: "stop", outcome: "failed", exit: EXIT.service_failure, note: "Ran out of time server-side." },
1192
+ { state: "timed_out", entry: "failed", decision: "stop", outcome: "failed", exit: EXIT.service_failure, note: "Ran out of time server-side." },
1193
+ { state: "cancelled", entry: null, decision: "stop", outcome: "cancelled", exit: EXIT.service_failure, note: "Cancelled or superseded. Not retryable; ask again instead." },
1194
+ { state: "cancelled", entry: "other", decision: "stop", outcome: "cancelled", exit: EXIT.service_failure, note: "Cancelled or superseded." },
1195
+ { state: "cancelled", entry: "failed", decision: "stop", outcome: "cancelled", exit: EXIT.service_failure, note: "Cancelled or superseded." }
1196
+ ]);
1197
+ var ENTRY_KINDS = /* @__PURE__ */ new Set(["draft", "question", "no_change", "failed"]);
1198
+ function classifyRequestWait(thread) {
1199
+ const latest = thread.latest_job;
1200
+ const mine = thread.entries.filter((entry2) => entry2.job_id === latest.job_id && (latest.attempt === null || entry2.attempt === latest.attempt));
1201
+ const newest = mine.at(-1);
1202
+ const entry = newest === void 0 ? null : ENTRY_KINDS.has(newest.kind) ? newest.kind : "other";
1203
+ const rule = REQUEST_WAIT_TABLE.find((row) => row.state === latest.state && row.entry === entry) ?? REQUEST_WAIT_TABLE.find((row) => row.state === latest.state && row.entry === null);
1204
+ if (rule === void 0) throw new Error(`No request wait rule for state ${latest.state}`);
1205
+ return { job_id: latest.job_id, attempt: latest.attempt, entry, rule, suggestion_id: newest?.suggestion_id ?? null };
1206
+ }
1207
+ var SUGGESTION_WAIT_TARGETS = ["reviewable", "published"];
1208
+ var SUGGESTION_WAIT_TABLE = Object.freeze([
1209
+ { target: "reviewable", state: "pending_review", decision: "stop", outcome: "reviewable", exit: EXIT.success, note: "Ready to read. Review limitations travel with the result." },
1210
+ { target: "reviewable", state: "changes_requested", decision: "continue", outcome: "working", exit: EXIT.success, note: "A revision was asked for and has not started." },
1211
+ { target: "reviewable", state: "revising", decision: "continue", outcome: "working", exit: EXIT.success, note: "Being revised." },
1212
+ { target: "reviewable", state: "publishing", decision: "stop", outcome: "no_longer_reviewable", exit: EXIT.conflict, note: "Already approved by someone; there is nothing left to review." },
1213
+ { target: "reviewable", state: "published", decision: "stop", outcome: "no_longer_reviewable", exit: EXIT.conflict, note: "Already published." },
1214
+ { target: "reviewable", state: "dismissed", decision: "stop", outcome: "dismissed", exit: EXIT.conflict, note: "Dismissed. Nothing to review." },
1215
+ { target: "reviewable", state: "superseded", decision: "stop", outcome: "superseded", exit: EXIT.conflict, note: "A newer suggestion replaced it. Approving this one would publish the older revision." },
1216
+ { target: "reviewable", state: "failed", decision: "stop", outcome: "failed", exit: EXIT.service_failure, note: "The work behind it failed." },
1217
+ { target: "published", state: "pending_review", decision: "continue", outcome: "working", exit: EXIT.success, note: "The approval may not be recorded yet." },
1218
+ { target: "published", state: "changes_requested", decision: "stop", outcome: "review_required", exit: EXIT.conflict, note: "A revision was asked for, so the reviewed revision will not publish. Review the new one." },
1219
+ { target: "published", state: "revising", decision: "stop", outcome: "review_required", exit: EXIT.conflict, note: "Approval led to another revision. Never approve the new draft automatically." },
1220
+ { target: "published", state: "publishing", decision: "continue", outcome: "working", exit: EXIT.success, note: "Merged and building." },
1221
+ { target: "published", state: "published", decision: "continue", outcome: "working", exit: EXIT.success, note: "State alone is not activation; the linked release decides." },
1222
+ { target: "published", state: "dismissed", decision: "stop", outcome: "dismissed", exit: EXIT.conflict, note: "Dismissed instead of published." },
1223
+ { target: "published", state: "superseded", decision: "stop", outcome: "superseded", exit: EXIT.conflict, note: "Replaced before it published." },
1224
+ { target: "published", state: "failed", decision: "stop", outcome: "failed", exit: EXIT.service_failure, note: "Publication failed." }
1225
+ ]);
1226
+ var ACTIVATION_TABLE = Object.freeze([
1227
+ { linked: false, activated: false, active: "unknown", decision: "continue", outcome: "working", exit: EXIT.success, note: "No release is linked yet." },
1228
+ { linked: true, activated: false, active: "unknown", decision: "continue", outcome: "working", exit: EXIT.success, note: "Built, never activated. Not published." },
1229
+ { linked: true, activated: false, active: "other", decision: "continue", outcome: "working", exit: EXIT.success, note: "Built, never activated." },
1230
+ { linked: true, activated: false, active: "this", decision: "continue", outcome: "working", exit: EXIT.success, note: "Named active with no activation record; wait for the record rather than claim it." },
1231
+ { linked: true, activated: true, active: "this", decision: "stop", outcome: "published_active", exit: EXIT.success, note: "Activated and serving readers now." },
1232
+ { linked: true, activated: true, active: "other", decision: "stop", outcome: "published_historical", exit: EXIT.success, note: "It did go live; a later release is serving readers now." },
1233
+ { linked: true, activated: true, active: "unknown", decision: "stop", outcome: "published_activation_unknown", exit: EXIT.success, note: "It did go live; whether it is serving readers now could not be read." }
1234
+ ]);
1235
+ function classifyActivation(input) {
1236
+ const linked = input.release_id !== null;
1237
+ const activated = linked && input.activated_at !== null;
1238
+ const active = input.active_release_status === "unknown" ? "unknown" : input.active_release_id !== null && input.active_release_id === input.release_id ? "this" : "other";
1239
+ const rule = ACTIVATION_TABLE.find((row) => row.linked === linked && row.activated === activated && row.active === active) ?? ACTIVATION_TABLE.find((row) => row.linked === linked && row.activated === activated);
1240
+ if (rule === void 0) throw new Error("No activation rule for the given release state");
1241
+ return rule;
1242
+ }
1243
+ function classifySuggestionWait(target, suggestion, activation) {
1244
+ const rule = SUGGESTION_WAIT_TABLE.find((row) => row.target === target && row.state === suggestion.state);
1245
+ if (rule === void 0) throw new Error(`No suggestion wait rule for ${target}/${suggestion.state}`);
1246
+ if (target !== "published" || rule.decision === "stop" || activation === void 0) {
1247
+ return { rule, activation: null, decision: rule.decision, outcome: rule.outcome, exit: rule.exit };
1248
+ }
1249
+ const activationRule = classifyActivation(activation);
1250
+ return { rule, activation: activationRule, decision: activationRule.decision, outcome: activationRule.outcome, exit: activationRule.exit };
1251
+ }
1252
+ var GLOBAL = Object.freeze([
1253
+ { name: "--json", value: "boolean", required: false, description: "One result object and a newline on stdout. Diagnostics stay on stderr." },
1254
+ { name: "--site", value: "string", required: false, description: "The site to act on. Required for every remote mutation." },
1255
+ { name: "--api", value: "string", required: false, description: "Management API origin." },
1256
+ { name: "--no-input", value: "boolean", required: false, description: "Never read from a terminal." },
1257
+ { name: "--timeout", value: "integer", required: false, description: "Seconds a wait or a request may take." }
1258
+ ]);
1259
+ var GLOBAL_FLAGS = GLOBAL;
1260
+ var TEXT = Object.freeze([
1261
+ { name: "--file", value: "string", required: false, description: "Read the text from a path, or from stdin with -." },
1262
+ { name: "--text", value: "string", required: false, description: "The text itself. Mutually exclusive with --file." }
1263
+ ]);
1264
+ var WAIT = Object.freeze([
1265
+ { name: "--timeout", value: "integer", required: false, description: "Seconds. Must be positive and finite; it bounds backoff and the request in flight alike." }
1266
+ ]);
1267
+ var READ_EXITS = Object.freeze([EXIT.success, EXIT.invalid_input, EXIT.unauthorized, EXIT.conflict, EXIT.service_failure, EXIT.interrupted]);
1268
+ var WRITE_EXITS = READ_EXITS;
1269
+ var WAIT_EXITS = Object.freeze([...READ_EXITS, EXIT.wait_timeout]);
1270
+ var REQUEST_TEXT_LIMIT = 4e3;
1271
+ var SESSION_MESSAGE_LIMIT = 2e3;
1272
+ var REVISION_NOTE_LIMIT = 4e3;
1273
+ var cursor = { name: "--cursor", value: "string", required: false, description: "The next_cursor of the preceding page. One page is read per invocation." };
1274
+ var limit = { name: "--limit", value: "integer", required: false, description: "Page size, within the operation's server ceiling." };
1275
+ var P0_COMMANDS = [
1276
+ { name: "auth status", operation: "sites.list", operations: ["sites.list"], permission: "manage:read", mutation: false, requires_site: false, text_input: "none", text_limit: null, flags: [], exits: READ_EXITS, summary: "Verify read access and report the credential's own site. Says nothing about scopes the server did not return." },
1277
+ { name: "sites list", operation: "sites.list", operations: ["sites.list"], permission: "manage:read", mutation: false, requires_site: false, text_input: "none", text_limit: null, flags: [], exits: READ_EXITS, summary: "The sites the presented credential resolves to." },
1278
+ { name: "sites get", operation: "site.get", operations: ["site.get", "docs-repo.get", "product-connections.list"], permission: "manage:read", mutation: false, requires_site: false, text_input: "none", text_limit: null, flags: [{ name: "--connections", value: "boolean", required: false, description: "Also read the documentation repository and watched repositories." }], exits: READ_EXITS, summary: "One site's status, hostnames, publication, and usage." },
1279
+ { name: "pages list", operation: "pages.list", operations: ["pages.list"], permission: "manage:read", mutation: false, requires_site: false, text_input: "none", text_limit: null, flags: [{ name: "--days", value: "integer", required: false, description: "Traffic window: 7, 30, or 90." }], exits: READ_EXITS, summary: "Published pages of the live release. An unpublished site is not an empty list." },
1280
+ { name: "status", operation: null, operations: ["site.get", "requests.list", "suggestions.list", "activity.list"], permission: "manage:read", mutation: false, requires_site: true, text_input: "none", text_limit: null, flags: [], exits: READ_EXITS, summary: "Bounded briefing. Each section reports available, empty, or unavailable on its own." },
1281
+ { name: "schema", operation: null, operations: [], permission: null, mutation: false, requires_site: false, text_input: "none", text_limit: null, flags: [], exits: [EXIT.success, EXIT.invalid_input, EXIT.interrupted], summary: "Versioned command metadata, locally. No credential and no network." },
1282
+ { name: "manage", operation: null, operations: [], permission: null, mutation: false, requires_site: true, text_input: "optional", text_limit: null, flags: [{ name: "--data", value: "string", required: false, description: "JSON body, or @path to read one." }, { name: "--file", value: "string", required: false, description: "A multipart file part, repeatable." }, { name: "--revision", value: "string", required: false, description: "The quoted settings revision for If-Match." }], exits: WRITE_EXITS, summary: "One existing management operation chosen by the caller, under the same target and precondition rules as every other command." },
1283
+ { name: "requests create", operation: "requests.create", operations: ["requests.create"], permission: "manage:write", mutation: true, requires_site: true, text_input: "required", text_limit: REQUEST_TEXT_LIMIT, flags: [...TEXT, { name: "--submission-id", value: "string", required: true, description: "A UUID the caller keeps. Replaying it with the same payload recovers the same job." }, { name: "--page", value: "string", required: false, description: "The page the request is about." }], exits: WRITE_EXITS, summary: "Submit a documentation request and return its queued job and root thread." },
1284
+ { name: "requests reply", operation: "requests.create", operations: ["requests.create"], permission: "manage:write", mutation: true, requires_site: true, text_input: "required", text_limit: REQUEST_TEXT_LIMIT, flags: [...TEXT, { name: "--thread", value: "string", required: true, description: "The root job of the conversation being answered." }, { name: "--submission-id", value: "string", required: true, description: "A UUID the caller keeps, so a lost answer replays instead of duplicating." }], exits: WRITE_EXITS, summary: "Answer a clarification. The root thread is kept; a new job comes back." },
1285
+ { name: "requests list", operation: "requests.list", operations: ["requests.list"], permission: "manage:read", mutation: false, requires_site: false, text_input: "none", text_limit: null, flags: [cursor, limit], exits: READ_EXITS, summary: "Conversations, newest original request first." },
1286
+ { name: "requests get", operation: "requests.thread", operations: ["requests.thread"], permission: "manage:read", mutation: false, requires_site: false, text_input: "none", text_limit: null, flags: [], exits: READ_EXITS, summary: "One conversation with its root, its latest job and attempt, and up to its newest 100 turns." },
1287
+ { name: "requests retry", operation: "requests.retry", operations: ["requests.retry"], permission: "manage:write", mutation: true, requires_site: true, text_input: "none", text_limit: null, flags: [], exits: WRITE_EXITS, summary: "Send a failed request again. Reports the returned attempt, including null." },
1288
+ { name: "requests wait", operation: "requests.thread", operations: ["requests.thread"], permission: "manage:read", mutation: false, requires_site: false, text_input: "none", text_limit: null, flags: [...WAIT], exits: WAIT_EXITS, summary: "Follow one conversation's latest job to a draft, a question, no change, or a failure." },
1289
+ { name: "suggestions list", operation: "suggestions.list", operations: ["suggestions.list"], permission: "manage:read", mutation: false, requires_site: false, text_input: "none", text_limit: null, flags: [cursor, limit, { name: "--state", value: "string", required: false, description: "pending, revising, published, dismissed, or failed." }], exits: READ_EXITS, summary: "Suggestions on one tab." },
1290
+ { name: "suggestions get", operation: "suggestions.get", operations: ["suggestions.get"], permission: "manage:read", mutation: false, requires_site: false, text_input: "none", text_limit: null, flags: [], exits: READ_EXITS, summary: "One suggestion, its exact revision, and its linked identities." },
1291
+ { name: "suggestions diff", operation: "suggestions.get", operations: ["suggestions.get"], permission: "manage:read", mutation: false, requires_site: false, text_input: "none", text_limit: null, flags: [{ name: "--path", value: "string", required: false, description: "One changed page instead of all of them." }], exits: READ_EXITS, summary: "The supplied page content as a diff, with every gap in it named." },
1292
+ { name: "suggestions conversation", operation: "suggestions.session.read", operations: ["suggestions.session.read"], permission: "manage:read", mutation: false, requires_site: false, text_input: "none", text_limit: null, flags: [], exits: READ_EXITS, summary: "The transcript of a suggestion conversation." },
1293
+ { name: "suggestions message", operation: "suggestions.session.post", operations: ["suggestions.session.post"], permission: "manage:write", mutation: true, requires_site: true, text_input: "required", text_limit: SESSION_MESSAGE_LIMIT, flags: [...TEXT, { name: "--question-id", value: "string", required: false, description: "The suggestion question being answered. Not a request thread id." }], exits: WRITE_EXITS, summary: "Ask about a suggestion or answer its question, reporting whether a revision started." },
1294
+ { name: "suggestions request-changes", operation: "suggestions.request-changes", operations: ["suggestions.request-changes"], permission: "manage:write", mutation: true, requires_site: true, text_input: "required", text_limit: REVISION_NOTE_LIMIT, flags: [...TEXT], exits: WRITE_EXITS, summary: "Ask for a revision. A note saved without a queued revision is reported as such." },
1295
+ { name: "suggestions preview", operation: "suggestions.preview.read", operations: ["suggestions.preview.read", "suggestions.preview.create"], permission: "manage:read", mutation: false, requires_site: false, text_input: "none", text_limit: null, flags: [{ name: "--create", value: "boolean", required: false, description: "Build a preview. Requires an explicit --site and manage:write." }], exits: WRITE_EXITS, summary: "Read the current revision's preview; build one only when asked." },
1296
+ { name: "suggestions approve", operation: "suggestions.approve", operations: ["suggestions.approve"], permission: "manage:approve", mutation: true, requires_site: true, text_input: "none", text_limit: null, flags: [{ name: "--base", value: "string", required: true, description: "The reviewed base SHA, sent as expected_base_sha." }, { name: "--head", value: "string", required: true, description: "The reviewed head SHA, sent as expected_head_sha." }], exits: WRITE_EXITS, summary: "Publish exactly the revision that was reviewed. A moved head is refused, never repaired." },
1297
+ { name: "suggestions dismiss", operation: "suggestions.dismiss", operations: ["suggestions.dismiss"], permission: "manage:write", mutation: true, requires_site: true, text_input: "optional", text_limit: REVISION_NOTE_LIMIT, flags: [{ name: "--base", value: "string", required: true, description: "The reviewed base SHA." }, { name: "--head", value: "string", required: true, description: "The reviewed head SHA." }, { name: "--reason", value: "string", required: true, description: "not_needed, wrong_pages, inaccurate, manual, or other." }, ...TEXT], exits: WRITE_EXITS, summary: "Dismiss the reviewed revision. Incomplete pull-request cleanup is reported, not hidden." },
1298
+ { name: "suggestions retry", operation: "suggestions.retry", operations: ["suggestions.retry"], permission: "manage:approve", mutation: true, requires_site: true, text_input: "none", text_limit: null, flags: [], exits: WRITE_EXITS, summary: "Retry the analysis or the publication, whichever failed, and report which happened." },
1299
+ { name: "suggestions wait", operation: "suggestions.get", operations: ["suggestions.get", "releases.list"], permission: "manage:read", mutation: false, requires_site: false, text_input: "none", text_limit: null, flags: [...WAIT, { name: "--until", value: "string", required: true, description: "reviewable or published." }], exits: WAIT_EXITS, summary: "Wait for a revision to become reviewable, or for its linked release to show activation." },
1300
+ { name: "activity list", operation: "activity.list", operations: ["activity.list"], permission: "manage:read", mutation: false, requires_site: false, text_input: "none", text_limit: null, flags: [cursor, limit, { name: "--outcome", value: "string", required: false, description: "suggestion, nochange, skipped, failed, or published." }, { name: "--repo", value: "string", required: false, description: "Full repository name." }, { name: "--from", value: "string", required: false, description: "Inclusive UTC date." }, { name: "--to", value: "string", required: false, description: "Inclusive UTC date." }], exits: READ_EXITS, summary: "Operational activity, newest first." },
1301
+ { name: "activity get", operation: "activity.list", operations: ["activity.list"], permission: "manage:read", mutation: false, requires_site: false, text_input: "none", text_limit: null, flags: [], exits: READ_EXITS, summary: "One job by id, through the list operation's job_id filter. No page-one scan." },
1302
+ { name: "releases list", operation: "releases.list", operations: ["releases.list"], permission: "manage:read", mutation: false, requires_site: false, text_input: "none", text_limit: null, flags: [cursor, limit], exits: READ_EXITS, summary: "Releases, newest first, with activation and current-active status." },
1303
+ { name: "releases get", operation: "releases.list", operations: ["releases.list"], permission: "manage:read", mutation: false, requires_site: false, text_input: "none", text_limit: null, flags: [], exits: READ_EXITS, summary: "One release by id, including one older than the first page." }
1304
+ ];
1305
+ var P0_COMMAND_NAMES = Object.freeze(P0_COMMANDS.map((command) => command.name));
1306
+ function commandSpec(name) {
1307
+ return P0_COMMANDS.find((command) => command.name === name);
1308
+ }
1309
+ var ManagementError = class extends Error {
1310
+ detail;
1311
+ constructor(detail, options) {
1312
+ super(detail.message, options);
1313
+ this.name = "ManagementError";
1314
+ this.detail = detail;
1315
+ }
1316
+ };
1317
+ function clientError(code, message, overrides = {}) {
1318
+ const category = CLIENT_ERROR_CATEGORIES[code];
1319
+ return {
1320
+ code,
1321
+ message,
1322
+ retryable: overrides.retryable ?? category.retryable,
1323
+ mutation: overrides.mutation ?? category.mutation,
1324
+ exit: category.exit,
1325
+ server: overrides.server ?? null
1326
+ };
1327
+ }
1328
+ function managementError(code, message, overrides = {}) {
1329
+ return new ManagementError(clientError(code, message, overrides));
1330
+ }
1331
+ function invalidInput(message) {
1332
+ return managementError("invalid_input", message);
1333
+ }
1334
+ function refusalError(refusal, details = {}) {
1335
+ const code = classifyRefusal(refusal, { exactReview: details.exactReview });
1336
+ const fault = serverFault(refusal, details.retryAfter ?? null, details.correlationId ?? null);
1337
+ const message = code === "unauthorized" ? OPAQUE_UNAUTHORIZED_MESSAGE : fault.message ?? `The management API answered ${refusal.status}${fault.code === null ? "" : ` ${fault.code}`}.`;
1338
+ return new ManagementError(clientError(code, message, { mutation: details.mutation, server: fault }));
1339
+ }
1340
+ function transportFailure(message, mutation = "not_submitted") {
1341
+ return managementError("transport_failure", message, { mutation });
1342
+ }
1343
+ function interrupted(mutation = "not_submitted") {
1344
+ return managementError("interrupted", "The command was interrupted before it finished. Nothing remote was cancelled.", { mutation });
1345
+ }
1346
+ function unknownOutcome(message) {
1347
+ return managementError("unknown_outcome", message);
1348
+ }
1349
+ function isManagementError(value) {
1350
+ return value instanceof ManagementError;
1351
+ }
1352
+ function toClientError(cause, mutation = "not_submitted") {
1353
+ if (isManagementError(cause)) return cause.detail;
1354
+ const message = cause instanceof Error ? cause.message : String(cause);
1355
+ return clientError("transport_failure", message === "" ? "The request failed without a reason." : message, { mutation });
1356
+ }
1357
+ var REDACTION = "[redacted]";
1358
+ function redactSecrets(text, secrets) {
1359
+ let out = text.replace(/[Bb]earer\s+[\w.~+/=-]+/gu, `Bearer ${REDACTION}`);
1360
+ for (const secret of secrets) {
1361
+ if (secret === void 0 || secret === "") continue;
1362
+ out = out.split(secret).join(REDACTION);
1363
+ }
1364
+ return out;
1365
+ }
1366
+ var COMMIT_SHA = /^[0-9a-f]{40}$/u;
1367
+ var SETTINGS_REVISION = /^[0-9a-f]{64}$/u;
1368
+ function parseReviewedRevision(base, head) {
1369
+ const base_sha = requireCommitSha(base, "--base");
1370
+ const head_sha = requireCommitSha(head, "--head");
1371
+ return { base_sha, head_sha };
1372
+ }
1373
+ function requireCommitSha(value, flag) {
1374
+ if (typeof value !== "string" || !COMMIT_SHA.test(value)) {
1375
+ throw invalidInput(`${flag} must be the full 40-character commit SHA exactly as the suggestion read returned it, in lower case.`);
1376
+ }
1377
+ return value;
1378
+ }
1379
+ function requireSettingsRevision(value) {
1380
+ if (typeof value !== "string" || !SETTINGS_REVISION.test(value)) {
1381
+ throw invalidInput("--revision must be the 64-character revision returned by the settings read or the proposal review, in lower case.");
1382
+ }
1383
+ return value;
1384
+ }
1385
+ function ifMatchHeader(revision) {
1386
+ return `"${requireSettingsRevision(revision)}"`;
1387
+ }
1388
+ var DEFAULT_ORIGIN = "https://app.saturndocs.com";
1389
+ var DEFAULT_MAX_BYTES = 4 * 1024 * 1024;
1390
+ var DEFAULT_DEADLINE_MS = 3e4;
1391
+ var MAX_READ_ATTEMPTS = 4;
1392
+ var BASE_BACKOFF_MS = 250;
1393
+ var MAX_BACKOFF_MS = 5e3;
1394
+ var IDEMPOTENT = /* @__PURE__ */ new Set(["GET"]);
1395
+ var RETRYABLE_STATUS = /* @__PURE__ */ new Set([429, 503]);
1396
+ function parseOrigin(value = DEFAULT_ORIGIN) {
1397
+ let url;
1398
+ try {
1399
+ url = new URL(value);
1400
+ } catch {
1401
+ throw invalidInput("The management API address must be a full HTTPS origin, such as https://app.saturndocs.com.");
1402
+ }
1403
+ if (url.protocol !== "https:") throw invalidInput("The management API address must use HTTPS; a token must never travel in clear text.");
1404
+ if (url.username !== "" || url.password !== "") throw invalidInput("The management API address must not carry credentials.");
1405
+ if (url.pathname !== "/" || url.search !== "" || url.hash !== "") throw invalidInput("The management API address must be a bare origin with no path, query, or fragment.");
1406
+ return url;
1407
+ }
1408
+ function resourcePath(siteId, segments = []) {
1409
+ const rest = segments.map((segment) => {
1410
+ if (segment === "") throw invalidInput("A resource id in the request path is empty.");
1411
+ return encodeURIComponent(segment);
1412
+ });
1413
+ const prefix = siteId === null ? ["v1"] : ["v1", "sites", encodeURIComponent(siteId)];
1414
+ return `/${[...prefix, ...rest].join("/")}`;
1415
+ }
1416
+ function confinedUrl(origin, path) {
1417
+ if (!path.startsWith("/v1")) throw invalidInput("A management request path must start with /v1.");
1418
+ if (path.includes("\\") || path.includes("#") || path.startsWith("//")) throw invalidInput("A management request path must not carry a host, a fragment, or a backslash.");
1419
+ for (const part of path.split("/")) {
1420
+ let decoded;
1421
+ try {
1422
+ decoded = decodeURIComponent(part);
1423
+ } catch {
1424
+ throw invalidInput("A management request path contains an invalid escape.");
1425
+ }
1426
+ if (decoded === "." || decoded === "..") throw invalidInput("A management request path must not contain relative segments.");
1427
+ }
1428
+ const url = new URL(path, origin);
1429
+ if (url.origin !== origin.origin) throw invalidInput("A management request must stay on the configured API origin.");
1430
+ if (url.pathname !== "/v1" && !url.pathname.startsWith("/v1/")) throw invalidInput("A management request must stay under /v1.");
1431
+ return url;
1432
+ }
1433
+ function parseRetryAfter(header, now) {
1434
+ if (header === null) return null;
1435
+ const trimmed = header.trim();
1436
+ if (/^\d+$/u.test(trimmed)) return Number(trimmed);
1437
+ const at = Date.parse(trimmed);
1438
+ if (Number.isNaN(at)) return null;
1439
+ return Math.max(0, Math.ceil((at - now) / 1e3));
1440
+ }
1441
+ async function readBounded(response, maxBytes) {
1442
+ const declared = response.headers.get("content-length");
1443
+ if (declared !== null && /^\d+$/u.test(declared) && Number(declared) > maxBytes) {
1444
+ throw new Error(`The response is ${declared} bytes, over the ${maxBytes}-byte read limit.`);
1445
+ }
1446
+ const body = response.body;
1447
+ if (body === null || body === void 0 || typeof body.getReader !== "function") {
1448
+ const buffer = await response.arrayBuffer();
1449
+ if (buffer.byteLength > maxBytes) throw new Error(`The response is over the ${maxBytes}-byte read limit.`);
1450
+ return new TextDecoder().decode(buffer);
1451
+ }
1452
+ const reader = body.getReader();
1453
+ const chunks = [];
1454
+ let total = 0;
1455
+ try {
1456
+ for (; ; ) {
1457
+ const { done, value } = await reader.read();
1458
+ if (done) break;
1459
+ if (value === void 0) continue;
1460
+ total += value.byteLength;
1461
+ if (total > maxBytes) throw new Error(`The response is over the ${maxBytes}-byte read limit.`);
1462
+ chunks.push(value);
1463
+ }
1464
+ } finally {
1465
+ await reader.cancel().catch(() => void 0);
1466
+ }
1467
+ const joined = new Uint8Array(total);
1468
+ let at = 0;
1469
+ for (const chunk of chunks) {
1470
+ joined.set(chunk, at);
1471
+ at += chunk.byteLength;
1472
+ }
1473
+ return new TextDecoder().decode(joined);
1474
+ }
1475
+ function defaultSleep(ms, signal) {
1476
+ return new Promise((resolve, reject) => {
1477
+ const timer = setTimeout(() => {
1478
+ signal.removeEventListener("abort", onAbort);
1479
+ resolve();
1480
+ }, ms);
1481
+ const onAbort = () => {
1482
+ clearTimeout(timer);
1483
+ reject(signal.reason instanceof Error ? signal.reason : new Error("aborted"));
1484
+ };
1485
+ if (signal.aborted) {
1486
+ onAbort();
1487
+ return;
1488
+ }
1489
+ signal.addEventListener("abort", onAbort, { once: true });
1490
+ });
1491
+ }
1492
+ function createClient(config = {}) {
1493
+ const origin = parseOrigin(config.origin);
1494
+ const token = config.token;
1495
+ const fetchImpl = config.fetchImpl ?? fetch;
1496
+ const callerSignal = config.signal;
1497
+ const defaultDeadline = config.deadlineMs ?? DEFAULT_DEADLINE_MS;
1498
+ const maxBytes = config.maxBytes ?? DEFAULT_MAX_BYTES;
1499
+ const sleep = config.sleep ?? defaultSleep;
1500
+ const now = config.now ?? (() => Date.now());
1501
+ const secrets = [token];
1502
+ const clean = (text) => redactSecrets(text, secrets);
1503
+ const cancelled = (signal) => (callerSignal?.aborted ?? false) || (signal?.aborted ?? false);
1504
+ async function request(input) {
1505
+ const url = confinedUrl(origin, input.path);
1506
+ for (const [name, value] of Object.entries(input.query ?? {})) {
1507
+ if (value !== void 0) url.searchParams.set(name, String(value));
1508
+ }
1509
+ if (input.method === "GET" && (input.body !== void 0 || input.rawBody !== void 0)) throw invalidInput("A GET request does not carry a body.");
1510
+ if (input.body !== void 0 && input.rawBody !== void 0) throw invalidInput("A request carries either a JSON body or an encoded one, never both.");
1511
+ const anonymous = input.anonymous === true;
1512
+ if (!anonymous && (token === void 0 || token === "")) throw invalidInput("Set SATURNDOCS_MANAGEMENT_TOKEN to a management credential for this site.");
1513
+ const mutation = input.mutation === true || !IDEMPOTENT.has(input.method);
1514
+ const headers = new Headers({ accept: "application/json" });
1515
+ if (!anonymous) headers.set("authorization", `Bearer ${token}`);
1516
+ if (input.revision !== void 0) headers.set("if-match", ifMatchHeader(input.revision));
1517
+ const body = input.rawBody ?? (input.body === void 0 ? void 0 : JSON.stringify(input.body));
1518
+ if (input.body !== void 0) headers.set("content-type", "application/json");
1519
+ const deadlineMs = input.deadlineMs ?? defaultDeadline;
1520
+ if (!Number.isFinite(deadlineMs) || deadlineMs <= 0) throw invalidInput("The timeout must be a positive number of seconds.");
1521
+ const startedAt = now();
1522
+ const remaining = () => deadlineMs - (now() - startedAt);
1523
+ let attempts = 0;
1524
+ for (; ; ) {
1525
+ if (cancelled(input.signal)) throw interrupted();
1526
+ const left = remaining();
1527
+ if (left <= 0) throw transportFailure(`The management API did not answer within ${Math.round(deadlineMs / 1e3)}s.`, mutation ? "unknown" : "not_submitted");
1528
+ const timeout = AbortSignal.timeout(left);
1529
+ const signals = [timeout, callerSignal, input.signal].filter((value) => value !== void 0);
1530
+ const signal = signals.length === 1 ? signals[0] : AbortSignal.any(signals);
1531
+ attempts += 1;
1532
+ let response;
1533
+ try {
1534
+ response = await fetchImpl(url, { method: input.method, headers, body, redirect: "manual", signal });
1535
+ } catch (cause) {
1536
+ if (cancelled(input.signal)) throw interrupted(mutation ? "unknown" : "not_submitted");
1537
+ const message = clean(cause instanceof Error ? cause.message : String(cause));
1538
+ if (!mutation && !timeout.aborted && attempts < MAX_READ_ATTEMPTS && remaining() > BASE_BACKOFF_MS) {
1539
+ await backoff(attempts, null, remaining(), input.signal);
1540
+ continue;
1541
+ }
1542
+ throw transportFailure(
1543
+ timeout.aborted ? `The management API did not answer within ${Math.round(deadlineMs / 1e3)}s.` : `The management request did not complete: ${message}`,
1544
+ mutation ? "unknown" : "not_submitted"
1545
+ );
1546
+ }
1547
+ if (response.status >= 300 && response.status < 400) {
1548
+ throw invalidInput("The management API redirected the request, so it was refused without forwarding the credential. Check the API address.");
1549
+ }
1550
+ const retryAfter = parseRetryAfter(response.headers.get("retry-after"), now());
1551
+ const correlationId = response.headers.get("x-correlation-id") ?? response.headers.get("cf-ray");
1552
+ let text;
1553
+ try {
1554
+ text = await readBounded(response, maxBytes);
1555
+ } catch (cause) {
1556
+ const message = clean(cause instanceof Error ? cause.message : String(cause));
1557
+ if (mutation && response.ok) throw unknownOutcome(`The write was accepted but its answer could not be read: ${message} Read the site's state before sending anything again.`);
1558
+ throw transportFailure(`The management answer could not be read: ${message}`, mutation ? "unknown" : "not_submitted");
1559
+ }
1560
+ let data = null;
1561
+ if (text.trim() !== "") {
1562
+ try {
1563
+ data = JSON.parse(text);
1564
+ } catch {
1565
+ if (mutation && response.ok) throw unknownOutcome("The write was accepted but its answer was not JSON. Read the site's state before sending anything again.");
1566
+ if (response.ok) throw transportFailure("The management API answered with something other than JSON.");
1567
+ data = null;
1568
+ }
1569
+ }
1570
+ if (response.ok) return { status: response.status, headers: response.headers, data, attempts };
1571
+ const refusal = refusalError({ status: response.status, body: data }, {
1572
+ exactReview: input.exactReview,
1573
+ retryAfter,
1574
+ correlationId,
1575
+ mutation: "not_submitted"
1576
+ });
1577
+ const retryable = !mutation && RETRYABLE_STATUS.has(response.status) && attempts < MAX_READ_ATTEMPTS;
1578
+ if (!retryable) throw refusal;
1579
+ const waitMs = retryAfter === null ? null : retryAfter * 1e3;
1580
+ const left2 = remaining();
1581
+ if (waitMs !== null && waitMs >= left2) throw refusal;
1582
+ if (waitMs === null && left2 <= BASE_BACKOFF_MS) throw refusal;
1583
+ try {
1584
+ await backoff(attempts, waitMs, left2, input.signal);
1585
+ } catch (cause) {
1586
+ if (cause instanceof ManagementError) throw cause;
1587
+ throw refusal;
1588
+ }
1589
+ }
1590
+ }
1591
+ async function backoff(attempt, waitMs, left, requestSignal) {
1592
+ const planned = waitMs ?? Math.min(MAX_BACKOFF_MS, BASE_BACKOFF_MS * 2 ** (attempt - 1));
1593
+ const signals = [callerSignal, requestSignal].filter((value) => value !== void 0);
1594
+ const signal = signals.length === 0 ? new AbortController().signal : AbortSignal.any(signals);
1595
+ try {
1596
+ await sleep(Math.min(planned, Math.max(0, left - 1)), signal);
1597
+ } catch (cause) {
1598
+ if (cancelled(requestSignal)) throw interrupted();
1599
+ throw cause;
1600
+ }
1601
+ }
1602
+ return {
1603
+ origin: origin.origin,
1604
+ request,
1605
+ get(path, options = {}) {
1606
+ return request({ ...options, method: "GET", path });
1607
+ }
1608
+ };
1609
+ }
1610
+ var DEFAULT_POLL_INTERVAL_MS = 2e3;
1611
+ var DEFAULT_MAX_POLL_INTERVAL_MS = 15e3;
1612
+ function defaultSleep2(ms, signal) {
1613
+ return new Promise((resolve, reject) => {
1614
+ const timer = setTimeout(() => {
1615
+ signal.removeEventListener("abort", onAbort);
1616
+ resolve();
1617
+ }, ms);
1618
+ const onAbort = () => {
1619
+ clearTimeout(timer);
1620
+ reject(signal.reason instanceof Error ? signal.reason : new Error("aborted"));
1621
+ };
1622
+ if (signal.aborted) {
1623
+ onAbort();
1624
+ return;
1625
+ }
1626
+ signal.addEventListener("abort", onAbort, { once: true });
1627
+ });
1628
+ }
1629
+ async function poll(attempt, options) {
1630
+ const { deadlineMs } = options;
1631
+ if (!Number.isFinite(deadlineMs) || deadlineMs <= 0) {
1632
+ throw invalidInput("A wait needs a positive, finite --timeout in seconds. Nothing was sent.");
1633
+ }
1634
+ const now = options.now ?? (() => Date.now());
1635
+ const sleep = options.sleep ?? defaultSleep2;
1636
+ const base = options.intervalMs ?? DEFAULT_POLL_INTERVAL_MS;
1637
+ const ceiling = options.maxIntervalMs ?? DEFAULT_MAX_POLL_INTERVAL_MS;
1638
+ const signal = options.signal ?? new AbortController().signal;
1639
+ const startedAt = now();
1640
+ const elapsed = () => now() - startedAt;
1641
+ const remaining = () => deadlineMs - elapsed();
1642
+ let attempts = 0;
1643
+ let last = null;
1644
+ for (; ; ) {
1645
+ if (signal.aborted) throw interrupted();
1646
+ const left = remaining();
1647
+ if (left <= 0) return { timed_out: true, last, attempts, elapsed_ms: elapsed() };
1648
+ attempts += 1;
1649
+ let step;
1650
+ try {
1651
+ step = await attempt({ signal, deadlineMs: left, attempt: attempts });
1652
+ } catch (cause) {
1653
+ if (signal.aborted) throw interrupted();
1654
+ if (remaining() > 0 || isManagementError(cause) && cause.detail.code === "interrupted") throw cause;
1655
+ return { timed_out: true, last, attempts, elapsed_ms: elapsed() };
1656
+ }
1657
+ last = step.value;
1658
+ if (step.done) return { timed_out: false, last, attempts, elapsed_ms: elapsed() };
1659
+ const before = remaining();
1660
+ if (before <= 0) return { timed_out: true, last, attempts, elapsed_ms: elapsed() };
1661
+ const wait = Math.min(base * 2 ** (attempts - 1), ceiling, before);
1662
+ try {
1663
+ await sleep(wait, signal);
1664
+ } catch (cause) {
1665
+ if (signal.aborted) throw interrupted();
1666
+ throw cause;
1667
+ }
1668
+ }
1669
+ }
1670
+ async function resolveTarget(client, request = {}) {
1671
+ if (request.site !== void 0 && request.site.trim() === "") throw invalidInput("--site needs a site id.");
1672
+ if (request.mutation === true && request.site === void 0) {
1673
+ throw invalidInput("This command writes, so it needs an explicit --site naming the site to act on.");
1674
+ }
1675
+ const response = await client.get("/v1/sites", { signal: request.signal, deadlineMs: request.deadlineMs });
1676
+ const parsed = SitesResponse2.safeParse(response.data);
1677
+ if (!parsed.success) {
1678
+ throw managementError(
1679
+ "transport_failure",
1680
+ "The site listing did not come back in the shape this client understands, so the target could not be verified.",
1681
+ { mutation: "not_submitted" }
1682
+ );
1683
+ }
1684
+ const sites = parsed.data.sites.map((site) => site.site_id);
1685
+ const current = parsed.data.current;
1686
+ if (request.site === void 0) return { site_id: current, current, sites, explicit: false };
1687
+ if (request.site !== current && !sites.includes(request.site)) {
1688
+ throw invalidInput(`This credential acts on ${current}, not ${request.site}. Nothing was sent. Use --site ${current}, or a credential for ${request.site}.`);
1689
+ }
1690
+ if (request.mutation === true && request.site !== current) {
1691
+ throw invalidInput(`This credential's own site is ${current}, so it may not write to ${request.site}. Nothing was sent.`);
1692
+ }
1693
+ return { site_id: request.site, current, sites, explicit: true };
1694
+ }
1695
+ var COMMAND_SCHEMA_VERSION = "saturndocs.cli.schema/v1";
1696
+ function selectorFor(name) {
1697
+ return name.replace(/ /gu, ".");
1698
+ }
1699
+ var TEXT_SOURCES = ["--text", "--file", "--data", "stdin"];
1700
+ var SITE_RULES = ["required", "inferred", "none"];
1701
+ var IDEMPOTENCY_KINDS = ["none", "submission_id", "expected_review"];
1702
+ var FlagSpecSchema = z4.object({
1703
+ name: z4.string().regex(/^--[a-z][a-z0-9-]*$/u),
1704
+ value: z4.enum(["string", "integer", "boolean"]),
1705
+ required: z4.boolean(),
1706
+ description: z4.string().min(1)
1707
+ });
1708
+ var ExitCodeSchema = z4.union(EXIT_CODES.map((code) => z4.literal(code)));
1709
+ var CommandMetadataSchema = z4.object({
1710
+ name: z4.string().min(1),
1711
+ selector: z4.string().regex(/^[a-z][a-z0-9-]*(\.[a-z][a-z0-9-]*)*$/u),
1712
+ group: z4.string().min(1),
1713
+ summary: z4.string().min(1),
1714
+ description: z4.string().min(1),
1715
+ text: z4.object({
1716
+ requirement: z4.enum(["none", "required", "optional"]),
1717
+ limit: z4.number().int().positive().nullable(),
1718
+ sources: z4.array(z4.enum(TEXT_SOURCES))
1719
+ }),
1720
+ target: z4.object({
1721
+ site: z4.enum(SITE_RULES),
1722
+ arguments: z4.array(z4.object({ name: z4.string().min(1), required: z4.boolean(), description: z4.string().min(1) }))
1723
+ }),
1724
+ permissions: z4.object({
1725
+ server: z4.enum(MANAGEMENT_SCOPES2).nullable(),
1726
+ preflight: z4.enum(MANAGEMENT_SCOPES2).nullable()
1727
+ }),
1728
+ side_effects: z4.object({
1729
+ mutation: z4.boolean(),
1730
+ effect: z4.string().min(1),
1731
+ idempotency: z4.enum(IDEMPOTENCY_KINDS)
1732
+ }),
1733
+ flags: z4.array(FlagSpecSchema),
1734
+ output: z4.object({
1735
+ fields: z4.array(z4.string().min(1)),
1736
+ ids: z4.array(z4.string().min(1)),
1737
+ limitations: z4.array(z4.enum(LIMITATION_CODES))
1738
+ }),
1739
+ errors: z4.array(z4.enum(CLIENT_ERROR_CODES)).min(1),
1740
+ exits: z4.array(ExitCodeSchema).min(1),
1741
+ examples: z4.array(z4.object({ command: z4.string().min(1), description: z4.string().min(1) })).min(1)
1742
+ }).superRefine((value, ctx) => {
1743
+ if (selectorFor(value.name) !== value.selector) {
1744
+ ctx.addIssue({ code: "custom", message: `${value.name} is spoken as ${selectorFor(value.name)}, not ${value.selector}` });
1745
+ }
1746
+ if (!value.name.startsWith(value.group)) {
1747
+ ctx.addIssue({ code: "custom", message: `${value.name} is not a command of the ${value.group} group` });
1748
+ }
1749
+ if (value.text.requirement === "none" !== (value.text.sources.length === 0)) {
1750
+ ctx.addIssue({ code: "custom", message: `${value.name} declares text sources that disagree with its text requirement` });
1751
+ }
1752
+ if (value.side_effects.mutation && value.target.site !== "required") {
1753
+ ctx.addIssue({ code: "custom", message: `${value.name} mutates and would accept whatever site the credential named` });
1754
+ }
1755
+ for (const example of value.examples) {
1756
+ if (!example.command.startsWith(`saturndocs ${value.name}`)) {
1757
+ ctx.addIssue({ code: "custom", message: `an example of ${value.name} runs ${example.command}` });
1758
+ }
1759
+ }
1760
+ });
1761
+ var TEXT_FLAGS = /* @__PURE__ */ new Set(["--text", "--file"]);
1762
+ function textSpec(requirement, limit2, flags) {
1763
+ if (requirement === "none") return { requirement, limit: null, sources: [] };
1764
+ const sources = [];
1765
+ for (const flag of flags) {
1766
+ if (flag.name === "--text") sources.push("--text");
1767
+ if (flag.name === "--file") sources.push("--file", "stdin");
1768
+ }
1769
+ return { requirement, limit: limit2, sources: Object.freeze(sources) };
1770
+ }
1771
+ function commandMetadata(name, details) {
1772
+ const spec = commandSpec(name);
1773
+ if (spec === void 0) {
1774
+ throw new Error(`${name} has no entry in the frozen P0 command surface; add it there before giving it metadata.`);
1775
+ }
1776
+ const flags = Object.freeze([...spec.flags, ...GLOBAL_FLAGS.filter((flag) => !spec.flags.some((own) => own.name === flag.name))]);
1777
+ const textFlags = spec.flags.filter((flag) => TEXT_FLAGS.has(flag.name));
1778
+ return Object.freeze({
1779
+ name: spec.name,
1780
+ selector: selectorFor(spec.name),
1781
+ group: spec.name.split(" ")[0],
1782
+ summary: spec.summary,
1783
+ description: details.description,
1784
+ text: details.text_sources === void 0 ? textSpec(spec.text_input, spec.text_limit, textFlags) : Object.freeze({ requirement: spec.text_input, limit: spec.text_input === "none" ? null : spec.text_limit, sources: Object.freeze([...details.text_sources]) }),
1785
+ target: Object.freeze({
1786
+ site: spec.requires_site ? "required" : spec.permission === null ? "none" : "inferred",
1787
+ arguments: Object.freeze([...details.target.arguments])
1788
+ }),
1789
+ permissions: Object.freeze({
1790
+ server: spec.permission,
1791
+ preflight: spec.mutation ? "manage:read" : null
1792
+ }),
1793
+ side_effects: Object.freeze({
1794
+ mutation: spec.mutation,
1795
+ effect: details.effect,
1796
+ idempotency: details.idempotency ?? "none"
1797
+ }),
1798
+ flags,
1799
+ output: Object.freeze({
1800
+ fields: Object.freeze([...details.output.fields]),
1801
+ ids: Object.freeze([...details.output.ids]),
1802
+ limitations: Object.freeze([...details.output.limitations])
1803
+ }),
1804
+ errors: Object.freeze([...details.errors]),
1805
+ exits: Object.freeze([...spec.exits]),
1806
+ examples: Object.freeze([...details.examples])
1807
+ });
1808
+ }
1809
+ function parseResponse(schema, data, what) {
1810
+ const parsed = schema.safeParse(data);
1811
+ if (!parsed.success) {
1812
+ throw managementError("transport_failure", `${what} did not come back in the shape this client understands, so nothing was reported from it.`, {
1813
+ mutation: "not_submitted"
1814
+ });
1815
+ }
1816
+ return parsed.data;
1817
+ }
1818
+ async function listSites(client, options = {}) {
1819
+ const response = await client.get(operationPath("sites.list", ""), options);
1820
+ return parseResponse(SitesResponse2, response.data, "The site listing");
1821
+ }
1822
+ async function getSite(client, siteId, options = {}) {
1823
+ const response = await client.get(operationPath("site.get", siteId), options);
1824
+ return parseResponse(PortalSite2, response.data, `The site read for ${siteId}`);
1825
+ }
1826
+ async function getDocsRepo(client, siteId, options = {}) {
1827
+ const response = await client.get(operationPath("docs-repo.get", siteId), options);
1828
+ return parseResponse(DocsRepoSettings2, response.data, `The documentation repository read for ${siteId}`);
1829
+ }
1830
+ async function listProductConnections(client, siteId, options = {}) {
1831
+ const response = await client.get(operationPath("product-connections.list", siteId), options);
1832
+ return parseResponse(ProductConnectionsResponse2, response.data, `The watched repositories read for ${siteId}`);
1833
+ }
1834
+ async function readSiteDetail(client, siteId, options = {}) {
1835
+ const { connections = false, ...read } = options;
1836
+ if (!connections) return { site_id: siteId, site: await getSite(client, siteId, read), connections: null };
1837
+ const [site, docsRepo, productConnections] = await Promise.all([
1838
+ getSite(client, siteId, read),
1839
+ getDocsRepo(client, siteId, read),
1840
+ listProductConnections(client, siteId, read)
1841
+ ]);
1842
+ return { site_id: siteId, site, connections: { docs_repo: docsRepo, product_connections: productConnections } };
1843
+ }
1844
+ var PAGE_TRAFFIC_WINDOWS = [7, 30, 90];
1845
+ function pagesUnavailable(result) {
1846
+ return "unavailable" in result;
1847
+ }
1848
+ async function listPages(client, siteId, options = {}) {
1849
+ const { days, ...read } = options;
1850
+ if (days !== void 0 && !PAGE_TRAFFIC_WINDOWS.includes(days)) {
1851
+ throw invalidInput(`--days takes ${PAGE_TRAFFIC_WINDOWS.join(", ")}. The server keeps no other traffic window.`);
1852
+ }
1853
+ const response = await client.get(operationPath("pages.list", siteId), {
1854
+ ...read,
1855
+ ...days === void 0 ? {} : { query: { days } }
1856
+ });
1857
+ return parseResponse(PagesResponse2, response.data, `The published page listing for ${siteId}`);
1858
+ }
1859
+ var ActivityPageRead = ActivityPage2.extend({ events: z5.array(ActivityEvent.loose()) });
1860
+ var ACTIVITY_OUTCOMES = ACTIVITY_OUTCOME_FILTERS;
1861
+ var BOUNDS = operationOf("activity.list").query;
1862
+ function boundOf(name) {
1863
+ const bound2 = BOUNDS.find((entry) => entry.name === name);
1864
+ if (bound2 === void 0) throw new Error(`The frozen activity.list operation declares no ${name} bound.`);
1865
+ return bound2;
1866
+ }
1867
+ var ACTIVITY_LIMIT_CEILING = boundOf("limit").max ?? 100;
1868
+ var ACTIVITY_REPO_MAX_CHARS = boundOf("repo").max ?? 200;
1869
+ var ACTIVITY_JOB_ID_MAX_CHARS = boundOf("job_id").max ?? 200;
1870
+ var UTC_DATE = z5.iso.date();
1871
+ function dateFilter(name, value) {
1872
+ if (!UTC_DATE.safeParse(value).success) {
1873
+ throw invalidInput(`--${name} takes an inclusive UTC date as YYYY-MM-DD; ${value} is not one.`);
1874
+ }
1875
+ return value;
1876
+ }
1877
+ function activityQuery(filters) {
1878
+ const query = {};
1879
+ if (filters.cursor !== void 0) {
1880
+ if (filters.cursor === "") throw invalidInput("--cursor takes the next_cursor of the preceding page.");
1881
+ query.cursor = filters.cursor;
1882
+ }
1883
+ if (filters.limit !== void 0) {
1884
+ if (!Number.isInteger(filters.limit) || filters.limit < 1) throw invalidInput("--limit takes a whole number of events, at least 1.");
1885
+ query.limit = filters.limit;
1886
+ }
1887
+ if (filters.outcome !== void 0) {
1888
+ if (!ACTIVITY_OUTCOMES.includes(filters.outcome)) {
1889
+ throw invalidInput(`--outcome takes ${ACTIVITY_OUTCOMES.join(", ")}. The server filters on no others.`);
1890
+ }
1891
+ query.outcome = filters.outcome;
1892
+ }
1893
+ if (filters.repo !== void 0) {
1894
+ if (filters.repo === "" || filters.repo.length > ACTIVITY_REPO_MAX_CHARS) {
1895
+ throw invalidInput(`--repo takes a full repository name of 1 to ${ACTIVITY_REPO_MAX_CHARS} characters, such as example/docs.`);
1896
+ }
1897
+ query.repo = filters.repo;
1898
+ }
1899
+ if (filters.from !== void 0) query.from = dateFilter("from", filters.from);
1900
+ if (filters.to !== void 0) query.to = dateFilter("to", filters.to);
1901
+ if (query.from !== void 0 && query.to !== void 0 && String(query.from) > String(query.to)) {
1902
+ throw invalidInput(`--from ${String(query.from)} is later than --to ${String(query.to)}, which can only ever match nothing. Nothing was read.`);
1903
+ }
1904
+ return query;
1905
+ }
1906
+ async function listActivity(client, siteId, options = {}) {
1907
+ const { cursor: cursor2, limit: limit2, outcome, repo, from, to, ...read } = options;
1908
+ const query = activityQuery({ cursor: cursor2, limit: limit2, outcome, repo, from, to });
1909
+ const response = await client.get(operationPath("activity.list", siteId), {
1910
+ ...read,
1911
+ ...Object.keys(query).length === 0 ? {} : { query }
1912
+ });
1913
+ return parseResponse(ActivityPageRead, response.data, `The activity listing for ${siteId}`);
1914
+ }
1915
+ function activityJobFound(read) {
1916
+ return read.event !== null;
1917
+ }
1918
+ async function readActivityJob(client, siteId, jobId, options = {}) {
1919
+ if (jobId === "" || jobId.length > ACTIVITY_JOB_ID_MAX_CHARS) {
1920
+ throw invalidInput(`A job id is 1 to ${ACTIVITY_JOB_ID_MAX_CHARS} characters; the server mints no others.`);
1921
+ }
1922
+ const response = await client.get(operationPath("activity.list", siteId), { ...options, query: { job_id: jobId } });
1923
+ const page = parseResponse(ActivityPageRead, response.data, `The activity read of ${jobId}`);
1924
+ return { site_id: page.site_id, job_id: jobId, event: page.events[0] ?? null };
1925
+ }
1926
+ var MAX_LIMIT = 50;
1927
+ function isTab(value) {
1928
+ return SUGGESTION_TABS.includes(value);
1929
+ }
1930
+ function parseTab(value) {
1931
+ if (value === void 0) return void 0;
1932
+ const trimmed = value.trim();
1933
+ if (!isTab(trimmed)) {
1934
+ throw invalidInput(`--state takes one of ${SUGGESTION_TABS.join(", ")}; ${value} is not a suggestion tab.`);
1935
+ }
1936
+ return trimmed;
1937
+ }
1938
+ function parseCursor(value) {
1939
+ if (value === void 0) return void 0;
1940
+ if (value.trim() === "") throw invalidInput("--cursor needs the next_cursor of the preceding page.");
1941
+ return value;
1942
+ }
1943
+ function parseLimit(value) {
1944
+ if (value === void 0) return void 0;
1945
+ if (!Number.isInteger(value) || value < 1 || value > MAX_LIMIT) {
1946
+ throw invalidInput(`--limit takes a whole number from 1 to ${MAX_LIMIT}.`);
1947
+ }
1948
+ return value;
1949
+ }
1950
+ function unreadable(what) {
1951
+ throw managementError("transport_failure", `The ${what} did not come back in the shape this client understands, so nothing is reported rather than a partial reading of it.`);
1952
+ }
1953
+ async function listSuggestions(client, input) {
1954
+ const state = parseTab(input.state);
1955
+ const response = await client.get(resourcePath(input.site, ["suggestions"]), {
1956
+ query: { state, cursor: parseCursor(input.cursor), limit: parseLimit(input.limit) },
1957
+ signal: input.signal,
1958
+ deadlineMs: input.deadlineMs
1959
+ });
1960
+ const parsed = SuggestionsPage2.safeParse(response.data);
1961
+ if (!parsed.success) unreadable("suggestion listing");
1962
+ return parsed.data;
1963
+ }
1964
+ async function getSuggestion(client, input) {
1965
+ if (input.suggestionId.trim() === "") throw invalidInput("Name the suggestion to read, as saturndocs suggestions get <suggestion-id>.");
1966
+ const response = await client.get(resourcePath(input.site, ["suggestions", input.suggestionId]), {
1967
+ signal: input.signal,
1968
+ deadlineMs: input.deadlineMs
1969
+ });
1970
+ const parsed = SuggestionDetailResponse2.safeParse(response.data);
1971
+ if (!parsed.success) unreadable("suggestion detail");
1972
+ return parsed.data;
1973
+ }
1974
+ function suggestionIds(suggestion) {
1975
+ const ids = { suggestion_id: suggestion.suggestion_id, base_sha: suggestion.base_sha, pr_number: suggestion.pr_number };
1976
+ if (typeof suggestion.head_sha === "string") ids.head_sha = suggestion.head_sha;
1977
+ if (typeof suggestion.request_thread_id === "string") ids.thread_id = suggestion.request_thread_id;
1978
+ const jobId = suggestion.publication?.job_id ?? suggestion.job_id;
1979
+ if (typeof jobId === "string") ids.job_id = jobId;
1980
+ const releaseId = suggestion.publication?.release_id;
1981
+ if (typeof releaseId === "string") ids.release_id = releaseId;
1982
+ return ids;
1983
+ }
1984
+ function pageContentAvailable(suggestion) {
1985
+ const pages = suggestion.pages ?? [];
1986
+ if (pages.length === 0) return false;
1987
+ return pages.some((page) => (page.diff?.length ?? 0) > 0 || typeof page.before === "string" || typeof page.after === "string");
1988
+ }
1989
+ function suggestionLimitations(suggestion) {
1990
+ const limitations = [];
1991
+ if (typeof suggestion.head_edited_by === "string" && suggestion.head_edited_by !== "") {
1992
+ limitations.push({
1993
+ code: "hand_edited",
1994
+ message: `The head of this suggestion was edited on GitHub by ${suggestion.head_edited_by}, so what publishes is not only what the agent drafted. Read ${suggestion.files_changed_url ?? "the pull request"} before approving.`,
1995
+ at: "suggestion.head_sha"
1996
+ });
1997
+ }
1998
+ if (suggestion.snapshots_available === false || suggestion.pages !== void 0 && !pageContentAvailable(suggestion)) {
1999
+ limitations.push({
2000
+ code: "content_unavailable",
2001
+ message: "The server did not supply page content for this revision, so the changed pages are listed without their text. This is not a suggestion that changes nothing.",
2002
+ at: "suggestion.pages"
2003
+ });
2004
+ }
2005
+ if (typeof suggestion.files_changed_url !== "string" || suggestion.files_changed_url === "") {
2006
+ limitations.push({
2007
+ code: "section_unavailable",
2008
+ message: "No pull-request URL was recorded for this suggestion, so there is no GitHub diff to compare this reading against.",
2009
+ at: "suggestion.files_changed_url"
2010
+ });
2011
+ }
2012
+ return Object.freeze(limitations);
2013
+ }
2014
+ var DEFAULT_DIFF_MAX_BYTES = 64 * 1024;
2015
+ var CONTEXT_LINES = 3;
2016
+ var MAX_CELLS = 4e5;
2017
+ var MAX_INPUT_LINES = 3e3;
2018
+ function splitLines(text) {
2019
+ if (text === "") return [];
2020
+ const normalized = text.replace(/\r\n?/gu, "\n");
2021
+ return (normalized.endsWith("\n") ? normalized.slice(0, -1) : normalized).split("\n");
2022
+ }
2023
+ function lcsChanges(before, after) {
2024
+ const cols = after.length + 1;
2025
+ const table = new Int32Array((before.length + 1) * cols);
2026
+ for (let i2 = before.length - 1; i2 >= 0; i2 -= 1) {
2027
+ for (let j2 = after.length - 1; j2 >= 0; j2 -= 1) {
2028
+ table[i2 * cols + j2] = before[i2] === after[j2] ? table[(i2 + 1) * cols + j2 + 1] + 1 : Math.max(table[(i2 + 1) * cols + j2], table[i2 * cols + j2 + 1]);
2029
+ }
2030
+ }
2031
+ const changes = [];
2032
+ let i = 0;
2033
+ let j = 0;
2034
+ while (i < before.length && j < after.length) {
2035
+ if (before[i] === after[j]) {
2036
+ changes.push({ op: "context", text: before[i] });
2037
+ i += 1;
2038
+ j += 1;
2039
+ } else if (table[(i + 1) * cols + j] >= table[i * cols + j + 1]) {
2040
+ changes.push({ op: "del", text: before[i] });
2041
+ i += 1;
2042
+ } else {
2043
+ changes.push({ op: "add", text: after[j] });
2044
+ j += 1;
2045
+ }
2046
+ }
2047
+ for (; i < before.length; i += 1) changes.push({ op: "del", text: before[i] });
2048
+ for (; j < after.length; j += 1) changes.push({ op: "add", text: after[j] });
2049
+ return changes;
2050
+ }
2051
+ function collapseContext(rows) {
2052
+ const keep = new Array(rows.length).fill(false);
2053
+ for (let index = 0; index < rows.length; index += 1) {
2054
+ if (rows[index].op === "context") continue;
2055
+ for (let near = Math.max(0, index - CONTEXT_LINES); near <= Math.min(rows.length - 1, index + CONTEXT_LINES); near += 1) keep[near] = true;
2056
+ }
2057
+ const out = [];
2058
+ let skipped = 0;
2059
+ for (let index = 0; index < rows.length; index += 1) {
2060
+ if (keep[index] === true) {
2061
+ if (skipped > 0) {
2062
+ out.push({ op: "skip", before_line: null, after_line: null, text: null, count: skipped });
2063
+ skipped = 0;
2064
+ }
2065
+ out.push(rows[index]);
2066
+ continue;
2067
+ }
2068
+ skipped += 1;
2069
+ }
2070
+ if (skipped > 0) out.push({ op: "skip", before_line: null, after_line: null, text: null, count: skipped });
2071
+ return out;
2072
+ }
2073
+ function compareContent(before, after) {
2074
+ const beforeAll = splitLines(before);
2075
+ const afterAll = splitLines(after);
2076
+ const overLength = beforeAll.length > MAX_INPUT_LINES || afterAll.length > MAX_INPUT_LINES;
2077
+ const beforeLines = beforeAll.slice(0, MAX_INPUT_LINES);
2078
+ const afterLines = afterAll.slice(0, MAX_INPUT_LINES);
2079
+ let head = 0;
2080
+ while (head < beforeLines.length && head < afterLines.length && beforeLines[head] === afterLines[head]) head += 1;
2081
+ let tail = 0;
2082
+ while (tail < beforeLines.length - head && tail < afterLines.length - head && beforeLines[beforeLines.length - 1 - tail] === afterLines[afterLines.length - 1 - tail]) tail += 1;
2083
+ const midBefore = beforeLines.slice(head, beforeLines.length - tail);
2084
+ const midAfter = afterLines.slice(head, afterLines.length - tail);
2085
+ const changes = beforeLines.slice(0, head).map((text) => ({ op: "context", text }));
2086
+ let truncated = overLength;
2087
+ if ((midBefore.length + 1) * (midAfter.length + 1) > MAX_CELLS) {
2088
+ truncated = true;
2089
+ for (const text of midBefore) changes.push({ op: "del", text });
2090
+ for (const text of midAfter) changes.push({ op: "add", text });
2091
+ } else {
2092
+ changes.push(...lcsChanges(midBefore, midAfter));
2093
+ }
2094
+ for (const text of beforeLines.slice(beforeLines.length - tail)) changes.push({ op: "context", text });
2095
+ const numbered = [];
2096
+ let beforeNumber = 0;
2097
+ let afterNumber = 0;
2098
+ let added = 0;
2099
+ let removed = 0;
2100
+ for (const change of changes) {
2101
+ if (change.op === "context") {
2102
+ beforeNumber += 1;
2103
+ afterNumber += 1;
2104
+ numbered.push({ op: "context", before_line: beforeNumber, after_line: afterNumber, text: change.text, count: null });
2105
+ } else if (change.op === "del") {
2106
+ beforeNumber += 1;
2107
+ removed += 1;
2108
+ numbered.push({ op: "del", before_line: beforeNumber, after_line: null, text: change.text, count: null });
2109
+ } else {
2110
+ afterNumber += 1;
2111
+ added += 1;
2112
+ numbered.push({ op: "add", before_line: null, after_line: afterNumber, text: change.text, count: null });
2113
+ }
2114
+ }
2115
+ return { rows: collapseContext(numbered), added, removed, truncated };
2116
+ }
2117
+ function serverRows(page) {
2118
+ return (page.diff ?? []).map((line) => ({
2119
+ op: line.op,
2120
+ before_line: line.before_line ?? null,
2121
+ after_line: line.after_line ?? null,
2122
+ text: line.text ?? null,
2123
+ count: line.count ?? null
2124
+ }));
2125
+ }
2126
+ var has = (value) => typeof value === "string";
2127
+ function pageDiff(page) {
2128
+ const base = {
2129
+ path: page.path,
2130
+ op: page.op,
2131
+ reason: page.reason ?? null,
2132
+ url: page.url ?? null,
2133
+ truncated: page.truncated === true,
2134
+ omitted_rows: 0,
2135
+ has_before: has(page.before),
2136
+ has_after: has(page.after)
2137
+ };
2138
+ const supplied = serverRows(page);
2139
+ if (supplied.length > 0) {
2140
+ return { ...base, availability: "server_diff", source: "server", added_lines: page.added_lines ?? null, removed_lines: page.removed_lines ?? null, rows: supplied };
2141
+ }
2142
+ if (has(page.before) && has(page.after)) {
2143
+ const local = compareContent(page.before, page.after);
2144
+ return { ...base, availability: "computed", source: "local", added_lines: local.added, removed_lines: local.removed, truncated: base.truncated || local.truncated, rows: local.rows };
2145
+ }
2146
+ if (page.op === "delete" && has(page.before)) {
2147
+ const local = compareContent(page.before, "");
2148
+ return { ...base, availability: "deletion", source: "local", added_lines: local.added, removed_lines: local.removed, truncated: base.truncated || local.truncated, rows: local.rows };
2149
+ }
2150
+ if (page.op === "create" && has(page.after)) {
2151
+ const local = compareContent("", page.after);
2152
+ return { ...base, availability: "creation", source: "local", added_lines: local.added, removed_lines: local.removed, truncated: base.truncated || local.truncated, rows: local.rows };
2153
+ }
2154
+ if (has(page.before) || has(page.after)) {
2155
+ return { ...base, availability: "partial", source: "none", added_lines: null, removed_lines: null, rows: [] };
2156
+ }
2157
+ return { ...base, availability: "unavailable", source: "none", added_lines: null, removed_lines: null, rows: [] };
2158
+ }
2159
+ function rowBytes(row) {
2160
+ return new TextEncoder().encode(row.text ?? "").length + 8;
2161
+ }
2162
+ function bound(pages, maxBytes) {
2163
+ const out = [];
2164
+ let used = 0;
2165
+ let omitted = 0;
2166
+ for (const page of pages) {
2167
+ used += new TextEncoder().encode(page.path).length + 16;
2168
+ const kept = [];
2169
+ let dropped = 0;
2170
+ for (const row of page.rows) {
2171
+ if (dropped > 0 || used + rowBytes(row) > maxBytes) {
2172
+ dropped += 1;
2173
+ continue;
2174
+ }
2175
+ used += rowBytes(row);
2176
+ kept.push(row);
2177
+ }
2178
+ if (dropped > 0) kept.push({ op: "omitted", before_line: null, after_line: null, text: null, count: dropped });
2179
+ omitted += dropped;
2180
+ out.push({ ...page, rows: kept, omitted_rows: dropped });
2181
+ }
2182
+ return { pages: out, used, omitted };
2183
+ }
2184
+ var HANDOFF = "This is the content the server supplied, formatted here. Nothing in it was checked against the product, so it is a starting point for an owner's review, not a completed one.";
2185
+ function limitationsFor(suggestion, pages, omitted, limit2) {
2186
+ const limitations = [
2187
+ { code: "review_incomplete", message: HANDOFF, at: null }
2188
+ ];
2189
+ pages.forEach((page, index) => {
2190
+ const at = `pages[${index}]`;
2191
+ if (page.availability === "unavailable") {
2192
+ limitations.push({ code: "content_unavailable", message: `Neither side of ${page.path} was supplied, so this page has no diff at all. An empty diff here would read as a page that does not change.`, at });
2193
+ }
2194
+ if (page.availability === "partial") {
2195
+ const missing = page.has_before ? "after" : "before";
2196
+ limitations.push({ code: "content_unavailable", message: `The ${missing} side of ${page.path} was not supplied, so this ${page.op} cannot be compared. Nothing is shown for it rather than half of it read as the whole change.`, at });
2197
+ }
2198
+ if (page.truncated) {
2199
+ limitations.push({ code: "page_bounded", message: `The comparison of ${page.path} hit a size bound, so this diff is part of the change and not all of it.`, at });
2200
+ }
2201
+ });
2202
+ if (omitted > 0) {
2203
+ limitations.push({ code: "output_truncated", message: `${omitted} diff lines were left out to stay inside the ${limit2}-byte output bound. Read one page at a time with --path, or read the pull request.`, at: null });
2204
+ }
2205
+ if (typeof suggestion.head_edited_by === "string" && suggestion.head_edited_by !== "") {
2206
+ limitations.push({ code: "hand_edited", message: `The head was edited on GitHub by ${suggestion.head_edited_by} after the draft, so this diff of the supplied content may not be what would publish.`, at: "head_sha" });
2207
+ }
2208
+ if (typeof suggestion.files_changed_url !== "string" || suggestion.files_changed_url === "") {
2209
+ limitations.push({ code: "section_unavailable", message: "No pull-request URL was recorded, so this reading cannot be checked against GitHub's own diff.", at: "files_changed_url" });
2210
+ }
2211
+ return limitations;
2212
+ }
2213
+ function suggestionDiff(suggestion, options = {}) {
2214
+ const limit2 = options.maxBytes ?? DEFAULT_DIFF_MAX_BYTES;
2215
+ const all = suggestion.pages ?? [];
2216
+ const selected = options.path === void 0 ? all : all.filter((page) => page.path === options.path);
2217
+ if (options.path !== void 0 && selected.length === 0) {
2218
+ const known = all.map((page) => page.path);
2219
+ throw invalidInput(
2220
+ known.length === 0 ? `This suggestion lists no changed pages, so --path ${options.path} names nothing to diff.` : `--path ${options.path} is not a page this suggestion changes. It changes: ${known.join(", ")}.`
2221
+ );
2222
+ }
2223
+ const computed = selected.map((page) => pageDiff(page));
2224
+ const bounded = bound(computed, limit2);
2225
+ return {
2226
+ suggestion_id: suggestion.suggestion_id,
2227
+ base_sha: suggestion.base_sha,
2228
+ head_sha: suggestion.head_sha ?? null,
2229
+ revision: suggestion.revision ?? null,
2230
+ files_changed_url: suggestion.files_changed_url ?? null,
2231
+ pages: Object.freeze(bounded.pages),
2232
+ bytes: { limit: limit2, used: bounded.used, omitted: bounded.omitted },
2233
+ review: { complete: false, handoff: HANDOFF },
2234
+ limitations: Object.freeze(limitationsFor(suggestion, bounded.pages, bounded.omitted, limit2))
2235
+ };
2236
+ }
2237
+ function rowText(row) {
2238
+ if (row.op === "skip") return `@@ ${row.count ?? 0} unchanged lines @@`;
2239
+ if (row.op === "omitted") return `... ${row.count ?? 0} lines omitted by the output bound ...`;
2240
+ const marker = row.op === "add" ? "+" : row.op === "del" ? "-" : " ";
2241
+ return `${marker}${row.text ?? ""}`;
2242
+ }
2243
+ var AVAILABILITY_TEXT = Object.freeze({
2244
+ server_diff: "diff supplied by the server",
2245
+ computed: "compared here from the supplied before and after",
2246
+ creation: "new page; there is no before",
2247
+ deletion: "page removed; there is no after",
2248
+ partial: "only one side was supplied; not comparable",
2249
+ unavailable: "neither side was supplied; nothing to compare"
2250
+ });
2251
+ function renderDiffText(result) {
2252
+ const out = [
2253
+ `suggestion ${result.suggestion_id} revision ${result.revision ?? "unknown"}`,
2254
+ `base ${result.base_sha}`,
2255
+ `head ${result.head_sha ?? "unknown"}`,
2256
+ `files changed: ${result.files_changed_url ?? "no pull-request URL was recorded"}`,
2257
+ ""
2258
+ ];
2259
+ if (result.pages.length === 0) out.push("This suggestion lists no changed pages.", "");
2260
+ for (const page of result.pages) {
2261
+ out.push(`--- a/${page.path}`, `+++ b/${page.path}`, `# ${page.op}: ${AVAILABILITY_TEXT[page.availability]}`);
2262
+ if (page.reason !== null) out.push(`# reason: ${page.reason}`);
2263
+ for (const row of page.rows) out.push(rowText(row));
2264
+ out.push("");
2265
+ }
2266
+ for (const limitation of result.limitations) out.push(`note (${limitation.code}): ${limitation.message}`);
2267
+ return out.join("\n");
2268
+ }
2269
+ var DISMISS_REASONS = Object.freeze([...DismissInput.shape.reason.options]);
2270
+ function requireSuggestionId(value, usage) {
2271
+ if (value.trim() === "") throw invalidInput(`Name the suggestion to act on, as ${usage}.`);
2272
+ return value;
2273
+ }
2274
+ function parseDismissReason(value) {
2275
+ if (typeof value !== "string" || !DISMISS_REASONS.includes(value)) {
2276
+ throw invalidInput(`--reason takes one of ${DISMISS_REASONS.join(", ")}; the server stores no other reason.`);
2277
+ }
2278
+ return value;
2279
+ }
2280
+ function staleReview(error, reviewed, verb) {
2281
+ const message = `Nothing was ${verb}: the server did not find this suggestion at the revision that was reviewed (base ${reviewed.base_sha}, head ${reviewed.head_sha}). A head that moved after the review is answered the same way as an id this site does not have, so read the suggestion again and review what it holds now. Sending this again with the current hashes would act on a revision nobody has read.`;
2282
+ return new ManagementError(clientError("stale_review", message, { server: error.detail.server }), { cause: error });
2283
+ }
2284
+ function withStaleReview(promise, reviewed, verb) {
2285
+ return promise.catch((error) => {
2286
+ if (isManagementError(error) && error.detail.code === "stale_review") throw staleReview(error, reviewed, verb);
2287
+ throw error;
2288
+ });
2289
+ }
2290
+ function unreadable2(what) {
2291
+ throw managementError(
2292
+ "unknown_outcome",
2293
+ `The ${what} was accepted and its answer was not in the shape this client understands, so what happened is not reported here. Read the suggestion before sending anything again.`
2294
+ );
2295
+ }
2296
+ function optionalNumber(value) {
2297
+ return typeof value === "number" ? value : null;
2298
+ }
2299
+ function optionalString(value) {
2300
+ return typeof value === "string" && value !== "" ? value : null;
2301
+ }
2302
+ async function approveSuggestion(client, input) {
2303
+ const suggestionId = requireSuggestionId(input.suggestionId, "saturndocs suggestions approve <suggestion-id>");
2304
+ const reviewed = parseReviewedRevision(input.base, input.head);
2305
+ const response = await withStaleReview(
2306
+ client.request({
2307
+ method: "POST",
2308
+ path: resourcePath(input.site, ["suggestions", suggestionId, "approve"]),
2309
+ body: { expected_base_sha: reviewed.base_sha, expected_head_sha: reviewed.head_sha },
2310
+ exactReview: true,
2311
+ signal: input.signal,
2312
+ deadlineMs: input.deadlineMs
2313
+ }),
2314
+ reviewed,
2315
+ "approved"
2316
+ );
2317
+ const parsed = ApproveOutcome2.safeParse(response.data);
2318
+ if (!parsed.success) unreadable2("approval");
2319
+ const outcome = parsed.data;
2320
+ const state = outcome.state;
2321
+ if (state === void 0) unreadable2("approval");
2322
+ const limitations = [];
2323
+ if (state === "revising") {
2324
+ limitations.push({
2325
+ code: "review_incomplete",
2326
+ message: "The approval did not publish: it produced another revision, which nobody has reviewed. Read the new revision and review it before approving again.",
2327
+ at: "state"
2328
+ });
2329
+ }
2330
+ if (state === "publishing") {
2331
+ limitations.push({
2332
+ code: "activation_unknown",
2333
+ message: "The merge was accepted and the build has started. Nothing here says a release went live; follow the suggestion or its release to find out whether readers see the change.",
2334
+ at: "state"
2335
+ });
2336
+ }
2337
+ return {
2338
+ suggestion_id: suggestionId,
2339
+ base_sha: reviewed.base_sha,
2340
+ head_sha: reviewed.head_sha,
2341
+ state,
2342
+ published: false,
2343
+ publication_started: state === "publishing",
2344
+ review_required: state === "revising",
2345
+ pr_number: optionalNumber(outcome.pr_number),
2346
+ merge_commit_sha: optionalString(outcome.merge_commit_sha),
2347
+ failure: state === "failed" ? { code: optionalString(outcome.failure) ?? optionalString(outcome.error), reason: optionalString(outcome.failure_reason) ?? optionalString(outcome.message) ?? optionalString(outcome.reason) } : null,
2348
+ outcome,
2349
+ limitations: Object.freeze(limitations)
2350
+ };
2351
+ }
2352
+ async function dismissSuggestion(client, input) {
2353
+ const suggestionId = requireSuggestionId(input.suggestionId, "saturndocs suggestions dismiss <suggestion-id>");
2354
+ const reviewed = parseReviewedRevision(input.base, input.head);
2355
+ const reason = parseDismissReason(input.reason);
2356
+ const note = input.note === void 0 || input.note === null || input.note === "" ? void 0 : input.note;
2357
+ const body = DismissInput.safeParse({
2358
+ expected_base_sha: reviewed.base_sha,
2359
+ expected_head_sha: reviewed.head_sha,
2360
+ reason,
2361
+ ...note === void 0 ? {} : { note }
2362
+ });
2363
+ if (!body.success) {
2364
+ throw invalidInput(`The dismissal note is longer than the server accepts, so nothing was sent.`);
2365
+ }
2366
+ const response = await withStaleReview(
2367
+ client.request({
2368
+ method: "POST",
2369
+ path: resourcePath(input.site, ["suggestions", suggestionId, "dismiss"]),
2370
+ body: body.data,
2371
+ exactReview: true,
2372
+ signal: input.signal,
2373
+ deadlineMs: input.deadlineMs
2374
+ }),
2375
+ reviewed,
2376
+ "dismissed"
2377
+ );
2378
+ const parsed = DismissOutcome2.safeParse(response.data);
2379
+ if (!parsed.success) unreadable2("dismissal");
2380
+ const outcome = parsed.data;
2381
+ if (outcome.state !== "dismissed") unreadable2("dismissal");
2382
+ const prNumber = optionalNumber(outcome.pr_number);
2383
+ const closed = typeof outcome.pull_request_closed === "boolean" ? outcome.pull_request_closed : null;
2384
+ const limitations = [];
2385
+ if (closed === false) {
2386
+ limitations.push({
2387
+ code: "cleanup_incomplete",
2388
+ message: `The suggestion is dismissed and its pull request${prNumber === null ? "" : ` #${prNumber}`} is still open. Close it on GitHub; nothing here will close it.`,
2389
+ at: "pull_request_closed"
2390
+ });
2391
+ } else if (closed === null && prNumber !== null) {
2392
+ limitations.push({
2393
+ code: "cleanup_incomplete",
2394
+ message: `The suggestion is dismissed and the server did not say whether pull request #${prNumber} was closed. Check it on GitHub rather than reading silence as a closed one.`,
2395
+ at: "pull_request_closed"
2396
+ });
2397
+ }
2398
+ return {
2399
+ suggestion_id: suggestionId,
2400
+ base_sha: reviewed.base_sha,
2401
+ head_sha: reviewed.head_sha,
2402
+ state: "dismissed",
2403
+ reason,
2404
+ pr_number: prNumber,
2405
+ pull_request_closed: closed,
2406
+ cleanup_complete: limitations.length === 0,
2407
+ outcome,
2408
+ limitations: Object.freeze(limitations)
2409
+ };
2410
+ }
2411
+ function reviewDecisionIds(result) {
2412
+ return {
2413
+ suggestion_id: result.suggestion_id,
2414
+ base_sha: result.base_sha,
2415
+ head_sha: result.head_sha,
2416
+ pr_number: result.pr_number
2417
+ };
2418
+ }
2419
+ function optionalNumber2(value) {
2420
+ return typeof value === "number" ? value : null;
2421
+ }
2422
+ function optionalString2(value) {
2423
+ return typeof value === "string" && value !== "" ? value : null;
2424
+ }
2425
+ function unreadable3() {
2426
+ throw managementError(
2427
+ "unknown_outcome",
2428
+ "The retry was accepted and its answer was not in a shape this client understands, so what it started is not reported here. Read the suggestion and its job before sending anything again."
2429
+ );
2430
+ }
2431
+ function notRetryable(reason, server) {
2432
+ const message = reason === null ? "Nothing was retried: the server refused this suggestion as not retryable and gave no reason. Read the suggestion before acting on it again." : `Nothing was retried: ${reason}`;
2433
+ return new ManagementError(clientError("conflict", message, { server }));
2434
+ }
2435
+ function analysisResult(suggestionId, outcome) {
2436
+ return {
2437
+ kind: "analysis",
2438
+ suggestion_id: suggestionId,
2439
+ job_id: outcome.job_id,
2440
+ attempt: outcome.attempt,
2441
+ budget_tokens: outcome.budget_tokens ?? null,
2442
+ completed: false,
2443
+ outcome,
2444
+ limitations: Object.freeze([])
2445
+ };
2446
+ }
2447
+ function publicationResult(suggestionId, outcome, state) {
2448
+ const limitations = [];
2449
+ if (state === "revising") {
2450
+ limitations.push({
2451
+ code: "review_incomplete",
2452
+ message: "The retry did not publish: it produced another revision, which nobody has reviewed. This is not an approval of that revision; read it and review it before approving anything.",
2453
+ at: "state"
2454
+ });
2455
+ }
2456
+ if (state === "publishing") {
2457
+ limitations.push({
2458
+ code: "activation_unknown",
2459
+ message: "The merge was accepted and the build has started. Nothing here says a release went live; follow the suggestion or its release to find out whether readers see the change.",
2460
+ at: "state"
2461
+ });
2462
+ }
2463
+ return {
2464
+ kind: "publication",
2465
+ suggestion_id: suggestionId,
2466
+ state,
2467
+ published: false,
2468
+ publication_started: state === "publishing",
2469
+ review_required: state === "revising",
2470
+ pr_number: optionalNumber2(outcome.pr_number),
2471
+ merge_commit_sha: optionalString2(outcome.merge_commit_sha),
2472
+ failure: state === "failed" ? {
2473
+ code: optionalString2(outcome.failure) ?? optionalString2(outcome.error),
2474
+ reason: optionalString2(outcome.failure_reason) ?? optionalString2(outcome.message) ?? optionalString2(outcome.reason)
2475
+ } : null,
2476
+ outcome,
2477
+ limitations: Object.freeze(limitations)
2478
+ };
2479
+ }
2480
+ async function retrySuggestion(client, input) {
2481
+ const suggestionId = input.suggestionId.trim();
2482
+ if (suggestionId === "") throw invalidInput("Name the suggestion to retry, as saturndocs suggestions retry <suggestion-id>.");
2483
+ let response;
2484
+ try {
2485
+ response = await client.request({
2486
+ method: "POST",
2487
+ path: resourcePath(input.site, ["suggestions", suggestionId, "retry"]),
2488
+ body: {},
2489
+ signal: input.signal,
2490
+ deadlineMs: input.deadlineMs
2491
+ });
2492
+ } catch (error) {
2493
+ if (isManagementError(error) && error.detail.server?.code === "not_retryable") {
2494
+ throw notRetryable(error.detail.server.message, error.detail.server);
2495
+ }
2496
+ throw error;
2497
+ }
2498
+ const analysis = AnalysisRetryAccepted.safeParse(response.data);
2499
+ if (analysis.success) return analysisResult(suggestionId, analysis.data);
2500
+ const refused = NotRetryable.safeParse(response.data);
2501
+ if (refused.success) {
2502
+ throw notRetryable(refused.data.reason ?? refused.data.message ?? null, serverFault({ status: response.status, body: response.data }));
2503
+ }
2504
+ const publication = PublicationRetryOutcome.safeParse(response.data);
2505
+ if (!publication.success) unreadable3();
2506
+ const state = publication.data.state;
2507
+ if (state === void 0) unreadable3();
2508
+ return publicationResult(suggestionId, publication.data, state);
2509
+ }
2510
+ function retryIds(result) {
2511
+ return result.kind === "analysis" ? { suggestion_id: result.suggestion_id, job_id: result.job_id, attempt: result.attempt } : { suggestion_id: result.suggestion_id, pr_number: result.pr_number };
2512
+ }
2513
+ function boundOf2(name) {
2514
+ const bound2 = operationOf("releases.list").query.find((entry) => entry.name === name);
2515
+ if (bound2?.max === void 0) throw new Error(`releases.list declares no ceiling for ${name}`);
2516
+ return bound2.max;
2517
+ }
2518
+ var RELEASE_PAGE_MAX = boundOf2("limit");
2519
+ var RELEASE_ID_MAX_CHARS = boundOf2("release_id");
2520
+ function activeStatusOf(page) {
2521
+ return page.active_release_status === "known" ? "known" : "unknown";
2522
+ }
2523
+ var STATE_OF = Object.freeze({
2524
+ published_active: "live",
2525
+ published_historical: "historical",
2526
+ published_activation_unknown: "unknown"
2527
+ });
2528
+ function summaryOf(release, state, active) {
2529
+ if (state === "live") return `${release.release_id} is the release readers are seeing, activated ${release.activated_at ?? "at an unrecorded time"}.`;
2530
+ if (state === "built") return `${release.release_id} was built and has no activation record, so it was never published.`;
2531
+ if (state === "historical") {
2532
+ const generation = release.generation === null ? "unrecorded" : String(release.generation);
2533
+ return `${release.release_id} went live at ${String(release.activated_at)} (generation ${generation}) and is not what readers are seeing now${active === null ? "" : `; that is ${active}`}.`;
2534
+ }
2535
+ return `${release.release_id} went live at ${String(release.activated_at)}. The activation record could not be read, so whether it is live now is unknown.`;
2536
+ }
2537
+ function classifyReleaseActivation(release, page) {
2538
+ const status = activeStatusOf(page);
2539
+ const active = page.active_release_id;
2540
+ const rule = classifyActivation({
2541
+ release_id: release.release_id,
2542
+ activated_at: release.activated_at,
2543
+ active_release_id: active,
2544
+ active_release_status: status
2545
+ });
2546
+ const state = STATE_OF[rule.outcome] ?? "built";
2547
+ return {
2548
+ state,
2549
+ activated_at: release.activated_at,
2550
+ generation: release.generation,
2551
+ active_release_id: active,
2552
+ active_release_status: status,
2553
+ rule,
2554
+ summary: summaryOf(release, state, active)
2555
+ };
2556
+ }
2557
+ function checkBounds(options) {
2558
+ if (options.limit !== void 0 && (!Number.isInteger(options.limit) || options.limit < 1 || options.limit > RELEASE_PAGE_MAX)) {
2559
+ throw invalidInput(`--limit takes a whole number from 1 to ${RELEASE_PAGE_MAX}. The release route refuses anything else rather than trimming it.`);
2560
+ }
2561
+ if (options.cursor !== void 0 && options.cursor.trim() === "") throw invalidInput("--cursor needs the next_cursor of the preceding page.");
2562
+ }
2563
+ async function listReleases(client, siteId, options = {}) {
2564
+ const { cursor: cursor2, limit: limit2, ...read } = options;
2565
+ checkBounds(options);
2566
+ const response = await client.get(operationPath("releases.list", siteId), {
2567
+ ...read,
2568
+ query: { ...cursor2 === void 0 ? {} : { cursor: cursor2 }, ...limit2 === void 0 ? {} : { limit: limit2 } }
2569
+ });
2570
+ return parseResponse(ReleasesPage2, response.data, `The release listing for ${siteId}`);
2571
+ }
2572
+ async function getRelease(client, siteId, releaseId, options = {}) {
2573
+ const id = releaseId.trim();
2574
+ if (id === "") throw invalidInput("Name the release to read.");
2575
+ if (id.length > RELEASE_ID_MAX_CHARS) throw invalidInput(`A release id is at most ${RELEASE_ID_MAX_CHARS} characters; the route refuses a longer one as invalid_release_id.`);
2576
+ const response = await client.get(operationPath("releases.list", siteId), { ...options, query: { release_id: id } });
2577
+ const page = parseResponse(ReleasesPage2, response.data, `The release read for ${id} on ${siteId}`);
2578
+ const release = page.releases[0];
2579
+ if (release === void 0) {
2580
+ throw managementError(
2581
+ "not_found",
2582
+ `${siteId} holds no release ${id}, or it is being deleted. This answer reports no release of that id and says nothing about which release the site is serving.`
2583
+ );
2584
+ }
2585
+ return { site_id: page.site_id, release, activation: classifyReleaseActivation(release, page), page };
2586
+ }
2587
+ var DEFAULT_SUGGESTION_WAIT_MS = 10 * 6e4;
2588
+ var TIMEOUT_NOTE = "The deadline passed with the suggestion still moving. Nothing remote was cancelled or approved; it can be waited on again.";
2589
+ var HANDOFF_NOTE = "The server supplied no page content for this revision, so what would publish cannot be read here. Read the pull request before approving.";
2590
+ var UNREADABLE_RELEASE_NOTE = "The suggestion names a release the release history does not answer for yet, so nothing is claimed about activation.";
2591
+ var WAIT_ERROR_CODES = Object.freeze({
2592
+ no_longer_reviewable: "conflict",
2593
+ review_required: "conflict",
2594
+ dismissed: "conflict",
2595
+ superseded: "conflict",
2596
+ failed: "remote_failure",
2597
+ timed_out: "wait_timeout"
2598
+ });
2599
+ function suggestionWaitErrorCode(outcome) {
2600
+ return WAIT_ERROR_CODES[outcome] ?? null;
2601
+ }
2602
+ function parseSuggestionWaitTarget(value) {
2603
+ const word = typeof value === "string" ? value.trim() : "";
2604
+ if (!SUGGESTION_WAIT_TARGETS.includes(word)) {
2605
+ throw invalidInput(`--until takes one of ${SUGGESTION_WAIT_TARGETS.join(", ")}; ${String(value)} is not one of them.`);
2606
+ }
2607
+ return word;
2608
+ }
2609
+ function activationLimitation(release) {
2610
+ if (release === null || release.active_release_status === "known") return [];
2611
+ return [{
2612
+ code: "activation_unknown",
2613
+ message: `${release.release_id} went live, and the activation record could not be read, so whether it is the release readers see now is unknown rather than no.`,
2614
+ at: "release.active_release_id"
2615
+ }];
2616
+ }
2617
+ function releaseNotFound(error) {
2618
+ return isManagementError(error) && error.detail.code === "not_found";
2619
+ }
2620
+ async function activationOf(client, input) {
2621
+ try {
2622
+ const read = await getRelease(client, input.site, input.releaseId, { signal: input.signal, deadlineMs: input.deadlineMs });
2623
+ const { activation } = read;
2624
+ return {
2625
+ activation: {
2626
+ release_id: read.release.release_id,
2627
+ activated_at: activation.activated_at,
2628
+ active_release_id: activation.active_release_id,
2629
+ active_release_status: activation.active_release_status
2630
+ },
2631
+ release: {
2632
+ release_id: read.release.release_id,
2633
+ state: activation.state,
2634
+ activated_at: activation.activated_at,
2635
+ generation: activation.generation,
2636
+ active_release_id: activation.active_release_id,
2637
+ active_release_status: activation.active_release_status,
2638
+ summary: activation.summary
2639
+ },
2640
+ unreadable: false
2641
+ };
2642
+ } catch (error) {
2643
+ if (!releaseNotFound(error)) throw error;
2644
+ return {
2645
+ activation: { release_id: input.releaseId, activated_at: null, active_release_id: null, active_release_status: "unknown" },
2646
+ release: null,
2647
+ unreadable: true
2648
+ };
2649
+ }
2650
+ }
2651
+ async function waitForSuggestion(client, input) {
2652
+ const target = parseSuggestionWaitTarget(input.until);
2653
+ const deadlineMs = input.deadlineMs ?? DEFAULT_SUGGESTION_WAIT_MS;
2654
+ const outcome = await poll(async (context) => {
2655
+ const detail = await getSuggestion(client, {
2656
+ site: input.site,
2657
+ suggestionId: input.suggestionId,
2658
+ signal: context.signal,
2659
+ deadlineMs: context.deadlineMs
2660
+ });
2661
+ const suggestion2 = detail.suggestion;
2662
+ const reached = classifySuggestionWait(target, suggestion2);
2663
+ if (target !== "published" || reached.decision === "stop") {
2664
+ return {
2665
+ done: reached.decision === "stop",
2666
+ value: { suggestion: suggestion2, outcome: reached.outcome, exit: reached.exit, note: reached.rule.note, release: null, release_unreadable: false }
2667
+ };
2668
+ }
2669
+ const releaseId = suggestion2.publication?.release_id ?? null;
2670
+ const read = releaseId === null ? { activation: { release_id: null, activated_at: null, active_release_id: null, active_release_status: "known" }, release: null, unreadable: false } : await activationOf(client, { site: input.site, releaseId, signal: context.signal, deadlineMs: context.deadlineMs });
2671
+ const verified = classifySuggestionWait(target, suggestion2, read.activation);
2672
+ const note = read.unreadable ? UNREADABLE_RELEASE_NOTE : read.release?.summary ?? verified.activation?.note ?? verified.rule.note;
2673
+ return {
2674
+ done: verified.decision === "stop",
2675
+ value: { suggestion: suggestion2, outcome: verified.outcome, exit: verified.exit, note, release: read.release, release_unreadable: read.unreadable }
2676
+ };
2677
+ }, {
2678
+ deadlineMs,
2679
+ signal: input.signal,
2680
+ intervalMs: input.intervalMs,
2681
+ maxIntervalMs: input.maxIntervalMs,
2682
+ now: input.now,
2683
+ sleep: input.sleep
2684
+ });
2685
+ const last = outcome.last;
2686
+ const suggestion = last?.suggestion ?? null;
2687
+ const release = last?.release ?? null;
2688
+ const reviewLimitations = suggestion === null ? [] : suggestionLimitations(suggestion);
2689
+ const missingContent = reviewLimitations.some((limitation) => limitation.code === "content_unavailable");
2690
+ const reviewable = !outcome.timed_out && last?.outcome === "reviewable";
2691
+ const limitations = Object.freeze([
2692
+ ...target === "reviewable" ? reviewLimitations : [],
2693
+ ...activationLimitation(release)
2694
+ ]);
2695
+ const base = {
2696
+ target,
2697
+ timed_out: outcome.timed_out,
2698
+ suggestion,
2699
+ state: suggestion?.state ?? null,
2700
+ revision: suggestion?.revision ?? null,
2701
+ base_sha: suggestion?.base_sha ?? null,
2702
+ head_sha: suggestion?.head_sha ?? null,
2703
+ pr_number: suggestion?.pr_number ?? null,
2704
+ publication_job_id: suggestion?.publication?.job_id ?? null,
2705
+ release,
2706
+ owner_handoff: reviewable && missingContent,
2707
+ limitations,
2708
+ polls: outcome.attempts,
2709
+ elapsed_ms: outcome.elapsed_ms
2710
+ };
2711
+ if (outcome.timed_out || last === void 0 || last === null) {
2712
+ return { ...base, outcome: "timed_out", exit: EXIT.wait_timeout, note: TIMEOUT_NOTE, timed_out: true };
2713
+ }
2714
+ return {
2715
+ ...base,
2716
+ outcome: last.outcome,
2717
+ exit: last.exit,
2718
+ note: base.owner_handoff ? `${last.note} ${HANDOFF_NOTE}` : last.note
2719
+ };
2720
+ }
2721
+ var REQUEST_THREAD_LIMIT_MAX = MAX_REQUEST_THREADS;
2722
+ function checkedLimit(limit2) {
2723
+ if (limit2 === null || limit2 === void 0) return void 0;
2724
+ if (!Number.isInteger(limit2) || limit2 < 1 || limit2 > REQUEST_THREAD_LIMIT_MAX) {
2725
+ throw invalidInput(`--limit takes a whole number from 1 to ${REQUEST_THREAD_LIMIT_MAX}. Nothing was sent.`);
2726
+ }
2727
+ return limit2;
2728
+ }
2729
+ function checkedCursor(cursor2) {
2730
+ if (cursor2 === null || cursor2 === void 0) return void 0;
2731
+ if (cursor2.trim() === "") throw invalidInput("--cursor needs the next_cursor of the preceding page.");
2732
+ return cursor2;
2733
+ }
2734
+ async function listRequestThreads(client, input) {
2735
+ const query = {
2736
+ view: "threads",
2737
+ cursor: checkedCursor(input.cursor),
2738
+ limit: checkedLimit(input.limit),
2739
+ attention: input.attention === true ? "1" : void 0
2740
+ };
2741
+ const response = await client.get(operationPath("requests.list", input.siteId), {
2742
+ query,
2743
+ signal: input.signal,
2744
+ deadlineMs: input.deadlineMs
2745
+ });
2746
+ return parseResponse(RequestsPage2, response.data, "The request thread listing");
2747
+ }
2748
+ async function readRequestThread(client, input) {
2749
+ if (input.jobId.trim() === "") throw invalidInput("Name the job whose conversation to read.");
2750
+ const response = await client.get(operationPath("requests.thread", input.siteId, { job: input.jobId }), {
2751
+ signal: input.signal,
2752
+ deadlineMs: input.deadlineMs
2753
+ });
2754
+ return parseResponse(RequestThread2, response.data, "The request thread");
2755
+ }
2756
+ var REQUEST_PAGE_LIMIT = 400;
2757
+ var UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/iu;
2758
+ function checkedSubmissionId(value) {
2759
+ if (!UUID.test(value.trim())) {
2760
+ throw invalidInput("--submission-id takes a UUID you mint and keep, so a lost answer can be replayed under the same identity. Nothing was sent.");
2761
+ }
2762
+ return value.trim();
2763
+ }
2764
+ function checkedRequestText(text) {
2765
+ if (text.trim() === "") throw invalidInput("The request is empty, and an empty request buys a model run with nothing to go on. Nothing was sent.");
2766
+ if (text.length > REQUEST_TEXT_LIMIT) {
2767
+ throw invalidInput(`A request may be at most ${REQUEST_TEXT_LIMIT} characters as the server counts them, and this one is ${text.length}. Nothing was sent.`);
2768
+ }
2769
+ return text;
2770
+ }
2771
+ function checkedPage(page) {
2772
+ if (page === null || page === void 0) return void 0;
2773
+ if (page.trim() === "") throw invalidInput("--page needs a page path, such as pages/export.mdx.");
2774
+ if (page.length > REQUEST_PAGE_LIMIT) throw invalidInput(`--page may be at most ${REQUEST_PAGE_LIMIT} characters. Nothing was sent.`);
2775
+ return page;
2776
+ }
2777
+ async function createRequest(client, input) {
2778
+ const page = checkedPage(input.page);
2779
+ const body = {
2780
+ submission_id: checkedSubmissionId(input.submissionId),
2781
+ request: checkedRequestText(input.request),
2782
+ ...page === void 0 ? {} : { page }
2783
+ };
2784
+ const accepted = UpdateRequestBody.safeParse(body);
2785
+ if (!accepted.success) {
2786
+ throw invalidInput(`The request was not in the shape the server accepts: ${accepted.error.issues.map((issue) => issue.path.join(".") || "body").join(", ")}. Nothing was sent.`);
2787
+ }
2788
+ const response = await client.request({
2789
+ method: "POST",
2790
+ path: operationPath("requests.create", input.siteId),
2791
+ body,
2792
+ signal: input.signal,
2793
+ deadlineMs: input.deadlineMs
2794
+ });
2795
+ const parsed = UpdateRequestResponse2.safeParse(response.data);
2796
+ if (!parsed.success) {
2797
+ throw unknownOutcome(`The request was accepted and its answer did not name the queued job. Replay submission_id ${body.submission_id} with the same words to recover it; do not send different words under that id.`);
2798
+ }
2799
+ return parsed.data;
2800
+ }
2801
+ var NEW_REQUEST_GUIDANCE = "Ask for the next change with `saturndocs requests create` under a new --submission-id; a closed conversation is never reopened.";
2802
+ function withNewRequestGuidance(error) {
2803
+ if (!isManagementError(error) || error.detail.server?.code !== "thread_closed") return error;
2804
+ return new ManagementError(
2805
+ clientError("conflict", `${error.detail.message} ${NEW_REQUEST_GUIDANCE}`, {
2806
+ mutation: error.detail.mutation,
2807
+ server: error.detail.server
2808
+ }),
2809
+ { cause: error }
2810
+ );
2811
+ }
2812
+ async function replyToRequest(client, input) {
2813
+ const submissionId = checkedSubmissionId(input.submissionId);
2814
+ const request = checkedRequestText(input.request);
2815
+ const thread = await readRequestThread(client, {
2816
+ siteId: input.siteId,
2817
+ jobId: input.jobId,
2818
+ signal: input.signal,
2819
+ deadlineMs: input.deadlineMs
2820
+ });
2821
+ const root = thread.root.job_id;
2822
+ const body = { submission_id: submissionId, request, thread: root };
2823
+ const accepted = UpdateRequestBody.safeParse(body);
2824
+ if (!accepted.success) {
2825
+ throw new ManagementError(clientError("invalid_input", `The reply was not in the shape the server accepts: ${accepted.error.issues.map((issue) => issue.path.join(".") || "body").join(", ")}. Nothing was sent.`));
2826
+ }
2827
+ let response;
2828
+ try {
2829
+ response = await client.request({
2830
+ method: "POST",
2831
+ path: operationPath("requests.create", input.siteId),
2832
+ body,
2833
+ signal: input.signal,
2834
+ deadlineMs: input.deadlineMs
2835
+ });
2836
+ } catch (error) {
2837
+ throw withNewRequestGuidance(error);
2838
+ }
2839
+ const parsed = UpdateRequestResponse2.safeParse(response.data);
2840
+ if (!parsed.success) {
2841
+ throw unknownOutcome(`The reply was accepted and its answer did not name the queued job. Read the conversation ${root}, or replay submission_id ${submissionId} with the same words; do not send different words under that id.`);
2842
+ }
2843
+ return { queued: parsed.data, threadId: thread.thread_id };
2844
+ }
2845
+ async function retryRequest(client, input) {
2846
+ if (input.jobId.trim() === "") throw invalidInput("Name the failed job to send again.");
2847
+ const response = await client.request({
2848
+ method: "POST",
2849
+ path: operationPath("requests.retry", input.siteId, { job: input.jobId }),
2850
+ signal: input.signal,
2851
+ deadlineMs: input.deadlineMs
2852
+ });
2853
+ const parsed = RequestRetryResponse2.safeParse(response.data);
2854
+ if (!parsed.success) {
2855
+ throw unknownOutcome(`The retry of ${input.jobId} was accepted and its answer could not be read. Read the conversation before sending it again.`);
2856
+ }
2857
+ return parsed.data;
2858
+ }
2859
+ var STATUS_SECTIONS = ["site", "requests", "suggestions", "activity"];
2860
+ var STATUS_PAGE_LIMIT = 10;
2861
+ function available(section2, data, empty, recovery) {
2862
+ return { section: section2, state: empty ? "empty" : "available", data, error: null, recovery };
2863
+ }
2864
+ function unavailable(section2, cause, recovery) {
2865
+ return { section: section2, state: "unavailable", data: null, error: toClientError(cause), recovery };
2866
+ }
2867
+ function readCommand(section2, siteId) {
2868
+ if (section2 === "site") return `saturndocs sites get ${siteId} --json`;
2869
+ if (section2 === "requests") return `saturndocs requests list --site ${siteId} --json`;
2870
+ if (section2 === "suggestions") return `saturndocs suggestions list --site ${siteId} --state pending --json`;
2871
+ return `saturndocs activity list ${siteId} --json`;
2872
+ }
2873
+ function retryRecovery(section2, siteId) {
2874
+ return {
2875
+ command: readCommand(section2, siteId),
2876
+ replay: null,
2877
+ message: `The ${section2} section of this briefing was not read. Run its own command to see whether the service answers, and read nothing into its absence here.`
2878
+ };
2879
+ }
2880
+ async function section(name, siteId, read, isEmpty, recovery) {
2881
+ try {
2882
+ const data = await read();
2883
+ return available(name, data, isEmpty(data), recovery(data));
2884
+ } catch (cause) {
2885
+ return unavailable(name, cause, retryRecovery(name, siteId));
2886
+ }
2887
+ }
2888
+ function awaitingReply(page, siteId) {
2889
+ const waiting = [];
2890
+ for (const thread of page.threads) {
2891
+ if (!thread.open) continue;
2892
+ const ids = { thread_id: thread.thread_id, job_id: thread.latest.job_id, attempt: thread.latest.attempt };
2893
+ waiting.push({
2894
+ kind: "request_awaiting_reply",
2895
+ ids,
2896
+ summary: `Request ${thread.thread_id} is waiting on an answer; its latest job is ${thread.latest.job_id}.`,
2897
+ recovery: {
2898
+ command: `saturndocs requests reply --site ${siteId} --thread ${thread.thread_id} --submission-id <uuid> --file -`,
2899
+ replay: null,
2900
+ message: "Answer the question to let the drafting run carry on. The answer's words go in on stdin, never in argv."
2901
+ }
2902
+ });
2903
+ }
2904
+ return Object.freeze(waiting);
2905
+ }
2906
+ function requestsRecovery(page, siteId) {
2907
+ const open = page.threads.filter((thread) => thread.open);
2908
+ const first = open[0];
2909
+ if (first === void 0) return null;
2910
+ return {
2911
+ command: `saturndocs requests reply --site ${siteId} --thread ${first.thread_id} --submission-id <uuid> --file -`,
2912
+ replay: null,
2913
+ message: `${open.length} conversation(s) on this page are waiting on an answer. ${first.thread_id} is the newest of them.`
2914
+ };
2915
+ }
2916
+ function countsOf(requests, suggestions) {
2917
+ return {
2918
+ requests_failed: requests.data?.failed ?? null,
2919
+ requests_waiting: requests.data?.waiting ?? null,
2920
+ suggestions: suggestions.data?.counts ?? null
2921
+ };
2922
+ }
2923
+ async function readSiteStatus(client, siteId, options = {}) {
2924
+ const { limit: limit2 = STATUS_PAGE_LIMIT, ...read } = options;
2925
+ const [site, requests, suggestions, activity] = await Promise.all([
2926
+ section("site", siteId, () => getSite(client, siteId, read), () => false, () => null),
2927
+ section(
2928
+ "requests",
2929
+ siteId,
2930
+ () => listRequestThreads(client, { siteId, limit: limit2, attention: true, ...read }),
2931
+ (page) => page.threads.length === 0,
2932
+ (page) => requestsRecovery(page, siteId)
2933
+ ),
2934
+ section(
2935
+ "suggestions",
2936
+ siteId,
2937
+ () => listSuggestions(client, { site: siteId, state: "pending", limit: limit2, ...read }),
2938
+ (page) => page.suggestions.length === 0,
2939
+ () => null
2940
+ ),
2941
+ section(
2942
+ "activity",
2943
+ siteId,
2944
+ () => listActivity(client, siteId, { limit: limit2, ...read }),
2945
+ (page) => page.events.length === 0,
2946
+ () => null
2947
+ )
2948
+ ]);
2949
+ return {
2950
+ site_id: siteId,
2951
+ site,
2952
+ requests,
2953
+ suggestions,
2954
+ activity,
2955
+ counts: countsOf(requests, suggestions),
2956
+ attention: requests.data === null ? Object.freeze([]) : awaitingReply(requests.data, siteId)
2957
+ };
2958
+ }
2959
+ function statusSections(status) {
2960
+ return [status.site, status.requests, status.suggestions, status.activity];
2961
+ }
2962
+ function statusLimitations(status) {
2963
+ const limitations = [];
2964
+ for (const part of statusSections(status)) {
2965
+ if (part.state !== "unavailable") continue;
2966
+ limitations.push({
2967
+ code: "section_unavailable",
2968
+ message: `The ${part.section} section could not be read: ${part.error?.message ?? "the read failed and said nothing."} Nothing here says this site has no ${part.section}.`,
2969
+ at: part.section
2970
+ });
2971
+ }
2972
+ if (status.requests.data?.next_cursor != null) {
2973
+ limitations.push({ code: "page_bounded", message: "This is one page of conversations. Read the rest with saturndocs requests list --cursor.", at: "requests.data.threads" });
2974
+ }
2975
+ if (status.suggestions.data?.next_cursor != null) {
2976
+ limitations.push({ code: "page_bounded", message: "This is one page of the pending tab. Read the rest with saturndocs suggestions list --cursor.", at: "suggestions.data.suggestions" });
2977
+ }
2978
+ if (status.activity.data?.next_cursor != null) {
2979
+ limitations.push({ code: "page_bounded", message: "This is one page of activity. Read the rest with saturndocs activity list --cursor.", at: "activity.data.events" });
2980
+ }
2981
+ return Object.freeze(limitations);
2982
+ }
2983
+ function statusBlocker(status) {
2984
+ const failures = statusSections(status).filter((part) => part.state === "unavailable");
2985
+ const refused = failures.find((part) => part.error?.code === "unauthorized" || part.error?.code === "forbidden");
2986
+ if (refused?.error != null) return refused.error;
2987
+ if (failures.length === STATUS_SECTIONS.length) return failures[0]?.error ?? null;
2988
+ return null;
2989
+ }
2990
+ function statusIds(status) {
2991
+ return status.attention[0]?.ids ?? {};
2992
+ }
2993
+ function statusRecovery(status) {
2994
+ return status.attention[0]?.recovery ?? status.requests.recovery ?? null;
2995
+ }
2996
+ var DEFAULT_REQUEST_WAIT_MS = 10 * 6e4;
2997
+ var TIMEOUT_NOTE2 = "The deadline passed with the work still running. Nothing remote was cancelled; the job carries on and can be waited on again.";
2998
+ var WAIT_ERROR_CODES2 = Object.freeze({
2999
+ failed: "remote_failure",
3000
+ cancelled: "remote_failure",
3001
+ outcome_unknown: "service_unavailable",
3002
+ timed_out: "wait_timeout"
3003
+ });
3004
+ function requestWaitErrorCode(outcome) {
3005
+ return WAIT_ERROR_CODES2[outcome] ?? null;
3006
+ }
3007
+ function summaryOf2(state, thread) {
3008
+ const entry = thread.entries.filter((row) => row.job_id === state.job_id && (state.attempt === null || row.attempt === state.attempt)).at(-1);
3009
+ const words = entry === void 0 ? null : entry.body.trim();
3010
+ return words === null || words === "" ? state.rule.note : words;
3011
+ }
3012
+ async function waitForRequest(client, input) {
3013
+ const deadlineMs = input.deadlineMs ?? DEFAULT_REQUEST_WAIT_MS;
3014
+ const outcome = await poll(async (context) => {
3015
+ const thread2 = await readRequestThread(client, {
3016
+ siteId: input.siteId,
3017
+ jobId: input.jobId,
3018
+ signal: context.signal,
3019
+ deadlineMs: context.deadlineMs
3020
+ });
3021
+ const state2 = classifyRequestWait(thread2);
3022
+ return { done: state2.rule.decision === "stop", value: { thread: thread2, state: state2 } };
3023
+ }, {
3024
+ deadlineMs,
3025
+ signal: input.signal,
3026
+ intervalMs: input.intervalMs,
3027
+ maxIntervalMs: input.maxIntervalMs,
3028
+ now: input.now,
3029
+ sleep: input.sleep
3030
+ });
3031
+ const thread = outcome.last?.thread ?? null;
3032
+ const state = outcome.last?.state ?? null;
3033
+ const base = {
3034
+ timed_out: outcome.timed_out,
3035
+ thread,
3036
+ job_id: state?.job_id ?? null,
3037
+ attempt: state?.attempt ?? null,
3038
+ state: thread?.latest_job.state ?? null,
3039
+ suggestion_id: state?.suggestion_id ?? null,
3040
+ polls: outcome.attempts,
3041
+ elapsed_ms: outcome.elapsed_ms
3042
+ };
3043
+ if (outcome.timed_out || state === null) {
3044
+ return { ...base, outcome: "timed_out", exit: EXIT.wait_timeout, note: TIMEOUT_NOTE2, timed_out: true };
3045
+ }
3046
+ return {
3047
+ ...base,
3048
+ outcome: state.rule.outcome,
3049
+ exit: state.rule.exit,
3050
+ note: thread === null ? state.rule.note : summaryOf2(state, thread)
3051
+ };
3052
+ }
3053
+ function requireSuggestionId2(value) {
3054
+ if (value.trim() === "") throw invalidInput("Name the suggestion to preview, as saturndocs suggestions preview <suggestion-id>.");
3055
+ return value;
3056
+ }
3057
+ function unreadable4() {
3058
+ throw managementError(
3059
+ "transport_failure",
3060
+ "The preview did not come back in the shape this client understands, so nothing is reported rather than a reading that might call a stale or absent preview a current one."
3061
+ );
3062
+ }
3063
+ function limitationsFor2(body) {
3064
+ const limitations = [];
3065
+ if (body.state === "building") {
3066
+ limitations.push({
3067
+ code: "activation_unknown",
3068
+ message: "A preview build is running. This command does not follow it and nothing here says one will open; read the preview again to find out how the build ended.",
3069
+ at: "state"
3070
+ });
3071
+ }
3072
+ if (body.state === "ready" && !body.current) {
3073
+ limitations.push({
3074
+ code: "review_incomplete",
3075
+ message: `This preview was built for revision ${body.revision === null ? "unknown" : String(body.revision)}, which is not the suggestion's current revision. Reviewing it reviews work that has since changed.`,
3076
+ at: "current"
3077
+ });
3078
+ }
3079
+ if (body.state === "ready" && body.url === null) {
3080
+ limitations.push({
3081
+ code: "content_unavailable",
3082
+ message: "The server reported a built preview and gave no address for it, so there is nothing to open. This is not a preview waiting to be opened; ask for it again.",
3083
+ at: "url"
3084
+ });
3085
+ }
3086
+ return Object.freeze(limitations);
3087
+ }
3088
+ function toResult(suggestionId, body, requestedBuild) {
3089
+ return {
3090
+ suggestion_id: suggestionId,
3091
+ state: body.state,
3092
+ revision: body.revision,
3093
+ current: body.current,
3094
+ url: body.url,
3095
+ leave_url: body.leave_url,
3096
+ error: body.error,
3097
+ current_preview_available: body.state === "ready" && body.current && body.url !== null,
3098
+ requested_build: requestedBuild,
3099
+ response: body,
3100
+ limitations: limitationsFor2(body)
3101
+ };
3102
+ }
3103
+ async function readPreview(client, input, method) {
3104
+ const suggestionId = requireSuggestionId2(input.suggestionId);
3105
+ const path = resourcePath(input.site, ["suggestions", suggestionId, "preview"]);
3106
+ const response = method === "GET" ? await client.get(path, { signal: input.signal, deadlineMs: input.deadlineMs }) : await client.request({ method: "POST", path, signal: input.signal, deadlineMs: input.deadlineMs });
3107
+ const parsed = SuggestionPreviewResponse2.safeParse(response.data);
3108
+ if (!parsed.success) unreadable4();
3109
+ return toResult(suggestionId, parsed.data, method === "POST");
3110
+ }
3111
+ async function getSuggestionPreview(client, input) {
3112
+ return readPreview(client, input, "GET");
3113
+ }
3114
+ async function createSuggestionPreview(client, input) {
3115
+ return readPreview(client, input, "POST");
3116
+ }
3117
+ function previewIds(result) {
3118
+ return { suggestion_id: result.suggestion_id };
3119
+ }
3120
+ var SESSION_MESSAGE_LIMIT2 = MAX_SESSION_MESSAGE_CHARS;
3121
+ function requireSuggestionId3(value, usage) {
3122
+ if (value.trim() === "") throw invalidInput(`Name the suggestion to act on, as ${usage}.`);
3123
+ return value;
3124
+ }
3125
+ function unreadableRead() {
3126
+ throw managementError(
3127
+ "transport_failure",
3128
+ "The conversation did not come back in the shape this client understands, so nothing is reported rather than a partial reading of it."
3129
+ );
3130
+ }
3131
+ function unreadableTurn(suggestionId) {
3132
+ throw unknownOutcome(
3133
+ `The message was accepted and its answer was not in the shape this client understands, so whether it queued a revision is not reported here. Read the transcript with saturndocs suggestions conversation ${suggestionId} before sending anything again; sending this message again would ask the suggestion twice.`
3134
+ );
3135
+ }
3136
+ async function readConversation(client, input) {
3137
+ const suggestionId = requireSuggestionId3(input.suggestionId, "saturndocs suggestions conversation <suggestion-id>");
3138
+ const response = await client.get(resourcePath(input.site, ["suggestions", suggestionId, "session"]), {
3139
+ signal: input.signal,
3140
+ deadlineMs: input.deadlineMs
3141
+ });
3142
+ const parsed = SuggestionThread2.safeParse(response.data);
3143
+ if (!parsed.success) unreadableRead();
3144
+ const entries = parsed.data.entries;
3145
+ return {
3146
+ suggestion_id: suggestionId,
3147
+ entries,
3148
+ latest_revision: entries.length === 0 ? null : entries.reduce((highest, entry) => Math.max(highest, entry.revision), entries[0].revision)
3149
+ };
3150
+ }
3151
+ async function sendSessionMessage(client, input) {
3152
+ const suggestionId = requireSuggestionId3(input.suggestionId, "saturndocs suggestions message <suggestion-id>");
3153
+ const questionId = input.questionId === void 0 || input.questionId === null || input.questionId === "" ? void 0 : input.questionId;
3154
+ const body = SessionMessageBody.safeParse({ message: input.message, ...questionId === void 0 ? {} : { question_id: questionId } });
3155
+ if (!body.success) {
3156
+ const onQuestion = body.error.issues.some((issue) => issue.path[0] === "question_id");
3157
+ throw invalidInput(
3158
+ onQuestion ? `--question-id takes a question id this suggestion carries, and this one is not a length the server mints. Read the suggestion's questions first; a request thread id is not a suggestion question.` : `saturndocs suggestions message accepts at most ${SESSION_MESSAGE_LIMIT2} characters and no empty message. Nothing was sent.`
3159
+ );
3160
+ }
3161
+ const response = await client.request({
3162
+ method: "POST",
3163
+ path: resourcePath(input.site, ["suggestions", suggestionId, "session"]),
3164
+ body: body.data,
3165
+ signal: input.signal,
3166
+ deadlineMs: input.deadlineMs
3167
+ });
3168
+ const parsed = SessionResponse2.safeParse(response.data);
3169
+ if (!parsed.success) unreadableTurn(suggestionId);
3170
+ const outcome = parsed.data;
3171
+ const limitations = [];
3172
+ if (outcome.startedRevision) {
3173
+ limitations.push({
3174
+ code: "review_incomplete",
3175
+ message: "The answer queued a revision. The draft it produces has nobody's review, so read the suggestion again before approving anything.",
3176
+ at: "started_revision"
3177
+ });
3178
+ }
3179
+ return {
3180
+ suggestion_id: suggestionId,
3181
+ question_id: questionId ?? null,
3182
+ answer: outcome.answer,
3183
+ evidence: outcome.evidence,
3184
+ started_revision: outcome.startedRevision,
3185
+ saved_answer: !outcome.startedRevision,
3186
+ outcome,
3187
+ limitations: Object.freeze(limitations)
3188
+ };
3189
+ }
3190
+ function conversationIds(result) {
3191
+ return { suggestion_id: result.suggestion_id };
3192
+ }
3193
+ var REVISION_NOTE_LIMIT2 = MAX_REQUEST_CHANGES_NOTE_CHARS;
3194
+ function unreadableRequest(suggestionId) {
3195
+ throw unknownOutcome(
3196
+ `The change request was accepted and its answer was not in the shape this client understands, so whether a revision was queued is not reported here. Read the transcript with saturndocs suggestions conversation ${suggestionId} before sending anything again; sending this note again would ask for the same changes twice.`
3197
+ );
3198
+ }
3199
+ async function requestSuggestionChanges(client, input) {
3200
+ const suggestionId = input.suggestionId;
3201
+ if (suggestionId.trim() === "") throw invalidInput("Name the suggestion to act on, as saturndocs suggestions request-changes <suggestion-id>.");
3202
+ const body = RequestChangesBody.safeParse({ note: input.note });
3203
+ if (!body.success) {
3204
+ throw invalidInput(`saturndocs suggestions request-changes accepts at most ${REVISION_NOTE_LIMIT2} characters and no empty note. Nothing was sent.`);
3205
+ }
3206
+ const response = await client.request({
3207
+ method: "POST",
3208
+ path: resourcePath(input.site, ["suggestions", suggestionId, "request-changes"]),
3209
+ body: body.data,
3210
+ signal: input.signal,
3211
+ deadlineMs: input.deadlineMs
3212
+ });
3213
+ const parsed = RequestChangesOutcome2.safeParse(response.data);
3214
+ if (!parsed.success) unreadableRequest(suggestionId);
3215
+ const outcome = parsed.data;
3216
+ if (outcome.state !== "revising") unreadableRequest(suggestionId);
3217
+ const alreadyRequested = outcome.already_requested === true;
3218
+ const revision = typeof outcome.revision === "number" ? outcome.revision : null;
3219
+ const limitations = [
3220
+ alreadyRequested ? {
3221
+ code: "review_incomplete",
3222
+ message: `This note queued nothing: a revision asked for earlier${revision === null ? "" : ` (revision ${revision})`} is still running, and it is what the suggestion is revising to. Read the conversation to see what was asked for; asking again does not add this note to it.`,
3223
+ at: "already_requested"
3224
+ } : {
3225
+ code: "review_incomplete",
3226
+ message: `A revision${revision === null ? "" : ` (revision ${revision})`} is queued. The draft it produces has nobody's review, so read the suggestion again before approving anything.`,
3227
+ at: "revision_queued"
3228
+ }
3229
+ ];
3230
+ return {
3231
+ suggestion_id: suggestionId,
3232
+ state: "revising",
3233
+ revision,
3234
+ pr_number: typeof outcome.pr_number === "number" ? outcome.pr_number : null,
3235
+ revision_queued: !alreadyRequested,
3236
+ already_requested: alreadyRequested,
3237
+ outcome,
3238
+ limitations: Object.freeze(limitations)
3239
+ };
3240
+ }
3241
+ function requestChangesIds(result) {
3242
+ return { suggestion_id: result.suggestion_id, pr_number: result.pr_number };
3243
+ }
3244
+
3245
+ // src/adapter/globals.ts
3246
+ import { InvalidArgumentError } from "commander";
3247
+ var GLOBAL_OPTION_KEYS = ["json", "site", "api", "input", "timeout"];
3248
+ var GLOBAL_DEFAULTS = { json: false, site: null, api: null, input: true, timeout: null };
3249
+ function positiveSeconds(value) {
3250
+ const seconds = Number(value);
3251
+ if (!Number.isInteger(seconds) || seconds <= 0) {
3252
+ throw new InvalidArgumentError("Give a positive whole number of seconds.");
3253
+ }
3254
+ return seconds;
3255
+ }
3256
+ var GLOBAL_FLAGS2 = [
3257
+ ["--json", "One result object and a newline on stdout. Diagnostics stay on stderr."],
3258
+ ["--site <site-id>", "The site to act on. Required for every remote mutation."],
3259
+ ["--api <origin>", "Management API origin."],
3260
+ ["--no-input", "Never read from a terminal."],
3261
+ ["--timeout <seconds>", "Seconds a wait or a request may take.", positiveSeconds]
3262
+ ];
3263
+ function longOf(flags) {
3264
+ return flags.split(/[ ,|]/)[0] ?? flags;
3265
+ }
3266
+ function applyGlobalFlags(command) {
3267
+ const declared = new Set(command.options.map((option) => option.long));
3268
+ for (const [flags, description, parser] of GLOBAL_FLAGS2) {
3269
+ const long = longOf(flags);
3270
+ if (declared.has(long) || long === "--no-input" && declared.has("--input")) continue;
3271
+ if (parser === void 0) command.option(flags, description);
3272
+ else command.option(flags, description, parser);
3273
+ }
3274
+ return command;
3275
+ }
3276
+ function applyGlobalFlagsDeep(command) {
3277
+ applyGlobalFlags(command);
3278
+ for (const child of command.commands) applyGlobalFlagsDeep(child);
3279
+ }
3280
+ function resolveGlobalOptions(command) {
3281
+ const chain = [];
3282
+ for (let node = command; node !== null; node = node.parent) chain.unshift(node);
3283
+ const resolved = { ...GLOBAL_DEFAULTS };
3284
+ for (const node of chain) {
3285
+ for (const key of GLOBAL_OPTION_KEYS) {
3286
+ const source = node.getOptionValueSource(key);
3287
+ if (source === void 0 || source === "default") continue;
3288
+ const value = node.getOptionValue(key);
3289
+ if (value === void 0) continue;
3290
+ resolved[key] = value;
3291
+ }
3292
+ }
3293
+ return {
3294
+ json: resolved.json === true,
3295
+ site: typeof resolved.site === "string" ? resolved.site : null,
3296
+ api: typeof resolved.api === "string" ? resolved.api : null,
3297
+ input: resolved.input !== false,
3298
+ timeout: typeof resolved.timeout === "number" ? resolved.timeout : null
3299
+ };
3300
+ }
3301
+
3302
+ // src/adapter/errors.ts
3303
+ var CliError = class extends Error {
3304
+ code;
3305
+ server;
3306
+ retryable;
3307
+ mutation;
3308
+ exit;
3309
+ limitations;
3310
+ recovery;
3311
+ data;
3312
+ constructor(code, message, options = {}) {
3313
+ super(message, options.cause === void 0 ? void 0 : { cause: options.cause });
3314
+ this.name = "CliError";
3315
+ this.code = code;
3316
+ this.server = options.server ?? null;
3317
+ this.retryable = options.retryable ?? CLIENT_ERROR_CATEGORIES[code].retryable;
3318
+ this.mutation = options.mutation ?? CLIENT_ERROR_CATEGORIES[code].mutation;
3319
+ this.exit = options.exit ?? CLIENT_ERROR_CATEGORIES[code].exit;
3320
+ this.limitations = options.limitations ?? [];
3321
+ this.recovery = options.recovery ?? null;
3322
+ this.data = options.data;
3323
+ }
3324
+ };
3325
+ function invalidInput2(message, options = {}) {
3326
+ return new CliError("invalid_input", message, options);
3327
+ }
3328
+ function interrupted2(message = CLIENT_ERROR_CATEGORIES.interrupted.meaning, options = {}) {
3329
+ return new CliError("interrupted", message, options);
3330
+ }
3331
+ function clientError2(code, message, server = null) {
3332
+ const category = CLIENT_ERROR_CATEGORIES[code];
3333
+ return { code, message, retryable: category.retryable, mutation: category.mutation, exit: category.exit, server };
3334
+ }
3335
+ function envelope2(context) {
3336
+ return {
3337
+ schema_version: CLIENT_SCHEMA_VERSION,
3338
+ command: context.command,
3339
+ site_id: context.siteId ?? null,
3340
+ ids: context.ids ?? {},
3341
+ limitations: [...context.limitations ?? []],
3342
+ recovery: context.recovery ?? null
3343
+ };
3344
+ }
3345
+ function successResult(context, data) {
3346
+ return { ...envelope2(context), ok: true, data };
3347
+ }
3348
+ function messageOf(error) {
3349
+ if (error instanceof Error && error.message !== "") return error.message;
3350
+ const text = String(error);
3351
+ return text === "" ? "The command failed and said nothing." : text;
3352
+ }
3353
+ var UNCLASSIFIED = "service_unavailable";
3354
+ function failureResult(error, context) {
3355
+ const classified = error instanceof CliError;
3356
+ const code = classified ? error.code : UNCLASSIFIED;
3357
+ const base = envelope2({
3358
+ ...context,
3359
+ limitations: [...context.limitations ?? [], ...classified ? error.limitations : []],
3360
+ recovery: (classified ? error.recovery : null) ?? context.recovery ?? null
3361
+ });
3362
+ const failure = {
3363
+ ...base,
3364
+ ok: false,
3365
+ error: classified ? {
3366
+ code,
3367
+ message: messageOf(error),
3368
+ retryable: error.retryable,
3369
+ mutation: error.mutation,
3370
+ exit: error.exit,
3371
+ server: error.server
3372
+ } : clientError2(code, messageOf(error))
3373
+ };
3374
+ if (classified && error.data !== void 0) failure.data = error.data;
3375
+ return failure;
3376
+ }
3377
+
3378
+ // src/adapter/output.ts
3379
+ function createResultWriter(options) {
3380
+ const stdout = options.stdout ?? process.stdout;
3381
+ const stderr = options.stderr ?? process.stderr;
3382
+ let written = false;
3383
+ let exit = 0;
3384
+ return {
3385
+ json: options.json,
3386
+ get written() {
3387
+ return written;
3388
+ },
3389
+ write(result, writeOptions = {}) {
3390
+ if (written) return exit;
3391
+ written = true;
3392
+ exit = exitForResult(result);
3393
+ if (options.json) {
3394
+ stdout.write(`${JSON.stringify(result)}
3395
+ `);
3396
+ return exit;
3397
+ }
3398
+ if (result.ok) {
3399
+ if (writeOptions.text !== void 0) stdout.write(`${writeOptions.text}
3400
+ `);
3401
+ } else {
3402
+ stderr.write(`error: ${result.error.message}
3403
+ `);
3404
+ }
3405
+ for (const limitation of result.limitations) {
3406
+ stderr.write(`note: ${limitation.message}
3407
+ `);
3408
+ }
3409
+ return exit;
3410
+ }
3411
+ };
3412
+ }
3413
+ function wantsJson(argv) {
3414
+ return argv.includes("--json");
3415
+ }
3416
+
3417
+ export {
3418
+ SUGGESTION_TABS,
3419
+ EXIT,
3420
+ EXIT_MEANINGS,
3421
+ CLIENT_ERROR_CODES,
3422
+ CLIENT_ERROR_CATEGORIES,
3423
+ CLIENT_SCHEMA_VERSION,
3424
+ GLOBAL_FLAGS,
3425
+ REQUEST_TEXT_LIMIT,
3426
+ isManagementError,
3427
+ requireCommitSha,
3428
+ createClient,
3429
+ resolveTarget,
3430
+ COMMAND_SCHEMA_VERSION,
3431
+ selectorFor,
3432
+ commandMetadata,
3433
+ listSites,
3434
+ readSiteDetail,
3435
+ PAGE_TRAFFIC_WINDOWS,
3436
+ pagesUnavailable,
3437
+ listPages,
3438
+ ACTIVITY_OUTCOMES,
3439
+ ACTIVITY_LIMIT_CEILING,
3440
+ listActivity,
3441
+ activityJobFound,
3442
+ readActivityJob,
3443
+ listSuggestions,
3444
+ getSuggestion,
3445
+ suggestionIds,
3446
+ suggestionLimitations,
3447
+ suggestionDiff,
3448
+ renderDiffText,
3449
+ DISMISS_REASONS,
3450
+ approveSuggestion,
3451
+ dismissSuggestion,
3452
+ reviewDecisionIds,
3453
+ retrySuggestion,
3454
+ retryIds,
3455
+ RELEASE_PAGE_MAX,
3456
+ activeStatusOf,
3457
+ classifyReleaseActivation,
3458
+ listReleases,
3459
+ getRelease,
3460
+ DEFAULT_SUGGESTION_WAIT_MS,
3461
+ suggestionWaitErrorCode,
3462
+ parseSuggestionWaitTarget,
3463
+ waitForSuggestion,
3464
+ listRequestThreads,
3465
+ readRequestThread,
3466
+ checkedSubmissionId,
3467
+ checkedRequestText,
3468
+ createRequest,
3469
+ replyToRequest,
3470
+ retryRequest,
3471
+ readSiteStatus,
3472
+ statusSections,
3473
+ statusLimitations,
3474
+ statusBlocker,
3475
+ statusIds,
3476
+ statusRecovery,
3477
+ DEFAULT_REQUEST_WAIT_MS,
3478
+ requestWaitErrorCode,
3479
+ waitForRequest,
3480
+ getSuggestionPreview,
3481
+ createSuggestionPreview,
3482
+ previewIds,
3483
+ readConversation,
3484
+ sendSessionMessage,
3485
+ conversationIds,
3486
+ requestSuggestionChanges,
3487
+ requestChangesIds,
3488
+ applyGlobalFlags,
3489
+ applyGlobalFlagsDeep,
3490
+ resolveGlobalOptions,
3491
+ CliError,
3492
+ invalidInput2 as invalidInput,
3493
+ interrupted2 as interrupted,
3494
+ clientError2 as clientError,
3495
+ successResult,
3496
+ failureResult,
3497
+ createResultWriter,
3498
+ wantsJson
3499
+ };