@bli-cockpit/mcp 0.1.30 → 0.1.31
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 +8 -1
- package/dist/careers-tools.d.ts +4 -0
- package/dist/careers-tools.js +19 -0
- package/dist/note-folder-tools.d.ts +4 -0
- package/dist/note-folder-tools.js +57 -0
- package/dist/notes-tools.d.ts +0 -17
- package/dist/notes-tools.js +3 -1
- package/dist/notes-write-tools.js +20 -5
- package/dist/server.js +2 -0
- package/dist/verb-census.js +7 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -34,7 +34,7 @@ about what an agent can reach.
|
|
|
34
34
|
|
|
35
35
|
<!-- BEGIN GENERATED verb census — `npm run mcp:readme` -->
|
|
36
36
|
|
|
37
|
-
**
|
|
37
|
+
**90 of 93 Tower verbs have an MCP twin.**
|
|
38
38
|
Each tool goes through the SAME door its CLI verb calls, with the same
|
|
39
39
|
collector device token — never a second route and never a service-role
|
|
40
40
|
reader. `src/verb-census.test.ts` fails when a verb is in none of the
|
|
@@ -55,6 +55,9 @@ three tables below.
|
|
|
55
55
|
| `cockpit cal sync` | `cal_sync` | `POST /api/cal/calendars/[id]/sync` |
|
|
56
56
|
| `cockpit cal today` | `cal_today` | `GET /api/cal/today` |
|
|
57
57
|
| `cockpit cal week` | `cal_week` | `GET /api/cal/week` |
|
|
58
|
+
| `cockpit careers list` | `careers_list` | `GET /api/careers/applications` |
|
|
59
|
+
| `cockpit careers rescreen` | `careers_rescreen` | `POST /api/careers/applications/[id]/rescreen` |
|
|
60
|
+
| `cockpit careers show` | `careers_show` | `GET /api/careers/applications/[id]` |
|
|
58
61
|
| `cockpit correct` | `brief_correct` | `POST /api/jarvis/corrections` |
|
|
59
62
|
| `cockpit docs create` | `docs_create` | `POST /api/docs/documents` |
|
|
60
63
|
| `cockpit docs list` | `docs_list` | `GET /api/docs/documents` |
|
|
@@ -94,10 +97,14 @@ three tables below.
|
|
|
94
97
|
| `cockpit msg read` | `msg_read` | `GET /api/msg/channels/[id]/messages` |
|
|
95
98
|
| `cockpit msg send` | `msg_send` | `POST /api/msg/channels/[id]/messages` |
|
|
96
99
|
| `cockpit msg thread` | `msg_thread` | `GET /api/msg/channels/[id]/messages?thread_parent_id=` |
|
|
100
|
+
| `cockpit notes folders` | `notes_folders` | `GET /api/notes/folders` |
|
|
97
101
|
| `cockpit notes list` | `notes_list` | `GET /api/notes/library` |
|
|
102
|
+
| `cockpit notes mkdir` | `notes_mkdir` | `POST /api/notes/folders` |
|
|
98
103
|
| `cockpit notes move` | `notes_move` | `POST /api/notes/move` |
|
|
99
104
|
| `cockpit notes paste` | `notes_paste` | `POST /api/notes/upload (text)` |
|
|
100
105
|
| `cockpit notes place` | `notes_place` | `POST /api/notes/place` |
|
|
106
|
+
| `cockpit notes rename` | `notes_rename` | `PATCH /api/notes/folders/[id]` |
|
|
107
|
+
| `cockpit notes rmdir` | `notes_rmdir` | `DELETE /api/notes/folders/[id]` |
|
|
101
108
|
| `cockpit notes share` | `notes_share` | `POST /api/notes/share` |
|
|
102
109
|
| `cockpit notes shelf` | `notes_shelf` | `GET /api/notes/shelf` |
|
|
103
110
|
| `cockpit notes shelves` | `notes_shelves` | `GET /api/notes/library` |
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { callAgentDoor } from './agent-door.js';
|
|
3
|
+
import { doorFailureText, errorResult, registrarFor, textResult, withSession } from './tool-result.js';
|
|
4
|
+
export function registerCareersTools(server, deps) {
|
|
5
|
+
const register = registrarFor(server);
|
|
6
|
+
register('careers_list', { title: 'List applications', description: 'Super-admin only. Newest applications, bounded to 100 with total and has_more.', inputSchema: { role: z.string().optional(), min_score: z.number().min(0).max(100).optional(), since: z.string().optional() } }, async (args) => withSession(deps, async (session) => {
|
|
7
|
+
const query = new URLSearchParams();
|
|
8
|
+
for (const key of ['role', 'min_score', 'since'])
|
|
9
|
+
if (args[key] !== undefined)
|
|
10
|
+
query.set(key, String(args[key]));
|
|
11
|
+
const answer = await callAgentDoor(session, deps.fetchImpl, 'GET', `/api/careers/applications?${query}`);
|
|
12
|
+
return answer.ok ? textResult('Applications', answer.body) : errorResult(doorFailureText('careers_list', answer));
|
|
13
|
+
}));
|
|
14
|
+
for (const action of ['show', 'rescreen'])
|
|
15
|
+
register(`careers_${action}`, { title: `${action} application`, description: `Super-admin only. ${action} one application.`, inputSchema: { id: z.string().uuid() } }, async (args) => withSession(deps, async (session) => {
|
|
16
|
+
const answer = await callAgentDoor(session, deps.fetchImpl, action === 'show' ? 'GET' : 'POST', `/api/careers/applications/${encodeURIComponent(String(args.id))}${action === 'rescreen' ? '/rescreen' : ''}`, undefined, 60_000);
|
|
17
|
+
return answer.ok ? textResult('Application', answer.body) : errorResult(doorFailureText(`careers_${action}`, answer));
|
|
18
|
+
}));
|
|
19
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
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 registerNoteFolderTools(server, deps) {
|
|
5
|
+
const register = registrarFor(server);
|
|
6
|
+
register("notes_folders", {
|
|
7
|
+
title: "List note folders", description: "The folder tree with caller-visible note counts.",
|
|
8
|
+
inputSchema: { tree: z.boolean().optional() },
|
|
9
|
+
}, async () => withSession(deps, async (session) => {
|
|
10
|
+
const response = await callAgentDoor(session, deps.fetchImpl, "GET", "/api/notes/folders");
|
|
11
|
+
if (!response.ok)
|
|
12
|
+
return errorResult(doorFailureText("notes_folders", response));
|
|
13
|
+
return textResult(JSON.stringify(response.body), response.body);
|
|
14
|
+
}));
|
|
15
|
+
register("notes_mkdir", {
|
|
16
|
+
title: "Create a note folder", description: "Create a path, including missing ancestors. Existing paths are reused.",
|
|
17
|
+
inputSchema: { path: z.string().min(1).max(2000) },
|
|
18
|
+
}, async (args) => withSession(deps, async (session) => {
|
|
19
|
+
const response = await callAgentDoor(session, deps.fetchImpl, "POST", "/api/notes/folders", { path: args.path });
|
|
20
|
+
if (!response.ok)
|
|
21
|
+
return errorResult(doorFailureText("notes_mkdir", response));
|
|
22
|
+
return textResult(JSON.stringify(response.body), response.body);
|
|
23
|
+
}));
|
|
24
|
+
register("notes_rmdir", {
|
|
25
|
+
title: "Delete an empty note folder", description: "Delete a folder by path. Refuses any folder with notes or children.",
|
|
26
|
+
inputSchema: { path: z.string().min(1).max(2000) },
|
|
27
|
+
}, async (args) => withSession(deps, async (session) => {
|
|
28
|
+
const listed = await callAgentDoor(session, deps.fetchImpl, "GET", "/api/notes/folders");
|
|
29
|
+
if (!listed.ok)
|
|
30
|
+
return errorResult(doorFailureText("notes_rmdir", listed));
|
|
31
|
+
const body = listed.body;
|
|
32
|
+
const path = String(args.path).split("/").map(part => part.trim()).join("/");
|
|
33
|
+
const folder = body.folders.find(folder => folder.path === path);
|
|
34
|
+
if (!folder)
|
|
35
|
+
return errorResult("That folder could not be found.");
|
|
36
|
+
const response = await callAgentDoor(session, deps.fetchImpl, "DELETE", `/api/notes/folders/${encodeURIComponent(folder.id)}`);
|
|
37
|
+
if (!response.ok)
|
|
38
|
+
return errorResult(doorFailureText("notes_rmdir", response));
|
|
39
|
+
return textResult(JSON.stringify(response.body), response.body);
|
|
40
|
+
}));
|
|
41
|
+
register("notes_rename", {
|
|
42
|
+
title: "Rename a note folder", description: "Rename a folder by path, updating every descendant path.",
|
|
43
|
+
inputSchema: { path: z.string().min(1).max(2000), name: z.string().min(1).max(120) },
|
|
44
|
+
}, async (args) => withSession(deps, async (session) => {
|
|
45
|
+
const listed = await callAgentDoor(session, deps.fetchImpl, "GET", "/api/notes/folders");
|
|
46
|
+
if (!listed.ok)
|
|
47
|
+
return errorResult(doorFailureText("notes_rename", listed));
|
|
48
|
+
const body = listed.body;
|
|
49
|
+
const folder = body.folders.find(folder => folder.path === String(args.path).split("/").map(part => part.trim()).join("/"));
|
|
50
|
+
if (!folder)
|
|
51
|
+
return errorResult("That folder could not be found.");
|
|
52
|
+
const response = await callAgentDoor(session, deps.fetchImpl, "PATCH", `/api/notes/folders/${encodeURIComponent(folder.id)}`, { name: args.name });
|
|
53
|
+
if (!response.ok)
|
|
54
|
+
return errorResult(doorFailureText("notes_rename", response));
|
|
55
|
+
return textResult(JSON.stringify(response.body), response.body);
|
|
56
|
+
}));
|
|
57
|
+
}
|
package/dist/notes-tools.d.ts
CHANGED
|
@@ -1,20 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* `notes_*` MCP tools (BLI-3756) — the meeting-notes library, read by an
|
|
3
|
-
* agent, over the same `/api/notes/**` doors `cockpit notes` calls with the
|
|
4
|
-
* same device token.
|
|
5
|
-
*
|
|
6
|
-
* **The narrowing is always said out loud.** `lib/notes/api-doors.ts` gives a
|
|
7
|
-
* caller with no signed-in browser session a NARROWER read and names it in the
|
|
8
|
-
* answer's own `scope`/`degradedBecause` — a device token is exactly such a
|
|
9
|
-
* caller, so an agent reading this library is routinely seeing less than a
|
|
10
|
-
* person would in a browser. Every tool here appends that reason to its text.
|
|
11
|
-
* Swallowing it would let an agent conclude a note was never taken when in
|
|
12
|
-
* fact it simply was not this caller's to read, which is a fact invented out
|
|
13
|
-
* of a permission.
|
|
14
|
-
*
|
|
15
|
-
* Reads only. `notes move|share|unshare|paste|upload` are batch 2 — sharing is
|
|
16
|
-
* a deliberate act and the paste door takes a body on stdin by rule.
|
|
17
|
-
*/
|
|
18
1
|
import { type ToolDeps } from "./tool-result.js";
|
|
19
2
|
export type NotesDeps = ToolDeps;
|
|
20
3
|
interface ScopedBody {
|
package/dist/notes-tools.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { registerNoteFolderTools } from "./note-folder-tools.js";
|
|
1
2
|
/**
|
|
2
3
|
* `notes_*` MCP tools (BLI-3756) — the meeting-notes library, read by an
|
|
3
4
|
* agent, over the same `/api/notes/**` doors `cockpit notes` calls with the
|
|
@@ -50,6 +51,7 @@ const LIBRARY_FILTERS = {
|
|
|
50
51
|
limit: z.number().int().min(1).max(500).optional(),
|
|
51
52
|
};
|
|
52
53
|
export function registerNotesTools(server, deps) {
|
|
54
|
+
registerNoteFolderTools(server, deps);
|
|
53
55
|
const register = registrarFor(server);
|
|
54
56
|
register("notes_list", {
|
|
55
57
|
title: "List Tower meeting notes",
|
|
@@ -92,7 +94,7 @@ export function registerNotesTools(server, deps) {
|
|
|
92
94
|
}
|
|
93
95
|
const room = note.participants.length > 0 ? `\nIn the room: ${note.participants.join(", ")}` : "";
|
|
94
96
|
return textResult(`${note.title}\n${note.meetingDate} · ${note.shelf} · ${note.fileName} · ${note.lineCount} lines`
|
|
95
|
-
+ `${room}\n${note.visibility}\n\n${note.content}${narrowingNote(body)}`, { scope: body.scope ?? null, note });
|
|
97
|
+
+ `${room}\n${note.visibility}${body.audio ? "\nTranscription: " + body.audio.transcription_status : ""}\n\n${note.content}${narrowingNote(body)}`, { scope: body.scope ?? null, note, audio: body.audio ?? null });
|
|
96
98
|
}));
|
|
97
99
|
register("notes_shelf", {
|
|
98
100
|
title: "Your own Tower notes shelf",
|
|
@@ -77,6 +77,9 @@ async function readNoteFile(filePath) {
|
|
|
77
77
|
}
|
|
78
78
|
if (size === 0)
|
|
79
79
|
return refuse("file_empty");
|
|
80
|
+
if (/\.(mp3|m4a|wav|webm|ogg)$/i.test(fileName) && size > 4_000_000) {
|
|
81
|
+
return { ok: false, refusal: "file_too_big", sentence: "file_too_large: Audio is limited to 4 MB (4,000,000 bytes)." };
|
|
82
|
+
}
|
|
80
83
|
if (size > NOTE_FILE_MAX_BYTES)
|
|
81
84
|
return refuse("file_too_big");
|
|
82
85
|
try {
|
|
@@ -102,18 +105,19 @@ async function postForm(deps, session, tool, form) {
|
|
|
102
105
|
// `stored: false` is the door saying it did not keep the note. That is a
|
|
103
106
|
// refusal with a reason, not a success with a caveat.
|
|
104
107
|
return body.stored === true
|
|
105
|
-
? textResult(text, { ok: true, noteId: body.noteId ?? null, scope: body.scope ?? null })
|
|
108
|
+
? textResult(text, { ok: true, noteId: body.noteId ?? null, scope: body.scope ?? null, transcription: body.transcription ?? null })
|
|
106
109
|
: errorResult(text);
|
|
107
110
|
}
|
|
108
111
|
export function registerNotesWriteTools(server, deps) {
|
|
109
112
|
const register = registrarFor(server);
|
|
110
113
|
register("notes_upload", {
|
|
111
114
|
title: "Put a local file in as a Tower meeting note",
|
|
112
|
-
description: "Uploads a transcript or
|
|
115
|
+
description: "Uploads a transcript, note, or MP3/M4A/WAV/WebM/OGG audio file from this machine. Audio is limited to 4 MB and returns a note id plus transcription status immediately. Poll notes_show for completion. "
|
|
113
116
|
+ "A name that looks like a key or credential file is refused without being opened; so is a missing, empty or "
|
|
114
117
|
+ "over-20-MB file. Tower reads the whole note with one model call, so a large file can take a couple of "
|
|
115
118
|
+ "minutes and your client may give up first — the upload keeps going; notes_list will show it.",
|
|
116
119
|
inputSchema: {
|
|
120
|
+
folder: z.string().min(1).max(2000).optional().describe("Folder path; creates missing ancestors. / means Root."),
|
|
117
121
|
path: z.string().min(1).max(4096).describe("An absolute path on this machine."),
|
|
118
122
|
exclude: z
|
|
119
123
|
.string()
|
|
@@ -128,9 +132,13 @@ export function registerNotesWriteTools(server, deps) {
|
|
|
128
132
|
if (!read.ok)
|
|
129
133
|
return errorResult(`Refused (${read.refusal}): ${read.sentence} Nothing was sent.`);
|
|
130
134
|
const form = new FormData();
|
|
131
|
-
|
|
135
|
+
const audioTypes = { mp3: "audio/mpeg", m4a: "audio/mp4", wav: "audio/wav", webm: "audio/webm", ogg: "audio/ogg" };
|
|
136
|
+
const mediaType = audioTypes[path.extname(read.fileName).slice(1).toLowerCase()] ?? "";
|
|
137
|
+
form.set("file", new File([new Uint8Array(read.bytes)], read.fileName, { type: mediaType }));
|
|
132
138
|
if (args.exclude)
|
|
133
139
|
form.set("exclusions", String(args.exclude));
|
|
140
|
+
if (args.folder)
|
|
141
|
+
form.set("folder_path", String(args.folder));
|
|
134
142
|
return postForm(deps, session, "notes_upload", form);
|
|
135
143
|
}));
|
|
136
144
|
register("notes_paste", {
|
|
@@ -139,6 +147,7 @@ export function registerNotesWriteTools(server, deps) {
|
|
|
139
147
|
+ "`cockpit notes paste` posts to. The CLI takes the body on stdin; here it is an argument, so the text "
|
|
140
148
|
+ "travels in the request and is never echoed back to you.",
|
|
141
149
|
inputSchema: {
|
|
150
|
+
folder: z.string().min(1).max(2000).optional().describe("Folder path; creates missing ancestors."),
|
|
142
151
|
text: z.string().min(1).max(PASTE_MAX_CHARS).describe("The note itself."),
|
|
143
152
|
name: z.string().min(1).max(300).optional().describe("What to call it. Tower names it by date if you do not."),
|
|
144
153
|
exclude: z.string().min(1).max(2000).optional().describe("Anything to leave out, in plain words."),
|
|
@@ -154,6 +163,8 @@ export function registerNotesWriteTools(server, deps) {
|
|
|
154
163
|
form.set("name", String(args.name));
|
|
155
164
|
if (args.exclude)
|
|
156
165
|
form.set("exclusions", String(args.exclude));
|
|
166
|
+
if (args.folder)
|
|
167
|
+
form.set("folder_path", String(args.folder));
|
|
157
168
|
return postForm(deps, session, "notes_paste", form);
|
|
158
169
|
}));
|
|
159
170
|
register("notes_share", {
|
|
@@ -182,15 +193,19 @@ export function registerNotesWriteTools(server, deps) {
|
|
|
182
193
|
+ "own move box sends. A shelf is free text; notes_shelves lists the ones in use.",
|
|
183
194
|
inputSchema: {
|
|
184
195
|
note_id: z.string().min(1).max(200).describe("A note id."),
|
|
196
|
+
folder: z.string().min(1).max(2000).optional().describe("Destination folder path; / means Root. Leaves the shelf unchanged."),
|
|
185
197
|
to: z.string().min(1).max(200).optional().describe("The shelf to move it to."),
|
|
186
198
|
clear: z.boolean().optional().describe("Take it off every shelf instead."),
|
|
187
199
|
},
|
|
188
200
|
}, async (args) => withSession(deps, async (session) => {
|
|
189
201
|
const clearing = args.clear === true;
|
|
190
|
-
if (!clearing && !args.to) {
|
|
202
|
+
if (!clearing && !args.to && !args.folder) {
|
|
191
203
|
return errorResult("Refused (no_destination): say which shelf with `to`, or pass `clear: true`. Nothing was moved.");
|
|
192
204
|
}
|
|
193
|
-
const response = await callAgentDoor(session, deps.fetchImpl, "POST", "/api/notes/move", { note_id: String(args.note_id ?? ""),
|
|
205
|
+
const response = await callAgentDoor(session, deps.fetchImpl, "POST", "/api/notes/move", { note_id: String(args.note_id ?? ""),
|
|
206
|
+
...(args.folder ? { folder_path: String(args.folder) } : {}),
|
|
207
|
+
...(clearing || args.to ? { category: clearing ? "" : String(args.to) } : {}),
|
|
208
|
+
}, WRITE_DEADLINE_MS);
|
|
194
209
|
if (!response.ok)
|
|
195
210
|
return errorResult(doorFailureText("notes_move", response));
|
|
196
211
|
const body = response.body;
|
package/dist/server.js
CHANGED
|
@@ -23,6 +23,7 @@ import { registerSettingsTools } from "./settings-tools.js";
|
|
|
23
23
|
import { registerSettingsWriteTools } from "./settings-write-tools.js";
|
|
24
24
|
import { registerTeamWriteTools } from "./team-write-tools.js";
|
|
25
25
|
import { registerWorkTools } from "./work-tools.js";
|
|
26
|
+
import { registerCareersTools } from "./careers-tools.js";
|
|
26
27
|
import { registerUsageTools } from "./usage-tools.js";
|
|
27
28
|
export const PACKAGE_NAME = "@bli-cockpit/mcp";
|
|
28
29
|
export const PACKAGE_VERSION = "0.1.4";
|
|
@@ -391,6 +392,7 @@ export function createServer(deps) {
|
|
|
391
392
|
// the docs/msg tools above, for the same reason: a coding session should
|
|
392
393
|
// file and move a Tower issue the way it files and moves a Linear one.
|
|
393
394
|
registerWorkTools(server, { fetchImpl: deps.fetchImpl });
|
|
395
|
+
registerCareersTools(server, deps);
|
|
394
396
|
registerUsageTools(server, { fetchImpl: deps.fetchImpl });
|
|
395
397
|
// BLI-3708: the mailboxes a person attached. Reads, a send and a sync over
|
|
396
398
|
// the same /api/mail/** doors `cockpit mail` calls; `add-imap` deliberately
|
package/dist/verb-census.js
CHANGED
|
@@ -209,6 +209,9 @@ export function memoryMcpToolNames() {
|
|
|
209
209
|
}
|
|
210
210
|
/** A CLI verb and the MCP tool that does the same thing, over the same door. */
|
|
211
211
|
export const MCP_TWINS = {
|
|
212
|
+
"careers list": { tool: "careers_list", door: "GET /api/careers/applications" },
|
|
213
|
+
"careers show": { tool: "careers_show", door: "GET /api/careers/applications/[id]" },
|
|
214
|
+
"careers rescreen": { tool: "careers_rescreen", door: "POST /api/careers/applications/[id]/rescreen" },
|
|
212
215
|
"usage people": { tool: "usage_people", door: "GET /api/usage/people" },
|
|
213
216
|
"docs list": { tool: "docs_list", door: "GET /api/docs/documents" },
|
|
214
217
|
"docs tree": { tool: "docs_tree", door: "GET /api/docs/tree" },
|
|
@@ -251,6 +254,10 @@ export const MCP_TWINS = {
|
|
|
251
254
|
"brief status": { tool: "brief_status", door: "GET /api/ops/brief-status" },
|
|
252
255
|
"brief rewrite": { tool: "brief_rewrite", door: "POST /api/jarvis/recompile" },
|
|
253
256
|
correct: { tool: "brief_correct", door: "POST /api/jarvis/corrections" },
|
|
257
|
+
"notes folders": { tool: "notes_folders", door: "GET /api/notes/folders" },
|
|
258
|
+
"notes mkdir": { tool: "notes_mkdir", door: "POST /api/notes/folders" },
|
|
259
|
+
"notes rename": { tool: "notes_rename", door: "PATCH /api/notes/folders/[id]" },
|
|
260
|
+
"notes rmdir": { tool: "notes_rmdir", door: "DELETE /api/notes/folders/[id]" },
|
|
254
261
|
"notes list": { tool: "notes_list", door: "GET /api/notes/library" },
|
|
255
262
|
"notes show": { tool: "notes_show", door: "GET /api/notes/library/[id]" },
|
|
256
263
|
"notes shelf": { tool: "notes_shelf", door: "GET /api/notes/shelf" },
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bli-cockpit/mcp",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.31",
|
|
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",
|