@bli-cockpit/cli 0.2.242 → 0.2.243

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 (121) hide show
  1. package/dist/adapters/git-state.js +1 -0
  2. package/dist/adapters/raw-evidence-collection-state.js +22 -11
  3. package/dist/adapters/raw-evidence-git-diff.js +1 -0
  4. package/dist/agent-rules.js +8 -0
  5. package/dist/autostart-contract.js +12 -0
  6. package/dist/autostart-darwin-plist.js +2 -2
  7. package/dist/autostart-darwin.js +1 -1
  8. package/dist/autostart-remind.js +245 -0
  9. package/dist/autostart-self-heal.js +17 -1
  10. package/dist/autostart-windows-scripts.js +7 -7
  11. package/dist/autostart.js +16 -15
  12. package/dist/commands/approvals-check-where.js +14 -0
  13. package/dist/commands/approvals-render.js +8 -1
  14. package/dist/commands/approvals.js +3 -2
  15. package/dist/commands/autostart-command.js +10 -0
  16. package/dist/commands/autostart-heal.js +36 -1
  17. package/dist/commands/brain.js +1 -1
  18. package/dist/commands/cal-agenda.js +44 -0
  19. package/dist/commands/cal-availability-share.js +97 -0
  20. package/dist/commands/cal-booking.js +147 -0
  21. package/dist/commands/cal-join.js +62 -0
  22. package/dist/commands/cal-links.js +68 -0
  23. package/dist/commands/cal-people.js +136 -0
  24. package/dist/commands/cal-remind.js +176 -0
  25. package/dist/commands/cal-reminders.js +33 -0
  26. package/dist/commands/cal-suggest.js +89 -0
  27. package/dist/commands/cal.js +41 -1
  28. package/dist/commands/cycle-args.js +127 -0
  29. package/dist/commands/cycles.js +117 -0
  30. package/dist/commands/docs-comments-args.js +85 -0
  31. package/dist/commands/docs-comments.js +128 -0
  32. package/dist/commands/docs-files-args.js +49 -0
  33. package/dist/commands/docs-files.js +179 -0
  34. package/dist/commands/docs-formatting.js +45 -0
  35. package/dist/commands/docs-inbox-args.js +93 -0
  36. package/dist/commands/docs-inbox.js +126 -0
  37. package/dist/commands/docs-notion.js +5 -0
  38. package/dist/commands/docs-read.js +2 -1
  39. package/dist/commands/docs-recovery.js +173 -0
  40. package/dist/commands/docs-refs-args.js +104 -0
  41. package/dist/commands/docs-refs.js +153 -0
  42. package/dist/commands/docs-search-args.js +79 -0
  43. package/dist/commands/docs-search.js +64 -0
  44. package/dist/commands/docs-sync-args.js +82 -0
  45. package/dist/commands/docs-sync.js +197 -0
  46. package/dist/commands/docs-tree.js +136 -0
  47. package/dist/commands/docs.js +85 -51
  48. package/dist/commands/doctor-update.js +4 -1
  49. package/dist/commands/inbox-args.js +81 -0
  50. package/dist/commands/inbox.js +88 -0
  51. package/dist/commands/issue-batch.js +61 -0
  52. package/dist/commands/issue-relations.js +151 -0
  53. package/dist/commands/issue-write.js +38 -5
  54. package/dist/commands/issue.js +32 -1
  55. package/dist/commands/local-args-tower-approvals.js +16 -4
  56. package/dist/commands/local-args-tower-cal-booking.js +81 -0
  57. package/dist/commands/local-args-tower-cal-links.js +42 -0
  58. package/dist/commands/local-args-tower-cal-remind.js +74 -0
  59. package/dist/commands/local-args-tower-cal-share.js +41 -0
  60. package/dist/commands/local-args-tower-cal.js +81 -8
  61. package/dist/commands/local-args-tower-docs-msg.js +98 -9
  62. package/dist/commands/local-args-tower-msg.js +222 -10
  63. package/dist/commands/local-args-tower-search.js +32 -5
  64. package/dist/commands/local-args-tower-work.js +110 -34
  65. package/dist/commands/local-auth.js +1 -1
  66. package/dist/commands/local-help-commands-tower-ops.js +2 -0
  67. package/dist/commands/local-help-commands-tower.js +47 -31
  68. package/dist/commands/local-help-cycles.js +9 -0
  69. package/dist/commands/local-help-docs-files.js +10 -0
  70. package/dist/commands/local-help-docs-formatting.js +7 -0
  71. package/dist/commands/local-help-docs-inbox.js +10 -0
  72. package/dist/commands/local-help-docs-recovery.js +10 -0
  73. package/dist/commands/local-help-docs-refs.js +11 -0
  74. package/dist/commands/local-help-docs-sync.js +9 -0
  75. package/dist/commands/local-help-inbox.js +9 -0
  76. package/dist/commands/local-help-project.js +21 -0
  77. package/dist/commands/local-help-templates.js +9 -0
  78. package/dist/commands/local-help-views.js +10 -0
  79. package/dist/commands/local-help-work-notify.js +6 -0
  80. package/dist/commands/local-help.js +2 -2
  81. package/dist/commands/mcp-stdio-probe.js +1 -0
  82. package/dist/commands/msg-activity.js +134 -0
  83. package/dist/commands/msg-channel-args.js +92 -0
  84. package/dist/commands/msg-channel.js +125 -0
  85. package/dist/commands/msg-context-args.js +67 -0
  86. package/dist/commands/msg-context.js +65 -0
  87. package/dist/commands/msg-dm.js +28 -0
  88. package/dist/commands/msg-file.js +185 -0
  89. package/dist/commands/msg-mark-args.js +78 -0
  90. package/dist/commands/msg-mark.js +148 -0
  91. package/dist/commands/msg-notify-args.js +100 -0
  92. package/dist/commands/msg-notify.js +122 -0
  93. package/dist/commands/msg-schedule-args.js +114 -0
  94. package/dist/commands/msg-schedule.js +226 -0
  95. package/dist/commands/msg-search-args.js +71 -0
  96. package/dist/commands/msg-search.js +48 -0
  97. package/dist/commands/msg-write.js +15 -1
  98. package/dist/commands/msg.js +62 -39
  99. package/dist/commands/project-args.js +82 -0
  100. package/dist/commands/project.js +71 -7
  101. package/dist/commands/public-root.js +1 -1
  102. package/dist/commands/search.js +23 -0
  103. package/dist/commands/session-sync-failures.js +17 -4
  104. package/dist/commands/session-sync-health.js +7 -2
  105. package/dist/commands/session-sync-hold.js +42 -1
  106. package/dist/commands/session-sync-upload.js +2 -0
  107. package/dist/commands/session-sync.js +1 -0
  108. package/dist/commands/sync-doctor.js +14 -1
  109. package/dist/commands/sync-report.js +1 -1
  110. package/dist/commands/template-args.js +122 -0
  111. package/dist/commands/templates.js +162 -0
  112. package/dist/commands/view-args.js +131 -0
  113. package/dist/commands/views.js +138 -0
  114. package/dist/commands/work-notify-args.js +67 -0
  115. package/dist/commands/work-notify.js +63 -0
  116. package/dist/delivery-hold-notice.js +34 -0
  117. package/dist/log-rotation.js +2 -2
  118. package/dist/notifier.js +83 -0
  119. package/dist/process-runner.js +4 -0
  120. package/dist/repo-identity-git.js +1 -0
  121. package/package.json +4 -4
@@ -0,0 +1,79 @@
1
+ /**
2
+ * The docs-search flags shared by `cockpit docs search` and `cockpit search --kind doc` (BLI-21697, docs parity
3
+ * step 6): `--title-only`, `--word`, `--by <me|email|id>`, `--under <id|slug>`, `--since` / `--until`
4
+ * (`YYYY-MM-DD`, inclusive local days on `updated_at`), `--timezone <IANA>` and `--sort relevance|updated`. One
5
+ * reader and one query-string writer, so the two commands cannot mean different things. The door checks the
6
+ * values; this file only checks shape.
7
+ */
8
+ import { optionalNonEmpty } from "./local-arg-values.js";
9
+ export const DOC_SEARCH_VALUE_FLAGS = ["--by", "--under", "--sort"];
10
+ export const DOC_SEARCH_BOOLEAN_FLAGS = ["--title-only", "--word"];
11
+ /** Flags only a docs search reads (`--since`, `--until` and `--timezone` are shared with the message search). */
12
+ export const DOC_SEARCH_ONLY_FLAGS = [...DOC_SEARCH_VALUE_FLAGS, ...DOC_SEARCH_BOOLEAN_FLAGS];
13
+ const DAY = /^\d{4}-\d{2}-\d{2}$/;
14
+ function machineZone() {
15
+ try {
16
+ return Intl.DateTimeFormat().resolvedOptions().timeZone || "UTC";
17
+ }
18
+ catch {
19
+ return "UTC";
20
+ }
21
+ }
22
+ function day(flag, value) {
23
+ const given = optionalNonEmpty(value);
24
+ if (given === undefined)
25
+ return undefined;
26
+ if (!DAY.test(given))
27
+ throw new Error(`${flag} takes a day written YYYY-MM-DD, not "${given}".`);
28
+ return given;
29
+ }
30
+ /** Reads the flags. The machine's IANA zone is sent unless `--timezone` is given. */
31
+ export function readDocSearchFilters(values) {
32
+ const since = day("--since", values.flags.get("--since"));
33
+ const until = day("--until", values.flags.get("--until"));
34
+ if (since && until && since > until)
35
+ throw new Error(`--since ${since} is after --until ${until}.`);
36
+ const sort = optionalNonEmpty(values.flags.get("--sort"));
37
+ if (sort !== undefined && sort !== "relevance" && sort !== "updated") {
38
+ throw new Error(`--sort takes relevance or updated, not "${sort}".`);
39
+ }
40
+ const by = optionalNonEmpty(values.flags.get("--by"));
41
+ const under = optionalNonEmpty(values.flags.get("--under"));
42
+ return {
43
+ ...(values.booleans.has("--title-only") ? { titleOnly: true } : {}),
44
+ ...(values.booleans.has("--word") ? { wholeWord: true } : {}),
45
+ ...(by ? { by } : {}),
46
+ ...(under ? { under } : {}),
47
+ ...(since ? { since } : {}),
48
+ ...(until ? { until } : {}),
49
+ timezone: optionalNonEmpty(values.flags.get("--timezone")) ?? machineZone(),
50
+ ...(sort ? { sort } : {}),
51
+ };
52
+ }
53
+ /** True when a flag that narrows by person, subtree or date was given (an empty query is then allowed). */
54
+ export function hasDocScopeFlags(filters) {
55
+ return Boolean(filters.by || filters.under || filters.since || filters.until);
56
+ }
57
+ /** True when any docs-only flag was given (so `cockpit search` can refuse them without `--kind doc`). */
58
+ export function hasDocSearchFlags(filters) {
59
+ return Boolean(filters.titleOnly || filters.wholeWord || filters.sort || hasDocScopeFlags(filters));
60
+ }
61
+ /** The query parameters `GET /api/docs/search` and `GET /api/search` read. */
62
+ export function appendDocSearchParams(params, filters) {
63
+ if (filters.titleOnly)
64
+ params.set("title_only", "1");
65
+ if (filters.wholeWord)
66
+ params.set("word", "1");
67
+ if (filters.by)
68
+ params.set("by", filters.by);
69
+ if (filters.under)
70
+ params.set("under", filters.under);
71
+ if (filters.since)
72
+ params.set("since", filters.since);
73
+ if (filters.until)
74
+ params.set("until", filters.until);
75
+ if (filters.timezone)
76
+ params.set("tz", filters.timezone);
77
+ if (filters.sort)
78
+ params.set("sort", filters.sort);
79
+ }
@@ -0,0 +1,64 @@
1
+ /**
2
+ * `cockpit docs search` (BLI-21697, docs parity step 6): the library searched by title or text, narrowed by
3
+ * person, subtree and day, over `GET /api/docs/search` — the SAME door the web box calls, so the ids match the
4
+ * browser, MCP `docs_search` and JARVIS `searchDocs`. Nothing is matched on this side.
5
+ *
6
+ * It prints the hits, `matched/returned` and the coverage line. A walk that stopped early is said out loud
7
+ * (`capped`, `failed_partial`), never folded into "nothing matched". A door refusal keeps its own `reason` and
8
+ * sentence (`invalid_filter` names the bad field).
9
+ */
10
+ import { askAgentDoor, emitAgentDoor, failAgentDoor } from "./agent-door.js";
11
+ import { writeLine } from "./cli-io.js";
12
+ import { appendDocSearchParams } from "./docs-search-args.js";
13
+ const TAG = "[docs search cli]";
14
+ const READ_DEADLINE_MS = 30_000;
15
+ export function docsSearchPath(command) {
16
+ const params = new URLSearchParams();
17
+ if (command.query)
18
+ params.set("q", command.query);
19
+ appendDocSearchParams(params, command.searchFilters ?? {});
20
+ if (command.limit !== undefined)
21
+ params.set("limit", String(command.limit));
22
+ return `/api/docs/search?${params.toString()}`;
23
+ }
24
+ /** The sentence under the hits: the walk's honesty, in words. */
25
+ export function docsSearchCoverageLine(body) {
26
+ const counts = `${body.returned ?? 0} of ${body.matched ?? 0} shown`;
27
+ if (body.coverage === "capped")
28
+ return `Coverage: capped after ${body.searched ?? 0} pages searched; more exist (${counts}).`;
29
+ if (body.coverage === "failed_partial") {
30
+ return `Coverage: partial, some pages could not be searched${body.coverage_reason ? ` (${body.coverage_reason})` : ""} (${counts}).`;
31
+ }
32
+ return `Coverage: exhausted (${counts}).`;
33
+ }
34
+ export async function searchDocs(command, door) {
35
+ const answer = await askAgentDoor(door, {
36
+ path: docsSearchPath(command),
37
+ method: "GET",
38
+ label: "docs search",
39
+ timeoutMs: READ_DEADLINE_MS,
40
+ });
41
+ if (!answer.ok)
42
+ return failAgentDoor(door, TAG, answer.reason, answer.detail);
43
+ const body = answer.body;
44
+ if (door.json)
45
+ return emitAgentDoor(door, body);
46
+ const results = body.results ?? [];
47
+ if (results.length === 0) {
48
+ writeLine(door.io.stdout, body.coverage === "exhausted" || body.coverage === undefined
49
+ ? "Nothing you can read matches that."
50
+ : "Nothing in the pages searched matches that.");
51
+ }
52
+ for (const result of results) {
53
+ writeLine(door.io.stdout, `${result.title} (${result.slug ?? result.id})`);
54
+ const where = result.matched_in ? `matched in ${result.matched_in} · ` : "";
55
+ writeLine(door.io.stdout, ` ${where}${result.id} · updated ${result.updated_at}`);
56
+ if (result.snippet)
57
+ writeLine(door.io.stdout, ` ${result.snippet}`);
58
+ }
59
+ writeLine(door.io.stdout, "");
60
+ writeLine(door.io.stdout, `${body.returned ?? results.length}/${body.matched ?? results.length} matched/returned.`);
61
+ writeLine(door.io.stdout, docsSearchCoverageLine(body));
62
+ console.error(`${TAG} searched`, JSON.stringify({ matched: body.matched ?? 0, returned: results.length, coverage: body.coverage ?? "unknown" }));
63
+ return 0;
64
+ }
@@ -0,0 +1,82 @@
1
+ /**
2
+ * `cockpit docs sync-resolve|manifest` argument parsing (BLI-21875, docs parity step 12). `sync-status` keeps its parser in
3
+ * `local-args-tower-docs-msg.ts`; its single-page answer gained detail, not flags.
4
+ *
5
+ * sync-resolve <id|slug> --version <id> --keep-current|--use-kept [--expected-revision N] [--request-id U]
6
+ * manifest [--under <id|slug>] [--cursor C] [--all] [--out <file>]
7
+ *
8
+ * The door checks authority and the revision; this file only checks shape. It never picks a choice for the person.
9
+ */
10
+ import { optionalNonEmpty, optionalUrl, parseNamedArgs } from "./local-arg-values.js";
11
+ const VALUE_FLAGS = ["--home", "--dashboard-url", "--version", "--expected-revision", "--request-id", "--under", "--cursor", "--out"];
12
+ const BOOLEAN_FLAGS = ["--json", "--keep-current", "--use-kept", "--all"];
13
+ const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
14
+ export function isDocsSyncInvocation(args) {
15
+ return args[0] === "sync-resolve" || args[0] === "manifest";
16
+ }
17
+ export function parseDocsSyncArgs(args) {
18
+ const values = parseNamedArgs(args, { allowedFlags: [...VALUE_FLAGS, ...BOOLEAN_FLAGS], valueFlags: VALUE_FLAGS });
19
+ const verb = args[0];
20
+ const rest = values.positionals.slice(1);
21
+ const base = {
22
+ kind: "docs",
23
+ clearParent: false,
24
+ bodyStdin: false,
25
+ homeDir: optionalNonEmpty(values.flags.get("--home")),
26
+ dashboardUrl: optionalUrl(values.flags.get("--dashboard-url")),
27
+ json: values.booleans.has("--json"),
28
+ };
29
+ const only = (allowed) => {
30
+ for (const flag of [...VALUE_FLAGS.slice(2), ...BOOLEAN_FLAGS.slice(1)]) {
31
+ const given = values.flags.has(flag) || values.booleans.has(flag);
32
+ if (given && !allowed.includes(flag))
33
+ throw new Error(`${flag} does not belong to \`cockpit docs ${verb}\`.`);
34
+ }
35
+ };
36
+ if (verb === "manifest") {
37
+ only(["--under", "--cursor", "--all", "--out"]);
38
+ if (rest.length !== 0)
39
+ throw new Error("docs manifest takes no page; use --under <id|slug> for one subtree.");
40
+ const cursor = optionalNonEmpty(values.flags.get("--cursor"));
41
+ return {
42
+ ...base,
43
+ action: "manifest",
44
+ ...(cursor ? { cursor } : {}),
45
+ all: values.booleans.has("--all"),
46
+ outPath: optionalNonEmpty(values.flags.get("--out")),
47
+ docsSync: { under: optionalNonEmpty(values.flags.get("--under")) },
48
+ };
49
+ }
50
+ only(["--version", "--keep-current", "--use-kept", "--expected-revision", "--request-id"]);
51
+ if (rest.length !== 1)
52
+ throw new Error("docs sync-resolve needs exactly one page id or slug, e.g. `cockpit docs sync-resolve runbook --version <id> --use-kept`.");
53
+ const keep = values.booleans.has("--keep-current");
54
+ const useKept = values.booleans.has("--use-kept");
55
+ if (keep === useKept) {
56
+ throw new Error("docs sync-resolve needs exactly one choice: --keep-current (keep the page's current text) or --use-kept (make the kept version current). Tower does not choose for you.");
57
+ }
58
+ const versionId = optionalNonEmpty(values.flags.get("--version"));
59
+ if (!versionId || !UUID.test(versionId))
60
+ throw new Error("docs sync-resolve needs --version <id>, the kept version's id (a UUID from `cockpit docs sync-status <page>`).");
61
+ const requestId = optionalNonEmpty(values.flags.get("--request-id"));
62
+ if (requestId && !UUID.test(requestId))
63
+ throw new Error("--request-id must be a UUID.");
64
+ const revisionRaw = optionalNonEmpty(values.flags.get("--expected-revision"));
65
+ let expectedRevision;
66
+ if (revisionRaw !== undefined) {
67
+ if (keep)
68
+ throw new Error("--expected-revision belongs with --use-kept: --keep-current writes no text.");
69
+ expectedRevision = /^\d+$/.test(revisionRaw) ? Number(revisionRaw) : Number.NaN;
70
+ if (!Number.isInteger(expectedRevision) || expectedRevision < 1)
71
+ throw new Error("--expected-revision is a positive whole number (the page revision you read).");
72
+ }
73
+ return {
74
+ ...base,
75
+ action: "sync-resolve",
76
+ docRef: rest[0],
77
+ versionId,
78
+ ...(requestId ? { requestId } : {}),
79
+ ...(expectedRevision !== undefined ? { expectedRevision } : {}),
80
+ docsSync: { choice: keep ? "keep_current" : "use_kept_version" },
81
+ };
82
+ }
@@ -0,0 +1,197 @@
1
+ /**
2
+ * `cockpit docs sync-status <page>` detail, `sync-resolve` and `manifest` (BLI-21875): the Notion sync of one page and the
3
+ * library export manifest, over the same doors the browser calls.
4
+ *
5
+ * sync-status <page> GET /api/docs/documents/[id]/notion-sync
6
+ * sync-resolve <page> POST /api/docs/documents/[id]/notion-sync/resolve
7
+ * manifest GET /api/docs/manifest[?cursor=&under=]
8
+ *
9
+ * Every line about a receipt, a loss or a conflict is the door's own `sentence`, printed verbatim, so this terminal says
10
+ * what the web panel, the MCP tool and JARVIS say. Tower never asks Notion from a request: the answer is what the last
11
+ * passes recorded, with its age. `manifest` reports readiness; it gives no cutover verdict.
12
+ */
13
+ import { open } from "node:fs/promises";
14
+ import { askAgentDoor, emitAgentDoor, failAgentDoor } from "./agent-door.js";
15
+ import { writeLine } from "./cli-io.js";
16
+ const TAG = "[docs sync cli]";
17
+ const READ_DEADLINE_MS = 60_000;
18
+ const WRITE_DEADLINE_MS = 60_000;
19
+ /** 100 calls of 2,000 pages: the most `manifest --all` walks before it says `capped`. */
20
+ const MANIFEST_MAX_CALLS = 100;
21
+ const list = (value) => (Array.isArray(value) ? value : []);
22
+ const str = (value) => (typeof value === "string" ? value : "");
23
+ const num = (value) => (typeof value === "number" ? value : 0);
24
+ const object = (value) => (typeof value === "object" && value !== null ? value : {});
25
+ export async function runDocsSync(command, door) {
26
+ switch (command.action) {
27
+ case "sync-resolve": return syncResolve(command, door);
28
+ case "manifest": return manifest(command, door);
29
+ default: return failAgentDoor(door, TAG, "invalid_body", "Try sync-status <page>, sync-resolve or manifest.");
30
+ }
31
+ }
32
+ /** One page's sync status: the list row plus the receipts, losses, conflicts and decisions the door's `detail` carries. */
33
+ export function printSyncDetail(door, ref, body) {
34
+ const detail = object(body["detail"]);
35
+ const conflicts = object(detail["conflicts"]);
36
+ writeLine(door.io.stderr, `${TAG} status ${JSON.stringify({ conflicts: conflicts["total"] ?? null, open: conflicts["open"] ?? null, incomplete: list(detail["incomplete"]).length })}`);
37
+ const row = list(body["documents"])[0] ?? {};
38
+ writeLine(door.io.stdout, `${str(row["id"])} ${str(row["state"])} ${str(row["slug"]) || "(no slug)"} ${str(row["title"])}`);
39
+ writeLine(door.io.stdout, str(detail["state_sentence"]));
40
+ const receipts = object(detail["last_receipt"]);
41
+ for (const direction of ["push", "pull"]) {
42
+ const receipt = object(receipts[direction]);
43
+ writeLine(door.io.stdout, str(receipt["sentence"]) || `${direction === "push" ? "Push" : "Pull"}: no check recorded for this page yet.`);
44
+ }
45
+ const losses = object(detail["losses"]);
46
+ for (const line of Array.isArray(losses["sentences"]) ? losses["sentences"].map(str) : [])
47
+ writeLine(door.io.stdout, `Loss: ${line}`);
48
+ for (const name of Array.isArray(detail["incomplete"]) ? detail["incomplete"].map(str) : []) {
49
+ writeLine(door.io.stdout, `Could not read just now: ${name}`);
50
+ }
51
+ const items = list(conflicts["items"]);
52
+ for (const item of items) {
53
+ const resolution = object(item["resolution"]);
54
+ const status = str(item["status"]) === "open" ? "OPEN " : "resolved";
55
+ const decided = str(item["status"]) === "open" ? "" : ` decision: ${str(resolution["choice"])} at ${str(resolution["resolved_at"])}`;
56
+ writeLine(door.io.stdout, `Conflict ${status} version ${str(item["version_id"])} (v${num(item["version_number"])}, kept ${str(item["kept_at"])}) ${str(item["sentence"])}${decided}`);
57
+ }
58
+ writeLine(door.io.stdout, `${num(conflicts["total"])} conflict(s) kept, ${num(conflicts["open"])} open; showing the newest ${items.length}. As of ${str(detail["as_of"])}.`);
59
+ if (num(conflicts["open"]) > 0) {
60
+ writeLine(door.io.stdout, detail["can_edit"] === true
61
+ ? `Resolve: cockpit docs sync-resolve ${ref} --version <version id> --keep-current | --use-kept [--expected-revision N]. ${str(detail["resolve_note"])}`
62
+ : "You can read this page but not edit it, so you cannot resolve its conflicts.");
63
+ }
64
+ return 0;
65
+ }
66
+ async function syncResolve(command, door) {
67
+ const ref = command.docRef ?? "";
68
+ const answer = await askAgentDoor(door, {
69
+ path: `/api/docs/documents/${encodeURIComponent(ref)}/notion-sync/resolve`,
70
+ method: "POST",
71
+ label: "docs sync-resolve",
72
+ timeoutMs: WRITE_DEADLINE_MS,
73
+ body: {
74
+ version_id: command.versionId,
75
+ choice: command.docsSync?.choice,
76
+ ...(command.expectedRevision !== undefined ? { expected_revision: command.expectedRevision } : {}),
77
+ ...(command.requestId ? { request_id: command.requestId } : {}),
78
+ },
79
+ });
80
+ if (!answer.ok)
81
+ return failAgentDoor(door, TAG, answer.reason, answer.detail);
82
+ const body = answer.body;
83
+ writeLine(door.io.stderr, `${TAG} resolve ${JSON.stringify({ outcome: body["outcome"] ?? null, choice: object(body["resolution"])["choice"] ?? null, body_written: body["body_written"] ?? null })}`);
84
+ if (door.json)
85
+ return emitAgentDoor(door, { ...body, ok: true });
86
+ const resolution = object(body["resolution"]);
87
+ writeLine(door.io.stdout, str(body["outcome"]) === "already_resolved"
88
+ ? `Already resolved (${str(resolution["choice"])}, ${str(resolution["resolved_at"])}). Nothing was changed.`
89
+ : `Recorded ${str(resolution["choice"])} for version ${str(resolution["version_id"])}. Page revision now ${String(body["revision"] ?? "unknown")}${body["body_written"] === true ? "; the kept text is now current" : ""}.`);
90
+ writeLine(door.io.stdout, str(body["note"]));
91
+ return 0;
92
+ }
93
+ async function manifest(command, door) {
94
+ const totals = { entries: [], seen: new Set(), requested: 0, returned: 0, failures: [] };
95
+ let cursor = command.cursor ?? null;
96
+ let calls = 0;
97
+ let status = "exhausted";
98
+ let reason;
99
+ let nextCursor = null;
100
+ let under = null;
101
+ let generatedAt = null;
102
+ for (;;) {
103
+ const query = new URLSearchParams();
104
+ if (cursor)
105
+ query.set("cursor", cursor);
106
+ if (command.docsSync?.under)
107
+ query.set("under", command.docsSync.under);
108
+ const answer = await askAgentDoor(door, {
109
+ path: `/api/docs/manifest${query.size > 0 ? `?${query.toString()}` : ""}`,
110
+ method: "GET",
111
+ label: "docs manifest",
112
+ timeoutMs: READ_DEADLINE_MS,
113
+ });
114
+ if (!answer.ok) {
115
+ if (calls === 0)
116
+ return failAgentDoor(door, TAG, answer.reason, answer.detail);
117
+ // A later call failed: the pages already read are kept, and the cursor resumes after them.
118
+ status = "failed_partial";
119
+ reason = answer.reason;
120
+ nextCursor = cursor;
121
+ writeLine(door.io.stderr, `${TAG} manifest ${JSON.stringify({ call: calls + 1, reason: answer.reason, kept: totals.entries.length })}`);
122
+ break;
123
+ }
124
+ calls += 1;
125
+ const page = answer.body;
126
+ const coverage = object(page["coverage"]);
127
+ totals.requested += num(page["requested"]);
128
+ totals.returned += num(page["returned"]);
129
+ for (const entry of list(page["entries"])) {
130
+ const id = str(entry["id"]);
131
+ if (!totals.seen.has(id)) {
132
+ totals.seen.add(id);
133
+ totals.entries.push(entry);
134
+ }
135
+ }
136
+ totals.failures.push(...list(page["failures"]));
137
+ under = page["under"] ?? under;
138
+ generatedAt = page["generated_at"] ?? generatedAt;
139
+ nextCursor = typeof page["next_cursor"] === "string" ? page["next_cursor"] : null;
140
+ status = str(coverage["status"]) || "exhausted";
141
+ reason = str(coverage["reason"]) || undefined;
142
+ if (status === "failed_partial" || nextCursor === null || !command.all)
143
+ break;
144
+ if (calls >= MANIFEST_MAX_CALLS) {
145
+ status = "capped";
146
+ reason = "call_ceiling";
147
+ break;
148
+ }
149
+ cursor = nextCursor;
150
+ }
151
+ const result = {
152
+ ok: true,
153
+ entries: totals.entries,
154
+ requested: totals.requested,
155
+ returned: totals.returned,
156
+ unique: totals.seen.size,
157
+ coverage: { status, calls, ...(reason ? { reason } : {}) },
158
+ coverage_text: `${totals.seen.size} unique pages in ${calls} call(s); requested ${totals.requested}, returned ${totals.returned}; read stopped: ${status}${reason ? ` (${reason})` : ""}.`,
159
+ failures: totals.failures,
160
+ next_cursor: status === "exhausted" ? null : nextCursor,
161
+ under,
162
+ generated_at: generatedAt,
163
+ };
164
+ writeLine(door.io.stderr, `${TAG} manifest ${JSON.stringify({ calls, requested: result.requested, returned: result.returned, unique: result.unique, status, failures: result.failures.length })}`);
165
+ if (command.outPath) {
166
+ try {
167
+ // "wx": never overwrite a file someone already has.
168
+ const handle = await open(command.outPath, "wx");
169
+ try {
170
+ await handle.writeFile(`${JSON.stringify(result, null, 2)}\n`);
171
+ }
172
+ finally {
173
+ await handle.close();
174
+ }
175
+ }
176
+ catch (error) {
177
+ const code = error.code;
178
+ return failAgentDoor(door, TAG, code === "EEXIST" ? "out_exists" : "out_write_failed", code === "EEXIST" ? `${command.outPath} already exists; choose another path or remove it.` : `Could not write ${command.outPath}.`);
179
+ }
180
+ writeLine(door.io.stderr, `${TAG} manifest written ${JSON.stringify({ entries: result.entries.length })}`);
181
+ }
182
+ if (door.json)
183
+ return emitAgentDoor(door, command.outPath ? { ...result, entries: undefined, written_to: command.outPath, entries_written: result.entries.length } : result, status === "failed_partial" ? 1 : 0);
184
+ for (const entry of totals.entries.slice(0, command.outPath ? 0 : 200)) {
185
+ const files = Array.isArray(entry["files"]) ? `${entry["files"].length} file(s)` : "files unknown";
186
+ writeLine(door.io.stdout, `${str(entry["id"])} r${num(entry["revision"])} ${str(entry["visibility"])} ${files} ${str(entry["slug"]) || "(no slug)"} ${str(entry["title"])}`);
187
+ }
188
+ if (!command.outPath && totals.entries.length > 200)
189
+ writeLine(door.io.stdout, `… ${totals.entries.length - 200} more; use --out <file> or --json for all of them.`);
190
+ writeLine(door.io.stdout, result.coverage_text);
191
+ for (const failure of totals.failures)
192
+ writeLine(door.io.stdout, `Failure: ${str(failure["stage"])} ${str(failure["reason"])} (${num(failure["count"])})`);
193
+ if (result.next_cursor)
194
+ writeLine(door.io.stdout, `More: cockpit docs manifest --cursor ${result.next_cursor}${command.docsSync?.under ? ` --under ${command.docsSync.under}` : ""}`);
195
+ writeLine(door.io.stdout, "This reports readiness only. It is not a cutover verdict.");
196
+ return status === "failed_partial" ? 1 : 0;
197
+ }
@@ -0,0 +1,136 @@
1
+ import { askAgentDoor, emitAgentDoor, failAgentDoor } from "./agent-door.js";
2
+ import { writeLine } from "./cli-io.js";
3
+ import { docsListPath, resolveDocId } from "./docs.js";
4
+ const TAG = "[docs cli]";
5
+ const READ_DEADLINE_MS = 30_000;
6
+ const WRITE_DEADLINE_MS = 60_000;
7
+ const WALK_MAX_PAGES = 20;
8
+ const PAGE_SIZE = 1000;
9
+ function renderTree(nodes, io, depth) {
10
+ for (const node of nodes) {
11
+ writeLine(io.stdout, `${" ".repeat(depth)}${node.title} (${node.slug ?? node.id})`);
12
+ if (node.children.length > 0)
13
+ renderTree(node.children, io, depth + 1);
14
+ }
15
+ }
16
+ function coverageSentence(coverage) {
17
+ const counts = `requested pages ${coverage.pages}, returned ${coverage.returned}, unique ${coverage.unique}`;
18
+ if (coverage.status === "exhausted")
19
+ return `Coverage: exhausted (${counts}).`;
20
+ if (coverage.status === "capped")
21
+ return `Coverage: capped at ${coverage.unique} documents; more exist (${counts}).`;
22
+ return `Coverage: partial, some pages could not load${coverage.reason ? ` (${coverage.reason})` : ""} (${counts}).`;
23
+ }
24
+ export async function treeDocs(door) {
25
+ const answer = await askAgentDoor(door, {
26
+ path: "/api/docs/tree",
27
+ method: "GET",
28
+ label: "docs tree",
29
+ timeoutMs: READ_DEADLINE_MS,
30
+ });
31
+ if (!answer.ok)
32
+ return failAgentDoor(door, TAG, answer.reason, answer.detail);
33
+ const body = answer.body;
34
+ const tree = body.tree ?? [];
35
+ if (door.json)
36
+ return emitAgentDoor(door, { ok: true, tree, ...(body.coverage ? { coverage: body.coverage } : {}) });
37
+ if (tree.length === 0) {
38
+ writeLine(door.io.stdout, "No documents.");
39
+ }
40
+ else {
41
+ renderTree(tree, door.io, 0);
42
+ }
43
+ if (body.coverage)
44
+ writeLine(door.io.stdout, coverageSentence(body.coverage));
45
+ return 0;
46
+ }
47
+ /** `docs list --all`: walks every `?cursor=` page with the same filters, then prints the receipt. */
48
+ export async function listAllDocs(command, door) {
49
+ const documents = [];
50
+ const seen = new Set();
51
+ let returned = 0;
52
+ let pages = 0;
53
+ let cursor;
54
+ let status = "exhausted";
55
+ let reason;
56
+ for (;;) {
57
+ const path = docsListPath({ ...command, cursor, limit: PAGE_SIZE });
58
+ const answer = await askAgentDoor(door, { path, method: "GET", label: "docs list", timeoutMs: READ_DEADLINE_MS });
59
+ if (!answer.ok) {
60
+ if (pages === 0)
61
+ return failAgentDoor(door, TAG, answer.reason, answer.detail);
62
+ status = "failed_partial";
63
+ reason = answer.reason;
64
+ break;
65
+ }
66
+ const body = answer.body;
67
+ pages += 1;
68
+ for (const doc of body.documents ?? []) {
69
+ returned += 1;
70
+ if (seen.has(doc.id))
71
+ continue;
72
+ seen.add(doc.id);
73
+ documents.push(doc);
74
+ }
75
+ if (!body.next_cursor)
76
+ break;
77
+ if (pages >= WALK_MAX_PAGES) {
78
+ status = "capped";
79
+ reason = "page_ceiling";
80
+ break;
81
+ }
82
+ cursor = body.next_cursor;
83
+ }
84
+ const coverage = { status, pages, returned, unique: seen.size, ...(reason ? { reason } : {}) };
85
+ writeLine(door.io.stderr, `${TAG} list-all ${JSON.stringify(coverage)}`);
86
+ if (door.json)
87
+ return emitAgentDoor(door, { ok: true, documents, coverage });
88
+ for (const doc of documents) {
89
+ const size = typeof doc.body_chars === "number" ? `${doc.body_chars}c`.padStart(8) : " ?";
90
+ writeLine(door.io.stdout, `${doc.id} ${doc.visibility.padEnd(7)} ${size} ${doc.slug ?? "(no slug)"} ${doc.title}`);
91
+ }
92
+ writeLine(door.io.stdout, "");
93
+ writeLine(door.io.stdout, `${documents.length} document(s). ${coverageSentence(coverage)}`);
94
+ return 0;
95
+ }
96
+ export async function favoriteDocs(command, door) {
97
+ const on = command.action === "favorite";
98
+ const ref = command.docRef ?? "";
99
+ const resolved = await resolveDocId(door, ref);
100
+ if (resolved.status === "list_failed")
101
+ return failAgentDoor(door, TAG, resolved.reason, resolved.detail);
102
+ if (resolved.status === "not_found") {
103
+ return failAgentDoor(door, TAG, "document_not_found_or_unreadable", `That document does not exist, or you cannot read it: ${ref}`);
104
+ }
105
+ const answer = await askAgentDoor(door, {
106
+ path: `/api/docs/documents/${encodeURIComponent(resolved.id)}/favorite`,
107
+ method: on ? "PUT" : "DELETE",
108
+ label: on ? "docs favorite" : "docs unfavorite",
109
+ timeoutMs: WRITE_DEADLINE_MS,
110
+ });
111
+ if (!answer.ok)
112
+ return failAgentDoor(door, TAG, answer.reason, answer.detail);
113
+ writeLine(door.io.stderr, `${TAG} favorite ${JSON.stringify({ document_id: resolved.id, favorite: on })}`);
114
+ if (door.json)
115
+ return emitAgentDoor(door, { ok: true, document_id: resolved.id, favorite: on });
116
+ writeLine(door.io.stdout, on ? `Favorited ${resolved.id}.` : `Removed ${resolved.id} from your favorites.`);
117
+ return 0;
118
+ }
119
+ export async function marksDocs(door) {
120
+ const answer = await askAgentDoor(door, { path: "/api/docs/marks", method: "GET", label: "docs marks", timeoutMs: READ_DEADLINE_MS });
121
+ if (!answer.ok)
122
+ return failAgentDoor(door, TAG, answer.reason, answer.detail);
123
+ const body = answer.body;
124
+ const favorites = body.favorites ?? [];
125
+ const recents = body.recents ?? [];
126
+ if (door.json)
127
+ return emitAgentDoor(door, { ok: true, favorites, recents });
128
+ const print = (heading, rows) => {
129
+ writeLine(door.io.stdout, `${heading} (${rows.length})`);
130
+ for (const row of rows)
131
+ writeLine(door.io.stdout, ` ${row.id} ${row.marked_at} ${row.slug ?? "(no slug)"} ${row.title}`);
132
+ };
133
+ print("Favorites", favorites);
134
+ print("Recent", recents);
135
+ return 0;
136
+ }