@bli-cockpit/mcp 0.1.98 → 0.1.101
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 +2 -1
- package/dist/memory-rules-tools.d.ts +4 -0
- package/dist/memory-rules-tools.js +25 -0
- package/dist/notes-account-tools.js +13 -4
- package/dist/notes-tools.js +1 -1
- package/dist/notes-write-tools.js +77 -6
- package/dist/server.js +2 -0
- package/dist/tool-catalog.d.ts +1 -1
- package/dist/tool-catalog.js +1 -0
- package/dist/verb-census.js +1 -0
- 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
|
+
**136 of 141 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
|
|
@@ -191,6 +191,7 @@ three tables below.
|
|
|
191
191
|
| `cockpit memory profile` | `memory_profile` | `GET /api/memory/profile` |
|
|
192
192
|
| `cockpit memory public` | `set_memory_visibility` (`bli-memory`) | `POST /api/memory/visibility` |
|
|
193
193
|
| `cockpit memory requeue` | `memory_requeue` | `POST /api/memory/requeue` |
|
|
194
|
+
| `cockpit memory rules` | `memory_rules` | `GET /api/memory/rules` |
|
|
194
195
|
| `cockpit memory save` | `save_memory` (`bli-memory`) | `POST /api/memory/save` |
|
|
195
196
|
| `cockpit memory search` | `search_memory` (`bli-memory`) | `POST /api/memory/search` |
|
|
196
197
|
| `cockpit memory update` | `update_memory` (`bli-memory`) | `POST /api/memory/update` |
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { callAgentDoor } from "./agent-door.js";
|
|
3
|
+
import { doorFailureText, errorResult, queryString, registrarFor, textResult, withSession } from "./tool-result.js";
|
|
4
|
+
export function registerMemoryRulesTools(server, deps) {
|
|
5
|
+
registrarFor(server)("memory_rules", {
|
|
6
|
+
title: "Read BLI agent ground rules",
|
|
7
|
+
description: "Standing instructions grouped by whose rule it is, with project and date. Reads only memories visible to you, over the same door as cockpit memory rules. Default 200, max 1000; capped means narrow the filters or increase the bound.",
|
|
8
|
+
inputSchema: { person: z.string().min(1).optional().describe("Exact roster name, or unattributed for memories with no author."), project: z.string().min(1).optional().describe("Exact project label, for example Pearl Portal or BLI-wide."), limit: z.number().int().min(1).max(1000).optional().describe("Maximum rows, default 200, maximum 1000. A full page is marked capped.") },
|
|
9
|
+
}, async (args) => withSession(deps, async (session) => {
|
|
10
|
+
const params = new URLSearchParams();
|
|
11
|
+
for (const key of ["person", "project", "limit"])
|
|
12
|
+
if (args[key] !== undefined)
|
|
13
|
+
params.set(key, String(args[key]));
|
|
14
|
+
const response = await callAgentDoor(session, deps.fetchImpl, "GET", `/api/memory/rules${queryString(params)}`);
|
|
15
|
+
if (!response.ok)
|
|
16
|
+
return errorResult(doorFailureText("memory_rules", response));
|
|
17
|
+
const body = response.body;
|
|
18
|
+
const lines = body.groups.flatMap((group) => [group.owner, ...group.rules.map((rule) => `[${rule.created_at.slice(0, 10)}] ${rule.project ? `[${rule.project}] ` : ""}${rule.text}${group.owner === "unattributed" ? ` (space: ${rule.container})` : ""}`)]);
|
|
19
|
+
if (!lines.length)
|
|
20
|
+
lines.push("No ground rules matched.");
|
|
21
|
+
if (body.capped)
|
|
22
|
+
lines.push("Result capped; narrow by person/project or increase limit (max 1000).");
|
|
23
|
+
return textResult(lines.join("\n"), response.body);
|
|
24
|
+
}));
|
|
25
|
+
}
|
|
@@ -97,16 +97,25 @@ export function registerNotesAccountTools(server, deps) {
|
|
|
97
97
|
+ "alternates, each one's transcript length, its notetaker, and the sentence saying why it holds its role "
|
|
98
98
|
+ "(BLI-12926). Open an alternate with `notes_recording`. Read-only; you only see recordings of meetings you "
|
|
99
99
|
+ "can already read.",
|
|
100
|
-
inputSchema: {
|
|
100
|
+
inputSchema: {
|
|
101
|
+
note_id: z.string().uuid().optional().describe("A note id; supply exactly one of note_id or calendar_event_id."),
|
|
102
|
+
calendar_event_id: z.string().min(1).max(512).optional().describe("Bot outcomes even when no note exists."),
|
|
103
|
+
},
|
|
101
104
|
}, async (args) => withSession(deps, async (session) => {
|
|
102
|
-
|
|
105
|
+
if (Boolean(args.note_id) === Boolean(args.calendar_event_id))
|
|
106
|
+
return errorResult("Supply exactly one of note_id or calendar_event_id.");
|
|
107
|
+
const response = await callAgentDoor(session, deps.fetchImpl, "GET", args.calendar_event_id
|
|
108
|
+
? `/api/notes/recordings?calendar_event_id=${encodeURIComponent(String(args.calendar_event_id))}`
|
|
109
|
+
: `/api/notes/recordings?note_id=${encodeURIComponent(String(args.note_id))}`);
|
|
103
110
|
if (!response.ok)
|
|
104
111
|
return errorResult(doorFailureText("notes_recordings", response));
|
|
105
112
|
const recordings = (Array.isArray(response.body.recordings) ? response.body.recordings : []);
|
|
106
113
|
const lines = recordings.map((row) => `${row.role === "primary" ? "SHOWN " : "alternate"} ${row.source_namespace} ${row.transcript_chars === null ? "length unknown" : `${row.transcript_chars} chars`} ${row.started_at ?? "no start time"} ${row.id}${row.chosen_reason ? ` (${row.chosen_reason})` : ""}`);
|
|
114
|
+
const missed = (Array.isArray(response.body.missed) ? response.body.missed : []);
|
|
115
|
+
const outcomes = missed.map((row) => `No bot transcript: ${row.status} (${row.reason}), ${row.started_at}`).join("\n");
|
|
107
116
|
return dataResult(recordings.length === 0
|
|
108
|
-
? "No recording is indexed for this note."
|
|
109
|
-
: `${recordings.length} recording(s) of this meeting.\n${lines.join("\n")}`, { note_id:
|
|
117
|
+
? outcomes || "No recording is indexed for this note."
|
|
118
|
+
: `${recordings.length} recording(s) of this meeting.\n${lines.join("\n")}\n${outcomes}`, { note_id: args.note_id, calendar_event_id: args.calendar_event_id, recordings, missed });
|
|
110
119
|
}));
|
|
111
120
|
register("notes_recording", {
|
|
112
121
|
title: "Open one recording of a meeting",
|
package/dist/notes-tools.js
CHANGED
|
@@ -110,7 +110,7 @@ export function registerNotesTools(server, deps) {
|
|
|
110
110
|
}
|
|
111
111
|
const invited = note.participants.length > 0 ? `\nInvited: ${note.participants.join(", ")}` : "";
|
|
112
112
|
return textResult(`${note.title}\n${note.meetingDate} · ${note.shelf} · ${note.fileName} · ${note.lineCount} lines`
|
|
113
|
-
+ `${invited}\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 });
|
|
113
|
+
+ `${invited}\n${note.visibility}${body.audio ? "\nTranscription: " + body.audio.transcription_status + (body.audio.transcription_detail ? ` (${body.audio.transcription_detail})` : "") : ""}\n\n${note.content}${narrowingNote(body)}`, { scope: body.scope ?? null, note, audio: body.audio ?? null });
|
|
114
114
|
}));
|
|
115
115
|
register("notes_shelf", {
|
|
116
116
|
title: "Your own Tower notes shelf",
|
|
@@ -25,14 +25,26 @@
|
|
|
25
25
|
* it manages to send one. An MCP client's own deadline may still be shorter —
|
|
26
26
|
* the description says so rather than letting a cut call read as a refusal.
|
|
27
27
|
*/
|
|
28
|
+
import { createReadStream } from "node:fs";
|
|
28
29
|
import { readFile, stat } from "node:fs/promises";
|
|
29
30
|
import path from "node:path";
|
|
31
|
+
import { Readable } from "node:stream";
|
|
30
32
|
import { isSecretLikePathSegment } from "@bli-cockpit/telemetry-core";
|
|
31
33
|
import { z } from "zod";
|
|
32
34
|
import { callAgentDoor } from "./agent-door.js";
|
|
33
35
|
import { CONFIRM_INPUT, doorFailureText, errorResult, registrarFor, textResult, unconfirmed, withSession, } from "./tool-result.js";
|
|
34
36
|
/** Mirrors `UPLOADED_NOTE_MAX_BYTES`; a courtesy check, not a second authority. */
|
|
35
37
|
export const NOTE_FILE_MAX_BYTES = 20 * 1024 * 1024;
|
|
38
|
+
/**
|
|
39
|
+
* Audio over this goes straight to storage and is transcribed in the
|
|
40
|
+
* background (BLI-12963); at or under it, the upload route takes it. Mirrors
|
|
41
|
+
* `AUDIO_MAX_BYTES` in the dashboard's `lib/notes/audio.ts`.
|
|
42
|
+
*/
|
|
43
|
+
const AUDIO_INLINE_MAX_BYTES = 4_000_000;
|
|
44
|
+
/** Mirrors the bucket's `file_size_limit`. */
|
|
45
|
+
const AUDIO_DIRECT_MAX_BYTES = 1_000_000_000;
|
|
46
|
+
/** A signed upload URL lives two hours. */
|
|
47
|
+
const STORAGE_PUT_DEADLINE_MS = 2 * 60 * 60 * 1000;
|
|
36
48
|
/** The route's own ceiling is 300 s. Wait past it so its sentence wins. */
|
|
37
49
|
const UPLOAD_DEADLINE_MS = 305_000;
|
|
38
50
|
const WRITE_DEADLINE_MS = 60_000;
|
|
@@ -77,13 +89,18 @@ async function readNoteFile(filePath) {
|
|
|
77
89
|
}
|
|
78
90
|
if (size === 0)
|
|
79
91
|
return refuse("file_empty");
|
|
80
|
-
if (/\.(mp3|m4a|wav|webm|ogg)$/i.test(fileName)
|
|
81
|
-
|
|
92
|
+
if (/\.(mp3|m4a|wav|webm|ogg)$/i.test(fileName)) {
|
|
93
|
+
if (size > AUDIO_DIRECT_MAX_BYTES) {
|
|
94
|
+
return { ok: false, refusal: "file_too_big", sentence: "file_too_large: Audio is limited to 1 GB (1,000,000,000 bytes)." };
|
|
95
|
+
}
|
|
96
|
+
// Big audio is never read into memory; the upload streams it from disk.
|
|
97
|
+
if (size > AUDIO_INLINE_MAX_BYTES)
|
|
98
|
+
return { ok: true, bytes: Buffer.alloc(0), fileName, size, direct: true };
|
|
82
99
|
}
|
|
83
|
-
if (size > NOTE_FILE_MAX_BYTES)
|
|
100
|
+
else if (size > NOTE_FILE_MAX_BYTES)
|
|
84
101
|
return refuse("file_too_big");
|
|
85
102
|
try {
|
|
86
|
-
return { ok: true, bytes: await readFile(filePath), fileName };
|
|
103
|
+
return { ok: true, bytes: await readFile(filePath), fileName, size, direct: false };
|
|
87
104
|
}
|
|
88
105
|
catch {
|
|
89
106
|
return refuse("file_unreadable");
|
|
@@ -108,11 +125,59 @@ async function postForm(deps, session, tool, form) {
|
|
|
108
125
|
? textResult(text, { ok: true, noteId: body.noteId ?? null, scope: body.scope ?? null, transcription: body.transcription ?? null })
|
|
109
126
|
: errorResult(text);
|
|
110
127
|
}
|
|
128
|
+
/**
|
|
129
|
+
* A whole-meeting recording: signed URL from Tower, the file streamed from disk
|
|
130
|
+
* to storage, then Tower told it is there (BLI-12963). Tower transcribes it in
|
|
131
|
+
* the background; poll notes_show for `transcription_detail` and the final
|
|
132
|
+
* transcript. The storage PUT carries the signed URL only, never the device
|
|
133
|
+
* token.
|
|
134
|
+
*/
|
|
135
|
+
async function postAudioDirect(deps, session, read, filePath, mediaType, extras) {
|
|
136
|
+
const target = await callAgentDoor(session, deps.fetchImpl, "POST", "/api/notes/audio/target", { file_name: read.fileName, bytes: read.size, media_type: mediaType }, WRITE_DEADLINE_MS);
|
|
137
|
+
if (!target.ok)
|
|
138
|
+
return errorResult(doorFailureText("notes_upload", target));
|
|
139
|
+
const issued = target.body;
|
|
140
|
+
if (!issued.objectKey || !issued.upload?.url) {
|
|
141
|
+
return errorResult("Refused (target_unreadable): Tower answered without an upload link. Nothing was stored.");
|
|
142
|
+
}
|
|
143
|
+
let put;
|
|
144
|
+
try {
|
|
145
|
+
put = await deps.fetchImpl(issued.upload.url, {
|
|
146
|
+
method: issued.upload.method ?? "PUT",
|
|
147
|
+
headers: { ...(issued.upload.headers ?? {}), "content-length": String(read.size) },
|
|
148
|
+
body: Readable.toWeb(createReadStream(filePath)),
|
|
149
|
+
duplex: "half",
|
|
150
|
+
signal: AbortSignal.timeout(STORAGE_PUT_DEADLINE_MS),
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
catch (error) {
|
|
154
|
+
console.error("[notes_upload] storage put failed", JSON.stringify({ reason: "storage_unreachable", bytes: read.size, error_name: error?.name ?? "Error" }));
|
|
155
|
+
return errorResult("Refused (storage_unreachable): could not reach storage to upload the file. Nothing was saved; try again.");
|
|
156
|
+
}
|
|
157
|
+
if (!put.ok) {
|
|
158
|
+
console.error("[notes_upload] storage put refused", JSON.stringify({ reason: `storage_http_${put.status}`, bytes: read.size }));
|
|
159
|
+
return errorResult(put.status === 413
|
|
160
|
+
? "Refused (storage_http_413): storage refused the file as too large. An admin may need to raise the project's storage file size limit."
|
|
161
|
+
: `Refused (storage_http_${put.status}): storage refused the upload. Nothing was saved; try again.`);
|
|
162
|
+
}
|
|
163
|
+
const saved = await callAgentDoor(session, deps.fetchImpl, "POST", "/api/notes/audio/saved", {
|
|
164
|
+
object_key: issued.objectKey, file_name: read.fileName,
|
|
165
|
+
...(extras.exclude ? { exclusions: extras.exclude } : {}),
|
|
166
|
+
...(extras.folder ? { folder_path: extras.folder } : {}),
|
|
167
|
+
}, WRITE_DEADLINE_MS);
|
|
168
|
+
if (!saved.ok)
|
|
169
|
+
return errorResult(doorFailureText("notes_upload", saved));
|
|
170
|
+
const body = saved.body;
|
|
171
|
+
const text = saidByTower(body);
|
|
172
|
+
return body.stored === true
|
|
173
|
+
? textResult(text, { ok: true, noteId: body.noteId ?? null, scope: body.scope ?? null, transcription: body.transcription ?? null })
|
|
174
|
+
: errorResult(text);
|
|
175
|
+
}
|
|
111
176
|
export function registerNotesWriteTools(server, deps) {
|
|
112
177
|
const register = registrarFor(server);
|
|
113
178
|
register("notes_upload", {
|
|
114
179
|
title: "Put a local file in as a Tower meeting note",
|
|
115
|
-
description: "Uploads a transcript, note, or MP3/M4A/WAV/WebM/OGG audio file from this machine. Audio
|
|
180
|
+
description: "Uploads a transcript, note, or MP3/M4A/WAV/WebM/OGG audio file from this machine. Audio up to 4 MB goes through the upload door; a longer recording (up to 1 GB, a whole meeting) is sent straight to storage and transcribed in the background. Either way it returns a note id plus transcription status immediately. Poll notes_show for completion and progress (a long meeting takes several minutes). "
|
|
116
181
|
+ "A name that looks like a key or credential file is refused without being opened; so is a missing, empty or "
|
|
117
182
|
+ "over-20-MB file. Tower reads the whole note with one model call, so a large file can take a couple of "
|
|
118
183
|
+ "minutes and your client may give up first — the upload keeps going; notes_list will show it.",
|
|
@@ -131,9 +196,15 @@ export function registerNotesWriteTools(server, deps) {
|
|
|
131
196
|
const read = await readNoteFile(filePath);
|
|
132
197
|
if (!read.ok)
|
|
133
198
|
return errorResult(`Refused (${read.refusal}): ${read.sentence} Nothing was sent.`);
|
|
134
|
-
const form = new FormData();
|
|
135
199
|
const audioTypes = { mp3: "audio/mpeg", m4a: "audio/mp4", wav: "audio/wav", webm: "audio/webm", ogg: "audio/ogg" };
|
|
136
200
|
const mediaType = audioTypes[path.extname(read.fileName).slice(1).toLowerCase()] ?? "";
|
|
201
|
+
if (read.direct) {
|
|
202
|
+
return postAudioDirect(deps, session, read, filePath, mediaType, {
|
|
203
|
+
...(args.exclude ? { exclude: String(args.exclude) } : {}),
|
|
204
|
+
...(args.folder ? { folder: String(args.folder) } : {}),
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
const form = new FormData();
|
|
137
208
|
form.set("file", new File([new Uint8Array(read.bytes)], read.fileName, { type: mediaType }));
|
|
138
209
|
if (args.exclude)
|
|
139
210
|
form.set("exclusions", String(args.exclude));
|
package/dist/server.js
CHANGED
|
@@ -21,6 +21,7 @@ import { registerNotesGranolaStatusTool } from "./notes-granola-status-tool.js";
|
|
|
21
21
|
import { registerNotesWriteTools } from "./notes-write-tools.js";
|
|
22
22
|
import { registerMemoryConsolidationTools } from "./memory-consolidation-tools.js";
|
|
23
23
|
import { registerMemoryExperienceTools } from "./memory-experience-tools.js";
|
|
24
|
+
import { registerMemoryRulesTools } from "./memory-rules-tools.js";
|
|
24
25
|
import { registerMemoryProfileTools } from "./memory-profile-tools.js";
|
|
25
26
|
import { registerMemoryRequeueTools } from "./memory-requeue-tools.js";
|
|
26
27
|
import { registerModelsTools } from "./models-tools.js";
|
|
@@ -475,6 +476,7 @@ export function createServer(deps, options = {}) {
|
|
|
475
476
|
registerNotesGranolaStatusTool(server, { fetchImpl: deps.fetchImpl });
|
|
476
477
|
registerOpsTools(server, { fetchImpl: deps.fetchImpl });
|
|
477
478
|
registerMemoryExperienceTools(server, { fetchImpl: deps.fetchImpl });
|
|
479
|
+
registerMemoryRulesTools(server, { fetchImpl: deps.fetchImpl });
|
|
478
480
|
// BLI-4533: the compiled one-page profile, the twin of `cockpit memory profile`.
|
|
479
481
|
registerMemoryProfileTools(server, { fetchImpl: deps.fetchImpl });
|
|
480
482
|
// BLI-4534: the nightly consolidation receipts, twin of `cockpit memory
|
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", "usage_people", "usage_sessions", "mail_accounts", "mail_inbox", "mail_read", "mail_search", "mail_attachment", "mail_sync_status", "cal_today", "cal_week", "cal_next", "cal_find", "cal_calendars", "cal_sync_status", "jarvis_ask", "jarvis_trace", "jarvis_check", "models_list", "models_card", "models_compare", "brief_read", "brief_history", "brief_status", "brief_missed", "notes_folders", "notes_list", "glossary_list", "notes_show", "notes_shelf", "notes_shelves", "notes_place", "notes_accounts", "notes_recordings", "notes_recording", "notes_sync_status", "notes_granola_status", "ops_status", "slack_coverage", "slack_read", "memory_profile", "memory_consolidation", "settings_show", "settings_list", "model_show", "team_members", "team_device_list", "scout_board", "workbook_read", "tower_search", "sources_check", "logs_digest", "brain_status"];
|
|
13
|
+
export declare const READ_TOOLS: readonly ["get_ticket_timeline", "get_active_tickets", "docs_list", "docs_tree", "docs_read", "docs_sync_status", "msg_channels", "msg_read", "msg_thread", "work_list_issues", "work_get_issue", "work_issue_history", "work_list_projects", "careers_list", "careers_show", "careers_resume_url", "careers_takehome_show", "careers_settings_show", "rename_pending", "github_pr", "setup_read", "usage_people", "usage_sessions", "mail_accounts", "mail_inbox", "mail_read", "mail_search", "mail_attachment", "mail_sync_status", "cal_today", "cal_week", "cal_next", "cal_find", "cal_calendars", "cal_sync_status", "jarvis_ask", "jarvis_trace", "jarvis_check", "models_list", "models_card", "models_compare", "brief_read", "brief_history", "brief_status", "brief_missed", "notes_folders", "notes_list", "glossary_list", "notes_show", "notes_shelf", "notes_shelves", "notes_place", "notes_accounts", "notes_recordings", "notes_recording", "notes_sync_status", "notes_granola_status", "ops_status", "slack_coverage", "slack_read", "memory_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).
|
package/dist/tool-catalog.js
CHANGED
package/dist/verb-census.js
CHANGED
|
@@ -329,6 +329,7 @@ export const MCP_TWINS = {
|
|
|
329
329
|
// BLI-4533: the compiled one-page profile. Its twin is on `bli-tower`, not
|
|
330
330
|
// `bli-memory`: that server's four tools are the vendor-compatible set the
|
|
331
331
|
// Claude Code plugin expects, and this page is ours.
|
|
332
|
+
"memory rules": { tool: "memory_rules", door: "GET /api/memory/rules" },
|
|
332
333
|
"memory profile": { tool: "memory_profile", door: "GET /api/memory/profile" },
|
|
333
334
|
// BLI-4534: the nightly consolidation receipts. This twin lives on
|
|
334
335
|
// `bli-tower` rather than `bli-memory`, because it is a read of a JOB s
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bli-cockpit/mcp",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.101",
|
|
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",
|