@bli-cockpit/cli 0.2.164 → 0.2.166
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/local-args-tower-admin.js +3 -0
- package/dist/commands/local-args-tower-mail.js +18 -8
- package/dist/commands/local-help-commands-tower-ops.js +3 -1
- package/dist/commands/local-help-commands-tower.js +3 -2
- package/dist/commands/local-help.js +1 -1
- package/dist/commands/mail-compose.js +96 -0
- package/dist/commands/mail.js +8 -34
- package/dist/commands/ops.js +23 -2
- package/dist/commands/public-root.js +1 -1
- package/package.json +2 -2
|
@@ -91,6 +91,8 @@ export function parseOpsArgs(args) {
|
|
|
91
91
|
"--models",
|
|
92
92
|
"--turns",
|
|
93
93
|
"--tool-router",
|
|
94
|
+
// BLI-12507: the MCP's `people: true`. Per-device and per-person rows in --json.
|
|
95
|
+
"--people",
|
|
94
96
|
"--person",
|
|
95
97
|
"--dry-run",
|
|
96
98
|
"--json",
|
|
@@ -134,6 +136,7 @@ export function parseOpsArgs(args) {
|
|
|
134
136
|
models: values.booleans.has("--models"),
|
|
135
137
|
turns: values.booleans.has("--turns"),
|
|
136
138
|
toolRouter: values.booleans.has("--tool-router"),
|
|
139
|
+
people: values.booleans.has("--people"),
|
|
137
140
|
...(memoryDays === undefined ? {} : { memoryDays: Number(memoryDays) }),
|
|
138
141
|
...base,
|
|
139
142
|
};
|
|
@@ -14,9 +14,14 @@
|
|
|
14
14
|
* captured by this very product's own session harvester. That is the
|
|
15
15
|
* sourcing-secrets verdict applied to the one credential this product
|
|
16
16
|
* stores.
|
|
17
|
-
* - **The message body.** `cockpit mail send`
|
|
18
|
-
* `--file`, the same discipline `cockpit issue comment` and
|
|
19
|
-
* update` keep.
|
|
17
|
+
* - **The message body.** `cockpit mail send` and `cockpit mail draft` take it
|
|
18
|
+
* from stdin or `--file`, the same discipline `cockpit issue comment` and
|
|
19
|
+
* `cockpit docs update` keep.
|
|
20
|
+
*
|
|
21
|
+
* `draft` (BLI-12530) takes `send`'s flags exactly — `--account`, `--to`,
|
|
22
|
+
* `--cc`, `--subject`, `--reply-to`, `--file` — and is refused the same two
|
|
23
|
+
* ways when `--account` or `--to` is missing: which mailbox a message is saved
|
|
24
|
+
* in is as much the caller's to say as which one it goes out from.
|
|
20
25
|
*/
|
|
21
26
|
import { optionalNonEmpty, optionalPositiveInteger, optionalUrl, parseNamedArgs, } from "./local-arg-values.js";
|
|
22
27
|
const MAIL_ACTIONS = new Set([
|
|
@@ -27,10 +32,13 @@ const MAIL_ACTIONS = new Set([
|
|
|
27
32
|
"read",
|
|
28
33
|
"search",
|
|
29
34
|
"send",
|
|
35
|
+
"draft",
|
|
30
36
|
"attachment",
|
|
31
37
|
"sync",
|
|
32
38
|
"sync-status",
|
|
33
39
|
]);
|
|
40
|
+
/** The two verbs that put a message in a mailbox: one sends it, one saves it unsent. */
|
|
41
|
+
const MAIL_ACTIONS_COMPOSING = new Set(["send", "draft"]);
|
|
34
42
|
/** Every verb that names ONE thing first: a thread, an attachment, a mailbox. */
|
|
35
43
|
const MAIL_ACTIONS_NEEDING_A_SUBJECT = new Set(["read", "attachment", "detach", "sync"]);
|
|
36
44
|
/** `sync-status [<account id>]` — every mailbox, or one's. Ticket 10730. */
|
|
@@ -85,7 +93,7 @@ function readMailAction(values) {
|
|
|
85
93
|
const first = values.positionals[0];
|
|
86
94
|
const action = (first === undefined ? "inbox" : first);
|
|
87
95
|
if (!MAIL_ACTIONS.has(action)) {
|
|
88
|
-
throw new Error(`Unknown mail command: ${first}. Try accounts, add-imap, detach, inbox, read, search, send, attachment, sync, or sync-status.`);
|
|
96
|
+
throw new Error(`Unknown mail command: ${first}. Try accounts, add-imap, detach, inbox, read, search, send, draft, attachment, sync, or sync-status.`);
|
|
89
97
|
}
|
|
90
98
|
return { action, rest: values.positionals.slice(first === undefined ? 0 : 1) };
|
|
91
99
|
}
|
|
@@ -127,11 +135,13 @@ function readImapAddress(action, values) {
|
|
|
127
135
|
function readMailRecipients(action, values) {
|
|
128
136
|
const to = splitAddresses(values.flags.get("--to"));
|
|
129
137
|
const cc = splitAddresses(values.flags.get("--cc"));
|
|
130
|
-
if (action
|
|
131
|
-
throw new Error(
|
|
138
|
+
if (MAIL_ACTIONS_COMPOSING.has(action) && to.length === 0) {
|
|
139
|
+
throw new Error(`mail ${action} needs --to "someone@example.com" (comma-separated for several).`);
|
|
132
140
|
}
|
|
133
|
-
if (action
|
|
134
|
-
throw new Error(
|
|
141
|
+
if (MAIL_ACTIONS_COMPOSING.has(action) && !optionalNonEmpty(values.flags.get("--account"))) {
|
|
142
|
+
throw new Error(action === "draft"
|
|
143
|
+
? "mail draft needs --account <id>: with several mailboxes attached, which one the draft is saved in is yours to say. `cockpit mail accounts` lists them."
|
|
144
|
+
: "mail send needs --account <id>: with several mailboxes attached, which address this goes out from is yours to say. `cockpit mail accounts` lists them.");
|
|
135
145
|
}
|
|
136
146
|
return { to, cc };
|
|
137
147
|
}
|
|
@@ -115,13 +115,15 @@ export const TOWER_OPS_COMMAND_HELP = [
|
|
|
115
115
|
[
|
|
116
116
|
"ops",
|
|
117
117
|
[
|
|
118
|
-
"Usage: cockpit ops [status [--job <id>] [--coverage] [--skips] [--memory [--memory-days N]] [--models] [--turns] [--tool-router] | recompile --person <email|name|id> [--dry-run]] [--json]",
|
|
118
|
+
"Usage: cockpit ops [status [--job <id>] [--coverage] [--skips] [--memory [--memory-days N]] [--models] [--turns] [--tool-router] [--people] | recompile --person <email|name|id> [--dry-run]] [--json]",
|
|
119
119
|
"",
|
|
120
120
|
" cockpit ops status",
|
|
121
121
|
" One line per scheduled job: when it last produced something, and whether that",
|
|
122
122
|
" is late FOR THAT JOB. A once-daily job quiet for 18 hours reads ok; a",
|
|
123
123
|
" quarter-hourly one does not. Every line names the schedule it is judged against.",
|
|
124
124
|
" --turns adds 30-day JARVIS turn aggregates; --tool-router adds the shadow-router agreement board.",
|
|
125
|
+
" --json carries the fleet and coverage SUMMARIES only (about 150 KB lighter);",
|
|
126
|
+
" --people --json adds every per-device and per-person row. The text board always prints them.",
|
|
125
127
|
" A job that is not ok also prints what its artifact does and does not prove —",
|
|
126
128
|
" several only write a row when there is something new, so quiet can mean a quiet",
|
|
127
129
|
" week rather than a broken cron.",
|
|
@@ -122,7 +122,7 @@ export const TOWER_COMMAND_HELP = [
|
|
|
122
122
|
[
|
|
123
123
|
"mail",
|
|
124
124
|
[
|
|
125
|
-
"Usage: cockpit mail [accounts|add-imap|inbox|read <thread>|search \"<words>\"|send|attachment <id>|sync <account>|sync-status [<account>]|detach <account>] [flags]",
|
|
125
|
+
"Usage: cockpit mail [accounts|add-imap|inbox|read <thread>|search \"<words>\"|send|draft|attachment <id>|sync <account>|sync-status [<account>]|detach <account>] [flags]",
|
|
126
126
|
"",
|
|
127
127
|
"Every mailbox you attached, in one place. One person, several addresses — nothing here has a \"current\" mailbox.",
|
|
128
128
|
"accounts — every mailbox, its provider (gmail_oauth or imap), its health and when it last synced. The ids other verbs take are on the last line.",
|
|
@@ -134,12 +134,13 @@ export const TOWER_COMMAND_HELP = [
|
|
|
134
134
|
"read <thread> — one conversation with its bodies. Thread ids come from `cockpit mail inbox` or `cockpit mail search`, in either --json or plain text.",
|
|
135
135
|
"search \"<words>\" [--account <id>] [--limit <n>] — full text over subject and body. Quotes and OR work the way they do in a search box. Every row (text or --json) carries the thread id.",
|
|
136
136
|
"send --account <id> --to <a[,b]> [--cc <c>] --subject \"<s>\" [--reply-to <message id>] [--file <path>] — the BODY comes from --file or stdin. --account is required: which address this goes out from is yours to say.",
|
|
137
|
+
"draft --account <id> --to <a[,b]> [--cc <c>] --subject \"<s>\" [--reply-to <message id>] [--file <path>] — saves the message as a DRAFT in that mailbox for a person to read, edit and send themselves. NOTHING IS SENT and nobody is notified, so there is no confirmation step. Same flags and same body rule as send (--file or stdin). Only the mailbox's owner can draft into it. --reply-to threads the draft into that conversation the way send does; Gmail files it there only when the subject matches the original's, and the answer says whether it did. Gmail mailboxes only: an IMAP mailbox is refused as imap_drafts_unsupported. --json answers { ok, draft: { account_id, address, provider_draft_id, provider_thread_id, threaded } } (BLI-12530).",
|
|
137
138
|
"attachment <id> --out <path> — downloads one attachment to a file. Attachments are pointers until you ask; nothing is stored in Tower.",
|
|
138
139
|
"sync <account> — reads that mailbox now instead of waiting for the cron. Prints what landed, or the reason it did not.",
|
|
139
140
|
"sync-status [<account>] — every mailbox's health, or one's: last successful sync, the last pass's own reason, how many messages it carried, failures in the last 24h, and the lag between a message's own clock and when Tower stored it (ticket 10730).",
|
|
140
141
|
"detach <account> — removes the mailbox, its stored mail and its credential.",
|
|
141
142
|
"--json writes one machine-readable object to stdout; every reason and receipt line stays on stderr. Shapes differ by verb: inbox/read answer { messages: [...] }, search answers { matches: [...] } — same MessageRow shape either name, always carrying thread_id.",
|
|
142
|
-
"A refusal keeps Tower's own reason label — needs_rls_client, account_not_found_or_unreadable, account_not_active, google_oauth_not_configured, credential_unavailable, and so on.",
|
|
143
|
+
"A refusal keeps Tower's own reason label — needs_rls_client, account_not_found_or_unreadable, account_not_active, account_not_yours, google_oauth_not_configured, credential_unavailable, and so on.",
|
|
143
144
|
"The command uses the existing paired device identity. Run `cockpit login` first if this machine is not paired.",
|
|
144
145
|
],
|
|
145
146
|
],
|
|
@@ -124,7 +124,7 @@ export function localCommandHelp(command) {
|
|
|
124
124
|
" 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]",
|
|
125
125
|
" 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]",
|
|
126
126
|
" 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]",
|
|
127
|
-
" cockpit mail [accounts|add-imap --address <a>|inbox|read <thread>|search \"<words>\"|send --account <id> --to <a> --subject <s>|attachment <id> --out <path>|sync <account>|detach <account>] [--account <id>] [--limit <n>] [--unread] [--label <l>] [--file <path>] [--dashboard-url <url>] [--json]",
|
|
127
|
+
" 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>] [--dashboard-url <url>] [--json]",
|
|
128
128
|
" 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]",
|
|
129
129
|
" cockpit project [list] [--archived] [--dashboard-url <url>] [--json]",
|
|
130
130
|
" 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]",
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The compose half of `cockpit mail` (BLI-12530): `draft`, and the two things
|
|
3
|
+
* it shares with `send` — where a message body comes from, and the JSON both
|
|
4
|
+
* verbs post.
|
|
5
|
+
*
|
|
6
|
+
* `cockpit mail draft` saves a message as a DRAFT in one of your own mailboxes
|
|
7
|
+
* (`POST /api/mail/drafts`) so a person can read it, edit it and press send
|
|
8
|
+
* themselves. It is the verb for an agent that prepares mail on somebody's
|
|
9
|
+
* behalf: nothing is sent, nobody is notified, and so — unlike `send` — there
|
|
10
|
+
* is nothing to confirm.
|
|
11
|
+
*
|
|
12
|
+
* Kept beside `mail.ts` rather than in it because `mail.ts` sits at the edge of
|
|
13
|
+
* the readability ceiling, and because the two verbs genuinely share one shape:
|
|
14
|
+
* the request body is built once here, so a field added to `send` cannot be
|
|
15
|
+
* forgotten by `draft` (both doors accept exactly the same fields).
|
|
16
|
+
*
|
|
17
|
+
* THE BODY NEVER TOUCHES ARGV. It comes from `--file` or stdin, the discipline
|
|
18
|
+
* `send`, `issue comment` and `docs update` already keep — argv is world-
|
|
19
|
+
* readable on a shared machine and is captured by this product's own session
|
|
20
|
+
* harvester.
|
|
21
|
+
*/
|
|
22
|
+
import { askAgentDoor, emitAgentDoor, failAgentDoor, } from "./agent-door.js";
|
|
23
|
+
import { isInteractiveStdin, readPipedText, writeLine } from "./cli-io.js";
|
|
24
|
+
import { readNoteFile } from "./notes-file.js";
|
|
25
|
+
// One spelling per surface: the same tag `mail.ts` uses.
|
|
26
|
+
const TAG = "[mail cli]";
|
|
27
|
+
const WRITE_DEADLINE_MS = 60_000;
|
|
28
|
+
const BODY_MAX_CHARS = 200_000;
|
|
29
|
+
/** Body on stdin, never argv — `--file` is the safest way on Windows. */
|
|
30
|
+
export async function readMailBody(command, door) {
|
|
31
|
+
if (command.filePath) {
|
|
32
|
+
const read = await readNoteFile(command.filePath);
|
|
33
|
+
if (!read.ok) {
|
|
34
|
+
return { ok: false, reason: read.refusal, detail: `Could not read ${command.filePath}: ${read.detail}` };
|
|
35
|
+
}
|
|
36
|
+
return { ok: true, text: read.bytes.toString("utf8") };
|
|
37
|
+
}
|
|
38
|
+
if (isInteractiveStdin(door.io))
|
|
39
|
+
return { ok: true, text: "" };
|
|
40
|
+
try {
|
|
41
|
+
const text = await readPipedText(door.io.stdin, {
|
|
42
|
+
maxChars: BODY_MAX_CHARS,
|
|
43
|
+
overflowMessage: `A message body is limited to ${BODY_MAX_CHARS} characters here.`,
|
|
44
|
+
});
|
|
45
|
+
return { ok: true, text };
|
|
46
|
+
}
|
|
47
|
+
catch (error) {
|
|
48
|
+
return { ok: false, reason: "body_too_long", detail: error instanceof Error ? error.message : String(error) };
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* The JSON that `send` and `draft` post. One shape on purpose: the two doors
|
|
53
|
+
* take the same fields, and a message that can be sent can be saved unsent.
|
|
54
|
+
*/
|
|
55
|
+
export function composedMessageBody(command, text) {
|
|
56
|
+
return {
|
|
57
|
+
account_id: command.accountId,
|
|
58
|
+
to: command.to.map((address) => ({ address })),
|
|
59
|
+
...(command.cc.length > 0 ? { cc: command.cc.map((address) => ({ address })) } : {}),
|
|
60
|
+
subject: command.subjectLine ?? "",
|
|
61
|
+
text,
|
|
62
|
+
...(command.replyTo ? { in_reply_to_message_id: command.replyTo } : {}),
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
/** `draft --account <id> --to <a[,b]> [--cc <c>] --subject "<s>" [--reply-to <message id>] [--file <path>]` */
|
|
66
|
+
export async function runMailDraft(command, door) {
|
|
67
|
+
const body = await readMailBody(command, door);
|
|
68
|
+
if (!body.ok)
|
|
69
|
+
return failAgentDoor(door, TAG, body.reason, body.detail);
|
|
70
|
+
if (body.text.trim() === "") {
|
|
71
|
+
return failAgentDoor(door, TAG, "body_required", 'A draft needs a body: `echo "Tuesday works for me." | cockpit mail draft --account <id> --to someone@example.com --subject "Re: lunch"`.');
|
|
72
|
+
}
|
|
73
|
+
const answer = await askAgentDoor(door, {
|
|
74
|
+
path: "/api/mail/drafts",
|
|
75
|
+
method: "POST",
|
|
76
|
+
label: "mail draft",
|
|
77
|
+
timeoutMs: WRITE_DEADLINE_MS,
|
|
78
|
+
body: composedMessageBody(command, body.text),
|
|
79
|
+
});
|
|
80
|
+
if (!answer.ok)
|
|
81
|
+
return failAgentDoor(door, TAG, answer.reason, answer.detail);
|
|
82
|
+
const draft = answer.body.draft;
|
|
83
|
+
if (door.json)
|
|
84
|
+
return emitAgentDoor(door, { ok: true, draft });
|
|
85
|
+
writeLine(door.io.stdout, `Saved a draft in ${draft?.address ?? "that mailbox"} for ${command.to.join(", ")}. `
|
|
86
|
+
+ "Nothing was sent: it is in Drafts for a person to read, edit and send.");
|
|
87
|
+
if (command.replyTo) {
|
|
88
|
+
// `threaded` is what Gmail answered, not what was asked: a reply is only
|
|
89
|
+
// filed in its conversation when the subject still matches the original's.
|
|
90
|
+
writeLine(door.io.stdout, draft?.threaded
|
|
91
|
+
? "It sits in the reply's conversation."
|
|
92
|
+
: "Gmail did not file it in that conversation, so it is a new one. A reply joins its thread only when the subject matches the original's.");
|
|
93
|
+
}
|
|
94
|
+
writeLine(door.io.stdout, `draft ${draft?.provider_draft_id ?? "(no id returned)"} thread ${draft?.provider_thread_id ?? "(none)"} account ${draft?.account_id ?? command.accountId}`);
|
|
95
|
+
return 0;
|
|
96
|
+
}
|
package/dist/commands/mail.js
CHANGED
|
@@ -2,10 +2,12 @@
|
|
|
2
2
|
* `cockpit mail` — the mailboxes a person attached, from a terminal
|
|
3
3
|
* (BLI-3708).
|
|
4
4
|
*
|
|
5
|
-
*
|
|
5
|
+
* Ten verbs over `/api/mail/**`, the same doors the browser will use when the
|
|
6
6
|
* UI lands. Every one of those routes already accepts the collector device
|
|
7
7
|
* token (`resolveCaller({ allowDeviceToken: true })`), so this command is a
|
|
8
8
|
* terminal in front of an existing door and adds no authority of its own.
|
|
9
|
+
* `draft` (BLI-12530) lives in `mail-compose.ts` beside the body reader it
|
|
10
|
+
* shares with `send`.
|
|
9
11
|
*
|
|
10
12
|
* The CLI is the FIRST surface for this wave, deliberately (AGENTS.md, "CLI is
|
|
11
13
|
* king"): a mailbox is easier to reason about as `--json` than as a rendered
|
|
@@ -23,7 +25,7 @@
|
|
|
23
25
|
import { writeFile } from "node:fs/promises";
|
|
24
26
|
import { askAgentDoor, emitAgentDoor, failAgentDoor, openAgentDoor, } from "./agent-door.js";
|
|
25
27
|
import { isInteractiveStdin, readPipedText, writeLine } from "./cli-io.js";
|
|
26
|
-
import {
|
|
28
|
+
import { composedMessageBody, readMailBody, runMailDraft } from "./mail-compose.js";
|
|
27
29
|
import { cacheAgeLabel, readThroughCache } from "./read-cache.js";
|
|
28
30
|
import { towerRequest } from "../tower-client.js";
|
|
29
31
|
const TAG = "[mail cli]";
|
|
@@ -31,7 +33,6 @@ const READ_DEADLINE_MS = 30_000;
|
|
|
31
33
|
const WRITE_DEADLINE_MS = 60_000;
|
|
32
34
|
/** A sync pass reads a bounded page, but a cold mailbox can still take a while. */
|
|
33
35
|
const SYNC_DEADLINE_MS = 120_000;
|
|
34
|
-
const BODY_MAX_CHARS = 200_000;
|
|
35
36
|
const PASSWORD_MAX_CHARS = 200;
|
|
36
37
|
export async function runMail(command, io) {
|
|
37
38
|
const door = await openAgentDoor("mail", command, io);
|
|
@@ -50,6 +51,8 @@ export async function runMail(command, io) {
|
|
|
50
51
|
return search(command, door);
|
|
51
52
|
case "send":
|
|
52
53
|
return send(command, door);
|
|
54
|
+
case "draft":
|
|
55
|
+
return runMailDraft(command, door);
|
|
53
56
|
case "attachment":
|
|
54
57
|
return attachment(command, door);
|
|
55
58
|
case "sync":
|
|
@@ -250,7 +253,7 @@ async function search(command, door) {
|
|
|
250
253
|
return 0;
|
|
251
254
|
}
|
|
252
255
|
async function send(command, door) {
|
|
253
|
-
const body = await
|
|
256
|
+
const body = await readMailBody(command, door);
|
|
254
257
|
if (!body.ok)
|
|
255
258
|
return failAgentDoor(door, TAG, body.reason, body.detail);
|
|
256
259
|
if (body.text.trim() === "") {
|
|
@@ -261,14 +264,7 @@ async function send(command, door) {
|
|
|
261
264
|
method: "POST",
|
|
262
265
|
label: "mail send",
|
|
263
266
|
timeoutMs: WRITE_DEADLINE_MS,
|
|
264
|
-
body:
|
|
265
|
-
account_id: command.accountId,
|
|
266
|
-
to: command.to.map((address) => ({ address })),
|
|
267
|
-
...(command.cc.length > 0 ? { cc: command.cc.map((address) => ({ address })) } : {}),
|
|
268
|
-
subject: command.subjectLine ?? "",
|
|
269
|
-
text: body.text,
|
|
270
|
-
...(command.replyTo ? { in_reply_to_message_id: command.replyTo } : {}),
|
|
271
|
-
},
|
|
267
|
+
body: composedMessageBody(command, body.text),
|
|
272
268
|
});
|
|
273
269
|
if (!answer.ok)
|
|
274
270
|
return failAgentDoor(door, TAG, answer.reason, answer.detail);
|
|
@@ -361,26 +357,4 @@ async function syncStatus(command, door) {
|
|
|
361
357
|
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)" : ""}`);
|
|
362
358
|
}
|
|
363
359
|
return 0;
|
|
364
|
-
}
|
|
365
|
-
/** Body on stdin, never argv — `--file` is the safest way on Windows. */
|
|
366
|
-
async function readBody(command, door) {
|
|
367
|
-
if (command.filePath) {
|
|
368
|
-
const read = await readNoteFile(command.filePath);
|
|
369
|
-
if (!read.ok) {
|
|
370
|
-
return { ok: false, reason: read.refusal, detail: `Could not read ${command.filePath}: ${read.detail}` };
|
|
371
|
-
}
|
|
372
|
-
return { ok: true, text: read.bytes.toString("utf8") };
|
|
373
|
-
}
|
|
374
|
-
if (isInteractiveStdin(door.io))
|
|
375
|
-
return { ok: true, text: "" };
|
|
376
|
-
try {
|
|
377
|
-
const text = await readPipedText(door.io.stdin, {
|
|
378
|
-
maxChars: BODY_MAX_CHARS,
|
|
379
|
-
overflowMessage: `A message body is limited to ${BODY_MAX_CHARS} characters here.`,
|
|
380
|
-
});
|
|
381
|
-
return { ok: true, text };
|
|
382
|
-
}
|
|
383
|
-
catch (error) {
|
|
384
|
-
return { ok: false, reason: "body_too_long", detail: error instanceof Error ? error.message : String(error) };
|
|
385
|
-
}
|
|
386
360
|
}
|
package/dist/commands/ops.js
CHANGED
|
@@ -111,14 +111,24 @@ function isUnhealthyPipeline(row) {
|
|
|
111
111
|
}
|
|
112
112
|
function writeOpsStatusOutput(command, io, payload, memory, models, turns, toolRouter) {
|
|
113
113
|
if (command.json) {
|
|
114
|
-
writeJsonOpsStatus(io, payload, memory, models, turns, toolRouter);
|
|
114
|
+
writeJsonOpsStatus(command.people === true, io, payload, memory, models, turns, toolRouter);
|
|
115
115
|
return;
|
|
116
116
|
}
|
|
117
117
|
writeHumanOpsStatus(command, io, payload, memory, models, turns, toolRouter);
|
|
118
118
|
}
|
|
119
|
-
|
|
119
|
+
/** The block without its per-device / per-person rows unless `--people` asked for them. */
|
|
120
|
+
function summaryOnly(block, withPeople) {
|
|
121
|
+
if (withPeople)
|
|
122
|
+
return block;
|
|
123
|
+
const { devices: _devices, people: _people, ...rest } = block;
|
|
124
|
+
return rest;
|
|
125
|
+
}
|
|
126
|
+
function writeJsonOpsStatus(withPeople, io, payload, memory, models, turns, toolRouter) {
|
|
120
127
|
writeLine(io.stdout, JSON.stringify({
|
|
121
128
|
...payload,
|
|
129
|
+
// BLI-12507: the MCP's rule, so one board reads the same on both doors.
|
|
130
|
+
...(payload.fleet ? { fleet: summaryOnly(payload.fleet, withPeople) } : {}),
|
|
131
|
+
...(payload.coverage ? { coverage: summaryOnly(payload.coverage, withPeople) } : {}),
|
|
122
132
|
...(memory
|
|
123
133
|
? { memory: memory.section, hooks: memory.hooks, experience: memory.experience }
|
|
124
134
|
: {}),
|
|
@@ -134,6 +144,13 @@ function writeHumanOpsStatus(command, io, payload, memory, models, turns, toolRo
|
|
|
134
144
|
})) {
|
|
135
145
|
writeLine(io.stdout, line);
|
|
136
146
|
}
|
|
147
|
+
// BLI-12507: a section the server could not read inside its budget is named
|
|
148
|
+
// here, never a silent gap in the board.
|
|
149
|
+
const unreadable = asRecord(payload)["unreadableSections"];
|
|
150
|
+
if (Array.isArray(unreadable) && unreadable.length > 0) {
|
|
151
|
+
writeLine(io.stdout, "");
|
|
152
|
+
writeLine(io.stdout, `NOT READ ${unreadable.join(", ")} (reader_timeout or read failure; see the Tower log)`);
|
|
153
|
+
}
|
|
137
154
|
if (memory?.section) {
|
|
138
155
|
for (const line of renderMemoryUsage(memory.section, (text) => dim(text, styled))) {
|
|
139
156
|
writeLine(io.stdout, line);
|
|
@@ -195,6 +212,10 @@ function createOpsStatusReadLog(command, payload, rows, unhealthy, memory, model
|
|
|
195
212
|
unhealthy: unhealthy.length,
|
|
196
213
|
unhealthy_ids: unhealthy.map((row) => row.id ?? "?"),
|
|
197
214
|
with_skips: Boolean(command.skips),
|
|
215
|
+
people_rows: Boolean(command.people) || !command.json ? "included" : "summary_only",
|
|
216
|
+
unreadable_sections: Array.isArray(asRecord(payload)["unreadableSections"])
|
|
217
|
+
? asRecord(payload)["unreadableSections"]
|
|
218
|
+
: [],
|
|
198
219
|
// The laptops (BLI-3550), counted on the same line: a run that shows a
|
|
199
220
|
// green board and says nothing about the fleet cannot answer "was
|
|
200
221
|
// anybody's machine dead this morning?".
|
|
@@ -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.166");
|
|
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.166",
|
|
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.45",
|
|
31
|
-
"@bli-cockpit/mcp": "0.1.
|
|
31
|
+
"@bli-cockpit/mcp": "0.1.84",
|
|
32
32
|
"@bli-cockpit/telemetry-core": "0.1.57"
|
|
33
33
|
},
|
|
34
34
|
"repository": {
|