@bli-cockpit/mcp 0.1.95 → 0.1.97

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
@@ -109,7 +109,7 @@ about what an agent can reach.
109
109
 
110
110
  <!-- BEGIN GENERATED verb census — `npm run mcp:readme` -->
111
111
 
112
- **126 of 131 Tower verbs have an MCP twin.**
112
+ **135 of 140 Tower verbs have an MCP twin.**
113
113
  Each tool goes through the SAME door its CLI verb calls, with the same
114
114
  collector device token — never a second route and never a service-role
115
115
  reader. `src/verb-census.test.ts` fails when a verb is in none of the
@@ -161,6 +161,9 @@ three tables below.
161
161
  | `cockpit github pr` | `github_pr` | `POST /api/github/pull-request (read)` |
162
162
  | `cockpit github rerun` | `github_rerun` | `POST /api/github/pull-request (rerun)` |
163
163
  | `cockpit github retitle` | `github_retitle` | `POST /api/github/pull-request (retitle)` |
164
+ | `cockpit glossary add` | `glossary_add` | `POST /api/glossary` |
165
+ | `cockpit glossary list` | `glossary_list` | `GET /api/glossary` |
166
+ | `cockpit glossary remove` | `glossary_remove` | `POST /api/glossary` |
164
167
  | `cockpit issue comment` | `work_comment_issue` | `POST /api/work/issues/[id]/comments` |
165
168
  | `cockpit issue create` | `work_create_issue` | `POST /api/work/issues` |
166
169
  | `cockpit issue history` | `work_issue_history` | `GET /api/work/issues/[id]/history` |
@@ -184,7 +187,9 @@ three tables below.
184
187
  | `cockpit memory consolidation` | `memory_consolidation` | `GET /api/memory/consolidation` |
185
188
  | `cockpit memory forget` | `forget_memory` (`bli-memory`) | `POST /api/memory/forget` |
186
189
  | `cockpit memory log` | `memory_experience` | `POST /api/memory/experience` |
190
+ | `cockpit memory private` | `set_memory_visibility` (`bli-memory`) | `POST /api/memory/visibility` |
187
191
  | `cockpit memory profile` | `memory_profile` | `GET /api/memory/profile` |
192
+ | `cockpit memory public` | `set_memory_visibility` (`bli-memory`) | `POST /api/memory/visibility` |
188
193
  | `cockpit memory requeue` | `memory_requeue` | `POST /api/memory/requeue` |
189
194
  | `cockpit memory save` | `save_memory` (`bli-memory`) | `POST /api/memory/save` |
190
195
  | `cockpit memory search` | `search_memory` (`bli-memory`) | `POST /api/memory/search` |
@@ -209,6 +214,8 @@ three tables below.
209
214
  | `cockpit notes move` | `notes_move` | `POST /api/notes/move` |
210
215
  | `cockpit notes paste` | `notes_paste` | `POST /api/notes/upload (text)` |
211
216
  | `cockpit notes place` | `notes_place` | `POST /api/notes/place` |
217
+ | `cockpit notes recording` | `notes_recording` | `GET /api/notes/recordings?recording_id` |
218
+ | `cockpit notes recordings` | `notes_recordings` | `GET /api/notes/recordings?note_id` |
212
219
  | `cockpit notes rename` | `notes_rename` | `PATCH /api/notes/folders/[id]` |
213
220
  | `cockpit notes rmdir` | `notes_rmdir` | `DELETE /api/notes/folders/[id]` |
214
221
  | `cockpit notes share` | `notes_share` | `POST /api/notes/share` |
@@ -233,8 +240,10 @@ three tables below.
233
240
  | `cockpit settings env list` | `settings_list` | `GET /api/settings/env-blobs` |
234
241
  | `cockpit settings switches set` | `settings_set` | `POST /api/settings/{jarvis-model,switches,model-routing,env-blobs} + PUT /api/settings/cli-floor` |
235
242
  | `cockpit settings` | `settings_show` | `GET /api/settings/* + /api/team/members` |
243
+ | `cockpit setup` | `setup_read` | `GET /api/setup` |
236
244
  | `cockpit slack coverage` | `slack_coverage` | `GET /api/ops/slack/coverage` |
237
245
  | `cockpit slack read` | `slack_read` | `POST /api/ops/slack/read` |
246
+ | `cockpit team device doctor` | `team_device_doctor` | `POST /api/ambient/devices/[deviceId]/doctor` |
238
247
  | `cockpit team device list` | `team_device_list` | `GET /api/team/devices` |
239
248
  | `cockpit team device revoke` | `team_device_revoke` | `POST /api/ambient/devices/[deviceId]/revoke` |
240
249
  | `cockpit team invite` | `team_invite` | `POST /api/team/invite` |
@@ -259,7 +268,7 @@ A claim about the verb's nature, not a backlog.
259
268
  | `cockpit brief edit` | opens the person's own $EDITOR on this machine and files what they changed; an agent has no editor to open (commands/editor.ts) |
260
269
  | `cockpit cal add-ical` | carries a calendar's SECRET iCal ADDRESS — a permanent, unauthenticated, read-anything-on-that-calendar URL. The CLI reads it from STDIN precisely so it never lands in an argument list; an MCP tool argument travels through a model's context window and whatever transcript store sits behind it, so attaching a calendar stays a thing a person does at a terminal (BLI-3709) |
261
270
  | `cockpit mail add-imap` | carries a Google app password. The CLI reads it from STDIN precisely so it never lands in an argument list; an MCP tool argument travels through a model's context window and whatever transcript store sits behind it, so attaching a mailbox stays a thing a person does at a terminal (BLI-3708) |
262
- | `cockpit notes connect` | carries a notetaker API key (Fellow or Circleback). The CLI reads it from STDIN precisely so it never lands in an argument list; an MCP tool argument travels through a model's context window and whatever transcript store sits behind it, so connecting a notetaker stays a thing a person does at a terminal or in Tower's Settings (BLI-4383) |
271
+ | `cockpit notes connect` | carries a notetaker credential: an API key (Fellow or Circleback), or for Wispr Flow a browser sign-in whose refresh token the CLI hands straight to Tower. The CLI reads a key from STDIN precisely so it never lands in an argument list, and a browser sign-in has no tool-call equivalent at all; an MCP tool argument travels through a model's context window and whatever transcript store sits behind it, so connecting a notetaker stays a thing a person does at a terminal or in Tower's Settings (BLI-4383, BLI-12926) |
263
272
  | `cockpit notes webhook` | carries the VENDOR's webhook signing secret, the key an HMAC over every delivered payload is checked against. Exactly the ruling `notes connect` already carries, for the same credential shape: the CLI reads it from STDIN so it never lands in an argument list, and an MCP tool argument travels through a model's context window and whatever transcript store sits behind it (BLI-4394) |
264
273
 
265
274
  <!-- END GENERATED verb census -->
@@ -0,0 +1,4 @@
1
+ import { type ToolDeps } from "./tool-result.js";
2
+ export declare function registerGlossaryTools(server: {
3
+ registerTool: (...args: never[]) => unknown;
4
+ }, deps: ToolDeps): void;
@@ -0,0 +1,19 @@
1
+ import { z } from "zod";
2
+ import { callAgentDoor } from "./agent-door.js";
3
+ import { registrarFor, withSession, textResult, errorResult, doorFailureText } from "./tool-result.js";
4
+ const WRONG_FORM = z.string().min(1).max(80).describe("The misheard spelling to correct, exactly as it shows up (for example a name transcribed wrongly).");
5
+ const RIGHT_FORM = z.string().min(1).max(80).describe("The correct spelling to put in its place.");
6
+ export function registerGlossaryTools(server, deps) {
7
+ const register = registrarFor(server);
8
+ for (const action of ["list", "add", "remove"]) {
9
+ register(`glossary_${action}`, {
10
+ title: `Glossary ${action}`, description: action === "remove" ? "Prevent future corrections of a wrong form." : "Read or add a shared spelling correction. Quote phrases in the CLI.",
11
+ inputSchema: action === "list" ? {} : action === "add" ? { wrong: WRONG_FORM, to: RIGHT_FORM } : { wrong: WRONG_FORM },
12
+ }, async (args) => withSession(deps, async (session) => {
13
+ const response = await callAgentDoor(session, deps.fetchImpl, action === "list" ? "GET" : "POST", "/api/glossary", action === "list" ? undefined : { action, wrong: args.wrong, to: args.to });
14
+ if (!response.ok)
15
+ return errorResult(doorFailureText(`glossary_${action}`, response));
16
+ return textResult(JSON.stringify(response.body), response.body);
17
+ }));
18
+ }
19
+ }
@@ -0,0 +1,9 @@
1
+ /**
2
+ * The `bli-memory` server's twins in the verb census (BLI-4047, BLI-12927).
3
+ *
4
+ * BLI Memory ships its own MCP server, so its store verbs are twinned against
5
+ * tools on THAT server, over the same `/api/memory/*` doors the CLI verbs call.
6
+ * Split out of `verb-census.ts` to keep that file under the readability band.
7
+ */
8
+ import type { McpTwin } from "./verb-census.js";
9
+ export declare const MEMORY_STORE_TWINS: Record<string, McpTwin>;
@@ -0,0 +1,17 @@
1
+ /**
2
+ * The `bli-memory` server's twins in the verb census (BLI-4047, BLI-12927).
3
+ *
4
+ * BLI Memory ships its own MCP server, so its store verbs are twinned against
5
+ * tools on THAT server, over the same `/api/memory/*` doors the CLI verbs call.
6
+ * Split out of `verb-census.ts` to keep that file under the readability band.
7
+ */
8
+ export const MEMORY_STORE_TWINS = {
9
+ "memory search": { tool: "search_memory", door: "POST /api/memory/search", server: "bli-memory" },
10
+ "memory save": { tool: "save_memory", door: "POST /api/memory/save", server: "bli-memory" },
11
+ "memory update": { tool: "update_memory", door: "POST /api/memory/update", server: "bli-memory" },
12
+ "memory forget": { tool: "forget_memory", door: "POST /api/memory/forget", server: "bli-memory" },
13
+ // BLI-12927: memory is public to the team until its owner narrows it. Two CLI
14
+ // verbs, one tool: `visibility` is the tool's argument.
15
+ "memory private": { tool: "set_memory_visibility", door: "POST /api/memory/visibility", server: "bli-memory" },
16
+ "memory public": { tool: "set_memory_visibility", door: "POST /api/memory/visibility", server: "bli-memory" },
17
+ };
@@ -19,7 +19,38 @@
19
19
  */
20
20
  import { z } from "zod";
21
21
  import { callAgentDoor } from "./agent-door.js";
22
- import { CONFIRM_INPUT, doorFailureText, errorResult, queryString, registrarFor, textResult, unconfirmed, withSession, } from "./tool-result.js";
22
+ import { CONFIRM_INPUT, doorFailureText, dataResult, errorResult, queryString, registrarFor, textResult, unconfirmed, withSession, } from "./tool-result.js";
23
+ /**
24
+ * What else feeds this person's notes (BLI-12925): Granola arrives through the
25
+ * organization's own key and cron, never an account a person connected, so an
26
+ * empty account list never meant "no notetaker". Same sentence the CLI prints.
27
+ */
28
+ async function sharedFeedsText(session, fetchImpl) {
29
+ const how = "read by the organization's own Granola key, swept once a day by Tower's server; no key of yours is involved";
30
+ const response = await callAgentDoor(session, fetchImpl, "GET", "/api/notes/granola-status?limit=1");
31
+ const runs = response.ok && Array.isArray(response.body.runs)
32
+ ? response.body.runs
33
+ : null;
34
+ const last = !response.ok
35
+ ? "last run unknown (the run ledger could not be read)"
36
+ : runs && runs[0]
37
+ ? `last run ${runs[0].started_at}: ${runs[0].reason}, saw ${runs[0].notes_seen}, stored ${runs[0].notes_stored}, failed ${runs[0].notes_failed}`
38
+ : "no run has landed a row yet";
39
+ return {
40
+ text: `\n\nAlso feeding your notes, for the whole organization:\n Granola ${how}; ${last}. Its runs in full: notes_granola_status.`,
41
+ feeds: [{ provider: "granola", name: "Granola", how, last_run: runs?.[0] ?? null, unreadable: !response.ok }],
42
+ };
43
+ }
44
+ function coverageLines(coverage) {
45
+ return (coverage.members ?? []).map((member) => {
46
+ const who = `${member.display_name ?? member.email}${member.is_you ? " (you)" : ""}`;
47
+ if (member.notetakers.length === 0)
48
+ return `${who} NO NOTETAKER CONNECTED`;
49
+ return `${who} ${member.notetakers
50
+ .map((taker) => (taker.status === "active" ? taker.provider : `${taker.provider} (${taker.status})`))
51
+ .join(", ")}`;
52
+ });
53
+ }
23
54
  function accountLine(account) {
24
55
  const health = account.status === "active"
25
56
  ? "active"
@@ -30,20 +61,66 @@ export function registerNotesAccountTools(server, deps) {
30
61
  const register = registrarFor(server);
31
62
  register("notes_accounts", {
32
63
  title: "List connected notetakers",
33
- description: "Every notetaker account (Fellow or Circleback; Otter and Granola are named but not connectable) connected to "
34
- + "Tower that you may see: name, provider, workspace, health and when it was last read. Start here when you "
35
- + "need an account id. Connecting one is NOT available here on purpose — it carries an API key, so it is done "
36
- + "at a terminal with `cockpit notes connect --provider fellow --workspace <subdomain> --key-stdin` or in "
37
- + "Tower's Settings.",
64
+ description: "Every notetaker account (Fellow, Circleback or Wispr Flow; Otter and Granola are named but not connectable) "
65
+ + "connected to Tower that you may see: name, provider, workspace, health and when it was last read, plus the feeds the organization reads for "
66
+ + "everyone (Granola, through its own key). Start here when you need an account id. ALSO answers `coverage`: for every member of the team, which notetakers feed "
67
+ + "their notes, and which members have none (BLI-12926). Connecting one is NOT available here on purpose — it "
68
+ + "carries a credential, so it is done at a terminal with `cockpit notes connect --provider fellow --workspace "
69
+ + "<subdomain> --key-stdin` (or `--provider wisprflow`, which signs in through a browser) or in Tower's Settings.",
38
70
  inputSchema: {},
39
71
  }, async () => withSession(deps, async (session) => {
40
72
  const response = await callAgentDoor(session, deps.fetchImpl, "GET", "/api/notes/accounts");
41
73
  if (!response.ok)
42
74
  return errorResult(doorFailureText("notes_accounts", response));
43
75
  const accounts = (Array.isArray(response.body.accounts) ? response.body.accounts : []);
44
- return textResult(accounts.length === 0
45
- ? "No notetaker is connected. A person connects one with `cockpit notes connect --provider fellow --workspace <subdomain> --key-stdin`, or in Tower's Settings."
46
- : `${accounts.length} notetaker(s).\n${accounts.map(accountLine).join("\n")}`, { accounts });
76
+ const shared = await sharedFeedsText(session, deps.fetchImpl);
77
+ // BLI-12926: added after the list so the list is exactly what it was. A
78
+ // failed read of it is said in the answer and never fails the list.
79
+ const coverageResponse = await callAgentDoor(session, deps.fetchImpl, "GET", "/api/notes/coverage");
80
+ const coverage = coverageResponse.ok ? coverageResponse.body : null;
81
+ const headline = accounts.length === 0
82
+ ? "You have not connected a notetaker of your own. A person connects one with `cockpit notes connect --provider fellow --workspace <subdomain> --key-stdin`, or in Tower's Settings."
83
+ : `${accounts.length} notetaker(s).\n${accounts.map(accountLine).join("\n")}`;
84
+ const who = coverage
85
+ ? `\nWho feeds Tower's notes (${coverage.members_with_none ?? 0} member(s) with none):\n${coverageLines(coverage).join("\n")}`
86
+ : "\nWho feeds Tower's notes: could not be read just now.";
87
+ return dataResult(`${headline}${shared.text}${who}`, {
88
+ accounts,
89
+ shared_feeds: shared.feeds,
90
+ coverage,
91
+ });
92
+ }));
93
+ register("notes_recordings", {
94
+ title: "List the recordings of one meeting",
95
+ description: "When several notetakers or people recorded one meeting, the library shows ONE note: the recording with the "
96
+ + "longest transcript. This lists every recording of the meeting a note is: which one is shown, which are "
97
+ + "alternates, each one's transcript length, its notetaker, and the sentence saying why it holds its role "
98
+ + "(BLI-12926). Open an alternate with `notes_recording`. Read-only; you only see recordings of meetings you "
99
+ + "can already read.",
100
+ inputSchema: { note_id: z.string().uuid().describe("A note's id, as notes_list returns it.") },
101
+ }, async (args) => withSession(deps, async (session) => {
102
+ const response = await callAgentDoor(session, deps.fetchImpl, "GET", `/api/notes/recordings?note_id=${encodeURIComponent(String(args.note_id))}`);
103
+ if (!response.ok)
104
+ return errorResult(doorFailureText("notes_recordings", response));
105
+ const recordings = (Array.isArray(response.body.recordings) ? response.body.recordings : []);
106
+ const lines = recordings.map((row) => `${row.role === "primary" ? "SHOWN " : "alternate"} ${row.source_namespace} ${row.transcript_chars === null ? "length unknown" : `${row.transcript_chars} chars`} ${row.started_at ?? "no start time"} ${row.id}${row.chosen_reason ? ` (${row.chosen_reason})` : ""}`);
107
+ return dataResult(recordings.length === 0
108
+ ? "No recording is indexed for this note."
109
+ : `${recordings.length} recording(s) of this meeting.\n${lines.join("\n")}`, { note_id: String(args.note_id), recordings });
110
+ }));
111
+ register("notes_recording", {
112
+ title: "Open one recording of a meeting",
113
+ description: "One recording by its own id, text and all, shown or alternate (BLI-12926). An alternate was never ingested, so "
114
+ + "it has no extracted claims; its text is here and nowhere else. Read-only.",
115
+ inputSchema: { recording_id: z.string().uuid().describe("A recording id, as notes_recordings returns it.") },
116
+ }, async (args) => withSession(deps, async (session) => {
117
+ const response = await callAgentDoor(session, deps.fetchImpl, "GET", `/api/notes/recordings?recording_id=${encodeURIComponent(String(args.recording_id))}`);
118
+ if (!response.ok)
119
+ return errorResult(doorFailureText("notes_recording", response));
120
+ const recording = response.body.recording;
121
+ if (!recording)
122
+ return errorResult("notes_recording: Tower answered without a recording.");
123
+ return dataResult(`${recording.role === "primary" ? "The recording shown as the note" : "An alternate"} from ${recording.source_namespace}.\n${recording.body ?? "(Tower did not keep this recording's text.)"}`, { recording });
47
124
  }));
48
125
  register("notes_sync_status", {
49
126
  title: "Read notetaker sync health",
@@ -56,7 +56,7 @@ export function registerNotesTools(server, deps) {
56
56
  const register = registrarFor(server);
57
57
  register("notes_list", {
58
58
  title: "List Tower meeting notes",
59
- description: "The meeting-notes library you may read, grouped by shelf: id, date, kind, participants and file name. "
59
+ description: "The meeting-notes library you may read, grouped by shelf: id, date, kind, participants and the meeting's title (file name when it has none). "
60
60
  + `Newest ${DEFAULT_LIST_LIMIT} unless you pass limit (up to 500); the answer says when more exist. `
61
61
  + "Never a note's text — call notes_show for one. Says when the answer is narrower than a browser's.",
62
62
  inputSchema: LIBRARY_FILTERS,
@@ -77,7 +77,7 @@ export function registerNotesTools(server, deps) {
77
77
  const notes = shelf.notes
78
78
  .map((note) => {
79
79
  const who = note.participants.length > 0 ? ` — ${note.participants.join(", ")}` : "";
80
- return ` ${note.meetingDate} ${note.id} ${note.fileName}${who}`;
80
+ return ` ${note.meetingDate} ${note.id} ${note.title?.trim() || note.fileName}${who}`;
81
81
  })
82
82
  .join("\n");
83
83
  return `${shelf.heading} (${shelf.notes.length})\n${notes}`;
package/dist/server.d.ts CHANGED
@@ -1,9 +1,3 @@
1
- /**
2
- * MCP server factory for @bli-cockpit/mcp.
3
- *
4
- * Pure factory — accepts injected fetch + base URL + token so it can be
5
- * unit-tested without binding to a real network or transport.
6
- */
7
1
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
8
2
  import { z } from "zod";
9
3
  import { type CreateServerOptions } from "./tool-registration.js";
package/dist/server.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { registerGlossaryTools } from "./glossary-tools.js";
1
2
  /**
2
3
  * MCP server factory for @bli-cockpit/mcp.
3
4
  *
@@ -33,6 +34,7 @@ import { registerTeamWriteTools } from "./team-write-tools.js";
33
34
  import { registerWorkTools } from "./work-tools.js";
34
35
  import { registerCareersTools } from "./careers-tools.js";
35
36
  import { registerRenameTools } from "./rename-tools.js";
37
+ import { registerSetupTools } from "./setup-tools.js";
36
38
  import { registerUsageTools } from "./usage-tools.js";
37
39
  import { registerLogsTools } from "./logs-tools.js";
38
40
  import { registerBrainTools } from "./brain-tools.js";
@@ -422,6 +424,7 @@ export function createServer(deps, options = {}) {
422
424
  // unconditionally — the session is loaded fresh per call, so a machine with
423
425
  // no `cockpit login` pairing yet still serves the three event tools; only a
424
426
  // docs/msg call on that machine fails, by name.
427
+ registerGlossaryTools(server, deps);
425
428
  registerDocsMsgTools(server, { fetchImpl: deps.fetchImpl });
426
429
  // BLI-10430: docs_link, the Notion half of the docs tools, on the same path.
427
430
  registerDocsNotionTools(server, { fetchImpl: deps.fetchImpl });
@@ -435,6 +438,7 @@ export function createServer(deps, options = {}) {
435
438
  registerRenameTools(server, deps);
436
439
  // BLI-12385: GitHub PR read, and merge / retitle / re-run behind the two-step code.
437
440
  registerGithubTools(server, deps);
441
+ registerSetupTools(server, { fetchImpl: deps.fetchImpl });
438
442
  registerUsageTools(server, { fetchImpl: deps.fetchImpl });
439
443
  // BLI-3708: the mailboxes a person attached. Reads, a send and a sync over
440
444
  // the same /api/mail/** doors `cockpit mail` calls; `add-imap` deliberately
@@ -0,0 +1,4 @@
1
+ import { type ToolDeps } from "./tool-result.js";
2
+ export declare function registerSetupTools(server: {
3
+ registerTool: (...args: never[]) => unknown;
4
+ }, deps: ToolDeps): void;
@@ -0,0 +1,18 @@
1
+ import { z } from "zod";
2
+ import { callAgentDoor } from "./agent-door.js";
3
+ import { dataResult, doorFailureText, errorResult, registrarFor, withSession } from "./tool-result.js";
4
+ export function registerSetupTools(server, deps) {
5
+ registrarFor(server)("setup_read", {
6
+ title: "Compare agent setups and outcomes",
7
+ description: "Read machine setup names and 30-day merged PR volume, tokens per merged PR and session yield. Compare exact emails. Differences are observations, not evidence that a setting causes better results. Missing or stale setup and incomplete token coverage are labeled. Members see their own outcomes; team comparisons require super_admin.",
8
+ inputSchema: { person: z.string().max(200).optional().describe("Only this person, by exact email address."), compare: z.string().max(401).optional().describe("Two exact email addresses separated by a comma.") },
9
+ }, async (args) => withSession(deps, async (session) => {
10
+ const query = new URLSearchParams();
11
+ if (args.person)
12
+ query.set("person", String(args.person));
13
+ if (args.compare)
14
+ query.set("compare", String(args.compare));
15
+ const response = await callAgentDoor(session, deps.fetchImpl, "GET", `/api/setup?${query}`);
16
+ return response.ok ? dataResult("Agent setups and outcomes over the last 30 days.", response.body) : errorResult(doorFailureText("setup_read", response));
17
+ }));
18
+ }
@@ -109,6 +109,22 @@ export function registerTeamWriteTools(server, deps) {
109
109
  return errorResult(doorFailureText("team_role", response));
110
110
  return textResult(`${String(response.body.user_id ?? args.user_id)} is now ${String(response.body.role ?? args.role)}.`, response.body);
111
111
  }));
112
+ register("team_device_doctor", {
113
+ title: "Repair a Tower machine on its next tick",
114
+ description: "Requests one full non-interactive doctor run. Only super admins may request it. No arbitrary commands. The pending request and before-and-after receipt are visible in the device list.",
115
+ inputSchema: { device: z.string().min(1).max(200).describe("A device id or one exact device name.") },
116
+ }, async (args) => withSession(deps, async (session) => {
117
+ const listed = await listDevices(deps, session);
118
+ if (!listed.ok)
119
+ return errorResult(listed.text);
120
+ const device = findDevice(listed.devices, String(args.device ?? ""));
121
+ if (!device?.device_id)
122
+ return errorResult("No unique device matches. Call team_device_list to see ids and names.");
123
+ const response = await callAgentDoor(session, deps.fetchImpl, "POST", `/api/ambient/devices/${encodeURIComponent(device.device_id)}/doctor`, { action: "doctor" }, WRITE_DEADLINE_MS);
124
+ if (!response.ok)
125
+ return errorResult(doorFailureText("team_device_doctor", response));
126
+ return textResult(`Doctor requested for ${String(device.device_name ?? device.device_id)}. Waiting for its next check-in.`, response.body);
127
+ }));
112
128
  register("team_device_revoke", {
113
129
  title: "End a collector device's pairing",
114
130
  description: "Revokes one paired machine's token — `cockpit team device revoke`, same door. Irreversible for that token "
@@ -17,7 +17,7 @@ export interface TowerToolAnnotations {
17
17
  * Writes a person cannot take back by writing again: deletes, revokes,
18
18
  * detaches and a merge. These also carry `destructiveHint: true`.
19
19
  */
20
- export declare const DESTRUCTIVE_TOOLS: readonly ["docs_delete", "cal_delete", "cal_detach", "mail_detach", "notes_detach", "notes_rmdir", "settings_delete", "team_device_revoke", "github_merge"];
20
+ export declare const DESTRUCTIVE_TOOLS: readonly ["docs_delete", "cal_delete", "cal_detach", "mail_detach", "notes_detach", "notes_rmdir", "glossary_remove", "settings_delete", "team_device_revoke", "github_merge"];
21
21
  /**
22
22
  * Tools the HOSTED server leaves out, each with the reason. A hosted process
23
23
  * is one machine answering many people: a tool that reads that machine's
@@ -20,6 +20,7 @@ export const DESTRUCTIVE_TOOLS = [
20
20
  "mail_detach",
21
21
  "notes_detach",
22
22
  "notes_rmdir",
23
+ "glossary_remove",
23
24
  "settings_delete",
24
25
  "team_device_revoke",
25
26
  "github_merge",
@@ -10,7 +10,7 @@
10
10
  * from `src/` under Node's type stripping, so it may import nothing.
11
11
  */
12
12
  /** Tools that change nothing. The smoke calls every one. */
13
- export declare const READ_TOOLS: readonly ["get_ticket_timeline", "get_active_tickets", "docs_list", "docs_tree", "docs_read", "docs_sync_status", "msg_channels", "msg_read", "msg_thread", "work_list_issues", "work_get_issue", "work_issue_history", "work_list_projects", "careers_list", "careers_show", "careers_resume_url", "careers_takehome_show", "careers_settings_show", "rename_pending", "github_pr", "usage_people", "usage_sessions", "mail_accounts", "mail_inbox", "mail_read", "mail_search", "mail_attachment", "mail_sync_status", "cal_today", "cal_week", "cal_next", "cal_find", "cal_calendars", "cal_sync_status", "jarvis_ask", "jarvis_trace", "jarvis_check", "models_list", "models_card", "models_compare", "brief_read", "brief_history", "brief_status", "brief_missed", "notes_folders", "notes_list", "notes_show", "notes_shelf", "notes_shelves", "notes_place", "notes_accounts", "notes_sync_status", "notes_granola_status", "ops_status", "slack_coverage", "slack_read", "memory_profile", "memory_consolidation", "settings_show", "settings_list", "model_show", "team_members", "team_device_list", "scout_board", "workbook_read", "tower_search", "sources_check", "logs_digest", "brain_status"];
13
+ export declare const READ_TOOLS: readonly ["get_ticket_timeline", "get_active_tickets", "docs_list", "docs_tree", "docs_read", "docs_sync_status", "msg_channels", "msg_read", "msg_thread", "work_list_issues", "work_get_issue", "work_issue_history", "work_list_projects", "careers_list", "careers_show", "careers_resume_url", "careers_takehome_show", "careers_settings_show", "rename_pending", "github_pr", "setup_read", "usage_people", "usage_sessions", "mail_accounts", "mail_inbox", "mail_read", "mail_search", "mail_attachment", "mail_sync_status", "cal_today", "cal_week", "cal_next", "cal_find", "cal_calendars", "cal_sync_status", "jarvis_ask", "jarvis_trace", "jarvis_check", "models_list", "models_card", "models_compare", "brief_read", "brief_history", "brief_status", "brief_missed", "notes_folders", "notes_list", "glossary_list", "notes_show", "notes_shelf", "notes_shelves", "notes_place", "notes_accounts", "notes_recordings", "notes_recording", "notes_sync_status", "notes_granola_status", "ops_status", "slack_coverage", "slack_read", "memory_profile", "memory_consolidation", "settings_show", "settings_list", "model_show", "team_members", "team_device_list", "scout_board", "workbook_read", "tower_search", "sources_check", "logs_digest", "brain_status"];
14
14
  /**
15
15
  * Tools that write, but answer a dry run that writes nothing. The smoke calls
16
16
  * each one ONLY with its dry-run flag set (`scripts/smoke.ts` says which).
@@ -21,7 +21,7 @@ export declare const DRY_RUN_TOOLS: readonly ["ops_recompile", "memory_requeue"]
21
21
  * candidates, share calendars. Never called by the smoke; covered by the unit
22
22
  * suites (`write-tools.test.ts` and each family's own).
23
23
  */
24
- export declare const WRITE_TOOLS: readonly ["emit_event", "docs_create", "docs_update", "docs_delete", "docs_link", "msg_create_channel", "msg_dm", "msg_send", "work_create_issue", "work_update_issue", "work_move_issue", "work_comment_issue", "careers_rescreen", "careers_send_takehome", "careers_invite", "careers_grade", "careers_decide", "careers_followup", "careers_interview", "careers_takehome_set", "careers_settings_set", "rename_resolve", "github_merge", "github_retitle", "github_rerun", "mail_send", "mail_draft", "mail_sync", "mail_detach", "cal_create", "cal_share", "cal_sync", "cal_detach", "cal_delete", "jarvis_dispatch", "notes_mkdir", "notes_rmdir", "notes_rename", "notes_upload", "notes_paste", "notes_share", "notes_unshare", "notes_move", "notes_sync", "notes_detach", "memory_experience", "scout_start", "scout_dismiss", "scout_undo", "brief_rewrite", "brief_correct", "brief_done", "brief_undone", "settings_set", "settings_delete", "model_set", "team_invite", "team_role", "team_device_revoke"];
24
+ export declare const WRITE_TOOLS: readonly ["emit_event", "docs_create", "docs_update", "docs_delete", "docs_link", "msg_create_channel", "msg_dm", "msg_send", "work_create_issue", "work_update_issue", "work_move_issue", "work_comment_issue", "careers_rescreen", "careers_send_takehome", "careers_invite", "careers_grade", "careers_decide", "careers_followup", "careers_interview", "careers_takehome_set", "careers_settings_set", "rename_resolve", "github_merge", "github_retitle", "github_rerun", "mail_send", "mail_draft", "mail_sync", "mail_detach", "cal_create", "cal_share", "cal_sync", "cal_detach", "cal_delete", "jarvis_dispatch", "glossary_add", "glossary_remove", "notes_mkdir", "notes_rmdir", "notes_rename", "notes_upload", "notes_paste", "notes_share", "notes_unshare", "notes_move", "notes_sync", "notes_detach", "memory_experience", "scout_start", "scout_dismiss", "scout_undo", "brief_rewrite", "brief_correct", "brief_done", "brief_undone", "settings_set", "settings_delete", "model_set", "team_invite", "team_role", "team_device_revoke", "team_device_doctor"];
25
25
  /** Reads that spend a model turn. The smoke's `--no-model` leaves these out. */
26
26
  export declare const MODEL_SPENDING_TOOLS: readonly ["jarvis_ask", "jarvis_check"];
27
27
  /**
@@ -31,6 +31,7 @@ export const READ_TOOLS = [
31
31
  "careers_settings_show",
32
32
  "rename_pending",
33
33
  "github_pr",
34
+ "setup_read",
34
35
  "usage_people",
35
36
  "usage_sessions",
36
37
  "mail_accounts",
@@ -57,11 +58,15 @@ export const READ_TOOLS = [
57
58
  "brief_missed",
58
59
  "notes_folders",
59
60
  "notes_list",
61
+ "glossary_list",
60
62
  "notes_show",
61
63
  "notes_shelf",
62
64
  "notes_shelves",
63
65
  "notes_place",
64
66
  "notes_accounts",
67
+ // BLI-12926: the alternates of a meeting several notetakers recorded.
68
+ "notes_recordings",
69
+ "notes_recording",
65
70
  "notes_sync_status",
66
71
  "notes_granola_status",
67
72
  "ops_status",
@@ -130,6 +135,8 @@ export const WRITE_TOOLS = [
130
135
  "cal_detach",
131
136
  "cal_delete",
132
137
  "jarvis_dispatch",
138
+ "glossary_add",
139
+ "glossary_remove",
133
140
  "notes_mkdir",
134
141
  "notes_rmdir",
135
142
  "notes_rename",
@@ -154,6 +161,7 @@ export const WRITE_TOOLS = [
154
161
  "team_invite",
155
162
  "team_role",
156
163
  "team_device_revoke",
164
+ "team_device_doctor",
157
165
  ];
158
166
  /** Reads that spend a model turn. The smoke's `--no-model` leaves these out. */
159
167
  export const MODEL_SPENDING_TOOLS = ["jarvis_ask", "jarvis_check"];
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Census key → what a person actually types. (BLI-4048)
3
+ *
4
+ * The keys above are derived MECHANICALLY from each noun's `action` union, and
5
+ * for two nouns that union is not the grammar. `settings`'s positional is a
6
+ * SECTION (`personal|switches|models|env|cli-floor`) and the `action` is what
7
+ * the parser decides afterwards; `team`'s `device-list` is one action word
8
+ * spelled as two typed words, `team device list`. So `cockpit settings show`
9
+ * and `cockpit team device-list` were rendered into the README's "CLI verb"
10
+ * column and into `cli-caller-identity.md`, and the CLI refused all six.
11
+ *
12
+ * This is the display layer and nothing else: the keys, the twin rule and the
13
+ * suite are untouched. Every spelling here is proven typable —
14
+ * `scripts/doc-doors.test.mjs` runs the rendered table through the collector's
15
+ * own parser, so a wrong entry fails the build rather than misleading a
16
+ * stranger. Where one tool serves several sections (`settings_set` writes any
17
+ * of them) the spelling names ONE of them and the door column names the rest.
18
+ *
19
+ * A verb whose key is already typable is absent from here on purpose; the
20
+ * shorter this map is, the closer the census keys are to the product.
21
+ */
22
+ export declare const TYPED_SPELLING: Record<string, string>;
23
+ /** How a census key is written when a person is being told to type it. */
24
+ export declare function typedSpelling(verb: string): string;
@@ -0,0 +1,41 @@
1
+ /**
2
+ * Census key → what a person actually types. (BLI-4048)
3
+ *
4
+ * The keys above are derived MECHANICALLY from each noun's `action` union, and
5
+ * for two nouns that union is not the grammar. `settings`'s positional is a
6
+ * SECTION (`personal|switches|models|env|cli-floor`) and the `action` is what
7
+ * the parser decides afterwards; `team`'s `device-list` is one action word
8
+ * spelled as two typed words, `team device list`. So `cockpit settings show`
9
+ * and `cockpit team device-list` were rendered into the README's "CLI verb"
10
+ * column and into `cli-caller-identity.md`, and the CLI refused all six.
11
+ *
12
+ * This is the display layer and nothing else: the keys, the twin rule and the
13
+ * suite are untouched. Every spelling here is proven typable —
14
+ * `scripts/doc-doors.test.mjs` runs the rendered table through the collector's
15
+ * own parser, so a wrong entry fails the build rather than misleading a
16
+ * stranger. Where one tool serves several sections (`settings_set` writes any
17
+ * of them) the spelling names ONE of them and the door column names the rest.
18
+ *
19
+ * A verb whose key is already typable is absent from here on purpose; the
20
+ * shorter this map is, the closer the census keys are to the product.
21
+ */
22
+ export const TYPED_SPELLING = {
23
+ // The action is `invite`; the button, the help wall and the docs all say
24
+ // send-takehome, so that is what a stranger is told to type.
25
+ "careers invite": "careers send-takehome",
26
+ "careers takehome-show": "careers takehome show",
27
+ "careers takehome-set": "careers takehome set",
28
+ "careers settings-show": "careers settings show",
29
+ "careers settings-set": "careers settings set",
30
+ "settings show": "settings",
31
+ "settings list": "settings env list",
32
+ "settings set": "settings switches set",
33
+ "settings delete": "settings env delete",
34
+ "team device-list": "team device list",
35
+ "team device-revoke": "team device revoke",
36
+ "team device-doctor": "team device doctor",
37
+ };
38
+ /** How a census key is written when a person is being told to type it. */
39
+ export function typedSpelling(verb) {
40
+ return TYPED_SPELLING[verb] ?? verb;
41
+ }
@@ -15,9 +15,8 @@
15
15
  * re-exports. That file is the authoritative "these are the commands that
16
16
  * talk to Tower" table — its siblings are a filing decision, not a contract.
17
17
  * - The VERBS are each noun's `action:` union in `LocalCommand`
18
- * (`local-args.ts`), with an `XAction` alias resolved from whichever
19
- * `local-args-tower-*.ts` declares it. A noun with no `action` field is one
20
- * verb wearing the noun's own name (`jarvis`, `correct`, `workbook`).
18
+ * (`local-args.ts`), resolving XAction aliases from the tower parser siblings.
19
+ * A noun without an action field is its own verb.
21
20
  *
22
21
  * ## What it deliberately does NOT see
23
22
  *
@@ -67,30 +66,7 @@ export interface McpTwin {
67
66
  }
68
67
  /** A CLI verb and the MCP tool that does the same thing, over the same door. */
69
68
  export declare const MCP_TWINS: Record<string, McpTwin>;
70
- /**
71
- * Census key → what a person actually types. (BLI-4048)
72
- *
73
- * The keys above are derived MECHANICALLY from each noun's `action` union, and
74
- * for two nouns that union is not the grammar. `settings`'s positional is a
75
- * SECTION (`personal|switches|models|env|cli-floor`) and the `action` is what
76
- * the parser decides afterwards; `team`'s `device-list` is one action word
77
- * spelled as two typed words, `team device list`. So `cockpit settings show`
78
- * and `cockpit team device-list` were rendered into the README's "CLI verb"
79
- * column and into `cli-caller-identity.md`, and the CLI refused all six.
80
- *
81
- * This is the display layer and nothing else: the keys, the twin rule and the
82
- * suite are untouched. Every spelling here is proven typable —
83
- * `scripts/doc-doors.test.mjs` runs the rendered table through the collector's
84
- * own parser, so a wrong entry fails the build rather than misleading a
85
- * stranger. Where one tool serves several sections (`settings_set` writes any
86
- * of them) the spelling names ONE of them and the door column names the rest.
87
- *
88
- * A verb whose key is already typable is absent from here on purpose; the
89
- * shorter this map is, the closer the census keys are to the product.
90
- */
91
- export declare const TYPED_SPELLING: Record<string, string>;
92
- /** How a census key is written when a person is being told to type it. */
93
- export declare function typedSpelling(verb: string): string;
69
+ export { TYPED_SPELLING, typedSpelling } from "./verb-census-spellings.js";
94
70
  /** Verbs that can never have an MCP twin, and why. A claim, not a backlog. */
95
71
  export declare const TERMINAL_ONLY: Record<string, string>;
96
72
  /**
@@ -15,9 +15,8 @@
15
15
  * re-exports. That file is the authoritative "these are the commands that
16
16
  * talk to Tower" table — its siblings are a filing decision, not a contract.
17
17
  * - The VERBS are each noun's `action:` union in `LocalCommand`
18
- * (`local-args.ts`), with an `XAction` alias resolved from whichever
19
- * `local-args-tower-*.ts` declares it. A noun with no `action` field is one
20
- * verb wearing the noun's own name (`jarvis`, `correct`, `workbook`).
18
+ * (`local-args.ts`), resolving XAction aliases from the tower parser siblings.
19
+ * A noun without an action field is its own verb.
21
20
  *
22
21
  * ## What it deliberately does NOT see
23
22
  *
@@ -30,6 +29,7 @@
30
29
  import { readFileSync, readdirSync } from "node:fs";
31
30
  import { dirname, join, resolve } from "node:path";
32
31
  import { fileURLToPath } from "node:url";
32
+ import { MEMORY_STORE_TWINS } from "./memory-twins.js";
33
33
  const HERE = dirname(fileURLToPath(import.meta.url));
34
34
  export const COLLECTOR_COMMANDS_DIR = resolve(HERE, "../../cockpit-local-collector/src/commands");
35
35
  function read(file) {
@@ -59,10 +59,6 @@ function actionAliases() {
59
59
  }
60
60
  return aliases;
61
61
  }
62
- /**
63
- * The `LocalCommand` variants, split the way the union is written: one chunk
64
- * per ` | {` at the top level of the type.
65
- */
66
62
  /**
67
63
  * The `LocalCommand` union, wherever it currently lives.
68
64
  *
@@ -245,7 +241,11 @@ export const MCP_TWINS = {
245
241
  "logs": { tool: "logs_digest", door: "LOCAL @bli-cockpit/telemetry-core/log-digest (this machine's logs); `project` reads GET /api/logs (the stored digests)" },
246
242
  "brain status": { tool: "brain_status", door: "GET /api/brain/incidents" },
247
243
  "usage sessions": { tool: "usage_sessions", door: "GET /api/usage/sessions" },
244
+ "setup": { tool: "setup_read", door: "GET /api/setup" },
248
245
  "usage people": { tool: "usage_people", door: "GET /api/usage/people" },
246
+ "glossary list": { tool: "glossary_list", door: "GET /api/glossary" },
247
+ "glossary add": { tool: "glossary_add", door: "POST /api/glossary" },
248
+ "glossary remove": { tool: "glossary_remove", door: "POST /api/glossary" },
249
249
  "docs list": { tool: "docs_list", door: "GET /api/docs/documents" },
250
250
  "docs tree": { tool: "docs_tree", door: "GET /api/docs/tree" },
251
251
  "docs read": { tool: "docs_read", door: "GET /api/docs/documents/[id]" },
@@ -313,21 +313,19 @@ export const MCP_TWINS = {
313
313
  "notes unshare": { tool: "notes_unshare", door: "POST /api/notes/share (share=false)" },
314
314
  "notes move": { tool: "notes_move", door: "POST /api/notes/move" },
315
315
  "notes place": { tool: "notes_place", door: "POST /api/notes/place" },
316
- // BLI-4383. `notes connect` is in TERMINAL_ONLY below, for the same reason
317
- // `mail add-imap` and `cal add-ical` are.
316
+ // BLI-4383. `notes connect` is in TERMINAL_ONLY below, like `mail add-imap` and `cal add-ical`.
318
317
  "notes accounts": { tool: "notes_accounts", door: "GET /api/notes/accounts" },
318
+ "notes recordings": { tool: "notes_recordings", door: "GET /api/notes/recordings?note_id" },
319
+ "notes recording": { tool: "notes_recording", door: "GET /api/notes/recordings?recording_id" },
319
320
  "notes detach": { tool: "notes_detach", door: "DELETE /api/notes/accounts" },
320
321
  "notes sync": { tool: "notes_sync", door: "POST /api/notes/accounts/[id]/sync" },
321
322
  "notes sync-status": { tool: "notes_sync_status", door: "GET /api/notes/sync-status" },
322
323
  // Ticket 10718: Granola's own cron health, over its shared server-side key.
323
324
  "notes granola-status": { tool: "notes_granola_status", door: "GET /api/notes/granola-status" },
324
325
  "memory log": { tool: "memory_experience", door: "POST /api/memory/experience" },
325
- // BLI-4047: the memory STORE at a terminal. The twins live on the second
326
- // server, `bli-memory`, over the same four doors these CLI verbs call.
327
- "memory search": { tool: "search_memory", door: "POST /api/memory/search", server: "bli-memory" },
328
- "memory save": { tool: "save_memory", door: "POST /api/memory/save", server: "bli-memory" },
329
- "memory update": { tool: "update_memory", door: "POST /api/memory/update", server: "bli-memory" },
330
- "memory forget": { tool: "forget_memory", door: "POST /api/memory/forget", server: "bli-memory" },
326
+ // BLI-4047, BLI-12927: the memory STORE at a terminal. Its twins live on the
327
+ // second server, `bli-memory` (see `memory-twins.ts`).
328
+ ...MEMORY_STORE_TWINS,
331
329
  // BLI-4533: the compiled one-page profile. Its twin is on `bli-tower`, not
332
330
  // `bli-memory`: that server's four tools are the vendor-compatible set the
333
331
  // Claude Code plugin expects, and this page is ours.
@@ -352,6 +350,7 @@ export const MCP_TWINS = {
352
350
  "team device-list": { tool: "team_device_list", door: "GET /api/team/devices" },
353
351
  "team invite": { tool: "team_invite", door: "POST /api/team/invite" },
354
352
  "team role": { tool: "team_role", door: "PATCH /api/team/members/[userId]/role" },
353
+ "team device-doctor": { tool: "team_device_doctor", door: "POST /api/ambient/devices/[deviceId]/doctor" },
355
354
  "team device-revoke": { tool: "team_device_revoke", door: "POST /api/ambient/devices/[deviceId]/revoke" },
356
355
  "model show": { tool: "model_show", door: "GET /api/settings/jarvis-model" },
357
356
  "model set": { tool: "model_set", door: "POST /api/settings/jarvis-model" },
@@ -376,52 +375,14 @@ export const MCP_TWINS = {
376
375
  // header of this file).
377
376
  "jarvis --trace": { tool: "jarvis_trace", door: "GET /api/ops/trace/[id]" },
378
377
  };
379
- /**
380
- * Census key → what a person actually types. (BLI-4048)
381
- *
382
- * The keys above are derived MECHANICALLY from each noun's `action` union, and
383
- * for two nouns that union is not the grammar. `settings`'s positional is a
384
- * SECTION (`personal|switches|models|env|cli-floor`) and the `action` is what
385
- * the parser decides afterwards; `team`'s `device-list` is one action word
386
- * spelled as two typed words, `team device list`. So `cockpit settings show`
387
- * and `cockpit team device-list` were rendered into the README's "CLI verb"
388
- * column and into `cli-caller-identity.md`, and the CLI refused all six.
389
- *
390
- * This is the display layer and nothing else: the keys, the twin rule and the
391
- * suite are untouched. Every spelling here is proven typable —
392
- * `scripts/doc-doors.test.mjs` runs the rendered table through the collector's
393
- * own parser, so a wrong entry fails the build rather than misleading a
394
- * stranger. Where one tool serves several sections (`settings_set` writes any
395
- * of them) the spelling names ONE of them and the door column names the rest.
396
- *
397
- * A verb whose key is already typable is absent from here on purpose; the
398
- * shorter this map is, the closer the census keys are to the product.
399
- */
400
- export const TYPED_SPELLING = {
401
- // The action is `invite`; the button, the help wall and the docs all say
402
- // send-takehome, so that is what a stranger is told to type.
403
- "careers invite": "careers send-takehome",
404
- "careers takehome-show": "careers takehome show",
405
- "careers takehome-set": "careers takehome set",
406
- "careers settings-show": "careers settings show",
407
- "careers settings-set": "careers settings set",
408
- "settings show": "settings",
409
- "settings list": "settings env list",
410
- "settings set": "settings switches set",
411
- "settings delete": "settings env delete",
412
- "team device-list": "team device list",
413
- "team device-revoke": "team device revoke",
414
- };
415
- /** How a census key is written when a person is being told to type it. */
416
- export function typedSpelling(verb) {
417
- return TYPED_SPELLING[verb] ?? verb;
418
- }
378
+ // Human command grammar is separate from the mechanical twin census.
379
+ export { TYPED_SPELLING, typedSpelling } from "./verb-census-spellings.js";
419
380
  /** Verbs that can never have an MCP twin, and why. A claim, not a backlog. */
420
381
  export const TERMINAL_ONLY = {
421
382
  "mail add-imap": "carries a Google app password. The CLI reads it from STDIN precisely so it never lands in an argument list; an MCP tool argument travels through a model's context window and whatever transcript store sits behind it, so attaching a mailbox stays a thing a person does at a terminal (BLI-3708)",
422
383
  "cal add-ical": "carries a calendar's SECRET iCal ADDRESS — a permanent, unauthenticated, read-anything-on-that-calendar URL. The CLI reads it from STDIN precisely so it never lands in an argument list; an MCP tool argument travels through a model's context window and whatever transcript store sits behind it, so attaching a calendar stays a thing a person does at a terminal (BLI-3709)",
423
384
  "brief edit": "opens the person's own $EDITOR on this machine and files what they changed; an agent has no editor to open (commands/editor.ts)",
424
- "notes connect": "carries a notetaker API key (Fellow or Circleback). The CLI reads it from STDIN precisely so it never lands in an argument list; an MCP tool argument travels through a model's context window and whatever transcript store sits behind it, so connecting a notetaker stays a thing a person does at a terminal or in Tower's Settings (BLI-4383)",
385
+ "notes connect": "carries a notetaker credential: an API key (Fellow or Circleback), or for Wispr Flow a browser sign-in whose refresh token the CLI hands straight to Tower. The CLI reads a key from STDIN precisely so it never lands in an argument list, and a browser sign-in has no tool-call equivalent at all; an MCP tool argument travels through a model's context window and whatever transcript store sits behind it, so connecting a notetaker stays a thing a person does at a terminal or in Tower's Settings (BLI-4383, BLI-12926)",
425
386
  "notes webhook": "carries the VENDOR's webhook signing secret, the key an HMAC over every delivered payload is checked against. Exactly the ruling `notes connect` already carries, for the same credential shape: the CLI reads it from STDIN so it never lands in an argument list, and an MCP tool argument travels through a model's context window and whatever transcript store sits behind it (BLI-4394)",
426
387
  };
427
388
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bli-cockpit/mcp",
3
- "version": "0.1.95",
3
+ "version": "0.1.97",
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",
@@ -37,7 +37,7 @@
37
37
  "readme": "npm run build && node scripts/write-readme-census.mjs"
38
38
  },
39
39
  "dependencies": {
40
- "@bli-cockpit/telemetry-core": "0.1.65",
40
+ "@bli-cockpit/telemetry-core": "0.1.66",
41
41
  "@modelcontextprotocol/sdk": "^1.29.0",
42
42
  "zod": "^4.3.6"
43
43
  },