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,739 @@
1
+ import {
2
+ readTextInput
3
+ } from "./chunk-N7QPZMLP.js";
4
+ import {
5
+ asCliError,
6
+ clientFor
7
+ } from "./chunk-TKISMN3P.js";
8
+ import {
9
+ interruptSignal
10
+ } from "./chunk-KGJESHJC.js";
11
+ import {
12
+ applyCommandHelp
13
+ } from "./chunk-M2BZEDAS.js";
14
+ import {
15
+ CliError,
16
+ DEFAULT_SUGGESTION_WAIT_MS,
17
+ DISMISS_REASONS,
18
+ SUGGESTION_TABS,
19
+ approveSuggestion,
20
+ commandMetadata,
21
+ conversationIds,
22
+ createResultWriter,
23
+ createSuggestionPreview,
24
+ dismissSuggestion,
25
+ failureResult,
26
+ getSuggestion,
27
+ getSuggestionPreview,
28
+ invalidInput,
29
+ listSuggestions,
30
+ parseSuggestionWaitTarget,
31
+ previewIds,
32
+ readConversation,
33
+ renderDiffText,
34
+ requestChangesIds,
35
+ requestSuggestionChanges,
36
+ resolveGlobalOptions,
37
+ resolveTarget,
38
+ retryIds,
39
+ retrySuggestion,
40
+ reviewDecisionIds,
41
+ sendSessionMessage,
42
+ successResult,
43
+ suggestionDiff,
44
+ suggestionIds,
45
+ suggestionLimitations,
46
+ suggestionWaitErrorCode,
47
+ waitForSuggestion
48
+ } from "./chunk-GYRVGWDM.js";
49
+
50
+ // src/commands/suggestions/runtime.ts
51
+ async function runSuggestionCommand(name, self, run, options = {}) {
52
+ const globals = resolveGlobalOptions(self);
53
+ const writer = createResultWriter({ json: globals.json });
54
+ let siteId = globals.site;
55
+ try {
56
+ const client = clientFor(globals);
57
+ const target = await resolveTarget(client, { ...globals.site === null ? {} : { site: globals.site }, mutation: options.mutation });
58
+ siteId = target.site_id;
59
+ const outcome = await run({ client, site: target.site_id, globals });
60
+ const context = { command: name, siteId: target.site_id, ids: outcome.ids, limitations: outcome.limitations };
61
+ process.exitCode = writer.write(successResult(context, outcome.data), { text: outcome.text });
62
+ } catch (error) {
63
+ process.exitCode = writer.write(failureResult(asCliError(error), { command: name, siteId }));
64
+ }
65
+ }
66
+
67
+ // src/commands/suggestions/read.ts
68
+ var SUGGESTIONS_READ_METADATA = Object.freeze([
69
+ commandMetadata("suggestions list", {
70
+ description: "One tab of a site's suggestions, one page per invocation. `counts` is the server's tally for every tab and `next_cursor` is its own token, so a bounded page is never reported as the site's total.",
71
+ target: { arguments: [] },
72
+ effect: "Nothing. This reads.",
73
+ output: { fields: ["state", "counts", "suggestions", "next_cursor"], ids: [], limitations: [] },
74
+ errors: ["invalid_input", "unauthorized", "forbidden", "not_found", "rate_limited", "service_unavailable", "transport_failure", "interrupted"],
75
+ examples: [
76
+ { command: "saturndocs suggestions list --site acme", description: "The pending tab, which is the server's default." },
77
+ { command: "saturndocs suggestions list --site acme --state failed", description: "One other tab." },
78
+ { command: "saturndocs suggestions list --site acme --cursor eyJ0IjoxfQ --json", description: "The next page, as one result object." }
79
+ ]
80
+ }),
81
+ commandMetadata("suggestions get", {
82
+ description: "One suggestion as the server holds it: the reviewed base and head, the revision, the rationale, the provenance of the run that drafted it, its open questions, what kind of failure it hit, every identity it links, its publication, and the GitHub files-changed URL. A hand-edited head and missing page content are reported as limitations rather than left for the caller to notice.",
83
+ target: { arguments: [{ name: "suggestion-id", required: true, description: "The suggestion to read." }] },
84
+ effect: "Nothing. This reads.",
85
+ output: {
86
+ fields: ["suggestion.base_sha", "suggestion.head_sha", "suggestion.revision", "suggestion.rationale", "suggestion.provenance", "suggestion.questions", "suggestion.failure_kind", "suggestion.publication", "suggestion.files_changed_url"],
87
+ ids: ["suggestion_id", "base_sha", "head_sha", "thread_id", "job_id", "release_id", "pr_number"],
88
+ limitations: ["hand_edited", "content_unavailable", "section_unavailable"]
89
+ },
90
+ errors: ["invalid_input", "unauthorized", "forbidden", "not_found", "rate_limited", "service_unavailable", "transport_failure", "interrupted"],
91
+ examples: [
92
+ { command: "saturndocs suggestions get sug1:0123456789abcdef --site acme", description: "Everything the detail route carries." },
93
+ { command: "saturndocs suggestions get sug1:0123456789abcdef --site acme --json", description: "The same, with the reviewed hashes in the envelope's ids." }
94
+ ]
95
+ })
96
+ ]);
97
+ function listText(page) {
98
+ const lines = [`${page.state}: ${page.suggestions.length} of ${page.counts[page.state]} on this tab`];
99
+ for (const suggestion of page.suggestions) {
100
+ lines.push(`${suggestion.suggestion_id} ${suggestion.state} ${suggestion.title ?? suggestion.first_path ?? "(untitled)"}`);
101
+ }
102
+ lines.push(`counts: ${SUGGESTION_TABS.map((tab) => `${tab}=${page.counts[tab]}`).join(" ")}`);
103
+ lines.push(page.next_cursor === null ? "next_cursor: none; this is the last page" : `next_cursor: ${page.next_cursor}`);
104
+ return lines.join("\n");
105
+ }
106
+ function detailText(detail) {
107
+ const s = detail.suggestion;
108
+ const lines = [
109
+ `${s.suggestion_id} ${s.state} revision ${s.revision ?? "unknown"}`,
110
+ `base ${s.base_sha}`,
111
+ `head ${s.head_sha ?? "unknown"}${typeof s.head_edited_by === "string" && s.head_edited_by !== "" ? ` (edited on GitHub by ${s.head_edited_by})` : ""}`,
112
+ `files changed: ${s.files_changed_url ?? "no pull-request URL was recorded"}`
113
+ ];
114
+ if (s.title !== null && s.title !== void 0) lines.push(`title: ${s.title}`);
115
+ if (s.failure_kind !== null && s.failure_kind !== void 0) lines.push(`failure (${s.failure_kind}): ${s.failure_reason ?? s.error_message ?? "no reason was recorded"}`);
116
+ for (const entry of s.rationale ?? []) lines.push(`rationale: ${entry.change} [${entry.affects.join(", ")}]`);
117
+ for (const question of s.questions ?? []) lines.push(`question ${question.question_id}${question.answered_at === null ? " (unanswered)" : ""}: ${question.body}`);
118
+ if (s.provenance != null) lines.push(`provenance: ${s.provenance.outcome} by ${s.provenance.model ?? "an unnamed model"}, job ${s.provenance.analysis_job_id ?? "unknown"}`);
119
+ if (s.publication != null) lines.push(`publication: ${s.publication.state}, job ${s.publication.job_id}, release ${s.publication.release_id ?? "none"}, activated ${s.publication.activated_at ?? "not yet"}`);
120
+ if (s.request_thread_id != null) lines.push(`request thread: ${s.request_thread_id}`);
121
+ return lines.join("\n");
122
+ }
123
+ function pageSize(value) {
124
+ if (value === void 0) return void 0;
125
+ const parsed = Number(value);
126
+ if (!Number.isInteger(parsed)) throw invalidInput(`--limit takes a whole number; ${value} is not one.`);
127
+ return parsed;
128
+ }
129
+ function registerSuggestionsRead(group) {
130
+ const [list, get] = SUGGESTIONS_READ_METADATA;
131
+ applyCommandHelp(
132
+ group.command("list").action(async (options, self) => {
133
+ await runSuggestionCommand("suggestions list", self, async ({ client, site }) => {
134
+ const limit = pageSize(options.limit);
135
+ const page = await listSuggestions(client, { site, state: options.state, cursor: options.cursor, limit });
136
+ return { data: page, text: listText(page) };
137
+ });
138
+ }),
139
+ list
140
+ );
141
+ applyCommandHelp(
142
+ group.command("get").argument("<suggestion-id>", "The suggestion to read.").action(async (suggestionId, _options, self) => {
143
+ await runSuggestionCommand("suggestions get", self, async ({ client, site }) => {
144
+ const detail = await getSuggestion(client, { site, suggestionId });
145
+ return {
146
+ data: detail,
147
+ ids: suggestionIds(detail.suggestion),
148
+ limitations: suggestionLimitations(detail.suggestion),
149
+ text: detailText(detail)
150
+ };
151
+ });
152
+ }),
153
+ get
154
+ );
155
+ }
156
+
157
+ // src/commands/suggestions/diff.ts
158
+ var SUGGESTIONS_DIFF_METADATA = Object.freeze([
159
+ commandMetadata("suggestions diff", {
160
+ description: "The suggestion's page content as a diff. Where the detail route supplied a diff it is formatted as given; where it supplied complete before and after text the two are compared in this process. No diff is computed anywhere else and none is invented: a page with neither side supplied is reported as unavailable, a deletion is reported as a deletion, and a server-truncated comparison, a hand-edited head, and lines dropped at the output bound are each named. The result hands the review to the owner rather than claiming to have done one.",
161
+ target: { arguments: [{ name: "suggestion-id", required: true, description: "The suggestion to diff." }] },
162
+ effect: "Nothing. This reads one suggestion and formats what came back.",
163
+ output: {
164
+ fields: ["suggestion_id", "base_sha", "head_sha", "revision", "files_changed_url", "pages", "bytes", "review"],
165
+ ids: ["suggestion_id", "base_sha", "head_sha"],
166
+ limitations: ["content_unavailable", "page_bounded", "output_truncated", "hand_edited", "section_unavailable", "review_incomplete"]
167
+ },
168
+ errors: ["invalid_input", "unauthorized", "forbidden", "not_found", "rate_limited", "service_unavailable", "transport_failure", "interrupted"],
169
+ examples: [
170
+ { command: "saturndocs suggestions diff sug1:0123456789abcdef --site acme", description: "Every changed page, inside the output bound." },
171
+ { command: "saturndocs suggestions diff sug1:0123456789abcdef --site acme --path guides/cli.mdx", description: "One page, when the whole set does not fit." },
172
+ { command: "saturndocs suggestions diff sug1:0123456789abcdef --site acme --json", description: "The same diff and the same limitations, as one result object." }
173
+ ]
174
+ })
175
+ ]);
176
+ function registerSuggestionsDiff(group) {
177
+ const metadata = SUGGESTIONS_DIFF_METADATA[0];
178
+ applyCommandHelp(
179
+ group.command("diff").argument("<suggestion-id>", "The suggestion to diff.").action(async (suggestionId, options, self) => {
180
+ await runSuggestionCommand("suggestions diff", self, async ({ client, site }) => {
181
+ const detail = await getSuggestion(client, { site, suggestionId });
182
+ const result = suggestionDiff(detail.suggestion, { path: options.path });
183
+ return {
184
+ data: result,
185
+ ids: suggestionIds(detail.suggestion),
186
+ limitations: result.limitations,
187
+ text: renderDiffText(result)
188
+ };
189
+ });
190
+ }),
191
+ metadata
192
+ );
193
+ }
194
+
195
+ // src/commands/suggestions/preview.ts
196
+ var SUGGESTIONS_PREVIEW_METADATA = Object.freeze([
197
+ commandMetadata("suggestions preview", {
198
+ description: "What preview of this suggestion exists on the site. The default invocation reads and builds nothing. --create asks the server to build a preview of the current revision; it is a manage:write call, so it needs an explicit --site and is sent only after the read that proves the credential names that site. The state the server returns is reported as it stands and nothing is waited for. Only a built preview of the current revision that came with an address is reported as one that opens: a preview of an earlier revision and a ready state with no address are each reported as what they are. There is no preview of a revision other than the last one built, and nothing here opens a browser.",
199
+ target: { arguments: [{ name: "suggestion-id", required: true, description: "The suggestion to preview." }] },
200
+ effect: "Nothing by default. With --create, the server is asked to build a preview of the suggestion's current revision, which starts work on the site's infrastructure.",
201
+ output: {
202
+ fields: ["state", "revision", "current", "url", "leave_url", "error", "current_preview_available", "requested_build"],
203
+ ids: ["suggestion_id"],
204
+ limitations: ["activation_unknown", "review_incomplete", "content_unavailable"]
205
+ },
206
+ errors: ["invalid_input", "unauthorized", "forbidden", "not_found", "conflict", "rate_limited", "service_unavailable", "transport_failure", "interrupted"],
207
+ examples: [
208
+ { command: "saturndocs suggestions preview sug1:0123456789abcdef --site acme", description: "What preview exists now. Nothing is built." },
209
+ { command: "saturndocs suggestions preview sug1:0123456789abcdef --site acme --create", description: "Ask for a preview of the current revision, then read the state the server returned." },
210
+ { command: "saturndocs suggestions preview sug1:0123456789abcdef --site acme --json", description: "The same reading, with the preview address in the result object." }
211
+ ]
212
+ })
213
+ ]);
214
+ var STATE_LINES = Object.freeze({
215
+ none: "No preview has been built for this suggestion.",
216
+ building: "A preview build is running. Read this again to find out how it ended.",
217
+ ready: "A preview has been built.",
218
+ failed: "The preview build failed."
219
+ });
220
+ function previewText(result) {
221
+ const lines = [
222
+ `${result.suggestion_id} ${result.state} revision ${result.revision === null ? "none" : result.revision}${result.state === "none" ? "" : result.current ? " (current)" : " (not the current revision)"}`,
223
+ STATE_LINES[result.state]
224
+ ];
225
+ if (result.error !== null) lines.push(`failure: ${result.error}`);
226
+ if (result.current_preview_available) {
227
+ lines.push(`open: ${String(result.url)}`);
228
+ if (result.leave_url !== null) lines.push(`leave: ${result.leave_url}`);
229
+ lines.push("That address carries the token that opens the preview. Treat it as a credential.");
230
+ } else if (result.url !== null) {
231
+ lines.push(`open (revision ${result.revision === null ? "unknown" : result.revision}, not the current one): ${result.url}`);
232
+ if (result.leave_url !== null) lines.push(`leave: ${result.leave_url}`);
233
+ }
234
+ return lines.join("\n");
235
+ }
236
+ function registerSuggestionsPreview(group) {
237
+ const metadata = SUGGESTIONS_PREVIEW_METADATA[0];
238
+ applyCommandHelp(
239
+ group.command("preview").argument("<suggestion-id>", "The suggestion to preview.").action(async (suggestionId, options, self) => {
240
+ const create = options.create === true;
241
+ await runSuggestionCommand(
242
+ "suggestions preview",
243
+ self,
244
+ async ({ client, site }) => {
245
+ const result = create ? await createSuggestionPreview(client, { site, suggestionId }) : await getSuggestionPreview(client, { site, suggestionId });
246
+ return { data: result, ids: previewIds(result), limitations: result.limitations, text: previewText(result) };
247
+ },
248
+ // Only --create writes; the default read must not demand more of a --site than any other read.
249
+ { mutation: create }
250
+ );
251
+ }),
252
+ metadata
253
+ );
254
+ }
255
+
256
+ // src/commands/suggestions/approve.ts
257
+ var SUGGESTIONS_APPROVE_METADATA = Object.freeze([
258
+ commandMetadata("suggestions approve", {
259
+ description: "Merge exactly the revision named by --base and --head. Both are mandatory here although the server leaves them optional, because an approval missing one publishes whatever the head moved to since it was read. A base or head that no longer matches is refused as a stale review and the hashes the suggestion holds now are never substituted: the answer is to read the revision again. An approval that starts a merge is reported as publishing, which is not a published site, and one that produces another revision is reported as work nobody has reviewed yet.",
260
+ target: { arguments: [{ name: "suggestion-id", required: true, description: "The suggestion whose reviewed revision is approved." }] },
261
+ effect: "Merges the reviewed revision's pull request and starts the publication of the release that follows it.",
262
+ idempotency: "expected_review",
263
+ output: {
264
+ fields: ["state", "published", "publication_started", "review_required", "pr_number", "merge_commit_sha", "failure"],
265
+ ids: ["suggestion_id", "base_sha", "head_sha", "pr_number"],
266
+ limitations: ["review_incomplete", "activation_unknown"]
267
+ },
268
+ errors: ["invalid_input", "unauthorized", "forbidden", "stale_review", "conflict", "rate_limited", "service_unavailable", "transport_failure", "unknown_outcome", "remote_failure", "interrupted"],
269
+ examples: [
270
+ {
271
+ command: "saturndocs suggestions approve sug1:0123456789abcdef --site acme --base aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa --head bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
272
+ description: "Approve the exact revision the preceding read and diff covered."
273
+ },
274
+ {
275
+ command: "saturndocs suggestions approve sug1:0123456789abcdef --site acme --base aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa --head bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb --json",
276
+ description: "The same approval, with the outcome and the reviewed identities as one result object."
277
+ }
278
+ ]
279
+ })
280
+ ]);
281
+ function approveText(result) {
282
+ const lines = [
283
+ `${result.suggestion_id} ${result.state}${result.pr_number === null ? "" : ` pull request #${result.pr_number}`}`,
284
+ `base ${result.base_sha}`,
285
+ `head ${result.head_sha}`
286
+ ];
287
+ if (result.merge_commit_sha !== null) lines.push(`merge commit ${result.merge_commit_sha}`);
288
+ lines.push(
289
+ result.state === "publishing" ? "The reviewed revision was merged and its publication has started. This command cannot say a release went live." : "The approval started another revision instead of publishing."
290
+ );
291
+ return lines.join("\n");
292
+ }
293
+ function approvalFailed(result) {
294
+ const failure = result.failure;
295
+ const reason = failure?.reason ?? "The server gave no reason.";
296
+ const code = failure?.code === null || failure?.code === void 0 ? "" : ` (${failure.code})`;
297
+ return new CliError("remote_failure", `The merge of the reviewed revision failed${code}: ${reason} Nothing was published.`, {
298
+ data: result,
299
+ limitations: result.limitations
300
+ });
301
+ }
302
+ function registerSuggestionsApprove(group) {
303
+ const metadata = SUGGESTIONS_APPROVE_METADATA[0];
304
+ applyCommandHelp(
305
+ group.command("approve").argument("<suggestion-id>", "The suggestion whose reviewed revision is approved.").action(async (suggestionId, options, self) => {
306
+ await runSuggestionCommand(
307
+ "suggestions approve",
308
+ self,
309
+ async ({ client, site }) => {
310
+ const result = await approveSuggestion(client, { site, suggestionId, base: options.base, head: options.head });
311
+ if (result.state === "failed") throw approvalFailed(result);
312
+ return { data: result, ids: reviewDecisionIds(result), limitations: result.limitations, text: approveText(result) };
313
+ },
314
+ { mutation: true }
315
+ );
316
+ }),
317
+ metadata
318
+ );
319
+ }
320
+
321
+ // src/commands/suggestions/dismiss.ts
322
+ var SUGGESTIONS_DISMISS_METADATA = Object.freeze([
323
+ commandMetadata("suggestions dismiss", {
324
+ description: `Dismiss exactly the revision named by --base and --head, with a reason the server stores: ${DISMISS_REASONS.join(", ")}. The optional note is the text of the dismissal and comes from --text or --file, as it does for every other text this CLI sends. This is a manage:write operation and needs no approval permission. Both hashes are mandatory here although the server leaves them optional, so a head that moved after the review is refused as a stale review rather than dismissed unseen. A dismissal the server recorded whose pull request is still open is reported as incomplete cleanup, because nothing here closes it.`,
325
+ target: { arguments: [{ name: "suggestion-id", required: true, description: "The suggestion whose reviewed revision is dismissed." }] },
326
+ effect: "Marks the reviewed revision dismissed and asks GitHub to close its pull request.",
327
+ idempotency: "expected_review",
328
+ output: {
329
+ fields: ["state", "reason", "pr_number", "pull_request_closed", "cleanup_complete"],
330
+ ids: ["suggestion_id", "base_sha", "head_sha", "pr_number"],
331
+ limitations: ["cleanup_incomplete"]
332
+ },
333
+ errors: ["invalid_input", "unauthorized", "forbidden", "stale_review", "conflict", "rate_limited", "service_unavailable", "transport_failure", "unknown_outcome", "remote_failure", "interrupted"],
334
+ examples: [
335
+ {
336
+ command: "saturndocs suggestions dismiss sug1:0123456789abcdef --site acme --base aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa --head bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb --reason not_needed",
337
+ description: "Dismiss the exact revision that was reviewed."
338
+ },
339
+ {
340
+ command: 'saturndocs suggestions dismiss sug1:0123456789abcdef --site acme --base aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa --head bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb --reason wrong_pages --text "It edits a page we removed." --json',
341
+ description: "The same dismissal with a note, as one result object."
342
+ }
343
+ ]
344
+ })
345
+ ]);
346
+ function dismissText(result) {
347
+ const lines = [
348
+ `${result.suggestion_id} dismissed (${result.reason})`,
349
+ `base ${result.base_sha}`,
350
+ `head ${result.head_sha}`
351
+ ];
352
+ if (result.pr_number !== null) {
353
+ lines.push(`pull request #${result.pr_number}: ${result.pull_request_closed === true ? "closed" : "not reported as closed"}`);
354
+ }
355
+ return lines.join("\n");
356
+ }
357
+ function cleanupIncomplete(result) {
358
+ const first = result.limitations[0];
359
+ return new CliError("remote_failure", `${result.suggestion_id} is dismissed, and its GitHub cleanup did not finish. ${first?.message ?? ""}`.trim(), {
360
+ data: result,
361
+ limitations: result.limitations
362
+ });
363
+ }
364
+ function registerSuggestionsDismiss(group) {
365
+ const metadata = SUGGESTIONS_DISMISS_METADATA[0];
366
+ applyCommandHelp(
367
+ group.command("dismiss").argument("<suggestion-id>", "The suggestion whose reviewed revision is dismissed.").action(async (suggestionId, options, self) => {
368
+ await runSuggestionCommand(
369
+ "suggestions dismiss",
370
+ self,
371
+ async ({ client, site, globals }) => {
372
+ const note = await readTextInput(options, {
373
+ command: "suggestions dismiss",
374
+ requirement: metadata.text.requirement,
375
+ limit: metadata.text.limit,
376
+ interactive: globals.input
377
+ });
378
+ const result = await dismissSuggestion(client, { site, suggestionId, base: options.base, head: options.head, reason: options.reason, note });
379
+ if (!result.cleanup_complete) throw cleanupIncomplete(result);
380
+ return { data: result, ids: reviewDecisionIds(result), limitations: result.limitations, text: dismissText(result) };
381
+ },
382
+ { mutation: true }
383
+ );
384
+ }),
385
+ metadata
386
+ );
387
+ }
388
+
389
+ // src/commands/suggestions/conversation.ts
390
+ var SUGGESTIONS_CONVERSATION_METADATA = Object.freeze([
391
+ commandMetadata("suggestions conversation", {
392
+ description: "The transcript of one suggestion's conversation, oldest first: every note, question, and answer with the revision it was said against and the documents cited for it. This reads one page of the session route and nothing else; there is no streaming and no interactive chat here, and a turn is sent with `suggestions message`.",
393
+ target: { arguments: [{ name: "suggestion-id", required: true, description: "The suggestion whose conversation is read." }] },
394
+ effect: "Nothing. This reads.",
395
+ output: { fields: ["entries", "latest_revision"], ids: ["suggestion_id"], limitations: [] },
396
+ errors: ["invalid_input", "unauthorized", "forbidden", "not_found", "rate_limited", "service_unavailable", "transport_failure", "interrupted"],
397
+ examples: [
398
+ { command: "saturndocs suggestions conversation sug1:0123456789abcdef --site acme", description: "Everything said about this suggestion, with each entry's revision." },
399
+ { command: "saturndocs suggestions conversation sug1:0123456789abcdef --site acme --json", description: "The same transcript as one result object." }
400
+ ]
401
+ })
402
+ ]);
403
+ function conversationText(result) {
404
+ const lines = [
405
+ `${result.suggestion_id} ${result.entries.length} ${result.entries.length === 1 ? "entry" : "entries"} latest revision ${result.latest_revision ?? "none"}`
406
+ ];
407
+ for (const entry of result.entries) {
408
+ lines.push(`[revision ${entry.revision}] ${entry.kind} by ${entry.author ?? "an unnamed author"} at ${entry.created_at}: ${entry.body}`);
409
+ for (const evidence of entry.evidence ?? []) lines.push(` evidence: ${evidence.title} (${evidence.document}) ${evidence.url}`);
410
+ }
411
+ if (result.entries.length === 0) lines.push("No entries. Nothing has been said about this suggestion.");
412
+ return lines.join("\n");
413
+ }
414
+ function registerSuggestionsConversation(group) {
415
+ const metadata = SUGGESTIONS_CONVERSATION_METADATA[0];
416
+ applyCommandHelp(
417
+ group.command("conversation").argument("<suggestion-id>", "The suggestion whose conversation is read.").action(async (suggestionId, _options, self) => {
418
+ await runSuggestionCommand("suggestions conversation", self, async ({ client, site }) => {
419
+ const result = await readConversation(client, { site, suggestionId });
420
+ return { data: result, ids: conversationIds(result), text: conversationText(result) };
421
+ });
422
+ }),
423
+ metadata
424
+ );
425
+ }
426
+
427
+ // src/commands/suggestions/message.ts
428
+ var SUGGESTIONS_MESSAGE_METADATA = Object.freeze([
429
+ commandMetadata("suggestions message", {
430
+ description: "Send one turn to a suggestion's conversation: a question about the draft, or the answer to a question the suggestion asked. `--question-id` names a question of this suggestion, which is not a request thread id and is refused if it is one. The answer comes back whole with the documents cited for it, and `started_revision` says what happened: false is an answer saved in the transcript, true is a revision run whose draft nobody has reviewed. One turn per invocation; there is no streaming and no interactive chat. If the answer to a sent message cannot be read, the result is an unknown outcome rather than a failure and nothing is sent again, because the turn may already be recorded.",
431
+ target: { arguments: [{ name: "suggestion-id", required: true, description: "The suggestion to speak to." }] },
432
+ effect: "Records the turn in the suggestion's conversation, which may queue a revision run.",
433
+ output: {
434
+ fields: ["answer", "evidence", "started_revision", "saved_answer", "question_id"],
435
+ ids: ["suggestion_id"],
436
+ limitations: ["review_incomplete"]
437
+ },
438
+ errors: ["invalid_input", "unauthorized", "forbidden", "not_found", "conflict", "rate_limited", "service_unavailable", "transport_failure", "unknown_outcome", "interrupted"],
439
+ examples: [
440
+ {
441
+ command: 'saturndocs suggestions message sug1:0123456789abcdef --site acme --text "Keep the public name." --question-id question-example',
442
+ description: "Answer one of the suggestion's own questions."
443
+ },
444
+ {
445
+ command: "saturndocs suggestions message sug1:0123456789abcdef --site acme --file - --json",
446
+ description: "Ask about the draft with the text on stdin, as one result object."
447
+ }
448
+ ]
449
+ })
450
+ ]);
451
+ function messageText(result) {
452
+ const lines = [
453
+ `${result.suggestion_id} ${result.started_revision ? "queued a revision" : "answer saved; nothing was queued"}`
454
+ ];
455
+ if (result.question_id !== null) lines.push(`answered suggestion question ${result.question_id} (a question of this suggestion, not a request thread)`);
456
+ lines.push(result.answer);
457
+ for (const evidence of result.evidence) lines.push(`evidence: ${evidence.title} (${evidence.document}) ${evidence.url}`);
458
+ if (result.started_revision) lines.push(result.limitations[0]?.message ?? "");
459
+ return lines.filter((line) => line !== "").join("\n");
460
+ }
461
+ function registerSuggestionsMessage(group) {
462
+ const metadata = SUGGESTIONS_MESSAGE_METADATA[0];
463
+ applyCommandHelp(
464
+ group.command("message").argument("<suggestion-id>", "The suggestion to speak to.").action(async (suggestionId, options, self) => {
465
+ await runSuggestionCommand(
466
+ "suggestions message",
467
+ self,
468
+ async ({ client, site, globals }) => {
469
+ const message = await readTextInput(options, {
470
+ command: "suggestions message",
471
+ requirement: metadata.text.requirement,
472
+ limit: metadata.text.limit,
473
+ interactive: globals.input
474
+ });
475
+ const result = await sendSessionMessage(client, { site, suggestionId, message: message ?? "", questionId: options.questionId });
476
+ return { data: result, ids: conversationIds(result), limitations: result.limitations, text: messageText(result) };
477
+ },
478
+ { mutation: true }
479
+ );
480
+ }),
481
+ metadata
482
+ );
483
+ }
484
+
485
+ // src/commands/suggestions/request-changes.ts
486
+ var SUGGESTIONS_REQUEST_CHANGES_METADATA = Object.freeze([
487
+ commandMetadata("suggestions request-changes", {
488
+ description: "Ask for a revision of a suggestion, with the note saying what to change. The note comes from --text or --file, as every other text this CLI sends does. What the call did is reported exactly: `revision_queued` is a revision run this call started, and `already_requested` is a revision asked for earlier that is still running, which this note did not join and did not change. Neither is a published change; the draft a revision produces has nobody's review.",
489
+ target: { arguments: [{ name: "suggestion-id", required: true, description: "The suggestion to revise." }] },
490
+ effect: "Queues a revision run for the suggestion when no earlier request is still running.",
491
+ output: {
492
+ fields: ["state", "revision", "pr_number", "revision_queued", "already_requested"],
493
+ ids: ["suggestion_id", "pr_number"],
494
+ limitations: ["review_incomplete"]
495
+ },
496
+ errors: ["invalid_input", "unauthorized", "forbidden", "not_found", "conflict", "rate_limited", "service_unavailable", "transport_failure", "unknown_outcome", "interrupted"],
497
+ examples: [
498
+ {
499
+ command: 'saturndocs suggestions request-changes sug1:0123456789abcdef --site acme --text "Remove the internal flag."',
500
+ description: "Ask for one revision."
501
+ },
502
+ {
503
+ command: "saturndocs suggestions request-changes sug1:0123456789abcdef --site acme --file notes.md --json",
504
+ description: "The same request with the note in a file, as one result object."
505
+ }
506
+ ]
507
+ })
508
+ ]);
509
+ function requestChangesText(result) {
510
+ const revision = result.revision === null ? "an unnumbered revision" : `revision ${result.revision}`;
511
+ const lines = [
512
+ result.already_requested ? `${result.suggestion_id} nothing queued: ${revision} was asked for earlier and is still running` : `${result.suggestion_id} queued ${revision}`
513
+ ];
514
+ if (result.pr_number !== null) lines.push(`pull request #${result.pr_number}`);
515
+ lines.push(result.limitations[0]?.message ?? "");
516
+ return lines.filter((line) => line !== "").join("\n");
517
+ }
518
+ function registerSuggestionsRequestChanges(group) {
519
+ const metadata = SUGGESTIONS_REQUEST_CHANGES_METADATA[0];
520
+ applyCommandHelp(
521
+ group.command("request-changes").argument("<suggestion-id>", "The suggestion to revise.").action(async (suggestionId, options, self) => {
522
+ await runSuggestionCommand(
523
+ "suggestions request-changes",
524
+ self,
525
+ async ({ client, site, globals }) => {
526
+ const note = await readTextInput(options, {
527
+ command: "suggestions request-changes",
528
+ requirement: metadata.text.requirement,
529
+ limit: metadata.text.limit,
530
+ interactive: globals.input
531
+ });
532
+ const result = await requestSuggestionChanges(client, { site, suggestionId, note: note ?? "" });
533
+ return { data: result, ids: requestChangesIds(result), limitations: result.limitations, text: requestChangesText(result) };
534
+ },
535
+ { mutation: true }
536
+ );
537
+ }),
538
+ metadata
539
+ );
540
+ }
541
+
542
+ // src/commands/suggestions/retry.ts
543
+ var SUGGESTIONS_RETRY_METADATA = Object.freeze([
544
+ commandMetadata("suggestions retry", {
545
+ description: "Retry whichever part of this suggestion failed. The server decides which: an analysis that failed or timed out is queued again and answers with the job and the attempt to follow, a publication that failed is attempted again and answers with the same outcome an approval does, and a revision that failed is refused as not retryable with the server's own reason. This is a manage:approve operation even on the analysis path, because the same route serves the merge. It carries no reviewed hashes and approves no draft: an answer of revising is another revision nobody has read, never an approval of it. An answer this client cannot read is reported as an unknown outcome and the retry is not sent again.",
546
+ target: { arguments: [{ name: "suggestion-id", required: true, description: "The suggestion, or the analysis job id the Failed tab shows in its place, to retry." }] },
547
+ effect: "Queues the failed analysis again, or merges the failed publication again. Neither is reported as finished.",
548
+ output: {
549
+ fields: ["kind", "job_id", "attempt", "budget_tokens", "state", "published", "publication_started", "review_required", "pr_number", "merge_commit_sha", "failure"],
550
+ ids: ["suggestion_id", "job_id", "attempt", "pr_number"],
551
+ limitations: ["review_incomplete", "activation_unknown"]
552
+ },
553
+ errors: ["invalid_input", "unauthorized", "forbidden", "not_found", "conflict", "rate_limited", "service_unavailable", "transport_failure", "unknown_outcome", "remote_failure", "interrupted"],
554
+ examples: [
555
+ {
556
+ command: "saturndocs suggestions retry sug1:0123456789abcdef --site acme",
557
+ description: "Retry whichever of the analysis and the publication failed."
558
+ },
559
+ {
560
+ command: "saturndocs suggestions retry sug1:0123456789abcdef --site acme --json",
561
+ description: "The same retry, with the job or the publication outcome as one result object."
562
+ }
563
+ ]
564
+ })
565
+ ]);
566
+ function retryText(result, site) {
567
+ if (result.kind === "analysis") {
568
+ return [
569
+ `${result.suggestion_id} analysis queued again job ${result.job_id}${result.attempt === null ? "" : ` attempt ${String(result.attempt)}`}`,
570
+ `The analysis was queued again and has not run yet. Follow it with saturndocs activity get ${result.job_id} --site ${site}.`
571
+ ].join("\n");
572
+ }
573
+ const lines = [`${result.suggestion_id} ${result.state}${result.pr_number === null ? "" : ` pull request #${result.pr_number}`}`];
574
+ if (result.merge_commit_sha !== null) lines.push(`merge commit ${result.merge_commit_sha}`);
575
+ lines.push(
576
+ result.state === "publishing" ? "The publication was attempted again and its build has started. This command cannot say a release went live." : "The retry started another revision instead of publishing. Nobody has reviewed it."
577
+ );
578
+ return lines.join("\n");
579
+ }
580
+ function retryFailed(result) {
581
+ const reason = result.failure?.reason ?? "The server gave no reason.";
582
+ const code = result.failure?.code === null || result.failure?.code === void 0 ? "" : ` (${result.failure.code})`;
583
+ return new CliError("remote_failure", `The retried publication failed again${code}: ${reason} Nothing was published.`, {
584
+ data: result,
585
+ limitations: result.limitations
586
+ });
587
+ }
588
+ function registerSuggestionsRetry(group) {
589
+ const metadata = SUGGESTIONS_RETRY_METADATA[0];
590
+ applyCommandHelp(
591
+ group.command("retry").argument("<suggestion-id>", "The suggestion, or the analysis job id the Failed tab shows in its place, to retry.").action(async (suggestionId, _options, self) => {
592
+ await runSuggestionCommand(
593
+ "suggestions retry",
594
+ self,
595
+ async ({ client, site }) => {
596
+ const result = await retrySuggestion(client, { site, suggestionId });
597
+ if (result.kind === "publication" && result.state === "failed") throw retryFailed(result);
598
+ return { data: result, ids: retryIds(result), limitations: result.limitations, text: retryText(result, site) };
599
+ },
600
+ { mutation: true }
601
+ );
602
+ }),
603
+ metadata
604
+ );
605
+ }
606
+
607
+ // src/commands/suggestions/wait.ts
608
+ var SUGGESTIONS_WAIT_METADATA = Object.freeze([
609
+ commandMetadata("suggestions wait", {
610
+ description: "Follow one suggestion to the state named by --until. With reviewable it ends on the revision to read, with its base, its head, and what a reading of it does not cover; a revision whose page content the server withheld is handed back for its owner to read on GitHub rather than reported as reviewed. With published it ends only where the release the suggestion's own publication names shows an activation record of its own, because a merged pull request, a succeeded publication job, and a finished build are none of them publication; whether that release is the one readers see now is reported separately, and an activation record that could not be read stays unknown rather than becoming no. A dismissal, a supersession, a failure, and an approval that led to another revision each end the wait with their own outcome. Nothing here approves, retries, or picks a release the suggestion did not name.",
611
+ target: { arguments: [{ name: "suggestion-id", required: true, description: "The suggestion to follow." }] },
612
+ effect: "Nothing. This reads one suggestion, and the release it names, until the target is reached or the deadline passes.",
613
+ output: {
614
+ fields: ["target", "outcome", "state", "revision", "base_sha", "head_sha", "pr_number", "publication_job_id", "release", "owner_handoff", "polls", "elapsed_ms"],
615
+ ids: ["suggestion_id", "base_sha", "head_sha", "job_id", "release_id", "pr_number"],
616
+ limitations: ["content_unavailable", "hand_edited", "section_unavailable", "activation_unknown"]
617
+ },
618
+ errors: ["invalid_input", "unauthorized", "forbidden", "not_found", "conflict", "rate_limited", "service_unavailable", "transport_failure", "remote_failure", "wait_timeout", "interrupted"],
619
+ examples: [
620
+ {
621
+ command: "saturndocs suggestions wait sug1:0123456789abcdef --until reviewable --site acme",
622
+ description: "Wait for the revision to be ready to read, and report what reading it does not cover."
623
+ },
624
+ {
625
+ command: "saturndocs suggestions wait sug1:0123456789abcdef --until published --site acme --timeout 900 --json",
626
+ description: "Wait for the linked release to show activation, bounded to fifteen minutes, as one result object."
627
+ }
628
+ ]
629
+ })
630
+ ]);
631
+ function idsOf(result, suggestionId) {
632
+ const ids = result.suggestion === null ? { suggestion_id: suggestionId } : suggestionIds(result.suggestion);
633
+ if (result.release !== null) ids.release_id = result.release.release_id;
634
+ return ids;
635
+ }
636
+ function readCommand(suggestionId, siteId) {
637
+ return `saturndocs suggestions get ${suggestionId} --site ${siteId}`;
638
+ }
639
+ function recoveryFor(result, suggestionId, siteId) {
640
+ const state = result.state ?? "unknown";
641
+ if (result.outcome === "timed_out") {
642
+ return {
643
+ command: `saturndocs suggestions wait ${suggestionId} --until ${result.target} --site ${siteId}`,
644
+ replay: null,
645
+ message: `The suggestion was ${state} on ${siteId} when the deadline passed. Nothing was cancelled; this resumes the wait.`
646
+ };
647
+ }
648
+ if (result.outcome === "review_required") {
649
+ return { command: readCommand(suggestionId, siteId), replay: null, message: "A revision nobody has read is waiting. Read it before approving anything." };
650
+ }
651
+ if (result.outcome === "failed" || result.outcome === "dismissed" || result.outcome === "superseded" || result.outcome === "no_longer_reviewable") {
652
+ return { command: readCommand(suggestionId, siteId), replay: null, message: `The suggestion is ${state} on ${siteId}. Read it before deciding what to do next.` };
653
+ }
654
+ if (result.release !== null && result.release.active_release_status === "unknown") {
655
+ return {
656
+ command: `saturndocs releases get ${result.release.release_id} --site ${siteId}`,
657
+ replay: null,
658
+ message: "The release went live. Whether it is the one readers see now could not be read here."
659
+ };
660
+ }
661
+ if (result.owner_handoff) {
662
+ return { command: `saturndocs suggestions diff ${suggestionId} --site ${siteId}`, replay: null, message: "The page content is not in this result; read the pull request before approving." };
663
+ }
664
+ return null;
665
+ }
666
+ function textFor(result, suggestionId) {
667
+ const head = `${suggestionId} ${result.outcome} (${result.state ?? "state unread"})`;
668
+ const release = result.release === null ? null : `release ${result.release.release_id}: ${result.release.summary}`;
669
+ return [head, result.note, release].filter((line) => line !== null).join("\n");
670
+ }
671
+ function registerSuggestionsWait(group, environment = {}) {
672
+ const metadata = SUGGESTIONS_WAIT_METADATA[0];
673
+ const command = group.command("wait").argument("<suggestion-id>", "The suggestion to follow.").action(async (suggestionId, options, self) => {
674
+ const globals = resolveGlobalOptions(self);
675
+ const writer = createResultWriter({ json: globals.json });
676
+ const context = { command: "suggestions wait", siteId: globals.site };
677
+ try {
678
+ const until = parseSuggestionWaitTarget(options.until);
679
+ const signal = environment.signal ?? interruptSignal() ?? void 0;
680
+ const client = clientFor(globals, environment);
681
+ const target = await resolveTarget(client, { site: globals.site ?? void 0, signal });
682
+ context.siteId = target.site_id;
683
+ const result = await waitForSuggestion(client, {
684
+ site: target.site_id,
685
+ suggestionId,
686
+ until,
687
+ deadlineMs: globals.timeout === null ? DEFAULT_SUGGESTION_WAIT_MS : globals.timeout * 1e3,
688
+ signal
689
+ });
690
+ const resolved = {
691
+ ...context,
692
+ ids: idsOf(result, suggestionId),
693
+ limitations: result.limitations,
694
+ recovery: recoveryFor(result, suggestionId, target.site_id)
695
+ };
696
+ const code = suggestionWaitErrorCode(result.outcome);
697
+ if (code === null) {
698
+ process.exitCode = writer.write(successResult(resolved, result), { text: textFor(result, suggestionId) });
699
+ return;
700
+ }
701
+ const failed = new CliError(code, textFor(result, suggestionId), { recovery: resolved.recovery, data: result });
702
+ process.exitCode = writer.write(failureResult(failed, resolved));
703
+ } catch (error) {
704
+ process.exitCode = writer.write(failureResult(asCliError(error), context));
705
+ }
706
+ });
707
+ applyCommandHelp(command, metadata);
708
+ }
709
+
710
+ // src/commands/suggestions/index.ts
711
+ var SUGGESTIONS_METADATA = Object.freeze([
712
+ ...SUGGESTIONS_READ_METADATA,
713
+ ...SUGGESTIONS_DIFF_METADATA,
714
+ ...SUGGESTIONS_PREVIEW_METADATA,
715
+ ...SUGGESTIONS_APPROVE_METADATA,
716
+ ...SUGGESTIONS_DISMISS_METADATA,
717
+ ...SUGGESTIONS_CONVERSATION_METADATA,
718
+ ...SUGGESTIONS_MESSAGE_METADATA,
719
+ ...SUGGESTIONS_REQUEST_CHANGES_METADATA,
720
+ ...SUGGESTIONS_RETRY_METADATA,
721
+ ...SUGGESTIONS_WAIT_METADATA
722
+ ]);
723
+ function registerSuggestions(program) {
724
+ const group = program.command("suggestions").description("Read, diff, and act on a site's suggestions");
725
+ registerSuggestionsRead(group);
726
+ registerSuggestionsDiff(group);
727
+ registerSuggestionsPreview(group);
728
+ registerSuggestionsApprove(group);
729
+ registerSuggestionsDismiss(group);
730
+ registerSuggestionsConversation(group);
731
+ registerSuggestionsMessage(group);
732
+ registerSuggestionsRequestChanges(group);
733
+ registerSuggestionsRetry(group);
734
+ registerSuggestionsWait(group);
735
+ }
736
+ export {
737
+ SUGGESTIONS_METADATA,
738
+ registerSuggestions
739
+ };