@bli-cockpit/mcp 0.1.55 → 0.1.58

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/README.md CHANGED
@@ -34,7 +34,7 @@ about what an agent can reach.
34
34
 
35
35
  <!-- BEGIN GENERATED verb census — `npm run mcp:readme` -->
36
36
 
37
- **104 of 109 Tower verbs have an MCP twin.**
37
+ **107 of 112 Tower verbs have an MCP twin.**
38
38
  Each tool goes through the SAME door its CLI verb calls, with the same
39
39
  collector device token — never a second route and never a service-role
40
40
  reader. `src/verb-census.test.ts` fails when a verb is in none of the
@@ -91,8 +91,11 @@ three tables below.
91
91
  | `cockpit mail search` | `mail_search` | `GET /api/mail/search` |
92
92
  | `cockpit mail send` | `mail_send` | `POST /api/mail/send` |
93
93
  | `cockpit mail sync` | `mail_sync` | `POST /api/mail/accounts/[id]/sync` |
94
+ | `cockpit memory consolidation` | `memory_consolidation` | `GET /api/memory/consolidation` |
94
95
  | `cockpit memory forget` | `forget_memory` (`bli-memory`) | `POST /api/memory/forget` |
95
96
  | `cockpit memory log` | `memory_experience` | `POST /api/memory/experience` |
97
+ | `cockpit memory profile` | `memory_profile` | `GET /api/memory/profile` |
98
+ | `cockpit memory requeue` | `memory_requeue` | `POST /api/memory/requeue` |
96
99
  | `cockpit memory save` | `save_memory` (`bli-memory`) | `POST /api/memory/save` |
97
100
  | `cockpit memory search` | `search_memory` (`bli-memory`) | `POST /api/memory/search` |
98
101
  | `cockpit memory update` | `update_memory` (`bli-memory`) | `POST /api/memory/update` |
@@ -0,0 +1,19 @@
1
+ /**
2
+ * `memory_consolidation` — the MCP twin of `cockpit memory consolidation`
3
+ * (BLI-4534).
4
+ *
5
+ * Same door, same device token, same answer: `GET /api/memory/consolidation`.
6
+ * It reads what the nightly BLI Memory tidy-up pass did — clusters looked at,
7
+ * edits by verb, refusals by reason, cost — and it cannot start, stop or
8
+ * change a pass. The pass itself is a Railway cron.
9
+ *
10
+ * It lives on `bli-tower` rather than on the `bli-memory` server that carries
11
+ * `search_memory`/`save_memory`/`update_memory`/`forget_memory`, because this
12
+ * is a read of a JOB's counts rather than a read or a write of the store.
13
+ *
14
+ * No memory text crosses this tool. The door does not send any.
15
+ */
16
+ import { type ToolDeps } from "./tool-result.js";
17
+ export declare function registerMemoryConsolidationTools(server: {
18
+ registerTool: (...args: never[]) => unknown;
19
+ }, deps: ToolDeps): void;
@@ -0,0 +1,53 @@
1
+ /**
2
+ * `memory_consolidation` — the MCP twin of `cockpit memory consolidation`
3
+ * (BLI-4534).
4
+ *
5
+ * Same door, same device token, same answer: `GET /api/memory/consolidation`.
6
+ * It reads what the nightly BLI Memory tidy-up pass did — clusters looked at,
7
+ * edits by verb, refusals by reason, cost — and it cannot start, stop or
8
+ * change a pass. The pass itself is a Railway cron.
9
+ *
10
+ * It lives on `bli-tower` rather than on the `bli-memory` server that carries
11
+ * `search_memory`/`save_memory`/`update_memory`/`forget_memory`, because this
12
+ * is a read of a JOB's counts rather than a read or a write of the store.
13
+ *
14
+ * No memory text crosses this tool. The door does not send any.
15
+ */
16
+ import { z } from "zod";
17
+ import { callAgentDoor } from "./agent-door.js";
18
+ import { doorFailureText, errorResult, queryString, registrarFor, textResult, withSession, } from "./tool-result.js";
19
+ export function registerMemoryConsolidationTools(server, deps) {
20
+ registrarFor(server)("memory_consolidation", {
21
+ title: "Read the nightly memory consolidation runs",
22
+ description: "What the nightly BLI Memory consolidation pass did: clusters, merges/rewrites/dated corrections by verb, refusals by reason, model and cost. With a container tag it answers that space's recent runs; without one, the newest run per container. Read-only — the pass never deletes a memory and this tool never starts one. An empty list means no run is visible to this account (the receipts are super_admin-readable), not an outage.",
23
+ inputSchema: {
24
+ container: z
25
+ .string()
26
+ .min(1)
27
+ .max(128)
28
+ .optional()
29
+ .describe("One container tag. Omit for the newest run per container."),
30
+ limit: z
31
+ .number()
32
+ .int()
33
+ .min(1)
34
+ .max(200)
35
+ .optional()
36
+ .describe("How many run rows to read. Default 50."),
37
+ },
38
+ }, async (args) => withSession(deps, async (session) => {
39
+ const params = new URLSearchParams();
40
+ if (args.container)
41
+ params.set("container", String(args.container));
42
+ if (args.limit !== undefined)
43
+ params.set("limit", String(args.limit));
44
+ const response = await callAgentDoor(session, deps.fetchImpl, "GET", `/api/memory/consolidation${queryString(params)}`);
45
+ if (!response.ok)
46
+ return errorResult(doorFailureText("memory_consolidation", response));
47
+ const runs = Array.isArray(response.body.runs) ? response.body.runs : [];
48
+ const reason = typeof response.body.reason === "string" ? response.body.reason : "runs_read";
49
+ return textResult(runs.length === 0
50
+ ? `No consolidation runs are visible (${reason}). The pass is a nightly cron and its receipts are readable by a super_admin; this is the access rule answering, not an outage.`
51
+ : `${runs.length} consolidation run${runs.length === 1 ? "" : "s"}. report = proposals only, nothing on the shelf changed; apply = accepted edits written as supersessions, which never delete anything.`, response.body);
52
+ }));
53
+ }
@@ -0,0 +1,23 @@
1
+ /**
2
+ * `memory_profile` (BLI-4533) — the compiled one-page briefing for a memory
3
+ * space, over the same `GET /api/memory/profile` door `cockpit memory profile`
4
+ * calls.
5
+ *
6
+ * Every session already opens with this page injected by the session-start
7
+ * hook. This tool exists for the turn AFTER that: an agent that wants to know
8
+ * what a DIFFERENT project's memory says about itself, or one that wants to
9
+ * re-read its own briefing without trusting a context window that has since
10
+ * been compacted.
11
+ *
12
+ * It lives on `bli-tower` rather than on `bli-memory` for one reason: the four
13
+ * store tools on that server are the vendor-compatible set the Claude Code
14
+ * plugin expects, and this is ours. The DOOR is the same either way.
15
+ *
16
+ * Read-only, and there is no compile argument on purpose. The page is written
17
+ * once a day by the `memory-profile` Railway service; a tool that could trigger
18
+ * a compile would put a paid model call behind a sentence an agent types.
19
+ */
20
+ import { type ToolDeps } from "./tool-result.js";
21
+ export declare function registerMemoryProfileTools(server: {
22
+ registerTool: (...args: never[]) => unknown;
23
+ }, deps: ToolDeps): void;
@@ -0,0 +1,65 @@
1
+ /**
2
+ * `memory_profile` (BLI-4533) — the compiled one-page briefing for a memory
3
+ * space, over the same `GET /api/memory/profile` door `cockpit memory profile`
4
+ * calls.
5
+ *
6
+ * Every session already opens with this page injected by the session-start
7
+ * hook. This tool exists for the turn AFTER that: an agent that wants to know
8
+ * what a DIFFERENT project's memory says about itself, or one that wants to
9
+ * re-read its own briefing without trusting a context window that has since
10
+ * been compacted.
11
+ *
12
+ * It lives on `bli-tower` rather than on `bli-memory` for one reason: the four
13
+ * store tools on that server are the vendor-compatible set the Claude Code
14
+ * plugin expects, and this is ours. The DOOR is the same either way.
15
+ *
16
+ * Read-only, and there is no compile argument on purpose. The page is written
17
+ * once a day by the `memory-profile` Railway service; a tool that could trigger
18
+ * a compile would put a paid model call behind a sentence an agent types.
19
+ */
20
+ import { z } from "zod";
21
+ import { callAgentDoor } from "./agent-door.js";
22
+ import { doorFailureText, errorResult, queryString, registrarFor, textResult, withSession, } from "./tool-result.js";
23
+ export function registerMemoryProfileTools(server, deps) {
24
+ registrarFor(server)("memory_profile", {
25
+ title: "Read a memory space's compiled profile",
26
+ description: "The one-page briefing BLI Memory compiles daily for a container: who works here, what is going on now, "
27
+ + "standing rules and preferences, how to communicate. The same page a session start injects. Read-only — "
28
+ + "the compile runs on a cron, never on demand. A container with no page yet answers `not_compiled_yet`, "
29
+ + "which means nothing has been compiled, NOT that the store is down.",
30
+ inputSchema: {
31
+ container: z
32
+ .string()
33
+ .min(1)
34
+ .max(128)
35
+ .describe("The container tag, e.g. repo_bli_cockpit__12e241cdbc00adfd."),
36
+ },
37
+ }, async (args) => withSession(deps, async (session) => {
38
+ const container = String(args.container);
39
+ const response = await callAgentDoor(session, deps.fetchImpl, "GET", `/api/memory/profile${queryString(new URLSearchParams({ container }))}`);
40
+ if (!response.ok)
41
+ return errorResult(doorFailureText("memory_profile", response));
42
+ const body = response.body;
43
+ const profile = body.profile ?? null;
44
+ const lines = profile?.lines ?? [];
45
+ if (lines.length === 0) {
46
+ // Named, not blank: the difference between "not compiled yet" and an
47
+ // empty page is the whole reason the door returns a reason at all.
48
+ return textResult(`No profile is compiled for ${container} yet (${String(body.reason ?? "unknown")}). `
49
+ + "The daily compile writes one once this space has memories.", { ok: true, containerTag: container, reason: body.reason ?? "unknown", lines: 0 });
50
+ }
51
+ return textResult([
52
+ ...lines,
53
+ "",
54
+ `Compiled ${String(profile?.compiledAt ?? "—")} from ${String(profile?.sourceRows ?? 0)} memories by ${String(profile?.model ?? "—")}.`,
55
+ ].join("\n"), {
56
+ ok: true,
57
+ containerTag: container,
58
+ reason: body.reason ?? "profile_read",
59
+ lines: lines.length,
60
+ compiledAt: profile?.compiledAt ?? null,
61
+ sourceRows: profile?.sourceRows ?? 0,
62
+ model: profile?.model ?? null,
63
+ });
64
+ }));
65
+ }
@@ -0,0 +1,23 @@
1
+ /**
2
+ * `memory_requeue` (BLI-4539) — put a window of finished extractions back in
3
+ * the queue, over the same `POST /api/memory/requeue` door
4
+ * `cockpit memory requeue` calls.
5
+ *
6
+ * It exists because the alternative was a one-off script. Between 04:16Z and
7
+ * 11:36Z on 2026-09-21 every insert into the memory store that carried aliases
8
+ * was refused, 122 extraction receipts closed as if nothing were wrong, and
9
+ * recovering them meant somebody hand-writing SQL against production.
10
+ *
11
+ * It lives on `bli-tower` rather than on `bli-memory` for the same reason
12
+ * `memory_profile` does: that server's four tools are the vendor-compatible
13
+ * set the Claude Code plugin expects, and this is ours.
14
+ *
15
+ * **`dryRun` defaults to TRUE, here as at the door.** An apply re-runs one
16
+ * model call per receipt, so an agent that means to count must never be able
17
+ * to spend by omission. The door's super_admin gate is the real boundary; this
18
+ * default is the one that stops an honest mistake.
19
+ */
20
+ import { type ToolDeps } from "./tool-result.js";
21
+ export declare function registerMemoryRequeueTools(server: {
22
+ registerTool: (...args: never[]) => unknown;
23
+ }, deps: ToolDeps): void;
@@ -0,0 +1,107 @@
1
+ /**
2
+ * `memory_requeue` (BLI-4539) — put a window of finished extractions back in
3
+ * the queue, over the same `POST /api/memory/requeue` door
4
+ * `cockpit memory requeue` calls.
5
+ *
6
+ * It exists because the alternative was a one-off script. Between 04:16Z and
7
+ * 11:36Z on 2026-09-21 every insert into the memory store that carried aliases
8
+ * was refused, 122 extraction receipts closed as if nothing were wrong, and
9
+ * recovering them meant somebody hand-writing SQL against production.
10
+ *
11
+ * It lives on `bli-tower` rather than on `bli-memory` for the same reason
12
+ * `memory_profile` does: that server's four tools are the vendor-compatible
13
+ * set the Claude Code plugin expects, and this is ours.
14
+ *
15
+ * **`dryRun` defaults to TRUE, here as at the door.** An apply re-runs one
16
+ * model call per receipt, so an agent that means to count must never be able
17
+ * to spend by omission. The door's super_admin gate is the real boundary; this
18
+ * default is the one that stops an honest mistake.
19
+ */
20
+ import { z } from "zod";
21
+ import { callAgentDoor } from "./agent-door.js";
22
+ import { doorFailureText, errorResult, registrarFor, textResult, withSession, } from "./tool-result.js";
23
+ /** `hook:104, harvest:18` — counts and labels only, never a receipt's content. */
24
+ function split(counts) {
25
+ if (!counts)
26
+ return "none";
27
+ const entries = Object.entries(counts).sort((left, right) => right[1] - left[1] || (left[0] < right[0] ? -1 : 1));
28
+ return entries.length === 0 ? "none" : entries.map(([label, count]) => `${label}:${count}`).join(", ");
29
+ }
30
+ export function registerMemoryRequeueTools(server, deps) {
31
+ registrarFor(server)("memory_requeue", {
32
+ title: "Requeue a window of memory extractions",
33
+ description: "Put finished `memory_extract_jobs` receipts back to `queued` after the store refused their writes — "
34
+ + "the recovery for a `store_failed` window once its cause is fixed. Nothing is ever deleted: the previous "
35
+ + "outcome is kept on the row under `result.requeued_from`. Super admins only. `dryRun` defaults to TRUE "
36
+ + "and an apply re-runs one model call per receipt, so count first. A window is required; there is no "
37
+ + "requeue-everything.",
38
+ inputSchema: {
39
+ since: z
40
+ .string()
41
+ .min(1)
42
+ .describe("ISO instant; receipts that FINISHED at or after it, e.g. 2026-09-21T04:10:00Z."),
43
+ until: z
44
+ .string()
45
+ .min(1)
46
+ .optional()
47
+ .describe("ISO instant; receipts that finished at or before it."),
48
+ source: z
49
+ .enum(["hook", "harvest"])
50
+ .optional()
51
+ .describe("One arm only. Absent means both."),
52
+ reason: z
53
+ .string()
54
+ .min(1)
55
+ .max(80)
56
+ .optional()
57
+ .describe("A short label, written onto the row as `requeued:<label>`. Never a sentence."),
58
+ dryRun: z
59
+ .boolean()
60
+ .optional()
61
+ .describe("Count and write nothing. Defaults to true; pass false to apply."),
62
+ },
63
+ }, async (args) => withSession(deps, async (session) => {
64
+ const dryRun = args.dryRun === undefined ? true : Boolean(args.dryRun);
65
+ const response = await callAgentDoor(session, deps.fetchImpl, "POST", "/api/memory/requeue", {
66
+ since: String(args.since),
67
+ ...(args.until === undefined ? {} : { until: String(args.until) }),
68
+ ...(args.source === undefined ? {} : { source: String(args.source) }),
69
+ ...(args.reason === undefined ? {} : { reason: String(args.reason) }),
70
+ dryRun,
71
+ });
72
+ if (!response.ok)
73
+ return errorResult(doorFailureText("memory_requeue", response));
74
+ const body = response.body;
75
+ const matched = body.matched ?? 0;
76
+ if (matched === 0) {
77
+ // Named, not blank: "nothing matched" and "the store is down" are
78
+ // different facts, and the second one took the branch above.
79
+ return textResult(`No finished extraction receipt is in that window. Nothing to requeue; the receipts table answered.`, { ok: true, dryRun: body.dryRun ?? dryRun, matched: 0, requeued: 0 });
80
+ }
81
+ const head = body.dryRun
82
+ ? `${matched} receipt(s) would be requeued`
83
+ : `${body.requeued ?? 0} of ${matched} receipt(s) requeued`;
84
+ return textResult([
85
+ `${head} (${split(body.bySource)}).`,
86
+ `By reason: ${split(body.byReason)}.`,
87
+ body.coverage === "capped"
88
+ ? "The page ceiling stopped this run before the window ended; run it again for the rest."
89
+ : "",
90
+ body.dryRun
91
+ ? "Nothing was written. Call again with dryRun: false to apply."
92
+ : "The previous result is kept on each row under result.requeued_from.",
93
+ ]
94
+ .filter((line) => line.length > 0)
95
+ .join("\n"), {
96
+ ok: true,
97
+ dryRun: body.dryRun ?? dryRun,
98
+ matched,
99
+ scanned: body.scanned ?? 0,
100
+ requeued: body.requeued ?? 0,
101
+ refused: body.refused ?? 0,
102
+ coverage: body.coverage ?? "unknown",
103
+ bySource: body.bySource ?? {},
104
+ byReason: body.byReason ?? {},
105
+ });
106
+ }));
107
+ }
package/dist/server.js CHANGED
@@ -15,7 +15,10 @@ import { registerMailTools } from "./mail-tools.js";
15
15
  import { registerNotesAccountTools } from "./notes-account-tools.js";
16
16
  import { registerNotesTools } from "./notes-tools.js";
17
17
  import { registerNotesWriteTools } from "./notes-write-tools.js";
18
+ import { registerMemoryConsolidationTools } from "./memory-consolidation-tools.js";
18
19
  import { registerMemoryExperienceTools } from "./memory-experience-tools.js";
20
+ import { registerMemoryProfileTools } from "./memory-profile-tools.js";
21
+ import { registerMemoryRequeueTools } from "./memory-requeue-tools.js";
19
22
  import { registerModelsTools } from "./models-tools.js";
20
23
  import { registerOpsTools } from "./ops-tools.js";
21
24
  import { registerPagesTools } from "./pages-tools.js";
@@ -423,6 +426,14 @@ export function createServer(deps) {
423
426
  registerNotesTools(server, { fetchImpl: deps.fetchImpl });
424
427
  registerOpsTools(server, { fetchImpl: deps.fetchImpl });
425
428
  registerMemoryExperienceTools(server, { fetchImpl: deps.fetchImpl });
429
+ // BLI-4533: the compiled one-page profile, the twin of `cockpit memory profile`.
430
+ registerMemoryProfileTools(server, { fetchImpl: deps.fetchImpl });
431
+ // BLI-4534: the nightly consolidation receipts, twin of `cockpit memory
432
+ // consolidation`. Read-only, and the pass it reports on never deletes.
433
+ registerMemoryConsolidationTools(server, { fetchImpl: deps.fetchImpl });
434
+ // BLI-4539: putting a window of refused extractions back, the twin of
435
+ // `cockpit memory requeue`. Dry run by default, super_admin at the door.
436
+ registerMemoryRequeueTools(server, { fetchImpl: deps.fetchImpl });
426
437
  registerSettingsTools(server, { fetchImpl: deps.fetchImpl });
427
438
  registerPagesTools(server, { fetchImpl: deps.fetchImpl });
428
439
  // BLI-3756 batch 2: the WRITES. Same doors, same device token, same refusal
@@ -300,6 +300,18 @@ export const MCP_TWINS = {
300
300
  "memory save": { tool: "save_memory", door: "POST /api/memory/save", server: "bli-memory" },
301
301
  "memory update": { tool: "update_memory", door: "POST /api/memory/update", server: "bli-memory" },
302
302
  "memory forget": { tool: "forget_memory", door: "POST /api/memory/forget", server: "bli-memory" },
303
+ // BLI-4533: the compiled one-page profile. Its twin is on `bli-tower`, not
304
+ // `bli-memory`: that server's four tools are the vendor-compatible set the
305
+ // Claude Code plugin expects, and this page is ours.
306
+ "memory profile": { tool: "memory_profile", door: "GET /api/memory/profile" },
307
+ // BLI-4534: the nightly consolidation receipts. This twin lives on
308
+ // `bli-tower` rather than `bli-memory`, because it is a read of a JOB s
309
+ // counts and not a read or write of the store itself.
310
+ "memory consolidation": { tool: "memory_consolidation", door: "GET /api/memory/consolidation" },
311
+ // BLI-4539: the recovery verb, after a window of extractions whose writes
312
+ // the store refused. Also on `bli-tower`, for the same reason
313
+ // `memory profile` is: it is ours, not the vendor's four.
314
+ "memory requeue": { tool: "memory_requeue", door: "POST /api/memory/requeue" },
303
315
  "ops status": { tool: "ops_status", door: "GET /api/ops/status" },
304
316
  "ops recompile": { tool: "ops_recompile", door: "POST /api/ops/recompile" },
305
317
  "slack coverage": { tool: "slack_coverage", door: "GET /api/ops/slack/coverage" },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bli-cockpit/mcp",
3
- "version": "0.1.55",
3
+ "version": "0.1.58",
4
4
  "private": false,
5
5
  "description": "bli-tower: an MCP server over BLI Cockpit's agent doors: JARVIS (jarvis_*), documents (docs_*), channels (msg_*), issues (work_*), the daily page (brief_*), meeting notes (notes_*), the ops board (ops_status/slack_*), settings/team/model, Scout and the workbook, plus the legacy event-stream tools (emit_event, get_ticket_timeline, get_active_tickets).",
6
6
  "type": "module",