@bli-cockpit/cli 0.2.239 → 0.2.241
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-delete.js +13 -3
- package/dist/commands/cal-scope.js +71 -0
- package/dist/commands/cal-view-settings.js +61 -0
- package/dist/commands/cal.js +11 -0
- package/dist/commands/docs-share.js +81 -0
- package/dist/commands/docs.js +5 -0
- package/dist/commands/drills-render.js +2 -2
- package/dist/commands/drills.js +4 -4
- package/dist/commands/issue-list-page.js +101 -0
- package/dist/commands/issue.js +23 -5
- package/dist/commands/local-arg-values.js +90 -19
- package/dist/commands/local-args-tower-cal.js +148 -8
- package/dist/commands/local-args-tower-docs-msg.js +30 -3
- package/dist/commands/local-args-tower-drills.js +1 -1
- package/dist/commands/local-args-tower-manager.js +128 -28
- package/dist/commands/local-args-tower-msg.js +43 -6
- package/dist/commands/local-args-tower-work.js +74 -6
- package/dist/commands/local-help-commands-tower-ops.js +35 -21
- package/dist/commands/local-help-commands-tower.js +15 -11
- package/dist/commands/local-help.js +1 -1
- package/dist/commands/manager-fields-file.js +208 -0
- package/dist/commands/manager-hints.js +88 -0
- package/dist/commands/manager-limits.json +269 -0
- package/dist/commands/manager-read-reasons.js +78 -0
- package/dist/commands/manager-render-home.js +213 -37
- package/dist/commands/manager-render.js +119 -21
- package/dist/commands/manager-requests.js +7 -3
- package/dist/commands/manager.js +67 -43
- package/dist/commands/msg-write.js +118 -0
- package/dist/commands/msg.js +20 -29
- package/dist/commands/project.js +15 -3
- package/dist/commands/public-root.js +1 -1
- package/package.json +2 -2
package/dist/commands/manager.js
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* them, adds and edits them, ticks them off, links an action to the meeting it came from, and
|
|
6
6
|
* decides the AI's suggestions, the same things the manager page does.
|
|
7
7
|
*
|
|
8
|
-
*
|
|
8
|
+
* Nineteen verbs (`home` and `summary`, BLI-5698: a client's Home and its "Where we are" card), each one `/api/manager` door on this machine's device token, the same door the MCP
|
|
9
9
|
* twins (`manager_*`) use; the door passes Railway's answer through unchanged. Every write on an
|
|
10
10
|
* existing item carries `--version`, the item's `updatedAt` as `list`/`show` print it: a stale one is
|
|
11
11
|
* refused with the door's own sentence and nothing is written. `approve`, `dismiss` and `proposal`
|
|
@@ -16,8 +16,11 @@ import { randomUUID } from "node:crypto";
|
|
|
16
16
|
import { askAgentDoor, emitAgentDoor, failAgentDoor, openAgentDoor } from "./agent-door.js";
|
|
17
17
|
import { colorEnabled, writeLine } from "./cli-io.js";
|
|
18
18
|
import { localCommandHelp } from "./local-help.js";
|
|
19
|
-
import { clientDetailLines, clientLines, itemDetailLines, itemRowLines, runLines, listLines, queueLines, } from "./manager-render.js";
|
|
19
|
+
import { savedTextLines, clientDetailLines, clientLines, itemDetailLines, itemRowLines, runLines, listLines, queueLines, } from "./manager-render.js";
|
|
20
20
|
import { homeLines, summaryWrittenLines } from "./manager-render-home.js";
|
|
21
|
+
import { parseManagerArgs } from "./local-args-tower-manager.js";
|
|
22
|
+
import { READ_ONLY_HINTS, RESEEN_REFUSALS, itemHints, summaryHints } from "./manager-hints.js";
|
|
23
|
+
import { readFieldsText } from "./manager-fields-file.js";
|
|
21
24
|
import { planManagerRequest } from "./manager-requests.js";
|
|
22
25
|
const TAG = "[manager cli]";
|
|
23
26
|
/**
|
|
@@ -26,12 +29,38 @@ const TAG = "[manager cli]";
|
|
|
26
29
|
* giving up first while the write may still land.
|
|
27
30
|
*/
|
|
28
31
|
export const MANAGER_DEADLINE_MS = 65_000;
|
|
29
|
-
const staleHint = (id, timeline) => `Run \`cockpit manager show ${id}${timeline ? " --timeline" : ""}\` for the current version and try again.`;
|
|
30
|
-
/** Refusals that mean "the item is not as you last saw it": reading it again is the way forward. */
|
|
31
32
|
/** The verbs that change an existing item. */
|
|
32
|
-
const WRITES = new Set(["summary", "edit", "done", "reopen", "link", "approve", "dismiss", "proposal", "archive", "unarchive"]);
|
|
33
|
-
|
|
34
|
-
|
|
33
|
+
const WRITES = new Set(["summary", "edit", "done", "drop", "reopen", "link", "approve", "dismiss", "proposal", "archive", "unarchive"]);
|
|
34
|
+
/**
|
|
35
|
+
* `--fields-file`: the parser left the fields unread. Read the file (or stdin), and parse the same args again with its text,
|
|
36
|
+
* so the file's values get exactly the flags' checks. Nothing has touched the network yet, so a bad file costs nothing.
|
|
37
|
+
*/
|
|
38
|
+
async function withFieldsFile(command, io) {
|
|
39
|
+
if (command.fieldsFile === undefined || command.rawArgs === undefined)
|
|
40
|
+
return command;
|
|
41
|
+
const source = command.fieldsFile === "-" ? "stdin" : "file";
|
|
42
|
+
try {
|
|
43
|
+
const text = await readFieldsText(command.fieldsFile, io);
|
|
44
|
+
const parsed = parseManagerArgs(command.rawArgs, { fieldsText: text });
|
|
45
|
+
writeLine(io.stderr, `${TAG} fields read ${JSON.stringify({ source, bytes: Buffer.byteLength(text, "utf8"), verb: command.action })}`);
|
|
46
|
+
const { fieldsFile: _file, rawArgs: _args, ...rest } = parsed;
|
|
47
|
+
return { ...rest, homeDir: command.homeDir, dashboardUrl: command.dashboardUrl, json: command.json };
|
|
48
|
+
}
|
|
49
|
+
catch (error) {
|
|
50
|
+
const detail = error instanceof Error ? error.message : "the fields file could not be read";
|
|
51
|
+
writeLine(io.stderr, `${TAG} refused ${JSON.stringify({ reason: "fields_file_invalid", source, verb: command.action })}`);
|
|
52
|
+
if (command.json)
|
|
53
|
+
writeLine(io.stdout, JSON.stringify({ ok: false, error: "fields_file_invalid", detail }));
|
|
54
|
+
else
|
|
55
|
+
writeLine(io.stderr, detail);
|
|
56
|
+
return 1;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
export async function runManager(given, io) {
|
|
60
|
+
const loaded = await withFieldsFile(given, io);
|
|
61
|
+
if (typeof loaded === "number")
|
|
62
|
+
return loaded;
|
|
63
|
+
const command = loaded;
|
|
35
64
|
if (command.help) {
|
|
36
65
|
writeLine(io.stderr, `${TAG} help printed ${JSON.stringify({ reason: "no_verb_or_help_asked" })}`);
|
|
37
66
|
writeLine(io.stdout, localCommandHelp("manager"));
|
|
@@ -107,12 +136,14 @@ function renderAnswer(command, body, color) {
|
|
|
107
136
|
case "home": return homeLines(body, command.client ?? "<client>", color);
|
|
108
137
|
case "summary": return summaryWrittenLines(body, color);
|
|
109
138
|
case "run": return runLines(body);
|
|
110
|
-
case "list": return listLines(body, timeline, color);
|
|
139
|
+
case "list": return listLines(body, timeline, color, new Date(), command.archived !== true && command.review !== "rejected", command.ownerFilter !== undefined || command.noOwner === true);
|
|
111
140
|
case "queue": return queueLines(body, color);
|
|
112
141
|
case "show": return itemDetailLines(body, timeline, color);
|
|
113
|
-
default: return [...writeSentence(command, body), ...itemRowLines(body.item, timeline, color)];
|
|
142
|
+
default: return [...writeSentence(command, body), ...itemRowLines(body.item, timeline, color), ...(command.action === "add" ? savedTextLines(body) : [])];
|
|
114
143
|
}
|
|
115
144
|
}
|
|
145
|
+
/** An add answered `created: false`: the same request id came back, so the item that already existed is returned. */
|
|
146
|
+
const ALREADY_ADDED = "Already added earlier under this request id; nothing new was written.";
|
|
116
147
|
/** What a write did, in one sentence a person reads; the item's new version follows on its row. */
|
|
117
148
|
function writeSentence(command, body) {
|
|
118
149
|
const as = "as you";
|
|
@@ -120,10 +151,14 @@ function writeSentence(command, body) {
|
|
|
120
151
|
const waiting = body.item?.proposedChanges?.length ?? 0;
|
|
121
152
|
const queue = waiting === 0 ? "It leaves the review queue." : `${waiting === 1 ? "Its other suggested change still waits" : `Its ${waiting} other suggested changes still wait`} in the review queue.`;
|
|
122
153
|
switch (command.action) {
|
|
123
|
-
|
|
124
|
-
case "
|
|
154
|
+
// Only an explicit `created: false` is "not created": an older server sends no field, and that add did write.
|
|
155
|
+
case "add": return body.created === false ? [ALREADY_ADDED] : [command.timeline && !command.occurredAt ? "Added. It has no date, so it sits in \"Needs a date\"." : "Added."];
|
|
156
|
+
// Railway writes nothing when every field already equals the stored one: no new version, no reviewed mark.
|
|
157
|
+
case "edit": return [body.unchanged === true ? "No change; nothing was written." : "Saved your changes."];
|
|
125
158
|
// Railway writes nothing when the action already has that status, so its version comes back unchanged.
|
|
126
159
|
case "done": return [body.item?.updatedAt === command.version ? "Already done; nothing changed." : "Marked done."];
|
|
160
|
+
// A dropped action was not done. Railway writes nothing when it is already dropped (`unchanged: true`).
|
|
161
|
+
case "drop": return [body.unchanged === true || body.item?.updatedAt === command.version ? "No change; nothing was written." : `Dropped: ${body.item?.title ?? "that action"}.`];
|
|
127
162
|
case "reopen": return [body.item?.updatedAt === command.version ? "Already open; nothing changed." : "Reopened."];
|
|
128
163
|
case "link": return [command.timelineItemId ? `Linked to ${body.timelineItem?.title ?? "that meeting"}.` : "Unlinked from its meeting."];
|
|
129
164
|
case "approve": return [command.restore ? `Restored: ${body.item?.title ?? "that item"}, back in the lists and marked reviewed by you.` : `Approved ${as}. ${queue.replace("other ", "")}`];
|
|
@@ -170,7 +205,7 @@ function nextStep(reason, refusal, command) {
|
|
|
170
205
|
if (reason === "not_found" && command?.id) {
|
|
171
206
|
return command.timeline
|
|
172
207
|
? " Check the id with `cockpit manager list <client> --timeline`; for an action, drop --timeline."
|
|
173
|
-
: ` Check the id with \`cockpit manager list <client>\`; for a timeline item, add --timeline${command.action === "done" || command.action === "reopen" || command.action === "link" ? " (done, reopen and link are for actions only)" : ""}.`;
|
|
208
|
+
: ` Check the id with \`cockpit manager list <client>\`; for a timeline item, add --timeline${command.action === "done" || command.action === "drop" || command.action === "reopen" || command.action === "link" ? " (done, drop, reopen and link are for actions only)" : ""}.`;
|
|
174
209
|
}
|
|
175
210
|
// A client-level command (home, summary, client, list, queue, add) the door cannot find the client of: "That client does not exist".
|
|
176
211
|
if (reason === "not_found" && !command?.id && command?.action !== "run")
|
|
@@ -183,26 +218,30 @@ function nextStep(reason, refusal, command) {
|
|
|
183
218
|
if ((reason === "manager_unreachable" || reason === "manager_timeout") && (command === undefined || !WRITES.has(command.action) && command.action !== "add")) {
|
|
184
219
|
return " Try again in a minute; if it keeps failing, Tower's manager server may be down.";
|
|
185
220
|
}
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
case "person_item_not_dismissable": return ` To delete an item a person added, use \`cockpit manager archive ${id}${flag} --version <v>\`.`;
|
|
193
|
-
case "timeline_not_live": return " Link to a live timeline item of the same client: `cockpit manager list <client> --timeline` lists them.";
|
|
194
|
-
case "item_not_archived": return " It is already in the lists; nothing to bring back.";
|
|
195
|
-
default: return "";
|
|
196
|
-
}
|
|
221
|
+
// The door's own 403 for a read-only key names itself as the reason.
|
|
222
|
+
const readOnly = READ_ONLY_HINTS[reason];
|
|
223
|
+
if (readOnly)
|
|
224
|
+
return readOnly;
|
|
225
|
+
const hint = refusal ? itemHints(id, flag)[refusal] : undefined;
|
|
226
|
+
return hint ? ` ${hint}` : "";
|
|
197
227
|
}
|
|
198
228
|
function refuse(door, answer, command, requestId) {
|
|
199
229
|
const body = answer.body && typeof answer.body === "object" ? answer.body : {};
|
|
200
230
|
const text = (value) => (typeof value === "string" && value.trim() !== "" ? value : undefined);
|
|
201
|
-
const sentence = text(body["message"]) ?? (text(body["reason"]) ? text(body["error"]) : undefined) ?? answer.detail;
|
|
202
231
|
const refusal = text(body["refusal"]);
|
|
232
|
+
// `--version none` says "this client has no card yet": a card that exists is the only way the version check can refuse it, and
|
|
233
|
+
// the server's own sentence ("Someone changed this card after you opened it") would send the person looking for a change nobody made.
|
|
234
|
+
const cardExists = refusal === "summary_changed" && command?.action === "summary" && command.version === null;
|
|
235
|
+
// An older Tower server's status door takes only done and open: its 400 for "dropped" is a schema message, not a way forward.
|
|
236
|
+
const noDrop = command?.action === "drop" && answer.httpStatus === 400 && refusal === undefined;
|
|
237
|
+
const sentence = noDrop
|
|
238
|
+
? "This Tower server can't drop actions yet; mark it done with a dated line saying why, or ask for a server update."
|
|
239
|
+
: cardExists
|
|
240
|
+
? `This client already has a card: pass its version (\`cockpit manager home ${clientWord(command)}\` shows it).`
|
|
241
|
+
: text(body["message"]) ?? (text(body["reason"]) ? text(body["error"]) : undefined) ?? answer.detail;
|
|
203
242
|
const reseen = answer.reason === "invalid_state" && (refusal === undefined || RESEEN_REFUSALS.has(refusal));
|
|
204
|
-
const hint = reseen && command?.id
|
|
205
|
-
? ` ${
|
|
243
|
+
const hint = cardExists || noDrop ? "" : reseen && command?.id
|
|
244
|
+
? ` ${itemHints(command.id, command.timeline ? " --timeline" : "").item_changed}`
|
|
206
245
|
: nextStep(answer.reason, refusal, command);
|
|
207
246
|
// An add with no answer from Tower (or a server error) may or may not have been made: the same request id makes the retry safe.
|
|
208
247
|
const unconfirmed = requestId !== undefined && (answer.httpStatus === undefined || answer.httpStatus >= 500);
|
|
@@ -222,24 +261,9 @@ function refuse(door, answer, command, requestId) {
|
|
|
222
261
|
return failAgentDoor(door, TAG, answer.reason, `${sentence}${stop}${hint}${retry}`, { ...(refusal ? { refusal } : {}), ...(unconfirmed ? { requestId } : {}) });
|
|
223
262
|
}
|
|
224
263
|
const clientWord = (command) => (command.client && /\s/.test(command.client) ? `"${command.client}"` : command.client ?? "<client>");
|
|
225
|
-
/** The way forward after each named refusal of a "Where we are" edit (`TOWER_MANAGER_SUMMARY_REFUSALS`). */
|
|
264
|
+
/** The way forward after each named refusal of a "Where we are" edit (`TOWER_MANAGER_SUMMARY_REFUSALS`, `manager-hints.ts`). */
|
|
226
265
|
function summaryHint(refusal, command) {
|
|
227
266
|
const home = `\`cockpit manager home ${clientWord(command)} --json\``;
|
|
228
|
-
const
|
|
229
|
-
summary_changed: `Someone or the check wrote the card since: read it again with ${home} and retry with its summary.updatedAt as --version (none only when it has no card).`,
|
|
230
|
-
summary_empty: "Give at least one of --health, --headline, --bullet, --next, --blocker.",
|
|
231
|
-
summary_incomplete: "This client has no card yet: give --health, --headline and at least one --bullet together (with --version none).",
|
|
232
|
-
summary_health_invalid: "--health is on_track, at_risk or blocked.",
|
|
233
|
-
summary_headline_empty: "Give the headline some words.",
|
|
234
|
-
summary_headline_too_long: "Keep --headline to 140 characters.",
|
|
235
|
-
summary_bullets_count: "Give 1 to 5 --bullet flags; together they replace the whole list.",
|
|
236
|
-
summary_bullet_empty: "Every --bullet needs words (after its date, when it has one).",
|
|
237
|
-
summary_bullet_too_long: "Keep each --bullet to 160 characters.",
|
|
238
|
-
summary_bullet_date_invalid: "Start a dated bullet with a real day, \"YYYY-MM-DD: \", or leave the date out.",
|
|
239
|
-
summary_next_too_long: "Keep --next to 200 characters.",
|
|
240
|
-
summary_blocker_too_long: "Keep --blocker to 200 characters.",
|
|
241
|
-
summary_blocked_needs_blocker: "blocked needs a blocker: add --blocker <what blocks it>, or pick another --health.",
|
|
242
|
-
};
|
|
243
|
-
const hint = hints[refusal];
|
|
267
|
+
const hint = summaryHints(home)[refusal];
|
|
244
268
|
return hint ? ` ${hint}` : ` Read the card with ${home} and try again.`;
|
|
245
269
|
}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The message WRITE verbs of `cockpit msg` (BLI-21314): `edit`, `delete`, `outcome`, plus what `send`
|
|
3
|
+
* shares with them (the piped body, the one deleted sentence, the refusal that says how to check a write).
|
|
4
|
+
* Split out of `msg.ts` to keep it under the file-size ratchet. Same doors the browser calls.
|
|
5
|
+
*/
|
|
6
|
+
import { randomUUID } from "node:crypto";
|
|
7
|
+
import { askAgentDoor, emitAgentDoor, failAgentDoor } from "./agent-door.js";
|
|
8
|
+
import { isInteractiveStdin, readPipedText, writeLine } from "./cli-io.js";
|
|
9
|
+
const TAG = "[msg cli]";
|
|
10
|
+
const READ_DEADLINE_MS = 30_000;
|
|
11
|
+
const WRITE_DEADLINE_MS = 30_000;
|
|
12
|
+
const CONTENT_MAX_CHARS = 8_000;
|
|
13
|
+
/** The one sentence a deleted message reads as, on every surface. */
|
|
14
|
+
export const DELETED_SENTENCE = "This message was deleted";
|
|
15
|
+
export function messageText(message) {
|
|
16
|
+
return message.deleted_at ? `(${DELETED_SENTENCE})` : (message.content ?? "");
|
|
17
|
+
}
|
|
18
|
+
export async function readContent(io) {
|
|
19
|
+
if (isInteractiveStdin(io)) {
|
|
20
|
+
return {
|
|
21
|
+
ok: false,
|
|
22
|
+
reason: "nothing_piped",
|
|
23
|
+
detail: "Pipe the message in, e.g. `echo \"hello\" | cockpit msg send general`. A message never travels on the command line.",
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
try {
|
|
27
|
+
const text = await readPipedText(io.stdin, {
|
|
28
|
+
maxChars: CONTENT_MAX_CHARS,
|
|
29
|
+
overflowMessage: `A message is limited to ${CONTENT_MAX_CHARS} characters.`,
|
|
30
|
+
});
|
|
31
|
+
return { ok: true, text: text.trim() };
|
|
32
|
+
}
|
|
33
|
+
catch (error) {
|
|
34
|
+
return { ok: false, reason: "content_too_long", detail: error instanceof Error ? error.message : String(error) };
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Says where the write stands, then fails with the door's own reason. A conflict names the current revision;
|
|
39
|
+
* an unknown result says how to resolve it by request id before retrying.
|
|
40
|
+
*/
|
|
41
|
+
export function refuseMsgWrite(door, answer, requestId, verb) {
|
|
42
|
+
const mutation = answer.body?.mutation;
|
|
43
|
+
writeLine(door.io.stderr, `${TAG} check the ${verb} with cockpit msg outcome ${requestId}; retry unchanged input with --request-id ${requestId}.`);
|
|
44
|
+
if (answer.reason === "revision_conflict") {
|
|
45
|
+
writeLine(door.io.stderr, `${TAG} conflict ${JSON.stringify({ request_id: requestId, current_revision: mutation?.current?.revision ?? mutation?.revision ?? null })}`);
|
|
46
|
+
}
|
|
47
|
+
return failAgentDoor(door, TAG, answer.reason, answer.detail, mutation ? { mutation } : {});
|
|
48
|
+
}
|
|
49
|
+
/** `cockpit msg edit <message-id> [--expected-revision <n>] [--request-id <uuid>]` — the new text is piped in. */
|
|
50
|
+
export async function editMessage(command, door) {
|
|
51
|
+
const content = await readContent(door.io);
|
|
52
|
+
if (!content.ok)
|
|
53
|
+
return failAgentDoor(door, TAG, content.reason, content.detail);
|
|
54
|
+
if (content.text === "")
|
|
55
|
+
return failAgentDoor(door, TAG, "invalid_body", "There was nothing to save. Nothing was changed.");
|
|
56
|
+
const requestId = command.requestId ?? randomUUID();
|
|
57
|
+
const answer = await askAgentDoor(door, {
|
|
58
|
+
path: `/api/msg/messages/${encodeURIComponent(command.targetId ?? "")}`,
|
|
59
|
+
method: "PATCH",
|
|
60
|
+
label: "msg edit",
|
|
61
|
+
timeoutMs: WRITE_DEADLINE_MS,
|
|
62
|
+
body: {
|
|
63
|
+
content: content.text,
|
|
64
|
+
request_id: requestId,
|
|
65
|
+
...(command.expectedRevision === undefined ? {} : { expected_revision: command.expectedRevision }),
|
|
66
|
+
},
|
|
67
|
+
});
|
|
68
|
+
if (!answer.ok)
|
|
69
|
+
return refuseMsgWrite(door, answer, requestId, "edit");
|
|
70
|
+
const { message = null, mutation = null } = answer.body;
|
|
71
|
+
writeLine(door.io.stderr, `${TAG} edited ${JSON.stringify({ message_id: command.targetId, request_id: requestId, revision: mutation?.["revision"] ?? null })}`);
|
|
72
|
+
if (door.json)
|
|
73
|
+
return emitAgentDoor(door, { ok: true, message, mutation });
|
|
74
|
+
writeLine(door.io.stdout, `Edited ${message?.id ?? command.targetId}, revision ${String(mutation?.["revision"] ?? message?.revision ?? "?")}.`);
|
|
75
|
+
return 0;
|
|
76
|
+
}
|
|
77
|
+
/** `cockpit msg delete <message-id> [--expected-revision <n>] [--request-id <uuid>]` — a tombstone: the thread stays, the text goes. */
|
|
78
|
+
export async function deleteMessage(command, door) {
|
|
79
|
+
const requestId = command.requestId ?? randomUUID();
|
|
80
|
+
const answer = await askAgentDoor(door, {
|
|
81
|
+
path: `/api/msg/messages/${encodeURIComponent(command.targetId ?? "")}`,
|
|
82
|
+
method: "DELETE",
|
|
83
|
+
label: "msg delete",
|
|
84
|
+
timeoutMs: WRITE_DEADLINE_MS,
|
|
85
|
+
body: {
|
|
86
|
+
request_id: requestId,
|
|
87
|
+
...(command.expectedRevision === undefined ? {} : { expected_revision: command.expectedRevision }),
|
|
88
|
+
},
|
|
89
|
+
});
|
|
90
|
+
if (!answer.ok)
|
|
91
|
+
return refuseMsgWrite(door, answer, requestId, "delete");
|
|
92
|
+
const { message = null, mutation = null } = answer.body;
|
|
93
|
+
writeLine(door.io.stderr, `${TAG} deleted ${JSON.stringify({ message_id: command.targetId, request_id: requestId, reason: mutation?.["reason"] ?? "ok" })}`);
|
|
94
|
+
if (door.json)
|
|
95
|
+
return emitAgentDoor(door, { ok: true, message, mutation });
|
|
96
|
+
writeLine(door.io.stdout, `Deleted ${command.targetId} (${DELETED_SENTENCE.toLowerCase()}; its thread and replies stay).`);
|
|
97
|
+
return 0;
|
|
98
|
+
}
|
|
99
|
+
/** `cockpit msg outcome <request-uuid>` — your own write receipt. No receipt is `unknown_check_again`, never "nothing changed". */
|
|
100
|
+
export async function messageOutcome(command, door) {
|
|
101
|
+
const answer = await askAgentDoor(door, {
|
|
102
|
+
path: `/api/msg/mutations/${encodeURIComponent(command.targetId ?? "")}`,
|
|
103
|
+
method: "GET",
|
|
104
|
+
label: "msg outcome",
|
|
105
|
+
timeoutMs: READ_DEADLINE_MS,
|
|
106
|
+
});
|
|
107
|
+
if (!answer.ok)
|
|
108
|
+
return failAgentDoor(door, TAG, answer.reason, answer.detail);
|
|
109
|
+
const mutation = answer.body.mutation ?? {};
|
|
110
|
+
writeLine(door.io.stderr, `${TAG} outcome ${JSON.stringify({ request_id: command.targetId, outcome: mutation["outcome"] ?? null })}`);
|
|
111
|
+
if (door.json)
|
|
112
|
+
return emitAgentDoor(door, { ok: true, mutation });
|
|
113
|
+
const outcome = String(mutation["outcome"] ?? "unknown_check_again");
|
|
114
|
+
writeLine(door.io.stdout, outcome === "unknown_check_again"
|
|
115
|
+
? "Unknown: Tower has no receipt for that request yet. It may still be in flight; check again before retrying, and retry with the same --request-id."
|
|
116
|
+
: `${outcome}${mutation["reason"] ? ` (${String(mutation["reason"])})` : ""}${mutation["message_id"] ? `, message ${String(mutation["message_id"])}` : ""}.`);
|
|
117
|
+
return 0;
|
|
118
|
+
}
|
package/dist/commands/msg.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* `cockpit msg` — channels and messages, typed (BLI-3706, BLI-3749).
|
|
3
3
|
*
|
|
4
|
-
*
|
|
4
|
+
* Eleven verbs over `/api/msg/**` (BLI-3654 Wave 1a), which already accepts the
|
|
5
5
|
* collector device token through `resolveCaller({ allowDeviceToken: true })`
|
|
6
6
|
* on every route — no server-side door change was needed for this terminal.
|
|
7
7
|
*
|
|
@@ -15,14 +15,15 @@
|
|
|
15
15
|
* silently adding a stranger. `dm` does not have to name the caller — the
|
|
16
16
|
* door unions them in.
|
|
17
17
|
*/
|
|
18
|
+
import { randomUUID } from "node:crypto";
|
|
18
19
|
import { askAgentDoor, emitAgentDoor, failAgentDoor, openAgentDoor, } from "./agent-door.js";
|
|
19
|
-
import {
|
|
20
|
+
import { writeLine } from "./cli-io.js";
|
|
20
21
|
import { cacheAgeLabel, cacheCoverage, readThroughCache } from "./read-cache.js";
|
|
21
22
|
import { markReadThrough } from "./msg-read-line.js";
|
|
23
|
+
import { deleteMessage, editMessage, messageOutcome, messageText, readContent, refuseMsgWrite } from "./msg-write.js";
|
|
22
24
|
const TAG = "[msg cli]";
|
|
23
25
|
const READ_DEADLINE_MS = 30_000;
|
|
24
26
|
const WRITE_DEADLINE_MS = 30_000;
|
|
25
|
-
const CONTENT_MAX_CHARS = 8_000;
|
|
26
27
|
export async function runMsg(command, io) {
|
|
27
28
|
const door = await openAgentDoor("msg", command, io);
|
|
28
29
|
switch (command.action) {
|
|
@@ -36,6 +37,12 @@ export async function runMsg(command, io) {
|
|
|
36
37
|
return markUnread(command, door);
|
|
37
38
|
case "send":
|
|
38
39
|
return sendMessage(command, door);
|
|
40
|
+
case "edit":
|
|
41
|
+
return editMessage(command, door);
|
|
42
|
+
case "delete":
|
|
43
|
+
return deleteMessage(command, door);
|
|
44
|
+
case "outcome":
|
|
45
|
+
return messageOutcome(command, door);
|
|
39
46
|
case "thread":
|
|
40
47
|
return readThread(command, door);
|
|
41
48
|
case "create":
|
|
@@ -180,7 +187,7 @@ async function readChannel(command, door) {
|
|
|
180
187
|
for (const message of [...messages].reverse()) {
|
|
181
188
|
// An imported Slack message has no Tower user; its Slack name is the label.
|
|
182
189
|
const who = message.agent_label ?? message.user_id ?? message.slack_author_label ?? "someone";
|
|
183
|
-
writeLine(door.io.stdout, `${message.id} ${message.created_at} ${who} ${message
|
|
190
|
+
writeLine(door.io.stdout, `${message.id} ${message.created_at} ${who} ${messageText(message)}`);
|
|
184
191
|
}
|
|
185
192
|
if (readMove) {
|
|
186
193
|
writeLine(readMove.ok ? door.io.stdout : door.io.stderr, readMove.ok
|
|
@@ -243,29 +250,10 @@ async function readThread(command, door) {
|
|
|
243
250
|
}
|
|
244
251
|
for (const message of [...messages].reverse()) {
|
|
245
252
|
const who = message.agent_label ?? message.user_id;
|
|
246
|
-
writeLine(door.io.stdout, `${message.id} ${message.created_at} ${who} ${message
|
|
253
|
+
writeLine(door.io.stdout, `${message.id} ${message.created_at} ${who} ${messageText(message)}`);
|
|
247
254
|
}
|
|
248
255
|
return 0;
|
|
249
256
|
}
|
|
250
|
-
async function readContent(io) {
|
|
251
|
-
if (isInteractiveStdin(io)) {
|
|
252
|
-
return {
|
|
253
|
-
ok: false,
|
|
254
|
-
reason: "nothing_piped",
|
|
255
|
-
detail: "Pipe the message in, e.g. `echo \"hello\" | cockpit msg send general`. A message never travels on the command line.",
|
|
256
|
-
};
|
|
257
|
-
}
|
|
258
|
-
try {
|
|
259
|
-
const text = await readPipedText(io.stdin, {
|
|
260
|
-
maxChars: CONTENT_MAX_CHARS,
|
|
261
|
-
overflowMessage: `A message is limited to ${CONTENT_MAX_CHARS} characters.`,
|
|
262
|
-
});
|
|
263
|
-
return { ok: true, text: text.trim() };
|
|
264
|
-
}
|
|
265
|
-
catch (error) {
|
|
266
|
-
return { ok: false, reason: "content_too_long", detail: error instanceof Error ? error.message : String(error) };
|
|
267
|
-
}
|
|
268
|
-
}
|
|
269
257
|
async function sendMessage(command, door) {
|
|
270
258
|
const ref = command.channelRef ?? "";
|
|
271
259
|
const resolved = await resolveChannelId(door, ref);
|
|
@@ -278,6 +266,8 @@ async function sendMessage(command, door) {
|
|
|
278
266
|
return failAgentDoor(door, TAG, content.reason, content.detail);
|
|
279
267
|
if (content.text === "")
|
|
280
268
|
return failAgentDoor(door, TAG, "invalid_body", "There was nothing to send. Nothing was sent.");
|
|
269
|
+
// BLI-21314: every send carries a request id, so a lost response can be checked and a retry posts once.
|
|
270
|
+
const requestId = command.requestId ?? randomUUID();
|
|
281
271
|
const answer = await askAgentDoor(door, {
|
|
282
272
|
path: `/api/msg/channels/${encodeURIComponent(resolved.id)}/messages`,
|
|
283
273
|
method: "POST",
|
|
@@ -285,15 +275,16 @@ async function sendMessage(command, door) {
|
|
|
285
275
|
timeoutMs: WRITE_DEADLINE_MS,
|
|
286
276
|
body: {
|
|
287
277
|
content: content.text,
|
|
278
|
+
request_id: requestId,
|
|
288
279
|
...(command.threadId ? { thread_parent_id: command.threadId } : {}),
|
|
289
280
|
},
|
|
290
281
|
});
|
|
291
282
|
if (!answer.ok)
|
|
292
|
-
return
|
|
293
|
-
const message = answer.body
|
|
294
|
-
writeLine(door.io.stderr, `${TAG} sent ${JSON.stringify({ channel_id: resolved.id, message_id: message?.id ?? null, chars: content.text.length })}`);
|
|
283
|
+
return refuseMsgWrite(door, answer, requestId, "send");
|
|
284
|
+
const { message = null, mutation = null } = answer.body;
|
|
285
|
+
writeLine(door.io.stderr, `${TAG} sent ${JSON.stringify({ channel_id: resolved.id, message_id: message?.id ?? null, request_id: requestId, chars: content.text.length })}`);
|
|
295
286
|
if (door.json)
|
|
296
|
-
return emitAgentDoor(door, { ok: true, message });
|
|
287
|
+
return emitAgentDoor(door, { ok: true, message, mutation });
|
|
297
288
|
writeLine(door.io.stdout, `Sent to ${ref} (${message?.id ?? "?"}).`);
|
|
298
289
|
return 0;
|
|
299
290
|
}
|
|
@@ -386,7 +377,7 @@ async function showMessage(command, door) {
|
|
|
386
377
|
writeLine(door.io.stdout, body.href ?? "Message context");
|
|
387
378
|
for (const page of [body.channelPage, body.threadPage]) {
|
|
388
379
|
for (const row of [...(page?.messages ?? [])].reverse())
|
|
389
|
-
writeLine(door.io.stdout, `${row.id} ${row.created_at} ${row
|
|
380
|
+
writeLine(door.io.stdout, `${row.id} ${row.created_at} ${messageText(row)}`);
|
|
390
381
|
if (page?.nextCursor)
|
|
391
382
|
writeLine(door.io.stdout, `More history: --before ${page.nextCursor}`);
|
|
392
383
|
}
|
package/dist/commands/project.js
CHANGED
|
@@ -10,21 +10,30 @@
|
|
|
10
10
|
*/
|
|
11
11
|
import { askAgentDoor, emitAgentDoor, failAgentDoor, openAgentDoor } from "./agent-door.js";
|
|
12
12
|
import { writeLine } from "./cli-io.js";
|
|
13
|
+
import { pageFieldsOf, pageFooter } from "./issue-list-page.js";
|
|
13
14
|
const TAG = "[project cli]";
|
|
14
15
|
const READ_DEADLINE_MS = 30_000;
|
|
15
16
|
export async function runProject(command, io) {
|
|
16
17
|
const door = await openAgentDoor("project", command, io);
|
|
18
|
+
const params = new URLSearchParams();
|
|
19
|
+
if (command.includeArchived)
|
|
20
|
+
params.set("include_archived", "true");
|
|
21
|
+
if (command.limit !== undefined)
|
|
22
|
+
params.set("limit", String(command.limit));
|
|
23
|
+
if (command.cursor)
|
|
24
|
+
params.set("cursor", command.cursor);
|
|
17
25
|
const answer = await askAgentDoor(door, {
|
|
18
|
-
path: `/api/work/projects${
|
|
26
|
+
path: `/api/work/projects${params.toString() ? `?${params.toString()}` : ""}`,
|
|
19
27
|
method: "GET",
|
|
20
28
|
label: "project list",
|
|
21
29
|
timeoutMs: READ_DEADLINE_MS,
|
|
22
30
|
});
|
|
23
31
|
if (!answer.ok)
|
|
24
32
|
return failAgentDoor(door, TAG, answer.reason, answer.detail);
|
|
25
|
-
const
|
|
33
|
+
const body = answer.body;
|
|
34
|
+
const projects = body.projects ?? [];
|
|
26
35
|
if (door.json)
|
|
27
|
-
return emitAgentDoor(door, { ok: true, projects });
|
|
36
|
+
return emitAgentDoor(door, { ok: true, projects, ...pageFieldsOf(body) });
|
|
28
37
|
if (projects.length === 0) {
|
|
29
38
|
writeLine(door.io.stdout, "No projects.");
|
|
30
39
|
return 0;
|
|
@@ -34,5 +43,8 @@ export async function runProject(command, io) {
|
|
|
34
43
|
}
|
|
35
44
|
writeLine(door.io.stdout, "");
|
|
36
45
|
writeLine(door.io.stdout, `${projects.length} project(s).`);
|
|
46
|
+
const footer = pageFooter(body, "projects");
|
|
47
|
+
if (footer)
|
|
48
|
+
writeLine(door.io.stdout, footer);
|
|
37
49
|
return 0;
|
|
38
50
|
}
|
|
@@ -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.241");
|
|
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.241",
|
|
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.70",
|
|
31
|
-
"@bli-cockpit/mcp": "0.1.
|
|
31
|
+
"@bli-cockpit/mcp": "0.1.146",
|
|
32
32
|
"@bli-cockpit/telemetry-core": "0.1.77"
|
|
33
33
|
},
|
|
34
34
|
"repository": {
|