@bli-cockpit/cli 0.2.241 → 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.
- package/dist/adapters/git-state.js +1 -0
- package/dist/adapters/raw-evidence-collection-state.js +22 -11
- package/dist/adapters/raw-evidence-git-diff.js +1 -0
- package/dist/agent-rules.js +8 -0
- package/dist/autostart-contract.js +12 -0
- package/dist/autostart-darwin-plist.js +2 -2
- package/dist/autostart-darwin.js +1 -1
- package/dist/autostart-remind.js +245 -0
- package/dist/autostart-self-heal.js +17 -1
- package/dist/autostart-windows-scripts.js +7 -7
- package/dist/autostart.js +16 -15
- package/dist/commands/approvals-check-where.js +14 -0
- package/dist/commands/approvals-render.js +8 -1
- package/dist/commands/approvals.js +3 -2
- package/dist/commands/autostart-command.js +10 -0
- package/dist/commands/autostart-heal.js +36 -1
- package/dist/commands/brain.js +1 -1
- package/dist/commands/cal-agenda.js +44 -0
- package/dist/commands/cal-availability-share.js +97 -0
- package/dist/commands/cal-booking.js +147 -0
- package/dist/commands/cal-join.js +62 -0
- package/dist/commands/cal-links.js +68 -0
- package/dist/commands/cal-people.js +136 -0
- package/dist/commands/cal-remind.js +176 -0
- package/dist/commands/cal-reminders.js +33 -0
- package/dist/commands/cal-suggest.js +89 -0
- package/dist/commands/cal.js +41 -1
- package/dist/commands/cycle-args.js +127 -0
- package/dist/commands/cycles.js +117 -0
- package/dist/commands/docs-comments-args.js +85 -0
- package/dist/commands/docs-comments.js +128 -0
- package/dist/commands/docs-files-args.js +49 -0
- package/dist/commands/docs-files.js +179 -0
- package/dist/commands/docs-formatting.js +45 -0
- package/dist/commands/docs-inbox-args.js +93 -0
- package/dist/commands/docs-inbox.js +126 -0
- package/dist/commands/docs-notion.js +5 -0
- package/dist/commands/docs-read.js +2 -1
- package/dist/commands/docs-recovery.js +173 -0
- package/dist/commands/docs-refs-args.js +104 -0
- package/dist/commands/docs-refs.js +153 -0
- package/dist/commands/docs-search-args.js +79 -0
- package/dist/commands/docs-search.js +64 -0
- package/dist/commands/docs-sync-args.js +82 -0
- package/dist/commands/docs-sync.js +197 -0
- package/dist/commands/docs-tree.js +136 -0
- package/dist/commands/docs.js +85 -51
- package/dist/commands/doctor-update.js +4 -1
- package/dist/commands/inbox-args.js +81 -0
- package/dist/commands/inbox.js +88 -0
- package/dist/commands/issue-batch.js +61 -0
- package/dist/commands/issue-relations.js +151 -0
- package/dist/commands/issue-write.js +38 -5
- package/dist/commands/issue.js +32 -1
- package/dist/commands/local-args-tower-approvals.js +16 -4
- package/dist/commands/local-args-tower-cal-booking.js +81 -0
- package/dist/commands/local-args-tower-cal-links.js +42 -0
- package/dist/commands/local-args-tower-cal-remind.js +74 -0
- package/dist/commands/local-args-tower-cal-share.js +41 -0
- package/dist/commands/local-args-tower-cal.js +81 -8
- package/dist/commands/local-args-tower-docs-msg.js +98 -9
- package/dist/commands/local-args-tower-msg.js +279 -10
- package/dist/commands/local-args-tower-search.js +32 -5
- package/dist/commands/local-args-tower-work.js +110 -34
- package/dist/commands/local-args-tower.js +1 -0
- package/dist/commands/local-args.js +2 -0
- package/dist/commands/local-auth.js +1 -1
- package/dist/commands/local-help-commands-tower-ops.js +2 -0
- package/dist/commands/local-help-commands-tower.js +48 -32
- package/dist/commands/local-help-commands.js +1 -0
- package/dist/commands/local-help-cycles.js +9 -0
- package/dist/commands/local-help-docs-files.js +10 -0
- package/dist/commands/local-help-docs-formatting.js +7 -0
- package/dist/commands/local-help-docs-inbox.js +10 -0
- package/dist/commands/local-help-docs-recovery.js +10 -0
- package/dist/commands/local-help-docs-refs.js +11 -0
- package/dist/commands/local-help-docs-sync.js +9 -0
- package/dist/commands/local-help-inbox.js +9 -0
- package/dist/commands/local-help-project.js +21 -0
- package/dist/commands/local-help-templates.js +9 -0
- package/dist/commands/local-help-views.js +10 -0
- package/dist/commands/local-help-work-notify.js +6 -0
- package/dist/commands/local-help.js +4 -2
- package/dist/commands/local.js +2 -0
- package/dist/commands/mcp-stdio-probe.js +1 -0
- package/dist/commands/msg-activity.js +134 -0
- package/dist/commands/msg-channel-args.js +92 -0
- package/dist/commands/msg-channel.js +125 -0
- package/dist/commands/msg-context-args.js +67 -0
- package/dist/commands/msg-context.js +65 -0
- package/dist/commands/msg-dm.js +28 -0
- package/dist/commands/msg-draft.js +193 -0
- package/dist/commands/msg-file.js +185 -0
- package/dist/commands/msg-mark-args.js +78 -0
- package/dist/commands/msg-mark.js +148 -0
- package/dist/commands/msg-notify-args.js +100 -0
- package/dist/commands/msg-notify.js +122 -0
- package/dist/commands/msg-schedule-args.js +114 -0
- package/dist/commands/msg-schedule.js +226 -0
- package/dist/commands/msg-search-args.js +71 -0
- package/dist/commands/msg-search.js +48 -0
- package/dist/commands/msg-write.js +15 -1
- package/dist/commands/msg.js +77 -39
- package/dist/commands/project-args.js +82 -0
- package/dist/commands/project.js +71 -7
- package/dist/commands/public-root.js +1 -1
- package/dist/commands/push.js +50 -0
- package/dist/commands/search.js +23 -0
- package/dist/commands/session-sync-failures.js +17 -4
- package/dist/commands/session-sync-health.js +7 -2
- package/dist/commands/session-sync-hold.js +42 -1
- package/dist/commands/session-sync-upload.js +2 -0
- package/dist/commands/session-sync.js +1 -0
- package/dist/commands/sync-doctor.js +14 -1
- package/dist/commands/sync-report.js +1 -1
- package/dist/commands/template-args.js +122 -0
- package/dist/commands/templates.js +162 -0
- package/dist/commands/view-args.js +131 -0
- package/dist/commands/views.js +138 -0
- package/dist/commands/work-notify-args.js +67 -0
- package/dist/commands/work-notify.js +63 -0
- package/dist/delivery-hold-notice.js +34 -0
- package/dist/log-rotation.js +2 -2
- package/dist/notifier.js +83 -0
- package/dist/process-runner.js +4 -0
- package/dist/repo-identity-git.js +1 -0
- package/package.json +4 -4
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The saved, pin and forward verbs of `cockpit msg` (BLI-21774): `save`, `saved`, `pin`, `pins`, `forward`. Same doors the
|
|
3
|
+
* browser calls (`/api/msg/messages/<id>/save`, `/api/msg/saved`, `.../pin`, `/api/msg/channels/<id>/pins`,
|
|
4
|
+
* `.../forward`); `--json` prints the door's body unchanged so ids, rows, coverage and reasons match HTTP. A save is private
|
|
5
|
+
* to you; a pin is shared channel state; a forward posts ONE message holding your comment and the source's exact link,
|
|
6
|
+
* never its text. A refusal keeps the door's reason: message_not_found_or_unreadable, pin_limit_reached, pin_deleted_message,
|
|
7
|
+
* channel_archived, forward_source_deleted.
|
|
8
|
+
*/
|
|
9
|
+
import { randomUUID } from "node:crypto";
|
|
10
|
+
import { askAgentDoor, emitAgentDoor, failAgentDoor } from "./agent-door.js";
|
|
11
|
+
import { isInteractiveStdin, readPipedText, writeLine } from "./cli-io.js";
|
|
12
|
+
import { runMsgContext } from "./msg-context.js";
|
|
13
|
+
import { runMsgSchedule } from "./msg-schedule.js";
|
|
14
|
+
import { refuseMsgWrite } from "./msg-write.js";
|
|
15
|
+
const TAG = "[msg cli]";
|
|
16
|
+
const DEADLINE_MS = 30_000;
|
|
17
|
+
const COMMENT_MAX_CHARS = 2_000;
|
|
18
|
+
function oneLine(content, deleted) {
|
|
19
|
+
if (deleted)
|
|
20
|
+
return "(This message was deleted)";
|
|
21
|
+
const line = (content ?? "").split("\n")[0] ?? "";
|
|
22
|
+
return line.length > 140 ? `${line.slice(0, 137)}...` : line;
|
|
23
|
+
}
|
|
24
|
+
async function markVerb(command, door, kind) {
|
|
25
|
+
const on = command.remove !== true;
|
|
26
|
+
const answer = await askAgentDoor(door, {
|
|
27
|
+
path: `/api/msg/messages/${encodeURIComponent(command.targetId ?? "")}/${kind}`,
|
|
28
|
+
method: on ? "PUT" : "DELETE",
|
|
29
|
+
label: `msg ${kind}`,
|
|
30
|
+
timeoutMs: DEADLINE_MS,
|
|
31
|
+
});
|
|
32
|
+
if (!answer.ok)
|
|
33
|
+
return failAgentDoor(door, TAG, answer.reason, answer.detail);
|
|
34
|
+
const body = answer.body;
|
|
35
|
+
writeLine(door.io.stderr, `${TAG} ${kind} ${JSON.stringify({ message_id: command.targetId, on, changed: body.changed === true })}`);
|
|
36
|
+
if (door.json)
|
|
37
|
+
return emitAgentDoor(door, { ok: true, ...answer.body });
|
|
38
|
+
const past = kind === "save" ? (on ? "Saved" : "Unsaved") : on ? "Pinned" : "Unpinned";
|
|
39
|
+
writeLine(door.io.stdout, body.changed === false ? `${past} already: ${command.targetId} was already ${past.toLowerCase()}.` : `${past} ${command.targetId}.`);
|
|
40
|
+
return 0;
|
|
41
|
+
}
|
|
42
|
+
async function savedVerb(command, door) {
|
|
43
|
+
const query = new URLSearchParams();
|
|
44
|
+
if (command.before)
|
|
45
|
+
query.set("before", command.before);
|
|
46
|
+
if (command.limit)
|
|
47
|
+
query.set("limit", String(command.limit));
|
|
48
|
+
const answer = await askAgentDoor(door, {
|
|
49
|
+
path: `/api/msg/saved${query.size > 0 ? `?${query.toString()}` : ""}`,
|
|
50
|
+
method: "GET",
|
|
51
|
+
label: "msg saved",
|
|
52
|
+
timeoutMs: DEADLINE_MS,
|
|
53
|
+
});
|
|
54
|
+
if (!answer.ok)
|
|
55
|
+
return failAgentDoor(door, TAG, answer.reason, answer.detail);
|
|
56
|
+
const body = answer.body;
|
|
57
|
+
const items = body.items ?? [];
|
|
58
|
+
writeLine(door.io.stderr, `${TAG} saved ${JSON.stringify({ returned: items.length, dropped: body.coverage?.dropped ?? 0, has_more: body.hasMore === true })}`);
|
|
59
|
+
if (door.json)
|
|
60
|
+
return emitAgentDoor(door, { ok: true, ...answer.body });
|
|
61
|
+
for (const item of items) {
|
|
62
|
+
writeLine(door.io.stdout, `${item.message_id} ${item.saved_at} ${oneLine(item.message.content, Boolean(item.message.deleted_at))}`);
|
|
63
|
+
}
|
|
64
|
+
if (items.length === 0)
|
|
65
|
+
writeLine(door.io.stdout, "Nothing saved yet.");
|
|
66
|
+
const dropped = body.coverage?.dropped ?? 0;
|
|
67
|
+
if (dropped > 0)
|
|
68
|
+
writeLine(door.io.stdout, `${dropped} saved ${dropped === 1 ? "message is" : "messages are"} not shown: you can no longer read ${dropped === 1 ? "it" : "them"}.`);
|
|
69
|
+
writeLine(door.io.stdout, body.hasMore && body.nextCursor ? `more: --before ${body.nextCursor}` : "complete");
|
|
70
|
+
return 0;
|
|
71
|
+
}
|
|
72
|
+
async function pinsVerb(command, door, resolve) {
|
|
73
|
+
const ref = command.channelRef ?? "";
|
|
74
|
+
const resolved = await resolve(ref);
|
|
75
|
+
if (resolved.status === "list_failed")
|
|
76
|
+
return failAgentDoor(door, TAG, resolved.reason, resolved.detail);
|
|
77
|
+
if (resolved.status === "not_found") {
|
|
78
|
+
return failAgentDoor(door, TAG, "channel_not_found_or_unreadable", `That channel does not exist, or you cannot read it: ${ref}`);
|
|
79
|
+
}
|
|
80
|
+
const answer = await askAgentDoor(door, { path: `/api/msg/channels/${encodeURIComponent(resolved.id)}/pins`, method: "GET", label: "msg pins", timeoutMs: DEADLINE_MS });
|
|
81
|
+
if (!answer.ok)
|
|
82
|
+
return failAgentDoor(door, TAG, answer.reason, answer.detail);
|
|
83
|
+
const body = answer.body;
|
|
84
|
+
const pins = body.pins ?? [];
|
|
85
|
+
writeLine(door.io.stderr, `${TAG} pins ${JSON.stringify({ channel_id: resolved.id, returned: pins.length, dropped: body.coverage?.dropped ?? 0 })}`);
|
|
86
|
+
if (door.json)
|
|
87
|
+
return emitAgentDoor(door, { ok: true, ...answer.body });
|
|
88
|
+
for (const pin of pins) {
|
|
89
|
+
const who = pin.pinned_by ? body.displayNameById?.[pin.pinned_by] ?? "someone" : "someone";
|
|
90
|
+
writeLine(door.io.stdout, `${pin.message_id} pinned ${pin.pinned_at} by ${who} ${oneLine(pin.message.content, Boolean(pin.message.deleted_at))}`);
|
|
91
|
+
}
|
|
92
|
+
if (pins.length === 0)
|
|
93
|
+
writeLine(door.io.stdout, "Nothing is pinned in this channel.");
|
|
94
|
+
return 0;
|
|
95
|
+
}
|
|
96
|
+
async function forwardVerb(command, door, resolve) {
|
|
97
|
+
const ref = command.forwardTo ?? "";
|
|
98
|
+
const resolved = await resolve(ref);
|
|
99
|
+
if (resolved.status === "list_failed")
|
|
100
|
+
return failAgentDoor(door, TAG, resolved.reason, resolved.detail);
|
|
101
|
+
if (resolved.status === "not_found") {
|
|
102
|
+
return failAgentDoor(door, TAG, "channel_not_found_or_unreadable", `That channel does not exist, or you cannot read it: ${ref}`);
|
|
103
|
+
}
|
|
104
|
+
let comment = "";
|
|
105
|
+
if (!isInteractiveStdin(door.io)) {
|
|
106
|
+
try {
|
|
107
|
+
comment = (await readPipedText(door.io.stdin, { maxChars: COMMENT_MAX_CHARS, overflowMessage: `A forward comment is limited to ${COMMENT_MAX_CHARS} characters.` })).trim();
|
|
108
|
+
}
|
|
109
|
+
catch (error) {
|
|
110
|
+
return failAgentDoor(door, TAG, "content_too_long", error instanceof Error ? error.message : String(error));
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
const requestId = command.requestId ?? randomUUID();
|
|
114
|
+
const answer = await askAgentDoor(door, {
|
|
115
|
+
path: `/api/msg/messages/${encodeURIComponent(command.targetId ?? "")}/forward`,
|
|
116
|
+
method: "POST",
|
|
117
|
+
label: "msg forward",
|
|
118
|
+
timeoutMs: DEADLINE_MS,
|
|
119
|
+
body: {
|
|
120
|
+
channel_id: resolved.id,
|
|
121
|
+
request_id: requestId,
|
|
122
|
+
...(comment ? { comment } : {}),
|
|
123
|
+
...(command.threadId ? { thread_parent_id: command.threadId } : {}),
|
|
124
|
+
},
|
|
125
|
+
});
|
|
126
|
+
if (!answer.ok)
|
|
127
|
+
return refuseMsgWrite(door, answer, requestId, "forward");
|
|
128
|
+
const body = answer.body;
|
|
129
|
+
writeLine(door.io.stderr, `${TAG} forwarded ${JSON.stringify({ source_id: command.targetId, destination_channel_id: resolved.id, message_id: body.message?.id ?? null, request_id: requestId })}`);
|
|
130
|
+
if (door.json)
|
|
131
|
+
return emitAgentDoor(door, { ok: true, ...answer.body });
|
|
132
|
+
writeLine(door.io.stdout, `Forwarded ${command.targetId} to ${ref} as ${body.message?.id ?? "a new message"}. It holds your comment and the link only.`);
|
|
133
|
+
return 0;
|
|
134
|
+
}
|
|
135
|
+
export async function runMsgMark(command, door, resolve) {
|
|
136
|
+
switch (command.action) {
|
|
137
|
+
case "save": return markVerb(command, door, "save");
|
|
138
|
+
case "pin": return markVerb(command, door, "pin");
|
|
139
|
+
case "saved": return savedVerb(command, door);
|
|
140
|
+
case "pins": return pinsVerb(command, door, resolve);
|
|
141
|
+
// BLI-21802: the schedule verbs ride the same entry so `msg.ts` stays under the file-size ratchet.
|
|
142
|
+
case "schedule":
|
|
143
|
+
case "remind":
|
|
144
|
+
case "scheduled": return runMsgSchedule(command, door, resolve);
|
|
145
|
+
case "context": return runMsgContext(command, door);
|
|
146
|
+
default: return forwardVerb(command, door, resolve);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Arguments of `cockpit msg notify` (BLI-21605): `notify [--json]`, `notify set --level <all|mentions|none>
|
|
3
|
+
* [--channel <id|name>] [--mute|--unmute] [--clear]`, `notify quiet <HH:MM-HH:MM> [--timezone <zone>] | --off`,
|
|
4
|
+
* `notify receipts [--message <id>] [--before <cursor>] [--limit <n>]`. Split out of `local-args-tower-msg.ts`.
|
|
5
|
+
*/
|
|
6
|
+
import { optionalNonEmpty, optionalPositiveInteger, optionalUrl } from "./local-arg-values.js";
|
|
7
|
+
export const MSG_NOTIFY_ONLY_FLAGS = ["--level", "--timezone", "--message", "--mute", "--unmute", "--clear", "--off"];
|
|
8
|
+
const VERBS = new Set(["set", "quiet", "receipts"]);
|
|
9
|
+
const LEVELS = ["all", "mentions", "none"];
|
|
10
|
+
const WINDOW = /^([01]\d|2[0-3]):([0-5]\d)-([01]\d|2[0-3]):([0-5]\d)$/;
|
|
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
|
+
function browserZone() {
|
|
13
|
+
return Intl.DateTimeFormat().resolvedOptions().timeZone || "UTC";
|
|
14
|
+
}
|
|
15
|
+
export function parseMsgNotifyArgs(values, rest) {
|
|
16
|
+
const first = rest[0];
|
|
17
|
+
if (first !== undefined && !VERBS.has(first)) {
|
|
18
|
+
throw new Error(`Unknown msg notify command: ${first}. Try set, quiet or receipts, or no verb to show your settings.`);
|
|
19
|
+
}
|
|
20
|
+
const verb = first ?? "show";
|
|
21
|
+
const positional = rest.slice(1);
|
|
22
|
+
const has = (flag) => values.flags.has(flag) || values.booleans.has(flag);
|
|
23
|
+
const allowed = {
|
|
24
|
+
show: [],
|
|
25
|
+
set: ["--level", "--channel", "--mute", "--unmute", "--clear"],
|
|
26
|
+
quiet: ["--timezone", "--off"],
|
|
27
|
+
receipts: ["--message", "--before", "--limit"],
|
|
28
|
+
};
|
|
29
|
+
const stray = [...MSG_NOTIFY_ONLY_FLAGS, "--channel", "--before", "--limit"].find((flag) => has(flag) && !allowed[verb].includes(flag));
|
|
30
|
+
if (stray)
|
|
31
|
+
throw new Error(`${stray} does not belong to \`cockpit msg notify${verb === "show" ? "" : ` ${verb}`}\`.`);
|
|
32
|
+
const stray2 = ["--thread", "--members", "--description", "--from", "--request-id", "--expected-revision", "--name", "--out", "--file", "--mark-read", "--mentions", "--threads", "--unread", "--remove", "--private"].find(has);
|
|
33
|
+
if (stray2)
|
|
34
|
+
throw new Error(`${stray2} does not belong to \`cockpit msg notify\`.`);
|
|
35
|
+
const level = optionalNonEmpty(values.flags.get("--level"));
|
|
36
|
+
const channelRef = optionalNonEmpty(values.flags.get("--channel"));
|
|
37
|
+
const mute = values.booleans.has("--mute");
|
|
38
|
+
const unmute = values.booleans.has("--unmute");
|
|
39
|
+
const clear = values.booleans.has("--clear");
|
|
40
|
+
const quietOff = values.booleans.has("--off");
|
|
41
|
+
const timezone = optionalNonEmpty(values.flags.get("--timezone"));
|
|
42
|
+
const messageId = optionalNonEmpty(values.flags.get("--message"));
|
|
43
|
+
const before = optionalNonEmpty(values.flags.get("--before"));
|
|
44
|
+
const limit = optionalPositiveInteger(values.flags.get("--limit"), "--limit");
|
|
45
|
+
let quietWindow;
|
|
46
|
+
if (verb === "set") {
|
|
47
|
+
if (positional.length > 0)
|
|
48
|
+
throw new Error(`msg notify set does not take "${positional[0]}".`);
|
|
49
|
+
if (level && !LEVELS.includes(level))
|
|
50
|
+
throw new Error("--level must be all, mentions or none.");
|
|
51
|
+
if (mute && unmute)
|
|
52
|
+
throw new Error("Pick one of --mute or --unmute.");
|
|
53
|
+
if (!channelRef && (mute || unmute || clear))
|
|
54
|
+
throw new Error("--mute, --unmute and --clear need --channel <id|name>.");
|
|
55
|
+
if (clear && (level || mute || unmute))
|
|
56
|
+
throw new Error("--clear removes the channel override; do not combine it with --level, --mute or --unmute.");
|
|
57
|
+
if (!level && !mute && !unmute && !clear)
|
|
58
|
+
throw new Error("msg notify set needs --level <all|mentions|none>, or with --channel also --mute, --unmute or --clear.");
|
|
59
|
+
}
|
|
60
|
+
if (verb === "quiet") {
|
|
61
|
+
quietWindow = optionalNonEmpty(positional[0]);
|
|
62
|
+
if (quietOff && quietWindow)
|
|
63
|
+
throw new Error("Pass a window or --off, not both.");
|
|
64
|
+
if (!quietOff && (!quietWindow || !WINDOW.test(quietWindow)))
|
|
65
|
+
throw new Error("msg notify quiet needs a window like 22:00-07:00 (24-hour, may wrap midnight), or --off.");
|
|
66
|
+
if (positional.length > 1)
|
|
67
|
+
throw new Error("msg notify quiet takes one window.");
|
|
68
|
+
if (quietOff && timezone)
|
|
69
|
+
throw new Error("--timezone belongs with a window, not --off.");
|
|
70
|
+
if (quietWindow && quietWindow.slice(0, 5) === quietWindow.slice(6))
|
|
71
|
+
throw new Error("Quiet hours need a start and an end that differ.");
|
|
72
|
+
}
|
|
73
|
+
if (verb === "receipts") {
|
|
74
|
+
if (positional.length > 0)
|
|
75
|
+
throw new Error(`msg notify receipts does not take "${positional[0]}".`);
|
|
76
|
+
if (messageId && !UUID.test(messageId))
|
|
77
|
+
throw new Error("--message takes a message id (a UUID).");
|
|
78
|
+
if (limit !== undefined && limit > 100)
|
|
79
|
+
throw new Error("--limit takes 1 to 100.");
|
|
80
|
+
}
|
|
81
|
+
return {
|
|
82
|
+
kind: "msg",
|
|
83
|
+
action: "notify",
|
|
84
|
+
notifyVerb: verb,
|
|
85
|
+
homeDir: optionalNonEmpty(values.flags.get("--home")),
|
|
86
|
+
dashboardUrl: optionalUrl(values.flags.get("--dashboard-url")),
|
|
87
|
+
isPrivate: false,
|
|
88
|
+
json: values.booleans.has("--json"),
|
|
89
|
+
...(level ? { notifyLevel: level } : {}),
|
|
90
|
+
...(channelRef ? { channelRef } : {}),
|
|
91
|
+
...(mute ? { mute } : {}),
|
|
92
|
+
...(unmute ? { unmute } : {}),
|
|
93
|
+
...(clear ? { clear } : {}),
|
|
94
|
+
...(quietOff ? { quietOff } : {}),
|
|
95
|
+
...(quietWindow ? { quietWindow, timezone: timezone ?? browserZone() } : {}),
|
|
96
|
+
...(messageId ? { targetId: messageId } : {}),
|
|
97
|
+
...(before ? { before } : {}),
|
|
98
|
+
...(limit === undefined ? {} : { limit }),
|
|
99
|
+
};
|
|
100
|
+
}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `cockpit msg notify` (BLI-21605): your message notification settings, quiet hours, channel overrides and delivery
|
|
3
|
+
* receipts. Same doors the browser calls (`/api/msg/notifications/settings`, `/api/msg/channels/<id>/notifications`,
|
|
4
|
+
* `/api/msg/notifications/receipts`); `--json` prints the door's body unchanged so ids, levels and reasons match HTTP.
|
|
5
|
+
* A phone receipt says `push_accepted` (the push service took it), never that a phone showed it.
|
|
6
|
+
*/
|
|
7
|
+
import { askAgentDoor, emitAgentDoor, failAgentDoor } from "./agent-door.js";
|
|
8
|
+
import { writeLine } from "./cli-io.js";
|
|
9
|
+
const TAG = "[msg cli]";
|
|
10
|
+
const DEADLINE_MS = 30_000;
|
|
11
|
+
function settingsLines(settings) {
|
|
12
|
+
const quiet = settings.quiet_hours;
|
|
13
|
+
const lines = [
|
|
14
|
+
`Default level: ${settings.default_level}`,
|
|
15
|
+
quiet ? `Quiet hours: ${quiet.start}-${quiet.end} (${quiet.timezone})` : "Quiet hours: off",
|
|
16
|
+
];
|
|
17
|
+
if (settings.channels.length === 0)
|
|
18
|
+
lines.push("No channel overrides.");
|
|
19
|
+
for (const channel of settings.channels) {
|
|
20
|
+
lines.push(`Channel ${channel.channel_id}: ${channel.muted ? "muted" : (channel.level ?? "default level")}${channel.muted && channel.level ? ` (level ${channel.level})` : ""}`);
|
|
21
|
+
}
|
|
22
|
+
return lines;
|
|
23
|
+
}
|
|
24
|
+
async function showSettings(door) {
|
|
25
|
+
const answer = await askAgentDoor(door, { path: "/api/msg/notifications/settings", method: "GET", label: "msg notify", timeoutMs: DEADLINE_MS });
|
|
26
|
+
if (!answer.ok)
|
|
27
|
+
return failAgentDoor(door, TAG, answer.reason, answer.detail);
|
|
28
|
+
const settings = answer.body.settings;
|
|
29
|
+
writeLine(door.io.stderr, `${TAG} notify settings ${JSON.stringify({ channels: settings.channels.length, quiet_hours: settings.quiet_hours !== null })}`);
|
|
30
|
+
if (door.json)
|
|
31
|
+
return emitAgentDoor(door, { ok: true, ...answer.body });
|
|
32
|
+
for (const line of settingsLines(settings))
|
|
33
|
+
writeLine(door.io.stdout, line);
|
|
34
|
+
return 0;
|
|
35
|
+
}
|
|
36
|
+
async function putSettings(door, label, body, done) {
|
|
37
|
+
const answer = await askAgentDoor(door, { path: "/api/msg/notifications/settings", method: "PUT", label, timeoutMs: DEADLINE_MS, body });
|
|
38
|
+
if (!answer.ok)
|
|
39
|
+
return failAgentDoor(door, TAG, answer.reason, answer.detail);
|
|
40
|
+
const result = answer.body;
|
|
41
|
+
writeLine(door.io.stderr, `${TAG} ${label} ${JSON.stringify({ changed: result.changed === true, fields: result.changedFields ?? [] })}`);
|
|
42
|
+
if (door.json)
|
|
43
|
+
return emitAgentDoor(door, { ok: true, ...answer.body });
|
|
44
|
+
writeLine(door.io.stdout, result.changed === false ? `${done} (it was already that way).` : `${done}.`);
|
|
45
|
+
return 0;
|
|
46
|
+
}
|
|
47
|
+
async function setChannel(command, door, resolve) {
|
|
48
|
+
const ref = command.channelRef ?? "";
|
|
49
|
+
const resolved = await resolve(ref);
|
|
50
|
+
if (resolved.status === "list_failed")
|
|
51
|
+
return failAgentDoor(door, TAG, resolved.reason, resolved.detail);
|
|
52
|
+
if (resolved.status === "not_found") {
|
|
53
|
+
return failAgentDoor(door, TAG, "channel_not_found_or_unreadable", `That channel does not exist, or you cannot read it: ${ref}`);
|
|
54
|
+
}
|
|
55
|
+
const path = `/api/msg/channels/${encodeURIComponent(resolved.id)}/notifications`;
|
|
56
|
+
const body = {
|
|
57
|
+
...(command.notifyLevel ? { level: command.notifyLevel } : {}),
|
|
58
|
+
...(command.mute ? { muted: true } : {}),
|
|
59
|
+
...(command.unmute ? { muted: false } : {}),
|
|
60
|
+
};
|
|
61
|
+
const answer = command.clear
|
|
62
|
+
? await askAgentDoor(door, { path, method: "DELETE", label: "msg notify clear", timeoutMs: DEADLINE_MS })
|
|
63
|
+
: await askAgentDoor(door, { path, method: "PUT", label: "msg notify set", timeoutMs: DEADLINE_MS, body });
|
|
64
|
+
if (!answer.ok)
|
|
65
|
+
return failAgentDoor(door, TAG, answer.reason, answer.detail);
|
|
66
|
+
const result = answer.body;
|
|
67
|
+
writeLine(door.io.stderr, `${TAG} notify channel ${JSON.stringify({ channel_id: resolved.id, changed: result.changed === true, cleared: command.clear === true })}`);
|
|
68
|
+
if (door.json)
|
|
69
|
+
return emitAgentDoor(door, { ok: true, channel_id: resolved.id, ...answer.body });
|
|
70
|
+
const now = result.channel ? (result.channel.muted ? "muted" : (result.channel.level ?? "your default level")) : "your default level";
|
|
71
|
+
writeLine(door.io.stdout, `Channel ${resolved.id}: ${now}${result.changed === false ? " (it was already that way)" : ""}.`);
|
|
72
|
+
return 0;
|
|
73
|
+
}
|
|
74
|
+
async function showReceipts(command, door) {
|
|
75
|
+
const query = new URLSearchParams();
|
|
76
|
+
if (command.targetId)
|
|
77
|
+
query.set("message_id", command.targetId);
|
|
78
|
+
if (command.before)
|
|
79
|
+
query.set("before", command.before);
|
|
80
|
+
if (command.limit)
|
|
81
|
+
query.set("limit", String(command.limit));
|
|
82
|
+
const answer = await askAgentDoor(door, {
|
|
83
|
+
path: `/api/msg/notifications/receipts${query.size > 0 ? `?${query.toString()}` : ""}`,
|
|
84
|
+
method: "GET",
|
|
85
|
+
label: "msg notify receipts",
|
|
86
|
+
timeoutMs: DEADLINE_MS,
|
|
87
|
+
});
|
|
88
|
+
if (!answer.ok)
|
|
89
|
+
return failAgentDoor(door, TAG, answer.reason, answer.detail);
|
|
90
|
+
const body = answer.body;
|
|
91
|
+
const receipts = body.receipts ?? [];
|
|
92
|
+
writeLine(door.io.stderr, `${TAG} notify receipts ${JSON.stringify({ returned: receipts.length, has_more: body.hasMore === true })}`);
|
|
93
|
+
if (door.json)
|
|
94
|
+
return emitAgentDoor(door, { ok: true, ...body });
|
|
95
|
+
if (receipts.length === 0)
|
|
96
|
+
writeLine(door.io.stdout, "No delivery receipts.");
|
|
97
|
+
for (const receipt of receipts) {
|
|
98
|
+
writeLine(door.io.stdout, `${receipt.created_at} ${receipt.transport} ${receipt.kind} ${receipt.outcome} ${receipt.reason} message ${receipt.message_id}`);
|
|
99
|
+
}
|
|
100
|
+
if (body.nextCursor)
|
|
101
|
+
writeLine(door.io.stdout, `More: --before ${body.nextCursor}`);
|
|
102
|
+
return 0;
|
|
103
|
+
}
|
|
104
|
+
export async function runMsgNotify(command, door, resolve) {
|
|
105
|
+
switch (command.notifyVerb ?? "show") {
|
|
106
|
+
case "show":
|
|
107
|
+
return showSettings(door);
|
|
108
|
+
case "set":
|
|
109
|
+
if (command.channelRef)
|
|
110
|
+
return setChannel(command, door, resolve);
|
|
111
|
+
return putSettings(door, "msg notify set", { default_level: command.notifyLevel }, `Default level is ${command.notifyLevel}`);
|
|
112
|
+
case "quiet": {
|
|
113
|
+
if (command.quietOff)
|
|
114
|
+
return putSettings(door, "msg notify quiet", { quiet_hours: null }, "Quiet hours are off");
|
|
115
|
+
const [start, end] = (command.quietWindow ?? "").split("-");
|
|
116
|
+
const zone = command.timezone ?? "UTC";
|
|
117
|
+
return putSettings(door, "msg notify quiet", { quiet_hours: { start, end, timezone: zone } }, `Quiet hours are ${start}-${end} (${zone})`);
|
|
118
|
+
}
|
|
119
|
+
case "receipts":
|
|
120
|
+
return showReceipts(command, door);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Arguments of `cockpit msg schedule|remind|scheduled` (BLI-21802):
|
|
3
|
+
* schedule <channel> --at <instant|"YYYY-MM-DD HH:MM"> [--timezone <zone>] [--thread <id>] [--request-id <uuid>] (body on stdin)
|
|
4
|
+
* remind <message-id> --at <...> [--timezone <zone>] [--note <text>] [--request-id <uuid>]
|
|
5
|
+
* scheduled [--state <state|open|all>] [--before <cursor>] [--limit <n>]
|
|
6
|
+
* scheduled change <id> [--at <...>] [--timezone <zone>] [--revision <n>] (optional new body on stdin)
|
|
7
|
+
* scheduled cancel <id> | scheduled done <id>
|
|
8
|
+
* Split out of `local-args-tower-msg.ts`.
|
|
9
|
+
*/
|
|
10
|
+
import { optionalNonEmpty, optionalPositiveInteger, optionalUrl } from "./local-arg-values.js";
|
|
11
|
+
export const MSG_SCHEDULE_ACTIONS = ["schedule", "remind", "scheduled"];
|
|
12
|
+
export const MSG_SCHEDULE_VALUE_FLAGS = ["--at", "--note", "--state", "--revision"];
|
|
13
|
+
export const SCHEDULE_STATE_NAMES = ["pending", "sent", "failed", "cancelled", "fired", "done", "open", "all"];
|
|
14
|
+
const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
15
|
+
const NOTE_MAX = 500;
|
|
16
|
+
export function isMsgScheduleAction(action) {
|
|
17
|
+
return MSG_SCHEDULE_ACTIONS.includes(action);
|
|
18
|
+
}
|
|
19
|
+
/** A schedule flag used on another msg verb is named, not ignored. */
|
|
20
|
+
export function rejectMsgScheduleFlags(values) {
|
|
21
|
+
const flag = MSG_SCHEDULE_VALUE_FLAGS.find((name) => values.flags.has(name));
|
|
22
|
+
if (flag)
|
|
23
|
+
throw new Error(`${flag} belongs to \`cockpit msg schedule\`, \`msg remind\` or \`msg scheduled\`.`);
|
|
24
|
+
}
|
|
25
|
+
export function parseMsgScheduleArgs(action, values, rest) {
|
|
26
|
+
const stray = ["--channel", "--members", "--description", "--from", "--expected-revision", "--name", "--out", "--file", "--level", "--message", "--to"]
|
|
27
|
+
.find((flag) => values.flags.has(flag));
|
|
28
|
+
const strayBoolean = ["--private", "--mentions", "--threads", "--unread", "--mark-read", "--remove"].find((flag) => values.booleans.has(flag));
|
|
29
|
+
if (stray || strayBoolean)
|
|
30
|
+
throw new Error(`${stray ?? strayBoolean} does not belong to \`cockpit msg ${action}\`.`);
|
|
31
|
+
const base = {
|
|
32
|
+
kind: "msg",
|
|
33
|
+
action,
|
|
34
|
+
homeDir: optionalNonEmpty(values.flags.get("--home")),
|
|
35
|
+
dashboardUrl: optionalUrl(values.flags.get("--dashboard-url")),
|
|
36
|
+
isPrivate: false,
|
|
37
|
+
json: values.booleans.has("--json"),
|
|
38
|
+
};
|
|
39
|
+
const at = optionalNonEmpty(values.flags.get("--at"));
|
|
40
|
+
const timezone = optionalNonEmpty(values.flags.get("--timezone"));
|
|
41
|
+
const thread = optionalNonEmpty(values.flags.get("--thread"));
|
|
42
|
+
const requestId = optionalNonEmpty(values.flags.get("--request-id"));
|
|
43
|
+
const note = values.flags.get("--note");
|
|
44
|
+
const state = optionalNonEmpty(values.flags.get("--state"));
|
|
45
|
+
const before = optionalNonEmpty(values.flags.get("--before"));
|
|
46
|
+
const limit = optionalPositiveInteger(values.flags.get("--limit"), "--limit");
|
|
47
|
+
const revisionText = optionalNonEmpty(values.flags.get("--revision"));
|
|
48
|
+
const revision = revisionText === undefined ? undefined : optionalPositiveInteger(revisionText, "--revision");
|
|
49
|
+
if (requestId && !UUID.test(requestId))
|
|
50
|
+
throw new Error("--request-id takes a UUID.");
|
|
51
|
+
if (thread && (action !== "schedule" || !UUID.test(thread))) {
|
|
52
|
+
throw new Error(action === "schedule" ? "--thread takes a message id (a UUID)." : "--thread belongs to `cockpit msg schedule`.");
|
|
53
|
+
}
|
|
54
|
+
if (note !== undefined && action !== "remind")
|
|
55
|
+
throw new Error("--note belongs to `cockpit msg remind`.");
|
|
56
|
+
if (note !== undefined && note.length > NOTE_MAX)
|
|
57
|
+
throw new Error(`--note is limited to ${NOTE_MAX} characters.`);
|
|
58
|
+
if (requestId && action === "scheduled")
|
|
59
|
+
throw new Error("--request-id belongs to `cockpit msg schedule` and `msg remind`.");
|
|
60
|
+
if (action === "scheduled") {
|
|
61
|
+
const verb = rest[0];
|
|
62
|
+
if (verb === "change" || verb === "cancel" || verb === "done") {
|
|
63
|
+
const targetId = optionalNonEmpty(rest[1]);
|
|
64
|
+
if (!targetId || !UUID.test(targetId))
|
|
65
|
+
throw new Error(`msg scheduled ${verb} needs a schedule id (a UUID) from \`cockpit msg scheduled\`.`);
|
|
66
|
+
if (rest.length > 2)
|
|
67
|
+
throw new Error(`msg scheduled ${verb} takes one schedule id, not ${rest.length - 1}.`);
|
|
68
|
+
if (state || before || limit !== undefined)
|
|
69
|
+
throw new Error("--state, --before and --limit belong to `cockpit msg scheduled` (the list).");
|
|
70
|
+
if (verb !== "change" && (at || timezone || revision !== undefined))
|
|
71
|
+
throw new Error(`--at, --timezone and --revision belong to \`cockpit msg scheduled change\`.`);
|
|
72
|
+
return {
|
|
73
|
+
...base, scheduleVerb: verb, targetId,
|
|
74
|
+
...(at ? { scheduleAt: at } : {}), ...(timezone ? { timezone } : {}), ...(revision === undefined ? {} : { scheduleRevision: revision }),
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
if (rest.length > 0)
|
|
78
|
+
throw new Error(`msg scheduled does not take "${rest[0]}". Try change, cancel or done, or no word to list.`);
|
|
79
|
+
if (at || timezone || revision !== undefined)
|
|
80
|
+
throw new Error("--at, --timezone and --revision belong to `cockpit msg scheduled change`.");
|
|
81
|
+
if (state && !SCHEDULE_STATE_NAMES.includes(state)) {
|
|
82
|
+
throw new Error(`--state takes one of ${SCHEDULE_STATE_NAMES.join(", ")}.`);
|
|
83
|
+
}
|
|
84
|
+
return {
|
|
85
|
+
...base, scheduleVerb: "list",
|
|
86
|
+
...(state ? { scheduleState: state } : {}), ...(before ? { before } : {}), ...(limit === undefined ? {} : { limit }),
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
if (state || before || limit !== undefined || revision !== undefined) {
|
|
90
|
+
throw new Error(`--state, --before, --limit and --revision belong to \`cockpit msg scheduled\`, not \`msg ${action}\`.`);
|
|
91
|
+
}
|
|
92
|
+
const target = optionalNonEmpty(rest[0]);
|
|
93
|
+
if (rest.length > 1)
|
|
94
|
+
throw new Error(`msg ${action} takes one ${action === "schedule" ? "channel" : "message id"}, not ${rest.length}.`);
|
|
95
|
+
if (!at) {
|
|
96
|
+
throw new Error(action === "schedule"
|
|
97
|
+
? 'msg schedule needs --at, e.g. `echo "standup notes" | cockpit msg schedule ops --at "2026-11-02 09:00" --timezone America/Toronto`.'
|
|
98
|
+
: 'msg remind needs --at, e.g. `cockpit msg remind <message-id> --at "2026-11-02 09:00" --timezone America/Toronto`.');
|
|
99
|
+
}
|
|
100
|
+
if (action === "schedule") {
|
|
101
|
+
if (!target)
|
|
102
|
+
throw new Error("msg schedule needs a channel id or name, e.g. `cockpit msg schedule ops --at ...`.");
|
|
103
|
+
return {
|
|
104
|
+
...base, channelRef: target, scheduleAt: at,
|
|
105
|
+
...(timezone ? { timezone } : {}), ...(thread ? { threadId: thread } : {}), ...(requestId ? { requestId } : {}),
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
if (!target || !UUID.test(target))
|
|
109
|
+
throw new Error("msg remind needs a message id (a UUID), e.g. `cockpit msg remind <message-id> --at ...`.");
|
|
110
|
+
return {
|
|
111
|
+
...base, targetId: target, scheduleAt: at,
|
|
112
|
+
...(timezone ? { timezone } : {}), ...(note !== undefined ? { scheduleNote: note } : {}), ...(requestId ? { requestId } : {}),
|
|
113
|
+
};
|
|
114
|
+
}
|