@bli-cockpit/cli 0.2.240 → 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-view-settings.js +61 -0
- package/dist/commands/cal.js +4 -0
- package/dist/commands/local-arg-values.js +90 -19
- package/dist/commands/local-args-tower-cal.js +52 -3
- package/dist/commands/local-args-tower-manager.js +128 -28
- package/dist/commands/local-help-commands-tower-ops.js +32 -18
- package/dist/commands/local-help-commands-tower.js +2 -2
- 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/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
|
}
|
|
@@ -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": {
|