@bli-cockpit/cli 0.2.40 → 0.2.42
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/adapters/local-sources.js +51 -5
- package/dist/autostart.js +105 -5
- package/dist/commands/brief-edit.js +289 -0
- package/dist/commands/brief-rewrite.js +260 -0
- package/dist/commands/brief.js +11 -10
- package/dist/commands/cli-io.js +29 -0
- package/dist/commands/correct.js +4 -5
- package/dist/commands/editor.js +204 -0
- package/dist/commands/jarvis.js +7 -8
- package/dist/commands/local-args.js +36 -2
- package/dist/commands/local-discovery.js +20 -0
- package/dist/commands/local-help.js +13 -2
- package/dist/commands/local.js +47 -1
- package/dist/commands/notes.js +9 -2
- package/dist/commands/public-root.js +1 -1
- package/dist/commands/scout-render.js +20 -18
- package/dist/commands/scout.js +4 -4
- package/dist/commands/sessions.js +45 -1
- package/dist/commands/settings-render.js +9 -4
- package/dist/commands/settings.js +6 -0
- package/dist/commands/text-width.js +108 -0
- package/dist/commands/workbook-render.js +34 -17
- package/dist/commands/workbook.js +6 -4
- package/dist/evidence-upload-client.js +42 -3
- package/dist/local-state.js +27 -2
- package/dist/upload-agent-artifacts.js +27 -2
- package/dist/upload-failure-reason.js +121 -0
- package/dist/upload-http.js +28 -0
- package/dist/upload.js +122 -21
- package/package.json +1 -1
|
@@ -1009,16 +1009,45 @@ function parseBriefArgs(args) {
|
|
|
1009
1009
|
"--full",
|
|
1010
1010
|
"--versions",
|
|
1011
1011
|
"--claims",
|
|
1012
|
+
"--reason",
|
|
1013
|
+
"--wait",
|
|
1014
|
+
"--no-wait",
|
|
1012
1015
|
"--json",
|
|
1013
1016
|
],
|
|
1014
|
-
valueFlags: ["--home", "--dashboard-url", "--for", "--as", "--version"],
|
|
1017
|
+
valueFlags: ["--home", "--dashboard-url", "--for", "--as", "--version", "--reason"],
|
|
1015
1018
|
});
|
|
1016
|
-
|
|
1019
|
+
// Bare `cockpit brief` reads the page, which is what somebody typing it almost
|
|
1020
|
+
// always wants — the same shape `cockpit notes` and `cockpit scout` have.
|
|
1021
|
+
const first = values.positionals[0];
|
|
1022
|
+
const action = (first === undefined ? "read" : first);
|
|
1023
|
+
if (!["read", "edit", "rewrite"].includes(action)) {
|
|
1024
|
+
throw new Error(`Unknown brief command: ${first}. Try edit or rewrite, or nothing to read it.`);
|
|
1025
|
+
}
|
|
1026
|
+
if (values.positionals.length > (first === undefined ? 0 : 1)) {
|
|
1027
|
+
throw new Error(`brief ${action} does not take "${values.positionals[1]}".`);
|
|
1028
|
+
}
|
|
1017
1029
|
const tldr = values.booleans.has("--tldr");
|
|
1018
1030
|
const full = values.booleans.has("--full");
|
|
1019
1031
|
if (tldr && full) {
|
|
1020
1032
|
throw new Error("brief accepts either --tldr or --full, not both.");
|
|
1021
1033
|
}
|
|
1034
|
+
const wait = values.booleans.has("--wait");
|
|
1035
|
+
const noWait = values.booleans.has("--no-wait");
|
|
1036
|
+
if (wait && noWait) {
|
|
1037
|
+
throw new Error("brief rewrite accepts either --wait or --no-wait, not both.");
|
|
1038
|
+
}
|
|
1039
|
+
const reason = optionalNonEmpty(values.flags.get("--reason"));
|
|
1040
|
+
if (reason && reason.length > 280) {
|
|
1041
|
+
// The server's own ceiling (`REASON_MAX`). Said here so the person is told
|
|
1042
|
+
// before the page is opened rather than after they have finished editing.
|
|
1043
|
+
throw new Error("A reason is limited to 280 characters.");
|
|
1044
|
+
}
|
|
1045
|
+
if (action !== "edit" && reason) {
|
|
1046
|
+
throw new Error("--reason belongs to `cockpit brief edit`.");
|
|
1047
|
+
}
|
|
1048
|
+
if (action !== "rewrite" && (wait || noWait)) {
|
|
1049
|
+
throw new Error("--wait and --no-wait belong to `cockpit brief rewrite`.");
|
|
1050
|
+
}
|
|
1022
1051
|
// `--as` is accepted as an alias so the two conversational commands read the
|
|
1023
1052
|
// same way; `cockpit jarvis --as <person>` has meant this since BLI-3380.
|
|
1024
1053
|
const forPerson = optionalNonEmpty(values.flags.get("--for"));
|
|
@@ -1028,6 +1057,7 @@ function parseBriefArgs(args) {
|
|
|
1028
1057
|
}
|
|
1029
1058
|
return {
|
|
1030
1059
|
kind: "brief",
|
|
1060
|
+
action,
|
|
1031
1061
|
homeDir: optionalNonEmpty(values.flags.get("--home")),
|
|
1032
1062
|
dashboardUrl: optionalUrl(values.flags.get("--dashboard-url")),
|
|
1033
1063
|
subject: forPerson ?? asPerson,
|
|
@@ -1035,6 +1065,10 @@ function parseBriefArgs(args) {
|
|
|
1035
1065
|
tldr,
|
|
1036
1066
|
versions: values.booleans.has("--versions"),
|
|
1037
1067
|
claims: values.booleans.has("--claims"),
|
|
1068
|
+
...(reason ? { reason } : {}),
|
|
1069
|
+
// Waiting is the default; only an explicit `--no-wait` turns it off. A
|
|
1070
|
+
// person who typed `rewrite` wants the page, not a receipt.
|
|
1071
|
+
...(noWait ? { wait: false } : {}),
|
|
1038
1072
|
json: values.booleans.has("--json"),
|
|
1039
1073
|
};
|
|
1040
1074
|
}
|
|
@@ -38,6 +38,26 @@ export async function discoverCommandWorktrees(repoRoot, discovery = {}, io) {
|
|
|
38
38
|
throw new Error(message);
|
|
39
39
|
}
|
|
40
40
|
if (worktrees.length === 0 && !discovery.allowEmpty) {
|
|
41
|
+
// "No git repos found" is only true when the scan could actually SEE
|
|
42
|
+
// everywhere it looked. With unreadable folders in hand, the sentence sent
|
|
43
|
+
// the operator to the wrong repair — "run from a git repo", when the repos
|
|
44
|
+
// may be sitting right there behind a permission wall (BLI-3483). The
|
|
45
|
+
// warning above prints the folders; this refusal has to carry the fact,
|
|
46
|
+
// because it is the line that ends the command.
|
|
47
|
+
if (result.unreadable_dirs.length > 0) {
|
|
48
|
+
console.error("[local-discovery] scan found no repos while folders were unopenable", JSON.stringify({
|
|
49
|
+
reason: "empty_scan_with_unreadable_dirs",
|
|
50
|
+
unreadable_dir_count: result.unreadable_dirs.length,
|
|
51
|
+
codes: [...new Set(result.unreadable_dirs.map((dir) => dir.code))],
|
|
52
|
+
}));
|
|
53
|
+
throw new Error([
|
|
54
|
+
`No git repos found, and ${result.unreadable_dirs.length} folder(s) could not be opened — so this is not proof there are none.`,
|
|
55
|
+
...(io
|
|
56
|
+
? ["The folders are listed in the warning above."]
|
|
57
|
+
: ["Run `cockpit doctor` to see which folders."]),
|
|
58
|
+
"Fix the permissions on those folders and run the command again, or point --workspace at the project folders you can read.",
|
|
59
|
+
].join("\n"));
|
|
60
|
+
}
|
|
41
61
|
throw new Error("No git repos found. Run from a git repo, or from a parent folder containing git repos.");
|
|
42
62
|
}
|
|
43
63
|
return worktrees;
|
|
@@ -60,7 +60,7 @@ export function localCommandHelp(command) {
|
|
|
60
60
|
" cockpit settings [personal [--chat-model <key>] [--brief-model <key>] | switches [set <key> <value>] | models [set --chat <key>] [--memory <id>] | env list|set --project <p> --file <f> --content-stdin|delete --id <uuid> [--yes]] [--json]",
|
|
61
61
|
" cockpit team [members | invite <email> --role <role> [--team-id <uuid>] | role <userId> --role <role> [--yes]] [--json]",
|
|
62
62
|
" cockpit workbook [<project> [<doc>]] [--section <id>] [--markdown] [--width <n>] [--dashboard-url <url>] [--json]",
|
|
63
|
-
" cockpit brief [--for <person>] [--version <pageId>] [--tldr|--full] [--versions] [--claims] [--dashboard-url <url>] [--json]",
|
|
63
|
+
" cockpit brief [edit|rewrite] [--for <person>] [--version <pageId>] [--tldr|--full] [--versions] [--claims] [--reason \"<why>\"] [--wait|--no-wait] [--dashboard-url <url>] [--json]",
|
|
64
64
|
" cockpit correct --claim <claimId> --text \"<what is wrong>\" [--for <person>] [--version <pageId>] [--supersedes <id>] [--dashboard-url <url>] [--json]",
|
|
65
65
|
" cockpit notes [list|show <id>|shelf|shelves|upload <paths...>|paste|share <id>|unshare <id>|move <id>] [--series <shelf>] [--kind <kind>] [--since <YYYY-MM-DD>] [--until <YYYY-MM-DD>] [--limit <n>] [--file <path>] [--name <n>] [--exclude \"<sentence>\"] [--to \"<shelf>\"|--clear-shelf] [--yes] [--dashboard-url <url>] [--json]",
|
|
66
66
|
" cockpit backfill (--since-days <n>|--all) [--source codex|claude] [--dry-run] [--max-files <n>] [--max-depth <n>] [--max-repos <n>] [--yes] [--workspace <path>] [--json]",
|
|
@@ -326,7 +326,7 @@ function localSubcommandHelp(command) {
|
|
|
326
326
|
[
|
|
327
327
|
"brief",
|
|
328
328
|
[
|
|
329
|
-
"Usage: cockpit brief [--for <person>] [--version <pageId>] [--tldr|--full] [--versions] [--claims] [--dashboard-url <url>] [--json]",
|
|
329
|
+
"Usage: cockpit brief [edit|rewrite] [--for <person>] [--version <pageId>] [--tldr|--full] [--versions] [--claims] [--reason \"<why>\"] [--wait|--no-wait] [--dashboard-url <url>] [--json]",
|
|
330
330
|
"",
|
|
331
331
|
"Prints the TODAY page — the same page the Tower website shows, rendered for a terminal.",
|
|
332
332
|
"--for opens somebody else's page; the website's own rule decides whether you may, and it refuses in plain words when you may not.",
|
|
@@ -334,6 +334,17 @@ function localSubcommandHelp(command) {
|
|
|
334
334
|
"--version <pageId> steps back to one exact earlier version; --versions lists them with their ids.",
|
|
335
335
|
"--claims prints the [claimId] beside every line, which is what `cockpit correct --claim` takes.",
|
|
336
336
|
"Reading it here counts as opening it, exactly as opening it in a browser does.",
|
|
337
|
+
"",
|
|
338
|
+
"edit — opens the page in $VISUAL/$EDITOR as a document: one [claimId] line per sentence you may rewrite, everything else a # comment. Change the words after an id, save, close. Only the lines that really changed are sent, so an accidental save writes nothing.",
|
|
339
|
+
" An id you delete or invent is refused by name BEFORE anything is sent: a missing line reads as no opinion at all, not as a sentence to remove, and Tower cannot tell those apart.",
|
|
340
|
+
" --reason \"<why>\" rides on every row, exactly like a commit message. Optional; the before-and-after already teaches on its own.",
|
|
341
|
+
" With no editor, or with something piped in, the edited document is read from stdin instead — `cockpit brief edit < page.md`.",
|
|
342
|
+
" The edit IS the correction: each changed line is filed in the ledger before the page is republished, so a publish that falls over never costs you the edit.",
|
|
343
|
+
"",
|
|
344
|
+
"rewrite — asks Tower to compile the page again and waits for the new version. Writing a page takes minutes, so this queues the work and watches your page rather than holding one long request open.",
|
|
345
|
+
" --no-wait returns as soon as the ask is on the record. Waiting is the default.",
|
|
346
|
+
" Running out of patience is not a failure and says so: the work is still going, and `cockpit brief` will show it when it lands.",
|
|
347
|
+
" Whose page you may ask about is the website's own rule — your own always, anybody's if you are an admin.",
|
|
337
348
|
"The command uses the existing paired device identity. Run `cockpit login` first if this machine is not paired.",
|
|
338
349
|
],
|
|
339
350
|
],
|
package/dist/commands/local.js
CHANGED
|
@@ -22,6 +22,9 @@
|
|
|
22
22
|
* workbook.ts `cockpit workbook` — the project document library
|
|
23
23
|
* (+ workbook-render.ts, the pure layout half)
|
|
24
24
|
* brief.ts `cockpit brief` — the TODAY page in the terminal
|
|
25
|
+
* brief-edit.ts `cockpit brief edit` — rewrite it like a document
|
|
26
|
+
* brief-rewrite.ts `cockpit brief rewrite` — have Tower compile it again
|
|
27
|
+
* editor.ts the one place a person's own $EDITOR is spawned
|
|
25
28
|
* correct.ts `cockpit correct` — one line on it is wrong
|
|
26
29
|
* notes.ts `cockpit notes` — the meeting-notes surface, typed
|
|
27
30
|
* notes-file.ts the local screen a note file passes before it is sent
|
|
@@ -52,6 +55,8 @@ import { runTeam } from "./team.js";
|
|
|
52
55
|
import { asRecord, callTower, openTower } from "./tower-command.js";
|
|
53
56
|
import { runWorkbook } from "./workbook.js";
|
|
54
57
|
import { runBrief } from "./brief.js";
|
|
58
|
+
import { runBriefEdit } from "./brief-edit.js";
|
|
59
|
+
import { runBriefRewrite } from "./brief-rewrite.js";
|
|
55
60
|
import { runCorrect } from "./correct.js";
|
|
56
61
|
import { runNotes } from "./notes.js";
|
|
57
62
|
import { createCollectorServer } from "../server.js";
|
|
@@ -133,6 +138,14 @@ export async function runLocalCockpitCli(argv, io = defaultIo()) {
|
|
|
133
138
|
case "workbook":
|
|
134
139
|
return await runWorkbook(command, io);
|
|
135
140
|
case "brief":
|
|
141
|
+
// BLI-3458 S2.7/S2.8: `edit` and `rewrite` are the two PANEL actions
|
|
142
|
+
// reaching the terminal. Dispatched here rather than inside `brief.ts`
|
|
143
|
+
// so each one stays its own module — `brief.ts` owns reading and
|
|
144
|
+
// nothing else.
|
|
145
|
+
if (command.action === "edit")
|
|
146
|
+
return await runBriefEdit(command, io);
|
|
147
|
+
if (command.action === "rewrite")
|
|
148
|
+
return await runBriefRewrite(command, io);
|
|
136
149
|
return await runBrief(command, io);
|
|
137
150
|
case "correct":
|
|
138
151
|
return await runCorrect(command, io);
|
|
@@ -155,6 +168,17 @@ export async function runLocalCockpitCli(argv, io = defaultIo()) {
|
|
|
155
168
|
}
|
|
156
169
|
}
|
|
157
170
|
catch (error) {
|
|
171
|
+
// The outermost net under every subcommand. It printed `error.message` and
|
|
172
|
+
// dropped the name, the code, the syscall and the cause — and never said
|
|
173
|
+
// WHICH subcommand died, so a scheduled `cockpit sync` and a hand-typed
|
|
174
|
+
// `cockpit doctor` failing the same way were indistinguishable in
|
|
175
|
+
// sync.err.log (BLI-3483). The human line is unchanged; the structured one
|
|
176
|
+
// beside it carries names and codes only, scrubbed by `describeError`.
|
|
177
|
+
console.error("[cockpit-cli] command failed", JSON.stringify({
|
|
178
|
+
reason: "command_failed",
|
|
179
|
+
command: command.kind,
|
|
180
|
+
...describeError(error),
|
|
181
|
+
}));
|
|
158
182
|
writeLine(io.stderr, errorMessage(error));
|
|
159
183
|
return 1;
|
|
160
184
|
}
|
|
@@ -1115,8 +1139,26 @@ async function runAutostartSelfHealAfterSync(command, io, dashboardUrl) {
|
|
|
1115
1139
|
let result;
|
|
1116
1140
|
try {
|
|
1117
1141
|
const rawExec = io.exec;
|
|
1118
|
-
if (!rawExec)
|
|
1142
|
+
if (!rawExec) {
|
|
1143
|
+
// BLI-3483: this was a bare `return`. On Windows the self-heal is the
|
|
1144
|
+
// only thing that puts a broken scheduler back, so abandoning it here
|
|
1145
|
+
// meant a machine could stop collecting forever and leave no receipt
|
|
1146
|
+
// anywhere — the exact shape the fleet contract forbids. The packed CLI
|
|
1147
|
+
// always supplies a runner (`commands/cli-io.ts`), so this fires only for
|
|
1148
|
+
// an embedder that built its own `io`; it costs one line either way.
|
|
1149
|
+
console.error("[autostart-self-heal] no process runner on this io; the repair could not be attempted", JSON.stringify({
|
|
1150
|
+
reason: "runner_unavailable",
|
|
1151
|
+
platform: process.platform,
|
|
1152
|
+
next_action: "reinstall the CLI (npm i -g @bli-cockpit/cli) and run `cockpit autostart install`",
|
|
1153
|
+
}));
|
|
1154
|
+
result = {
|
|
1155
|
+
status: "skipped",
|
|
1156
|
+
reason: "runner_unavailable",
|
|
1157
|
+
detail: "No process runner available to this CLI invocation; run `cockpit autostart install` by hand.",
|
|
1158
|
+
};
|
|
1159
|
+
await reportAutostartSelfHealOutcome(command, io, dashboardUrl, result);
|
|
1119
1160
|
return;
|
|
1161
|
+
}
|
|
1120
1162
|
const spawnEnv = envWithNodeRuntimeOnPath(io.env ?? process.env);
|
|
1121
1163
|
const exec = (cmd, args, options) => rawExec(cmd, args, { ...options, env: options?.env ?? spawnEnv });
|
|
1122
1164
|
result = await runAutostartSelfHeal(getCollectorRuntimePaths(command.homeDir), {
|
|
@@ -1137,6 +1179,10 @@ async function runAutostartSelfHealAfterSync(command, io, dashboardUrl) {
|
|
|
1137
1179
|
// throttle are silent; an actual repair attempt reports either way.
|
|
1138
1180
|
if (!result || result.reason === "repair_throttled_recent_attempt")
|
|
1139
1181
|
return;
|
|
1182
|
+
await reportAutostartSelfHealOutcome(command, io, dashboardUrl, result);
|
|
1183
|
+
}
|
|
1184
|
+
/** One receipt for the repair, whichever branch above produced the outcome. */
|
|
1185
|
+
async function reportAutostartSelfHealOutcome(command, io, dashboardUrl, result) {
|
|
1140
1186
|
await reportInstallEventsBestEffort({
|
|
1141
1187
|
homeDir: command.homeDir,
|
|
1142
1188
|
dashboardUrl,
|
package/dist/commands/notes.js
CHANGED
|
@@ -293,8 +293,14 @@ async function pasteNote(command, door) {
|
|
|
293
293
|
form.set("name", command.name);
|
|
294
294
|
if (command.exclude)
|
|
295
295
|
form.set("exclusions", command.exclude);
|
|
296
|
-
|
|
297
|
-
|
|
296
|
+
// Bytes, not `.length` (BLI-3482). `NOTE_SLOW_UPLOAD_BYTES` is a BYTE
|
|
297
|
+
// threshold, and the file path above already compares `bytes.byteLength`
|
|
298
|
+
// against it; `text.length` counts UTF-16 units, so a note in any non-Latin
|
|
299
|
+
// script was measured at a third to a half of the size actually being sent
|
|
300
|
+
// and the "this will take a while" line stayed silent through the wait.
|
|
301
|
+
const pastedBytes = Buffer.byteLength(text, "utf8");
|
|
302
|
+
if (pastedBytes >= NOTE_SLOW_UPLOAD_BYTES) {
|
|
303
|
+
writeLine(door.io.stderr, `Reading ${Math.round(pastedBytes / 1024)} KB of pasted text. This can take a couple of minutes.`);
|
|
298
304
|
}
|
|
299
305
|
const answer = await ask(door, {
|
|
300
306
|
path: "/api/notes/upload",
|
|
@@ -311,6 +317,7 @@ async function pasteNote(command, door) {
|
|
|
311
317
|
note_id: body.noteId ?? null,
|
|
312
318
|
scope: body.scope ?? null,
|
|
313
319
|
chars: text.length,
|
|
320
|
+
byte_size: pastedBytes,
|
|
314
321
|
named_by_caller: Boolean(command.name),
|
|
315
322
|
})}`);
|
|
316
323
|
if (door.json)
|
|
@@ -15,7 +15,7 @@ export async function runCockpitCli(argv, io) {
|
|
|
15
15
|
}
|
|
16
16
|
|
|
17
17
|
if (command === "--version" || command === "-V" || command === "version") {
|
|
18
|
-
writeLine(io?.stdout ?? process.stdout, "0.2.
|
|
18
|
+
writeLine(io?.stdout ?? process.stdout, "0.2.42");
|
|
19
19
|
return 0;
|
|
20
20
|
}
|
|
21
21
|
|
|
@@ -15,12 +15,15 @@
|
|
|
15
15
|
* **A bounded read says so.** The server returns at most 8 cards and 12
|
|
16
16
|
* signals; when there are more, `truncationLines` prints how many. A quiet
|
|
17
17
|
* board and a truncated board must never look alike.
|
|
18
|
+
*
|
|
19
|
+
* **Styling arrives as an argument, never as a global.** `styled` says whether
|
|
20
|
+
* this run's stdout is a terminal that asked for colour (BLI-3482); `scout.ts`
|
|
21
|
+
* decides it once with `colorEnabled(io)` and the layout stays deterministic.
|
|
18
22
|
*/
|
|
19
|
-
|
|
20
|
-
const RESET = "\x1b[0m";
|
|
23
|
+
import { dim } from "./cli-io.js";
|
|
21
24
|
// ------------------------------------------------------------------ rendering
|
|
22
25
|
/** The whole board as terminal lines, in the page's three sections and order. */
|
|
23
|
-
export function renderScoutBoard(payload) {
|
|
26
|
+
export function renderScoutBoard(payload, styled) {
|
|
24
27
|
const board = payload.board ?? {};
|
|
25
28
|
const lines = payload.lines ?? {};
|
|
26
29
|
const out = [];
|
|
@@ -35,7 +38,7 @@ export function renderScoutBoard(payload) {
|
|
|
35
38
|
}
|
|
36
39
|
else {
|
|
37
40
|
for (const card of experiments)
|
|
38
|
-
out.push(...renderExperiment(card));
|
|
41
|
+
out.push(...renderExperiment(card, styled));
|
|
39
42
|
}
|
|
40
43
|
const settled = board.settled ?? [];
|
|
41
44
|
if (settled.length > 0) {
|
|
@@ -51,14 +54,14 @@ export function renderScoutBoard(payload) {
|
|
|
51
54
|
}
|
|
52
55
|
else {
|
|
53
56
|
for (const signal of signals)
|
|
54
|
-
out.push(...renderSignal(signal));
|
|
57
|
+
out.push(...renderSignal(signal, styled));
|
|
55
58
|
}
|
|
56
59
|
const truncation = truncationLines(board);
|
|
57
60
|
if (truncation.length > 0)
|
|
58
61
|
out.push("", ...truncation);
|
|
59
62
|
return out;
|
|
60
63
|
}
|
|
61
|
-
function renderExperiment(card) {
|
|
64
|
+
function renderExperiment(card, styled) {
|
|
62
65
|
const sourceCount = card.sourceCount ?? 0;
|
|
63
66
|
const head = [
|
|
64
67
|
shortId(card.id ?? ""),
|
|
@@ -74,23 +77,25 @@ function renderExperiment(card) {
|
|
|
74
77
|
if (card.claimSummary)
|
|
75
78
|
out.push(` ${card.claimSummary}`);
|
|
76
79
|
if (card.title)
|
|
77
|
-
out.push(dim(` ${card.title}
|
|
80
|
+
out.push(dim(` ${card.title}`, styled));
|
|
78
81
|
for (const row of card.crossref ?? []) {
|
|
79
|
-
if (row.person)
|
|
80
|
-
out.push(dim(` On your team · ${row.person} · ${row.observedPattern ?? ""}
|
|
82
|
+
if (row.person) {
|
|
83
|
+
out.push(dim(` On your team · ${row.person} · ${row.observedPattern ?? ""}`, styled));
|
|
84
|
+
}
|
|
81
85
|
}
|
|
82
86
|
if (card.expectedPayoff)
|
|
83
|
-
out.push(dim(` Worth trying · ${card.expectedPayoff}
|
|
87
|
+
out.push(dim(` Worth trying · ${card.expectedPayoff}`, styled));
|
|
84
88
|
for (const source of card.sources ?? []) {
|
|
85
|
-
if (source.title)
|
|
86
|
-
out.push(dim(` ${source.title} · ${source.source ?? ""} · ${source.url ?? ""}
|
|
89
|
+
if (source.title) {
|
|
90
|
+
out.push(dim(` ${source.title} · ${source.source ?? ""} · ${source.url ?? ""}`, styled));
|
|
91
|
+
}
|
|
87
92
|
}
|
|
88
93
|
return out;
|
|
89
94
|
}
|
|
90
|
-
function renderSignal(signal) {
|
|
95
|
+
function renderSignal(signal, styled) {
|
|
91
96
|
const out = [` ⏺ ${signal.title ?? "untitled"} · ${dayStamp(signal.gatheredAt)}`];
|
|
92
97
|
if (signal.synopsis)
|
|
93
|
-
out.push(dim(` ${signal.synopsis}
|
|
98
|
+
out.push(dim(` ${signal.synopsis}`, styled));
|
|
94
99
|
const foot = [
|
|
95
100
|
signal.source,
|
|
96
101
|
signal.evidenceStrength ?? undefined,
|
|
@@ -99,7 +104,7 @@ function renderSignal(signal) {
|
|
|
99
104
|
signal.url,
|
|
100
105
|
].filter((part) => Boolean(part));
|
|
101
106
|
if (foot.length > 0)
|
|
102
|
-
out.push(dim(` ${foot.join(" · ")}
|
|
107
|
+
out.push(dim(` ${foot.join(" · ")}`, styled));
|
|
103
108
|
return out;
|
|
104
109
|
}
|
|
105
110
|
/**
|
|
@@ -164,9 +169,6 @@ export function shortId(id) {
|
|
|
164
169
|
function dayStamp(iso) {
|
|
165
170
|
return iso ? iso.slice(0, 10) : "";
|
|
166
171
|
}
|
|
167
|
-
function dim(text) {
|
|
168
|
-
return `${DIM}${text}${RESET}`;
|
|
169
|
-
}
|
|
170
172
|
function indent(text) {
|
|
171
173
|
return ` ${text}`;
|
|
172
174
|
}
|
package/dist/commands/scout.js
CHANGED
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
* as the page is; moving a card is super_admin whichever surface asks, and a
|
|
17
17
|
* refusal never costs a person the board.
|
|
18
18
|
*/
|
|
19
|
-
import { writeLine } from "./cli-io.js";
|
|
19
|
+
import { colorEnabled, writeLine } from "./cli-io.js";
|
|
20
20
|
import { renderScoutBoard, resolveExperimentRef, refusalSentence, shortId, truncationLines, } from "./scout-render.js";
|
|
21
21
|
import { loadPairedSession, towerFailureDetail, towerJsonRequest, } from "../tower-client.js";
|
|
22
22
|
/** The dashboard route caps nothing here, but a board read should never hang a shell. */
|
|
@@ -49,7 +49,7 @@ export async function runScout(command, io) {
|
|
|
49
49
|
writeLine(io.stdout, JSON.stringify({ ok: true, audience: payload.audience ?? null, windowDays: payload.windowDays ?? board.windowDays ?? null, board, lines }));
|
|
50
50
|
}
|
|
51
51
|
else {
|
|
52
|
-
for (const line of renderScoutBoard(payload))
|
|
52
|
+
for (const line of renderScoutBoard(payload, colorEnabled(io)))
|
|
53
53
|
writeLine(io.stdout, line);
|
|
54
54
|
}
|
|
55
55
|
writeLine(io.stderr, `[scout cli] board read ${JSON.stringify({
|
|
@@ -78,7 +78,7 @@ export async function runScout(command, io) {
|
|
|
78
78
|
}
|
|
79
79
|
else {
|
|
80
80
|
writeLine(io.stderr, sentence);
|
|
81
|
-
for (const line of renderScoutBoard(payload))
|
|
81
|
+
for (const line of renderScoutBoard(payload, colorEnabled(io)))
|
|
82
82
|
writeLine(io.stdout, line);
|
|
83
83
|
}
|
|
84
84
|
return 1;
|
|
@@ -119,7 +119,7 @@ export async function runScout(command, io) {
|
|
|
119
119
|
if (applied.httpStatus === 403) {
|
|
120
120
|
writeLine(io.stderr, "Deciding a Scout card is a super_admin action; reading the board is not.");
|
|
121
121
|
}
|
|
122
|
-
for (const line of renderScoutBoard(payload))
|
|
122
|
+
for (const line of renderScoutBoard(payload, colorEnabled(io)))
|
|
123
123
|
writeLine(io.stdout, line);
|
|
124
124
|
return 1;
|
|
125
125
|
}
|
|
@@ -71,9 +71,16 @@ export async function runSessions(command, io) {
|
|
|
71
71
|
reason: sidecar.skipped_reason,
|
|
72
72
|
})),
|
|
73
73
|
}));
|
|
74
|
+
// BLI-3483: the scanners have always counted the folders and files they
|
|
75
|
+
// could not read, and this command has always thrown those counts away
|
|
76
|
+
// except for one field in `--json`. So "No sessions observed" was printed on
|
|
77
|
+
// a machine where the scan had been blocked from looking — the operator's
|
|
78
|
+
// one diagnostic tool answering the question with the failure removed.
|
|
79
|
+
const readFailures = countReadFailures(codex, claude);
|
|
74
80
|
if (command.json) {
|
|
75
81
|
writeLine(io.stdout, JSON.stringify({
|
|
76
82
|
window,
|
|
83
|
+
read_failures: readFailures,
|
|
77
84
|
...(codex
|
|
78
85
|
? { codex: { counts: codex.counts, sessions: codexRows } }
|
|
79
86
|
: {}),
|
|
@@ -101,10 +108,47 @@ export async function runSessions(command, io) {
|
|
|
101
108
|
}
|
|
102
109
|
}
|
|
103
110
|
if (codexRows.length === 0 && claudeRows.length === 0) {
|
|
104
|
-
writeLine(io.stdout,
|
|
111
|
+
writeLine(io.stdout, readFailures.total > 0
|
|
112
|
+
? `No sessions listed, but ${readFailures.total} read failure(s) mean the scan could not see everywhere — this is not proof there were none.`
|
|
113
|
+
: "No sessions observed in the scan window.");
|
|
114
|
+
}
|
|
115
|
+
if (readFailures.total > 0) {
|
|
116
|
+
for (const line of readFailureLines(readFailures)) {
|
|
117
|
+
writeLine(io.stdout, line);
|
|
118
|
+
}
|
|
105
119
|
}
|
|
106
120
|
return 0;
|
|
107
121
|
}
|
|
122
|
+
/**
|
|
123
|
+
* Everything the two scanners looked at and could not read. Counts only — the
|
|
124
|
+
* project-dir slug encodes a local path and never leaves this machine.
|
|
125
|
+
*/
|
|
126
|
+
function countReadFailures(codex, claude) {
|
|
127
|
+
const failures = {
|
|
128
|
+
codex_directory_read_failed: codex?.directory_read_failed_count ?? 0,
|
|
129
|
+
codex_stat_failed: codex?.stat_failed_count ?? 0,
|
|
130
|
+
codex_secret_path_skipped: codex?.secret_path_skipped_count ?? 0,
|
|
131
|
+
claude_project_dir_read_failed: claude?.project_dir_read_failed_count ?? 0,
|
|
132
|
+
claude_project_dirs_skipped: claude?.project_dirs_skipped ?? 0,
|
|
133
|
+
claude_session_stat_failed: claude?.session_stat_failed_count ?? 0,
|
|
134
|
+
claude_sidecar_dir_read_failed: claude?.sidecar_dir_read_failed_count ?? 0,
|
|
135
|
+
claude_sidecar_stat_failed: claude?.sidecar_stat_failed_count ?? 0,
|
|
136
|
+
};
|
|
137
|
+
return {
|
|
138
|
+
total: Object.values(failures).reduce((sum, count) => sum + count, 0),
|
|
139
|
+
...failures,
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
function readFailureLines(failures) {
|
|
143
|
+
const named = Object.entries(failures)
|
|
144
|
+
.filter(([field, count]) => field !== "total" && Number(count) > 0)
|
|
145
|
+
.map(([field, count]) => ` ${field}: ${count}`);
|
|
146
|
+
return [
|
|
147
|
+
`read failures during this scan (${failures.total}) — sessions under these are not listed:`,
|
|
148
|
+
...named,
|
|
149
|
+
" a permission wall is the usual cause; on macOS grant Full Disk Access, on Windows check the folder ACL",
|
|
150
|
+
];
|
|
151
|
+
}
|
|
108
152
|
/** `--all`, an explicit `--since-days` capped at pairing, or the default window. */
|
|
109
153
|
async function sessionsScanWindow(command, now) {
|
|
110
154
|
if (command.all) {
|
|
@@ -8,7 +8,12 @@
|
|
|
8
8
|
* Every one of these is defensive about shape on purpose. These bodies come off
|
|
9
9
|
* the wire from a dashboard that may be a release ahead or behind the installed
|
|
10
10
|
* CLI, and a missing field should cost one line of output, not the command.
|
|
11
|
+
*
|
|
12
|
+
* Columns are padded in SCREEN CELLS, not UTF-16 code units (BLI-3482) — a
|
|
13
|
+
* display name written in another script is exactly the row where `padEnd`
|
|
14
|
+
* silently stops lining up.
|
|
11
15
|
*/
|
|
16
|
+
import { padEndDisplay } from "./text-width.js";
|
|
12
17
|
const INDENT = " ";
|
|
13
18
|
function text(value, fallback = "—") {
|
|
14
19
|
if (typeof value === "string" && value.trim())
|
|
@@ -82,7 +87,7 @@ export function renderTeamMembers(body) {
|
|
|
82
87
|
for (const person of people) {
|
|
83
88
|
const standing = text(person.standing, "");
|
|
84
89
|
const name = text(person.displayName, text(person.email));
|
|
85
|
-
lines.push(`${INDENT}${name
|
|
90
|
+
lines.push(`${INDENT}${padEndDisplay(name, 28)} ${padEndDisplay(text(person.role, "—"), 20)} ${standing}`);
|
|
86
91
|
if (typeof person.userId === "string") {
|
|
87
92
|
lines.push(`${INDENT}${INDENT}id ${person.userId}`);
|
|
88
93
|
}
|
|
@@ -101,7 +106,7 @@ export function renderSwitches(body) {
|
|
|
101
106
|
const options = Array.isArray(entry.options)
|
|
102
107
|
? entry.options.map((option) => text(record(option).value)).join(" | ")
|
|
103
108
|
: "";
|
|
104
|
-
return `${INDENT}${text(entry.key)
|
|
109
|
+
return `${INDENT}${padEndDisplay(text(entry.key), 24)} ${padEndDisplay(text(entry.value), 12)} (${text(entry.source)}${options ? `; choices: ${options}` : ""})`;
|
|
105
110
|
});
|
|
106
111
|
if (typeof body.readFailureReason === "string") {
|
|
107
112
|
lines.push(`${INDENT}values could not be read: ${body.readFailureReason}`);
|
|
@@ -114,7 +119,7 @@ export function renderModelRouting(body) {
|
|
|
114
119
|
`${INDENT}memory ${text(body.memoryEffectiveLabel)} (${text(body.memorySource)}${typeof body.memoryVia === "string" ? ` via ${body.memoryVia}` : ""})`,
|
|
115
120
|
];
|
|
116
121
|
for (const slot of list(body.displaySlots)) {
|
|
117
|
-
lines.push(`${INDENT}${text(slot.name)
|
|
122
|
+
lines.push(`${INDENT}${padEndDisplay(text(slot.name), 24)} ${text(slot.currentLabel)} — set in code (${text(slot.pointer)})`);
|
|
118
123
|
}
|
|
119
124
|
if (typeof body.readFailureReason === "string") {
|
|
120
125
|
lines.push(`${INDENT}settings could not be read: ${body.readFailureReason}`);
|
|
@@ -132,6 +137,6 @@ export function renderEnvBlobs(body) {
|
|
|
132
137
|
const blobs = list(body.blobs);
|
|
133
138
|
if (blobs.length === 0)
|
|
134
139
|
return [`${INDENT}no env files stored`];
|
|
135
|
-
return blobs.map((blob) => `${INDENT}${text(blob.project)
|
|
140
|
+
return blobs.map((blob) => `${INDENT}${padEndDisplay(text(blob.project), 12)} ${padEndDisplay(text(blob.file_name), 16)} ` +
|
|
136
141
|
`updated ${text(blob.updated_at)} id ${text(blob.id)}`);
|
|
137
142
|
}
|
|
@@ -213,6 +213,12 @@ async function setModels(command, tower, io) {
|
|
|
213
213
|
return 0;
|
|
214
214
|
}
|
|
215
215
|
writeLine(io.stdout, `Saved: ${asList(body.saved).join(", ") || "nothing"}.`);
|
|
216
|
+
// BLI-3481: the save succeeded but the belt pre-flight could not reach the
|
|
217
|
+
// provider, so nobody has checked that the model just chosen will accept
|
|
218
|
+
// Tower's tool belt. A bare "Saved." there would be a silent success.
|
|
219
|
+
if (typeof body.warning === "string" && body.warning.length > 0) {
|
|
220
|
+
writeLine(io.stderr, body.warning);
|
|
221
|
+
}
|
|
216
222
|
return 0;
|
|
217
223
|
}
|
|
218
224
|
// ── env files ─────────────────────────────────────────────────────────
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* How wide a string is ON SCREEN, in terminal cells (BLI-3482).
|
|
3
|
+
*
|
|
4
|
+
* `"".length` counts UTF-16 code units, which is the wrong number twice over
|
|
5
|
+
* for a column: an emoji costs two units and draws two cells, a CJK ideograph
|
|
6
|
+
* costs one unit and draws two, and a combining accent costs one unit and draws
|
|
7
|
+
* nothing. Padding by `.length` therefore ragged-edges any table whose cells are
|
|
8
|
+
* not plain ASCII — the failure a person sees is a column that no longer lines
|
|
9
|
+
* up, on exactly the rows that carry a name or a title in another script.
|
|
10
|
+
*
|
|
11
|
+
* This is a deliberately small wcwidth, not the real one, and its limits are
|
|
12
|
+
* honest:
|
|
13
|
+
*
|
|
14
|
+
* - The wide ranges below are the common East Asian Wide/Fullwidth blocks plus
|
|
15
|
+
* the main emoji planes. Ambiguous-width characters (box drawing, `⏺`, Greek,
|
|
16
|
+
* Cyrillic) are counted as ONE cell, which is what a Western-locale terminal
|
|
17
|
+
* draws; a CJK-locale terminal may draw some of them as two.
|
|
18
|
+
* - A ZWJ emoji sequence (👨👩👧) is measured as the sum of its parts, so it
|
|
19
|
+
* over-counts on terminals that draw the whole cluster in two cells. Nothing
|
|
20
|
+
* here inspects grapheme clusters; that needs `Intl.Segmenter` and a real
|
|
21
|
+
* emoji table, which is more machinery than a column deserves.
|
|
22
|
+
* - U+FE0F (emoji presentation) is counted as zero, so a text-default symbol
|
|
23
|
+
* promoted to emoji presentation is under-counted by one.
|
|
24
|
+
*
|
|
25
|
+
* Every one of those errors costs alignment, never content: nothing in this
|
|
26
|
+
* file may drop, cut or reorder text.
|
|
27
|
+
*/
|
|
28
|
+
/** SGR colour sequences occupy no cells, so they are removed before counting. */
|
|
29
|
+
// eslint-disable-next-line no-control-regex
|
|
30
|
+
const ANSI_SGR = /\x1b\[[0-9;]*m/gu;
|
|
31
|
+
/** Zero-cell code points: combining marks, joiners, variation selectors. */
|
|
32
|
+
const ZERO_WIDTH = [
|
|
33
|
+
[0x0300, 0x036f], // combining diacritical marks
|
|
34
|
+
[0x0483, 0x0489],
|
|
35
|
+
[0x0591, 0x05bd],
|
|
36
|
+
[0x0610, 0x061a],
|
|
37
|
+
[0x064b, 0x065f],
|
|
38
|
+
[0x0670, 0x0670],
|
|
39
|
+
[0x06d6, 0x06dc],
|
|
40
|
+
[0x0e31, 0x0e31],
|
|
41
|
+
[0x0e34, 0x0e3a],
|
|
42
|
+
[0x0e47, 0x0e4e],
|
|
43
|
+
[0x1ab0, 0x1aff], // combining diacritical marks extended
|
|
44
|
+
[0x1dc0, 0x1dff], // combining diacritical marks supplement
|
|
45
|
+
[0x200b, 0x200f], // zero-width space through RTL mark (incl. ZWNJ, ZWJ)
|
|
46
|
+
[0x20d0, 0x20f0], // combining marks for symbols
|
|
47
|
+
[0xfe00, 0xfe0f], // variation selectors
|
|
48
|
+
[0xfe20, 0xfe2f], // combining half marks
|
|
49
|
+
[0xfeff, 0xfeff], // byte-order mark
|
|
50
|
+
];
|
|
51
|
+
/** Two-cell code points: the common Wide / Fullwidth blocks and emoji planes. */
|
|
52
|
+
const WIDE = [
|
|
53
|
+
[0x1100, 0x115f], // Hangul Jamo
|
|
54
|
+
[0x2e80, 0x303e], // CJK radicals, Kangxi, CJK symbols and punctuation
|
|
55
|
+
[0x3041, 0x33ff], // Hiragana, Katakana, Bopomofo, Hangul Compatibility Jamo
|
|
56
|
+
[0x3400, 0x4dbf], // CJK Unified Ideographs Extension A
|
|
57
|
+
[0x4e00, 0x9fff], // CJK Unified Ideographs
|
|
58
|
+
[0xa000, 0xa4cf], // Yi
|
|
59
|
+
[0xa960, 0xa97f], // Hangul Jamo Extended-A
|
|
60
|
+
[0xac00, 0xd7a3], // Hangul syllables
|
|
61
|
+
[0xf900, 0xfaff], // CJK compatibility ideographs
|
|
62
|
+
[0xfe10, 0xfe19], // vertical forms
|
|
63
|
+
[0xfe30, 0xfe6f], // CJK compatibility forms
|
|
64
|
+
[0xff00, 0xff60], // fullwidth forms
|
|
65
|
+
[0xffe0, 0xffe6], // fullwidth signs
|
|
66
|
+
[0x1f300, 0x1f64f], // misc symbols and pictographs, emoticons
|
|
67
|
+
[0x1f680, 0x1f6ff], // transport and map symbols
|
|
68
|
+
[0x1f900, 0x1f9ff], // supplemental symbols and pictographs
|
|
69
|
+
[0x20000, 0x3fffd], // CJK Unified Ideographs Extensions B onward
|
|
70
|
+
];
|
|
71
|
+
function inRanges(code, ranges) {
|
|
72
|
+
for (const [low, high] of ranges) {
|
|
73
|
+
if (code < low)
|
|
74
|
+
return false; // ranges are ascending
|
|
75
|
+
if (code <= high)
|
|
76
|
+
return true;
|
|
77
|
+
}
|
|
78
|
+
return false;
|
|
79
|
+
}
|
|
80
|
+
/** Cells one code point draws: 0 for combining/invisible, 2 for wide, else 1. */
|
|
81
|
+
export function codePointWidth(code) {
|
|
82
|
+
if (code === 0x0a || code === 0x0d)
|
|
83
|
+
return 0;
|
|
84
|
+
if (code < 0x20 || (code >= 0x7f && code < 0xa0))
|
|
85
|
+
return 0; // control characters
|
|
86
|
+
if (inRanges(code, ZERO_WIDTH))
|
|
87
|
+
return 0;
|
|
88
|
+
if (inRanges(code, WIDE))
|
|
89
|
+
return 2;
|
|
90
|
+
return 1;
|
|
91
|
+
}
|
|
92
|
+
/** Cells a string draws, ignoring any SGR colour sequences inside it. */
|
|
93
|
+
export function displayWidth(text) {
|
|
94
|
+
let total = 0;
|
|
95
|
+
for (const character of text.replace(ANSI_SGR, "")) {
|
|
96
|
+
total += codePointWidth(character.codePointAt(0) ?? 0);
|
|
97
|
+
}
|
|
98
|
+
return total;
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* `padEnd` that counts cells instead of code units. Like `padEnd`, a string
|
|
102
|
+
* already at or past the column is returned untouched — a column is a minimum,
|
|
103
|
+
* never a limit, because cutting a cell to fit loses content.
|
|
104
|
+
*/
|
|
105
|
+
export function padEndDisplay(text, width) {
|
|
106
|
+
const missing = width - displayWidth(text);
|
|
107
|
+
return missing > 0 ? `${text}${" ".repeat(missing)}` : text;
|
|
108
|
+
}
|