@bli-cockpit/cli 0.2.86 → 0.2.88

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.
@@ -170,9 +170,25 @@ export function parseAgentRulesArgs(args) {
170
170
  */
171
171
  export function parseMemoryArgs(args) {
172
172
  const values = parseNamedArgs(args, {
173
- allowedFlags: ["--home", "--dashboard-url", "--dry-run", "--json"],
174
- valueFlags: ["--home", "--dashboard-url"],
173
+ allowedFlags: ["--home", "--dashboard-url", "--dry-run", "--json", "--store", "--reason-stdin"],
174
+ valueFlags: ["--home", "--dashboard-url", "--store"],
175
175
  });
176
+ if (values.positionals[0] === "log") {
177
+ const [, verdict, reason = ""] = values.positionals;
178
+ const store = values.flags.get("--store");
179
+ const reasonStdin = values.booleans.has("--reason-stdin");
180
+ if (values.booleans.has("--dry-run") || values.positionals.length > 3 || (reasonStdin && reason))
181
+ throw new Error("memory log takes one reason or --reason-stdin, never --dry-run");
182
+ if (verdict !== "win" && verdict !== "loss" && verdict !== "noise")
183
+ throw new Error("memory log needs win|loss|noise");
184
+ if (store !== "bli" && store !== "supermemory" && store !== "both")
185
+ throw new Error("memory log needs --store bli|supermemory|both");
186
+ if (!reasonStdin && !reason.trim())
187
+ throw new Error("memory log needs a reason");
188
+ return { kind: "memory-log", verdict, store, reason, reasonStdin, homeDir: optionalNonEmpty(values.flags.get("--home")), dashboardUrl: optionalUrl(values.flags.get("--dashboard-url")), json: values.booleans.has("--json") };
189
+ }
190
+ if (values.flags.has("--store") || values.booleans.has("--reason-stdin"))
191
+ throw new Error("--store and --reason-stdin require memory log");
176
192
  if (values.positionals.length > 1) {
177
193
  throw new Error("memory accepts at most one action (install|status).");
178
194
  }
@@ -526,7 +526,7 @@ export function localSubcommandHelp(command) {
526
526
  [
527
527
  "memory",
528
528
  [
529
- "Usage: cockpit memory [install|status] [--dashboard-url <url>] [--dry-run] [--json]",
529
+ "Usage: cockpit memory [install|status|log <win|loss|noise> --store <bli|supermemory|both> \"<reason>\"] [--dashboard-url <url>] [--dry-run] [--json]",
530
530
  "",
531
531
  "Registers BLI Memory on this machine: the `bli-memory` MCP server plus the",
532
532
  "recall/save hooks, for Claude Code (~/.claude.json, ~/.claude/settings.json)",
@@ -535,6 +535,11 @@ export function localSubcommandHelp(command) {
535
535
  "entries, and reads the stored config back before reporting success.",
536
536
  "`do-everything` runs it, and the sync tick re-runs it at most once a day.",
537
537
  "Action defaults to `install`. See docs/runbooks/bli-memory-install.md.",
538
+ "log appends an opinion to ~/.codex/AGENT-EXPERIENCE.md and posts it to Tower.",
539
+ "Use --reason-stdin instead of a quoted reason; --json reports shipped status.",
540
+ "Reasons: one line, max 500 characters; no prompts, memory bodies or secrets.",
541
+ "Markers: [bli] = BLI Memory, ◪ = Supermemory, [bli]+◪ = both.",
542
+ "Offline entries stay queued; the next collector sync retries with the same ID.",
538
543
  ],
539
544
  ],
540
545
  [
@@ -86,7 +86,7 @@ export function localCommandHelp(command) {
86
86
  " cockpit serve [--port <port>] [--workspace <path>]",
87
87
  " cockpit autostart [install|uninstall|status] [--workspace <path>] [--dashboard-url <url>] [--interval-seconds <n>] [--json]",
88
88
  " cockpit agent-rules [install|uninstall|status] [--host codex|claude|all] [--workspace <path>] [--json]",
89
- " cockpit memory [install|status] [--dashboard-url <url>] [--dry-run] [--json]",
89
+ " cockpit memory [install|status|log <win|loss|noise> --store <bli|supermemory|both> \"<reason>\"] [--dashboard-url <url>] [--dry-run] [--json]",
90
90
  " cockpit clean [--dry-run] [--all-committed] [--reconcile] [--dashboard-url <url>] [--json]",
91
91
  " cockpit docs [list|tree|read <id|slug>|create --title <t>|update <id>] [--parent <id>|root|--clear-parent] [--query <text>] [--limit <n>] [--visibility org|private] [--file <path>|--body-stdin] [--allow-empty] [--dashboard-url <url>] [--json]",
92
92
  " cockpit msg [channels|create <name>|dm <email>|read <channel>|send <channel>|thread <id> --channel <channel>] [--private] [--members a@x,b@y] [--description <text>] [--thread <id>] [--limit <n>] [--dashboard-url <url>] [--json]",
@@ -30,6 +30,7 @@ import { runNotes } from "./notes.js";
30
30
  import { runServe } from "./serve.js";
31
31
  import { runAutostart } from "./autostart-command.js";
32
32
  import { runAgentRules } from "./agent-rules-command.js";
33
+ import { runMemoryLog } from "./memory-log.js";
33
34
  import { runMemoryInstall } from "./memory-install.js";
34
35
  import { runClean } from "./clean.js";
35
36
  import { runDocs } from "./docs.js";
@@ -141,6 +142,8 @@ export async function runLocalCockpitCli(argv, io = defaultIo()) {
141
142
  return await runAutostart(command, io);
142
143
  case "agent-rules":
143
144
  return await runAgentRules(command, io);
145
+ case "memory-log":
146
+ return await runMemoryLog(command, io);
144
147
  case "memory":
145
148
  return await runMemoryInstall(command, io);
146
149
  case "clean":
@@ -54,6 +54,7 @@ export function readMemoryHookCounts(options) {
54
54
  }
55
55
  : {}),
56
56
  hook_skipped_trivial_24h: window.skippedTrivial,
57
+ hook_billing_exhausted_24h: window.billingExhausted,
57
58
  },
58
59
  reason: "ok",
59
60
  };
@@ -0,0 +1,28 @@
1
+ import path from "node:path";
2
+ import { appendExperience, drainExperiences, shipExperience, validateExperience } from "@bli-cockpit/telemetry-core/memory-experience";
3
+ import { readPipedText, writeLine } from "./cli-io.js";
4
+ import { callTower, openTower } from "./tower-command.js";
5
+ function sender(command, io) {
6
+ return async (entry) => {
7
+ const tower = await openTower("memory log", command, io);
8
+ const result = await callTower(tower, { path: "/api/memory/experience", method: "POST", body: entry, label: "memory experience", timeoutMs: 5000 });
9
+ return result.ok ? { ok: result.body?.ok === true, reason: "experience_not_acknowledged" } : result;
10
+ };
11
+ }
12
+ export async function runMemoryLog(command, io) {
13
+ const reason = command.reasonStdin ? (await readPipedText(io.stdin, { maxChars: 1002 })).trim() : command.reason;
14
+ validateExperience(command.store, command.verdict, reason);
15
+ const entry = await appendExperience({ store: command.store, verdict: command.verdict, reason }, {
16
+ homeDir: command.homeDir, agent: "cockpit", project: path.basename(process.cwd()),
17
+ });
18
+ const delivery = await shipExperience(entry, sender(command, io), command.homeDir);
19
+ const receipt = { ok: true, id: entry.id, local: true, ...delivery };
20
+ writeLine(io.stderr, `[memory experience] recorded ${JSON.stringify(receipt)}`);
21
+ writeLine(io.stdout, command.json ? JSON.stringify(receipt) : `Experience appended; shipped: ${delivery.shipped} (${delivery.reason}).`);
22
+ return 0;
23
+ }
24
+ export async function runMemoryExperienceAfterSync(command, io) {
25
+ const result = await drainExperiences(sender(command, io), command.homeDir);
26
+ if (result.reason !== "empty")
27
+ writeLine(io.stderr, `[memory experience] sync ${JSON.stringify(result)}`);
28
+ }
@@ -96,11 +96,14 @@ export function renderMemoryHooks(hooks, dim) {
96
96
  const rightKnown = typeof right.runs === "number" ? 0 : 1;
97
97
  if (leftKnown !== rightKnown)
98
98
  return leftKnown - rightKnown;
99
- return (right.timeouts ?? 0) - (left.timeouts ?? 0);
99
+ // BLI-3891: a billing refusal ranks beside a missed deadline. Both are a
100
+ // recall the person did not get; only the fix differs.
101
+ return ((right.timeouts ?? 0) + (right.billingExhausted ?? 0)
102
+ - ((left.timeouts ?? 0) + (left.billingExhausted ?? 0)));
100
103
  });
101
104
  for (const device of ranked) {
102
105
  const line = ` ${device.line ?? `${device.displayName ?? "?"}: (no line)`}`;
103
- lines.push((device.timeouts ?? 0) > 0 ? line : dim(line));
106
+ lines.push((device.timeouts ?? 0) > 0 || (device.billingExhausted ?? 0) > 0 ? line : dim(line));
104
107
  if (device.performanceLine)
105
108
  lines.push(` ${device.performanceLine}`);
106
109
  // BLI-3884. Written on the server (`lib/ops/memory-hook-misses.ts`) and
@@ -0,0 +1,36 @@
1
+ /**
2
+ * `cockpit ops` — WHAT TODAY COST, by model (BLI-3909).
3
+ *
4
+ * Split out of `ops-render.ts` the way `ops-render-memory.ts` and
5
+ * `ops-render-coverage.ts` were, when that file crossed the repo's 500-line
6
+ * readability floor.
7
+ *
8
+ * Same rule as the rest of that family: every sentence here was written on the
9
+ * SERVER (`lib/ops/model-spend-today.ts`) and is printed VERBATIM. This module
10
+ * decides order, indentation and column widths and nothing else — a terminal
11
+ * that composed its own total from the same fields would eventually disagree
12
+ * with the page about what a $25 day means, and the one that is wrong is
13
+ * always the one somebody is reading.
14
+ *
15
+ * A CLI too old to know this section simply does not print it. Nothing is lost
16
+ * by that: the section rides the ordinary `/api/ops/status` response, so
17
+ * `cockpit ops --json` carries it on every version.
18
+ */
19
+ /** `$18.65`, `$0.0031`, `—` when the server sent no number at all. */
20
+ function usdColumn(usd) {
21
+ if (typeof usd !== "number")
22
+ return "—";
23
+ return `$${usd.toFixed(usd < 0.01 ? 4 : 2)}`;
24
+ }
25
+ /**
26
+ * The SPEND block: the server's one line, then one row per model. `$314 of a
27
+ * $357 month on one model` is the thing this exists to make visible before the
28
+ * invoice does.
29
+ */
30
+ export function renderSpend(spend, dim) {
31
+ const lines = ["", `SPEND ${spend.summary ?? "(no summary)"}`];
32
+ for (const row of spend.models ?? []) {
33
+ lines.push(dim(` ${usdColumn(row.usd).padStart(9)} ${row.model ?? "?"} · ${row.calls ?? 0} call(s) · ${(row.bases ?? []).join("+") || "?"} · ${(row.sources ?? []).join(",") || "?"}`));
34
+ }
35
+ return lines;
36
+ }
@@ -13,6 +13,10 @@
13
13
  * somebody is about to conclude something from it.
14
14
  */
15
15
  import { renderCoverageBuckets, } from "./ops-render-coverage.js";
16
+ // BLI-3909: today's spend by model, its own renderer for the same reason the
17
+ // coverage table has one — this file is at the repo's readability floor.
18
+ import { renderSpend } from "./ops-render-spend.js";
19
+ export { renderSpend } from "./ops-render-spend.js";
16
20
  export { renderMemoryHooks, renderMemoryUsage } from "./ops-render-memory.js";
17
21
  export { renderCoverageBuckets };
18
22
  /** The word a person reads. Short, fixed width, and never a bare colour. */
@@ -124,6 +128,8 @@ options = {}) {
124
128
  lines.push(...renderCoverageBuckets(payload.coverage.buckets, dim));
125
129
  }
126
130
  }
131
+ if (payload.spend)
132
+ lines.push(...renderSpend(payload.spend, dim));
127
133
  const slack = payload.skips?.slack;
128
134
  const external = payload.skips?.external;
129
135
  if (slack || external) {
@@ -88,7 +88,7 @@ async function runOpsStatus(command, io, tower) {
88
88
  // BLI-3762: a job that ran and wrote less than it owed is not healthy.
89
89
  row.verdict === "degraded");
90
90
  if (command.json) {
91
- writeLine(io.stdout, JSON.stringify(memory ? { ...payload, memory: memory.section, hooks: memory.hooks } : payload));
91
+ writeLine(io.stdout, JSON.stringify(memory ? { ...payload, memory: memory.section, hooks: memory.hooks, experience: memory.experience } : payload));
92
92
  }
93
93
  else {
94
94
  const styled = colorEnabled(io);
@@ -111,6 +111,11 @@ async function runOpsStatus(command, io, tower) {
111
111
  writeLine(io.stdout, line);
112
112
  }
113
113
  }
114
+ if (memory?.experience) {
115
+ writeLine(io.stdout, `\nEXPERIENCE ${memory.experience.summary}`);
116
+ for (const line of memory.experience.lines)
117
+ writeLine(io.stdout, ` ${line}`);
118
+ }
114
119
  if (memory && !memory.section) {
115
120
  // Never a silent gap where a section was asked for: the reason the gauge
116
121
  // could not be read is printed where the gauge would have been.
@@ -147,6 +152,9 @@ async function runOpsStatus(command, io, tower) {
147
152
  // BLI-3881: runs the hook declined to search for. A SUBSET of runs, so
148
153
  // `runs - skipped_trivial` is how many turns actually asked the door.
149
154
  memory_hook_skipped_trivial_24h: memory?.hooks?.totals?.skippedTrivial ?? null,
155
+ // BLI-3891: null means the fleet reported none of these at all, which is
156
+ // "not measured" on a machine older than the count — never "no refusals".
157
+ memory_hook_billing_exhausted_24h: memory?.hooks?.totals?.billingExhausted ?? null,
150
158
  memory_hook_devices_reporting: memory?.hooks?.reporting ?? null,
151
159
  // BLI-3884: which route answered them, fleet-wide. Null while no machine
152
160
  // names one — a rollout fact, not a count of zero.
@@ -177,15 +185,15 @@ async function readMemoryUsage(command, io, tower) {
177
185
  reason: result.reason,
178
186
  http_status: result.httpStatus ?? null,
179
187
  })}`);
180
- return { section: null, hooks: null, reason: result.reason };
188
+ return { section: null, hooks: null, experience: null, reason: result.reason };
181
189
  }
182
190
  const body = asRecord(result.body);
183
191
  // BLI-3788: the hook counts ride the same answer, and their absence is a
184
192
  // fact about the SERVER's version rather than about this fleet — an older
185
193
  // dashboard sends no `hooks` key, and printing nothing is right there.
186
194
  if (!body.memory)
187
- return { section: null, hooks: body.hooks ?? null, reason: "memory_section_absent" };
188
- return { section: body.memory, hooks: body.hooks ?? null, reason: "ok" };
195
+ return { section: null, hooks: body.hooks ?? null, experience: body.experience ?? null, reason: "memory_section_absent" };
196
+ return { section: body.memory, hooks: body.hooks ?? null, experience: body.experience ?? null, reason: "ok" };
189
197
  }
190
198
  async function runOpsRecompile(command, io, tower) {
191
199
  const person = command.person ?? "";
@@ -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.86");
18
+ writeLine(io?.stdout ?? process.stdout, "0.2.88");
19
19
  return 0;
20
20
  }
21
21
 
@@ -1,3 +1,4 @@
1
+ import { runMemoryExperienceAfterSync } from "./memory-log.js";
1
2
  import { writeLine } from "./cli-io.js";
2
3
  import { attributedSyncRunStatus, cursorStatusLine, displayTicketId, rawEvidenceSyncLine, shortSha, worktreeSyncRow, writeAgentSessionSummary, } from "./collection-report.js";
3
4
  import { collectionRootConsentAliases } from "./collection-roots.js";
@@ -52,6 +53,7 @@ export async function runSync(command, io) {
52
53
  // BLI-3580: BLI Memory's registration converges the same way — after
53
54
  // collection, at most once a day, its own receipt either way.
54
55
  await runMemoryInstallAfterSync(command, io, dashboardUrl);
56
+ await runMemoryExperienceAfterSync({ ...command, dashboardUrl }, io);
55
57
  // BLI-3619: and the disk stops growing without bound — same daily cadence,
56
58
  // same rule that a follow-up never blocks or fails collection.
57
59
  await runStagingPruneAfterSync(command, io, dashboardUrl);
@@ -86,6 +88,7 @@ export async function runSync(command, io) {
86
88
  // BLI-3580: BLI Memory's registration converges the same way — after
87
89
  // collection, at most once a day, its own receipt either way.
88
90
  await runMemoryInstallAfterSync(command, io, dashboardUrl);
91
+ await runMemoryExperienceAfterSync({ ...command, dashboardUrl }, io);
89
92
  throw error;
90
93
  }
91
94
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bli-cockpit/cli",
3
- "version": "0.2.86",
3
+ "version": "0.2.88",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "bin": {
@@ -27,8 +27,8 @@
27
27
  "test": "node dist/cli.js --help && node ../../scripts/assert-public-cli-routing.mjs && node ../../scripts/assert-public-cli-verb-help.mjs && node ../../scripts/assert-public-cli-runtime-files.mjs && node ../../scripts/assert-public-cli-no-fleet-posts.mjs && node ../../scripts/assert-public-package-pack.mjs --workspace=@bli-cockpit/cli"
28
28
  },
29
29
  "dependencies": {
30
- "@bli-cockpit/memory-mcp": "0.1.18",
31
- "@bli-cockpit/mcp": "0.1.19",
32
- "@bli-cockpit/telemetry-core": "0.1.35"
30
+ "@bli-cockpit/memory-mcp": "0.1.20",
31
+ "@bli-cockpit/mcp": "0.1.21",
32
+ "@bli-cockpit/telemetry-core": "0.1.37"
33
33
  }
34
34
  }