@bli-cockpit/cli 0.2.57 → 0.2.59
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/browser-open.js +88 -0
- package/dist/commands/docs.js +27 -6
- package/dist/commands/doctor-report.js +17 -1
- package/dist/commands/doctor.js +12 -2
- package/dist/commands/heartbeat.js +65 -1
- package/dist/commands/issue-contracts.js +99 -0
- package/dist/commands/issue-write.js +129 -0
- package/dist/commands/issue.js +189 -0
- package/dist/commands/jarvis-answer-envelope.js +80 -0
- package/dist/commands/jarvis-turn.js +16 -1
- package/dist/commands/jarvis.js +3 -0
- package/dist/commands/local-args-collector-setup.js +17 -0
- package/dist/commands/local-args-tower-admin.js +12 -2
- package/dist/commands/local-args-tower-docs-msg.js +95 -6
- package/dist/commands/local-args-tower-search.js +50 -0
- package/dist/commands/local-args-tower-work.js +178 -0
- package/dist/commands/local-args-tower.js +7 -1
- package/dist/commands/local-args.js +29 -14
- package/dist/commands/local-command-shapes.js +12 -0
- package/dist/commands/local-help-commands.js +643 -0
- package/dist/commands/local-help.js +12 -551
- package/dist/commands/local.js +9 -0
- package/dist/commands/login.js +91 -8
- package/dist/commands/memory-install-claude.js +35 -15
- package/dist/commands/memory-install-codex-hooks.js +200 -0
- package/dist/commands/memory-install-codex.js +12 -2
- package/dist/commands/memory-install-config.js +140 -0
- package/dist/commands/memory-install-receipt.js +222 -0
- package/dist/commands/memory-install-report.js +113 -0
- package/dist/commands/memory-install.js +99 -276
- package/dist/commands/msg.js +85 -2
- package/dist/commands/notes-door.js +120 -0
- package/dist/commands/notes-reads.js +134 -0
- package/dist/commands/notes-writes.js +208 -0
- package/dist/commands/notes.js +16 -442
- package/dist/commands/onboard-completion.js +47 -0
- package/dist/commands/onboard-setup.js +82 -2
- package/dist/commands/ops-render-memory.js +76 -0
- package/dist/commands/ops-render.js +13 -1
- package/dist/commands/ops.js +65 -3
- package/dist/commands/project.js +38 -0
- package/dist/commands/public-root.js +1 -1
- package/dist/commands/search.js +122 -0
- package/dist/commands/setup-receipt-lines.js +71 -0
- package/dist/commands/setup-receipt.js +241 -0
- package/dist/commands/status.js +20 -1
- package/dist/local-state-pairing-code.js +200 -0
- package/dist/local-state.js +6 -0
- package/dist/repo-identity-fingerprint.js +88 -0
- package/dist/repo-identity-git.js +76 -0
- package/dist/repo-identity-linked-worktrees.js +81 -0
- package/dist/repo-identity.js +5 -222
- package/package.json +7 -7
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* How the BLI Memory adoption gauge reads in a terminal (BLI-3729). Pure
|
|
3
|
+
* layout: no io, no network, no clock of its own — the sibling of
|
|
4
|
+
* `ops-render.ts`, split out to keep both under the repo's readability band
|
|
5
|
+
* (`repo-shape/file-size-ratchet.test.ts`).
|
|
6
|
+
*
|
|
7
|
+
* Same rule as the fleet and coverage sections next door: every SENTENCE was
|
|
8
|
+
* written on the server (`lib/ops/memory-usage.ts`) and is printed verbatim;
|
|
9
|
+
* this file decides column widths and order and nothing else.
|
|
10
|
+
*
|
|
11
|
+
* Both public names are re-exported from `./ops-render.js`, so a caller of that
|
|
12
|
+
* module never has to know the split happened.
|
|
13
|
+
*/
|
|
14
|
+
/**
|
|
15
|
+
* Is the org using BLI Memory? One row per person, one column per day, and the
|
|
16
|
+
* two agent sources counted apart from the import — an imported row is a
|
|
17
|
+
* backfill and must never be read as somebody using the thing.
|
|
18
|
+
*/
|
|
19
|
+
export function renderMemoryUsage(memory, dim) {
|
|
20
|
+
const lines = ["", `MEMORY ${memory.summary ?? "(no summary)"}`];
|
|
21
|
+
if (memory.readError) {
|
|
22
|
+
lines.push(` usage could not be read (${memory.readError}); nothing is known about whether anybody is using BLI Memory`);
|
|
23
|
+
return lines;
|
|
24
|
+
}
|
|
25
|
+
const days = memory.days ?? [];
|
|
26
|
+
const people = memory.people ?? [];
|
|
27
|
+
if (people.length === 0) {
|
|
28
|
+
lines.push(dim(" nobody on the roster has any memory activity to show"));
|
|
29
|
+
return lines;
|
|
30
|
+
}
|
|
31
|
+
const nameWidth = Math.max(6, ...people.map((person) => (person.displayName ?? "").length));
|
|
32
|
+
// `MM-DD` per column: the year is the same on every one of seven days and
|
|
33
|
+
// spending four characters on it costs the terminal a column per day.
|
|
34
|
+
const header = ` ${"person".padEnd(nameWidth)} ` +
|
|
35
|
+
days.map((day) => day.slice(5).padStart(5)).join(" ") +
|
|
36
|
+
` ${"mcp".padStart(5)} ${"hook".padStart(5)} ${"impt".padStart(5)}`;
|
|
37
|
+
lines.push(dim(header));
|
|
38
|
+
// Most active first, so the answer to "who is using it" is the top of the
|
|
39
|
+
// list; the unattributed bucket sinks to the bottom whatever its total,
|
|
40
|
+
// because it is not a person and its number is mostly the import.
|
|
41
|
+
const ranked = [...people].sort((left, right) => {
|
|
42
|
+
const leftOrphan = left.personId === null || left.personId === undefined ? 1 : 0;
|
|
43
|
+
const rightOrphan = right.personId === null || right.personId === undefined ? 1 : 0;
|
|
44
|
+
if (leftOrphan !== rightOrphan)
|
|
45
|
+
return leftOrphan - rightOrphan;
|
|
46
|
+
return agentSaves(right.counts) - agentSaves(left.counts);
|
|
47
|
+
});
|
|
48
|
+
for (const person of ranked) {
|
|
49
|
+
const byDay = new Map((person.days ?? []).map((entry) => [entry.day ?? "", entry]));
|
|
50
|
+
const cells = days
|
|
51
|
+
.map((day) => {
|
|
52
|
+
const entry = byDay.get(day);
|
|
53
|
+
const saved = agentSaves(entry?.counts);
|
|
54
|
+
// A zero prints as `·`, not `0`: seven zeroes in a row is the shape a
|
|
55
|
+
// reader is scanning for, and seven `0`s hide it in the noise.
|
|
56
|
+
return (saved === 0 ? "·" : String(saved)).padStart(5);
|
|
57
|
+
})
|
|
58
|
+
.join(" ");
|
|
59
|
+
const counts = person.counts ?? {};
|
|
60
|
+
const row = ` ${(person.displayName ?? "?").padEnd(nameWidth)} ${cells} ` +
|
|
61
|
+
`${String(counts.mcp ?? 0).padStart(5)} ${String(counts.stop_hook ?? 0).padStart(5)} ` +
|
|
62
|
+
`${String(counts.import ?? 0).padStart(5)}`;
|
|
63
|
+
lines.push(agentSaves(counts) > 0 ? row : dim(row));
|
|
64
|
+
}
|
|
65
|
+
if (memory.truncated) {
|
|
66
|
+
lines.push(dim(" this read stopped at its page cap, so every number above is a FLOOR — it can understate use and never invent it"));
|
|
67
|
+
}
|
|
68
|
+
if (memory.searches?.counted === false) {
|
|
69
|
+
lines.push(dim(` searches are not counted (${memory.searches.reason ?? "no ledger"}): nothing in Tower records one, so this is saves only`));
|
|
70
|
+
}
|
|
71
|
+
return lines;
|
|
72
|
+
}
|
|
73
|
+
/** The two sources that mean a PERSON's agent used memory. Never the import. */
|
|
74
|
+
function agentSaves(counts) {
|
|
75
|
+
return (counts?.mcp ?? 0) + (counts?.stop_hook ?? 0);
|
|
76
|
+
}
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
* artifact does and does not prove — because that is exactly the moment
|
|
13
13
|
* somebody is about to conclude something from it.
|
|
14
14
|
*/
|
|
15
|
+
export { renderMemoryUsage } from "./ops-render-memory.js";
|
|
15
16
|
/** The word a person reads. Short, fixed width, and never a bare colour. */
|
|
16
17
|
export function verdictWord(verdict) {
|
|
17
18
|
switch (verdict) {
|
|
@@ -19,6 +20,11 @@ export function verdictWord(verdict) {
|
|
|
19
20
|
return "ok";
|
|
20
21
|
case "stale":
|
|
21
22
|
return "STALE";
|
|
23
|
+
// BLI-3723. Its own word, not a shade of STALE: a `failing` reader's input
|
|
24
|
+
// is CURRENT and its answer is bad, which sends a person to a laptop
|
|
25
|
+
// rather than to a cron config.
|
|
26
|
+
case "failing":
|
|
27
|
+
return "BAD";
|
|
22
28
|
case "never_produced":
|
|
23
29
|
return "EMPTY";
|
|
24
30
|
case "unreadable":
|
|
@@ -69,7 +75,9 @@ export function renderOpsStatus(payload, dim) {
|
|
|
69
75
|
for (const row of rows) {
|
|
70
76
|
const verdict = verdictWord(row.verdict).padEnd(6);
|
|
71
77
|
const id = (row.id ?? "?").padEnd(idWidth);
|
|
72
|
-
|
|
78
|
+
// BLI-3723: a row that has no age BY DESIGN says `n/a`, never `—`. The two
|
|
79
|
+
// look alike and mean opposite things — `—` is "the table is empty".
|
|
80
|
+
const age = (row.ageAbsence ? "n/a" : ageWord(row.ageHours)).padStart(7);
|
|
73
81
|
// BLI-3722: name the device this age belongs to right in the row — a bare
|
|
74
82
|
// `STALE collector-fleet 11h` was already misread once (BLI-3699) as "the
|
|
75
83
|
// fleet", not "one machine in it", and the name is otherwise buried in
|
|
@@ -77,6 +85,10 @@ export function renderOpsStatus(payload, dim) {
|
|
|
77
85
|
const staleName = row.verdict === "stale" && row.staleDeviceName ? ` ${dim(`(${row.staleDeviceName})`)}` : "";
|
|
78
86
|
lines.push(`${verdict} ${id} ${age}${staleName} ${dim(intervalWord(row))}`);
|
|
79
87
|
if (row.verdict !== "healthy") {
|
|
88
|
+
// The absence of an age is explained BEFORE the detail, because it is
|
|
89
|
+
// the first thing a reader's eye stopped on.
|
|
90
|
+
if (row.ageAbsence)
|
|
91
|
+
lines.push(dim(` no age: ${row.ageAbsence}`));
|
|
80
92
|
if (row.detail)
|
|
81
93
|
lines.push(dim(` ${row.detail}`));
|
|
82
94
|
if (row.caveat)
|
package/dist/commands/ops.js
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
*
|
|
13
13
|
* **Exit codes are the whole point of running this in a script.** Zero when
|
|
14
14
|
* every job that CAN be answered for is healthy; 1 when anything is stale,
|
|
15
|
-
* empty or unreadable. `no_artifact` — a job that writes no row — does not fail
|
|
15
|
+
* failing, empty or unreadable. `no_artifact` — a job that writes no row — does not fail
|
|
16
16
|
* the command, and it does not pass silently either: it prints as `n/a` with
|
|
17
17
|
* the reason there is nothing to read.
|
|
18
18
|
*
|
|
@@ -25,6 +25,7 @@
|
|
|
25
25
|
*/
|
|
26
26
|
import { colorEnabled, dim, writeLine } from "./cli-io.js";
|
|
27
27
|
import { renderOpsStatus } from "./ops-render.js";
|
|
28
|
+
import { renderMemoryUsage, } from "./ops-render-memory.js";
|
|
28
29
|
import { asRecord, callTower, openTower, writeCommandFailure } from "./tower-command.js";
|
|
29
30
|
/**
|
|
30
31
|
* A whole compile, plus a little. `maxDuration` on `/api/ops/recompile` is 800
|
|
@@ -60,15 +61,40 @@ async function runOpsStatus(command, io, tower) {
|
|
|
60
61
|
}
|
|
61
62
|
const payload = asRecord(result.body);
|
|
62
63
|
const rows = payload.pipelines ?? [];
|
|
63
|
-
|
|
64
|
+
// BLI-3729. A SECOND door, asked for only when `--memory` was: the adoption
|
|
65
|
+
// gauge is a different question from pipeline health and a different read,
|
|
66
|
+
// and putting it on every `cockpit ops` would make the common run slower for
|
|
67
|
+
// a number most runs do not want. Its failure is its own line and never the
|
|
68
|
+
// status board's exit code — "is the org using memory" going unread is not a
|
|
69
|
+
// pipeline outage.
|
|
70
|
+
const memory = command.memory ? await readMemoryUsage(command, io, tower) : null;
|
|
71
|
+
// BLI-3723: `failing` joins the list. An older CLI that has never heard of it
|
|
72
|
+
// simply does not count it — which is why the server keeps the sentence in
|
|
73
|
+
// `detail`, so an un-upgraded terminal still PRINTS the outage even when it
|
|
74
|
+
// exits 0 over it.
|
|
75
|
+
const unhealthy = rows.filter((row) => row.verdict === "stale" ||
|
|
76
|
+
row.verdict === "never_produced" ||
|
|
77
|
+
row.verdict === "unreadable" ||
|
|
78
|
+
row.verdict === "failing");
|
|
64
79
|
if (command.json) {
|
|
65
|
-
writeLine(io.stdout, JSON.stringify(payload));
|
|
80
|
+
writeLine(io.stdout, JSON.stringify(memory ? { ...payload, memory: memory.section } : payload));
|
|
66
81
|
}
|
|
67
82
|
else {
|
|
68
83
|
const styled = colorEnabled(io);
|
|
69
84
|
for (const line of renderOpsStatus(payload, (text) => dim(text, styled))) {
|
|
70
85
|
writeLine(io.stdout, line);
|
|
71
86
|
}
|
|
87
|
+
if (memory?.section) {
|
|
88
|
+
for (const line of renderMemoryUsage(memory.section, (text) => dim(text, styled))) {
|
|
89
|
+
writeLine(io.stdout, line);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
else if (memory) {
|
|
93
|
+
// Never a silent gap where a section was asked for: the reason the gauge
|
|
94
|
+
// could not be read is printed where the gauge would have been.
|
|
95
|
+
writeLine(io.stdout, "");
|
|
96
|
+
writeLine(io.stdout, `MEMORY not read (${memory.reason})`);
|
|
97
|
+
}
|
|
72
98
|
}
|
|
73
99
|
// Both branches log — an all-green board that says nothing cannot answer
|
|
74
100
|
// "did anybody look today?".
|
|
@@ -84,9 +110,45 @@ async function runOpsStatus(command, io, tower) {
|
|
|
84
110
|
fleet_devices: payload.fleet?.counts?.devices ?? null,
|
|
85
111
|
fleet_red: payload.fleet?.counts?.red ?? null,
|
|
86
112
|
fleet_amber: payload.fleet?.counts?.amber ?? null,
|
|
113
|
+
// BLI-3729: whether the adoption gauge was asked for, and what it said.
|
|
114
|
+
// Both branches, as everywhere else here.
|
|
115
|
+
memory_asked: Boolean(command.memory),
|
|
116
|
+
memory_reason: memory?.reason ?? null,
|
|
117
|
+
memory_agent_saves: memory?.section
|
|
118
|
+
? (memory.section.counts?.mcp ?? 0) + (memory.section.counts?.stop_hook ?? 0)
|
|
119
|
+
: null,
|
|
87
120
|
})}`);
|
|
88
121
|
return unhealthy.length > 0 ? 1 : 0;
|
|
89
122
|
}
|
|
123
|
+
/**
|
|
124
|
+
* The adoption gauge, read through its own door.
|
|
125
|
+
*
|
|
126
|
+
* Never fails the board: a `cockpit ops --memory` whose memory half is
|
|
127
|
+
* unreadable still prints the pipelines, the fleet and the coverage, and says
|
|
128
|
+
* in one line which half is missing and why. The status board's exit code is
|
|
129
|
+
* about collection health, and adoption is not that.
|
|
130
|
+
*/
|
|
131
|
+
async function readMemoryUsage(command, io, tower) {
|
|
132
|
+
const params = new URLSearchParams();
|
|
133
|
+
if (command.memoryDays !== undefined)
|
|
134
|
+
params.set("days", String(command.memoryDays));
|
|
135
|
+
const query = params.toString();
|
|
136
|
+
const result = await callTower(tower, {
|
|
137
|
+
path: `/api/ops/memory-usage${query ? `?${query}` : ""}`,
|
|
138
|
+
label: "ops-memory-usage",
|
|
139
|
+
});
|
|
140
|
+
if (!result.ok) {
|
|
141
|
+
writeLine(io.stderr, `[ops cli] memory usage not read ${JSON.stringify({
|
|
142
|
+
reason: result.reason,
|
|
143
|
+
http_status: result.httpStatus ?? null,
|
|
144
|
+
})}`);
|
|
145
|
+
return { section: null, reason: result.reason };
|
|
146
|
+
}
|
|
147
|
+
const body = asRecord(result.body);
|
|
148
|
+
if (!body.memory)
|
|
149
|
+
return { section: null, reason: "memory_section_absent" };
|
|
150
|
+
return { section: body.memory, reason: "ok" };
|
|
151
|
+
}
|
|
90
152
|
async function runOpsRecompile(command, io, tower) {
|
|
91
153
|
const person = command.person ?? "";
|
|
92
154
|
if (!command.json) {
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `cockpit project list` — the projects issues are filed under (BLI-3716).
|
|
3
|
+
*
|
|
4
|
+
* One verb, because `GET /api/work/projects` is the whole of the Work
|
|
5
|
+
* surface's project door today (BLI-3703 scoped projects to list-only; there
|
|
6
|
+
* is no create/update/delete route to put a terminal in front of). Its own
|
|
7
|
+
* command rather than `cockpit issue projects` because a person says
|
|
8
|
+
* "projects", and `cockpit issue list --project <name>` resolves a name
|
|
9
|
+
* against exactly this list.
|
|
10
|
+
*/
|
|
11
|
+
import { askAgentDoor, emitAgentDoor, failAgentDoor, openAgentDoor } from "./agent-door.js";
|
|
12
|
+
import { writeLine } from "./cli-io.js";
|
|
13
|
+
const TAG = "[project cli]";
|
|
14
|
+
const READ_DEADLINE_MS = 30_000;
|
|
15
|
+
export async function runProject(command, io) {
|
|
16
|
+
const door = await openAgentDoor("project", command, io);
|
|
17
|
+
const answer = await askAgentDoor(door, {
|
|
18
|
+
path: `/api/work/projects${command.includeArchived ? "?include_archived=true" : ""}`,
|
|
19
|
+
method: "GET",
|
|
20
|
+
label: "project list",
|
|
21
|
+
timeoutMs: READ_DEADLINE_MS,
|
|
22
|
+
});
|
|
23
|
+
if (!answer.ok)
|
|
24
|
+
return failAgentDoor(door, TAG, answer.reason, answer.detail);
|
|
25
|
+
const projects = answer.body.projects ?? [];
|
|
26
|
+
if (door.json)
|
|
27
|
+
return emitAgentDoor(door, { ok: true, projects });
|
|
28
|
+
if (projects.length === 0) {
|
|
29
|
+
writeLine(door.io.stdout, "No projects.");
|
|
30
|
+
return 0;
|
|
31
|
+
}
|
|
32
|
+
for (const project of projects) {
|
|
33
|
+
writeLine(door.io.stdout, `${project.id} ${project.archived_at ? "archived" : "active "} ${project.name}`);
|
|
34
|
+
}
|
|
35
|
+
writeLine(door.io.stdout, "");
|
|
36
|
+
writeLine(door.io.stdout, `${projects.length} project(s).`);
|
|
37
|
+
return 0;
|
|
38
|
+
}
|
|
@@ -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.59");
|
|
19
19
|
return 0;
|
|
20
20
|
}
|
|
21
21
|
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `cockpit search` — one bar over five corpora, in a terminal (BLI-3728).
|
|
3
|
+
*
|
|
4
|
+
* The SAME door the browser's search bar presses: `GET /api/search`, opted
|
|
5
|
+
* into the collector device token in `route-caller-census.test.ts`. So a
|
|
6
|
+
* result a person reads in the rail is the same row, the same ranking and the
|
|
7
|
+
* same snippet an agent gets here. That is the whole point of the ticket's
|
|
8
|
+
* "agent-friendly is the definition of done" clause; nothing about search is
|
|
9
|
+
* computed on this side.
|
|
10
|
+
*
|
|
11
|
+
* WHAT THIS PRINTS THAT THE BROWSER ALSO SHOWS
|
|
12
|
+
* --------------------------------------------
|
|
13
|
+
* A corpus that could not answer gets its own line. "Messages did not answer"
|
|
14
|
+
* and "no messages matched" are different facts, and a terminal that folds
|
|
15
|
+
* them together lets a person conclude the record is silent when the search
|
|
16
|
+
* was simply broken. Same rule, same words, as the overlay.
|
|
17
|
+
*
|
|
18
|
+
* `[[` / `]]` are `ts_headline`'s markers, chosen server-side precisely
|
|
19
|
+
* BECAUSE they are inert in a terminal (`src/lib/search/snippet.ts`). Without
|
|
20
|
+
* a TTY they are stripped; with one they become reverse video, and nothing
|
|
21
|
+
* here parses HTML.
|
|
22
|
+
*
|
|
23
|
+
* A refusal keeps the door's own `reason` label verbatim (`agent-door.ts`):
|
|
24
|
+
* `needs_rls_client`, `query_too_short`, `query_too_long`, `unknown_kind`,
|
|
25
|
+
* `read_failed` — never rephrased here.
|
|
26
|
+
*/
|
|
27
|
+
import { askAgentDoor, emitAgentDoor, failAgentDoor, openAgentDoor, } from "./agent-door.js";
|
|
28
|
+
import { writeLine } from "./cli-io.js";
|
|
29
|
+
const TAG = "[search cli]";
|
|
30
|
+
const READ_DEADLINE_MS = 30_000;
|
|
31
|
+
/** Group headings, in the order a person reads them. Mirrors the overlay. */
|
|
32
|
+
const KIND_LABELS = {
|
|
33
|
+
doc: "DOCUMENTS",
|
|
34
|
+
msg: "MESSAGES",
|
|
35
|
+
issue: "ISSUES",
|
|
36
|
+
note: "MEETING NOTES",
|
|
37
|
+
memory: "MEMORY",
|
|
38
|
+
};
|
|
39
|
+
const KIND_ORDER = ["doc", "msg", "issue", "note", "memory"];
|
|
40
|
+
export async function runSearch(command, io) {
|
|
41
|
+
const door = await openAgentDoor("search", command, io);
|
|
42
|
+
const params = new URLSearchParams({ q: command.query });
|
|
43
|
+
if (command.kinds && command.kinds.length > 0)
|
|
44
|
+
params.set("kinds", command.kinds.join(","));
|
|
45
|
+
if (command.limit !== undefined)
|
|
46
|
+
params.set("limit", String(command.limit));
|
|
47
|
+
const answer = await askAgentDoor(door, {
|
|
48
|
+
path: `/api/search?${params.toString()}`,
|
|
49
|
+
method: "GET",
|
|
50
|
+
label: "search",
|
|
51
|
+
timeoutMs: READ_DEADLINE_MS,
|
|
52
|
+
});
|
|
53
|
+
if (!answer.ok)
|
|
54
|
+
return failAgentDoor(door, TAG, answer.reason, answer.detail);
|
|
55
|
+
const body = answer.body;
|
|
56
|
+
if (door.json)
|
|
57
|
+
return emitAgentDoor(door, body);
|
|
58
|
+
return render(door, body);
|
|
59
|
+
}
|
|
60
|
+
function render(door, body) {
|
|
61
|
+
const hits = body.hits ?? [];
|
|
62
|
+
const failures = body.failures ?? {};
|
|
63
|
+
const colour = Boolean(door.io.stdout.isTTY);
|
|
64
|
+
if (hits.length === 0) {
|
|
65
|
+
if (Object.keys(failures).length === 0) {
|
|
66
|
+
writeLine(door.io.stdout, "Nothing matched. Every corpus answered — this is silence, not a failure.");
|
|
67
|
+
}
|
|
68
|
+
else {
|
|
69
|
+
writeLine(door.io.stdout, "Nothing matched in the corpora that answered.");
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
for (const kind of KIND_ORDER) {
|
|
73
|
+
const group = hits.filter((hit) => hit.kind === kind);
|
|
74
|
+
if (group.length === 0)
|
|
75
|
+
continue;
|
|
76
|
+
writeLine(door.io.stdout, "");
|
|
77
|
+
writeLine(door.io.stdout, KIND_LABELS[kind] ?? kind.toUpperCase());
|
|
78
|
+
for (const hit of group) {
|
|
79
|
+
writeLine(door.io.stdout, ` ${hit.title}`);
|
|
80
|
+
const snippet = paintSnippet(hit.snippet, colour);
|
|
81
|
+
if (snippet.length > 0)
|
|
82
|
+
writeLine(door.io.stdout, ` ${snippet}`);
|
|
83
|
+
const meta = [
|
|
84
|
+
hit.author,
|
|
85
|
+
hit.date ? hit.date.slice(0, 10) : null,
|
|
86
|
+
(hit.channels ?? []).join("+"),
|
|
87
|
+
`score ${hit.score}`,
|
|
88
|
+
// A memory has no address, and saying so is better than printing a
|
|
89
|
+
// blank column somebody reads as a missing link.
|
|
90
|
+
hit.href ?? "(no page — the text above is the whole record)",
|
|
91
|
+
]
|
|
92
|
+
.filter(Boolean)
|
|
93
|
+
.join(" · ");
|
|
94
|
+
writeLine(door.io.stdout, ` ${meta}`);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
// Never folded into "nothing matched". Same rule as the overlay.
|
|
98
|
+
const failed = Object.entries(failures);
|
|
99
|
+
if (failed.length > 0) {
|
|
100
|
+
writeLine(door.io.stdout, "");
|
|
101
|
+
for (const [kind, reason] of failed) {
|
|
102
|
+
writeLine(door.io.stdout, `${KIND_LABELS[kind] ?? kind.toUpperCase()} did not answer (${reason}). Nothing from there is in this list.`);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
writeLine(door.io.stdout, "");
|
|
106
|
+
writeLine(door.io.stdout, `${hits.length} result(s) in ${body.elapsedMs ?? 0} ms.`);
|
|
107
|
+
return 0;
|
|
108
|
+
}
|
|
109
|
+
const REVERSE_ON = "\u001B[7m";
|
|
110
|
+
const REVERSE_OFF = "\u001B[27m";
|
|
111
|
+
/**
|
|
112
|
+
* `[[match]]` into reverse video, or into plain text without a TTY.
|
|
113
|
+
*
|
|
114
|
+
* A pipe gets clean text — `cockpit search x | grep` must not have to know
|
|
115
|
+
* about escape codes — and a terminal gets the highlight the browser shows.
|
|
116
|
+
*/
|
|
117
|
+
export function paintSnippet(snippet, colour) {
|
|
118
|
+
const collapsed = snippet.replace(/\s+/g, " ").trim();
|
|
119
|
+
if (!colour)
|
|
120
|
+
return collapsed.split("[[").join("").split("]]").join("");
|
|
121
|
+
return collapsed.split("[[").join(REVERSE_ON).split("]]").join(REVERSE_OFF);
|
|
122
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What the setup receipt SAYS (BLI-3731). One line, then one fix per gap.
|
|
3
|
+
*
|
|
4
|
+
* Split from `setup-receipt.ts` for the reason the repo splits everything:
|
|
5
|
+
* computing a state and wording it are different jobs, and only one of them
|
|
6
|
+
* touches the machine. This file touches nothing.
|
|
7
|
+
*
|
|
8
|
+
* The rule for the fix lines is the one the message standard already sets — a
|
|
9
|
+
* refusal that does not say what to do next is half a message. Every gap here
|
|
10
|
+
* gets ONE command or ONE click, never a paragraph and never "see the docs".
|
|
11
|
+
*/
|
|
12
|
+
import { setupPieceMark, setupReceiptLine, setupReceiptPieces, } from "@bli-cockpit/telemetry-core";
|
|
13
|
+
/** The one fix for each piece, by key. Present for every key the block prints. */
|
|
14
|
+
const FIXES = {
|
|
15
|
+
browser: "Run `cockpit login` and open the link it prints.",
|
|
16
|
+
device: "Run `cockpit login` — this machine's session has lapsed.",
|
|
17
|
+
"claude.mcp": "Run `cockpit memory install`.",
|
|
18
|
+
"claude.hooks": "Run `cockpit memory install`.",
|
|
19
|
+
"codex.mcp": "Run `cockpit memory install`.",
|
|
20
|
+
"codex.skill": "Run `cockpit memory install`.",
|
|
21
|
+
"codex.hooks": "Run `cockpit memory install`.",
|
|
22
|
+
"collector.autostart": "Run `cockpit autostart install`.",
|
|
23
|
+
};
|
|
24
|
+
/** Codex hooks a person switched OFF is a decision, not a fault. */
|
|
25
|
+
const UNSUPPORTED_FIX = "Switched off in your own config; nothing to do.";
|
|
26
|
+
/**
|
|
27
|
+
* The one exception to "a gap gets a command": Codex hooks that are installed
|
|
28
|
+
* and waiting to be trusted are finished by a PERSON, in the Codex UI, and no
|
|
29
|
+
* command we could print would do it.
|
|
30
|
+
*/
|
|
31
|
+
const NEEDS_TRUST_FIX = "Open Codex and press `/hooks` to trust them (once, ~15s).";
|
|
32
|
+
/**
|
|
33
|
+
* The block a terminal prints: the one line, then one fix per gap, then
|
|
34
|
+
* nothing. A fully connected machine gets one line and no advice — a receipt
|
|
35
|
+
* that keeps talking after saying yes trains people to stop reading it.
|
|
36
|
+
*/
|
|
37
|
+
export function setupReceiptBlock(reading, options = {}) {
|
|
38
|
+
const { receipt, memory } = reading;
|
|
39
|
+
const indent = options.indent ?? "";
|
|
40
|
+
const lines = [`${indent}${setupReceiptLine(receipt, memory)}`];
|
|
41
|
+
for (const { key, label, piece } of setupReceiptPieces(receipt, memory)) {
|
|
42
|
+
if (piece.status === "ok")
|
|
43
|
+
continue;
|
|
44
|
+
lines.push(`${indent} ${label} ${setupPieceMark(piece.status)}${reasonSuffix(piece)} — ${fixFor(key, piece)}`);
|
|
45
|
+
}
|
|
46
|
+
if (options.showCheckedAt) {
|
|
47
|
+
lines.push(`${indent}Last read: ${receipt.checked_at}`);
|
|
48
|
+
}
|
|
49
|
+
return lines;
|
|
50
|
+
}
|
|
51
|
+
/** The whole receipt in one string, for a single `writeLine`. */
|
|
52
|
+
export function setupReceiptText(reading, options = {}) {
|
|
53
|
+
return setupReceiptBlock(reading, options).join("\n");
|
|
54
|
+
}
|
|
55
|
+
function fixFor(key, piece) {
|
|
56
|
+
if (piece.status === "needs_trust")
|
|
57
|
+
return NEEDS_TRUST_FIX;
|
|
58
|
+
if (piece.status === "unsupported")
|
|
59
|
+
return UNSUPPORTED_FIX;
|
|
60
|
+
if (piece.status === "unknown") {
|
|
61
|
+
// "We did not look" is not somebody's chore. Say what would look.
|
|
62
|
+
return "Not read this run. Run `cockpit doctor` to check it.";
|
|
63
|
+
}
|
|
64
|
+
if (piece.status === "skipped") {
|
|
65
|
+
return "Skipped on purpose; nothing to do.";
|
|
66
|
+
}
|
|
67
|
+
return FIXES[key] ?? "Run `cockpit doctor`.";
|
|
68
|
+
}
|
|
69
|
+
function reasonSuffix(piece) {
|
|
70
|
+
return piece.reason ? ` (${piece.reason})` : "";
|
|
71
|
+
}
|