@bli-cockpit/cli 0.2.242 → 0.2.244

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 (135) hide show
  1. package/dist/adapters/git-state.js +1 -0
  2. package/dist/adapters/raw-evidence-budget-admission.js +63 -0
  3. package/dist/adapters/raw-evidence-collection-state.js +6 -50
  4. package/dist/adapters/raw-evidence-git-diff.js +1 -0
  5. package/dist/agent-rules.js +8 -0
  6. package/dist/autostart-contract.js +12 -0
  7. package/dist/autostart-darwin-plist.js +2 -2
  8. package/dist/autostart-darwin.js +1 -1
  9. package/dist/autostart-remind.js +245 -0
  10. package/dist/autostart-self-heal.js +17 -1
  11. package/dist/autostart-windows-scripts.js +7 -7
  12. package/dist/autostart.js +16 -15
  13. package/dist/commands/approvals-check-where.js +14 -0
  14. package/dist/commands/approvals-render.js +8 -1
  15. package/dist/commands/approvals.js +3 -2
  16. package/dist/commands/autostart-command.js +10 -0
  17. package/dist/commands/autostart-heal.js +36 -1
  18. package/dist/commands/brain.js +1 -1
  19. package/dist/commands/cal-agenda.js +44 -0
  20. package/dist/commands/cal-availability-share.js +97 -0
  21. package/dist/commands/cal-booking.js +147 -0
  22. package/dist/commands/cal-join.js +62 -0
  23. package/dist/commands/cal-links.js +68 -0
  24. package/dist/commands/cal-people.js +136 -0
  25. package/dist/commands/cal-remind.js +176 -0
  26. package/dist/commands/cal-reminders.js +33 -0
  27. package/dist/commands/cal-suggest.js +89 -0
  28. package/dist/commands/cal.js +41 -1
  29. package/dist/commands/cycle-args.js +127 -0
  30. package/dist/commands/cycles.js +117 -0
  31. package/dist/commands/docs-comments-args.js +85 -0
  32. package/dist/commands/docs-comments.js +128 -0
  33. package/dist/commands/docs-files-args.js +49 -0
  34. package/dist/commands/docs-files.js +179 -0
  35. package/dist/commands/docs-formatting.js +45 -0
  36. package/dist/commands/docs-inbox-args.js +93 -0
  37. package/dist/commands/docs-inbox.js +126 -0
  38. package/dist/commands/docs-notion.js +5 -0
  39. package/dist/commands/docs-read.js +2 -1
  40. package/dist/commands/docs-recovery.js +173 -0
  41. package/dist/commands/docs-refs-args.js +104 -0
  42. package/dist/commands/docs-refs.js +153 -0
  43. package/dist/commands/docs-search-args.js +79 -0
  44. package/dist/commands/docs-search.js +64 -0
  45. package/dist/commands/docs-sync-args.js +82 -0
  46. package/dist/commands/docs-sync.js +197 -0
  47. package/dist/commands/docs-tree.js +136 -0
  48. package/dist/commands/docs.js +85 -51
  49. package/dist/commands/doctor-update.js +4 -1
  50. package/dist/commands/inbox-args.js +81 -0
  51. package/dist/commands/inbox.js +88 -0
  52. package/dist/commands/issue-batch.js +61 -0
  53. package/dist/commands/issue-relations.js +151 -0
  54. package/dist/commands/issue-write.js +38 -5
  55. package/dist/commands/issue.js +32 -1
  56. package/dist/commands/local-args-collector-sources.js +2 -1
  57. package/dist/commands/local-args-tower-approvals.js +16 -4
  58. package/dist/commands/local-args-tower-cal-booking.js +81 -0
  59. package/dist/commands/local-args-tower-cal-links.js +42 -0
  60. package/dist/commands/local-args-tower-cal-remind.js +74 -0
  61. package/dist/commands/local-args-tower-cal-share.js +41 -0
  62. package/dist/commands/local-args-tower-cal.js +81 -8
  63. package/dist/commands/local-args-tower-docs-msg.js +98 -9
  64. package/dist/commands/local-args-tower-msg.js +222 -10
  65. package/dist/commands/local-args-tower-search.js +32 -5
  66. package/dist/commands/local-args-tower-work.js +110 -34
  67. package/dist/commands/local-auth.js +1 -1
  68. package/dist/commands/local-help-commands-tower-ops.js +6 -4
  69. package/dist/commands/local-help-commands-tower.js +47 -31
  70. package/dist/commands/local-help-commands.js +3 -1
  71. package/dist/commands/local-help-cycles.js +9 -0
  72. package/dist/commands/local-help-docs-files.js +10 -0
  73. package/dist/commands/local-help-docs-formatting.js +7 -0
  74. package/dist/commands/local-help-docs-inbox.js +10 -0
  75. package/dist/commands/local-help-docs-recovery.js +10 -0
  76. package/dist/commands/local-help-docs-refs.js +11 -0
  77. package/dist/commands/local-help-docs-sync.js +9 -0
  78. package/dist/commands/local-help-inbox.js +9 -0
  79. package/dist/commands/local-help-project.js +21 -0
  80. package/dist/commands/local-help-templates.js +9 -0
  81. package/dist/commands/local-help-views.js +10 -0
  82. package/dist/commands/local-help-work-notify.js +6 -0
  83. package/dist/commands/local-help.js +3 -3
  84. package/dist/commands/manager-hints.js +4 -2
  85. package/dist/commands/manager-limits.json +3 -1
  86. package/dist/commands/manager-read-reasons.js +1 -0
  87. package/dist/commands/manager-render-home.js +13 -5
  88. package/dist/commands/manager-render.js +3 -0
  89. package/dist/commands/manager.js +3 -1
  90. package/dist/commands/mcp-stdio-probe.js +1 -0
  91. package/dist/commands/msg-activity.js +134 -0
  92. package/dist/commands/msg-channel-args.js +92 -0
  93. package/dist/commands/msg-channel.js +125 -0
  94. package/dist/commands/msg-context-args.js +67 -0
  95. package/dist/commands/msg-context.js +65 -0
  96. package/dist/commands/msg-dm.js +28 -0
  97. package/dist/commands/msg-file.js +185 -0
  98. package/dist/commands/msg-mark-args.js +78 -0
  99. package/dist/commands/msg-mark.js +148 -0
  100. package/dist/commands/msg-notify-args.js +100 -0
  101. package/dist/commands/msg-notify.js +122 -0
  102. package/dist/commands/msg-schedule-args.js +114 -0
  103. package/dist/commands/msg-schedule.js +226 -0
  104. package/dist/commands/msg-search-args.js +71 -0
  105. package/dist/commands/msg-search.js +48 -0
  106. package/dist/commands/msg-write.js +15 -1
  107. package/dist/commands/msg.js +62 -39
  108. package/dist/commands/project-args.js +82 -0
  109. package/dist/commands/project.js +71 -7
  110. package/dist/commands/public-root.js +1 -1
  111. package/dist/commands/search.js +23 -0
  112. package/dist/commands/session-sync-failures.js +17 -4
  113. package/dist/commands/session-sync-health.js +7 -2
  114. package/dist/commands/session-sync-hold.js +42 -1
  115. package/dist/commands/session-sync-outside-roots.js +72 -14
  116. package/dist/commands/session-sync-upload.js +2 -0
  117. package/dist/commands/session-sync.js +1 -0
  118. package/dist/commands/sources-check.js +63 -2
  119. package/dist/commands/sources-discovery.js +1 -1
  120. package/dist/commands/sources-reconciliation.js +87 -0
  121. package/dist/commands/sync-doctor.js +14 -1
  122. package/dist/commands/sync-report.js +1 -1
  123. package/dist/commands/template-args.js +122 -0
  124. package/dist/commands/templates.js +162 -0
  125. package/dist/commands/view-args.js +131 -0
  126. package/dist/commands/views.js +138 -0
  127. package/dist/commands/work-notify-args.js +67 -0
  128. package/dist/commands/work-notify.js +63 -0
  129. package/dist/delivery-hold-notice.js +34 -0
  130. package/dist/evidence-transcripts-client.js +1 -0
  131. package/dist/log-rotation.js +2 -2
  132. package/dist/notifier.js +83 -0
  133. package/dist/process-runner.js +4 -0
  134. package/dist/repo-identity-git.js +1 -0
  135. package/package.json +4 -4
@@ -0,0 +1,151 @@
1
+ import { randomUUID } from "node:crypto";
2
+ /**
3
+ * `cockpit ticket relations|relate|unrelate` (BLI-21558, P6): what blocks what, over the same doors the
4
+ * browser uses (`/api/work/issues/<id>/relations`). A child is not a blocker; the parent link is `--parent`.
5
+ *
6
+ * `relate <id> --blocks B | --blocked-by B | --related B | --duplicate-of B` takes exactly one kind flag.
7
+ * `unrelate <id> --relation <relation-id>` or the same kind flag plus the other ticket, found through the read.
8
+ * Parsing lives here too, so `local-args-tower-work.ts` only lists the flag names.
9
+ */
10
+ import { askAgentDoor, emitAgentDoor, failAgentDoor } from "./agent-door.js";
11
+ import { writeLine } from "./cli-io.js";
12
+ import { askTicketWrite, refuseWrite } from "./issue-write.js";
13
+ import { READ_DEADLINE_MS, TAG, WRITE_DEADLINE_MS } from "./issue-contracts.js";
14
+ export const RELATION_VALUE_FLAGS = ["--blocks", "--blocked-by", "--related", "--duplicate-of", "--relation"];
15
+ const KIND_FLAGS = [
16
+ ["--blocks", "blocks"], ["--blocked-by", "blocked_by"], ["--related", "related"], ["--duplicate-of", "duplicate"],
17
+ ];
18
+ const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
19
+ /** Reads the relation flags for one action; refuses a flag on the wrong verb, none, or several. */
20
+ export function readRelationFlags(action, values) {
21
+ const given = KIND_FLAGS.filter(([flag]) => values.flags.has(flag));
22
+ const relationId = values.flags.get("--relation");
23
+ if (action !== "relate" && action !== "unrelate") {
24
+ if (given.length > 0 || relationId !== undefined)
25
+ throw new Error("--blocks, --blocked-by, --related, --duplicate-of and --relation belong to `cockpit ticket relate` and `unrelate`.");
26
+ return {};
27
+ }
28
+ if (given.length > 1)
29
+ throw new Error("Name exactly one of --blocks, --blocked-by, --related or --duplicate-of.");
30
+ if (relationId !== undefined && (action === "relate" || given.length > 0)) {
31
+ throw new Error("--relation names the edge to remove on `unrelate`; use it instead of a kind flag.");
32
+ }
33
+ if (relationId !== undefined && !UUID.test(relationId))
34
+ throw new Error("--relation must be a relation id (see `cockpit ticket relations <id> --json`).");
35
+ const [flag, kind] = given[0] ?? [];
36
+ if (!flag || !kind) {
37
+ if (action === "relate")
38
+ throw new Error("ticket relate needs one of --blocks <id>, --blocked-by <id>, --related <id> or --duplicate-of <id>.");
39
+ if (relationId === undefined)
40
+ throw new Error("ticket unrelate needs --relation <relation-id>, or a kind flag with the other ticket.");
41
+ return { relationId };
42
+ }
43
+ return { relation: { kind, target: values.flags.get(flag) ?? "" } };
44
+ }
45
+ const GROUPS = [
46
+ ["blocks", "Blocks"], ["blocked_by", "Blocked by"], ["related", "Related"], ["duplicate_of", "Duplicate of"], ["duplicates", "Duplicates"],
47
+ ];
48
+ export async function showRelations(command, door) {
49
+ const ref = command.issueRef ?? "";
50
+ const answer = await askAgentDoor(door, {
51
+ path: `/api/work/issues/${encodeURIComponent(ref)}/relations`, method: "GET", label: "issue relations", timeoutMs: READ_DEADLINE_MS,
52
+ });
53
+ if (!answer.ok)
54
+ return failAgentDoor(door, TAG, answer.reason, answer.detail);
55
+ const relations = answer.body.relations;
56
+ if (!relations)
57
+ return failAgentDoor(door, TAG, "read_failed", "The door answered without relations.");
58
+ console.error(`${TAG} relations ${JSON.stringify(relations.coverage)}`);
59
+ if (door.json)
60
+ return emitAgentDoor(door, { ok: true, relations });
61
+ let printed = 0;
62
+ for (const [key, title] of GROUPS) {
63
+ const items = relations[key];
64
+ if (items.length === 0)
65
+ continue;
66
+ writeLine(door.io.stdout, `${title}:`);
67
+ for (const item of items)
68
+ writeLine(door.io.stdout, ` ${item.issue.identifier.padEnd(9)} ${item.issue.state.padEnd(11)} ${item.issue.title} (${item.relation_id})`);
69
+ printed += items.length;
70
+ }
71
+ if (relations.unmapped.length > 0) {
72
+ writeLine(door.io.stdout, "Not in Tower (from Linear):");
73
+ for (const item of relations.unmapped)
74
+ writeLine(door.io.stdout, ` ${item.kind} ${item.label} (${item.relation_id})`);
75
+ printed += relations.unmapped.length;
76
+ }
77
+ if (printed === 0)
78
+ writeLine(door.io.stdout, `${ref} has no relations.`);
79
+ const { hidden, truncated, limit } = relations.coverage;
80
+ if (hidden > 0)
81
+ writeLine(door.io.stdout, `${hidden} relation(s) to tickets you cannot read or that were deleted are not shown.`);
82
+ if (truncated)
83
+ writeLine(door.io.stdout, `Showing the first ${limit} relations; there are more.`);
84
+ return 0;
85
+ }
86
+ export async function relateIssue(command, door) {
87
+ const ref = command.issueRef ?? "";
88
+ const relation = command.relation;
89
+ if (!relation)
90
+ return failAgentDoor(door, TAG, "invalid_body", "ticket relate needs a kind flag and the other ticket.");
91
+ const requestId = command.requestId ?? randomUUID();
92
+ writeLine(door.io.stderr, `${TAG} request ${JSON.stringify({ request_id: requestId, expected_revision: command.expectedRevision })}`);
93
+ const answer = await askTicketWrite(door, {
94
+ path: `/api/work/issues/${encodeURIComponent(ref)}/relations`, method: "POST", label: "issue relate", timeoutMs: WRITE_DEADLINE_MS,
95
+ body: { request_id: requestId, expected_revision: command.expectedRevision, kind: relation.kind, target: relation.target },
96
+ });
97
+ if (!answer.ok)
98
+ return refuseWrite(door, answer, requestId);
99
+ return finishWrite(door, answer.body, requestId, `${ref} ${relation.kind.replace("_", " ")} ${relation.target}: recorded.`, "related");
100
+ }
101
+ export async function unrelateIssue(command, door) {
102
+ const ref = command.issueRef ?? "";
103
+ let relationId = command.relationId;
104
+ if (!relationId && command.relation) {
105
+ const found = await findRelation(command, door);
106
+ if (typeof found === "number")
107
+ return found;
108
+ relationId = found;
109
+ }
110
+ const requestId = command.requestId ?? randomUUID();
111
+ writeLine(door.io.stderr, `${TAG} request ${JSON.stringify({ request_id: requestId, expected_revision: command.expectedRevision })}`);
112
+ const query = command.expectedRevision === undefined ? `?request_id=${requestId}` : `?request_id=${requestId}&expected_revision=${command.expectedRevision}`;
113
+ const answer = await askTicketWrite(door, {
114
+ path: `/api/work/issues/${encodeURIComponent(ref)}/relations/${encodeURIComponent(relationId ?? "")}${query}`,
115
+ method: "DELETE", label: "issue unrelate", timeoutMs: WRITE_DEADLINE_MS, body: { request_id: requestId },
116
+ });
117
+ if (!answer.ok)
118
+ return refuseWrite(door, answer, requestId);
119
+ return finishWrite(door, answer.body, requestId, `${ref}: relation removed.`, "unrelated");
120
+ }
121
+ function finishWrite(door, body, requestId, sentence, tag) {
122
+ writeLine(door.io.stderr, `${TAG} ${tag} ${JSON.stringify({ relation_id: body.relation_id ?? null })}`);
123
+ writeLine(door.io.stderr, `${TAG} local commit receipt ${JSON.stringify(body.mutation ?? { outcome: "unknown_check_again", request_id: requestId })}`);
124
+ if (door.json)
125
+ return emitAgentDoor(door, { ok: true, issue: body.issue, relation_id: body.relation_id, mutation: body.mutation });
126
+ writeLine(door.io.stdout, sentence);
127
+ return 0;
128
+ }
129
+ /** `unrelate <id> --blocks B`: the edge id, found through the same read `relations` prints. */
130
+ async function findRelation(command, door) {
131
+ const ref = command.issueRef ?? "";
132
+ const wanted = command.relation;
133
+ if (!wanted)
134
+ return failAgentDoor(door, TAG, "invalid_body", "ticket unrelate needs --relation or a kind flag.");
135
+ const answer = await askAgentDoor(door, {
136
+ path: `/api/work/issues/${encodeURIComponent(ref)}/relations`, method: "GET", label: "issue relations", timeoutMs: READ_DEADLINE_MS,
137
+ });
138
+ if (!answer.ok)
139
+ return failAgentDoor(door, TAG, answer.reason, answer.detail);
140
+ const relations = answer.body.relations;
141
+ const group = wanted.kind === "blocks" ? relations?.blocks : wanted.kind === "blocked_by" ? relations?.blocked_by
142
+ : wanted.kind === "related" ? relations?.related : relations?.duplicate_of;
143
+ const target = wanted.target.trim().toUpperCase();
144
+ const hit = (group ?? []).find((item) => item.issue.identifier.toUpperCase() === target || item.issue.id.toLowerCase() === target.toLowerCase())
145
+ ?? (relations?.unmapped ?? []).find((item) => item.label.toUpperCase() === target && item.kind === (wanted.kind === "blocked_by" ? "" : wanted.kind));
146
+ if (!hit) {
147
+ console.error(`${TAG} unrelate refused ${JSON.stringify({ reason: "relation_not_found", kind: wanted.kind })}`);
148
+ return failAgentDoor(door, TAG, "relation_not_found", `${ref} has no ${wanted.kind.replace("_", " ")} relation with ${wanted.target}.`);
149
+ }
150
+ return hit.relation_id;
151
+ }
@@ -35,6 +35,9 @@ export async function createIssue(command, door) {
35
35
  ...(command.priority === undefined ? {} : { priority: command.priority }),
36
36
  ...(command.estimate === undefined ? {} : { estimate: command.estimate }),
37
37
  ...(command.assignee ? { assignee_id: command.assignee } : {}),
38
+ ...(command.startState ? { state: command.startState } : {}),
39
+ ...(command.due ? { due_date: command.due } : {}),
40
+ ...(command.cycle ? { cycle: command.cycle } : {}),
38
41
  },
39
42
  });
40
43
  if (!answer.ok)
@@ -60,7 +63,7 @@ export async function updateIssue(command, door) {
60
63
  description = body.text;
61
64
  }
62
65
  if (!changesAnything(command, description, refs)) {
63
- return failAgentDoor(door, TAG, "invalid_body", "issue update needs at least one of --title, --priority, --size, --assignee, --project, --parent, or a description on --body-stdin/--file.");
66
+ return failAgentDoor(door, TAG, "invalid_body", "issue update needs at least one of --title, --priority, --size, --assignee, --due, --project, --parent, --cycle, or a description on --body-stdin/--file.");
64
67
  }
65
68
  const requestId = command.requestId ?? randomUUID();
66
69
  writeLine(door.io.stderr, `${TAG} request ${JSON.stringify({ request_id: requestId, expected_revision: command.expectedRevision })}`);
@@ -74,9 +77,11 @@ export async function updateIssue(command, door) {
74
77
  ...(description !== undefined ? { description } : {}),
75
78
  ...(command.priority === undefined ? {} : { priority: command.priority }),
76
79
  ...(command.estimate === undefined ? {} : { estimate: command.estimate }),
77
- ...(command.assignee ? { assignee_id: command.assignee } : {}),
80
+ ...(command.assignee ? { assignee_id: command.assignee === "none" ? null : command.assignee } : {}),
81
+ ...(command.due !== undefined ? { due_date: command.due } : {}),
78
82
  ...(refs.projectId ? { project_id: refs.projectId } : {}),
79
83
  ...(refs.parentId ? { parent_id: refs.parentId } : {}),
84
+ ...(command.cycle === undefined ? {} : { cycle: command.cycle }),
80
85
  },
81
86
  });
82
87
  if (!answer.ok)
@@ -137,8 +142,34 @@ export async function commentIssue(command, door) {
137
142
  writeLine(door.io.stdout, `Commented on ${ref} (${comment?.id ?? ""}).`);
138
143
  return 0;
139
144
  }
145
+ /** `ticket label <id> --add <name> --remove <name>`: one receipt, one history row (BLI-21422). */
146
+ export async function labelIssue(command, door) {
147
+ const ref = command.issueRef ?? "";
148
+ const requestId = command.requestId ?? randomUUID();
149
+ writeLine(door.io.stderr, `${TAG} request ${JSON.stringify({ request_id: requestId, expected_revision: command.expectedRevision })}`);
150
+ const answer = await askTicketWrite(door, {
151
+ path: `/api/work/issues/${encodeURIComponent(ref)}/labels`,
152
+ method: "POST",
153
+ label: "issue label",
154
+ timeoutMs: WRITE_DEADLINE_MS,
155
+ body: { request_id: requestId, expected_revision: command.expectedRevision,
156
+ ...(command.addLabels?.length ? { add: command.addLabels } : {}),
157
+ ...(command.removeLabels?.length ? { remove: command.removeLabels } : {}),
158
+ },
159
+ });
160
+ if (!answer.ok)
161
+ return refuseWrite(door, answer, requestId);
162
+ const body = answer.body;
163
+ const names = (body.labels ?? []).map((l) => l.name);
164
+ writeLine(door.io.stderr, `${TAG} labelled ${JSON.stringify({ issue_id: body.issue?.id ?? null, added: command.addLabels?.length ?? 0, removed: command.removeLabels?.length ?? 0, now: names.length })}`);
165
+ writeLine(door.io.stderr, `${TAG} local commit receipt ${JSON.stringify(answer.body.mutation ?? { outcome: "unknown_check_again", request_id: requestId })}`);
166
+ if (door.json)
167
+ return emitAgentDoor(door, { ok: true, issue: maybeWithSizeLabel(body.issue), labels: body.labels ?? [], mutation: answer.body.mutation });
168
+ writeLine(door.io.stdout, `${body.issue?.identifier ?? ref} labels: ${names.length ? names.join(", ") : "none"}.`);
169
+ return 0;
170
+ }
140
171
  /** A truncated or missing response body cannot be called a successful ticket write. */
141
- async function askTicketWrite(door, options) {
172
+ export async function askTicketWrite(door, options) {
142
173
  try {
143
174
  const answer = await askAgentDoor(door, options);
144
175
  if (!answer.ok)
@@ -161,11 +192,13 @@ function changesAnything(command, description, refs) {
161
192
  || command.priority !== undefined
162
193
  || command.estimate !== undefined
163
194
  || command.assignee !== undefined
195
+ || command.due !== undefined
164
196
  || refs.projectId !== undefined
165
- || refs.parentId !== undefined;
197
+ || refs.parentId !== undefined
198
+ || command.cycle !== undefined;
166
199
  }
167
200
  /** Says where the local commit stands, then fails with the door's own reason. */
168
- function refuseWrite(door, answer, requestId) {
201
+ export function refuseWrite(door, answer, requestId) {
169
202
  writeLine(door.io.stderr, `${TAG} check local commit with cockpit ticket mutation-status ${requestId}; retry unchanged input with --request-id ${requestId}.`);
170
203
  const mutation = answer.body?.mutation ?? { request_id: requestId, outcome: "unknown_check_again" };
171
204
  if (door.json) {
@@ -33,7 +33,14 @@ import { readTicketBoard } from "./tickets-board.js";
33
33
  import { showIssue } from "./issue-show.js";
34
34
  import { addIssuePageQuery, pageFieldsOf, pageFooter, streamAllIssues } from "./issue-list-page.js";
35
35
  import { READ_DEADLINE_MS, TAG, resolveProjectId, sizeForEstimate, withSizeLabel, } from "./issue-contracts.js";
36
- import { commentIssue, createIssue, moveIssue, updateIssue } from "./issue-write.js";
36
+ import { commentIssue, createIssue, labelIssue, moveIssue, updateIssue } from "./issue-write.js";
37
+ import { batchIssues } from "./issue-batch.js";
38
+ import { runInbox } from "./inbox.js";
39
+ import { runWorkNotify } from "./work-notify.js";
40
+ import { runViews } from "./views.js";
41
+ import { runTemplates } from "./templates.js";
42
+ import { runCycles } from "./cycles.js";
43
+ import { relateIssue, showRelations, unrelateIssue } from "./issue-relations.js";
37
44
  import { listIssueVersions, restoreIssueVersion } from "./issue-versions.js";
38
45
  export async function runIssue(command, io) {
39
46
  const door = await openAgentDoor("issue", command, io);
@@ -62,12 +69,36 @@ export async function runIssue(command, io) {
62
69
  return moveIssue(command, door);
63
70
  case "comment":
64
71
  return commentIssue(command, door);
72
+ case "label":
73
+ return labelIssue(command, door);
74
+ case "relations":
75
+ return showRelations(command, door);
76
+ case "relate":
77
+ return relateIssue(command, door);
78
+ case "unrelate":
79
+ return unrelateIssue(command, door);
65
80
  case "history":
66
81
  return issueHistory(command, door);
67
82
  case "versions":
68
83
  return listIssueVersions(command, door);
69
84
  case "restore":
70
85
  return restoreIssueVersion(command, door);
86
+ case "batch":
87
+ return batchIssues(command, door);
88
+ case "views":
89
+ return runViews(command, io, door);
90
+ case "templates":
91
+ return runTemplates(command, io, door);
92
+ case "cycles":
93
+ return runCycles(command, io, door);
94
+ case "inbox":
95
+ case "read":
96
+ case "unread":
97
+ case "subscribe":
98
+ case "unsubscribe":
99
+ return runInbox(command, io, door);
100
+ case "notifications":
101
+ return runWorkNotify(command, io, door);
71
102
  }
72
103
  }
73
104
  function issueLine(issue, directory) {
@@ -9,7 +9,7 @@
9
9
  import { optionalNonEmpty, optionalPositiveInteger, optionalUrl, parseNamedArgs, } from "./local-arg-values.js";
10
10
  export function parseSourcesArgs(args) {
11
11
  const values = parseNamedArgs(args, {
12
- allowedFlags: ["--older-than", "--home", "--dashboard-url", "--json"],
12
+ allowedFlags: ["--older-than", "--home", "--dashboard-url", "--json", "--reconcile"],
13
13
  valueFlags: ["--older-than", "--home", "--dashboard-url"],
14
14
  });
15
15
  const action = values.positionals[0] ?? "check";
@@ -17,6 +17,7 @@ export function parseSourcesArgs(args) {
17
17
  throw new Error("sources has one verb: `cockpit sources check [--older-than <days>] [--json]`.");
18
18
  }
19
19
  return {
20
+ reconcile: values.booleans.has("--reconcile"),
20
21
  kind: "sources",
21
22
  action: "check",
22
23
  olderThanDays: optionalPositiveInteger(values.flags.get("--older-than"), "--older-than") ?? null,
@@ -4,7 +4,7 @@
4
4
  * cockpit approvals [list] [--status open|done|pending|executing|executed|failed|rejected] [--limit <n>] [--before <cursor>] [--json]
5
5
  * cockpit approvals show <id>
6
6
  * cockpit approvals create (the card as JSON on stdin)
7
- * cockpit approvals approve <id> [--edit] (--edit: the edit as JSON on stdin)
7
+ * cockpit approvals approve <id> [--edit] [--checked-sent sent|not-sent] (--edit: the edit as JSON on stdin)
8
8
  * cockpit approvals edit <id> [--approve] (the edit as JSON on stdin)
9
9
  * cockpit approvals reject <id>
10
10
  * cockpit approvals rules [--active true|false] [--limit <n>]
@@ -25,19 +25,19 @@ const FLAGS_BY_ACTION = {
25
25
  list: ["--status", "--limit", "--before"],
26
26
  show: [],
27
27
  create: [],
28
- approve: ["--edit"],
28
+ approve: ["--edit", "--checked-sent"],
29
29
  edit: ["--approve"],
30
30
  reject: [],
31
31
  rules: ["--active", "--limit"],
32
32
  "rules-create": ["--any-audience", "--label"],
33
33
  "rules-revoke": ["--reason"],
34
34
  };
35
- const OWN_FLAGS = ["--status", "--limit", "--before", "--edit", "--approve", "--active", "--any-audience", "--label", "--reason"];
35
+ const OWN_FLAGS = ["--status", "--limit", "--before", "--edit", "--checked-sent", "--approve", "--active", "--any-audience", "--label", "--reason"];
36
36
  const COMMON_FLAGS = ["--home", "--dashboard-url", "--json"];
37
37
  export function parseApprovalsArgs(args) {
38
38
  const values = parseNamedArgs(args, {
39
39
  allowedFlags: [...OWN_FLAGS, ...COMMON_FLAGS],
40
- valueFlags: ["--status", "--limit", "--before", "--active", "--label", "--reason", "--home", "--dashboard-url"],
40
+ valueFlags: ["--status", "--limit", "--before", "--checked-sent", "--active", "--label", "--reason", "--home", "--dashboard-url"],
41
41
  });
42
42
  const { action, words } = readAction(values.positionals);
43
43
  const given = [...values.flags.keys(), ...values.booleans].filter(flag => OWN_FLAGS.includes(flag));
@@ -60,6 +60,7 @@ export function parseApprovalsArgs(args) {
60
60
  if (limit !== undefined && limit > LIST_MAX_LIMIT)
61
61
  throw new Error(`--limit is at most ${LIST_MAX_LIMIT}.`);
62
62
  const active = readActive(values.flags.get("--active"));
63
+ const deliveryChecked = readCheckedSent(values.flags.get("--checked-sent"));
63
64
  return {
64
65
  kind: "approvals",
65
66
  action,
@@ -69,6 +70,7 @@ export function parseApprovalsArgs(args) {
69
70
  ...(optionalNonEmpty(values.flags.get("--before")) ? { before: optionalNonEmpty(values.flags.get("--before")) } : {}),
70
71
  ...(values.booleans.has("--edit") ? { withEdit: true } : {}),
71
72
  ...(values.booleans.has("--approve") ? { thenApprove: true } : {}),
73
+ ...(deliveryChecked ? { deliveryChecked } : {}),
72
74
  ...(active !== undefined ? { active } : {}),
73
75
  ...(values.booleans.has("--any-audience") ? { anyAudience: true } : {}),
74
76
  ...(optionalNonEmpty(values.flags.get("--label")) ? { label: optionalNonEmpty(values.flags.get("--label")) } : {}),
@@ -93,6 +95,16 @@ function readAction(positionals) {
93
95
  return { action: first, words: first };
94
96
  throw new Error(ONE_VERB);
95
97
  }
98
+ /** BLI-21954: what the person found where it was going (Sent, or the channel), on a card whose delivery is unknown. Typed with a hyphen, sent as the door's word. */
99
+ function readCheckedSent(raw) {
100
+ if (raw === undefined)
101
+ return undefined;
102
+ if (raw === "sent")
103
+ return "sent";
104
+ if (raw === "not-sent" || raw === "not_sent")
105
+ return "not_sent";
106
+ throw new Error("--checked-sent wants sent (it went out: Sent for an email, the channel for a message; mark it done) or not-sent (it did not: send or post it now).");
107
+ }
96
108
  function readActive(raw) {
97
109
  if (raw === undefined)
98
110
  return undefined;
@@ -0,0 +1,81 @@
1
+ /**
2
+ * BLI-21556 `cockpit cal booking-page|booking-slots|book|booking-change|booking-cancel`: the flags, read into a plain
3
+ * shape. Shape only; the door owns every limit (ranges, duration, buffers, notice, window, the guest cap).
4
+ */
5
+ import { optionalNonEmpty } from "./local-arg-values.js";
6
+ export const BOOKING_ACTIONS = ["booking-page", "booking-slots", "book", "booking-change", "booking-cancel"];
7
+ /** The guest verbs: they take a page link or token (or a management link or token) and need no device token. */
8
+ export const BOOKING_GUEST_ACTIONS = ["booking-slots", "book", "booking-change", "booking-cancel"];
9
+ export const BOOKING_BOOLEANS = ["--rotate", "--close", "--open"];
10
+ export const BOOKING_VALUE_FLAGS = ["--weekly", "--buffer-before", "--buffer-after", "--notice", "--window", "--start", "--email", "--note"];
11
+ const OWN_FLAGS = [...BOOKING_BOOLEANS, ...BOOKING_VALUE_FLAGS];
12
+ const DAYS = ["mon", "tue", "wed", "thu", "fri", "sat", "sun"];
13
+ /** `mon/09:00-12:00`, `mon,wed/13:00-17:00` or `1/09:00-12:00` -> weekly ranges, or null when it is not that shape. */
14
+ export function parseWeekly(raw) {
15
+ const match = /^([A-Za-z0-9,]+)\/(\d{2}:\d{2})-(\d{2}:\d{2})$/.exec(raw.trim());
16
+ if (!match)
17
+ return null;
18
+ const days = match[1].split(",").map((day) => {
19
+ const lower = day.toLowerCase();
20
+ const named = DAYS.findIndex((name) => lower === name || lower === name + "day" || lower.startsWith(name));
21
+ return named >= 0 ? named + 1 : Number(day);
22
+ });
23
+ if (days.some((day) => !Number.isInteger(day) || day < 1 || day > 7))
24
+ return null;
25
+ return days.map((weekday) => ({ weekday, start: match[2], end: match[3] }));
26
+ }
27
+ function wholeNumber(values, flag) {
28
+ const raw = optionalNonEmpty(values.flags.get(flag));
29
+ if (raw === undefined)
30
+ return undefined;
31
+ if (!/^\d+$/.test(raw))
32
+ throw new Error(`${flag} takes a whole number.`);
33
+ return Number(raw);
34
+ }
35
+ export function readBookingFlags(action, values) {
36
+ if (!BOOKING_ACTIONS.includes(action)) {
37
+ for (const flag of OWN_FLAGS)
38
+ if (values.flags.has(flag) || values.booleans.has(flag))
39
+ throw new Error(`cal ${action} does not take ${flag}; it belongs to the booking verbs.`);
40
+ return {};
41
+ }
42
+ const weekly = (values.flagValues.get("--weekly") ?? []).map((raw) => raw.trim()).filter(Boolean);
43
+ for (const raw of weekly)
44
+ if (!parseWeekly(raw))
45
+ throw new Error("--weekly takes <day>/<HH:MM>-<HH:MM>, like mon/09:00-12:00 or mon,wed/13:00-17:00.");
46
+ const start = optionalNonEmpty(values.flags.get("--start"));
47
+ const email = optionalNonEmpty(values.flags.get("--email"));
48
+ if (action === "book" && (!start || !optionalNonEmpty(values.flags.get("--name")) || !email))
49
+ throw new Error("cal book needs --start <slot start>, --name \"<you>\" and --email <you@x.com>.");
50
+ if (action === "booking-change" && !start)
51
+ throw new Error("cal booking-change needs --start <new slot start>.");
52
+ if (action !== "booking-page") {
53
+ for (const flag of ["--weekly", "--buffer-before", "--buffer-after", "--notice", "--window", "--rotate", "--close", "--open"])
54
+ if (values.flags.has(flag) || values.booleans.has(flag))
55
+ throw new Error(`cal ${action} does not take ${flag}; it belongs to cal booking-page.`);
56
+ return { ...(start ? { bookingStart: start } : {}), ...(email ? { guestEmail: email } : {}), ...(optionalNonEmpty(values.flags.get("--note")) ? { guestNote: optionalNonEmpty(values.flags.get("--note")) } : {}) };
57
+ }
58
+ if (values.booleans.has("--close") && values.booleans.has("--open"))
59
+ throw new Error("cal booking-page takes --close or --open, not both.");
60
+ const id = optionalNonEmpty(values.flags.get("--id"));
61
+ const list = values.booleans.has("--list");
62
+ if (list && id)
63
+ throw new Error("cal booking-page takes --list or --id, not both.");
64
+ const edits = values.booleans.has("--rotate") || values.booleans.has("--close") || values.booleans.has("--open");
65
+ if (edits && !id)
66
+ throw new Error("--rotate, --close and --open change an existing page: add --id <page id> (from --list).");
67
+ const numbers = {
68
+ bufferBefore: wholeNumber(values, "--buffer-before"), bufferAfter: wholeNumber(values, "--buffer-after"),
69
+ minNotice: wholeNumber(values, "--notice"), windowDays: wholeNumber(values, "--window"),
70
+ };
71
+ return {
72
+ bookingMode: list ? "list" : id ? "change" : "create",
73
+ ...(id ? { bookingId: id } : {}),
74
+ ...(weekly.length ? { bookingWeekly: weekly } : {}),
75
+ ...Object.fromEntries(Object.entries(numbers).filter(([, value]) => value !== undefined)),
76
+ ...(values.booleans.has("--rotate") ? { rotate: true } : {}),
77
+ ...(values.booleans.has("--close") ? { bookingClosed: true } : values.booleans.has("--open") ? { bookingClosed: false } : {}),
78
+ ...(values.booleans.has("--single-use") ? { singleUse: true } : {}),
79
+ ...(optionalNonEmpty(values.flags.get("--duration")) ? { duration: wholeNumber(values, "--duration") } : {}),
80
+ };
81
+ }
@@ -0,0 +1,42 @@
1
+ /**
2
+ * BLI-21588 `cockpit cal links <event id>` and `cockpit cal agenda`: the flags, read into a plain shape. Shape only;
3
+ * the doors own the limits (20 links per event, the occurrence rules, who may read the target).
4
+ */
5
+ import { optionalNonEmpty } from "./local-arg-values.js";
6
+ export const LINKS_ACTIONS = ["links", "agenda"];
7
+ /** `links <event id>` names one event first. */
8
+ export const LINKS_SUBJECT_ACTIONS = ["links"];
9
+ export const LINKS_BOOLEANS = ["--series"];
10
+ export const LINKS_VALUE_FLAGS = ["--add"];
11
+ const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
12
+ /** `doc:<uuid>` or `note:<uuid>`; anything else is named, never guessed. */
13
+ export function readLinkTarget(raw) {
14
+ const [kind, id, ...extra] = raw.split(":");
15
+ if ((kind !== "doc" && kind !== "note") || !id || extra.length > 0 || !UUID.test(id)) {
16
+ throw new Error("--add takes doc:<id> or note:<id>, where <id> is the document's or note's uuid.");
17
+ }
18
+ return { kind, id: id.toLowerCase() };
19
+ }
20
+ export function readLinksFlags(action, values) {
21
+ const add = optionalNonEmpty(values.flags.get("--add"));
22
+ const remove = action === "links" ? optionalNonEmpty(values.flags.get("--remove")) : undefined;
23
+ const series = values.booleans.has("--series");
24
+ if (action !== "links") {
25
+ for (const flag of ["--add", "--series"]) {
26
+ if (values.flags.has(flag) || values.booleans.has(flag))
27
+ throw new Error(`cal ${action} does not take ${flag}; it belongs to cal links.`);
28
+ }
29
+ return {};
30
+ }
31
+ if (add && remove)
32
+ throw new Error("cal links takes --add or --remove, not both.");
33
+ if (series && !add)
34
+ throw new Error("--series goes with --add: it links the whole series instead of one occurrence.");
35
+ if (add && !series && !optionalNonEmpty(values.flags.get("--occurrence") ?? values.flags.get("--occurrence-start"))) {
36
+ throw new Error("cal links --add needs --occurrence <the occurrence's ISO start> or --series.");
37
+ }
38
+ return {
39
+ ...(add ? { linkAdd: readLinkTarget(add), ...(series ? { linkSeries: true } : {}) } : {}),
40
+ ...(remove ? { linkRemove: remove } : {}),
41
+ };
42
+ }
@@ -0,0 +1,74 @@
1
+ /**
2
+ * BLI-21564 `cockpit cal remind|join|reminders|snooze|dismiss`: the flags, read into a plain shape. Shape only; the
3
+ * doors own every limit (lead 0 to 60, snooze 5/10/15, the occurrence rules).
4
+ */
5
+ import { optionalNonEmpty } from "./local-arg-values.js";
6
+ export const REMIND_ACTIONS = ["remind", "join", "reminders", "snooze", "dismiss"];
7
+ /** Verbs that name one event first: `snooze <event id> --occurrence <iso> --minutes 10`. */
8
+ export const REMIND_SUBJECT_ACTIONS = ["snooze", "dismiss"];
9
+ export const REMIND_BOOLEANS = ["--on", "--off", "--print"];
10
+ export const REMIND_VALUE_FLAGS = ["--event", "--lead", "--sound", "--minutes"];
11
+ const OWN_FLAGS = [...REMIND_BOOLEANS, ...REMIND_VALUE_FLAGS];
12
+ function wholeNumber(values, flag) {
13
+ const raw = optionalNonEmpty(values.flags.get(flag));
14
+ if (raw === undefined)
15
+ return undefined;
16
+ if (!/^\d+$/.test(raw))
17
+ throw new Error(`${flag} takes a whole number of minutes.`);
18
+ return Number(raw);
19
+ }
20
+ export function readRemindFlags(action, values) {
21
+ const owns = (flag) => {
22
+ if (flag === "--print" || flag === "--event")
23
+ return action === "join";
24
+ if (flag === "--minutes")
25
+ return action === "snooze";
26
+ return action === "reminders";
27
+ };
28
+ for (const flag of OWN_FLAGS) {
29
+ if ((values.flags.has(flag) || values.booleans.has(flag)) && !owns(flag)) {
30
+ throw new Error(`cal ${action} does not take ${flag}; it belongs to the cal remind verbs (remind, join, reminders, snooze, dismiss).`);
31
+ }
32
+ }
33
+ const occurrence = optionalNonEmpty(values.flags.get("--occurrence") ?? values.flags.get("--occurrence-start"));
34
+ if (action === "snooze" || action === "dismiss") {
35
+ if (!occurrence)
36
+ throw new Error(`cal ${action} needs --occurrence <the occurrence's ISO start>, as shown by cal next.`);
37
+ if (action === "dismiss")
38
+ return {};
39
+ const minutes = wholeNumber(values, "--minutes");
40
+ if (minutes === undefined || ![5, 10, 15].includes(minutes))
41
+ throw new Error("cal snooze needs --minutes 5, 10 or 15.");
42
+ return { snoozeMinutes: minutes };
43
+ }
44
+ if (action === "join") {
45
+ const event = optionalNonEmpty(values.flags.get("--event"));
46
+ if (event && !occurrence)
47
+ throw new Error("cal join --event needs --occurrence <the occurrence's ISO start>.");
48
+ if (!event && occurrence)
49
+ throw new Error("cal join --occurrence needs --event <event id>.");
50
+ return { ...(event ? { remindEvent: event } : {}), ...(values.booleans.has("--print") ? { printOnly: true } : {}) };
51
+ }
52
+ if (action !== "reminders")
53
+ return {};
54
+ if (values.booleans.has("--on") && values.booleans.has("--off"))
55
+ throw new Error("cal reminders takes --on or --off, not both.");
56
+ const patch = {};
57
+ if (values.booleans.has("--on"))
58
+ patch.enabled = true;
59
+ if (values.booleans.has("--off"))
60
+ patch.enabled = false;
61
+ const lead = wholeNumber(values, "--lead");
62
+ if (lead !== undefined) {
63
+ if (lead > 60)
64
+ throw new Error("--lead takes 0 to 60 minutes.");
65
+ patch.lead_minutes = lead;
66
+ }
67
+ const sound = optionalNonEmpty(values.flags.get("--sound"));
68
+ if (sound !== undefined) {
69
+ if (sound !== "on" && sound !== "off")
70
+ throw new Error("--sound takes on or off.");
71
+ patch.sound = sound === "on";
72
+ }
73
+ return Object.keys(patch).length ? { reminderPatch: patch } : {};
74
+ }
@@ -0,0 +1,41 @@
1
+ /**
2
+ * BLI-21548 `cockpit cal availability-share|availability-revoke`: the flags, read into a plain shape.
3
+ * Shape only; the door owns every limit (ranges, duration, expiry).
4
+ */
5
+ import { optionalNonEmpty } from "./local-arg-values.js";
6
+ export const SHARE_ACTIONS = ["availability-share", "availability-revoke"];
7
+ export const SHARE_BOOLEANS = ["--list", "--text", "--link", "--single-use", "--allow-conflicts"];
8
+ export const SHARE_VALUE_FLAGS = ["--range", "--expires", "--label", "--id"];
9
+ const SHARE_FLAGS = [...SHARE_BOOLEANS, ...SHARE_VALUE_FLAGS];
10
+ export function readShareFlags(action, values) {
11
+ if (action !== "availability-share") {
12
+ for (const flag of SHARE_FLAGS) {
13
+ if (action === "booking-page" && ["--list", "--id", "--single-use"].includes(flag))
14
+ continue;
15
+ if (values.flags.has(flag) || values.booleans.has(flag))
16
+ throw new Error(`cal ${action} does not take ${flag}; it belongs to cal availability-share.`);
17
+ }
18
+ return {};
19
+ }
20
+ const ranges = (values.flagValues.get("--range") ?? []).map((range) => range.trim()).filter(Boolean);
21
+ for (const range of ranges)
22
+ if (range.split("/").length !== 2 || range.split("/").some((end) => !end.trim()))
23
+ throw new Error("--range takes <start>/<end>, like 2026-10-20T10:00/2026-10-20T17:00 (times without an offset are read in --tz).");
24
+ const modes = ["--list", "--text", "--link"].filter((flag) => values.booleans.has(flag));
25
+ if (modes.length > 1)
26
+ throw new Error(`cal availability-share takes one of --list, --text or --link, not ${modes.join(" and ")}.`);
27
+ const id = optionalNonEmpty(values.flags.get("--id"));
28
+ const duration = optionalNonEmpty(values.flags.get("--duration"));
29
+ if (duration !== undefined && !/^\d+$/.test(duration))
30
+ throw new Error("--duration takes a whole number of minutes.");
31
+ return {
32
+ ...(ranges.length ? { shareRanges: ranges } : {}),
33
+ shareMode: values.booleans.has("--list") ? "list" : id ? "change" : values.booleans.has("--link") ? "link" : "text",
34
+ ...(id ? { shareId: id } : {}),
35
+ ...(duration === undefined ? {} : { duration: Number(duration) }),
36
+ ...(optionalNonEmpty(values.flags.get("--expires")) ? { shareExpires: optionalNonEmpty(values.flags.get("--expires")) } : {}),
37
+ ...(optionalNonEmpty(values.flags.get("--label")) ? { shareLabel: optionalNonEmpty(values.flags.get("--label")) } : {}),
38
+ ...(values.booleans.has("--single-use") ? { singleUse: true } : {}),
39
+ ...(values.booleans.has("--allow-conflicts") ? { allowConflicts: true } : {}),
40
+ };
41
+ }