@bli-cockpit/cli 0.2.222 → 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/agent-plugins-converge.js +182 -0
- package/dist/agent-plugins.js +116 -0
- package/dist/autostart-darwin-plist.js +2 -2
- package/dist/autostart-windows-scripts.js +2 -2
- 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/doctor-access.js +2 -2
- package/dist/commands/doctor-agent-plugins.js +82 -0
- package/dist/commands/doctor-pipeline.js +2 -2
- package/dist/commands/doctor-report.js +1 -0
- package/dist/commands/doctor-update.js +2 -2
- package/dist/commands/doctor.js +6 -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-auth.js +5 -2
- 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/sync-heal-agent-plugins.js +46 -0
- package/dist/commands/sync-heal.js +6 -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/dist/local-state-config.js +21 -1
- package/dist/local-state.js +1 -1
- package/dist/process-runner.js +7 -3
- package/dist/state-dir-audit.js +1 -0
- package/package.json +4 -4
package/dist/commands/logs.js
CHANGED
|
@@ -18,6 +18,7 @@ import { asRecord, callTower, openTower, writeCommandFailure } from "./tower-com
|
|
|
18
18
|
import { getCollectorRuntimePaths } from "../local-state-paths.js";
|
|
19
19
|
import { isInteractiveStdin, readPipedText, writeLine } from "./cli-io.js";
|
|
20
20
|
import { runStoredLogs, storedProject } from "./logs-stored.js";
|
|
21
|
+
import { runLogsSearch } from "./logs-search.js";
|
|
21
22
|
const TAG = "[logs cli]";
|
|
22
23
|
const MAX_STDIN_CHARS = 64 * 1024 * 1024;
|
|
23
24
|
function refuse(io, json, reason, detail) {
|
|
@@ -38,6 +39,8 @@ export async function runLogs(command, io, deps = {}) {
|
|
|
38
39
|
writeLine(io.stdout, command.json ? JSON.stringify(result.body) : String(asRecord(result.body).key));
|
|
39
40
|
return 0;
|
|
40
41
|
}
|
|
42
|
+
if (command.search)
|
|
43
|
+
return runLogsSearch(command, io);
|
|
41
44
|
// BLI-12855: a project name (or --stored, or --list) reads the digests Tower's cron stored, not a local log.
|
|
42
45
|
const stored = storedProject(command);
|
|
43
46
|
if (command.list || stored)
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `cockpit memory exam` - the nightly memory exam score (BLI-19249).
|
|
3
|
+
*
|
|
4
|
+
* A READ of `GET /api/memory/exam`, with this machine's device token, through `agent-door.ts`: the same door the
|
|
5
|
+
* `memory_exam` MCP tool calls. The exam itself is a step of the nightly `memory-consolidate` Railway job. Output: the
|
|
6
|
+
* one line (purity, graded of planned, trend, worst project); `--misses` adds the missed questions with their reason
|
|
7
|
+
* label, true answer and fix status. The rows are the owner's own words, so no one else reads them, a super admin
|
|
8
|
+
* included; a caller with no exam gets `no exam yet`, never somebody else's night. Logs carry counts only.
|
|
9
|
+
*/
|
|
10
|
+
import { askAgentDoor, emitAgentDoor, failAgentDoor } from "./agent-door.js";
|
|
11
|
+
import { writeLine } from "./cli-io.js";
|
|
12
|
+
const TAG = "[memory cli]";
|
|
13
|
+
const READ_DEADLINE_MS = 30_000;
|
|
14
|
+
export async function runMemoryExam(command, door) {
|
|
15
|
+
const params = new URLSearchParams();
|
|
16
|
+
if (command.night)
|
|
17
|
+
params.set("night", command.night);
|
|
18
|
+
if (command.misses)
|
|
19
|
+
params.set("misses", "1");
|
|
20
|
+
const query = params.size > 0 ? `?${params.toString()}` : "";
|
|
21
|
+
const answer = await askAgentDoor(door, { path: `/api/memory/exam${query}`, method: "GET", label: "memory exam", timeoutMs: READ_DEADLINE_MS });
|
|
22
|
+
if (!answer.ok)
|
|
23
|
+
return failAgentDoor(door, TAG, answer.reason, answer.detail);
|
|
24
|
+
const body = answer.body;
|
|
25
|
+
writeLine(door.io.stderr, `${TAG} exam ${JSON.stringify({ reason: body.reason ?? "exam_read", night: body.latest?.night ?? null, previous_nights: body.previous?.length ?? 0, misses: body.misses?.length ?? null })}`);
|
|
26
|
+
if (door.json)
|
|
27
|
+
return emitAgentDoor(door, answer.body);
|
|
28
|
+
if (!body.latest || !body.line) {
|
|
29
|
+
writeLine(door.io.stdout, body.reason === "night_not_found"
|
|
30
|
+
? `No exam is recorded for ${command.night ?? "that night"}.`
|
|
31
|
+
: "No memory exam yet. It runs as the last step of the nightly memory-consolidate job; nothing has been recorded for you.");
|
|
32
|
+
return 0;
|
|
33
|
+
}
|
|
34
|
+
const { latest } = body;
|
|
35
|
+
writeLine(door.io.stdout, body.line);
|
|
36
|
+
for (const detail of body.detail ?? [])
|
|
37
|
+
writeLine(door.io.stdout, detail);
|
|
38
|
+
const reasons = Object.entries(latest.missReasons ?? {}).map(([label, count]) => `${label} ${count}`).join(", ");
|
|
39
|
+
writeLine(door.io.stdout, `Mode ${latest.mode ?? "report"}; spent $${Number(latest.costUsd ?? 0).toFixed(4)}; misses: ${reasons || "none"}.`);
|
|
40
|
+
for (const miss of body.misses ?? []) {
|
|
41
|
+
writeLine(door.io.stdout, ` #${miss.ordinal ?? "?"} [${miss.source ?? "?"}${miss.project ? ` / ${miss.project}` : ""}] ${miss.reason ?? "no reason"}${miss.fixStatus ? ` (${miss.fixKind ?? "fix"}: ${miss.fixStatus})` : ""}`);
|
|
42
|
+
writeLine(door.io.stdout, ` asked: ${miss.question ?? ""}`);
|
|
43
|
+
writeLine(door.io.stdout, ` truth: ${miss.expected ?? ""}`);
|
|
44
|
+
writeLine(door.io.stdout, ` said: ${miss.answer ?? "(no answer)"}`);
|
|
45
|
+
}
|
|
46
|
+
return 0;
|
|
47
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { askAgentDoor, emitAgentDoor, failAgentDoor } from "./agent-door.js";
|
|
2
|
+
import { writeLine } from "./cli-io.js";
|
|
3
|
+
import { optionalNonEmpty, optionalUrl, parseNamedArgs } from "./local-arg-values.js";
|
|
4
|
+
/** A whole number inside a range, or a refusal that names the flag. */
|
|
5
|
+
function bounded(flag, raw, min, max) {
|
|
6
|
+
if (raw === undefined)
|
|
7
|
+
return undefined;
|
|
8
|
+
if (!/^\d+$/.test(raw) || Number(raw) < min || Number(raw) > max)
|
|
9
|
+
throw new Error(`${flag} must be between ${min} and ${max}.`);
|
|
10
|
+
return Number(raw);
|
|
11
|
+
}
|
|
12
|
+
/** BLI-19253: `cockpit memory repeats [--project <name>] [--min <n>] [--limit <n>] [--json]`. */
|
|
13
|
+
export function parseMemoryRepeatsArgs(args) {
|
|
14
|
+
const values = parseNamedArgs(args, { allowedFlags: ["--project", "--min", "--limit", "--json", "--home", "--dashboard-url"], valueFlags: ["--project", "--min", "--limit", "--home", "--dashboard-url"] });
|
|
15
|
+
if (values.positionals.slice(1).length)
|
|
16
|
+
throw new Error("memory repeats takes no positionals; use --project, --min and --limit.");
|
|
17
|
+
return { kind: "memory-store", action: "repeats", containerTags: [], project: optionalNonEmpty(values.flags.get("--project")),
|
|
18
|
+
minCount: bounded("--min", values.flags.get("--min"), 2, 50), limit: bounded("--limit", values.flags.get("--limit"), 1, 200), json: values.booleans.has("--json"),
|
|
19
|
+
homeDir: optionalNonEmpty(values.flags.get("--home")), dashboardUrl: optionalUrl(values.flags.get("--dashboard-url")) };
|
|
20
|
+
}
|
|
21
|
+
export async function runMemoryRepeats(command, door) {
|
|
22
|
+
const params = new URLSearchParams();
|
|
23
|
+
if (command.project)
|
|
24
|
+
params.set("project", command.project);
|
|
25
|
+
if (command.minCount !== undefined)
|
|
26
|
+
params.set("min", String(command.minCount));
|
|
27
|
+
if (command.limit !== undefined)
|
|
28
|
+
params.set("limit", String(command.limit));
|
|
29
|
+
const query = params.toString();
|
|
30
|
+
const answer = await askAgentDoor(door, { method: "GET", path: `/api/memory/repeats${query ? `?${query}` : ""}`, label: "memory repeats", timeoutMs: 60_000 });
|
|
31
|
+
if (!answer.ok)
|
|
32
|
+
return failAgentDoor(door, "[memory repeats cli]", answer.reason, answer.detail);
|
|
33
|
+
const body = answer.body;
|
|
34
|
+
writeLine(door.io.stderr, `[memory repeats cli] answered ${JSON.stringify({ reason: body.capped ? "capped" : "exhausted", requested: body.requested, returned: body.returned })}`);
|
|
35
|
+
if (door.json)
|
|
36
|
+
return emitAgentDoor(door, body);
|
|
37
|
+
writeLine(door.io.stdout, body.words);
|
|
38
|
+
for (const repeat of body.repeats)
|
|
39
|
+
writeLine(door.io.stdout, `- ${repeat.line}${repeat.project ? ` [${repeat.project}]` : ""}`);
|
|
40
|
+
for (const row of body.coverage)
|
|
41
|
+
writeLine(door.io.stdout, `${row.label}: ${row.labelled.toLocaleString("en-US")} memories, ${row.grouped.toLocaleString("en-US")} grouped by subject`);
|
|
42
|
+
writeLine(door.io.stdout, `As of ${body.asOf}.${body.capped ? " Result capped; raise --limit (max 200)." : ""}`);
|
|
43
|
+
return 0;
|
|
44
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* How `cockpit memory search` prints its rows (split out of `memory-store.ts`, which is at the file-length ceiling).
|
|
3
|
+
*
|
|
4
|
+
* A merged memory prints its originals under it (BLI-19246); an original found by an exact-detail question prints as
|
|
5
|
+
* its own row, tagged. One already printed as its own row is not repeated under its merged sentence.
|
|
6
|
+
*/
|
|
7
|
+
import { memoryProjectPrefix } from "./memory-project-label.js";
|
|
8
|
+
import { writeLine } from "./cli-io.js";
|
|
9
|
+
/** One terminal line: newlines folded, so a long memory cannot break the listing. */
|
|
10
|
+
export function oneLine(value) {
|
|
11
|
+
return String(value ?? "").replace(/\s+/g, " ").trim();
|
|
12
|
+
}
|
|
13
|
+
/** `[87%] ` when the row was found by meaning, empty when it was a keyword hit. */
|
|
14
|
+
function similarityLabel(row) {
|
|
15
|
+
const channels = row["channels"];
|
|
16
|
+
if (Array.isArray(channels) && !channels.includes("semantic"))
|
|
17
|
+
return "";
|
|
18
|
+
const similarity = Number(row["similarity"]);
|
|
19
|
+
return Number.isFinite(similarity) && similarity >= 0 && similarity <= 1
|
|
20
|
+
? `[${Math.round(similarity * 100)}%] `
|
|
21
|
+
: "";
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* BLI-12927: memory is public across the team, so a hit can be a teammate's.
|
|
25
|
+
* ` filed by Edward on 2026-09-14` ahead of nothing when it is the caller's own.
|
|
26
|
+
*/
|
|
27
|
+
function filedByLabel(row) {
|
|
28
|
+
const name = typeof row["savedBy"] === "string" ? row["savedBy"].trim() : "";
|
|
29
|
+
if (!name)
|
|
30
|
+
return "";
|
|
31
|
+
const filed = typeof row["createdAt"] === "string" ? new Date(row["createdAt"]) : null;
|
|
32
|
+
const day = filed && !Number.isNaN(filed.getTime()) ? filed.toISOString().slice(0, 10) : null;
|
|
33
|
+
return day ? ` filed by ${name} on ${day}` : ` filed by ${name}`;
|
|
34
|
+
}
|
|
35
|
+
/** `, true from 2026-07-01 to 2026-09-01`, or nothing when the door sent no window. */
|
|
36
|
+
function windowText(row) {
|
|
37
|
+
return typeof row["window"] === "string" && row["window"] ? `, ${row["window"]}` : "";
|
|
38
|
+
}
|
|
39
|
+
export function printSearchRows(out, results) {
|
|
40
|
+
const printed = new Set(results.filter((entry) => entry["original"] === true || entry["earlierVersion"] === true).map((entry) => String(entry["id"])));
|
|
41
|
+
for (const row of results) {
|
|
42
|
+
const head = row["original"] === true
|
|
43
|
+
? `(original, merged into ${String(row["successorId"] ?? "?")}) `
|
|
44
|
+
: row["earlierVersion"] === true
|
|
45
|
+
// BLI-19247: a fact a newer one replaced, with when it was true.
|
|
46
|
+
? `(earlier version${windowText(row)}, replaced by ${String(row["successorId"] ?? "?")}) `
|
|
47
|
+
: similarityLabel(row);
|
|
48
|
+
writeLine(out, `${head}${memoryProjectPrefix(row)}${oneLine(row["memory"])}`);
|
|
49
|
+
writeLine(out, ` ${String(row["id"] ?? "")} ${String(row["containerTag"] ?? "")}${filedByLabel(row)}`);
|
|
50
|
+
printOriginals(out, row, printed);
|
|
51
|
+
printSource(out, row["source"], " ");
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
/** Why a source gave no text, in words (BLI-19255). */
|
|
55
|
+
const SOURCE_STATUS_WORDS = {
|
|
56
|
+
no_pointer: "this memory names no source",
|
|
57
|
+
unsupported_source: "it was imported from another store, so there is no raw source here",
|
|
58
|
+
needs_rls_client: "no session the database can attribute to you; run `cockpit login` again",
|
|
59
|
+
not_readable: "the source belongs to someone else and you may not read it",
|
|
60
|
+
missing: "the source is no longer available",
|
|
61
|
+
too_large: "the stored session is too large to read for an excerpt",
|
|
62
|
+
redaction_stub: "only a redaction stub was stored for that session",
|
|
63
|
+
fact_not_found: "the source was read and does not contain this fact",
|
|
64
|
+
budget: "reading the source ran out of time",
|
|
65
|
+
read_failed: "the archive did not answer",
|
|
66
|
+
};
|
|
67
|
+
/** The verbatim stretch of the raw session or note behind a memory, or the reason there is none (BLI-19255). */
|
|
68
|
+
export function printSource(out, source, indent) {
|
|
69
|
+
if (!source)
|
|
70
|
+
return;
|
|
71
|
+
const excerpt = (source["excerpt"] ?? null);
|
|
72
|
+
if (source["status"] !== "excerpt" || !excerpt) {
|
|
73
|
+
writeLine(out, `${indent}source not shown: ${SOURCE_STATUS_WORDS[String(source["status"])] ?? String(source["status"])}.`);
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
const kind = source["type"] === "meeting_note" ? "meeting note" : "session";
|
|
77
|
+
writeLine(out, `${indent}source, verbatim from the ${kind} (lines ${String(excerpt["startLine"])}-${String(excerpt["endLine"])} of ${String(excerpt["totalLines"])}):`);
|
|
78
|
+
for (const line of String(excerpt["text"] ?? "").split("\n"))
|
|
79
|
+
writeLine(out, `${indent} | ${line}`);
|
|
80
|
+
}
|
|
81
|
+
function printOriginals(out, row, alreadyPrinted) {
|
|
82
|
+
const originals = Array.isArray(row["originals"]) ? row["originals"] : [];
|
|
83
|
+
const fresh = originals.filter((original) => !alreadyPrinted.has(String(original["id"])));
|
|
84
|
+
if (fresh.length === 0)
|
|
85
|
+
return;
|
|
86
|
+
const merged = Number(row["consolidatedFrom"] ?? 0);
|
|
87
|
+
writeLine(out, merged > 0 ? ` merged from ${merged} memories; originals, for exact details:` : " earlier versions, replaced by this one:");
|
|
88
|
+
for (const original of fresh) {
|
|
89
|
+
const earlier = original["kind"] === "earlier_version" ? `[earlier version${windowText(original)}] ` : "";
|
|
90
|
+
writeLine(out, ` - ${earlier}${oneLine(original["memory"])} (${String(original["id"] ?? "")})`);
|
|
91
|
+
}
|
|
92
|
+
const hidden = (row["originalsHidden"] ?? {});
|
|
93
|
+
if (Number(hidden["private"] ?? 0) > 0)
|
|
94
|
+
writeLine(out, ` (${String(hidden["private"])} private original(s) not shown)`);
|
|
95
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `cockpit memory source <id> [--query "<words>"] [--chars 200..6000]` (BLI-19255): the raw session or meeting
|
|
3
|
+
* behind a memory, as a bounded verbatim excerpt.
|
|
4
|
+
*
|
|
5
|
+
* Memory is an index over the archive. A READ over `GET /api/memory/source`, the same door the `memory_source` MCP
|
|
6
|
+
* tool calls, with this machine's device token. The door follows the memory's own source pointer and returns the
|
|
7
|
+
* stretch of the stored transcript or note around the fact, as stored (redaction unchanged), or the reason there is
|
|
8
|
+
* none (`not_readable`, `missing`, `too_large`, ...). It reads only what your own session may read and writes nothing.
|
|
9
|
+
*/
|
|
10
|
+
import { askAgentDoor, emitAgentDoor, failAgentDoor } from "./agent-door.js";
|
|
11
|
+
import { writeLine } from "./cli-io.js";
|
|
12
|
+
import { printSource } from "./memory-search-print.js";
|
|
13
|
+
const TAG = "[memory cli]";
|
|
14
|
+
const READ_DEADLINE_MS = 30_000;
|
|
15
|
+
export async function runMemorySource(command, door) {
|
|
16
|
+
if (!command.memoryId) {
|
|
17
|
+
return failAgentDoor(door, TAG, "id_required", "memory source needs the memory id `cockpit memory search` printed.");
|
|
18
|
+
}
|
|
19
|
+
const params = new URLSearchParams({ id: command.memoryId });
|
|
20
|
+
if (command.sourceQuery)
|
|
21
|
+
params.set("query", command.sourceQuery);
|
|
22
|
+
if (command.sourceChars !== undefined)
|
|
23
|
+
params.set("chars", String(command.sourceChars));
|
|
24
|
+
const answer = await askAgentDoor(door, {
|
|
25
|
+
path: `/api/memory/source?${params.toString()}`,
|
|
26
|
+
method: "GET",
|
|
27
|
+
label: "memory source",
|
|
28
|
+
timeoutMs: READ_DEADLINE_MS,
|
|
29
|
+
});
|
|
30
|
+
if (!answer.ok)
|
|
31
|
+
return failAgentDoor(door, TAG, answer.reason, answer.detail);
|
|
32
|
+
const body = answer.body;
|
|
33
|
+
// Status and sizes only: a source's text never enters a log line.
|
|
34
|
+
writeLine(door.io.stderr, `${TAG} source ${JSON.stringify({ status: body.source?.status ?? null, type: body.source?.type ?? null, chars: body.source?.excerpt?.chars ?? 0 })}`);
|
|
35
|
+
if (door.json)
|
|
36
|
+
return emitAgentDoor(door, answer.body);
|
|
37
|
+
printSource(door.io.stdout, body.source, "");
|
|
38
|
+
return 0;
|
|
39
|
+
}
|
|
@@ -43,13 +43,17 @@
|
|
|
43
43
|
* newline in it cannot be quoted correctly anyway.
|
|
44
44
|
*/
|
|
45
45
|
import { runMemoryMap } from "./memory-map.js";
|
|
46
|
+
import { runMemoryRepeats } from "./memory-repeats.js";
|
|
46
47
|
import { runMemoryRules } from "./memory-rules.js";
|
|
47
48
|
import { memoryProjectPrefix } from "./memory-project-label.js";
|
|
49
|
+
import { oneLine, printSearchRows } from "./memory-search-print.js";
|
|
48
50
|
import { memorySaveOutsideRoots } from "@bli-cockpit/memory-mcp/dist/recorded-outside-roots.js";
|
|
49
51
|
import { resolveContainerTag } from "@bli-cockpit/memory-mcp/dist/container-tag.js";
|
|
50
52
|
import { askAgentDoor, emitAgentDoor, failAgentDoor, openAgentDoor, } from "./agent-door.js";
|
|
51
53
|
import { isInteractiveStdin, readPipedText, writeLine } from "./cli-io.js";
|
|
52
54
|
import { runMemoryChain } from "./memory-chain.js";
|
|
55
|
+
import { runMemoryExam } from "./memory-exam.js";
|
|
56
|
+
import { runMemorySource } from "./memory-source.js";
|
|
53
57
|
import { runMemoryConsolidation } from "./memory-consolidation.js";
|
|
54
58
|
import { cacheAgeLabel, cacheCoverage, readThroughCache } from "./read-cache.js";
|
|
55
59
|
import { runMemoryRequeue } from "./memory-requeue.js";
|
|
@@ -69,6 +73,9 @@ export async function runMemoryStore(command, io) {
|
|
|
69
73
|
switch (command.action) {
|
|
70
74
|
case "map":
|
|
71
75
|
return runMemoryMap(door);
|
|
76
|
+
case "repeats":
|
|
77
|
+
// BLI-19253: subjects people corrected or ordered more than once. Its own module and door.
|
|
78
|
+
return runMemoryRepeats(command, door);
|
|
72
79
|
case "knowledge":
|
|
73
80
|
case "rules":
|
|
74
81
|
return runMemoryRules(command, door);
|
|
@@ -90,6 +97,12 @@ export async function runMemoryStore(command, io) {
|
|
|
90
97
|
return runMemoryRequeue(command, io);
|
|
91
98
|
case "chain":
|
|
92
99
|
return runMemoryChain(command, door);
|
|
100
|
+
case "exam":
|
|
101
|
+
// BLI-19249. Its own module: the nightly memory exam score, owner-only.
|
|
102
|
+
return runMemoryExam(command, door);
|
|
103
|
+
case "source":
|
|
104
|
+
// BLI-19255: the raw session or meeting behind a memory. Its own module, a different door and shape.
|
|
105
|
+
return runMemorySource(command, door);
|
|
93
106
|
case "private":
|
|
94
107
|
case "public":
|
|
95
108
|
// BLI-12927: the narrowing switch. Its own module, so this file stays
|
|
@@ -163,6 +176,10 @@ async function searchMemory(command, door) {
|
|
|
163
176
|
containerTag: containerTags,
|
|
164
177
|
...(command.limit === undefined ? {} : { limit: command.limit }),
|
|
165
178
|
includeProfile: command.includeRecent !== false,
|
|
179
|
+
// BLI-19246: absent lets the door decide by the question.
|
|
180
|
+
...(command.includeOriginals === undefined ? {} : { includeOriginals: command.includeOriginals }),
|
|
181
|
+
// BLI-19255: absent lets the door decide by the question.
|
|
182
|
+
...(command.includeSource === undefined ? {} : { includeSource: command.includeSource }),
|
|
166
183
|
};
|
|
167
184
|
const answer = await readThroughCache({
|
|
168
185
|
io: door.io,
|
|
@@ -216,10 +233,7 @@ async function searchMemory(command, door) {
|
|
|
216
233
|
writeLine(door.io.stdout, "No memories matched. The record is silent on this — it does not mean the store is down.");
|
|
217
234
|
}
|
|
218
235
|
else {
|
|
219
|
-
|
|
220
|
-
writeLine(door.io.stdout, `${similarityLabel(row)}${memoryProjectPrefix(row)}${oneLine(row["memory"])}`);
|
|
221
|
-
writeLine(door.io.stdout, ` ${String(row["id"] ?? "")} ${String(row["containerTag"] ?? "")}${filedByLabel(row)}`);
|
|
222
|
-
}
|
|
236
|
+
printSearchRows(door.io.stdout, results);
|
|
223
237
|
}
|
|
224
238
|
if (recent.length > 0) {
|
|
225
239
|
writeLine(door.io.stdout, "");
|
|
@@ -382,30 +396,4 @@ async function readBody(command, door, maxChars, what) {
|
|
|
382
396
|
detail: error instanceof Error ? error.message : String(error),
|
|
383
397
|
};
|
|
384
398
|
}
|
|
385
|
-
}
|
|
386
|
-
/** `[87%] ` when the row was found by meaning, empty when it was a keyword hit. */
|
|
387
|
-
function similarityLabel(row) {
|
|
388
|
-
const channels = row["channels"];
|
|
389
|
-
if (Array.isArray(channels) && !channels.includes("semantic"))
|
|
390
|
-
return "";
|
|
391
|
-
const similarity = Number(row["similarity"]);
|
|
392
|
-
return Number.isFinite(similarity) && similarity >= 0 && similarity <= 1
|
|
393
|
-
? `[${Math.round(similarity * 100)}%] `
|
|
394
|
-
: "";
|
|
395
|
-
}
|
|
396
|
-
/**
|
|
397
|
-
* BLI-12927: memory is public across the team, so a hit can be a teammate's.
|
|
398
|
-
* ` filed by Edward on 2026-09-14` ahead of nothing when it is the caller's own.
|
|
399
|
-
*/
|
|
400
|
-
function filedByLabel(row) {
|
|
401
|
-
const name = typeof row["savedBy"] === "string" ? row["savedBy"].trim() : "";
|
|
402
|
-
if (!name)
|
|
403
|
-
return "";
|
|
404
|
-
const filed = typeof row["createdAt"] === "string" ? new Date(row["createdAt"]) : null;
|
|
405
|
-
const day = filed && !Number.isNaN(filed.getTime()) ? filed.toISOString().slice(0, 10) : null;
|
|
406
|
-
return day ? ` filed by ${name} on ${day}` : ` filed by ${name}`;
|
|
407
|
-
}
|
|
408
|
-
/** One terminal line: newlines folded, so a long memory cannot break the listing. */
|
|
409
|
-
function oneLine(value) {
|
|
410
|
-
return String(value ?? "").replace(/\s+/g, " ").trim();
|
|
411
399
|
}
|
|
@@ -26,7 +26,7 @@ export function runMemoryVerbList(io, json) {
|
|
|
26
26
|
ok: true,
|
|
27
27
|
noun: "memory",
|
|
28
28
|
machine_verbs: ["install", "status"],
|
|
29
|
-
store_verbs: ["rules", "knowledge", "search", "save", "update", "forget", "private", "public", "profile", "consolidation", "chain", "requeue"],
|
|
29
|
+
store_verbs: ["rules", "knowledge", "search", "save", "update", "forget", "private", "public", "profile", "consolidation", "chain", "source", "exam", "requeue"],
|
|
30
30
|
log_verbs: ["log"],
|
|
31
31
|
}));
|
|
32
32
|
return 0;
|
|
@@ -35,7 +35,7 @@ export function runMemoryVerbList(io, json) {
|
|
|
35
35
|
"cockpit memory — BLI Memory: the store, and this machine's registration.",
|
|
36
36
|
"",
|
|
37
37
|
"The store (Tower, over /api/memory/*):",
|
|
38
|
-
" cockpit memory search \"<question>\" [--container <tag>] [--limit 1..20] [--no-recent] [--json]",
|
|
38
|
+
" cockpit memory search \"<question>\" [--container <tag>] [--limit 1..20] [--no-recent] [--include-originals|--no-originals] [--include-source|--no-source] [--json]",
|
|
39
39
|
" cockpit memory save [--container <tag>] [--custom-id <id>] [--extract] [--file <path>] [--json]",
|
|
40
40
|
" the memory comes from stdin, or --file; never from the command line",
|
|
41
41
|
" cockpit memory update <id> [--file <path>] [--json] replacement text on stdin",
|
|
@@ -45,6 +45,8 @@ export function runMemoryVerbList(io, json) {
|
|
|
45
45
|
" cockpit memory private <id> [--json] only you and super admins can read this memory",
|
|
46
46
|
" cockpit memory public <id> [--json] every member can read it again (how every memory starts)",
|
|
47
47
|
" cockpit memory map [--json]",
|
|
48
|
+
" cockpit memory repeats [--project <name-or-slug>] [--min 2..50] [--limit 1..200] [--json]",
|
|
49
|
+
" subjects a person corrected an agent about, or ordered with emphasis, more than once",
|
|
48
50
|
" cockpit memory rules [--person <name>] [--project <name-or-slug>] [--limit <n>] [--per-project <n>] [--machine] [--json]",
|
|
49
51
|
" cockpit memory knowledge [--person <name>] [--project <name-or-slug>] [--limit <n>] [--per-project <n>] [--machine] [--json]",
|
|
50
52
|
" plain sentences per project; --per-project 1..100 (default 15) is each project's own",
|
|
@@ -55,6 +57,9 @@ export function runMemoryVerbList(io, json) {
|
|
|
55
57
|
" cockpit memory consolidation [--container <tag>] [--json]",
|
|
56
58
|
" what the nightly tidy-up pass did: clusters, edits by verb, refusals, cost",
|
|
57
59
|
" cockpit memory chain <id> [--json]",
|
|
60
|
+
" cockpit memory exam [--night <YYYY-MM-DD>] [--misses] [--json]",
|
|
61
|
+
" the nightly memory exam: purity, coverage, trend and worst project; yours alone",
|
|
62
|
+
" cockpit memory source <id> [--query \"<words>\"] [--chars 200..6000] [--json]",
|
|
58
63
|
" the memories the nightly pass folded into this one: who, when, what they said",
|
|
59
64
|
" cockpit memory requeue --since <iso> [--until <iso>] [--source hook|harvest] [--reason <label>] [--apply] [--json]",
|
|
60
65
|
" put a window of finished extractions back in the queue after the store refused",
|
|
@@ -107,6 +107,21 @@ export async function listShelves(command, door) {
|
|
|
107
107
|
}
|
|
108
108
|
return 0;
|
|
109
109
|
}
|
|
110
|
+
/** One line per claim with its citations: `- text [s12 Ada 00:03:04]`. */
|
|
111
|
+
export function renderCitedSummary(summary) {
|
|
112
|
+
const lines = [`Summary v${summary.version} (${summary.supported} cited claims, ${summary.dropped} dropped as unsupported or uncited)`];
|
|
113
|
+
for (const [kind, heading] of [["decision", "Decisions"], ["action_item", "Action items"], ["key_point", "Key points"]]) {
|
|
114
|
+
const items = summary.items.filter((item) => item.kind === kind);
|
|
115
|
+
if (items.length === 0)
|
|
116
|
+
continue;
|
|
117
|
+
lines.push("", `${heading}:`);
|
|
118
|
+
for (const item of items) {
|
|
119
|
+
const cites = item.citations.map((c) => `${c.segment_id} ${c.speaker}${c.at ? ` ${c.at}` : ""}`).join("; ");
|
|
120
|
+
lines.push(`- ${item.owner ? `${item.owner}: ` : ""}${item.text} [${cites}]`);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
return lines;
|
|
124
|
+
}
|
|
110
125
|
/** `?offset=&limit=` for `notes show`: characters, like `docs read` (BLI-18006). */
|
|
111
126
|
function showQuery(command) {
|
|
112
127
|
const query = new URLSearchParams();
|
|
@@ -149,6 +164,11 @@ export async function showNote(command, door) {
|
|
|
149
164
|
const { offset, returnedChars, totalChars, nextOffset } = body.slice;
|
|
150
165
|
writeLine(door.io.stdout, `Characters ${offset}-${offset + returnedChars} of ${totalChars}${nextOffset === null ? " (the end)" : `; continue with --offset ${nextOffset}`}.`);
|
|
151
166
|
}
|
|
167
|
+
if (body.summary && body.summary.items.length > 0) {
|
|
168
|
+
writeLine(door.io.stdout, "");
|
|
169
|
+
for (const line of renderCitedSummary(body.summary))
|
|
170
|
+
writeLine(door.io.stdout, line);
|
|
171
|
+
}
|
|
152
172
|
writeLine(door.io.stdout, "");
|
|
153
173
|
writeLine(door.io.stdout, note.content.replace(/^In the room:/gm, "Invited:"));
|
|
154
174
|
return 0;
|
|
@@ -205,6 +205,30 @@ export async function shareNote(command, door) {
|
|
|
205
205
|
sayUpload(door, body);
|
|
206
206
|
return 0;
|
|
207
207
|
}
|
|
208
|
+
/** A summary run is two model calls over a long transcript: give it room (BLI-20882). */
|
|
209
|
+
const SUMMARIZE_DEADLINE_MS = 330_000;
|
|
210
|
+
/**
|
|
211
|
+
* `cockpit notes summarize <note-id>` (BLI-20882): write a new summary where every decision,
|
|
212
|
+
* action item and key point cites the speaker, timestamp and segment id it came from. A claim
|
|
213
|
+
* the transcript does not support is dropped. Earlier summaries are kept as older versions.
|
|
214
|
+
*/
|
|
215
|
+
export async function summarizeNote(command, door) {
|
|
216
|
+
const answer = await ask(door, {
|
|
217
|
+
path: `/api/notes/library/${encodeURIComponent(command.noteId ?? "")}/summary`,
|
|
218
|
+
method: "POST",
|
|
219
|
+
label: "notes summarize",
|
|
220
|
+
timeoutMs: SUMMARIZE_DEADLINE_MS,
|
|
221
|
+
body: {},
|
|
222
|
+
});
|
|
223
|
+
if (!answer.ok)
|
|
224
|
+
return fail(door, answer.reason, answer.detail);
|
|
225
|
+
const body = answer.body;
|
|
226
|
+
writeLine(door.io.stderr, `${TAG} summarize answered ${JSON.stringify({ note_id: command.noteId ?? null, status: body.status ?? null })}`);
|
|
227
|
+
if (door.json)
|
|
228
|
+
return emit(door, body);
|
|
229
|
+
writeLine(door.io.stdout, body.text ?? body.message ?? "Tower answered without a summary.");
|
|
230
|
+
return 0;
|
|
231
|
+
}
|
|
208
232
|
/**
|
|
209
233
|
* `cockpit notes place <note-id> [--apply]` — BLI-4058.
|
|
210
234
|
*
|
package/dist/commands/notes.js
CHANGED
|
@@ -43,7 +43,7 @@
|
|
|
43
43
|
*/
|
|
44
44
|
import { loadPairedSession } from "../tower-client.js";
|
|
45
45
|
import { listNotes, listShelves, showNote, showShelf } from "./notes-reads.js";
|
|
46
|
-
import { uploadNotes, pasteNote, shareNote, moveNote, placeNote } from "./notes-writes.js";
|
|
46
|
+
import { uploadNotes, pasteNote, shareNote, summarizeNote, moveNote, placeNote } from "./notes-writes.js";
|
|
47
47
|
import { runFolderCommand } from "./notes-folders.js";
|
|
48
48
|
import { armNotesWebhook, connectNotetaker, detachNotetaker, listNotesAccounts, showNotesWebhook, syncNotetaker, syncStatusNotetakers, } from "./notes-accounts.js";
|
|
49
49
|
import { showBotUpcoming } from "./notes-bot-upcoming.js";
|
|
@@ -51,7 +51,7 @@ import { showGranolaStatus } from "./notes-granola-status.js";
|
|
|
51
51
|
import { showTranscriptionStats } from "./notes-transcription-stats.js";
|
|
52
52
|
import { listRecordings, showRecording } from "./notes-recordings.js";
|
|
53
53
|
export { listNotes, listShelves, showNote, showShelf } from "./notes-reads.js";
|
|
54
|
-
export { uploadNotes, pasteNote, shareNote, moveNote, placeNote } from "./notes-writes.js";
|
|
54
|
+
export { uploadNotes, pasteNote, shareNote, summarizeNote, moveNote, placeNote } from "./notes-writes.js";
|
|
55
55
|
export { armNotesWebhook, connectNotetaker, detachNotetaker, listNotesAccounts, showNotesWebhook, syncNotetaker, syncStatusNotetakers, } from "./notes-accounts.js";
|
|
56
56
|
export { showGranolaStatus } from "./notes-granola-status.js";
|
|
57
57
|
export { showTranscriptionStats } from "./notes-transcription-stats.js";
|
|
@@ -86,6 +86,8 @@ export async function runNotes(command, io) {
|
|
|
86
86
|
case "share":
|
|
87
87
|
case "unshare":
|
|
88
88
|
return shareNote(command, door);
|
|
89
|
+
case "summarize":
|
|
90
|
+
return summarizeNote(command, door);
|
|
89
91
|
case "move":
|
|
90
92
|
return moveNote(command, door);
|
|
91
93
|
case "place":
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { askAgentDoor, emitAgentDoor, failAgentDoor, openAgentDoor } from "./agent-door.js";
|
|
2
|
+
import { writeLine } from "./cli-io.js";
|
|
3
|
+
const TAG = "[people cli]";
|
|
4
|
+
const READ_DEADLINE_MS = 30_000;
|
|
5
|
+
export async function runPeople(command, io) {
|
|
6
|
+
let door;
|
|
7
|
+
try {
|
|
8
|
+
door = await openAgentDoor("people", 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
|
+
const path = command.person ? `/api/people?${new URLSearchParams({ person: command.person }).toString()}` : "/api/people";
|
|
20
|
+
const answer = await askAgentDoor(door, { path, method: "GET", label: "people show", timeoutMs: READ_DEADLINE_MS });
|
|
21
|
+
if (!answer.ok)
|
|
22
|
+
return failAgentDoor(door, TAG, answer.reason, answer.detail);
|
|
23
|
+
const body = answer.body;
|
|
24
|
+
writeLine(io.stderr, `${TAG} answered ${JSON.stringify({ view: command.person ? "person" : "list", people: body.people?.length ?? 1 })}`);
|
|
25
|
+
if (command.json)
|
|
26
|
+
return emitAgentDoor(door, body);
|
|
27
|
+
writeLine(io.stdout, body.text);
|
|
28
|
+
return 0;
|
|
29
|
+
}
|
|
@@ -21,7 +21,7 @@ export async function runCockpitCli(argv, io) {
|
|
|
21
21
|
}
|
|
22
22
|
|
|
23
23
|
if (command === "--version" || command === "-V" || command === "version") {
|
|
24
|
-
writeLine(io?.stdout ?? process.stdout, "0.2.
|
|
24
|
+
writeLine(io?.stdout ?? process.stdout, "0.2.236");
|
|
25
25
|
return 0;
|
|
26
26
|
}
|
|
27
27
|
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `cockpit spend memory [--days 1|7|30] [--json]`: what each memory operation cost, held against the real OpenAI
|
|
3
|
+
* bill (BLI-19254).
|
|
4
|
+
*
|
|
5
|
+
* One read of `GET /api/ops/memory-spend`, the same door the `spend_memory` MCP tool and JARVIS call. The
|
|
6
|
+
* terminal prints the operations ranked by list dollars, top first, with real dollars, share of the ledger, the
|
|
7
|
+
* trend against the previous window, and then how much of OpenAI's own bill the ledger explains and what it does
|
|
8
|
+
* not (`untagged`, never hidden). `--json` prints the door's body untouched.
|
|
9
|
+
*
|
|
10
|
+
* A super_admin reads it; anyone else gets the door's own refusal reason, printed verbatim by `failAgentDoor`.
|
|
11
|
+
*/
|
|
12
|
+
import { askAgentDoor, emitAgentDoor, failAgentDoor, openAgentDoor } from "./agent-door.js";
|
|
13
|
+
import { writeLine } from "./cli-io.js";
|
|
14
|
+
import { optionalNonEmpty, optionalUrl, parseNamedArgs } from "./local-arg-values.js";
|
|
15
|
+
const USAGE = "Usage: cockpit spend memory [--days 1|7|30] [--dashboard-url <url>] [--json]";
|
|
16
|
+
export function parseSpendArgs(args) {
|
|
17
|
+
const values = parseNamedArgs(args, { allowedFlags: ["--days", "--json", "--home", "--dashboard-url"], valueFlags: ["--days", "--home", "--dashboard-url"] });
|
|
18
|
+
const [action, ...rest] = values.positionals;
|
|
19
|
+
if (action !== "memory" || rest.length > 0)
|
|
20
|
+
throw new Error(USAGE);
|
|
21
|
+
const raw = values.flags.get("--days");
|
|
22
|
+
const days = raw === undefined ? 7 : Number(raw);
|
|
23
|
+
if (days !== 1 && days !== 7 && days !== 30)
|
|
24
|
+
throw new Error(`--days must be 1, 7 or 30. ${USAGE}`);
|
|
25
|
+
return { kind: "spend", action: "memory", days, json: values.booleans.has("--json"), homeDir: optionalNonEmpty(values.flags.get("--home")), dashboardUrl: optionalUrl(values.flags.get("--dashboard-url")) };
|
|
26
|
+
}
|
|
27
|
+
const usd = (value) => (value === null || value === undefined ? "n/a" : value.toFixed(value >= 100 ? 0 : 2));
|
|
28
|
+
const money = (value) => (value === null || value === undefined ? "n/a" : `$${usd(value)}`);
|
|
29
|
+
const trend = (value) => (value === null ? "new" : `${value >= 0 ? "+" : ""}${value}%`);
|
|
30
|
+
const millions = (tokens) => `${(tokens / 1e6).toFixed(1)}M`;
|
|
31
|
+
export function renderMemorySpend(body) {
|
|
32
|
+
const lines = [];
|
|
33
|
+
const { window } = body;
|
|
34
|
+
lines.push(`Tower memory spend, last ${window.days} day${window.days === 1 ? "" : "s"} (${window.from} to ${window.to} UTC), as of ${body.asOf.slice(11, 16)}Z. Today is partial; trend is against the ${window.days} day${window.days === 1 ? "" : "s"} before (${window.previousFrom} to ${window.previousTo}).`);
|
|
35
|
+
if (body.operations.length === 0) {
|
|
36
|
+
lines.push("The ledger has no spend in this window yet. Live tallying starts at deploy; `backfill` rebuilds history from the older receipt tables.");
|
|
37
|
+
}
|
|
38
|
+
else {
|
|
39
|
+
const header = ["operation", "list $", "real $", "share", "vs prev", "calls", "biggest callers"];
|
|
40
|
+
const rows = body.operations.map((op) => [
|
|
41
|
+
op.operation, usd(op.listUsd), usd(op.realUsd), `${Math.round(op.share * 100)}%`, trend(op.trendPct), String(op.calls),
|
|
42
|
+
op.topCallers.map((caller) => `${caller.caller} $${usd(caller.listUsd)}`).join(", "),
|
|
43
|
+
]);
|
|
44
|
+
rows.push(["total", usd(body.totals.listUsd), usd(body.totals.realUsd), "", trend(body.totals.trendPct), String(body.totals.calls), ""]);
|
|
45
|
+
const widths = header.map((cell, column) => Math.max(cell.length, ...rows.map((row) => row[column].length)));
|
|
46
|
+
const render = (cells) => cells.map((cell, column) => (column === 0 || column === 6 ? cell.padEnd(widths[column]) : cell.padStart(widths[column]))).join(" ").trimEnd();
|
|
47
|
+
lines.push("", render(header), ...rows.map(render));
|
|
48
|
+
}
|
|
49
|
+
const bill = body.reconcile;
|
|
50
|
+
lines.push("");
|
|
51
|
+
if (!bill.hasBill) {
|
|
52
|
+
lines.push(`OpenAI bill: not reconciled. ${bill.reason ?? "no reason recorded"}.`);
|
|
53
|
+
}
|
|
54
|
+
else {
|
|
55
|
+
lines.push(`OpenAI bill for Tower's keys over ${bill.billDays} complete day${bill.billDays === 1 ? "" : "s"} (read ${bill.billAsOf?.slice(0, 16).replace("T", " ") ?? "?"}Z): list ${money(bill.towerBillListUsd)}, real ${money(bill.towerBillRealUsd)}.`);
|
|
56
|
+
lines.push(`The ledger explains ${bill.coveragePct === null ? "n/a" : `${bill.coveragePct}%`} of it (list ${money(bill.ledgerOpenAiListUsd)}). Untagged: list ${money(bill.untaggedListUsd)}, real ${money(bill.untaggedRealUsd)}, which no tagged call accounts for.`);
|
|
57
|
+
if (bill.tokenGaps.length > 0) {
|
|
58
|
+
lines.push("Untagged input tokens by model and tier (bill minus ledger): " + bill.tokenGaps.slice(0, 5).map((gap) => `${gap.model} ${gap.tier} ${millions(gap.untaggedInputTokens)}`).join("; ") + ".");
|
|
59
|
+
}
|
|
60
|
+
if ((bill.otherAppsBillListUsd ?? 0) > 0)
|
|
61
|
+
lines.push(`Other apps on the same OpenAI org (not Tower's, not counted above): list ${money(bill.otherAppsBillListUsd)}.`);
|
|
62
|
+
}
|
|
63
|
+
if (body.reads.failures.length > 0)
|
|
64
|
+
lines.push("", `Could not read: ${body.reads.failures.join(", ")}. The rest is shown as read.`);
|
|
65
|
+
for (const note of body.notes)
|
|
66
|
+
lines.push(note);
|
|
67
|
+
return lines;
|
|
68
|
+
}
|
|
69
|
+
export async function runSpend(command, io) {
|
|
70
|
+
const door = await openAgentDoor("spend", command, io);
|
|
71
|
+
const answer = await askAgentDoor(door, { path: `/api/ops/memory-spend?days=${command.days}`, method: "GET", label: "spend memory", timeoutMs: 60_000 });
|
|
72
|
+
if (!answer.ok)
|
|
73
|
+
return failAgentDoor(door, "[spend cli]", answer.reason, answer.detail);
|
|
74
|
+
if (door.json)
|
|
75
|
+
return emitAgentDoor(door, answer.body);
|
|
76
|
+
for (const line of renderMemorySpend(answer.body))
|
|
77
|
+
writeLine(door.io.stdout, line);
|
|
78
|
+
return 0;
|
|
79
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The tick's half of the company plugin install (BLI-18200). Doctor's
|
|
3
|
+
* `agent-plugins` row converges a machine when a person runs `cockpit doctor`;
|
|
4
|
+
* this puts the same plugin on a machine nobody is looking at, every 15 minutes
|
|
5
|
+
* and cheaply: a steady machine costs four file reads, a failed host waits six
|
|
6
|
+
* hours, and an installed plugin is refreshed once a day.
|
|
7
|
+
*
|
|
8
|
+
* It reports through the same install-event channel as the agent-rules heal
|
|
9
|
+
* (`agent_plugins_install`), once per real attempt, with reason labels only.
|
|
10
|
+
*/
|
|
11
|
+
import os from "node:os";
|
|
12
|
+
import { convergeAgentPlugins } from "../agent-plugins-converge.js";
|
|
13
|
+
export async function healAgentPlugins(command, io, paths, now) {
|
|
14
|
+
const exec = io.exec;
|
|
15
|
+
if (!exec) {
|
|
16
|
+
console.error("[sync-heal] agent plugins not checked; this io has no process runner", JSON.stringify({ reason: "runner_unavailable" }));
|
|
17
|
+
return null;
|
|
18
|
+
}
|
|
19
|
+
const outcomes = await convergeAgentPlugins({
|
|
20
|
+
homeDir: command.homeDir ?? os.homedir(),
|
|
21
|
+
stateDir: paths.state_dir,
|
|
22
|
+
exec,
|
|
23
|
+
unattended: true,
|
|
24
|
+
refresh: true,
|
|
25
|
+
now,
|
|
26
|
+
});
|
|
27
|
+
return eventFor(outcomes);
|
|
28
|
+
}
|
|
29
|
+
/** One event per real attempt; steady state, throttled retries and absent hosts report nothing. */
|
|
30
|
+
export function eventFor(outcomes) {
|
|
31
|
+
const acted = outcomes.filter((outcome) => !outcome.quiet);
|
|
32
|
+
if (acted.length === 0)
|
|
33
|
+
return null;
|
|
34
|
+
const detail = acted.map((outcome) => `${outcome.host}=${outcome.reason}`).join("; ");
|
|
35
|
+
const failed = acted.find((outcome) => outcome.status === "failed");
|
|
36
|
+
if (failed)
|
|
37
|
+
return { step: "agent_plugins_install", status: "fail", error_code: failed.reason, error_detail: detail };
|
|
38
|
+
if (acted.every((outcome) => outcome.status === "skipped"))
|
|
39
|
+
return { step: "agent_plugins_install", status: "skipped", error_code: acted[0].reason, error_detail: detail };
|
|
40
|
+
const refreshOnly = acted.every((outcome) => outcome.status === "refreshed");
|
|
41
|
+
// A failed daily refresh keeps the plugin; it is still worth one line, as a skipped step.
|
|
42
|
+
const refreshFailed = acted.find((outcome) => outcome.reason.startsWith("refresh_failed_"));
|
|
43
|
+
if (refreshFailed)
|
|
44
|
+
return { step: "agent_plugins_install", status: "skipped", error_code: refreshFailed.reason, error_detail: detail };
|
|
45
|
+
return { step: refreshOnly ? "agent_plugins_refresh" : "agent_plugins_install", status: "ok", error_detail: detail };
|
|
46
|
+
}
|