@bli-cockpit/cli 0.2.224 → 0.2.236
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/agent-door.js +35 -0
- package/dist/commands/approvals.js +3 -1
- package/dist/commands/brain.js +23 -0
- package/dist/commands/ci.js +23 -0
- package/dist/commands/docs-export.js +62 -0
- package/dist/commands/docs.js +3 -0
- package/dist/commands/drills-render.js +63 -0
- package/dist/commands/drills.js +152 -0
- package/dist/commands/hits.js +83 -0
- package/dist/commands/intentions.js +49 -0
- package/dist/commands/local-args-docs-export.js +23 -0
- package/dist/commands/local-args-tower-admin.js +8 -0
- package/dist/commands/local-args-tower-brain-ladder.js +90 -0
- package/dist/commands/local-args-tower-brain.js +11 -1
- package/dist/commands/local-args-tower-docs-msg.js +9 -3
- package/dist/commands/local-args-tower-drills.js +87 -0
- package/dist/commands/local-args-tower-hits.js +93 -0
- package/dist/commands/local-args-tower-intentions.js +70 -0
- package/dist/commands/local-args-tower-logs-search.js +51 -0
- package/dist/commands/local-args-tower-logs.js +3 -0
- package/dist/commands/local-args-tower-memory.js +67 -1
- package/dist/commands/local-args-tower-pages.js +3 -1
- package/dist/commands/local-args-tower-people.js +25 -0
- package/dist/commands/local-args-tower-usage.js +4 -4
- package/dist/commands/local-args-tower-who.js +36 -0
- package/dist/commands/local-args-tower.js +8 -1
- package/dist/commands/local-args.js +15 -1
- package/dist/commands/local-command-shapes-auth.js +1 -0
- package/dist/commands/local-command-shapes-drills.js +7 -0
- package/dist/commands/local-command-shapes-hits.js +5 -0
- package/dist/commands/local-help-commands-tower-ops.js +123 -1
- package/dist/commands/local-help-commands-tower.js +19 -7
- package/dist/commands/local-help-commands.js +3 -0
- package/dist/commands/local-help.js +24 -4
- package/dist/commands/local-run-ops.js +11 -0
- package/dist/commands/local.js +16 -8
- package/dist/commands/logs-search.js +45 -0
- package/dist/commands/logs.js +3 -0
- package/dist/commands/memory-exam.js +47 -0
- package/dist/commands/memory-repeats.js +44 -0
- package/dist/commands/memory-search-print.js +95 -0
- package/dist/commands/memory-source.js +39 -0
- package/dist/commands/memory-store.js +18 -30
- package/dist/commands/memory-verb-list.js +7 -2
- package/dist/commands/notes-reads.js +20 -0
- package/dist/commands/notes-writes.js +24 -0
- package/dist/commands/notes.js +4 -2
- package/dist/commands/people.js +29 -0
- package/dist/commands/public-root.js +1 -1
- package/dist/commands/spend.js +79 -0
- package/dist/commands/team.js +35 -0
- package/dist/commands/usage.js +9 -0
- package/dist/commands/who.js +36 -0
- package/dist/evidence-upload-direct.js +44 -0
- package/dist/evidence-upload-object.js +18 -4
- package/dist/evidence-upload-plan.js +1 -0
- package/dist/evidence-upload-transport.js +2 -0
- package/package.json +4 -4
|
@@ -57,6 +57,41 @@ export async function askAgentDoor(door, options) {
|
|
|
57
57
|
}
|
|
58
58
|
return { ok: true, httpStatus: result.response.status, body };
|
|
59
59
|
}
|
|
60
|
+
/**
|
|
61
|
+
* A GET whose answer is a FILE (ticket 20694, `cockpit docs export`): the bytes
|
|
62
|
+
* come back untouched, never through a JSON or text decode. A refusal keeps the
|
|
63
|
+
* door's own `reason` and `message`, exactly as `askAgentDoor` does.
|
|
64
|
+
*/
|
|
65
|
+
export async function fetchAgentDoorFile(door, options) {
|
|
66
|
+
const result = await towerRequest({
|
|
67
|
+
dashboardUrl: door.dashboardUrl,
|
|
68
|
+
path: options.path,
|
|
69
|
+
deviceToken: door.deviceToken,
|
|
70
|
+
fetch: door.io.fetch,
|
|
71
|
+
method: "GET",
|
|
72
|
+
label: options.label,
|
|
73
|
+
timeoutMs: options.timeoutMs,
|
|
74
|
+
...(door.homeDir === undefined ? {} : { homeDir: door.homeDir }),
|
|
75
|
+
log: (line) => writeLine(door.io.stderr, line),
|
|
76
|
+
});
|
|
77
|
+
if (!result.ok) {
|
|
78
|
+
const failure = result;
|
|
79
|
+
return { ok: false, reason: failure.reason, detail: towerFailureDetail(failure.reason, failure.detail) };
|
|
80
|
+
}
|
|
81
|
+
if (!result.response.ok) {
|
|
82
|
+
const status = result.response.status;
|
|
83
|
+
const body = await readResponseJson(result.response);
|
|
84
|
+
return {
|
|
85
|
+
ok: false,
|
|
86
|
+
reason: doorReason(body) ?? `http_${status}`,
|
|
87
|
+
detail: doorMessage(body) ?? `Tower answered ${status} and said nothing about why.`,
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
const bytes = Buffer.from(await result.response.arrayBuffer());
|
|
91
|
+
const disposition = result.response.headers.get("content-disposition") ?? "";
|
|
92
|
+
const filename = /filename="([^"]+)"/.exec(disposition)?.[1] ?? null;
|
|
93
|
+
return { ok: true, bytes, filename, contentType: result.response.headers.get("content-type") ?? "" };
|
|
94
|
+
}
|
|
60
95
|
/** The door's own `reason` label (`document_not_found_or_unreadable`, `needs_rls_client`, …), verbatim. */
|
|
61
96
|
function doorReason(body) {
|
|
62
97
|
if (!body || typeof body !== "object")
|
|
@@ -232,4 +232,6 @@ async function readJsonObject(door, verb) {
|
|
|
232
232
|
return { ok: false, reason: "invalid_json_body", detail: "stdin must be one JSON object, like {\"text\":\"...\"}. Nothing was sent." };
|
|
233
233
|
}
|
|
234
234
|
return { ok: true, value: parsed };
|
|
235
|
-
}
|
|
235
|
+
}
|
|
236
|
+
// BLI-19259: `cockpit drills` is re-exported here so `local.ts` (at the readability ceiling) imports both on one line.
|
|
237
|
+
export { runDrills } from "./drills.js";
|
package/dist/commands/brain.js
CHANGED
|
@@ -2,6 +2,27 @@ import { askAgentDoor, emitAgentDoor, failAgentDoor, openAgentDoor } from "./age
|
|
|
2
2
|
import { writeLine } from "./cli-io.js";
|
|
3
3
|
const TAG = "[brain cli]";
|
|
4
4
|
const READ_DEADLINE_MS = 55_000;
|
|
5
|
+
async function runLadder(door, ladder, json) {
|
|
6
|
+
const base = "/api/brain/ladder";
|
|
7
|
+
const request = (() => {
|
|
8
|
+
switch (ladder.verb) {
|
|
9
|
+
case "show": return { path: base, method: "GET", label: "brain ladder show" };
|
|
10
|
+
case "promote": return { path: `${base}/promote`, method: "POST", label: "brain ladder promote", body: { class: ladder.className, anyway: ladder.anyway, note: ladder.note, via: "cli" } };
|
|
11
|
+
case "demote": return { path: `${base}/demote`, method: "POST", label: "brain ladder demote", body: { class: ladder.className, note: ladder.note, via: "cli" } };
|
|
12
|
+
case "predict": return { path: `${base}/predictions`, method: "POST", label: "brain ladder predict", body: { question: ladder.text, context: ladder.context, asker: ladder.asker, source_kind: ladder.sourceKind, source_ref: ladder.sourceRef } };
|
|
13
|
+
case "answer": return { path: `${base}/answers`, method: "POST", label: "brain ladder answer", body: { answer: ladder.text, prediction_id: ladder.predictionId, source_kind: ladder.sourceKind, source_ref: ladder.sourceRef } };
|
|
14
|
+
}
|
|
15
|
+
})();
|
|
16
|
+
const answer = await askAgentDoor(door, { path: request.path, method: request.method, label: request.label, timeoutMs: READ_DEADLINE_MS, ...(request.body ? { body: request.body } : {}) });
|
|
17
|
+
if (!answer.ok)
|
|
18
|
+
return failAgentDoor(door, TAG, answer.reason, answer.detail);
|
|
19
|
+
const body = answer.body;
|
|
20
|
+
writeLine(door.io.stderr, `${TAG} ladder ${ladder.verb} answered ${JSON.stringify({ ok: body.ok !== false, reason: body.reason ?? "ok" })}`);
|
|
21
|
+
if (json)
|
|
22
|
+
return emitAgentDoor(door, body, body.ok === false ? 1 : 0);
|
|
23
|
+
writeLine(door.io.stdout, body.text ?? body.message ?? "");
|
|
24
|
+
return body.ok === false ? 1 : 0;
|
|
25
|
+
}
|
|
5
26
|
export async function runBrain(command, io) {
|
|
6
27
|
let door;
|
|
7
28
|
try {
|
|
@@ -16,6 +37,8 @@ export async function runBrain(command, io) {
|
|
|
16
37
|
writeLine(io.stderr, detail);
|
|
17
38
|
return 1;
|
|
18
39
|
}
|
|
40
|
+
if (command.ladder)
|
|
41
|
+
return runLadder(door, command.ladder, command.json);
|
|
19
42
|
if (command.action === "retry") {
|
|
20
43
|
const answer = await askAgentDoor(door, { path: "/api/brain/incidents/retry", method: "POST", label: "brain retry", timeoutMs: READ_DEADLINE_MS,
|
|
21
44
|
body: { ids: command.ids, all_needs_human: command.allNeedsHuman, project: command.project, reason: command.reason } });
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { askAgentDoor, emitAgentDoor, failAgentDoor, openAgentDoor } from "./agent-door.js";
|
|
2
|
+
import { writeLine } from "./cli-io.js";
|
|
3
|
+
import { parseNamedArgs, optionalNonEmpty, optionalUrl } from "./local-arg-values.js";
|
|
4
|
+
export function parseCiArgs(args) {
|
|
5
|
+
const values = parseNamedArgs(args, { allowedFlags: ["--repo", "--json", "--home", "--dashboard-url"], valueFlags: ["--repo", "--home", "--dashboard-url"] });
|
|
6
|
+
const repo = values.flags.get("--repo");
|
|
7
|
+
if (values.positionals.length !== 1 || values.positionals[0] !== "report" || (repo !== undefined && !/^BLI-INC\/[A-Za-z0-9_.-]+$/.test(repo)))
|
|
8
|
+
throw new Error("Usage: cockpit ci report [--repo BLI-INC/name] [--json]");
|
|
9
|
+
return { kind: "ci", action: "report", repo, json: values.booleans.has("--json"), homeDir: optionalNonEmpty(values.flags.get("--home")), dashboardUrl: optionalUrl(values.flags.get("--dashboard-url")) };
|
|
10
|
+
}
|
|
11
|
+
export async function runCi(command, io) {
|
|
12
|
+
const door = await openAgentDoor("ci", command, io);
|
|
13
|
+
const query = command.repo ? `?${new URLSearchParams({ repo: command.repo })}` : "";
|
|
14
|
+
const answer = await askAgentDoor(door, { path: `/api/ops/ci/report${query}`, method: "GET", label: "ci report", timeoutMs: 30_000 });
|
|
15
|
+
if (!answer.ok)
|
|
16
|
+
return failAgentDoor(door, "[ci cli]", answer.reason, answer.detail);
|
|
17
|
+
if (command.json)
|
|
18
|
+
return emitAgentDoor(door, answer.body);
|
|
19
|
+
const body = answer.body;
|
|
20
|
+
for (const line of body.lines)
|
|
21
|
+
writeLine(io.stdout, line);
|
|
22
|
+
return 0;
|
|
23
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `cockpit docs export <id|slug> [--format md|pdf|docx|txt] [--out <path>]`
|
|
3
|
+
* (ticket 20694). A sibling of `docs.ts`: one verb, one door.
|
|
4
|
+
*
|
|
5
|
+
* `GET /api/docs/documents/[id]/export?format=` is the same door the browser's
|
|
6
|
+
* Export menu calls. The bytes are written untouched; the filename is the one
|
|
7
|
+
* the door chose (a slug of the title plus the extension) unless `--out` names
|
|
8
|
+
* another. `cockpit docs read` already covers "copy": it prints the Markdown.
|
|
9
|
+
*/
|
|
10
|
+
import { mkdir, stat, writeFile } from "node:fs/promises";
|
|
11
|
+
import path from "node:path";
|
|
12
|
+
import { emitAgentDoor, failAgentDoor, fetchAgentDoorFile } from "./agent-door.js";
|
|
13
|
+
import { writeLine } from "./cli-io.js";
|
|
14
|
+
import { resolveDocId } from "./docs.js";
|
|
15
|
+
const TAG = "[docs cli]";
|
|
16
|
+
const EXPORT_DEADLINE_MS = 90_000;
|
|
17
|
+
async function isDirectory(target) {
|
|
18
|
+
try {
|
|
19
|
+
return (await stat(target)).isDirectory();
|
|
20
|
+
}
|
|
21
|
+
catch {
|
|
22
|
+
return false;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
export async function exportDoc(command, door) {
|
|
26
|
+
const ref = command.docRef ?? "";
|
|
27
|
+
const format = command.exportFormat ?? "md";
|
|
28
|
+
const resolved = await resolveDocId(door, ref);
|
|
29
|
+
if (resolved.status === "list_failed")
|
|
30
|
+
return failAgentDoor(door, TAG, resolved.reason, resolved.detail);
|
|
31
|
+
if (resolved.status === "not_found") {
|
|
32
|
+
return failAgentDoor(door, TAG, "document_not_found_or_unreadable", `That document does not exist, or you cannot read it: ${ref}`);
|
|
33
|
+
}
|
|
34
|
+
const started = Date.now();
|
|
35
|
+
const answer = await fetchAgentDoorFile(door, {
|
|
36
|
+
path: `/api/docs/documents/${encodeURIComponent(resolved.id)}/export?format=${encodeURIComponent(format)}`,
|
|
37
|
+
label: "docs export",
|
|
38
|
+
timeoutMs: EXPORT_DEADLINE_MS,
|
|
39
|
+
});
|
|
40
|
+
if (!answer.ok)
|
|
41
|
+
return failAgentDoor(door, TAG, answer.reason, answer.detail);
|
|
42
|
+
const filename = path.basename(answer.filename ?? `${ref}.${format}`);
|
|
43
|
+
let target = path.resolve(process.cwd(), filename);
|
|
44
|
+
if (command.outPath) {
|
|
45
|
+
const asked = path.resolve(command.outPath);
|
|
46
|
+
const wantsDirectory = /[\\/]$/.test(command.outPath) || (await isDirectory(asked));
|
|
47
|
+
target = wantsDirectory ? path.join(asked, filename) : asked;
|
|
48
|
+
}
|
|
49
|
+
try {
|
|
50
|
+
await mkdir(path.dirname(target), { recursive: true });
|
|
51
|
+
await writeFile(target, answer.bytes);
|
|
52
|
+
}
|
|
53
|
+
catch (error) {
|
|
54
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
55
|
+
return failAgentDoor(door, TAG, "export_write_failed", `Could not write ${target}: ${detail}`);
|
|
56
|
+
}
|
|
57
|
+
writeLine(door.io.stderr, `${TAG} export ${JSON.stringify({ document_id: resolved.id, format, bytes: answer.bytes.length, duration_ms: Date.now() - started })}`);
|
|
58
|
+
if (door.json)
|
|
59
|
+
return emitAgentDoor(door, { ok: true, path: target, bytes: answer.bytes.length, format });
|
|
60
|
+
writeLine(door.io.stdout, target);
|
|
61
|
+
return 0;
|
|
62
|
+
}
|
package/dist/commands/docs.js
CHANGED
|
@@ -30,6 +30,7 @@
|
|
|
30
30
|
import { askAgentDoor, emitAgentDoor, failAgentDoor, openAgentDoor, } from "./agent-door.js";
|
|
31
31
|
import { isInteractiveStdin, readPipedText, writeLine } from "./cli-io.js";
|
|
32
32
|
import { readDoc } from "./docs-read.js";
|
|
33
|
+
import { exportDoc } from "./docs-export.js";
|
|
33
34
|
import { linkDoc, syncStatusDocs } from "./docs-notion.js";
|
|
34
35
|
import { readNoteFile } from "./notes-file.js";
|
|
35
36
|
import { cacheAgeLabel, cacheCoverage, readThroughCache } from "./read-cache.js";
|
|
@@ -56,6 +57,8 @@ export async function runDocs(command, io) {
|
|
|
56
57
|
return linkDoc(command, door);
|
|
57
58
|
case "sync-status":
|
|
58
59
|
return syncStatusDocs(command, door);
|
|
60
|
+
case "export":
|
|
61
|
+
return exportDoc(command, door);
|
|
59
62
|
}
|
|
60
63
|
}
|
|
61
64
|
/** `--parent`, `--query`, `--word`, `--limit`, `--bodies` (BLI-10450) as the door's own query parameters (BLI-3737). */
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/** Plain-words rendering of the drills doors for the terminal (BLI-19259). No system nouns, no dashes used as punctuation. */
|
|
2
|
+
const indent = (text, prefix = " ") => text.split("\n").map((line) => `${prefix}${line}`);
|
|
3
|
+
export function drillLines(current) {
|
|
4
|
+
const lines = [`${current.framing.bossName}: ${current.framing.roundLine}${current.redrillDay ? ` (back after ${current.redrillDay} day${current.redrillDay === 1 ? "" : "s"})` : ""}`, "", current.scenario.title, current.scenario.context, ""];
|
|
5
|
+
lines.push(`The ${current.scenario.artifactKind}:`, ...indent(current.scenario.artifact));
|
|
6
|
+
if (current.scenario.logExcerpt)
|
|
7
|
+
lines.push("", "What it printed, up to the moment before it broke:", ...indent(current.scenario.logExcerpt));
|
|
8
|
+
lines.push("", current.scenario.question);
|
|
9
|
+
if (current.scenario.reconstructed)
|
|
10
|
+
lines.push("(Rebuilt from the incident write-up, so the details are close, not word for word.)");
|
|
11
|
+
return lines;
|
|
12
|
+
}
|
|
13
|
+
const minutes = (value) => (value === null ? "none yet" : value >= 120 ? `${(value / 60).toFixed(1)} hours` : `${Math.round(value)} minutes`);
|
|
14
|
+
export function overviewLines(body) {
|
|
15
|
+
const lines = [];
|
|
16
|
+
if (body.current)
|
|
17
|
+
lines.push("You have a drill open. Answer it before you see the cause:", "", ...drillLines(body.current), "", "Answer with: echo \"what you think\" | cockpit drills answer --confidence <1-5>", "");
|
|
18
|
+
else if (body.queue.redrillsDue > 0)
|
|
19
|
+
lines.push(`${body.queue.redrillsDue} drill${body.queue.redrillsDue === 1 ? "" : "s"} you were sure about ${body.queue.redrillsDue === 1 ? "is" : "are"} due for another look. Run: cockpit drills start`, "");
|
|
20
|
+
else if (body.queue.newLeft > 0)
|
|
21
|
+
lines.push(`${body.queue.newLeft} new drill${body.queue.newLeft === 1 ? "" : "s"} waiting. Run: cockpit drills start`, "");
|
|
22
|
+
else
|
|
23
|
+
lines.push("You are all caught up. Nothing new is waiting.", "");
|
|
24
|
+
lines.push(`Theme: ${body.theme.label}${body.theme.chosen ? "" : " (the default; change it with `cockpit drills theme set <theme>`)"}`, "");
|
|
25
|
+
if (body.bosses.length > 0) {
|
|
26
|
+
lines.push("Bosses");
|
|
27
|
+
for (const boss of body.bosses)
|
|
28
|
+
lines.push(` ${boss.bossName}: ${boss.progressLine}`);
|
|
29
|
+
lines.push("");
|
|
30
|
+
}
|
|
31
|
+
const { stats } = body;
|
|
32
|
+
lines.push(`You have answered ${stats.answered} and passed ${stats.passed}. ${stats.confidentMisses} wrong answer${stats.confidentMisses === 1 ? "" : "s"} you were sure about.`);
|
|
33
|
+
lines.push(stats.firstRunGreenStreak.known ? `PRs green on the first CI run, in a row: at least ${stats.firstRunGreenStreak.value}.` : (stats.firstRunGreenStreak.note ?? ""));
|
|
34
|
+
if (stats.recovery.known && stats.recovery.value)
|
|
35
|
+
lines.push(`Recovering from a hit: last ${minutes(stats.recovery.value.lastMinutes)}, your best ${minutes(stats.recovery.value.bestMinutes)}.`);
|
|
36
|
+
if (stats.teamBeaten.known && stats.teamBeaten.value)
|
|
37
|
+
lines.push(`Across the team, ${stats.teamBeaten.value.fightsBeaten} boss fight${stats.teamBeaten.value.fightsBeaten === 1 ? "" : "s"} won, ${stats.teamBeaten.value.patternsBeaten} pattern${stats.teamBeaten.value.patternsBeaten === 1 ? "" : "s"} beaten at least once.`);
|
|
38
|
+
if (body.history.length > 0) {
|
|
39
|
+
lines.push("", "Recently");
|
|
40
|
+
for (const item of body.history.slice(0, 5))
|
|
41
|
+
lines.push(` ${item.verdict === "pass" ? "passed" : item.verdict === "miss" ? "missed" : "unmarked"} ${item.title} (sure: ${item.confidence} of 5${item.hypercorrection ? ", comes back" : ""})`);
|
|
42
|
+
}
|
|
43
|
+
for (const note of body.notes)
|
|
44
|
+
lines.push("", note);
|
|
45
|
+
return lines.filter((line, index, all) => !(line === "" && all[index - 1] === ""));
|
|
46
|
+
}
|
|
47
|
+
export function reviewLines(body) {
|
|
48
|
+
const lines = [body.headline];
|
|
49
|
+
const section = (title, items) => {
|
|
50
|
+
if (items.length > 0)
|
|
51
|
+
lines.push("", title, ...items.map((item) => ` ${item}`));
|
|
52
|
+
};
|
|
53
|
+
if (body.hits.known && body.hits.value)
|
|
54
|
+
section("Your hits this week", body.hits.value.length === 0 ? ["None."] : body.hits.value.map((hit) => `A ${hit.weight} one: ${hit.patternLabel.toLowerCase()}${hit.recoveredMinutes === null ? "" : `, back to green in ${minutes(hit.recoveredMinutes)}`}.`));
|
|
55
|
+
section("Wins", body.wins);
|
|
56
|
+
section("Highs", body.highs);
|
|
57
|
+
section("Lows", body.lows);
|
|
58
|
+
section("Where you have stopped improving", body.plateaus);
|
|
59
|
+
section("What to practise", body.practise.map((item) => `${item.patternLabel}: ${item.why}`));
|
|
60
|
+
for (const note of body.notes)
|
|
61
|
+
lines.push("", note);
|
|
62
|
+
return lines;
|
|
63
|
+
}
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import { askAgentDoor, emitAgentDoor, failAgentDoor, openAgentDoor } from "./agent-door.js";
|
|
2
|
+
import { drillLines, overviewLines, reviewLines } from "./drills-render.js";
|
|
3
|
+
import { isInteractiveStdin, readPipedText, writeLine } from "./cli-io.js";
|
|
4
|
+
const TAG = "[drills cli]";
|
|
5
|
+
const READ_DEADLINE_MS = 30_000;
|
|
6
|
+
const WRITE_DEADLINE_MS = 55_000;
|
|
7
|
+
const ANSWER_MAX_CHARS = 4_000;
|
|
8
|
+
export async function runDrills(command, io) {
|
|
9
|
+
let door;
|
|
10
|
+
try {
|
|
11
|
+
door = await openAgentDoor("drills", command, io);
|
|
12
|
+
}
|
|
13
|
+
catch (error) {
|
|
14
|
+
const detail = error instanceof Error ? error.message : "this machine is not paired with Tower";
|
|
15
|
+
writeLine(io.stderr, `${TAG} refused ${JSON.stringify({ reason: "not_paired" })}`);
|
|
16
|
+
if (command.json)
|
|
17
|
+
writeLine(io.stdout, JSON.stringify({ ok: false, error: "not_paired", detail }));
|
|
18
|
+
else
|
|
19
|
+
writeLine(io.stderr, detail);
|
|
20
|
+
return 1;
|
|
21
|
+
}
|
|
22
|
+
switch (command.action) {
|
|
23
|
+
case "list": return list(door);
|
|
24
|
+
case "start": return start(door);
|
|
25
|
+
case "answer": return answer(command, door);
|
|
26
|
+
case "theme": return themeShow(door);
|
|
27
|
+
case "theme-set": return themeSet(command, door);
|
|
28
|
+
case "review": return review(door);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
async function list(door) {
|
|
32
|
+
const response = await askAgentDoor(door, { path: "/api/drills", method: "GET", label: "drills list", timeoutMs: READ_DEADLINE_MS });
|
|
33
|
+
if (!response.ok)
|
|
34
|
+
return failAgentDoor(door, TAG, response.reason, response.detail);
|
|
35
|
+
const body = response.body;
|
|
36
|
+
writeLine(door.io.stderr, `${TAG} list answered ${JSON.stringify({ open: body.current !== null, bosses: body.bosses.length, history: body.history.length, new_left: body.queue.newLeft })}`);
|
|
37
|
+
if (door.json)
|
|
38
|
+
return emitAgentDoor(door, response.body);
|
|
39
|
+
for (const line of overviewLines(body))
|
|
40
|
+
writeLine(door.io.stdout, line);
|
|
41
|
+
return 0;
|
|
42
|
+
}
|
|
43
|
+
async function start(door) {
|
|
44
|
+
const response = await askAgentDoor(door, { path: "/api/drills/start", method: "POST", label: "drills start", timeoutMs: READ_DEADLINE_MS, body: {} });
|
|
45
|
+
if (!response.ok)
|
|
46
|
+
return failAgentDoor(door, TAG, response.reason, response.detail);
|
|
47
|
+
const body = response.body;
|
|
48
|
+
writeLine(door.io.stderr, `${TAG} start answered ${JSON.stringify({ started: body.started, reason: body.reason })}`);
|
|
49
|
+
if (door.json)
|
|
50
|
+
return emitAgentDoor(door, response.body);
|
|
51
|
+
if (!body.started || !body.current) {
|
|
52
|
+
writeLine(door.io.stdout, body.message ?? "Nothing new is waiting for you right now.");
|
|
53
|
+
return 0;
|
|
54
|
+
}
|
|
55
|
+
for (const line of drillLines(body.current))
|
|
56
|
+
writeLine(door.io.stdout, line);
|
|
57
|
+
writeLine(door.io.stdout, "");
|
|
58
|
+
writeLine(door.io.stdout, "Answer first, then see the cause: echo \"what you think\" | cockpit drills answer --confidence <1-5>");
|
|
59
|
+
return 0;
|
|
60
|
+
}
|
|
61
|
+
async function openAttemptId(door) {
|
|
62
|
+
const response = await askAgentDoor(door, { path: "/api/drills", method: "GET", label: "drills list", timeoutMs: READ_DEADLINE_MS });
|
|
63
|
+
if (!response.ok)
|
|
64
|
+
return { ok: false, reason: response.reason, detail: response.detail };
|
|
65
|
+
const current = response.body.current;
|
|
66
|
+
if (!current)
|
|
67
|
+
return { ok: false, reason: "no_open_drill", detail: "No drill is open. Run `cockpit drills start` first." };
|
|
68
|
+
return { ok: true, id: current.attemptId };
|
|
69
|
+
}
|
|
70
|
+
async function answer(command, door) {
|
|
71
|
+
let text = command.text;
|
|
72
|
+
if (!text) {
|
|
73
|
+
if (isInteractiveStdin(door.io))
|
|
74
|
+
return failAgentDoor(door, TAG, "nothing_piped", "Pipe your answer in, e.g. `echo \"it will stop at 1,000 rows\" | cockpit drills answer --confidence 4`, or use --text. Nothing was sent.");
|
|
75
|
+
try {
|
|
76
|
+
text = (await readPipedText(door.io.stdin, { maxChars: ANSWER_MAX_CHARS, overflowMessage: `Keep your answer under ${ANSWER_MAX_CHARS} characters.` })).trim();
|
|
77
|
+
}
|
|
78
|
+
catch (error) {
|
|
79
|
+
return failAgentDoor(door, TAG, "answer_too_long", error instanceof Error ? error.message : String(error));
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
let id = command.attemptId;
|
|
83
|
+
if (!id) {
|
|
84
|
+
const open = await openAttemptId(door);
|
|
85
|
+
if (!open.ok)
|
|
86
|
+
return failAgentDoor(door, TAG, open.reason, open.detail);
|
|
87
|
+
id = open.id;
|
|
88
|
+
}
|
|
89
|
+
const response = await askAgentDoor(door, { path: "/api/drills/answer", method: "POST", label: "drills answer", timeoutMs: WRITE_DEADLINE_MS, body: { attemptId: id, answer: text, confidence: command.confidence } });
|
|
90
|
+
if (!response.ok)
|
|
91
|
+
return failAgentDoor(door, TAG, response.reason, response.detail);
|
|
92
|
+
const body = response.body;
|
|
93
|
+
writeLine(door.io.stderr, `${TAG} answered ${JSON.stringify({ verdict: body.verdict, hypercorrection: body.hypercorrection, redrills: body.redrills.length, new_win: body.newWin })}`);
|
|
94
|
+
if (door.json)
|
|
95
|
+
return emitAgentDoor(door, response.body);
|
|
96
|
+
const out = door.io.stdout;
|
|
97
|
+
writeLine(out, body.message);
|
|
98
|
+
writeLine(out, "");
|
|
99
|
+
writeLine(out, "The real cause:");
|
|
100
|
+
writeLine(out, ` ${body.reveal.realCause}`);
|
|
101
|
+
writeLine(out, "The guard that would have caught it:");
|
|
102
|
+
writeLine(out, ` ${body.reveal.guard}`);
|
|
103
|
+
if (body.reveal.aftermath)
|
|
104
|
+
writeLine(out, `What it cost: ${body.reveal.aftermath}`);
|
|
105
|
+
if (body.redrills.length > 0)
|
|
106
|
+
writeLine(out, `It comes back in ${body.redrills.map((plan) => plan.day).join(", ")} days.`);
|
|
107
|
+
return 0;
|
|
108
|
+
}
|
|
109
|
+
async function themeShow(door) {
|
|
110
|
+
const response = await askAgentDoor(door, { path: "/api/drills/theme", method: "GET", label: "drills theme", timeoutMs: READ_DEADLINE_MS });
|
|
111
|
+
if (!response.ok)
|
|
112
|
+
return failAgentDoor(door, TAG, response.reason, response.detail);
|
|
113
|
+
const body = response.body;
|
|
114
|
+
writeLine(door.io.stderr, `${TAG} theme answered ${JSON.stringify({ key: body.theme.key, chosen: body.theme.chosen, ready: body.ready })}`);
|
|
115
|
+
if (door.json)
|
|
116
|
+
return emitAgentDoor(door, response.body);
|
|
117
|
+
writeLine(door.io.stdout, `Your theme: ${body.theme.label}${body.theme.chosen ? "" : " (the default)"}`);
|
|
118
|
+
writeLine(door.io.stdout, "Choices:");
|
|
119
|
+
for (const choice of body.themes)
|
|
120
|
+
writeLine(door.io.stdout, ` ${choice.key}: ${choice.blurb}${choice.key === body.suggestedTheme && !body.theme.chosen ? " (suggested)" : ""}`);
|
|
121
|
+
writeLine(door.io.stdout, "Or use your own words. Change it with: cockpit drills theme set <theme> [--play <what you play>] [--watch <what you watch>] [--train <what you train>]");
|
|
122
|
+
return 0;
|
|
123
|
+
}
|
|
124
|
+
async function themeSet(command, door) {
|
|
125
|
+
const response = await askAgentDoor(door, {
|
|
126
|
+
path: "/api/drills/theme",
|
|
127
|
+
method: "POST",
|
|
128
|
+
label: "drills theme set",
|
|
129
|
+
timeoutMs: WRITE_DEADLINE_MS,
|
|
130
|
+
body: { theme: command.theme, ...(command.play ? { play: command.play } : {}), ...(command.watch ? { watch: command.watch } : {}), ...(command.train ? { train: command.train } : {}) },
|
|
131
|
+
});
|
|
132
|
+
if (!response.ok)
|
|
133
|
+
return failAgentDoor(door, TAG, response.reason, response.detail);
|
|
134
|
+
const body = response.body;
|
|
135
|
+
writeLine(door.io.stderr, `${TAG} theme saved ${JSON.stringify({ key: body.theme.key, closed: body.theme.closed, preparing: body.preparing ?? false })}`);
|
|
136
|
+
if (door.json)
|
|
137
|
+
return emitAgentDoor(door, response.body);
|
|
138
|
+
writeLine(door.io.stdout, body.message ?? "Theme saved.");
|
|
139
|
+
return 0;
|
|
140
|
+
}
|
|
141
|
+
async function review(door) {
|
|
142
|
+
const response = await askAgentDoor(door, { path: "/api/drills/review", method: "GET", label: "drills review", timeoutMs: READ_DEADLINE_MS });
|
|
143
|
+
if (!response.ok)
|
|
144
|
+
return failAgentDoor(door, TAG, response.reason, response.detail);
|
|
145
|
+
const body = response.body;
|
|
146
|
+
writeLine(door.io.stderr, `${TAG} review answered ${JSON.stringify({ practise: body.practise.length, plateaus: body.plateaus.length })}`);
|
|
147
|
+
if (door.json)
|
|
148
|
+
return emitAgentDoor(door, response.body);
|
|
149
|
+
for (const line of reviewLines(body))
|
|
150
|
+
writeLine(door.io.stdout, line);
|
|
151
|
+
return 0;
|
|
152
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { askAgentDoor, emitAgentDoor, failAgentDoor, openAgentDoor } from "./agent-door.js";
|
|
2
|
+
import { writeLine } from "./cli-io.js";
|
|
3
|
+
const TAG = "[hits cli]";
|
|
4
|
+
const READ_DEADLINE_MS = 55_000;
|
|
5
|
+
/** The door's own query for a read: only what the person asked for, so the door's defaults stay the door's. */
|
|
6
|
+
export function hitsReadQuery(command) {
|
|
7
|
+
const query = new URLSearchParams();
|
|
8
|
+
if (command.who)
|
|
9
|
+
query.set("who", command.who);
|
|
10
|
+
if (command.person)
|
|
11
|
+
query.set("person", command.person);
|
|
12
|
+
if (command.project)
|
|
13
|
+
query.set("project", command.project);
|
|
14
|
+
if (command.days !== undefined)
|
|
15
|
+
query.set("days", String(command.days));
|
|
16
|
+
if (command.top !== undefined)
|
|
17
|
+
query.set("top", String(command.top));
|
|
18
|
+
if (command.learning)
|
|
19
|
+
query.set("learning", "1");
|
|
20
|
+
if (command.patterns)
|
|
21
|
+
query.set("view", "patterns");
|
|
22
|
+
return query.toString();
|
|
23
|
+
}
|
|
24
|
+
export async function runHits(command, io) {
|
|
25
|
+
let door;
|
|
26
|
+
try {
|
|
27
|
+
door = await openAgentDoor("hits", command, io);
|
|
28
|
+
}
|
|
29
|
+
catch (error) {
|
|
30
|
+
const detail = error instanceof Error ? error.message : "this machine is not paired with Tower";
|
|
31
|
+
writeLine(io.stderr, `${TAG} refused ${JSON.stringify({ reason: "not_paired" })}`);
|
|
32
|
+
if (command.json)
|
|
33
|
+
writeLine(io.stdout, JSON.stringify({ ok: false, error: "not_paired", detail }));
|
|
34
|
+
else
|
|
35
|
+
writeLine(io.stderr, detail);
|
|
36
|
+
return 1;
|
|
37
|
+
}
|
|
38
|
+
if (command.action === "approve") {
|
|
39
|
+
const answer = await askAgentDoor(door, { path: "/api/hits/patterns/approve", method: "POST", label: "hits approve", timeoutMs: READ_DEADLINE_MS,
|
|
40
|
+
body: { scope: command.scope, ...(command.version !== undefined ? { version: command.version } : {}) } });
|
|
41
|
+
if (!answer.ok)
|
|
42
|
+
return failAgentDoor(door, TAG, answer.reason, answer.detail);
|
|
43
|
+
const body = answer.body;
|
|
44
|
+
writeLine(io.stderr, `${TAG} approve answered ${JSON.stringify({ reason: body.reason, approved: body.approved ?? 0, version: body.version ?? null })}`);
|
|
45
|
+
if (command.json)
|
|
46
|
+
return emitAgentDoor(door, body, body.ok ? 0 : 1);
|
|
47
|
+
writeLine(io.stdout, body.text);
|
|
48
|
+
return body.ok ? 0 : 1;
|
|
49
|
+
}
|
|
50
|
+
if (command.action === "timeline") {
|
|
51
|
+
const path = command.weeks !== undefined ? `/api/hits/timeline?weeks=${command.weeks}` : "/api/hits/timeline";
|
|
52
|
+
const answer = await askAgentDoor(door, { path, method: "GET", label: "hits timeline", timeoutMs: READ_DEADLINE_MS });
|
|
53
|
+
if (!answer.ok)
|
|
54
|
+
return failAgentDoor(door, TAG, answer.reason, answer.detail);
|
|
55
|
+
const body = answer.body;
|
|
56
|
+
writeLine(io.stderr, `${TAG} timeline read ${JSON.stringify({ weeks: body.weeks.length, hits: body.coverage.hits, labels: body.coverage.labels, end: body.coverage.end })}`);
|
|
57
|
+
if (command.json)
|
|
58
|
+
return emitAgentDoor(door, body);
|
|
59
|
+
writeLine(io.stdout, body.text);
|
|
60
|
+
return 0;
|
|
61
|
+
}
|
|
62
|
+
if (command.action === "coach") {
|
|
63
|
+
const answer = await askAgentDoor(door, { path: "/api/hits/coach", method: "GET", label: "hits coach", timeoutMs: READ_DEADLINE_MS });
|
|
64
|
+
if (!answer.ok)
|
|
65
|
+
return failAgentDoor(door, TAG, answer.reason, answer.detail);
|
|
66
|
+
const body = answer.body;
|
|
67
|
+
writeLine(io.stderr, `${TAG} coach read ${JSON.stringify({ alerts: body.alerts.length, rows: body.coverage.rows, pages: body.coverage.pages, end: body.coverage.end })}`);
|
|
68
|
+
if (command.json)
|
|
69
|
+
return emitAgentDoor(door, body);
|
|
70
|
+
writeLine(io.stdout, body.text);
|
|
71
|
+
return 0;
|
|
72
|
+
}
|
|
73
|
+
const asked = hitsReadQuery(command);
|
|
74
|
+
const answer = await askAgentDoor(door, { path: asked ? `/api/hits?${asked}` : "/api/hits", method: "GET", label: "hits read", timeoutMs: READ_DEADLINE_MS });
|
|
75
|
+
if (!answer.ok)
|
|
76
|
+
return failAgentDoor(door, TAG, answer.reason, answer.detail);
|
|
77
|
+
const body = answer.body;
|
|
78
|
+
writeLine(io.stderr, `${TAG} read answered ${JSON.stringify({ who: body.who, hits: body.totals.hits, rows: body.coverage.rows, pages: body.coverage.pages, end: body.coverage.end })}`);
|
|
79
|
+
if (command.json)
|
|
80
|
+
return emitAgentDoor(door, body);
|
|
81
|
+
writeLine(io.stdout, body.text);
|
|
82
|
+
return 0;
|
|
83
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { askAgentDoor, emitAgentDoor, failAgentDoor, openAgentDoor } from "./agent-door.js";
|
|
2
|
+
import { writeLine } from "./cli-io.js";
|
|
3
|
+
const TAG = "[intentions cli]";
|
|
4
|
+
const DEADLINE_MS = 55_000;
|
|
5
|
+
export async function runIntentions(command, io) {
|
|
6
|
+
let door;
|
|
7
|
+
try {
|
|
8
|
+
door = await openAgentDoor("intentions", command, io);
|
|
9
|
+
}
|
|
10
|
+
catch (error) {
|
|
11
|
+
const detail = error instanceof Error ? error.message : "this machine is not paired with Tower";
|
|
12
|
+
writeLine(io.stderr, `${TAG} refused ${JSON.stringify({ reason: "not_paired" })}`);
|
|
13
|
+
if (command.json)
|
|
14
|
+
writeLine(io.stdout, JSON.stringify({ ok: false, error: "not_paired", detail }));
|
|
15
|
+
else
|
|
16
|
+
writeLine(io.stderr, detail);
|
|
17
|
+
return 1;
|
|
18
|
+
}
|
|
19
|
+
let request;
|
|
20
|
+
if (command.action === "list") {
|
|
21
|
+
const query = new URLSearchParams();
|
|
22
|
+
if (command.status)
|
|
23
|
+
query.set("status", command.status);
|
|
24
|
+
if (command.project)
|
|
25
|
+
query.set("project", command.project);
|
|
26
|
+
if (command.limit !== undefined)
|
|
27
|
+
query.set("limit", String(command.limit));
|
|
28
|
+
const asked = query.toString();
|
|
29
|
+
request = { path: asked ? `/api/intentions?${asked}` : "/api/intentions", method: "GET", label: "intentions list" };
|
|
30
|
+
}
|
|
31
|
+
else if (command.action === "show") {
|
|
32
|
+
request = { path: `/api/intentions/${encodeURIComponent(command.id)}`, method: "GET", label: "intentions show" };
|
|
33
|
+
}
|
|
34
|
+
else {
|
|
35
|
+
request = {
|
|
36
|
+
path: `/api/intentions/${encodeURIComponent(command.id)}`, method: "POST", label: `intentions ${command.action}`,
|
|
37
|
+
body: { action: command.action, ...(command.evidence ? { evidence: command.evidence } : {}), ...(command.by ? { by: command.by } : {}) },
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
const answer = await askAgentDoor(door, { ...request, timeoutMs: DEADLINE_MS });
|
|
41
|
+
if (!answer.ok)
|
|
42
|
+
return failAgentDoor(door, TAG, answer.reason, answer.detail);
|
|
43
|
+
const body = answer.body;
|
|
44
|
+
writeLine(io.stderr, `${TAG} ${command.action} answered ${JSON.stringify({ rows: body.intentions?.length ?? 1, ...(body.coverage ? { requested: body.coverage.requested, returned: body.coverage.returned, unique: body.coverage.unique, end: body.coverage.end } : {}) })}`);
|
|
45
|
+
if (command.json)
|
|
46
|
+
return emitAgentDoor(door, body);
|
|
47
|
+
writeLine(io.stdout, body.text ?? "");
|
|
48
|
+
return 0;
|
|
49
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `cockpit docs export` flag parsing (ticket 20694), split out of `local-args-tower-docs-msg.ts`,
|
|
3
|
+
* which sits at the file-size ratchet.
|
|
4
|
+
*/
|
|
5
|
+
import { optionalNonEmpty } from "./local-arg-values.js";
|
|
6
|
+
const DOCS_EXPORT_FORMATS = ["md", "pdf", "docx", "txt"];
|
|
7
|
+
/** Ticket 20694: `--format`/`--out` mean something on `export` alone, and an unknown format is refused by name. */
|
|
8
|
+
export function parseDocsExportFlags(action, values) {
|
|
9
|
+
const format = optionalNonEmpty(values.flags.get("--format"))?.toLowerCase();
|
|
10
|
+
const outPath = optionalNonEmpty(values.flags.get("--out"));
|
|
11
|
+
if (action !== "export") {
|
|
12
|
+
if (format)
|
|
13
|
+
throw new Error("--format belongs to `cockpit docs export`.");
|
|
14
|
+
if (outPath)
|
|
15
|
+
throw new Error("--out belongs to `cockpit docs export`.");
|
|
16
|
+
return {};
|
|
17
|
+
}
|
|
18
|
+
const chosen = format === "markdown" ? "md" : format === "word" ? "docx" : format === "text" ? "txt" : (format ?? "md");
|
|
19
|
+
if (!DOCS_EXPORT_FORMATS.includes(chosen)) {
|
|
20
|
+
throw new Error(`docs export --format must be one of ${DOCS_EXPORT_FORMATS.join(", ")} (Word is docx), not "${format}".`);
|
|
21
|
+
}
|
|
22
|
+
return { exportFormat: chosen, ...(outPath ? { outPath } : {}) };
|
|
23
|
+
}
|
|
@@ -301,6 +301,14 @@ export function parseTeamArgs(args) {
|
|
|
301
301
|
}
|
|
302
302
|
return { kind: "team", action: `github-login-${operation}`, personRef, githubLogin, ...common };
|
|
303
303
|
}
|
|
304
|
+
if (verb === "timezone") {
|
|
305
|
+
const [operation, personRef, zone, ...extra] = rest;
|
|
306
|
+
if (!["set", "list"].includes(operation ?? "") || extra.length ||
|
|
307
|
+
(operation === "list" ? personRef !== undefined : !personRef || !zone)) {
|
|
308
|
+
throw new Error("team timezone takes set <person> <iana-zone> (or the word unknown), or list.");
|
|
309
|
+
}
|
|
310
|
+
return { kind: "team", action: operation === "set" ? "timezone-set" : "timezone-list", ...(personRef ? { personRef } : {}), ...(zone ? { zone } : {}), ...common };
|
|
311
|
+
}
|
|
304
312
|
if (verb === "device") {
|
|
305
313
|
return parseTeamDeviceArgs(rest, values.flags, common);
|
|
306
314
|
}
|