@bli-cockpit/mcp 0.1.109 → 0.1.111
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 +10 -1
- package/dist/account-tools.d.ts +2 -0
- package/dist/account-tools.js +19 -0
- package/dist/brain-tools.js +17 -0
- package/dist/cal-tools-delete.js +21 -3
- package/dist/cal-tools.js +5 -2
- package/dist/docs-msg-tools.js +2 -1
- package/dist/docs-notion-tools.js +3 -1
- package/dist/mail-compose.js +2 -1
- package/dist/mail-tools.js +12 -8
- package/dist/notes-bot-upcoming-tool.d.ts +4 -0
- package/dist/notes-bot-upcoming-tool.js +14 -0
- package/dist/server.js +2 -0
- package/dist/setup-tools.js +1 -1
- package/dist/tool-catalog.d.ts +2 -2
- package/dist/tool-catalog.js +3 -0
- package/dist/usage-tools.d.ts +1 -0
- package/dist/usage-tools.js +3 -2
- package/dist/verb-census.js +9 -0
- package/dist/work-tools.js +3 -2
- package/package.json +1 -1
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
|
-
**
|
|
112
|
+
**147 of 152 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
|
|
@@ -117,6 +117,7 @@ three tables below.
|
|
|
117
117
|
|
|
118
118
|
| CLI verb | MCP tool | Door |
|
|
119
119
|
| --- | --- | --- |
|
|
120
|
+
| `cockpit brain retry` | `brain_retry` | `POST /api/brain/incidents/retry` |
|
|
120
121
|
| `cockpit brain status` | `brain_status` | `GET /api/brain/incidents` |
|
|
121
122
|
| `cockpit brief done` | `brief_done` | `POST /api/jarvis/ticks` |
|
|
122
123
|
| `cockpit brief history` | `brief_history` | `GET /api/jarvis/brief?history=1` |
|
|
@@ -125,6 +126,8 @@ three tables below.
|
|
|
125
126
|
| `cockpit brief status` | `brief_status` | `GET /api/ops/brief-status` |
|
|
126
127
|
| `cockpit brief undone` | `brief_undone` | `POST /api/jarvis/ticks` |
|
|
127
128
|
| `cockpit cal calendars` | `cal_calendars` | `GET /api/cal/calendars` |
|
|
129
|
+
| `cockpit cal cancel` | `cal_delete` | `DELETE /api/cal/events/[eventId]` |
|
|
130
|
+
| `cockpit cal change` | `cal_change` | `PATCH /api/cal/events/[eventId]` |
|
|
128
131
|
| `cockpit cal create` | `cal_create` | `POST /api/cal/events` |
|
|
129
132
|
| `cockpit cal delete` | `cal_delete` | `DELETE /api/cal/events/[eventId]` |
|
|
130
133
|
| `cockpit cal detach` | `cal_detach` | `DELETE /api/cal/calendars` |
|
|
@@ -176,10 +179,15 @@ three tables below.
|
|
|
176
179
|
| `cockpit logs` | `logs_digest` | `LOCAL @bli-cockpit/telemetry-core/log-digest (this machine's logs); `project` reads GET /api/logs (the stored digests)` |
|
|
177
180
|
| `cockpit mail accounts` | `mail_accounts` | `GET /api/mail/accounts` |
|
|
178
181
|
| `cockpit mail attachment` | `mail_attachment` | `GET /api/mail/attachments/[id]?meta=1` |
|
|
182
|
+
| `cockpit mail default` | `mail_default` | `PATCH /api/mail/accounts` |
|
|
179
183
|
| `cockpit mail detach` | `mail_detach` | `DELETE /api/mail/accounts` |
|
|
184
|
+
| `cockpit mail disable` | `mail_disable` | `PATCH /api/mail/accounts` |
|
|
180
185
|
| `cockpit mail draft` | `mail_draft` | `POST /api/mail/drafts` |
|
|
186
|
+
| `cockpit mail enable` | `mail_enable` | `PATCH /api/mail/accounts` |
|
|
181
187
|
| `cockpit mail inbox` | `mail_inbox` | `GET /api/mail/inbox` |
|
|
182
188
|
| `cockpit mail read` | `mail_read` | `GET /api/mail/threads/[id]` |
|
|
189
|
+
| `cockpit mail remove` | `mail_remove` | `DELETE /api/mail/accounts` |
|
|
190
|
+
| `cockpit mail rename` | `mail_rename` | `PATCH /api/mail/accounts` |
|
|
183
191
|
| `cockpit mail search` | `mail_search` | `GET /api/mail/search` |
|
|
184
192
|
| `cockpit mail send` | `mail_send` | `POST /api/mail/send` |
|
|
185
193
|
| `cockpit mail sync` | `mail_sync` | `POST /api/mail/accounts/[id]/sync` |
|
|
@@ -207,6 +215,7 @@ three tables below.
|
|
|
207
215
|
| `cockpit msg send` | `msg_send` | `POST /api/msg/channels/[id]/messages` |
|
|
208
216
|
| `cockpit msg thread` | `msg_thread` | `GET /api/msg/channels/[id]/messages?thread_parent_id=` |
|
|
209
217
|
| `cockpit notes accounts` | `notes_accounts` | `GET /api/notes/accounts` |
|
|
218
|
+
| `cockpit notes bot-upcoming` | `notes_bot_upcoming` | `GET /api/notetaker/upcoming` |
|
|
210
219
|
| `cockpit notes detach` | `notes_detach` | `DELETE /api/notes/accounts` |
|
|
211
220
|
| `cockpit notes folders` | `notes_folders` | `GET /api/notes/folders` |
|
|
212
221
|
| `cockpit notes granola-status` | `notes_granola_status` | `GET /api/notes/granola-status` |
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { callAgentDoor } from "./agent-door.js";
|
|
3
|
+
import { withSession, textResult, errorResult, doorFailureText } from "./tool-result.js";
|
|
4
|
+
export function registerAccountTools(register, deps) {
|
|
5
|
+
for (const action of ["rename", "enable", "disable", "default", "remove"]) {
|
|
6
|
+
register(`mail_${action}`, { title: `${action} a connected account`, description: "Manage the caller's shared mail/calendar account. Exact case-folded label, address or id. Removing disconnects the account and deletes Tower's stored copies; provider data is untouched.",
|
|
7
|
+
inputSchema: {
|
|
8
|
+
account: z.string().min(1).max(320).describe("The connected account: its exact label, address or id (mail_accounts lists them)."),
|
|
9
|
+
...(action === "rename" ? { name: z.string().trim().min(1).max(100).describe("The new label, e.g. personal 1.") } : {}),
|
|
10
|
+
...(["enable", "disable"].includes(action) ? { source: z.enum(["mail", "calendar"]).describe("Which source to switch: mail or calendar.") } : {}),
|
|
11
|
+
},
|
|
12
|
+
}, async (args) => withSession(deps, async (session) => {
|
|
13
|
+
const response = await callAgentDoor(session, deps.fetchImpl, action === "remove" ? "DELETE" : "PATCH", action === "remove" ? `/api/mail/accounts?account=${encodeURIComponent(String(args.account))}` : "/api/mail/accounts", action === "remove" ? undefined : { account: args.account, ...(action === "rename" ? { account_label: args.name } : action === "default" ? { is_default: true } : { [`${args.source}_enabled`]: action === "enable" }) });
|
|
14
|
+
if (!response.ok)
|
|
15
|
+
return errorResult(doorFailureText(`mail_${action}`, response));
|
|
16
|
+
return textResult(`Account ${args.account}: ${action} completed.`, response.body);
|
|
17
|
+
}));
|
|
18
|
+
}
|
|
19
|
+
}
|
package/dist/brain-tools.js
CHANGED
|
@@ -36,4 +36,21 @@ export function registerBrainTools(server, deps) {
|
|
|
36
36
|
const text = typeof response.body.text === "string" ? response.body.text : "";
|
|
37
37
|
return dataResult(text, response.body);
|
|
38
38
|
}));
|
|
39
|
+
registrarFor(server)("brain_retry", {
|
|
40
|
+
title: "Send incidents back to Tower's brain",
|
|
41
|
+
description: "Retry needs_human or not_actionable incidents through the same door as cockpit brain retry. Only members and super admins. Resets attempts and claims, records who/why/when, and comments once on each incident issue. Other states are skipped.",
|
|
42
|
+
inputSchema: {
|
|
43
|
+
ids: z.array(z.string()).max(100).optional().describe("Incident UUIDs or BLI issue identifiers."),
|
|
44
|
+
all_needs_human: z.boolean().optional().describe("Retry all needs_human incidents, optionally in one project."),
|
|
45
|
+
project: z.string().optional().describe("Narrow every selection to this registry project id."),
|
|
46
|
+
reason: z.string().trim().min(1).max(4000).describe("Why these incidents should be tried again."),
|
|
47
|
+
},
|
|
48
|
+
}, async (args) => withSession(deps, async (session) => {
|
|
49
|
+
const response = await callAgentDoor(session, deps.fetchImpl, "POST", "/api/brain/incidents/retry", args);
|
|
50
|
+
if (!response.ok)
|
|
51
|
+
return errorResult(doorFailureText("brain_retry", response));
|
|
52
|
+
if (response.body.ok === false)
|
|
53
|
+
return { ...dataResult(String(response.body.text ?? "Retry partially failed."), response.body), isError: true };
|
|
54
|
+
return dataResult(String(response.body.text ?? ""), response.body);
|
|
55
|
+
}));
|
|
39
56
|
}
|
package/dist/cal-tools-delete.js
CHANGED
|
@@ -9,6 +9,22 @@ import { z } from "zod";
|
|
|
9
9
|
import { callAgentDoor } from "./agent-door.js";
|
|
10
10
|
import { CONFIRM_INPUT, doorFailureText, errorResult, textResult, unconfirmed, withSession, } from "./tool-result.js";
|
|
11
11
|
export function registerCalDeleteTool(register, deps) {
|
|
12
|
+
register("cal_change", { title: "Change an event", description: "Change a caller-owned event, checking the named account before writing to Google.", inputSchema: {
|
|
13
|
+
event_id: z.string().uuid().describe("Tower's event id (cal_today/cal_week/cal_next/cal_find return it as event_id)."),
|
|
14
|
+
account: z.string().min(1).max(100).optional().describe("Exact connected account label, address or id (mail_accounts lists them). Omit to use the account the event is on."),
|
|
15
|
+
title: z.string().min(1).max(500).optional().describe("The new title."),
|
|
16
|
+
starts_at: z.string().optional().describe("The new start, ISO 8601 with offset. Send with ends_at."),
|
|
17
|
+
ends_at: z.string().optional().describe("The new end, ISO 8601 with offset. Send with starts_at."),
|
|
18
|
+
location: z.string().max(1000).optional().describe("The new location."),
|
|
19
|
+
confirm: CONFIRM_INPUT,
|
|
20
|
+
} }, async (args) => withSession(deps, async (session) => {
|
|
21
|
+
const refusal = unconfirmed(args, "This changes the event at Google and may notify guests.");
|
|
22
|
+
if (refusal)
|
|
23
|
+
return refusal;
|
|
24
|
+
const { event_id, confirm: _confirm, ...changes } = args;
|
|
25
|
+
const response = await callAgentDoor(session, deps.fetchImpl, "PATCH", `/api/cal/events/${encodeURIComponent(String(event_id))}`, changes);
|
|
26
|
+
return response.ok ? textResult(`Event changed. Account: ${response.body.event?.account_name ?? "the event’s account"}.`, response.body) : errorResult(doorFailureText("cal_change", response));
|
|
27
|
+
}));
|
|
12
28
|
register("cal_delete", {
|
|
13
29
|
title: "Delete an event",
|
|
14
30
|
description: "Removes ONE event, AT GOOGLE first and then here — the same order every calendar write in this server "
|
|
@@ -16,6 +32,7 @@ export function registerCalDeleteTool(register, deps) {
|
|
|
16
32
|
+ "Attendees are notified (sendUpdates=all) only when the event has any. Google already having it gone "
|
|
17
33
|
+ "(404/410) is answered as success, not a refusal. Requires confirm: true.",
|
|
18
34
|
inputSchema: {
|
|
35
|
+
account: z.string().min(1).max(100).optional().describe("Exact connected account label, address or id (mail_accounts lists them). Omit to use the account the event is on."),
|
|
19
36
|
event_id: z.string().uuid().describe("Tower's event id (cal_today/cal_week/cal_next/cal_find return it as event_id)."),
|
|
20
37
|
confirm: CONFIRM_INPUT,
|
|
21
38
|
},
|
|
@@ -23,18 +40,19 @@ export function registerCalDeleteTool(register, deps) {
|
|
|
23
40
|
const refusal = unconfirmed(args, "This would remove that event at Google (emailing any attendees) and then here.");
|
|
24
41
|
if (refusal)
|
|
25
42
|
return refusal;
|
|
26
|
-
const response = await callAgentDoor(session, deps.fetchImpl, "DELETE", `/api/cal/events/${encodeURIComponent(String(args.event_id))}`);
|
|
43
|
+
const response = await callAgentDoor(session, deps.fetchImpl, "DELETE", `/api/cal/events/${encodeURIComponent(String(args.event_id))}${args.account ? `?account=${encodeURIComponent(String(args.account))}` : ""}`);
|
|
27
44
|
if (!response.ok)
|
|
28
45
|
return errorResult(doorFailureText("cal_delete", response));
|
|
29
46
|
const deleted = response.body.deleted;
|
|
30
47
|
if (!deleted)
|
|
31
48
|
return textResult("Tower answered without a result. Nothing to report.", response.body);
|
|
32
49
|
const alreadyGone = deleted.google === "already_gone_at_google";
|
|
50
|
+
const accountReceipt = deleted.account_name ? ` Account: ${deleted.account_name}.` : "";
|
|
33
51
|
const stored = deleted.local_store === "removed"
|
|
34
52
|
? "Tower's copy is removed."
|
|
35
53
|
: "Tower's copy is still there for now — the next sync will pick up that it is gone.";
|
|
36
54
|
return textResult(alreadyGone
|
|
37
|
-
? `Google already had that event gone. ${stored}`
|
|
38
|
-
: `Deleted at Google${deleted.notified === "all" ? " — attendees were notified." : "."} ${stored}`, { deleted });
|
|
55
|
+
? `Google already had that event gone. ${stored}${accountReceipt}`
|
|
56
|
+
: `Deleted at Google${deleted.notified === "all" ? " — attendees were notified." : "."} ${stored}${accountReceipt}`, { deleted });
|
|
39
57
|
}));
|
|
40
58
|
}
|
package/dist/cal-tools.js
CHANGED
|
@@ -169,7 +169,7 @@ export function registerCalTools(server, deps) {
|
|
|
169
169
|
? "No calendars are attached. A person attaches one with `cockpit cal add-ical` (personal) or by connecting "
|
|
170
170
|
+ "Google in the browser (work)."
|
|
171
171
|
: `${calendars.length} calendar(s).\n${calendars
|
|
172
|
-
.map((calendar) => `${calendar.name} ${calendar.provider} ${calendar.org_visible ? "shared" : "private"} ${calendar.status}${calendar.status_reason ? ` (${calendar.status_reason})` : ""} ${calendar.id}`)
|
|
172
|
+
.map((calendar) => `${calendar.account_label ?? calendar.provider} ${calendar.name} ${calendar.provider} ${calendar.org_visible ? "shared" : "private"} ${calendar.status}${calendar.status_reason ? ` (${calendar.status_reason})` : ""} ${calendar.id}`)
|
|
173
173
|
.join("\n")}`, { calendars });
|
|
174
174
|
}));
|
|
175
175
|
register("cal_create", {
|
|
@@ -179,7 +179,8 @@ export function registerCalTools(server, deps) {
|
|
|
179
179
|
+ "says so by name. Times are ISO instants, or plain dates with all_day. Requires confirm: true, because "
|
|
180
180
|
+ "attendees are emailed by Google the moment this succeeds.",
|
|
181
181
|
inputSchema: {
|
|
182
|
-
calendar_id: z.string().uuid().describe("A google_oauth calendar id you own (cal_calendars)."),
|
|
182
|
+
calendar_id: z.string().uuid().optional().describe("A google_oauth calendar id you own (cal_calendars)."),
|
|
183
|
+
account: z.string().min(1).max(100).optional().describe("Exact connected account label; omitted uses default."),
|
|
183
184
|
title: z.string().min(1).max(500).describe("The event title."),
|
|
184
185
|
starts_at: z.string().min(4).max(40).describe("An ISO instant, or a YYYY-MM-DD date with all_day."),
|
|
185
186
|
ends_at: z.string().min(4).max(40).describe("An ISO instant, or a YYYY-MM-DD date with all_day."),
|
|
@@ -196,6 +197,7 @@ export function registerCalTools(server, deps) {
|
|
|
196
197
|
return refusal;
|
|
197
198
|
const response = await callAgentDoor(session, deps.fetchImpl, "POST", "/api/cal/events", {
|
|
198
199
|
calendar_id: args.calendar_id,
|
|
200
|
+
...(args.account ? { account: args.account } : {}),
|
|
199
201
|
title: args.title,
|
|
200
202
|
starts_at: args.starts_at,
|
|
201
203
|
ends_at: args.ends_at,
|
|
@@ -209,6 +211,7 @@ export function registerCalTools(server, deps) {
|
|
|
209
211
|
return errorResult(doorFailureText("cal_create", response));
|
|
210
212
|
const event = response.body.event;
|
|
211
213
|
return textResult(`Created "${event?.title ?? String(args.title)}" at ${event?.starts_at ?? String(args.starts_at)}.`
|
|
214
|
+
+ (event?.account_name ? `\nAccount: ${event.account_name}.` : "")
|
|
212
215
|
+ towerIdLine(event)
|
|
213
216
|
+ (event?.html_link ? `\n${event.html_link}` : "")
|
|
214
217
|
+ (args.meet === true
|
package/dist/docs-msg-tools.js
CHANGED
|
@@ -39,7 +39,8 @@ export function registerDocsMsgTools(server, deps) {
|
|
|
39
39
|
const more = args.limit === undefined && all.length > DEFAULT_LIST_LIMIT;
|
|
40
40
|
const rows = more ? all.slice(0, DEFAULT_LIST_LIMIT) : all;
|
|
41
41
|
const lines = rows.map(docListLine).join("\n");
|
|
42
|
-
|
|
42
|
+
const counts = response.body.counts;
|
|
43
|
+
return textResult(`${rows.length} document(s).${counts ? ` ${counts.total} match in all. ${counts.coverage}` : ""}${moreNote(more, DOCS_LIST_MAX)}${lines ? `\n${lines}` : ""}`, { documents: rows.map(docListRow), more, counts });
|
|
43
44
|
}));
|
|
44
45
|
register("docs_tree", {
|
|
45
46
|
title: "Read the Tower document tree",
|
|
@@ -85,8 +85,10 @@ export function registerDocsNotionTools(server, deps) {
|
|
|
85
85
|
const runLine = ["push", "pull"]
|
|
86
86
|
.map((direction) => `${direction} ${runSentence(lastRun?.[direction] ?? null)}`)
|
|
87
87
|
.join(" · ");
|
|
88
|
-
|
|
88
|
+
const coverage = response.body.coverage ? `${response.body.coverage} ` : "";
|
|
89
|
+
return textResult(`${counts?.linked ?? 0} linked: ${summary}. ${coverage}Last run — ${runLine}.`, {
|
|
89
90
|
counts,
|
|
91
|
+
...response.body,
|
|
90
92
|
documents: response.body.documents ?? [],
|
|
91
93
|
last_run: lastRun ?? { push: null, pull: null },
|
|
92
94
|
});
|
package/dist/mail-compose.js
CHANGED
|
@@ -28,7 +28,8 @@ export const ATTACHMENTS_INPUT = z
|
|
|
28
28
|
/** The JSON both tools post. `attachments` is present only when there are files. */
|
|
29
29
|
export function composedBody(args, attachments = []) {
|
|
30
30
|
return {
|
|
31
|
-
account_id: args.account_id,
|
|
31
|
+
...(args.account_id ? { account_id: args.account_id } : {}),
|
|
32
|
+
...(args.account ? { account: args.account } : {}),
|
|
32
33
|
to: args.to.map((address) => ({ address })),
|
|
33
34
|
...(Array.isArray(args.cc) && args.cc.length > 0
|
|
34
35
|
? { cc: args.cc.map((address) => ({ address })) }
|
package/dist/mail-tools.js
CHANGED
|
@@ -34,6 +34,7 @@
|
|
|
34
34
|
* call, so a machine with no `cockpit login` pairing still serves this
|
|
35
35
|
* server's other tools and only a `mail_` call fails, by name.
|
|
36
36
|
*/
|
|
37
|
+
import { registerAccountTools } from "./account-tools.js";
|
|
37
38
|
import { z } from "zod";
|
|
38
39
|
import { callAgentDoor } from "./agent-door.js";
|
|
39
40
|
import { ATTACHMENTS_INPUT, attachmentResult, prepareMessage, unconfirmedAttachments } from "./mail-compose.js";
|
|
@@ -45,10 +46,11 @@ function messageLine(message) {
|
|
|
45
46
|
const ADDRESS = EMAIL_INPUT.describe("An email address.");
|
|
46
47
|
export function registerMailTools(server, deps) {
|
|
47
48
|
const register = registrarFor(server);
|
|
49
|
+
registerAccountTools(register, deps);
|
|
48
50
|
register("mail_accounts", {
|
|
49
51
|
title: "List attached mailboxes",
|
|
50
52
|
description: "Every mailbox attached to Tower that you may see — address, provider (gmail_oauth or imap), health and last sync. "
|
|
51
|
-
+ "One person may have several
|
|
53
|
+
+ "One person may have several, each with a label (e.g. personal 1) and per-source switches; one may be marked default. Start here when you need an account name or id.",
|
|
52
54
|
inputSchema: {},
|
|
53
55
|
}, async () => withSession(deps, async (session) => {
|
|
54
56
|
const response = await callAgentDoor(session, deps.fetchImpl, "GET", "/api/mail/accounts");
|
|
@@ -56,7 +58,7 @@ export function registerMailTools(server, deps) {
|
|
|
56
58
|
return errorResult(doorFailureText("mail_accounts", response));
|
|
57
59
|
const accounts = (Array.isArray(response.body.accounts) ? response.body.accounts : []);
|
|
58
60
|
const lines = accounts
|
|
59
|
-
.map((account) => `${account.address} ${account.provider} ${account.status}${account.status_reason ? ` (${account.status_reason})` : ""} ${account.id}`)
|
|
61
|
+
.map((account) => `${account.account_label ?? account.address} ${account.is_default ? "default" : ""} mail=${account.mail_enabled !== false} calendar=${account.calendar_enabled !== false} ${account.address} ${account.provider} ${account.status}${account.status_reason ? ` (${account.status_reason})` : ""} ${account.id}`)
|
|
60
62
|
.join("\n");
|
|
61
63
|
return textResult(accounts.length === 0
|
|
62
64
|
? "No mailboxes are attached. A person attaches one with `cockpit mail add-imap` (personal) or the browser (work)."
|
|
@@ -187,7 +189,8 @@ export function registerMailTools(server, deps) {
|
|
|
187
189
|
+ "see that argument for what it refuses. "
|
|
188
190
|
+ "This actually sends mail to real people: confirm must be true.",
|
|
189
191
|
inputSchema: {
|
|
190
|
-
account_id: z.string().uuid().describe("The mailbox
|
|
192
|
+
account_id: z.string().uuid().optional().describe("The mailbox id, or use account name/default."),
|
|
193
|
+
account: z.string().min(1).max(100).optional().describe("Exact connected account label."),
|
|
191
194
|
to: z.array(ADDRESS).min(1).max(50).describe("Recipient email addresses."),
|
|
192
195
|
cc: z.array(ADDRESS).max(50).optional().describe("Copied email addresses."),
|
|
193
196
|
subject: z.string().max(998).describe("The subject line."),
|
|
@@ -213,14 +216,14 @@ export function registerMailTools(server, deps) {
|
|
|
213
216
|
if (notConfirmed)
|
|
214
217
|
return errorResult(notConfirmed);
|
|
215
218
|
const files = attachmentResult(message.summary);
|
|
216
|
-
return textResult(`Sent from ${sent?.address ?? "that mailbox"} to ${args.to.join(", ")}${sent?.threaded ? " (as a reply)" : ""}.${files.sentence}`, { sent, ...files.structured });
|
|
219
|
+
return textResult(`Sent from ${sent?.account_name ?? sent?.address ?? "that mailbox"} to ${args.to.join(", ")}${sent?.threaded ? " (as a reply)" : ""}.${files.sentence}`, { sent, ...files.structured });
|
|
217
220
|
}));
|
|
218
221
|
register("mail_draft", {
|
|
219
222
|
title: "Save a draft in one mailbox",
|
|
220
223
|
description: "Saves a message as a DRAFT in one of YOUR mailboxes, for a person to read, edit and send themselves. Use it "
|
|
221
224
|
+ "when you are preparing mail on somebody's behalf and they should be the one to press send. NOTHING IS SENT and "
|
|
222
|
-
+ "nobody is notified, so unlike mail_send there is no confirm flag. `
|
|
223
|
-
+ "draft is saved in (mail_accounts)
|
|
225
|
+
+ "nobody is notified, so unlike mail_send there is no confirm flag. `account` is an exact label; `account_id` selects a mailbox id. Omit both for your default. The "
|
|
226
|
+
+ "draft is saved in that mailbox (mail_accounts). With several accounts and no default, Tower asks which one. "
|
|
224
227
|
+ "`in_reply_to_message_id` (a message id from mail_read) threads the draft into that conversation the way mail_send "
|
|
225
228
|
+ "threads a reply; Gmail files it there only when the subject matches the original's, and the answer says whether it "
|
|
226
229
|
+ "did (`threaded`). `attachments` ([{ path }]) puts files from this machine in the draft, up to 10 and 3,000,000 bytes "
|
|
@@ -228,7 +231,8 @@ export function registerMailTools(server, deps) {
|
|
|
228
231
|
+ "mailboxes can take one: an IMAP mailbox is refused as imap_drafts_unsupported. Answers the provider draft id, "
|
|
229
232
|
+ "the thread id and the account id.",
|
|
230
233
|
inputSchema: {
|
|
231
|
-
account_id: z.string().uuid().describe("The mailbox
|
|
234
|
+
account_id: z.string().uuid().optional().describe("The mailbox id, or use account name/default."),
|
|
235
|
+
account: z.string().min(1).max(100).optional().describe("Exact account label, e.g. work 1."),
|
|
232
236
|
to: z.array(ADDRESS).min(1).max(50).describe("Recipient email addresses, so the draft is ready to send as it is."),
|
|
233
237
|
cc: z.array(ADDRESS).max(50).optional().describe("Copied email addresses."),
|
|
234
238
|
subject: z.string().max(998).describe("The subject line. For a reply, keep the original's subject or Gmail will not thread it."),
|
|
@@ -252,7 +256,7 @@ export function registerMailTools(server, deps) {
|
|
|
252
256
|
const files = attachmentResult(message.summary);
|
|
253
257
|
// Both halves carry the ids (BLI-10432): a client that hands the model only
|
|
254
258
|
// `content`, or only `structuredContent`, must still learn where the draft is.
|
|
255
|
-
return textResult(`Saved a draft in ${draft?.address ?? "that mailbox"} for ${args.to.join(", ")}`
|
|
259
|
+
return textResult(`Saved a draft in ${draft?.account_name ?? draft?.address ?? "that mailbox"} for ${args.to.join(", ")}`
|
|
256
260
|
+ `${draft?.threaded ? " (in the reply's thread)" : args.in_reply_to_message_id ? " (Gmail did NOT file it in that thread: its subject has to match the original's)" : ""}. `
|
|
257
261
|
+ "Nothing was sent: it waits in Drafts for a person to read, edit and send."
|
|
258
262
|
+ `${files.sentence} `
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { callAgentDoor } from "./agent-door.js";
|
|
3
|
+
import { registrarFor, withSession, textResult, errorResult, doorFailureText } from "./tool-result.js";
|
|
4
|
+
export function registerNotesBotUpcomingTool(server, deps) {
|
|
5
|
+
registrarFor(server)("notes_bot_upcoming", {
|
|
6
|
+
title: "Bot meetings today", description: "Your own Google Meet meetings today the Tower bot will join, with claim state. Decline the event or mark it private to opt out.",
|
|
7
|
+
inputSchema: { tz: z.string().optional().describe("Your IANA time zone; defaults to UTC.") },
|
|
8
|
+
}, async (args) => withSession(deps, async (session) => {
|
|
9
|
+
const response = await callAgentDoor(session, deps.fetchImpl, "GET", `/api/notetaker/upcoming?tz=${encodeURIComponent(String(args.tz ?? "UTC"))}`);
|
|
10
|
+
if (!response.ok)
|
|
11
|
+
return errorResult(doorFailureText("notes_bot_upcoming", response));
|
|
12
|
+
return textResult(JSON.stringify(response.body), response.body);
|
|
13
|
+
}));
|
|
14
|
+
}
|
package/dist/server.js
CHANGED
|
@@ -17,6 +17,7 @@ import { registerCalTools } from "./cal-tools.js";
|
|
|
17
17
|
import { registerMailTools } from "./mail-tools.js";
|
|
18
18
|
import { registerNotesAccountTools } from "./notes-account-tools.js";
|
|
19
19
|
import { registerNotesTools } from "./notes-tools.js";
|
|
20
|
+
import { registerNotesBotUpcomingTool } from "./notes-bot-upcoming-tool.js";
|
|
20
21
|
import { registerNotesGranolaStatusTool } from "./notes-granola-status-tool.js";
|
|
21
22
|
import { registerNotesWriteTools } from "./notes-write-tools.js";
|
|
22
23
|
import { registerMemoryConsolidationTools } from "./memory-consolidation-tools.js";
|
|
@@ -476,6 +477,7 @@ export function createServer(deps, options = {}) {
|
|
|
476
477
|
registerNotesTools(server, { fetchImpl: deps.fetchImpl });
|
|
477
478
|
// Ticket 10718: the Granola cron's own health, read-only, over the shared
|
|
478
479
|
// server-side key rather than a `notes_accounts` row.
|
|
480
|
+
registerNotesBotUpcomingTool(server, { fetchImpl: deps.fetchImpl });
|
|
479
481
|
registerNotesGranolaStatusTool(server, { fetchImpl: deps.fetchImpl });
|
|
480
482
|
registerOpsTools(server, { fetchImpl: deps.fetchImpl });
|
|
481
483
|
registerMemoryExperienceTools(server, { fetchImpl: deps.fetchImpl });
|
package/dist/setup-tools.js
CHANGED
|
@@ -15,6 +15,6 @@ export function registerSetupTools(server, deps) {
|
|
|
15
15
|
if (args.compare)
|
|
16
16
|
query.set("compare", String(args.compare));
|
|
17
17
|
const response = await callAgentDoor(session, deps.fetchImpl, "GET", `/api/setup?${query}`);
|
|
18
|
-
return response.ok ? dataResult(
|
|
18
|
+
return response.ok ? dataResult(`Agent setups and outcomes over the requested window. ${response.body.read_coverage?.join(" ") ?? ""}`, response.body) : errorResult(doorFailureText("setup_read", response));
|
|
19
19
|
}));
|
|
20
20
|
}
|
package/dist/tool-catalog.d.ts
CHANGED
|
@@ -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", "ops_status", "slack_coverage", "slack_read", "memory_rules", "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", "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_read", "memory_rules", "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", "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 ["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"];
|
|
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
|
/**
|
package/dist/tool-catalog.js
CHANGED
|
@@ -71,6 +71,7 @@ export const READ_TOOLS = [
|
|
|
71
71
|
"notes_recording",
|
|
72
72
|
"notes_sync_status",
|
|
73
73
|
"notes_granola_status",
|
|
74
|
+
"notes_bot_upcoming",
|
|
74
75
|
"ops_status",
|
|
75
76
|
"slack_coverage",
|
|
76
77
|
"slack_read",
|
|
@@ -102,6 +103,7 @@ export const DRY_RUN_TOOLS = ["ops_recompile", "memory_requeue"];
|
|
|
102
103
|
* suites (`write-tools.test.ts` and each family's own).
|
|
103
104
|
*/
|
|
104
105
|
export const WRITE_TOOLS = [
|
|
106
|
+
"brain_retry",
|
|
105
107
|
"emit_event",
|
|
106
108
|
"docs_create",
|
|
107
109
|
"docs_update",
|
|
@@ -130,6 +132,7 @@ export const WRITE_TOOLS = [
|
|
|
130
132
|
"mail_send",
|
|
131
133
|
// BLI-12530: writes into a mailbox (a draft), reaches nobody. Never smoked.
|
|
132
134
|
"mail_draft",
|
|
135
|
+
"mail_rename", "mail_enable", "mail_disable", "mail_default", "mail_remove", "cal_change",
|
|
133
136
|
"mail_sync",
|
|
134
137
|
"mail_detach",
|
|
135
138
|
"cal_create",
|
package/dist/usage-tools.d.ts
CHANGED
package/dist/usage-tools.js
CHANGED
|
@@ -21,6 +21,7 @@ export function githubWords(github, people) {
|
|
|
21
21
|
`GitHub: ${noun(sum("pull_requests_opened"), "pull request")} opened, ${sum("pull_requests_merged")} merged, ${noun(sum("commits"), "commit")}, ` +
|
|
22
22
|
`from ${noun((github.repositories ?? []).length, "mirrored repository", "mirrored repositories")}${range}.`,
|
|
23
23
|
];
|
|
24
|
+
lines.push(...(github.read_coverage ?? []));
|
|
24
25
|
const unmatched = github.unattributed;
|
|
25
26
|
if (unmatched && unmatched.pull_requests_opened + unmatched.pull_requests_merged + unmatched.commits > 0) {
|
|
26
27
|
lines.push(`Not matched to a person: ${noun(unmatched.pull_requests_opened, "pull request")} opened, ${unmatched.pull_requests_merged} merged, ${noun(unmatched.commits, "commit")}.`);
|
|
@@ -80,7 +81,7 @@ export function registerUsageTools(server, deps) {
|
|
|
80
81
|
// agent reading only the text half cannot quote a pull-request count without
|
|
81
82
|
// what it covers and who it left out.
|
|
82
83
|
const github = githubSentence(response.body);
|
|
83
|
-
return dataResult(`${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);
|
|
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);
|
|
84
85
|
}));
|
|
85
86
|
registrarFor(server)("usage_sessions", {
|
|
86
87
|
title: "Read task or subject sessions",
|
|
@@ -117,6 +118,6 @@ export function registerUsageTools(server, deps) {
|
|
|
117
118
|
return dataResult(`${windows} digest window(s) of session ${String(args.session)}.`, response.body);
|
|
118
119
|
}
|
|
119
120
|
const sessions = Array.isArray(response.body.sessions) ? response.body.sessions.length : 0;
|
|
120
|
-
return dataResult(`${sessions} session(s) for ${args.task ? `task ${String(args.task)}` : `topic "${String(args.subject)}"`}.`, response.body);
|
|
121
|
+
return dataResult(`${response.body.read_coverage?.join(" ") ?? ""} ${sessions} session(s) for ${args.task ? `task ${String(args.task)}` : `topic "${String(args.subject)}"`}.`, response.body);
|
|
121
122
|
}));
|
|
122
123
|
}
|
package/dist/verb-census.js
CHANGED
|
@@ -239,6 +239,7 @@ export const MCP_TWINS = {
|
|
|
239
239
|
// this machine's collector logs and the `vercel` / `railway` CLIs.
|
|
240
240
|
// 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.
|
|
241
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)" },
|
|
242
|
+
"brain retry": { tool: "brain_retry", door: "POST /api/brain/incidents/retry" },
|
|
242
243
|
"brain status": { tool: "brain_status", door: "GET /api/brain/incidents" },
|
|
243
244
|
"usage sessions": { tool: "usage_sessions", door: "GET /api/usage/sessions" },
|
|
244
245
|
"setup": { tool: "setup_read", door: "GET /api/setup" },
|
|
@@ -272,6 +273,11 @@ export const MCP_TWINS = {
|
|
|
272
273
|
"issue comment": { tool: "work_comment_issue", door: "POST /api/work/issues/[id]/comments" },
|
|
273
274
|
"issue history": { tool: "work_issue_history", door: "GET /api/work/issues/[id]/history" },
|
|
274
275
|
"project list": { tool: "work_list_projects", door: "GET /api/work/projects" },
|
|
276
|
+
"mail rename": { tool: "mail_rename", door: "PATCH /api/mail/accounts" },
|
|
277
|
+
"mail enable": { tool: "mail_enable", door: "PATCH /api/mail/accounts" },
|
|
278
|
+
"mail disable": { tool: "mail_disable", door: "PATCH /api/mail/accounts" },
|
|
279
|
+
"mail default": { tool: "mail_default", door: "PATCH /api/mail/accounts" },
|
|
280
|
+
"mail remove": { tool: "mail_remove", door: "DELETE /api/mail/accounts" },
|
|
275
281
|
"mail accounts": { tool: "mail_accounts", door: "GET /api/mail/accounts" },
|
|
276
282
|
"mail inbox": { tool: "mail_inbox", door: "GET /api/mail/inbox" },
|
|
277
283
|
"mail read": { tool: "mail_read", door: "GET /api/mail/threads/[id]" },
|
|
@@ -288,6 +294,8 @@ export const MCP_TWINS = {
|
|
|
288
294
|
"cal next": { tool: "cal_next", door: "GET /api/cal/next" },
|
|
289
295
|
"cal find": { tool: "cal_find", door: "GET /api/cal/find" },
|
|
290
296
|
"cal calendars": { tool: "cal_calendars", door: "GET /api/cal/calendars" },
|
|
297
|
+
"cal change": { tool: "cal_change", door: "PATCH /api/cal/events/[eventId]" },
|
|
298
|
+
"cal cancel": { tool: "cal_delete", door: "DELETE /api/cal/events/[eventId]" },
|
|
291
299
|
"cal create": { tool: "cal_create", door: "POST /api/cal/events" },
|
|
292
300
|
"cal share": { tool: "cal_share", door: "PATCH /api/cal/calendars" },
|
|
293
301
|
"cal sync": { tool: "cal_sync", door: "POST /api/cal/calendars/[id]/sync" },
|
|
@@ -325,6 +333,7 @@ export const MCP_TWINS = {
|
|
|
325
333
|
"notes sync": { tool: "notes_sync", door: "POST /api/notes/accounts/[id]/sync" },
|
|
326
334
|
"notes sync-status": { tool: "notes_sync_status", door: "GET /api/notes/sync-status" },
|
|
327
335
|
// Ticket 10718: Granola's own cron health, over its shared server-side key.
|
|
336
|
+
"notes bot-upcoming": { tool: "notes_bot_upcoming", door: "GET /api/notetaker/upcoming" },
|
|
328
337
|
"notes granola-status": { tool: "notes_granola_status", door: "GET /api/notes/granola-status" },
|
|
329
338
|
"memory log": { tool: "memory_experience", door: "POST /api/memory/experience" },
|
|
330
339
|
// BLI-4047, BLI-12927: the memory STORE at a terminal. Its twins live on the
|
package/dist/work-tools.js
CHANGED
|
@@ -135,10 +135,11 @@ export function registerWorkTools(server, deps) {
|
|
|
135
135
|
+ ` [p${issue.priority ?? 0}${size ? `, size ${size}` : ""}, assignee ${assigneeLabel(issue.assignee_id, directory)}, project ${issue.project_id ?? "none"}]`;
|
|
136
136
|
})
|
|
137
137
|
.join("\n");
|
|
138
|
-
const
|
|
138
|
+
const counts = response.body.counts;
|
|
139
|
+
const totalNote = counts ? ` ${counts.total} match in all. ${counts.coverage}` : completeness?.total != null && completeness.total > issues.length ? ` ${completeness.total} match in all.` : "";
|
|
139
140
|
return textResult(`${issues.length} issue(s).${totalNote}${moreNote(more, ISSUE_LIST_MAX)}${outsideNote(outside)}${lines ? `\n${lines}` : ""}`, {
|
|
140
141
|
issues: issues.map((issue) => withSizeLabel(withAssigneeIdentity(issueListRow(issue), directory))),
|
|
141
|
-
more,
|
|
142
|
+
more, counts,
|
|
142
143
|
...(completeness ? { total: completeness.total } : {}),
|
|
143
144
|
...(outside ? { outside_project: outside } : {}),
|
|
144
145
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bli-cockpit/mcp",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.111",
|
|
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",
|