@bli-cockpit/cli 0.2.236 → 0.2.237
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/commands/cal-overlay.js +63 -0
- package/dist/commands/cal.js +4 -0
- package/dist/commands/doctor-usage-telemetry.js +2 -1
- package/dist/commands/local-args-tower-cal.js +6 -2
- package/dist/commands/local-args-tower-docs-msg.js +1 -143
- package/dist/commands/local-args-tower-msg.js +149 -0
- package/dist/commands/local-help-commands-tower.js +6 -3
- package/dist/commands/local-help.js +2 -2
- package/dist/commands/mail.js +6 -4
- package/dist/commands/msg.js +51 -9
- package/dist/commands/public-root.js +1 -1
- package/package.json +2 -2
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { askAgentDoor, emitAgentDoor, failAgentDoor } from "./agent-door.js";
|
|
2
|
+
import { readPipedText, isInteractiveStdin, writeLine } from "./cli-io.js";
|
|
3
|
+
export async function runCalOverlay(command, door) {
|
|
4
|
+
if (command.action === "calendar-change") {
|
|
5
|
+
let body = command.color ? { color_override: command.color === "provider" ? null : command.color } : {};
|
|
6
|
+
if (!command.color && !isInteractiveStdin(door.io)) {
|
|
7
|
+
try {
|
|
8
|
+
const parsed = JSON.parse(await readPipedText(door.io.stdin, { maxChars: 4000, overflowMessage: "Preferences are too large." }));
|
|
9
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
|
|
10
|
+
throw new Error("invalid_preferences");
|
|
11
|
+
body = parsed;
|
|
12
|
+
}
|
|
13
|
+
catch {
|
|
14
|
+
return failAgentDoor(door, "[cal cli]", "invalid_body", "Pipe JSON preferences or use --color '#4285f4' (provider resets).");
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
if (!Object.keys(body).length)
|
|
18
|
+
return failAgentDoor(door, "[cal cli]", "invalid_body", "Use --color '#4285f4', --color provider, or pipe JSON with color_override/selected.");
|
|
19
|
+
const answer = await askAgentDoor(door, { method: "PATCH", path: `/api/cal/calendars/${encodeURIComponent(command.subjectId)}`, body, label: "cal calendar-change", timeoutMs: 30000 });
|
|
20
|
+
if (!answer.ok)
|
|
21
|
+
return failAgentDoor(door, "[cal cli]", answer.reason, answer.detail);
|
|
22
|
+
if (door.json)
|
|
23
|
+
return emitAgentDoor(door, answer.body);
|
|
24
|
+
writeLine(door.io.stdout, "Calendar preferences saved. Provider sync preserves your colour override.");
|
|
25
|
+
return 0;
|
|
26
|
+
}
|
|
27
|
+
const query = new URLSearchParams({ tz: command.timeZone ?? Intl.DateTimeFormat().resolvedOptions().timeZone });
|
|
28
|
+
if (command.from)
|
|
29
|
+
query.set("from", command.from);
|
|
30
|
+
if (command.to)
|
|
31
|
+
query.set("to", command.to);
|
|
32
|
+
if (command.offset !== undefined)
|
|
33
|
+
query.set("offset", String(command.offset));
|
|
34
|
+
if (command.includeDeselected)
|
|
35
|
+
query.set("all", "true");
|
|
36
|
+
const answer = await askAgentDoor(door, { method: "GET", path: `/api/cal/conflicts?${query}`, label: "cal conflicts", timeoutMs: 30000 });
|
|
37
|
+
if (!answer.ok)
|
|
38
|
+
return failAgentDoor(door, "[cal cli]", answer.reason, answer.detail);
|
|
39
|
+
if (door.json)
|
|
40
|
+
return emitAgentDoor(door, answer.body);
|
|
41
|
+
const result = answer.body;
|
|
42
|
+
const zone = result.window?.time_zone ?? command.timeZone ?? Intl.DateTimeFormat().resolvedOptions().timeZone;
|
|
43
|
+
writeLine(door.io.stdout, `${result.groups.length} double-bookings · own calendars`);
|
|
44
|
+
for (const group of result.groups) {
|
|
45
|
+
const parts = new Intl.DateTimeFormat("en-GB", { timeZone: zone, weekday: "short", day: "numeric", month: "short", hour: "2-digit", minute: "2-digit", hourCycle: "h23" }).formatToParts(new Date(group.from));
|
|
46
|
+
const part = (name) => parts.find(item => item.type === name)?.value;
|
|
47
|
+
writeLine(door.io.stdout, `${part("weekday")} ${part("day")} ${part("month")} ${part("hour")}:${part("minute")}: ${group.meetings.map(meeting => `${meeting.event.title} · ${meeting.event.calendar_name}`).join(" / ")}`);
|
|
48
|
+
}
|
|
49
|
+
const checked = result.coverage.calendars.filter(calendar => calendar.checked);
|
|
50
|
+
const oldest = Math.min(...checked.map(calendar => calendar.lastSyncAt ? Date.parse(calendar.lastSyncAt) : NaN));
|
|
51
|
+
const minutes = Math.max(0, Math.floor((Date.now() - oldest) / 60000));
|
|
52
|
+
const age = !checked.length ? "none checked" : !Number.isFinite(oldest) ? "sync age unknown" : minutes < 1 ? "just now" : minutes < 60 ? `${minutes} min ago` : minutes < 1440 ? `${Math.floor(minutes / 60)} h ago` : `${Math.floor(minutes / 1440)} days ago`;
|
|
53
|
+
writeLine(door.io.stdout, `Checked ${checked.length} calendar${checked.length === 1 ? "" : "s"} · ${age}`);
|
|
54
|
+
for (const calendar of result.coverage.calendars.filter(calendar => calendar.hidden || !calendar.checked)) {
|
|
55
|
+
writeLine(door.io.stdout, `${calendar.name}: ${calendar.hidden ? "hidden; " : ""}${calendar.checked ? "included by request" : `not checked (${(calendar.excludedReason ?? "unavailable").replaceAll("_", " ")})`}`);
|
|
56
|
+
}
|
|
57
|
+
for (const account of result.coverage.accounts ?? [])
|
|
58
|
+
if (!account.calendars)
|
|
59
|
+
writeLine(door.io.stdout, `${account.label}: no discovered calendars (${account.status.replaceAll("_", " ")})`);
|
|
60
|
+
if (result.coverage.incomplete || result.coverage.truncated || result.coverage.notes.length)
|
|
61
|
+
writeLine(door.io.stdout, "Incomplete window. This check cannot confirm free time.");
|
|
62
|
+
return 0;
|
|
63
|
+
}
|
package/dist/commands/cal.js
CHANGED
|
@@ -22,6 +22,7 @@
|
|
|
22
22
|
*/
|
|
23
23
|
import { askAgentDoor, emitAgentDoor, failAgentDoor, openAgentDoor, } from "./agent-door.js";
|
|
24
24
|
import { runCalAddIcal, runCalDetach, runCalShare, runCalSync } from "./cal-calendars-write.js";
|
|
25
|
+
import { runCalOverlay } from "./cal-overlay.js";
|
|
25
26
|
import { runCalDelete } from "./cal-delete.js";
|
|
26
27
|
import { runCalSyncStatus } from "./cal-sync-status.js";
|
|
27
28
|
import { writeLine } from "./cli-io.js";
|
|
@@ -32,6 +33,9 @@ const WRITE_DEADLINE_MS = 60_000;
|
|
|
32
33
|
export async function runCal(command, io) {
|
|
33
34
|
const door = await openAgentDoor("cal", command, io);
|
|
34
35
|
switch (command.action) {
|
|
36
|
+
case "conflicts":
|
|
37
|
+
case "calendar-change":
|
|
38
|
+
return runCalOverlay(command, door);
|
|
35
39
|
case "today":
|
|
36
40
|
case "week":
|
|
37
41
|
case "next":
|
|
@@ -12,7 +12,8 @@ export function mergeClaudeUsageSettings(raw, origin, token, off) {
|
|
|
12
12
|
throw new Error("config_unreadable");
|
|
13
13
|
root.env = { ...root.env, CLAUDE_CODE_ENABLE_TELEMETRY: off ? "0" : "1",
|
|
14
14
|
OTEL_METRICS_EXPORTER: off ? "none" : "otlp", OTEL_LOGS_EXPORTER: off ? "none" : "otlp",
|
|
15
|
-
|
|
15
|
+
OTEL_LOGS_EXPORT_INTERVAL: "60000", OTEL_METRIC_EXPORT_INTERVAL: "300000",
|
|
16
|
+
OTEL_EXPORTER_OTLP_COMPRESSION: "gzip", OTEL_EXPORTER_OTLP_PROTOCOL: "http/json", OTEL_EXPORTER_OTLP_ENDPOINT: `${new URL(origin).origin}/api/otel`,
|
|
16
17
|
OTEL_EXPORTER_OTLP_HEADERS: `Authorization=Bearer ${token}` };
|
|
17
18
|
// OTEL_LOG_USER_PROMPTS is never added or changed. Existing person keys survive.
|
|
18
19
|
return `${JSON.stringify(root, null, 2)}\n`;
|
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
*/
|
|
21
21
|
import { optionalNonEmpty, optionalPositiveInteger, optionalUrl, parseNamedArgs, } from "./local-arg-values.js";
|
|
22
22
|
const CAL_ACTIONS = new Set([
|
|
23
|
+
"conflicts", "calendar-change",
|
|
23
24
|
"today",
|
|
24
25
|
"week",
|
|
25
26
|
"next",
|
|
@@ -35,7 +36,7 @@ const CAL_ACTIONS = new Set([
|
|
|
35
36
|
"delete",
|
|
36
37
|
]);
|
|
37
38
|
/** Every verb that names ONE thing first — a calendar id, or (delete) an event id. */
|
|
38
|
-
const CAL_ACTIONS_NEEDING_A_SUBJECT = new Set(["sync", "detach", "share", "delete", "change", "cancel"]);
|
|
39
|
+
const CAL_ACTIONS_NEEDING_A_SUBJECT = new Set(["calendar-change", "sync", "detach", "share", "delete", "change", "cancel"]);
|
|
39
40
|
/** `sync-status [<calendar id>]` — every calendar, or one's. Ticket 10730. */
|
|
40
41
|
const CAL_ACTIONS_WITH_OPTIONAL_SUBJECT = new Set(["sync-status"]);
|
|
41
42
|
export function parseCalArgs(args) {
|
|
@@ -43,6 +44,7 @@ export function parseCalArgs(args) {
|
|
|
43
44
|
allowedFlags: [
|
|
44
45
|
"--home",
|
|
45
46
|
"--dashboard-url",
|
|
47
|
+
"--color",
|
|
46
48
|
"--calendar",
|
|
47
49
|
"--account",
|
|
48
50
|
"--tz",
|
|
@@ -69,6 +71,7 @@ export function parseCalArgs(args) {
|
|
|
69
71
|
valueFlags: [
|
|
70
72
|
"--home",
|
|
71
73
|
"--dashboard-url",
|
|
74
|
+
"--color",
|
|
72
75
|
"--calendar",
|
|
73
76
|
"--account",
|
|
74
77
|
"--tz",
|
|
@@ -100,7 +103,7 @@ function readCalAction(values) {
|
|
|
100
103
|
const first = values.positionals[0];
|
|
101
104
|
const action = (first === undefined ? "today" : first);
|
|
102
105
|
if (!CAL_ACTIONS.has(action)) {
|
|
103
|
-
throw new Error(`Unknown cal command: ${first}. Try today, week, next, find, calendars, add-ical, create, share, detach, sync, sync-status, or delete.`);
|
|
106
|
+
throw new Error(`Unknown cal command: ${first}. Try today, week, conflicts, calendar-change, next, find, calendars, add-ical, create, share, detach, sync, sync-status, or delete.`);
|
|
104
107
|
}
|
|
105
108
|
return { action, rest: values.positionals.slice(first === undefined ? 0 : 1) };
|
|
106
109
|
}
|
|
@@ -170,6 +173,7 @@ function readCalOffset(values) {
|
|
|
170
173
|
function buildCalCommand(values, action, subject, query, title, startsAt, endsAt, offset, limit) {
|
|
171
174
|
return {
|
|
172
175
|
kind: "cal",
|
|
176
|
+
color: optionalNonEmpty(values.flags.get("--color")),
|
|
173
177
|
action,
|
|
174
178
|
homeDir: optionalNonEmpty(values.flags.get("--home")),
|
|
175
179
|
dashboardUrl: optionalUrl(values.flags.get("--dashboard-url")),
|
|
@@ -243,146 +243,4 @@ function buildDocsCommand(values, action, docRef, docRefs, visibility, clearPare
|
|
|
243
243
|
json: values.booleans.has("--json"),
|
|
244
244
|
};
|
|
245
245
|
}
|
|
246
|
-
|
|
247
|
-
const MSG_ACTIONS_NEEDING_A_CHANNEL = new Set(["read", "send"]);
|
|
248
|
-
/**
|
|
249
|
-
* `--members a@x.test,b@y.test` — addresses only, split on commas, never
|
|
250
|
-
* fuzzy (BLI-3749). The door resolves each one exactly, so a typo comes back
|
|
251
|
-
* as `person_not_found` naming the address rather than as a stranger added
|
|
252
|
-
* to a private channel.
|
|
253
|
-
*/
|
|
254
|
-
function parseMemberEmails(raw) {
|
|
255
|
-
const value = optionalNonEmpty(raw);
|
|
256
|
-
if (!value)
|
|
257
|
-
return undefined;
|
|
258
|
-
const emails = value
|
|
259
|
-
.split(",")
|
|
260
|
-
.map((part) => part.trim())
|
|
261
|
-
.filter((part) => part !== "");
|
|
262
|
-
if (emails.length === 0)
|
|
263
|
-
throw new Error("msg create --members needs at least one email address.");
|
|
264
|
-
const notAnAddress = emails.find((email) => !email.includes("@"));
|
|
265
|
-
if (notAnAddress) {
|
|
266
|
-
throw new Error(`msg create --members takes email addresses; "${notAnAddress}" is not one.`);
|
|
267
|
-
}
|
|
268
|
-
return emails;
|
|
269
|
-
}
|
|
270
|
-
export function parseMsgArgs(args) {
|
|
271
|
-
const values = parseNamedArgs(args, {
|
|
272
|
-
allowedFlags: [
|
|
273
|
-
"--home",
|
|
274
|
-
"--dashboard-url",
|
|
275
|
-
"--channel",
|
|
276
|
-
"--thread",
|
|
277
|
-
"--limit",
|
|
278
|
-
"--private",
|
|
279
|
-
"--members",
|
|
280
|
-
"--description",
|
|
281
|
-
"--json",
|
|
282
|
-
],
|
|
283
|
-
valueFlags: ["--home", "--dashboard-url", "--channel", "--thread", "--limit", "--members", "--description"],
|
|
284
|
-
});
|
|
285
|
-
const { action, rest } = parseMsgAction(values.positionals);
|
|
286
|
-
const { channelName, dmEmail } = parseMsgCreateTarget(action, rest);
|
|
287
|
-
const { isPrivate, memberEmails, description } = parseMsgCreateFlags(action, values);
|
|
288
|
-
const { channelRef, threadId, limit } = parseMsgReadTarget(action, rest, values);
|
|
289
|
-
return buildMsgCommand(values, action, channelRef, channelName, dmEmail, memberEmails, description, isPrivate, threadId, limit);
|
|
290
|
-
}
|
|
291
|
-
function parseMsgAction(positionals) {
|
|
292
|
-
const first = positionals[0];
|
|
293
|
-
const action = (first === undefined ? "channels" : first);
|
|
294
|
-
if (!MSG_ACTIONS.has(action)) {
|
|
295
|
-
throw new Error(`Unknown msg command: ${first}. Try channels, read, send, thread, create, or dm.`);
|
|
296
|
-
}
|
|
297
|
-
return { action, rest: positionals.slice(first === undefined ? 0 : 1) };
|
|
298
|
-
}
|
|
299
|
-
function parseMsgCreateTarget(action, rest) {
|
|
300
|
-
let channelName;
|
|
301
|
-
let dmEmail;
|
|
302
|
-
if (action === "create") {
|
|
303
|
-
channelName = optionalNonEmpty(rest[0]);
|
|
304
|
-
if (!channelName)
|
|
305
|
-
throw new Error("msg create needs a channel name, e.g. `cockpit msg create general`.");
|
|
306
|
-
if (rest.length > 1)
|
|
307
|
-
throw new Error(`msg create takes one channel name, not ${rest.length}.`);
|
|
308
|
-
// `#general` is how a person says it and how `read`/`send` accept it, so
|
|
309
|
-
// the leading # is dropped here rather than becoming part of the name.
|
|
310
|
-
if (channelName.startsWith("#"))
|
|
311
|
-
channelName = channelName.slice(1);
|
|
312
|
-
if (channelName === "")
|
|
313
|
-
throw new Error("msg create needs a channel name, e.g. `cockpit msg create general`.");
|
|
314
|
-
}
|
|
315
|
-
else if (action === "dm") {
|
|
316
|
-
dmEmail = optionalNonEmpty(rest[0]);
|
|
317
|
-
if (!dmEmail)
|
|
318
|
-
throw new Error("msg dm needs the person's email address, e.g. `cockpit msg dm ada@example.com`.");
|
|
319
|
-
if (rest.length > 1)
|
|
320
|
-
throw new Error(`msg dm takes one email address, not ${rest.length}.`);
|
|
321
|
-
if (!dmEmail.includes("@"))
|
|
322
|
-
throw new Error(`msg dm takes an email address; "${dmEmail}" is not one.`);
|
|
323
|
-
}
|
|
324
|
-
return { channelName, dmEmail };
|
|
325
|
-
}
|
|
326
|
-
function parseMsgCreateFlags(action, values) {
|
|
327
|
-
const isPrivate = values.booleans.has("--private");
|
|
328
|
-
if (isPrivate && action !== "create") {
|
|
329
|
-
throw new Error("--private belongs to `cockpit msg create`.");
|
|
330
|
-
}
|
|
331
|
-
const memberEmails = parseMemberEmails(values.flags.get("--members"));
|
|
332
|
-
if (memberEmails && action !== "create") {
|
|
333
|
-
throw new Error("--members belongs to `cockpit msg create`.");
|
|
334
|
-
}
|
|
335
|
-
const description = optionalNonEmpty(values.flags.get("--description"));
|
|
336
|
-
if (description && action !== "create") {
|
|
337
|
-
throw new Error("--description belongs to `cockpit msg create`.");
|
|
338
|
-
}
|
|
339
|
-
return { isPrivate, memberEmails, description };
|
|
340
|
-
}
|
|
341
|
-
function parseMsgReadTarget(action, rest, values) {
|
|
342
|
-
let channelRef;
|
|
343
|
-
if (MSG_ACTIONS_NEEDING_A_CHANNEL.has(action)) {
|
|
344
|
-
channelRef = optionalNonEmpty(rest[0]);
|
|
345
|
-
if (!channelRef)
|
|
346
|
-
throw new Error(`msg ${action} needs a channel id or name.`);
|
|
347
|
-
if (rest.length > 1)
|
|
348
|
-
throw new Error(`msg ${action} takes one channel, not ${rest.length}.`);
|
|
349
|
-
}
|
|
350
|
-
else if (action === "thread") {
|
|
351
|
-
// `thread <id> --channel <channel>` — the channel is a flag here because
|
|
352
|
-
// the positional names the THREAD, not the channel (`--channel` below).
|
|
353
|
-
if (rest.length > 1)
|
|
354
|
-
throw new Error(`msg thread takes one thread id, not ${rest.length}.`);
|
|
355
|
-
}
|
|
356
|
-
else if (action !== "create" && action !== "dm" && rest.length > 0) {
|
|
357
|
-
throw new Error(`msg ${action} does not take "${rest[0]}".`);
|
|
358
|
-
}
|
|
359
|
-
const threadFlag = optionalNonEmpty(values.flags.get("--thread"));
|
|
360
|
-
const threadId = action === "thread" ? optionalNonEmpty(rest[0]) : threadFlag;
|
|
361
|
-
if (action === "thread" && !threadId)
|
|
362
|
-
throw new Error("msg thread needs a thread id.");
|
|
363
|
-
const channelFlag = optionalNonEmpty(values.flags.get("--channel"));
|
|
364
|
-
if (action === "thread")
|
|
365
|
-
channelRef = channelFlag;
|
|
366
|
-
const limit = optionalPositiveInteger(values.flags.get("--limit"), "--limit");
|
|
367
|
-
if (limit !== undefined && action !== "read" && action !== "thread") {
|
|
368
|
-
throw new Error("--limit belongs to `cockpit msg read` or `cockpit msg thread`.");
|
|
369
|
-
}
|
|
370
|
-
return { channelRef, threadId, limit };
|
|
371
|
-
}
|
|
372
|
-
function buildMsgCommand(values, action, channelRef, channelName, dmEmail, memberEmails, description, isPrivate, threadId, limit) {
|
|
373
|
-
return {
|
|
374
|
-
kind: "msg",
|
|
375
|
-
action,
|
|
376
|
-
homeDir: optionalNonEmpty(values.flags.get("--home")),
|
|
377
|
-
dashboardUrl: optionalUrl(values.flags.get("--dashboard-url")),
|
|
378
|
-
...(channelRef ? { channelRef } : {}),
|
|
379
|
-
...(channelName ? { channelName } : {}),
|
|
380
|
-
...(dmEmail ? { dmEmail } : {}),
|
|
381
|
-
...(memberEmails ? { memberEmails } : {}),
|
|
382
|
-
...(description ? { description } : {}),
|
|
383
|
-
isPrivate,
|
|
384
|
-
...(threadId ? { threadId } : {}),
|
|
385
|
-
...(limit === undefined ? {} : { limit }),
|
|
386
|
-
json: values.booleans.has("--json"),
|
|
387
|
-
};
|
|
388
|
-
}
|
|
246
|
+
export { parseMsgArgs } from "./local-args-tower-msg.js";
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import { optionalNonEmpty, optionalPositiveInteger, optionalUrl, parseNamedArgs } from "./local-arg-values.js";
|
|
2
|
+
const MSG_ACTIONS = new Set(["channels", "read", "send", "thread", "create", "dm", "show"]);
|
|
3
|
+
const MSG_ACTIONS_NEEDING_A_CHANNEL = new Set(["read", "send"]);
|
|
4
|
+
/**
|
|
5
|
+
* `--members a@x.test,b@y.test` — addresses only, split on commas, never
|
|
6
|
+
* fuzzy (BLI-3749). The door resolves each one exactly, so a typo comes back
|
|
7
|
+
* as `person_not_found` naming the address rather than as a stranger added
|
|
8
|
+
* to a private channel.
|
|
9
|
+
*/
|
|
10
|
+
function parseMemberEmails(raw) {
|
|
11
|
+
const value = optionalNonEmpty(raw);
|
|
12
|
+
if (!value)
|
|
13
|
+
return undefined;
|
|
14
|
+
const emails = value
|
|
15
|
+
.split(",")
|
|
16
|
+
.map((part) => part.trim())
|
|
17
|
+
.filter((part) => part !== "");
|
|
18
|
+
if (emails.length === 0)
|
|
19
|
+
throw new Error("msg create --members needs at least one email address.");
|
|
20
|
+
const notAnAddress = emails.find((email) => !email.includes("@"));
|
|
21
|
+
if (notAnAddress) {
|
|
22
|
+
throw new Error(`msg create --members takes email addresses; "${notAnAddress}" is not one.`);
|
|
23
|
+
}
|
|
24
|
+
return emails;
|
|
25
|
+
}
|
|
26
|
+
export function parseMsgArgs(args) {
|
|
27
|
+
const values = parseNamedArgs(args, {
|
|
28
|
+
allowedFlags: [
|
|
29
|
+
"--home",
|
|
30
|
+
"--dashboard-url",
|
|
31
|
+
"--channel",
|
|
32
|
+
"--thread",
|
|
33
|
+
"--limit",
|
|
34
|
+
"--before",
|
|
35
|
+
"--private",
|
|
36
|
+
"--members",
|
|
37
|
+
"--description",
|
|
38
|
+
"--json",
|
|
39
|
+
],
|
|
40
|
+
valueFlags: ["--home", "--dashboard-url", "--channel", "--thread", "--limit", "--members", "--description", "--before"],
|
|
41
|
+
});
|
|
42
|
+
const { action, rest } = parseMsgAction(values.positionals);
|
|
43
|
+
if (values.flags.has("--before") && action !== "read" && action !== "thread") {
|
|
44
|
+
throw new Error("--before belongs to msg read or msg thread.");
|
|
45
|
+
}
|
|
46
|
+
const { channelName, dmEmail } = parseMsgCreateTarget(action, rest);
|
|
47
|
+
const { isPrivate, memberEmails, description } = parseMsgCreateFlags(action, values);
|
|
48
|
+
const { channelRef, threadId, limit } = parseMsgReadTarget(action, rest, values);
|
|
49
|
+
return buildMsgCommand(values, action, channelRef, channelName, dmEmail, memberEmails, description, isPrivate, threadId, limit);
|
|
50
|
+
}
|
|
51
|
+
function parseMsgAction(positionals) {
|
|
52
|
+
const first = positionals[0];
|
|
53
|
+
const action = (first === undefined ? "channels" : first);
|
|
54
|
+
if (!MSG_ACTIONS.has(action)) {
|
|
55
|
+
throw new Error(`Unknown msg command: ${first}. Try channels, read, send, thread, create, dm, or show.`);
|
|
56
|
+
}
|
|
57
|
+
return { action, rest: positionals.slice(first === undefined ? 0 : 1) };
|
|
58
|
+
}
|
|
59
|
+
function parseMsgCreateTarget(action, rest) {
|
|
60
|
+
let channelName;
|
|
61
|
+
let dmEmail;
|
|
62
|
+
if (action === "create") {
|
|
63
|
+
channelName = optionalNonEmpty(rest[0]);
|
|
64
|
+
if (!channelName)
|
|
65
|
+
throw new Error("msg create needs a channel name, e.g. `cockpit msg create general`.");
|
|
66
|
+
if (rest.length > 1)
|
|
67
|
+
throw new Error(`msg create takes one channel name, not ${rest.length}.`);
|
|
68
|
+
// `#general` is how a person says it and how `read`/`send` accept it, so
|
|
69
|
+
// the leading # is dropped here rather than becoming part of the name.
|
|
70
|
+
if (channelName.startsWith("#"))
|
|
71
|
+
channelName = channelName.slice(1);
|
|
72
|
+
if (channelName === "")
|
|
73
|
+
throw new Error("msg create needs a channel name, e.g. `cockpit msg create general`.");
|
|
74
|
+
}
|
|
75
|
+
else if (action === "dm") {
|
|
76
|
+
dmEmail = optionalNonEmpty(rest[0]);
|
|
77
|
+
if (!dmEmail)
|
|
78
|
+
throw new Error("msg dm needs the person's email address, e.g. `cockpit msg dm ada@example.com`.");
|
|
79
|
+
if (rest.length > 1)
|
|
80
|
+
throw new Error(`msg dm takes one email address, not ${rest.length}.`);
|
|
81
|
+
if (!dmEmail.includes("@"))
|
|
82
|
+
throw new Error(`msg dm takes an email address; "${dmEmail}" is not one.`);
|
|
83
|
+
}
|
|
84
|
+
return { channelName, dmEmail };
|
|
85
|
+
}
|
|
86
|
+
function parseMsgCreateFlags(action, values) {
|
|
87
|
+
const isPrivate = values.booleans.has("--private");
|
|
88
|
+
if (isPrivate && action !== "create") {
|
|
89
|
+
throw new Error("--private belongs to `cockpit msg create`.");
|
|
90
|
+
}
|
|
91
|
+
const memberEmails = parseMemberEmails(values.flags.get("--members"));
|
|
92
|
+
if (memberEmails && action !== "create") {
|
|
93
|
+
throw new Error("--members belongs to `cockpit msg create`.");
|
|
94
|
+
}
|
|
95
|
+
const description = optionalNonEmpty(values.flags.get("--description"));
|
|
96
|
+
if (description && action !== "create") {
|
|
97
|
+
throw new Error("--description belongs to `cockpit msg create`.");
|
|
98
|
+
}
|
|
99
|
+
return { isPrivate, memberEmails, description };
|
|
100
|
+
}
|
|
101
|
+
function parseMsgReadTarget(action, rest, values) {
|
|
102
|
+
let channelRef;
|
|
103
|
+
if (MSG_ACTIONS_NEEDING_A_CHANNEL.has(action)) {
|
|
104
|
+
channelRef = optionalNonEmpty(rest[0]);
|
|
105
|
+
if (!channelRef)
|
|
106
|
+
throw new Error(`msg ${action} needs a channel id or name.`);
|
|
107
|
+
if (rest.length > 1)
|
|
108
|
+
throw new Error(`msg ${action} takes one channel, not ${rest.length}.`);
|
|
109
|
+
}
|
|
110
|
+
else if (action === "thread" || action === "show") {
|
|
111
|
+
// `thread <id> --channel <channel>` — the channel is a flag here because
|
|
112
|
+
// the positional names the THREAD, not the channel (`--channel` below).
|
|
113
|
+
if (rest.length > 1)
|
|
114
|
+
throw new Error(`msg thread takes one thread id, not ${rest.length}.`);
|
|
115
|
+
}
|
|
116
|
+
else if (action !== "create" && action !== "dm" && rest.length > 0) {
|
|
117
|
+
throw new Error(`msg ${action} does not take "${rest[0]}".`);
|
|
118
|
+
}
|
|
119
|
+
const threadFlag = optionalNonEmpty(values.flags.get("--thread"));
|
|
120
|
+
const threadId = (action === "thread" || action === "show") ? optionalNonEmpty(rest[0]) : threadFlag;
|
|
121
|
+
if ((action === "thread" || action === "show") && !threadId)
|
|
122
|
+
throw new Error("msg thread needs a thread id.");
|
|
123
|
+
const channelFlag = optionalNonEmpty(values.flags.get("--channel"));
|
|
124
|
+
if (action === "thread")
|
|
125
|
+
channelRef = channelFlag;
|
|
126
|
+
const limit = optionalPositiveInteger(values.flags.get("--limit"), "--limit");
|
|
127
|
+
if (limit !== undefined && action !== "read" && action !== "thread") {
|
|
128
|
+
throw new Error("--limit belongs to `cockpit msg read` or `cockpit msg thread`.");
|
|
129
|
+
}
|
|
130
|
+
return { channelRef, threadId, limit };
|
|
131
|
+
}
|
|
132
|
+
function buildMsgCommand(values, action, channelRef, channelName, dmEmail, memberEmails, description, isPrivate, threadId, limit) {
|
|
133
|
+
return {
|
|
134
|
+
kind: "msg",
|
|
135
|
+
action,
|
|
136
|
+
homeDir: optionalNonEmpty(values.flags.get("--home")),
|
|
137
|
+
dashboardUrl: optionalUrl(values.flags.get("--dashboard-url")),
|
|
138
|
+
...(values.flags.has("--before") ? { before: optionalNonEmpty(values.flags.get("--before")) } : {}),
|
|
139
|
+
...(channelRef ? { channelRef } : {}),
|
|
140
|
+
...(channelName ? { channelName } : {}),
|
|
141
|
+
...(dmEmail ? { dmEmail } : {}),
|
|
142
|
+
...(memberEmails ? { memberEmails } : {}),
|
|
143
|
+
...(description ? { description } : {}),
|
|
144
|
+
isPrivate,
|
|
145
|
+
...(threadId ? { threadId } : {}),
|
|
146
|
+
...(limit === undefined ? {} : { limit }),
|
|
147
|
+
json: values.booleans.has("--json"),
|
|
148
|
+
};
|
|
149
|
+
}
|
|
@@ -77,7 +77,7 @@ export const TOWER_COMMAND_HELP = [
|
|
|
77
77
|
[
|
|
78
78
|
"msg",
|
|
79
79
|
[
|
|
80
|
-
"Usage: cockpit msg [channels|create <name>|dm <email>|read <channel>|send <channel>|thread <id> --channel <channel>] [flags]",
|
|
80
|
+
"Usage: cockpit msg [channels|create <name>|dm <email>|read <channel>|send <channel>|thread <id> --channel <channel>|show <message-id>] [flags]",
|
|
81
81
|
"",
|
|
82
82
|
"Channels and messages, typed. <channel> is a channel id, or its name with or without a leading #.",
|
|
83
83
|
"channels — every channel you are a member of (or, as a super_admin, every channel).",
|
|
@@ -85,7 +85,8 @@ export const TOWER_COMMAND_HELP = [
|
|
|
85
85
|
"dm <email> — opens (or re-opens) the direct message with one person. Idempotent: the same address always resolves to the same channel, and you never have to name yourself.",
|
|
86
86
|
"read <channel> [--limit <n>] [--thread <id>] — the channel's most recent top-level messages, oldest first; --thread <id> reads one thread's replies instead.",
|
|
87
87
|
"send <channel> [--thread <id>] — posts a message. The content is never accepted on the command line: pipe it in, e.g. `echo \"hello\" | cockpit msg send general`.",
|
|
88
|
-
"thread <id> --channel <channel> — one thread's replies by the parent message's id.",
|
|
88
|
+
"thread <id> --channel <channel> [--before <cursor>] [--limit <n>] — one thread's replies by the parent message's id.",
|
|
89
|
+
"show <message-id> — exact message, thread root and bounded neighbors. read/thread --before takes the preceding nextCursor; repeat until hasMore is false.",
|
|
89
90
|
"--json writes one machine-readable object to stdout; every reason and receipt line stays on stderr.",
|
|
90
91
|
"A refusal keeps Tower's own reason label — needs_rls_client, channel_not_found_or_unreadable, content_too_long, and so on.",
|
|
91
92
|
"The command uses the existing paired device identity. Run `cockpit login` first if this machine is not paired.",
|
|
@@ -142,13 +143,15 @@ export const TOWER_COMMAND_HELP = [
|
|
|
142
143
|
[
|
|
143
144
|
"cal",
|
|
144
145
|
[
|
|
145
|
-
'Usage: cockpit cal [today|week|next|find "<words>"|calendars|add-ical|create|share <id>|sync <id>|sync-status [<id>]|detach <id>|delete <event id> --yes] [flags]',
|
|
146
|
+
'Usage: cockpit cal [today|week|conflicts|calendar-change <id> --color <hex-or-provider>|next|find "<words>"|calendars|add-ical|create|share <id>|sync <id>|sync-status [<id>]|detach <id>|delete <event id> --yes] [flags]',
|
|
146
147
|
"",
|
|
147
148
|
"Every calendar you attached, in one place — plus any calendar a colleague marked shared.",
|
|
148
149
|
"today [--offset 1] [--tz <zone>] — what is on today, in YOUR zone (this machine's, unless --tz says otherwise). --offset 1 is tomorrow, -1 yesterday.",
|
|
149
150
|
"week [--offset 1] — Monday to Sunday. The week starts on Monday here.",
|
|
150
151
|
"next [--hours 12] — what is coming, from RIGHT NOW rather than from midnight. Defaults to the next 72 hours, so a Friday evening still answers.",
|
|
151
152
|
'find "<words>" — full text over titles, locations and descriptions. A repeating event is answered with the NEXT time it happens, not its first one in 2024.',
|
|
153
|
+
"conflicts [--from <iso> --to <iso>] [--tz <IANA>] — own-account double-bookings with duplicate invitation merging, checked calendars and age. Visible calendars only by default; --all includes hidden calendars. All-day and free events never conflict.",
|
|
154
|
+
"calendar-change <id> --color <hex|provider> — persistent owner colour override, or reset to provider colour. Also accepts stdin JSON {color_override, selected}. History is retained.",
|
|
152
155
|
"calendars — every calendar, its provider (google_oauth or ical_url), whether it is shared, and when it last synced. The ids other verbs take are on the last line.",
|
|
153
156
|
"add-ical [--name \"<what to call it>\"] — attaches a personal calendar by its SECRET iCal ADDRESS, read from stdin. There is no flag for it:",
|
|
154
157
|
' macOS: printf "%s" "https://calendar.google.com/calendar/ical/…/basic.ics" | cockpit cal add-ical --name "Personal"',
|
|
@@ -143,10 +143,10 @@ export function localCommandHelp(command) {
|
|
|
143
143
|
" cockpit clean [--dry-run] [--all-committed] [--reconcile] [--dashboard-url <url>] [--json]",
|
|
144
144
|
" cockpit sources check [--older-than <days>] [--dashboard-url <url>] [--json]",
|
|
145
145
|
" cockpit docs [list|tree|read <id|slug>|create --title <t>|update <id>|delete <id|slug>] [--parent <id>|root|--clear-parent] [--query <text> [--word]] [--limit <n>] [--visibility org|private] [--file <path>|--body-stdin] [--allow-empty] [--dashboard-url <url>] [--json]",
|
|
146
|
-
" cockpit msg [channels|create <name>|dm <email>|read <channel>|send <channel>|thread <id> --channel <channel>] [--private] [--members a@x,b@y] [--description <text>] [--thread <id>] [--limit <n>] [--dashboard-url <url>] [--json]",
|
|
146
|
+
" cockpit msg [channels|create <name>|dm <email>|read <channel>|send <channel>|thread <id> --channel <channel>|show <message-id>] [--private] [--members a@x,b@y] [--description <text>] [--thread <id>] [--limit <n>] [--before <cursor>] [--dashboard-url <url>] [--json]",
|
|
147
147
|
" cockpit issue [list|show <BLI-id>|create --title <t>|update <BLI-id>|move <BLI-id> <state>|comment <BLI-id>|history <BLI-id>] [--state <s>] [--assignee me|unassigned|<uuid>] [--project <name|id>] [--limit <n>] [--priority 0-4] [--size S|M|L] [--parent <BLI-id>] [--file <path>|--body-stdin] [--dashboard-url <url>] [--json]",
|
|
148
148
|
" cockpit mail [accounts|add-imap --address <a>|inbox|read <thread>|search \"<words>\"|send --account <id> --to <a> --subject <s>|draft --account <id> --to <a> --subject <s> [--reply-to <message id>]|attachment <id> --out <path>|sync <account>|detach <account>] [--account <id>] [--limit <n>] [--unread] [--label <l>] [--file <path>] [--attach <path>]... [--dashboard-url <url>] [--json]",
|
|
149
|
-
" cockpit cal [today|week|next|find \"<words>\"|calendars|add-ical|create --calendar <id> --title <t> --at <iso> --until <iso>|share <id> --org-visible|--private|sync <id> [--full]|detach <id>] [--tz <zone>] [--offset <n>] [--hours <n>] [--from <d> --to <d>] [--calendar <id>] [--limit <n>] [--all] [--dashboard-url <url>] [--json]",
|
|
149
|
+
" cockpit cal [today|week|conflicts|calendar-change <id> --color <hex-or-provider>|next|find \"<words>\"|calendars|add-ical|create --calendar <id> --title <t> --at <iso> --until <iso>|share <id> --org-visible|--private|sync <id> [--full]|detach <id>] [--tz <zone>] [--offset <n>] [--hours <n>] [--from <d> --to <d>] [--calendar <id>] [--limit <n>] [--all] [--dashboard-url <url>] [--json]",
|
|
150
150
|
" cockpit project [list] [--archived] [--dashboard-url <url>] [--json]",
|
|
151
151
|
" cockpit github <pr|merge|retitle|rerun> <owner/repo> <n> [--claim <state>] [--method squash|merge|rebase] [--title \"<text>\"] [--run <id>] [--confirm <code>] [--dashboard-url <url>] [--json]",
|
|
152
152
|
` cockpit search "<words>" [--kind <category,...>] [--limit <n>] [--dashboard-url <url>] [--json]`,
|
package/dist/commands/mail.js
CHANGED
|
@@ -278,7 +278,7 @@ async function send(command, door) {
|
|
|
278
278
|
body: message.body,
|
|
279
279
|
});
|
|
280
280
|
if (!answer.ok)
|
|
281
|
-
return failAgentDoor(door, TAG, answer.reason, answer.detail);
|
|
281
|
+
return failAgentDoor(door, TAG, answer.httpStatus ? answer.reason : "send_outcome_unknown", answer.httpStatus ? answer.detail : "The send receipt was lost. Check Sent before sending again; Tower will not retry automatically.");
|
|
282
282
|
const sent = answer.body.sent;
|
|
283
283
|
// Report only what the door's receipt shows (BLI-12661), never what was posted.
|
|
284
284
|
const unconfirmed = unconfirmedAttachments(message.receipt, sent?.attachments, "send");
|
|
@@ -339,8 +339,8 @@ async function sync(command, door) {
|
|
|
339
339
|
writeLine(door.io.stdout, "Tower answered without a run. Nothing to report.");
|
|
340
340
|
return 0;
|
|
341
341
|
}
|
|
342
|
-
if (
|
|
343
|
-
writeLine(door.io.stdout, `
|
|
342
|
+
if (["ok", "quota_deferred", "history_gap", "interrupted"].includes(run.reason)) {
|
|
343
|
+
writeLine(door.io.stdout, `Sync pass: ${run.messagesAdded} new, ${run.messagesUpdated} updated, ${run.messagesDeleted} gone. ${run.reason}${run.coverage?.complete === false ? "; more work remains" : ""}.`);
|
|
344
344
|
return 0;
|
|
345
345
|
}
|
|
346
346
|
// A failure names itself; the exit code says it failed.
|
|
@@ -367,11 +367,13 @@ async function syncStatus(command, door) {
|
|
|
367
367
|
return 0;
|
|
368
368
|
}
|
|
369
369
|
for (const account of accounts) {
|
|
370
|
+
const coverage = account.last_run?.coverage;
|
|
371
|
+
const coverageLine = coverage ? ` requested=${coverage.requested} returned=${coverage.returned} unique=${coverage.unique} pending=${coverage.pending} complete=${coverage.complete}` : "";
|
|
370
372
|
const lastRun = account.last_run
|
|
371
373
|
? `${account.last_run.reason} @ ${account.last_run.started_at} (${account.last_run.items} item(s))`
|
|
372
374
|
: "never synced";
|
|
373
375
|
const lag = account.lag ? `${account.lag.lag_seconds}s` : "unknown";
|
|
374
|
-
writeLine(door.io.stdout, `${account.address} last_success=${account.last_success_at ?? "never"} last_run=${lastRun} failures_24h=${account.failures_24h} lag=${lag}${account.data_incomplete ? " (partial read)" : ""}`);
|
|
376
|
+
writeLine(door.io.stdout, `${account.address} last_success=${account.last_success_at ?? "never"} last_run=${lastRun} failures_24h=${account.failures_24h} lag=${lag}${account.data_incomplete ? " (partial read)" : ""}${coverageLine}`);
|
|
375
377
|
}
|
|
376
378
|
return 0;
|
|
377
379
|
}
|
package/dist/commands/msg.js
CHANGED
|
@@ -25,6 +25,8 @@ const CONTENT_MAX_CHARS = 8_000;
|
|
|
25
25
|
export async function runMsg(command, io) {
|
|
26
26
|
const door = await openAgentDoor("msg", command, io);
|
|
27
27
|
switch (command.action) {
|
|
28
|
+
case "show":
|
|
29
|
+
return showMessage(command, door);
|
|
28
30
|
case "channels":
|
|
29
31
|
return listChannels(door);
|
|
30
32
|
case "read":
|
|
@@ -59,9 +61,20 @@ async function fetchChannels(door) {
|
|
|
59
61
|
});
|
|
60
62
|
if (!answer.ok)
|
|
61
63
|
return { ok: false, reason: answer.reason, detail: answer.detail };
|
|
64
|
+
const channels = answer.body.channels ?? [];
|
|
65
|
+
let page = answer.body;
|
|
66
|
+
for (let count = 0; page.hasMore; count++) {
|
|
67
|
+
if (count >= 200 || !page.nextCursor)
|
|
68
|
+
return { ok: false, reason: "read_failed", detail: "Channel listing is incomplete. Retry online or use an ID." };
|
|
69
|
+
const next = await askAgentDoor(door, { path: `/api/msg/channels?before=${encodeURIComponent(page.nextCursor)}`, method: "GET", label: "msg channels page", timeoutMs: READ_DEADLINE_MS });
|
|
70
|
+
if (!next.ok)
|
|
71
|
+
return { ok: false, reason: next.reason, detail: next.detail };
|
|
72
|
+
channels.push(...(next.body.channels ?? []));
|
|
73
|
+
page = next.body;
|
|
74
|
+
}
|
|
62
75
|
return {
|
|
63
76
|
ok: true,
|
|
64
|
-
channels
|
|
77
|
+
channels,
|
|
65
78
|
fromCache: answer.fromCache,
|
|
66
79
|
ageMs: answer.ageMs,
|
|
67
80
|
coverage: answer.coverage,
|
|
@@ -89,6 +102,12 @@ async function listChannels(door) {
|
|
|
89
102
|
}
|
|
90
103
|
/** Exact id, or exact name with or without a leading `#`. Never fuzzy. */
|
|
91
104
|
async function resolveChannelId(door, ref) {
|
|
105
|
+
if (/^[0-9a-f]{8}-[0-9a-f-]{27}$/i.test(ref)) {
|
|
106
|
+
const answer = await askAgentDoor(door, { path: `/api/msg/channels?channel_id=${encodeURIComponent(ref)}`, method: "GET", label: "msg channel", timeoutMs: READ_DEADLINE_MS });
|
|
107
|
+
if (!answer.ok)
|
|
108
|
+
return answer.reason === "channel_not_found_or_unreadable" ? { status: "not_found" } : { status: "list_failed", reason: answer.reason, detail: answer.detail };
|
|
109
|
+
return { status: "ok", id: ref };
|
|
110
|
+
}
|
|
92
111
|
const fetched = await fetchChannels(door);
|
|
93
112
|
if (!fetched.ok)
|
|
94
113
|
return { status: "list_failed", reason: fetched.reason, detail: fetched.detail };
|
|
@@ -112,6 +131,8 @@ async function readChannel(command, door) {
|
|
|
112
131
|
if (resolved.status === "not_found")
|
|
113
132
|
return channelNotFound(door, ref);
|
|
114
133
|
const query = new URLSearchParams();
|
|
134
|
+
if (command.before)
|
|
135
|
+
query.set("before", command.before);
|
|
115
136
|
if (command.limit !== undefined)
|
|
116
137
|
query.set("limit", String(command.limit));
|
|
117
138
|
if (command.threadId)
|
|
@@ -124,7 +145,7 @@ async function readChannel(command, door) {
|
|
|
124
145
|
timeoutMs: READ_DEADLINE_MS,
|
|
125
146
|
});
|
|
126
147
|
if (!answer.ok)
|
|
127
|
-
return failAgentDoor(door, TAG, answer.reason, answer.detail);
|
|
148
|
+
return failAgentDoor(door, TAG, answer.reason, answer.detail, answer.body?.coverage === "failed_partial" ? answer.body : {});
|
|
128
149
|
const body = answer.body;
|
|
129
150
|
const messages = body.messages ?? [];
|
|
130
151
|
// BLI-10449: whether this channel is Tower's own or a Slack snapshot, and
|
|
@@ -132,12 +153,14 @@ async function readChannel(command, door) {
|
|
|
132
153
|
const provenance = body.provenance ?? null;
|
|
133
154
|
const limitApplied = body.limitApplied ?? null;
|
|
134
155
|
if (door.json)
|
|
135
|
-
return emitAgentDoor(door, { ok: true, channelId: resolved.id, provenance, limitApplied, messages });
|
|
156
|
+
return emitAgentDoor(door, { ok: true, ...body, channelId: resolved.id, provenance, limitApplied, messages, nextCursor: body.nextCursor, hasMore: body.hasMore, coverage: body.coverage });
|
|
136
157
|
if (provenance?.sentence)
|
|
137
158
|
writeLine(door.io.stdout, provenance.sentence);
|
|
138
159
|
if (limitApplied !== null) {
|
|
139
160
|
writeLine(door.io.stdout, `Showing the newest ${limitApplied}: one read returns at most ${limitApplied} messages.`);
|
|
140
161
|
}
|
|
162
|
+
if (body.nextCursor)
|
|
163
|
+
writeLine(door.io.stdout, `More history: --before ${body.nextCursor}`);
|
|
141
164
|
if (messages.length === 0) {
|
|
142
165
|
writeLine(door.io.stdout, "No messages.");
|
|
143
166
|
return 0;
|
|
@@ -145,7 +168,7 @@ async function readChannel(command, door) {
|
|
|
145
168
|
for (const message of [...messages].reverse()) {
|
|
146
169
|
// An imported Slack message has no Tower user; its Slack name is the label.
|
|
147
170
|
const who = message.agent_label ?? message.user_id ?? message.slack_author_label ?? "someone";
|
|
148
|
-
writeLine(door.io.stdout, `${message.created_at} ${who} ${message.content ?? ""}`);
|
|
171
|
+
writeLine(door.io.stdout, `${message.id} ${message.created_at} ${who} ${message.content ?? ""}`);
|
|
149
172
|
}
|
|
150
173
|
return 0;
|
|
151
174
|
}
|
|
@@ -159,23 +182,26 @@ async function readThread(command, door) {
|
|
|
159
182
|
if (resolved.status === "not_found")
|
|
160
183
|
return channelNotFound(door, command.channelRef);
|
|
161
184
|
const answer = await askAgentDoor(door, {
|
|
162
|
-
path: `/api/msg/channels/${encodeURIComponent(resolved.id)}/messages?thread_parent_id=${encodeURIComponent(command.threadId ?? "")}`,
|
|
185
|
+
path: `/api/msg/channels/${encodeURIComponent(resolved.id)}/messages?thread_parent_id=${encodeURIComponent(command.threadId ?? "")}${command.before ? `&before=${encodeURIComponent(command.before)}` : ""}${command.limit ? `&limit=${command.limit}` : ""}`,
|
|
163
186
|
method: "GET",
|
|
164
187
|
label: "msg thread",
|
|
165
188
|
timeoutMs: READ_DEADLINE_MS,
|
|
166
189
|
});
|
|
167
190
|
if (!answer.ok)
|
|
168
|
-
return failAgentDoor(door, TAG, answer.reason, answer.detail);
|
|
169
|
-
const
|
|
191
|
+
return failAgentDoor(door, TAG, answer.reason, answer.detail, answer.body?.coverage === "failed_partial" ? answer.body : {});
|
|
192
|
+
const body = answer.body;
|
|
193
|
+
const messages = body.messages ?? [];
|
|
170
194
|
if (door.json)
|
|
171
|
-
return emitAgentDoor(door, { ok: true, channelId: resolved.id, threadParentId: command.threadId, messages });
|
|
195
|
+
return emitAgentDoor(door, { ok: true, ...body, channelId: resolved.id, threadParentId: command.threadId, messages, nextCursor: body.nextCursor, hasMore: body.hasMore, coverage: body.coverage });
|
|
196
|
+
if (body.nextCursor)
|
|
197
|
+
writeLine(door.io.stdout, `More history: --before ${body.nextCursor}`);
|
|
172
198
|
if (messages.length === 0) {
|
|
173
199
|
writeLine(door.io.stdout, "No replies.");
|
|
174
200
|
return 0;
|
|
175
201
|
}
|
|
176
202
|
for (const message of [...messages].reverse()) {
|
|
177
203
|
const who = message.agent_label ?? message.user_id;
|
|
178
|
-
writeLine(door.io.stdout, `${message.created_at} ${who} ${message.content ?? ""}`);
|
|
204
|
+
writeLine(door.io.stdout, `${message.id} ${message.created_at} ${who} ${message.content ?? ""}`);
|
|
179
205
|
}
|
|
180
206
|
return 0;
|
|
181
207
|
}
|
|
@@ -307,4 +333,20 @@ async function openDm(command, door) {
|
|
|
307
333
|
// true either way rather than claiming a creation that may not have happened.
|
|
308
334
|
writeLine(door.io.stdout, `Direct message with ${email}: ${channel?.id ?? "?"}`);
|
|
309
335
|
return 0;
|
|
336
|
+
}
|
|
337
|
+
async function showMessage(command, door) {
|
|
338
|
+
const answer = await askAgentDoor(door, { path: `/api/msg/messages/${encodeURIComponent(command.threadId ?? "")}`, method: "GET", label: "msg show", timeoutMs: READ_DEADLINE_MS });
|
|
339
|
+
if (!answer.ok)
|
|
340
|
+
return failAgentDoor(door, TAG, answer.reason, answer.detail);
|
|
341
|
+
if (door.json)
|
|
342
|
+
return emitAgentDoor(door, { ok: true, ...answer.body });
|
|
343
|
+
const body = answer.body;
|
|
344
|
+
writeLine(door.io.stdout, body.href ?? "Message context");
|
|
345
|
+
for (const page of [body.channelPage, body.threadPage]) {
|
|
346
|
+
for (const row of [...(page?.messages ?? [])].reverse())
|
|
347
|
+
writeLine(door.io.stdout, `${row.id} ${row.created_at} ${row.content ?? ""}`);
|
|
348
|
+
if (page?.nextCursor)
|
|
349
|
+
writeLine(door.io.stdout, `More history: --before ${page.nextCursor}`);
|
|
350
|
+
}
|
|
351
|
+
return 0;
|
|
310
352
|
}
|
|
@@ -21,7 +21,7 @@ export async function runCockpitCli(argv, io) {
|
|
|
21
21
|
}
|
|
22
22
|
|
|
23
23
|
if (command === "--version" || command === "-V" || command === "version") {
|
|
24
|
-
writeLine(io?.stdout ?? process.stdout, "0.2.
|
|
24
|
+
writeLine(io?.stdout ?? process.stdout, "0.2.237");
|
|
25
25
|
return 0;
|
|
26
26
|
}
|
|
27
27
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bli-cockpit/cli",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.237",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
},
|
|
29
29
|
"dependencies": {
|
|
30
30
|
"@bli-cockpit/memory-mcp": "0.1.69",
|
|
31
|
-
"@bli-cockpit/mcp": "0.1.
|
|
31
|
+
"@bli-cockpit/mcp": "0.1.142",
|
|
32
32
|
"@bli-cockpit/telemetry-core": "0.1.76"
|
|
33
33
|
},
|
|
34
34
|
"repository": {
|