@bli-cockpit/mcp 0.1.18 → 0.1.20

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
- **73 of 76 Tower verbs have an MCP twin.**
37
+ **74 of 77 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
@@ -78,6 +78,7 @@ three tables below.
78
78
  | `cockpit mail search` | `mail_search` | `GET /api/mail/search` |
79
79
  | `cockpit mail send` | `mail_send` | `POST /api/mail/send` |
80
80
  | `cockpit mail sync` | `mail_sync` | `POST /api/mail/accounts/[id]/sync` |
81
+ | `cockpit memory log` | `memory_experience` | `POST /api/memory/experience` |
81
82
  | `cockpit model set` | `model_set` | `POST /api/settings/jarvis-model` |
82
83
  | `cockpit model show` | `model_show` | `GET /api/settings/jarvis-model` |
83
84
  | `cockpit msg channels` | `msg_channels` | `GET /api/msg/channels` |
@@ -0,0 +1,6 @@
1
+ import { type ToolDeps } from "./tool-result.js";
2
+ export declare function registerMemoryExperienceTools(server: {
3
+ registerTool: (...args: never[]) => unknown;
4
+ }, deps: ToolDeps & {
5
+ homeDir?: string;
6
+ }): void;
@@ -0,0 +1,24 @@
1
+ import { z } from "zod";
2
+ import { appendExperience, shipExperience } from "@bli-cockpit/telemetry-core/memory-experience";
3
+ import { callAgentDoor } from "./agent-door.js";
4
+ import { loadAgentDoorSession } from "./agent-door-session.js";
5
+ import { registrarFor, textResult } from "./tool-result.js";
6
+ export function registerMemoryExperienceTools(server, deps) {
7
+ registrarFor(server)("memory_experience", {
8
+ title: "Log a memory-store experience",
9
+ description: "Append an experience receipt locally and post to Tower. Offline receipts ship on the next collector sync. Copy the recall marker: [bli] = BLI Memory, ◪ = Supermemory. Opinions only; never prompts, memory bodies or secrets.",
10
+ inputSchema: { store: z.enum(["bli", "supermemory", "both"]), verdict: z.enum(["win", "loss", "noise"]), reason: z.string().min(1).max(500) },
11
+ }, async (args) => {
12
+ const entry = await appendExperience({ store: args.store, verdict: args.verdict, reason: String(args.reason) }, { homeDir: deps.homeDir, agent: "MCP", project: "workspace" });
13
+ const delivery = await shipExperience(entry, async (body) => {
14
+ const session = (deps.loadSession ?? loadAgentDoorSession)();
15
+ if (!session.ok)
16
+ return { ok: false, reason: session.reason };
17
+ const result = await callAgentDoor(session.session, deps.fetchImpl, "POST", "/api/memory/experience", body, 5000);
18
+ return { ok: result.ok && result.body.ok === true, reason: result.transportError ?? String(result.body.error ?? "experience_refused") };
19
+ }, deps.homeDir);
20
+ const receipt = { ok: true, id: entry.id, local: true, ...delivery };
21
+ console.error("[memory experience] recorded", JSON.stringify(receipt));
22
+ return textResult(`Experience appended; shipped: ${delivery.shipped} (${delivery.reason}).`, receipt);
23
+ });
24
+ }
package/dist/ops-tools.js CHANGED
@@ -77,6 +77,8 @@ export function registerOpsTools(server, deps) {
77
77
  ? await callAgentDoor(session, deps.fetchImpl, "GET", "/api/ops/memory-usage") : null;
78
78
  if (memory && !memory.ok)
79
79
  return errorResult(doorFailureText("ops_status memory", memory));
80
+ const experience = memory?.ok ? asRecord(memory.body.experience) : null;
81
+ const experienceLines = experience ? ["EXPERIENCE", experience.summary, ...(Array.isArray(experience.lines) ? experience.lines : [])].join("\n") : "";
80
82
  const hooks = memory?.ok ? asRecord(memory.body.hooks) : null;
81
83
  const memoryLines = hooks && Array.isArray(hooks.devices)
82
84
  ? hooks.devices.map((device) => {
@@ -88,7 +90,8 @@ export function registerOpsTools(server, deps) {
88
90
  return textResult(`${rows.length} pipeline(s), ${unhealthy.length} unhealthy`
89
91
  + `${unhealthy.length > 0 ? `: ${unhealthy.map((row) => row.id ?? "?").join(", ")}` : "."}`
90
92
  + `${lines ? `\n${lines}` : ""}`
91
- + `${memoryLines ? `\n${memoryLines}` : ""}`,
93
+ + `${memoryLines ? `\n${memoryLines}` : ""}`
94
+ + `${experienceLines ? `\n${experienceLines}` : ""}`,
92
95
  // The API owns the wording and the timing boundary on every surface.
93
96
  {
94
97
  pipelines: rows,
@@ -96,7 +99,7 @@ export function registerOpsTools(server, deps) {
96
99
  ...(body.fleet ? { fleet: body.fleet } : {}),
97
100
  ...(body.coverage ? { coverage: body.coverage } : {}),
98
101
  ...(body.skips ? { skips: body.skips } : {}),
99
- ...(memory?.ok ? { memory: memory.body.memory, hooks: memory.body.hooks } : {}),
102
+ ...(memory?.ok ? { memory: memory.body.memory, hooks: memory.body.hooks, experience: memory.body.experience } : {}),
100
103
  });
101
104
  }));
102
105
  register("slack_coverage", {
package/dist/server.js CHANGED
@@ -14,6 +14,7 @@ import { registerCalTools } from "./cal-tools.js";
14
14
  import { registerMailTools } from "./mail-tools.js";
15
15
  import { registerNotesTools } from "./notes-tools.js";
16
16
  import { registerNotesWriteTools } from "./notes-write-tools.js";
17
+ import { registerMemoryExperienceTools } from "./memory-experience-tools.js";
17
18
  import { registerOpsTools } from "./ops-tools.js";
18
19
  import { registerPagesTools } from "./pages-tools.js";
19
20
  import { registerSearchTool } from "./search-tool.js";
@@ -411,6 +412,7 @@ export function createServer(deps) {
411
412
  registerBriefTools(server, { fetchImpl: deps.fetchImpl });
412
413
  registerNotesTools(server, { fetchImpl: deps.fetchImpl });
413
414
  registerOpsTools(server, { fetchImpl: deps.fetchImpl });
415
+ registerMemoryExperienceTools(server, { fetchImpl: deps.fetchImpl });
414
416
  registerSettingsTools(server, { fetchImpl: deps.fetchImpl });
415
417
  registerPagesTools(server, { fetchImpl: deps.fetchImpl });
416
418
  // BLI-3756 batch 2: the WRITES. Same doors, same device token, same refusal
@@ -88,15 +88,57 @@ function commandVariants() {
88
88
  if (start < 0)
89
89
  continue;
90
90
  const end = source.indexOf("\nexport ", start + 10);
91
- return source.slice(start, end < 0 ? undefined : end).split(/\n\s{2}\|\s/);
91
+ return source
92
+ .slice(start, end < 0 ? undefined : end)
93
+ .split(/\n\s{2}\|\s/)
94
+ .map((variant) => resolveShapeAlias(variant, source));
92
95
  }
93
96
  throw new Error("LocalCommand union not found in local-command-shapes.ts or local-args.ts");
94
97
  }
98
+ /**
99
+ * A union member that is a bare NAME rather than an inline object.
100
+ *
101
+ * BLI-3880 wrote `cockpit search`'s shape as its own exported interface in a
102
+ * sibling file (`local-command-shapes-search.ts`) and put `| SearchCommandShape`
103
+ * in the union — perfectly ordinary TypeScript, and invisible to a census that
104
+ * only looks for an inline `kind: "…"`. That silence was not silent for long:
105
+ * `towerVerbs` throws when a noun has no variant, which is exactly what it did.
106
+ * Rather than teach the union one spelling, this follows the alias: find the
107
+ * import that names it, read that file, and hand back the interface body so
108
+ * every regex below sees the same thing it would have seen inline.
109
+ *
110
+ * A name that cannot be followed is returned unchanged, so it still reaches
111
+ * the "no LocalCommand variant" error rather than disappearing quietly.
112
+ */
113
+ function resolveShapeAlias(variant, unionSource) {
114
+ // The chunk is the alias NAME plus whatever comment introduces the next
115
+ // union member, because the split is on `\n | `. Only the first line is the
116
+ // member itself.
117
+ const firstLine = variant.split("\n").find((line) => line.trim().length > 0) ?? "";
118
+ const name = /^\s*([A-Z][A-Za-z0-9_]*)\s*;?\s*$/.exec(firstLine)?.[1];
119
+ if (!name)
120
+ return variant;
121
+ const from = new RegExp(`import type \\{[^}]*\\b${name}\\b[^}]*\\} from "\\.\\/([\\w.-]+)\\.js"`).exec(unionSource)?.[1];
122
+ if (!from)
123
+ return variant;
124
+ let source;
125
+ try {
126
+ source = readFileSync(join(COLLECTOR_COMMANDS_DIR, `${from}.ts`), "utf8");
127
+ }
128
+ catch {
129
+ return variant;
130
+ }
131
+ const start = source.indexOf(`export interface ${name} {`);
132
+ if (start < 0)
133
+ return variant;
134
+ const end = source.indexOf("\n}", start);
135
+ return source.slice(start, end < 0 ? undefined : end);
136
+ }
95
137
  /** Every Tower verb a person may type today. */
96
138
  export function towerVerbs() {
97
139
  const nouns = new Set(towerNouns());
98
140
  const aliases = actionAliases();
99
- const verbs = [];
141
+ const verbs = [{ noun: "memory", action: "log", spelling: "memory log" }];
100
142
  const seenNouns = new Set();
101
143
  for (const variant of commandVariants()) {
102
144
  const kind = /kind:\s*"([a-z-]+)"/.exec(variant)?.[1];
@@ -179,6 +221,7 @@ export const MCP_TWINS = {
179
221
  "notes share": { tool: "notes_share", door: "POST /api/notes/share" },
180
222
  "notes unshare": { tool: "notes_unshare", door: "POST /api/notes/share (share=false)" },
181
223
  "notes move": { tool: "notes_move", door: "POST /api/notes/move" },
224
+ "memory log": { tool: "memory_experience", door: "POST /api/memory/experience" },
182
225
  "ops status": { tool: "ops_status", door: "GET /api/ops/status" },
183
226
  "ops recompile": { tool: "ops_recompile", door: "POST /api/ops/recompile" },
184
227
  "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.18",
3
+ "version": "0.1.20",
4
4
  "private": false,
5
5
  "description": "bli-tower \u2014 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",
@@ -30,7 +30,7 @@
30
30
  "readme": "npm run build && node scripts/write-readme-census.mjs"
31
31
  },
32
32
  "dependencies": {
33
- "@bli-cockpit/telemetry-core": "0.1.34",
33
+ "@bli-cockpit/telemetry-core": "0.1.36",
34
34
  "@modelcontextprotocol/sdk": "^1.29.0",
35
35
  "zod": "^4.3.6"
36
36
  },