@bli-cockpit/mcp 0.1.113 → 0.1.114

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
- **150 of 155 Tower verbs have an MCP twin.**
112
+ **159 of 164 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
@@ -167,6 +167,7 @@ three tables below.
167
167
  | `cockpit glossary add` | `glossary_add` | `POST /api/glossary` |
168
168
  | `cockpit glossary list` | `glossary_list` | `GET /api/glossary` |
169
169
  | `cockpit glossary remove` | `glossary_remove` | `POST /api/glossary` |
170
+ | `cockpit issue board` | `work_board` | `GET /api/work/board` |
170
171
  | `cockpit issue comment` | `work_comment_issue` | `POST /api/work/issues/[id]/comments` |
171
172
  | `cockpit issue create` | `work_create_issue` | `POST /api/work/issues` |
172
173
  | `cockpit issue history` | `work_issue_history` | `GET /api/work/issues/[id]/history` |
@@ -177,6 +178,8 @@ three tables below.
177
178
  | `cockpit jarvis` | `jarvis_ask` | `POST /api/jarvis/cli` |
178
179
  | `cockpit jarvis --trace` | `jarvis_trace` | `GET /api/ops/trace/[id]` |
179
180
  | `cockpit logs` | `logs_digest` | `LOCAL @bli-cockpit/telemetry-core/log-digest (this machine's logs); `project` reads GET /api/logs (the stored digests)` |
181
+ | `cockpit logs keys mint` | `logs_key_mint` | `POST /api/settings/log-ingest-keys` |
182
+ | `cockpit logs keys rotate` | `logs_key_mint` | `POST /api/settings/log-ingest-keys` |
180
183
  | `cockpit mail accounts` | `mail_accounts` | `GET /api/mail/accounts` |
181
184
  | `cockpit mail attachment` | `mail_attachment` | `GET /api/mail/attachments/[id]?meta=1` |
182
185
  | `cockpit mail default` | `mail_default` | `PATCH /api/mail/accounts` |
@@ -196,6 +199,7 @@ three tables below.
196
199
  | `cockpit memory forget` | `forget_memory` (`bli-memory`) | `POST /api/memory/forget` |
197
200
  | `cockpit memory knowledge` | `memory_knowledge` | `GET /api/memory/knowledge` |
198
201
  | `cockpit memory log` | `memory_experience` | `POST /api/memory/experience` |
202
+ | `cockpit memory map` | `memory_map` | `GET /api/memory/map` |
199
203
  | `cockpit memory private` | `set_memory_visibility` (`bli-memory`) | `POST /api/memory/visibility` |
200
204
  | `cockpit memory profile` | `memory_profile` | `GET /api/memory/profile` |
201
205
  | `cockpit memory public` | `set_memory_visibility` (`bli-memory`) | `POST /api/memory/visibility` |
@@ -261,8 +265,13 @@ three tables below.
261
265
  | `cockpit team device doctor` | `team_device_doctor` | `POST /api/ambient/devices/[deviceId]/doctor` |
262
266
  | `cockpit team device list` | `team_device_list` | `GET /api/team/devices` |
263
267
  | `cockpit team device revoke` | `team_device_revoke` | `POST /api/ambient/devices/[deviceId]/revoke` |
268
+ | `cockpit team github-login add` | `team_github_login_add` | `POST /api/team/members/[userId]/github-logins` |
269
+ | `cockpit team github-login list` | `team_github_login_list` | `GET /api/team/members/[userId]/github-logins` |
270
+ | `cockpit team github-login remove` | `team_github_login_remove` | `DELETE /api/team/members/[userId]/github-logins` |
271
+ | `cockpit team grant` | `team_grant` | `POST /api/team/grants` |
264
272
  | `cockpit team invite` | `team_invite` | `POST /api/team/invite` |
265
273
  | `cockpit team members` | `team_members` | `GET /api/team/members` |
274
+ | `cockpit team revoke` | `team_revoke` | `POST /api/team/grants` |
266
275
  | `cockpit team role` | `team_role` | `PATCH /api/team/members/[userId]/role` |
267
276
  | `cockpit usage people` | `usage_people` | `GET /api/usage/people` |
268
277
  | `cockpit usage sessions` | `usage_sessions` | `GET /api/usage/sessions` |
@@ -34,6 +34,16 @@ const MAX_TEXT_CHARS = 256_000;
34
34
  export function registerLogsTools(server, deps = {}) {
35
35
  const digest = deps.digest ?? digestSource;
36
36
  const door = { fetchImpl: deps.fetchImpl ?? fetch, ...(deps.loadSession ? { loadSession: deps.loadSession } : {}) };
37
+ registrarFor(server)("logs_key_mint", {
38
+ title: "Mint or rotate a source log ingest key",
39
+ description: "Super admin only. Returns the key once. Rotation invalidates the old key. Same door as cockpit logs keys mint|rotate.",
40
+ inputSchema: { source: z.string().min(3).max(120).describe("The push source id, e.g. droplet:kaiben-ridestyler."), action: z.enum(["mint", "rotate"]).describe("mint creates the first key; rotate replaces it and invalidates the old one.") },
41
+ }, async (args) => withSession(door, async (session) => {
42
+ const response = await callAgentDoor(session, door.fetchImpl, "POST", "/api/settings/log-ingest-keys", args);
43
+ if (!response.ok)
44
+ return errorResult(doorFailureText("logs_key_mint", response));
45
+ return textResult(String(response.body.key), response.body);
46
+ }));
37
47
  registrarFor(server)("logs_digest", {
38
48
  title: "Digest Tower's logs (about 2 KB)",
39
49
  description: "A token-cheap digest of Tower's own logs, read on THIS machine and never uploaded. Use it BEFORE reading raw logs: a 15-minute window of a "
@@ -2,6 +2,19 @@ import { z } from "zod";
2
2
  import { callAgentDoor } from "./agent-door.js";
3
3
  import { doorFailureText, errorResult, queryString, registrarFor, textResult, withSession } from "./tool-result.js";
4
4
  export function registerMemoryRulesTools(server, deps) {
5
+ registrarFor(server)("memory_map", {
6
+ title: "Read the memory map",
7
+ description: "Visible projects, topics, memory counts, importance, top ideas and shared-session edges. Same door as cockpit memory map; coverage and age are included.",
8
+ inputSchema: {},
9
+ }, async () => withSession(deps, async (session) => {
10
+ const response = await callAgentDoor(session, deps.fetchImpl, "GET", "/api/memory/map");
11
+ if (!response.ok)
12
+ return errorResult(doorFailureText("memory_map", response));
13
+ const body = response.body;
14
+ const lines = body.projects.flatMap(project => [`${project.project} (${project.count} memories)`, ...project.topics.map(topic => ` ${topic.label}: ${topic.count}`)]);
15
+ lines.push(`${body.examined} of ${body.total} visible memories counted; as of ${body.computedAt}.${body.capped ? " Scan capped at 10,000." : ""}`);
16
+ return textResult(lines.join("\n"), response.body);
17
+ }));
5
18
  for (const action of ["rules", "knowledge"])
6
19
  registrarFor(server)(`memory_${action}`, {
7
20
  title: `Read BLI agent ${action}`,
@@ -18,7 +18,7 @@ export interface SpeedPage {
18
18
  path: string;
19
19
  note?: string;
20
20
  }
21
- export declare function speedPages(memoryContainer: string): SpeedPage[];
21
+ export declare function speedPages(_memoryContainer: string): SpeedPage[];
22
22
  export declare const SPEED_PAGE_NAMES: string[];
23
23
  export interface SpeedSample {
24
24
  status: number | null;
@@ -14,7 +14,7 @@
14
14
  import { z } from "zod";
15
15
  import { callAgentDoor } from "./agent-door.js";
16
16
  import { dataResult, doorFailureText, errorResult, registrarFor, withSession } from "./tool-result.js";
17
- export function speedPages(memoryContainer) {
17
+ export function speedPages(_memoryContainer) {
18
18
  return [
19
19
  { name: "today", label: "Today (JARVIS home)", path: "/api/jarvis/brief" },
20
20
  { name: "meeting-notes", label: "Meeting notes", path: "/api/notes/library" },
@@ -23,13 +23,10 @@ export function speedPages(memoryContainer) {
23
23
  { name: "messages", label: "Messages", path: "/api/msg/channels" },
24
24
  { name: "mail", label: "Mail", path: "/api/mail/inbox" },
25
25
  { name: "docs", label: "Docs", path: "/api/docs/tree" },
26
- {
27
- name: "memory",
28
- label: "Memory",
29
- path: `/api/memory/profile?container=${encodeURIComponent(memoryContainer)}`,
30
- note: "the page reads agent_memories on the server; the profile door is the nearest read",
31
- },
32
- { name: "tickets", label: "Tickets", path: "/api/work/issues" },
26
+ { name: "memory-knowledge", label: "Memory knowledge", path: "/api/memory/knowledge" },
27
+ { name: "memory-rules", label: "Memory rules", path: "/api/memory/rules" },
28
+ { name: "tickets", label: "Tickets", path: "/api/work/board" },
29
+ { name: "tickets-list", label: "Tickets list", path: "/api/work/issues" },
33
30
  { name: "setup", label: "Setup", path: "/api/setup" },
34
31
  { name: "usage", label: "Usage", path: "/api/usage/people?since=30d" },
35
32
  { name: "calendar", label: "Calendar", path: "/api/cal/week" },
@@ -212,7 +209,7 @@ export function registerSpeedTools(server, deps) {
212
209
  .describe("Only these pages. Omit for every page."),
213
210
  runs: z.number().int().min(1).max(MAX_RUNS).optional().describe("Requests per page (default 5)."),
214
211
  container: z.string().min(1).max(200).optional()
215
- .describe("The memory container tag to time the Memory page with. Without it the Memory page is left out."),
212
+ .describe("Legacy container tag; Memory tab doors no longer require it."),
216
213
  },
217
214
  }, async (args) => withSession(deps, async (session) => {
218
215
  const runs = typeof args.runs === "number" ? args.runs : DEFAULT_RUNS;
@@ -222,14 +219,10 @@ export function registerSpeedTools(server, deps) {
222
219
  const chosen = speedPages(container ?? "-").filter((page) => {
223
220
  if (asked.length > 0 && !asked.includes(page.name))
224
221
  return false;
225
- if (page.name === "memory" && container === null) {
226
- skipped.push("memory");
227
- return false;
228
- }
229
222
  return true;
230
223
  });
231
224
  if (chosen.length === 0) {
232
- return errorResult("speed_read has nothing to time: the Memory page needs a `container` tag.");
225
+ return errorResult("speed_read has nothing to time.");
233
226
  }
234
227
  const results = [];
235
228
  for (const page of chosen) {
@@ -62,6 +62,48 @@ async function listDevices(deps, session) {
62
62
  }
63
63
  export function registerTeamWriteTools(server, deps) {
64
64
  const register = registrarFor(server);
65
+ for (const action of ["add", "remove", "list"]) {
66
+ const name = `team_github_login_${action}`;
67
+ register(name, {
68
+ title: `${action} an extra GitHub login`,
69
+ description: "Manage extra GitHub logins by exact person UUID, name or email. Server permits super admins or members managing themselves. Primary login and history are preserved.",
70
+ inputSchema: { person: z.string().min(1).describe("Exact roster UUID, name or email from team_members."),
71
+ ...(action === "list" ? {} : { login: z.string().min(1).max(39).describe("GitHub login; the server validates its format.") }) },
72
+ }, async (args) => withSession(deps, async (session) => {
73
+ let personId = String(args.person);
74
+ if (!/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(personId)) {
75
+ const directory = await callAgentDoor(session, deps.fetchImpl, "GET", "/api/team/members");
76
+ if (!directory.ok)
77
+ return errorResult(doorFailureText(name, directory));
78
+ if (directory.body.rosterError)
79
+ return errorResult("Refused (roster_read_failed): use a person UUID or retry.");
80
+ const needle = personId.toLowerCase();
81
+ const people = (Array.isArray(directory.body.people) ? directory.body.people : []);
82
+ const ids = [...new Set(people.filter(person => [person.rosterId, person.name, person.email]
83
+ .some(label => typeof label === "string" && label.toLowerCase() === needle))
84
+ .map(person => person.rosterId).filter((id) => typeof id === "string"))];
85
+ if (ids.length !== 1)
86
+ return errorResult(`Refused (${ids.length ? "ambiguous_person" : "person_not_found"}): use an exact roster UUID, name or email.`);
87
+ personId = ids[0];
88
+ }
89
+ const response = await callAgentDoor(session, deps.fetchImpl, action === "list" ? "GET" : action === "add" ? "POST" : "DELETE", `/api/team/members/${encodeURIComponent(personId)}/github-logins`, action === "list" ? undefined : { login: args.login });
90
+ if (!response.ok)
91
+ return errorResult(doorFailureText(name, response));
92
+ return textResult(JSON.stringify(response.body), response.body);
93
+ }));
94
+ }
95
+ for (const action of ["grant", "revoke"]) {
96
+ register(`team_${action}`, {
97
+ title: `${action === "grant" ? "Grant" : "Revoke"} Careers access`,
98
+ description: "Change one person's see or act grant. Super admin only, decided by the shared Team door. Levels are independent; defaults to see.",
99
+ inputSchema: { person_id: z.string().uuid().describe("The person (roster user id) whose access changes."), section: z.enum(["careers"]).describe("The section; only careers is grantable today."), level: z.enum(["see", "act"]).optional().describe("Defaults to see. see reads candidates; act also decides, sends take-homes and changes settings.") },
100
+ }, async (args) => withSession(deps, async (session) => {
101
+ const response = await callAgentDoor(session, deps.fetchImpl, "POST", "/api/team/grants", { person_id: args.person_id, section: args.section, level: args.level ?? "see", action }, WRITE_DEADLINE_MS);
102
+ if (!response.ok)
103
+ return errorResult(doorFailureText(`team_${action}`, response));
104
+ return textResult(`Careers ${String(args.level ?? "see")} ${action === "grant" ? "granted" : "revoked"}.`, response.body);
105
+ }));
106
+ }
65
107
  register("team_invite", {
66
108
  title: "Invite somebody to Tower",
67
109
  description: "Sends a sign-in link to an email address — `cockpit team invite`, same door. super_admin, decided on the "
@@ -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", "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_rules", "memory_knowledge", "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_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", "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"];
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 ["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_device_revoke", "team_device_doctor"];
24
+ export declare const WRITE_TOOLS: readonly ["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"];
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
  /**
@@ -20,6 +20,7 @@ export const READ_TOOLS = [
20
20
  "msg_channels",
21
21
  "msg_read",
22
22
  "msg_thread",
23
+ "work_board",
23
24
  "work_list_issues",
24
25
  "work_get_issue",
25
26
  "work_issue_history",
@@ -77,6 +78,7 @@ export const READ_TOOLS = [
77
78
  "slack_connect",
78
79
  "slack_disconnect",
79
80
  "slack_read",
81
+ "memory_map",
80
82
  "memory_rules",
81
83
  "memory_knowledge",
82
84
  "memory_profile",
@@ -85,6 +87,9 @@ export const READ_TOOLS = [
85
87
  "settings_list",
86
88
  "model_show",
87
89
  "team_members",
90
+ "team_github_login_add",
91
+ "team_github_login_remove",
92
+ "team_github_login_list",
88
93
  "team_device_list",
89
94
  "scout_board",
90
95
  "workbook_read",
@@ -106,6 +111,7 @@ export const DRY_RUN_TOOLS = ["ops_recompile", "memory_requeue"];
106
111
  * suites (`write-tools.test.ts` and each family's own).
107
112
  */
108
113
  export const WRITE_TOOLS = [
114
+ "logs_key_mint",
109
115
  "brain_retry",
110
116
  "emit_event",
111
117
  "docs_create",
@@ -169,6 +175,8 @@ export const WRITE_TOOLS = [
169
175
  "model_set",
170
176
  "team_invite",
171
177
  "team_role",
178
+ "team_grant",
179
+ "team_revoke",
172
180
  "team_device_revoke",
173
181
  "team_device_doctor",
174
182
  ];
@@ -84,28 +84,26 @@ export function registerUsageTools(server, deps) {
84
84
  return dataResult(`${response.body.read_coverage?.join(" ") ?? ""} ${coverage?.sessions_extracted ?? 0} of ${coverage?.sessions_observed ?? 0} sessions extracted.${why} ${coverage?.sessions_counted_at_upload ?? 0} of ${coverage?.sessions_observed ?? 0} sessions counted at upload, ${coverage?.sessions_counted_by_server ?? 0} by server backfill, ${coverage?.sessions_counted_by_ladder ?? 0} by the ladder.${args.groupBy === "task_type" ? ` ${coverage?.sessions_labelled ?? 0} of ${coverage?.sessions_observed ?? 0} sessions labelled by topic.` : ""}${priceLabel}${github}`, response.body);
85
85
  }));
86
86
  registrarFor(server)("usage_sessions", {
87
- title: "Read task or subject sessions",
88
- description: "The sessions behind one ticket or one topic: titles, tokens and work types. Name exactly one of task (a BLI-#### id) or subject (a TOPIC label, as usage_people with groupBy topic lists it; not a person). Or pass session (a session id) alone to read that session's digest windows, ten at a time from fromWindow. This read can take 25 seconds.",
87
+ title: "Read sessions and session detail",
88
+ description: "List a bounded page of sessions filtered by exact person, window, repo, tool, ticket or topic. Returns transcript start time, predominant model by tokens, known effort, tokens, duration, tool calls, recorded commit/PR links and L1 summary or its missing reason. Continue with next_cursor. Pass session for detail even if no digest exists.",
89
89
  inputSchema: {
90
+ cursor: z.string().max(500).optional().describe("next_cursor from the previous page; keep filters and absolute window unchanged."),
91
+ limit: z.number().int().min(1).max(200).optional().describe("Page size, default 100."),
92
+ tool: z.enum(["claude", "codex"]).optional().describe("Only this agent tool."),
90
93
  task: z.string().regex(/^BLI-[0-9]+$/).optional().describe("A ticket id, e.g. BLI-4258: the sessions labelled with it."),
91
94
  subject: z.string().min(1).max(300).optional().describe("A topic label exactly as usage_people groupBy topic prints it. Not a person or an email."),
92
95
  repo: z.string().min(1).max(200).optional().describe("Only sessions in this repo label."),
93
- person: z.string().min(1).max(64).optional().describe("Only this person's sessions, by user id (usage_people user_id)."),
96
+ person: z.string().min(1).max(320).optional().describe("Exact roster name, alias, email, GitHub login or account id; me means the caller."),
94
97
  subtopic: z.string().min(1).max(300).optional().describe("Only sessions with this subtopic label."),
95
98
  since: z.string().min(1).max(40).optional().describe(`${WINDOW_BOUND}. Default 30d.`),
96
99
  until: z.string().min(1).max(40).optional().describe(`${WINDOW_BOUND}. Default now.`),
97
100
  include_automated: z.boolean().optional().describe("Count automated sessions too. Off by default."),
98
- session: z.string().min(1).max(200).optional().describe("A session id: read its digest windows instead of listing sessions."),
101
+ session: z.string().min(1).max(200).optional().describe("An ambient row UUID or transcript session id: session detail plus optional digest windows, independent of the list window."),
99
102
  fromWindow: z.number().int().min(1).optional().describe("With session: the first window number to read. Default 1."),
100
103
  },
101
104
  }, async (args) => withSession(deps, async (session) => {
102
- // The door answers a bare 400 `subject_and_valid_window_required` when
103
- // neither is named; say what to pass instead of "Tower answered 400".
104
- if (args.session === undefined && args.task === undefined && (typeof args.subject !== "string" || args.subject.trim() === "")) {
105
- return errorResult("usage_sessions needs task (a BLI-#### id), subject (a topic label from usage_people groupBy topic) or session (a session id) (task_or_subject_required). Nothing was read.");
106
- }
107
105
  const query = new URLSearchParams();
108
- for (const key of ["task", "subject", "repo", "person", "subtopic", "since", "until", "session", "fromWindow"])
106
+ for (const key of ["tool", "cursor", "limit", "task", "subject", "repo", "person", "subtopic", "since", "until", "session", "fromWindow"])
109
107
  if (args[key] !== undefined)
110
108
  query.set(key, String(args[key]));
111
109
  if (args.include_automated)
@@ -115,9 +113,9 @@ export function registerUsageTools(server, deps) {
115
113
  return errorResult(doorFailureText("usage_sessions", response));
116
114
  if (args.session !== undefined) {
117
115
  const windows = Array.isArray(response.body.windows) ? response.body.windows.length : 0;
118
- return dataResult(`${windows} digest window(s) of session ${String(args.session)}.`, response.body);
116
+ return dataResult(`Session detail and ${windows} digest window(s) of session ${String(args.session)}.`, response.body);
119
117
  }
120
118
  const sessions = Array.isArray(response.body.sessions) ? response.body.sessions.length : 0;
121
- return dataResult(`${response.body.read_coverage?.join(" ") ?? ""} ${sessions} session(s) for ${args.task ? `task ${String(args.task)}` : `topic "${String(args.subject)}"`}.`, response.body);
119
+ return dataResult(`${response.body.read_coverage?.join(" ") ?? ""} ${sessions} session(s) for ${args.task ? `task ${String(args.task)}` : args.subject ? `topic "${String(args.subject)}"` : args.person ? `person ${String(args.person)}` : "the requested window"}.`, response.body);
122
120
  }));
123
121
  }
@@ -31,6 +31,9 @@ export const TYPED_SPELLING = {
31
31
  "settings list": "settings env list",
32
32
  "settings set": "settings switches set",
33
33
  "settings delete": "settings env delete",
34
+ "team github-login-add": "team github-login add",
35
+ "team github-login-remove": "team github-login remove",
36
+ "team github-login-list": "team github-login list",
34
37
  "team device-list": "team device list",
35
38
  "team device-revoke": "team device revoke",
36
39
  "team device-doctor": "team device doctor",
@@ -137,6 +137,7 @@ export function towerVerbs() {
137
137
  const verbs = [
138
138
  { noun: "memory", action: "log", spelling: "memory log" },
139
139
  ...memoryStoreVerbs(aliases),
140
+ ...["mint", "rotate"].map(action => ({ noun: "logs", action: `keys ${action}`, spelling: `logs keys ${action}` })),
140
141
  ];
141
142
  const seenNouns = new Set();
142
143
  for (const variant of commandVariants()) {
@@ -238,6 +239,8 @@ export const MCP_TWINS = {
238
239
  // BLI-12833: a local read, not a Tower door: it runs the shared log digest over
239
240
  // this machine's collector logs and the `vercel` / `railway` CLIs.
240
241
  // BLI-12855: a project name (or --stored, or a local CLI that is missing) reads the digests Tower's cron stored, through GET /api/logs.
242
+ "logs keys mint": { tool: "logs_key_mint", door: "POST /api/settings/log-ingest-keys" },
243
+ "logs keys rotate": { tool: "logs_key_mint", door: "POST /api/settings/log-ingest-keys" },
241
244
  "logs": { tool: "logs_digest", door: "LOCAL @bli-cockpit/telemetry-core/log-digest (this machine's logs); `project` reads GET /api/logs (the stored digests)" },
242
245
  "brain retry": { tool: "brain_retry", door: "POST /api/brain/incidents/retry" },
243
246
  "brain status": { tool: "brain_status", door: "GET /api/brain/incidents" },
@@ -265,6 +268,7 @@ export const MCP_TWINS = {
265
268
  "msg read": { tool: "msg_read", door: "GET /api/msg/channels/[id]/messages" },
266
269
  "msg thread": { tool: "msg_thread", door: "GET /api/msg/channels/[id]/messages?thread_parent_id=" },
267
270
  "msg send": { tool: "msg_send", door: "POST /api/msg/channels/[id]/messages" },
271
+ "issue board": { tool: "work_board", door: "GET /api/work/board" },
268
272
  "issue list": { tool: "work_list_issues", door: "GET /api/work/issues" },
269
273
  "issue show": { tool: "work_get_issue", door: "GET /api/work/issues/[id]" },
270
274
  "issue create": { tool: "work_create_issue", door: "POST /api/work/issues" },
@@ -342,6 +346,7 @@ export const MCP_TWINS = {
342
346
  // BLI-4533: the compiled one-page profile. Its twin is on `bli-tower`, not
343
347
  // `bli-memory`: that server's four tools are the vendor-compatible set the
344
348
  // Claude Code plugin expects, and this page is ours.
349
+ "memory map": { tool: "memory_map", door: "GET /api/memory/map" },
345
350
  "memory knowledge": { tool: "memory_knowledge", door: "GET /api/memory/knowledge" },
346
351
  "memory rules": { tool: "memory_rules", door: "GET /api/memory/rules" },
347
352
  "memory profile": { tool: "memory_profile", door: "GET /api/memory/profile" },
@@ -363,9 +368,14 @@ export const MCP_TWINS = {
363
368
  "settings list": { tool: "settings_list", door: "GET /api/settings/env-blobs" },
364
369
  "settings set": { tool: "settings_set", door: "POST /api/settings/{jarvis-model,switches,model-routing,env-blobs} + PUT /api/settings/cli-floor" },
365
370
  "settings delete": { tool: "settings_delete", door: "DELETE /api/settings/env-blobs" },
371
+ "team github-login-add": { tool: "team_github_login_add", door: "POST /api/team/members/[userId]/github-logins" },
372
+ "team github-login-remove": { tool: "team_github_login_remove", door: "DELETE /api/team/members/[userId]/github-logins" },
373
+ "team github-login-list": { tool: "team_github_login_list", door: "GET /api/team/members/[userId]/github-logins" },
366
374
  "team members": { tool: "team_members", door: "GET /api/team/members" },
367
375
  "team device-list": { tool: "team_device_list", door: "GET /api/team/devices" },
368
376
  "team invite": { tool: "team_invite", door: "POST /api/team/invite" },
377
+ "team grant": { tool: "team_grant", door: "POST /api/team/grants" },
378
+ "team revoke": { tool: "team_revoke", door: "POST /api/team/grants" },
369
379
  "team role": { tool: "team_role", door: "PATCH /api/team/members/[userId]/role" },
370
380
  "team device-doctor": { tool: "team_device_doctor", door: "POST /api/ambient/devices/[deviceId]/doctor" },
371
381
  "team device-revoke": { tool: "team_device_revoke", door: "POST /api/ambient/devices/[deviceId]/revoke" },
@@ -0,0 +1,5 @@
1
+ import { type ToolDeps } from "./tool-result.js";
2
+ /** Board windows and coverage over the same door the Tickets page reader backs. */
3
+ export declare function registerWorkBoardTool(server: {
4
+ registerTool: (...args: never[]) => unknown;
5
+ }, deps: ToolDeps): void;
@@ -0,0 +1,20 @@
1
+ import { z } from "zod";
2
+ import { callAgentDoor } from "./agent-door.js";
3
+ import { doorFailureText, errorResult, registrarFor, textResult, withSession } from "./tool-result.js";
4
+ /** Board windows and coverage over the same door the Tickets page reader backs. */
5
+ export function registerWorkBoardTool(server, deps) {
6
+ const register = registrarFor(server);
7
+ register("work_board", { title: "Read Tower Tickets board", description: "Per-state card windows, counts, coverage and named failures, matching /tickets.",
8
+ inputSchema: { state: z.enum(["backlog", "todo", "in_progress", "in_review", "done", "canceled"]).describe("Show one state column only.").optional(), assignee: z.string().describe("me, unassigned, or a user uuid.").optional(),
9
+ project: z.string().uuid().describe("Project id (uuid).").optional(), label: z.string().describe("Label name to filter by.").optional(), rows: z.string().describe("Per-state row window tokens, as the board page uses.").optional() } }, async (args) => withSession(deps, async (session) => {
10
+ const query = new URLSearchParams();
11
+ for (const key of ["state", "assignee", "project", "label", "rows"])
12
+ if (args[key] !== undefined)
13
+ query.set(key, String(args[key]));
14
+ const suffix = query.toString();
15
+ const response = await callAgentDoor(session, deps.fetchImpl, "GET", `/api/work/board${suffix ? `?${suffix}` : ""}`);
16
+ if (!response.ok)
17
+ return errorResult(doorFailureText("work_board", response));
18
+ return textResult(JSON.stringify(response.body), response.body);
19
+ }));
20
+ }
@@ -86,6 +86,30 @@ export function mcpToolFieldNames(toolName) {
86
86
  return Object.keys(shape);
87
87
  }
88
88
  export const EXPECTED_ONE_SIDED_FIELDS = [
89
+ {
90
+ verb: "issue create",
91
+ side: "cli",
92
+ field: "label",
93
+ reason: "a board filter: the parser accepts --label only for `cockpit issue board` (BLI-13354) and refuses it on create; work_board carries the matching field.",
94
+ },
95
+ {
96
+ verb: "issue create",
97
+ side: "cli",
98
+ field: "rows",
99
+ reason: "a board filter: the parser accepts --rows only for `cockpit issue board` (BLI-13354) and refuses it on create; work_board carries the matching field.",
100
+ },
101
+ {
102
+ verb: "issue update",
103
+ side: "cli",
104
+ field: "label",
105
+ reason: "a board filter: the parser accepts --label only for `cockpit issue board` (BLI-13354) and refuses it on update; work_board carries the matching field.",
106
+ },
107
+ {
108
+ verb: "issue update",
109
+ side: "cli",
110
+ field: "rows",
111
+ reason: "a board filter: the parser accepts --rows only for `cockpit issue board` (BLI-13354) and refuses it on update; work_board carries the matching field.",
112
+ },
89
113
  {
90
114
  verb: "issue update",
91
115
  side: "mcp",
@@ -15,6 +15,7 @@
15
15
  * server's other tools and only a `work_` call fails, by name.
16
16
  */
17
17
  import { z } from "zod";
18
+ import { registerWorkBoardTool } from "./work-board-tools.js";
18
19
  import { callAgentDoor } from "./agent-door.js";
19
20
  import { DEFAULT_LIST_LIMIT, doorFailureText, errorResult, moreNote, registrarFor, textResult, withSession, } from "./tool-result.js";
20
21
  import { assigneeLabel, resolveAssigneeDirectory, withAssigneeIdentity } from "./work-directory.js";
@@ -83,6 +84,7 @@ async function resolveParentId(deps, session, ref) {
83
84
  }
84
85
  export function registerWorkTools(server, deps) {
85
86
  const register = registrarFor(server);
87
+ registerWorkBoardTool(server, deps);
86
88
  register("work_list_issues", {
87
89
  title: "List Tower tickets",
88
90
  description: "Tower tickets you may see, most recently updated first: identifier, state, title, priority, assignee, project. "
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bli-cockpit/mcp",
3
- "version": "0.1.113",
3
+ "version": "0.1.114",
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.69",
40
+ "@bli-cockpit/telemetry-core": "0.1.70",
41
41
  "@modelcontextprotocol/sdk": "^1.29.0",
42
42
  "zod": "^4.3.6"
43
43
  },