@bli-cockpit/mcp 0.1.122 → 0.1.125

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
- **187 of 192 Tower verbs have an MCP twin.**
112
+ **188 of 193 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
@@ -293,6 +293,7 @@ three tables below.
293
293
  | `cockpit team device doctor` | `team_device_doctor` | `POST /api/ambient/devices/[deviceId]/doctor` |
294
294
  | `cockpit team device list` | `team_device_list` | `GET /api/team/devices` |
295
295
  | `cockpit team device revoke` | `team_device_revoke` | `POST /api/ambient/devices/[deviceId]/revoke` |
296
+ | `cockpit team device scope` | `team_device_scope` | `POST /api/ambient/devices/[deviceId]/scope` |
296
297
  | `cockpit team github-login add` | `team_github_login_add` | `POST /api/team/members/[userId]/github-logins` |
297
298
  | `cockpit team github-login list` | `team_github_login_list` | `GET /api/team/members/[userId]/github-logins` |
298
299
  | `cockpit team github-login remove` | `team_github_login_remove` | `DELETE /api/team/members/[userId]/github-logins` |
@@ -320,7 +321,7 @@ A claim about the verb's nature, not a backlog.
320
321
  | `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) |
321
322
  | `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) |
322
323
  | `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) |
323
- | `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) |
324
+ | `cockpit notes connect` | an API-key connect (Fellow or Circleback) carries a notetaker credential. The CLI reads the key from STDIN precisely 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, so connecting a key stays a thing a person does at a terminal or in Tower's Settings (BLI-4383). Wispr Flow carries no credential through a call: its sign-in is a link the person clicks, started and watched over the same doors by the MCP tools notes_connect_wisprflow and notes_connect_status (BLI-17973) |
324
325
  | `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) |
325
326
 
326
327
  <!-- END GENERATED verb census -->
@@ -0,0 +1,10 @@
1
+ /**
2
+ * The device-scope twin in the verb census (BLI-17978).
3
+ *
4
+ * `cockpit team device scope <device> notetaker_bot|none` and `team_device_scope` both ride
5
+ * `POST /api/ambient/devices/[deviceId]/scope`, the one door that gives the meeting bot's device the
6
+ * scope Tower's claim door requires (or takes it away). Split out of `verb-census.ts`, which sits at
7
+ * the file-size ratchet.
8
+ */
9
+ import type { McpTwin } from "./verb-census.js";
10
+ export declare const DEVICE_TWINS: Record<string, McpTwin>;
@@ -0,0 +1,11 @@
1
+ /**
2
+ * The device-scope twin in the verb census (BLI-17978).
3
+ *
4
+ * `cockpit team device scope <device> notetaker_bot|none` and `team_device_scope` both ride
5
+ * `POST /api/ambient/devices/[deviceId]/scope`, the one door that gives the meeting bot's device the
6
+ * scope Tower's claim door requires (or takes it away). Split out of `verb-census.ts`, which sits at
7
+ * the file-size ratchet.
8
+ */
9
+ export const DEVICE_TWINS = {
10
+ "team device-scope": { tool: "team_device_scope", door: "POST /api/ambient/devices/[deviceId]/scope" },
11
+ };
@@ -5,13 +5,18 @@
5
5
  * authenticated with this machine's collector device token.
6
6
  *
7
7
  * ONE DELIBERATE ASYMMETRY WITH THE CLI, recorded in `verb-census.ts`:
8
- * **`cockpit notes connect` has NO MCP twin.** It carries a notetaker API key,
9
- * and the CLI takes that on stdin precisely so it never lands in an argument
10
- * list. An MCP tool argument IS an argument list: it travels through a model's
11
- * context window and whatever transcript store sits behind it. That is the
12
- * same ruling `mail add-imap` and `cal add-ical` already carry, for the same
13
- * credential shape, and connecting a notetaker stays a thing a person does at
14
- * a terminal or in Settings.
8
+ * **an API-key `cockpit notes connect` has NO MCP twin.** It carries a
9
+ * notetaker API key, and the CLI takes that on stdin precisely so it never
10
+ * lands in an argument list. An MCP tool argument IS an argument list: it
11
+ * travels through a model's context window and whatever transcript store sits
12
+ * behind it. That is the same ruling `mail add-imap` and `cal add-ical`
13
+ * already carry, for the same credential shape.
14
+ *
15
+ * Wispr Flow is the exception that proves it (BLI-17973): its sign-in carries
16
+ * no credential through a tool call. `notes_connect_wisprflow` starts it over
17
+ * the same `/api/notes/accounts/wisprflow/start` door the Settings button and
18
+ * `cockpit notes connect --provider wisprflow` call and returns a link for the
19
+ * PERSON to click; `notes_connect_status` polls the same status door.
15
20
  *
16
21
  * Same session discipline as `mail-tools.ts`: the session is loaded fresh per
17
22
  * call, so a machine with no `cockpit login` pairing still serves this
@@ -5,13 +5,18 @@
5
5
  * authenticated with this machine's collector device token.
6
6
  *
7
7
  * ONE DELIBERATE ASYMMETRY WITH THE CLI, recorded in `verb-census.ts`:
8
- * **`cockpit notes connect` has NO MCP twin.** It carries a notetaker API key,
9
- * and the CLI takes that on stdin precisely so it never lands in an argument
10
- * list. An MCP tool argument IS an argument list: it travels through a model's
11
- * context window and whatever transcript store sits behind it. That is the
12
- * same ruling `mail add-imap` and `cal add-ical` already carry, for the same
13
- * credential shape, and connecting a notetaker stays a thing a person does at
14
- * a terminal or in Settings.
8
+ * **an API-key `cockpit notes connect` has NO MCP twin.** It carries a
9
+ * notetaker API key, and the CLI takes that on stdin precisely so it never
10
+ * lands in an argument list. An MCP tool argument IS an argument list: it
11
+ * travels through a model's context window and whatever transcript store sits
12
+ * behind it. That is the same ruling `mail add-imap` and `cal add-ical`
13
+ * already carry, for the same credential shape.
14
+ *
15
+ * Wispr Flow is the exception that proves it (BLI-17973): its sign-in carries
16
+ * no credential through a tool call. `notes_connect_wisprflow` starts it over
17
+ * the same `/api/notes/accounts/wisprflow/start` door the Settings button and
18
+ * `cockpit notes connect --provider wisprflow` call and returns a link for the
19
+ * PERSON to click; `notes_connect_status` polls the same status door.
15
20
  *
16
21
  * Same session discipline as `mail-tools.ts`: the session is loaded fresh per
17
22
  * call, so a machine with no `cockpit login` pairing still serves this
@@ -64,9 +69,9 @@ export function registerNotesAccountTools(server, deps) {
64
69
  description: "Every notetaker account (Fellow, Circleback or Wispr Flow; Otter and Granola are named but not connectable) "
65
70
  + "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
71
  + "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 "
72
+ + "their notes, and which members have none (BLI-12926). Connecting a Fellow or Circleback key is NOT available here on purpose — it "
68
73
  + "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.",
74
+ + "<subdomain> --key-stdin` or in Tower's Settings. Wispr Flow signs in with a link instead: notes_connect_wisprflow.",
70
75
  inputSchema: {},
71
76
  }, async () => withSession(deps, async (session) => {
72
77
  const response = await callAgentDoor(session, deps.fetchImpl, "GET", "/api/notes/accounts");
@@ -155,10 +160,40 @@ export function registerNotesAccountTools(server, deps) {
155
160
  accounts,
156
161
  });
157
162
  }));
163
+ register("notes_connect_wisprflow", {
164
+ title: "Start connecting Wispr Flow",
165
+ description: "Starts signing in to Wispr Flow, the notetaker with no API key. Answers `authorize_url`: give it to the person as a link to click "
166
+ + "(they approve on Wispr Flow's page, signed in with the Wispr account that holds their notes, using Google, Apple, Microsoft or company single sign-on) "
167
+ + "and `attempt_id`, then call notes_connect_status with it to see whether it connected. It is the same door the Settings button and "
168
+ + "`cockpit notes connect --provider wisprflow` call. Carries no credential: the link is one-time and the verifier stays on Tower's server. "
169
+ + "Refuses with account_already_connected when Wispr Flow is already connected (detach it first).",
170
+ inputSchema: {},
171
+ }, async () => withSession(deps, async (session) => {
172
+ const response = await callAgentDoor(session, deps.fetchImpl, "POST", "/api/notes/accounts/wisprflow/start", {});
173
+ if (!response.ok)
174
+ return errorResult(doorFailureText("notes_connect_wisprflow", response));
175
+ const url = String(response.body.authorize_url ?? "");
176
+ return textResult(`Open this link in a browser and approve on Wispr Flow's page, then check with notes_connect_status (attempt_id ${String(response.body.attempt_id)}), `
177
+ + `expires ${String(response.body.expires_at)}:\n${url}`, response.body);
178
+ }));
179
+ register("notes_connect_status", {
180
+ title: "Check a Wispr Flow sign-in",
181
+ description: "Where a Wispr Flow sign-in stands: pending (still waiting for the click), connected (with the account id), failed (with a reason label) or expired. "
182
+ + "Pass the attempt_id notes_connect_wisprflow returned. Only the person who started the attempt can read it.",
183
+ inputSchema: { attempt_id: z.string().uuid().describe("The attempt_id notes_connect_wisprflow returned.") },
184
+ }, async (args) => withSession(deps, async (session) => {
185
+ const response = await callAgentDoor(session, deps.fetchImpl, "GET", `/api/notes/accounts/wisprflow/status?attempt_id=${encodeURIComponent(String(args.attempt_id))}`);
186
+ if (!response.ok)
187
+ return errorResult(doorFailureText("notes_connect_status", response));
188
+ const reason = response.body.reason ? ` (${String(response.body.reason)})` : "";
189
+ return textResult(`Wispr Flow sign-in ${String(response.body.status)}${reason}.`, response.body);
190
+ }));
158
191
  register("notes_sync", {
159
192
  title: "Read a notetaker now",
160
193
  description: "Reads the meetings this notetaker has recorded since the last pass and files them as meeting notes. "
161
- + "Answers with the run's own counts and reason label, which is exactly what the ledger row says. "
194
+ + "One call is a bounded pass (under a minute): it answers with the run id, the run's own counts and reason label "
195
+ + "(exactly what the ledger row says), and `more`. While `more` is true, call notes_sync again; each call resumes "
196
+ + "from the saved cursor, and `done` means the account is caught up. "
162
197
  + "The half-hourly cron does this unasked; this is the button for when you do not want to wait.",
163
198
  inputSchema: { account_id: z.string().uuid().describe("A notetaker account id (notes_accounts).") },
164
199
  }, async (args) => withSession(deps, async (session) => {
@@ -168,6 +203,10 @@ export function registerNotesAccountTools(server, deps) {
168
203
  const run = response.body.run;
169
204
  return textResult(run
170
205
  ? `${run.reason}: ${run.notesAdded} stored, ${run.notesUpdated} unchanged, ${run.notesSkipped} skipped. ${run.detail}`
206
+ + (run.runId ? ` Run ${run.runId}.` : "")
207
+ + (run.ok && run.more
208
+ ? ` More is waiting${typeof run.remaining === "number" ? ` (${run.remaining})` : ""}: call notes_sync again to continue from the saved cursor.`
209
+ : run.ok ? " Caught up." : "")
171
210
  : "Tower answered without a run.", response.body);
172
211
  }));
173
212
  register("notes_detach", {
@@ -1,5 +1,5 @@
1
1
  /**
2
- * `team_invite` / `team_role` / `team_device_revoke` MCP tools (BLI-3756 batch
2
+ * `team_invite` / `team_role` / `team_device_revoke` (and `team_device_scope`, BLI-17978) MCP tools (BLI-3756 batch
3
3
  * 2) — the three things an admin does to the team, over the same `/api/team/*`
4
4
  * and `/api/ambient/devices/*` doors `cockpit team` calls with the same device
5
5
  * token.
@@ -1,5 +1,5 @@
1
1
  /**
2
- * `team_invite` / `team_role` / `team_device_revoke` MCP tools (BLI-3756 batch
2
+ * `team_invite` / `team_role` / `team_device_revoke` (and `team_device_scope`, BLI-17978) MCP tools (BLI-3756 batch
3
3
  * 2) — the three things an admin does to the team, over the same `/api/team/*`
4
4
  * and `/api/ambient/devices/*` doors `cockpit team` calls with the same device
5
5
  * token.
@@ -167,6 +167,28 @@ export function registerTeamWriteTools(server, deps) {
167
167
  return errorResult(doorFailureText("team_device_doctor", response));
168
168
  return textResult(`Doctor requested for ${String(device.device_name ?? device.device_id)}. Waiting for its next check-in.`, response.body);
169
169
  }));
170
+ register("team_device_scope", {
171
+ title: "Give the meeting bot's device its scope, or take it away",
172
+ description: "Grants (`notetaker_bot`) or revokes (`none`) a device's service scope, the same door as `cockpit team device scope`. "
173
+ + "Without `notetaker_bot` Tower refuses every meeting claim from the bot (bot_scope_required). Super admin only, decided on the server.",
174
+ inputSchema: {
175
+ device: z.string().min(1).max(200).describe("A device id or one exact device name."),
176
+ scope: z.enum(["notetaker_bot", "none"]).describe("`notetaker_bot` to grant, `none` to revoke."),
177
+ },
178
+ }, async (args) => withSession(deps, async (session) => {
179
+ const listed = await listDevices(deps, session);
180
+ if (!listed.ok)
181
+ return errorResult(listed.text);
182
+ const device = findDevice(listed.devices, String(args.device ?? ""));
183
+ if (!device?.device_id)
184
+ return errorResult("No unique device matches. Call team_device_list to see ids and names. Nothing was changed.");
185
+ const response = await callAgentDoor(session, deps.fetchImpl, "POST", `/api/ambient/devices/${encodeURIComponent(device.device_id)}/scope`, { scope: String(args.scope ?? "") }, WRITE_DEADLINE_MS);
186
+ if (!response.ok)
187
+ return errorResult(doorFailureText("team_device_scope", response));
188
+ const name = String(device.device_name ?? device.device_id);
189
+ return textResult(response.body.changed === false ? `${name} already had that scope. Nothing changed.`
190
+ : args.scope === "none" ? `Took the service scope away from ${name}.` : `${name} now holds the notetaker_bot scope.`, response.body);
191
+ }));
170
192
  register("team_device_revoke", {
171
193
  title: "End a collector device's pairing",
172
194
  description: "Revokes one paired machine's token — `cockpit team device revoke`, same door. Irreversible for that token "
@@ -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_board", "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", "speed_read", "speed_seen", "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", "notes_bot_upcoming", "ops_status", "slack_coverage", "slack_connect", "slack_disconnect", "slack_read", "memory_map", "memory_rules", "memory_knowledge", "memory_profile", "memory_consolidation", "memory_chain", "settings_show", "settings_list", "model_show", "team_members", "team_github_login_add", "team_github_login_remove", "team_github_login_list", "team_device_list", "scout_board", "workbook_read", "tower_search", "sources_check", "logs_digest", "brain_status", "approvals_list", "approvals_show", "approvals_rules", "manager_clients", "manager_client", "manager_run", "manager_list", "manager_queue", "manager_show", "image_show"];
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_board", "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", "speed_read", "speed_seen", "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_connect_status", "notes_recordings", "notes_recording", "notes_sync_status", "notes_granola_status", "notes_bot_upcoming", "ops_status", "slack_coverage", "slack_connect", "slack_disconnect", "slack_read", "memory_map", "memory_rules", "memory_knowledge", "memory_profile", "memory_consolidation", "memory_chain", "settings_show", "settings_list", "model_show", "team_members", "team_github_login_add", "team_github_login_remove", "team_github_login_list", "team_device_list", "scout_board", "workbook_read", "tower_search", "sources_check", "logs_digest", "brain_status", "approvals_list", "approvals_show", "approvals_rules", "manager_clients", "manager_client", "manager_run", "manager_list", "manager_queue", "manager_show", "image_show"];
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 ["image_describe", "approvals_create", "approvals_approve", "approvals_edit", "approvals_reject", "approvals_rules_create", "approvals_rules_revoke", "manager_add", "manager_edit", "manager_done", "manager_reopen", "manager_link", "manager_approve", "manager_dismiss", "manager_proposal", "manager_archive", "manager_unarchive", "logs_key_mint", "brain_retry", "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_rename", "mail_enable", "mail_disable", "mail_default", "mail_remove", "cal_change", "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_grant", "team_revoke", "team_device_revoke", "team_device_doctor"];
24
+ export declare const WRITE_TOOLS: readonly ["image_describe", "approvals_create", "approvals_approve", "approvals_edit", "approvals_reject", "approvals_rules_create", "approvals_rules_revoke", "manager_add", "manager_edit", "manager_done", "manager_reopen", "manager_link", "manager_approve", "manager_dismiss", "manager_proposal", "manager_archive", "manager_unarchive", "logs_key_mint", "brain_retry", "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_rename", "mail_enable", "mail_disable", "mail_default", "mail_remove", "cal_change", "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_connect_wisprflow", "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_grant", "team_revoke", "team_device_revoke", "team_device_doctor", "team_device_scope"];
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
  /**
@@ -67,6 +67,8 @@ export const READ_TOOLS = [
67
67
  "notes_shelves",
68
68
  "notes_place",
69
69
  "notes_accounts",
70
+ // BLI-17973: where a Wispr Flow sign-in stands (its start is a write, below).
71
+ "notes_connect_status",
70
72
  // BLI-12926: the alternates of a meeting several notetakers recorded.
71
73
  "notes_recordings",
72
74
  "notes_recording",
@@ -195,6 +197,8 @@ export const WRITE_TOOLS = [
195
197
  "notes_share",
196
198
  "notes_unshare",
197
199
  "notes_move",
200
+ // BLI-17973: registers a client at Wispr and records an attempt, so never smoked.
201
+ "notes_connect_wisprflow",
198
202
  "notes_sync",
199
203
  "notes_detach",
200
204
  "memory_experience",
@@ -214,6 +218,7 @@ export const WRITE_TOOLS = [
214
218
  "team_revoke",
215
219
  "team_device_revoke",
216
220
  "team_device_doctor",
221
+ "team_device_scope",
217
222
  ];
218
223
  /** Reads that spend a model turn. The smoke's `--no-model` leaves these out. */
219
224
  export const MODEL_SPENDING_TOOLS = ["jarvis_ask", "jarvis_check"];
@@ -39,6 +39,7 @@ export const TYPED_SPELLING = {
39
39
  "team device-list": "team device list",
40
40
  "team device-revoke": "team device revoke",
41
41
  "team device-doctor": "team device doctor",
42
+ "team device-scope": "team device scope",
42
43
  };
43
44
  /** How a census key is written when a person is being told to type it. */
44
45
  export function typedSpelling(verb) {
@@ -29,6 +29,7 @@
29
29
  import { readFileSync, readdirSync } from "node:fs";
30
30
  import { dirname, join, resolve } from "node:path";
31
31
  import { fileURLToPath } from "node:url";
32
+ import { DEVICE_TWINS } from "./device-twins.js";
32
33
  import { MANAGER_TWINS } from "./manager-twins.js";
33
34
  import { IMAGE_TWINS, MEMORY_STORE_TWINS } from "./memory-twins.js";
34
35
  const HERE = dirname(fileURLToPath(import.meta.url));
@@ -350,8 +351,8 @@ export const MCP_TWINS = {
350
351
  "notes bot-upcoming": { tool: "notes_bot_upcoming", door: "GET /api/notetaker/upcoming" },
351
352
  "notes granola-status": { tool: "notes_granola_status", door: "GET /api/notes/granola-status" },
352
353
  "memory log": { tool: "memory_experience", door: "POST /api/memory/experience" },
353
- // BLI-4047, BLI-12927: the memory STORE (twins on the `bli-memory` server) and images: `memory-twins.ts`; BLI-5006: the Manager: `manager-twins.ts`.
354
- ...MEMORY_STORE_TWINS, ...IMAGE_TWINS, ...MANAGER_TWINS,
354
+ // BLI-4047, BLI-12927: the memory STORE (twins on the `bli-memory` server) and images: `memory-twins.ts`; BLI-5006: the Manager: `manager-twins.ts`; BLI-17978: the bot scope: `device-twins.ts`.
355
+ ...MEMORY_STORE_TWINS, ...IMAGE_TWINS, ...MANAGER_TWINS, ...DEVICE_TWINS,
355
356
  // BLI-4533: the compiled one-page profile. Its twin is on `bli-tower`, not
356
357
  // `bli-memory`: that server's four tools are the vendor-compatible set the
357
358
  // Claude Code plugin expects, and this page is ours.
@@ -410,8 +411,7 @@ export const MCP_TWINS = {
410
411
  // are.
411
412
  search: { tool: "tower_search", door: "GET /api/search" },
412
413
  jarvis: { tool: "jarvis_ask", door: "POST /api/jarvis/cli" },
413
- // Entered by hand: a mode flag, not an `action` the parser names (see the
414
- // header of this file).
414
+ // Entered by hand: a mode flag, not an `action` the parser names (see the header of this file).
415
415
  "jarvis --trace": { tool: "jarvis_trace", door: "GET /api/ops/trace/[id]" },
416
416
  };
417
417
  // Human command grammar is separate from the mechanical twin census.
@@ -421,7 +421,7 @@ export const TERMINAL_ONLY = {
421
421
  "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
422
  "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
423
  "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 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)",
424
+ "notes connect": "an API-key connect (Fellow or Circleback) carries a notetaker credential. The CLI reads the key from STDIN precisely 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, so connecting a key stays a thing a person does at a terminal or in Tower's Settings (BLI-4383). Wispr Flow carries no credential through a call: its sign-in is a link the person clicks, started and watched over the same doors by the MCP tools notes_connect_wisprflow and notes_connect_status (BLI-17973)",
425
425
  "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
426
  };
427
427
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bli-cockpit/mcp",
3
- "version": "0.1.122",
3
+ "version": "0.1.125",
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",