@bli-cockpit/cli 0.2.238 → 0.2.240
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-direct.js +119 -0
- package/dist/commands/cal-scope.js +71 -0
- package/dist/commands/cal.js +33 -2
- package/dist/commands/docs-share.js +81 -0
- package/dist/commands/docs.js +43 -6
- 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-versions.js +93 -0
- package/dist/commands/issue.js +28 -5
- package/dist/commands/local-args-tower-cal.js +113 -9
- package/dist/commands/local-args-tower-docs-msg.js +52 -7
- package/dist/commands/local-args-tower-drills.js +1 -1
- package/dist/commands/local-args-tower-msg.js +62 -7
- package/dist/commands/local-args-tower-setup.js +11 -4
- package/dist/commands/local-args-tower-work.js +97 -11
- package/dist/commands/local-help-commands-tower-ops.js +3 -3
- package/dist/commands/local-help-commands-tower.js +27 -14
- package/dist/commands/local-help-commands.js +1 -1
- package/dist/commands/local-help.js +2 -2
- package/dist/commands/msg-read-line.js +32 -0
- package/dist/commands/msg-write.js +118 -0
- package/dist/commands/msg.js +64 -31
- package/dist/commands/project.js +15 -3
- package/dist/commands/public-root.js +1 -1
- package/dist/commands/setup.js +13 -0
- package/dist/commands/usage.js +3 -1
- package/dist/spool/local-spool-parse.js +13 -12
- package/package.json +4 -4
|
@@ -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,13 +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";
|
|
22
|
+
import { markReadThrough } from "./msg-read-line.js";
|
|
23
|
+
import { deleteMessage, editMessage, messageOutcome, messageText, readContent, refuseMsgWrite } from "./msg-write.js";
|
|
21
24
|
const TAG = "[msg cli]";
|
|
22
25
|
const READ_DEADLINE_MS = 30_000;
|
|
23
26
|
const WRITE_DEADLINE_MS = 30_000;
|
|
24
|
-
const CONTENT_MAX_CHARS = 8_000;
|
|
25
27
|
export async function runMsg(command, io) {
|
|
26
28
|
const door = await openAgentDoor("msg", command, io);
|
|
27
29
|
switch (command.action) {
|
|
@@ -31,8 +33,16 @@ export async function runMsg(command, io) {
|
|
|
31
33
|
return listChannels(door);
|
|
32
34
|
case "read":
|
|
33
35
|
return readChannel(command, door);
|
|
36
|
+
case "unread":
|
|
37
|
+
return markUnread(command, door);
|
|
34
38
|
case "send":
|
|
35
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);
|
|
36
46
|
case "thread":
|
|
37
47
|
return readThread(command, door);
|
|
38
48
|
case "create":
|
|
@@ -151,11 +161,20 @@ async function readChannel(command, door) {
|
|
|
151
161
|
// BLI-10449: whether this channel is Tower's own or a Slack snapshot, and
|
|
152
162
|
// how fresh — the door's own sentence, printed as sent.
|
|
153
163
|
const provenance = body.provenance ?? null;
|
|
164
|
+
const readBoundary = body.readBoundary ?? null;
|
|
154
165
|
const limitApplied = body.limitApplied ?? null;
|
|
155
|
-
|
|
156
|
-
|
|
166
|
+
// `--mark-read` (BLI-21299): the person has just been shown these messages, so
|
|
167
|
+
// the read line moves to the newest top-level one - the only way a terminal
|
|
168
|
+
// moves it. Never implied by reading alone.
|
|
169
|
+
const readMove = command.markRead ? await markReadThrough(door, resolved.id, messages) : null;
|
|
170
|
+
const exitCode = readMove && !readMove.ok ? 1 : 0;
|
|
171
|
+
if (door.json) {
|
|
172
|
+
return emitAgentDoor(door, { ok: true, ...body, channelId: resolved.id, provenance, readBoundary, limitApplied, messages, nextCursor: body.nextCursor, hasMore: body.hasMore, coverage: body.coverage, ...(readMove ? { readMove } : {}) }, exitCode);
|
|
173
|
+
}
|
|
157
174
|
if (provenance?.sentence)
|
|
158
175
|
writeLine(door.io.stdout, provenance.sentence);
|
|
176
|
+
if (readBoundary?.sentence)
|
|
177
|
+
writeLine(door.io.stdout, readBoundary.sentence);
|
|
159
178
|
if (limitApplied !== null) {
|
|
160
179
|
writeLine(door.io.stdout, `Showing the newest ${limitApplied}: one read returns at most ${limitApplied} messages.`);
|
|
161
180
|
}
|
|
@@ -168,8 +187,38 @@ async function readChannel(command, door) {
|
|
|
168
187
|
for (const message of [...messages].reverse()) {
|
|
169
188
|
// An imported Slack message has no Tower user; its Slack name is the label.
|
|
170
189
|
const who = message.agent_label ?? message.user_id ?? message.slack_author_label ?? "someone";
|
|
171
|
-
writeLine(door.io.stdout, `${message.id} ${message.created_at} ${who} ${message
|
|
190
|
+
writeLine(door.io.stdout, `${message.id} ${message.created_at} ${who} ${messageText(message)}`);
|
|
172
191
|
}
|
|
192
|
+
if (readMove) {
|
|
193
|
+
writeLine(readMove.ok ? door.io.stdout : door.io.stderr, readMove.ok
|
|
194
|
+
? `Read line moved to ${readMove.messageId} (${readMove.changed ? "advanced" : "already at or past it"}).`
|
|
195
|
+
: `Read line NOT moved (${readMove.reason}): ${readMove.detail} Run the same command again to retry.`);
|
|
196
|
+
}
|
|
197
|
+
return exitCode;
|
|
198
|
+
}
|
|
199
|
+
/** `cockpit msg unread <channel> --from <message-id>` — this message and everything after it counts as unread. */
|
|
200
|
+
async function markUnread(command, door) {
|
|
201
|
+
const ref = command.channelRef ?? "";
|
|
202
|
+
const messageId = command.unreadFrom ?? "";
|
|
203
|
+
const resolved = await resolveChannelId(door, ref);
|
|
204
|
+
if (resolved.status === "list_failed")
|
|
205
|
+
return failAgentDoor(door, TAG, resolved.reason, resolved.detail);
|
|
206
|
+
if (resolved.status === "not_found")
|
|
207
|
+
return channelNotFound(door, ref);
|
|
208
|
+
const answer = await askAgentDoor(door, {
|
|
209
|
+
path: `/api/msg/channels/${encodeURIComponent(resolved.id)}/read`,
|
|
210
|
+
method: "POST",
|
|
211
|
+
label: "msg unread",
|
|
212
|
+
timeoutMs: WRITE_DEADLINE_MS,
|
|
213
|
+
body: { message_id: messageId, action: "unread" },
|
|
214
|
+
});
|
|
215
|
+
if (!answer.ok)
|
|
216
|
+
return failAgentDoor(door, TAG, answer.reason, answer.detail);
|
|
217
|
+
const move = answer.body.move ?? {};
|
|
218
|
+
writeLine(door.io.stderr, `${TAG} unread ${JSON.stringify({ channel_id: resolved.id, message_id: messageId })}`);
|
|
219
|
+
if (door.json)
|
|
220
|
+
return emitAgentDoor(door, { ok: true, channelId: resolved.id, messageId, move });
|
|
221
|
+
writeLine(door.io.stdout, `Marked ${messageId} and everything after it unread in ${ref}. Read line now at ${move.lastReadAt ?? "the start"}.`);
|
|
173
222
|
return 0;
|
|
174
223
|
}
|
|
175
224
|
async function readThread(command, door) {
|
|
@@ -201,29 +250,10 @@ async function readThread(command, door) {
|
|
|
201
250
|
}
|
|
202
251
|
for (const message of [...messages].reverse()) {
|
|
203
252
|
const who = message.agent_label ?? message.user_id;
|
|
204
|
-
writeLine(door.io.stdout, `${message.id} ${message.created_at} ${who} ${message
|
|
253
|
+
writeLine(door.io.stdout, `${message.id} ${message.created_at} ${who} ${messageText(message)}`);
|
|
205
254
|
}
|
|
206
255
|
return 0;
|
|
207
256
|
}
|
|
208
|
-
async function readContent(io) {
|
|
209
|
-
if (isInteractiveStdin(io)) {
|
|
210
|
-
return {
|
|
211
|
-
ok: false,
|
|
212
|
-
reason: "nothing_piped",
|
|
213
|
-
detail: "Pipe the message in, e.g. `echo \"hello\" | cockpit msg send general`. A message never travels on the command line.",
|
|
214
|
-
};
|
|
215
|
-
}
|
|
216
|
-
try {
|
|
217
|
-
const text = await readPipedText(io.stdin, {
|
|
218
|
-
maxChars: CONTENT_MAX_CHARS,
|
|
219
|
-
overflowMessage: `A message is limited to ${CONTENT_MAX_CHARS} characters.`,
|
|
220
|
-
});
|
|
221
|
-
return { ok: true, text: text.trim() };
|
|
222
|
-
}
|
|
223
|
-
catch (error) {
|
|
224
|
-
return { ok: false, reason: "content_too_long", detail: error instanceof Error ? error.message : String(error) };
|
|
225
|
-
}
|
|
226
|
-
}
|
|
227
257
|
async function sendMessage(command, door) {
|
|
228
258
|
const ref = command.channelRef ?? "";
|
|
229
259
|
const resolved = await resolveChannelId(door, ref);
|
|
@@ -236,6 +266,8 @@ async function sendMessage(command, door) {
|
|
|
236
266
|
return failAgentDoor(door, TAG, content.reason, content.detail);
|
|
237
267
|
if (content.text === "")
|
|
238
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();
|
|
239
271
|
const answer = await askAgentDoor(door, {
|
|
240
272
|
path: `/api/msg/channels/${encodeURIComponent(resolved.id)}/messages`,
|
|
241
273
|
method: "POST",
|
|
@@ -243,15 +275,16 @@ async function sendMessage(command, door) {
|
|
|
243
275
|
timeoutMs: WRITE_DEADLINE_MS,
|
|
244
276
|
body: {
|
|
245
277
|
content: content.text,
|
|
278
|
+
request_id: requestId,
|
|
246
279
|
...(command.threadId ? { thread_parent_id: command.threadId } : {}),
|
|
247
280
|
},
|
|
248
281
|
});
|
|
249
282
|
if (!answer.ok)
|
|
250
|
-
return
|
|
251
|
-
const message = answer.body
|
|
252
|
-
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 })}`);
|
|
253
286
|
if (door.json)
|
|
254
|
-
return emitAgentDoor(door, { ok: true, message });
|
|
287
|
+
return emitAgentDoor(door, { ok: true, message, mutation });
|
|
255
288
|
writeLine(door.io.stdout, `Sent to ${ref} (${message?.id ?? "?"}).`);
|
|
256
289
|
return 0;
|
|
257
290
|
}
|
|
@@ -344,7 +377,7 @@ async function showMessage(command, door) {
|
|
|
344
377
|
writeLine(door.io.stdout, body.href ?? "Message context");
|
|
345
378
|
for (const page of [body.channelPage, body.threadPage]) {
|
|
346
379
|
for (const row of [...(page?.messages ?? [])].reverse())
|
|
347
|
-
writeLine(door.io.stdout, `${row.id} ${row.created_at} ${row
|
|
380
|
+
writeLine(door.io.stdout, `${row.id} ${row.created_at} ${messageText(row)}`);
|
|
348
381
|
if (page?.nextCursor)
|
|
349
382
|
writeLine(door.io.stdout, `More history: --before ${page.nextCursor}`);
|
|
350
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.240");
|
|
25
25
|
return 0;
|
|
26
26
|
}
|
|
27
27
|
|
package/dist/commands/setup.js
CHANGED
|
@@ -3,6 +3,10 @@ import { writeLine } from "./cli-io.js";
|
|
|
3
3
|
export async function runSetup(command, io) {
|
|
4
4
|
const door = await openAgentDoor("setup", command, io);
|
|
5
5
|
const query = new URLSearchParams();
|
|
6
|
+
if (command.until)
|
|
7
|
+
query.set("until", command.until);
|
|
8
|
+
if (command.machines)
|
|
9
|
+
query.set("machines", "1");
|
|
6
10
|
if (command.since)
|
|
7
11
|
query.set("since", command.since);
|
|
8
12
|
if (command.person)
|
|
@@ -18,6 +22,10 @@ export async function runSetup(command, io) {
|
|
|
18
22
|
const number = (value) => value === null ? "n/a" : value.toLocaleString(undefined, { maximumFractionDigits: 1 });
|
|
19
23
|
// BLI-13066: the server writes what each number rests on (the share of sessions counted, the sessions judged), so the CLI, the web page and JARVIS say the same words. An older server sends none.
|
|
20
24
|
const coverage = (words) => words ? ` (${words})` : "";
|
|
25
|
+
if (body.window)
|
|
26
|
+
writeLine(io.stdout, `Requested window: ${body.window.since} to ${body.window.until}. ${body.automation_basis ?? "Includes automated work"}`);
|
|
27
|
+
if (body.freshness)
|
|
28
|
+
writeLine(io.stdout, body.freshness.words);
|
|
21
29
|
for (const person of body.people ?? []) {
|
|
22
30
|
writeLine(io.stdout, `${person.rank ? `#${person.rank} ` : "Unranked · "}${person.display_name ?? person.email ?? "Unknown person"}: ${number(person.prs_per_week)} merged PRs/week · ${number(person.tokens_per_pr)} tokens/merged PR${coverage(person.coverage_words?.tokens)} · ${person.yield === null ? "no session judged yet" : `${number(person.yield * 100)}% session yield`}${coverage(person.coverage_words?.yield)}`);
|
|
23
31
|
if (person.tokens_basis)
|
|
@@ -28,6 +36,11 @@ export async function runSetup(command, io) {
|
|
|
28
36
|
if (!person.machines.length)
|
|
29
37
|
writeLine(io.stdout, " No setup received.");
|
|
30
38
|
for (const machine of person.machines) {
|
|
39
|
+
if (machine.health) {
|
|
40
|
+
writeLine(io.stdout, ` Machine reading: ${machine.health.words ?? machine.health.reason}. ${machine.health.next_action?.words ?? ""}`);
|
|
41
|
+
for (const piece of machine.health.pieces ?? [])
|
|
42
|
+
writeLine(io.stdout, ` ${piece.key}: ${piece.status}${piece.reason ? ` (${piece.reason})` : ""}. ${piece.next_action?.words ?? ""}`);
|
|
43
|
+
}
|
|
31
44
|
if (!machine.setup) {
|
|
32
45
|
writeLine(io.stdout, " No readable setup from this machine yet.");
|
|
33
46
|
continue;
|
package/dist/commands/usage.js
CHANGED
|
@@ -5,6 +5,8 @@ import { writeLine } from "./cli-io.js";
|
|
|
5
5
|
export async function runUsage(command, io) {
|
|
6
6
|
const door = await openAgentDoor("usage", command, io);
|
|
7
7
|
const query = new URLSearchParams({ since: command.since });
|
|
8
|
+
if (command.person)
|
|
9
|
+
query.set("person", command.person);
|
|
8
10
|
addUsageGroupingFilters(command, query);
|
|
9
11
|
if (command.action === "sessions")
|
|
10
12
|
return runUsageSessions(command, io, door, query);
|
|
@@ -19,7 +21,7 @@ export async function runUsage(command, io) {
|
|
|
19
21
|
writeLine(io.stdout, String(answer.body.text ?? ""));
|
|
20
22
|
return 0;
|
|
21
23
|
}
|
|
22
|
-
if (command.person) {
|
|
24
|
+
if (command.person && !body.person_filter && !(body.people ?? []).some(person => person.identity)) {
|
|
23
25
|
const email = command.person === "me" ? (await loadPairedSession("usage", command.homeDir)).email : command.person;
|
|
24
26
|
if (!email)
|
|
25
27
|
return failAgentDoor(door, "[usage]", "caller_email_unavailable", "The paired session has no email. Sign in again or pass --person <email>.");
|
|
@@ -65,24 +65,25 @@ function parseSourceRetryEntry(value, fieldPath) {
|
|
|
65
65
|
function parsePendingSessionReport(value, fieldPath) {
|
|
66
66
|
const record = requireRecord(value, fieldPath);
|
|
67
67
|
const sessions = parseMetadataOnlySessionReportRows(record["sessions"], `${fieldPath}.sessions`);
|
|
68
|
+
const text = (key) => requireString(record[key], `${fieldPath}.${key}`);
|
|
68
69
|
return {
|
|
69
|
-
report_id:
|
|
70
|
-
created_at:
|
|
71
|
-
last_attempt_at:
|
|
72
|
-
dashboard_url:
|
|
73
|
-
generated_at:
|
|
74
|
-
work_context_id:
|
|
75
|
-
repo_label:
|
|
76
|
-
branch:
|
|
77
|
-
repo_fingerprint:
|
|
70
|
+
report_id: text("report_id"),
|
|
71
|
+
created_at: text("created_at"),
|
|
72
|
+
last_attempt_at: text("last_attempt_at"),
|
|
73
|
+
dashboard_url: text("dashboard_url"),
|
|
74
|
+
generated_at: text("generated_at"),
|
|
75
|
+
work_context_id: text("work_context_id"),
|
|
76
|
+
repo_label: text("repo_label"),
|
|
77
|
+
branch: text("branch"),
|
|
78
|
+
repo_fingerprint: text("repo_fingerprint"),
|
|
78
79
|
repo_origin_url: record["repo_origin_url"] === undefined
|
|
79
80
|
? null
|
|
80
81
|
: requireNullableString(record["repo_origin_url"], `${fieldPath}.repo_origin_url`),
|
|
81
|
-
worktree_label:
|
|
82
|
-
worktree_fingerprint:
|
|
82
|
+
worktree_label: text("worktree_label"),
|
|
83
|
+
worktree_fingerprint: text("worktree_fingerprint"),
|
|
83
84
|
worktree_is_primary: requireBoolean(record["worktree_is_primary"], `${fieldPath}.worktree_is_primary`),
|
|
84
85
|
sessions,
|
|
85
|
-
failure_reason:
|
|
86
|
+
failure_reason: text("failure_reason"),
|
|
86
87
|
};
|
|
87
88
|
}
|
|
88
89
|
export function parseMetadataOnlySessionReportRows(value, fieldPath) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bli-cockpit/cli",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.240",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -27,9 +27,9 @@
|
|
|
27
27
|
"test": "node dist/cli.js --help && node ../../scripts/assert-public-cli-routing.mjs && node ../../scripts/assert-public-cli-verb-help.mjs && node ../../scripts/assert-public-cli-surface-doors.mjs && node ../../scripts/assert-public-cli-runtime-files.mjs && node ../../scripts/assert-public-cli-exit-contract.mjs && node ../../scripts/assert-public-cli-no-fleet-posts.mjs && node ../../scripts/assert-public-cli-doctor-json.mjs && node ../../scripts/assert-public-package-pack.mjs --workspace=@bli-cockpit/cli"
|
|
28
28
|
},
|
|
29
29
|
"dependencies": {
|
|
30
|
-
"@bli-cockpit/memory-mcp": "0.1.
|
|
31
|
-
"@bli-cockpit/mcp": "0.1.
|
|
32
|
-
"@bli-cockpit/telemetry-core": "0.1.
|
|
30
|
+
"@bli-cockpit/memory-mcp": "0.1.70",
|
|
31
|
+
"@bli-cockpit/mcp": "0.1.145",
|
|
32
|
+
"@bli-cockpit/telemetry-core": "0.1.77"
|
|
33
33
|
},
|
|
34
34
|
"repository": {
|
|
35
35
|
"type": "git",
|