@bli-cockpit/mcp 0.1.114 → 0.1.115
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-chain-tools.d.ts +13 -0
- package/dist/memory-chain-tools.js +44 -0
- package/dist/memory-consolidation-tools.js +19 -1
- package/dist/memory-rules-tools.js +40 -13
- package/dist/server.js +3 -0
- package/dist/tool-catalog.d.ts +1 -1
- package/dist/tool-catalog.js +2 -0
- package/dist/verb-census.js +3 -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
|
+
**160 of 165 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
|
|
@@ -195,6 +195,7 @@ three tables below.
|
|
|
195
195
|
| `cockpit mail send` | `mail_send` | `POST /api/mail/send` |
|
|
196
196
|
| `cockpit mail sync` | `mail_sync` | `POST /api/mail/accounts/[id]/sync` |
|
|
197
197
|
| `cockpit mail sync-status` | `mail_sync_status` | `GET /api/mail/sync-status` |
|
|
198
|
+
| `cockpit memory chain` | `memory_chain` | `GET /api/memory/chain` |
|
|
198
199
|
| `cockpit memory consolidation` | `memory_consolidation` | `GET /api/memory/consolidation` |
|
|
199
200
|
| `cockpit memory forget` | `forget_memory` (`bli-memory`) | `POST /api/memory/forget` |
|
|
200
201
|
| `cockpit memory knowledge` | `memory_knowledge` | `GET /api/memory/knowledge` |
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `memory_chain` (BLI-13376) - the MCP twin of `cockpit memory chain <id>`.
|
|
3
|
+
*
|
|
4
|
+
* Same door, same device token: `GET /api/memory/chain?id=<memory id>`. For a
|
|
5
|
+
* memory the nightly consolidation wrote, it lists the memories that were
|
|
6
|
+
* folded into it (date, project, text, who saved it). The door applies the
|
|
7
|
+
* privacy rule: a private original shows only to its owner or a super_admin,
|
|
8
|
+
* and the rest are counted, never shown. Read-only.
|
|
9
|
+
*/
|
|
10
|
+
import { type ToolDeps } from "./tool-result.js";
|
|
11
|
+
export declare function registerMemoryChainTools(server: {
|
|
12
|
+
registerTool: (...args: never[]) => unknown;
|
|
13
|
+
}, deps: ToolDeps): void;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `memory_chain` (BLI-13376) - the MCP twin of `cockpit memory chain <id>`.
|
|
3
|
+
*
|
|
4
|
+
* Same door, same device token: `GET /api/memory/chain?id=<memory id>`. For a
|
|
5
|
+
* memory the nightly consolidation wrote, it lists the memories that were
|
|
6
|
+
* folded into it (date, project, text, who saved it). The door applies the
|
|
7
|
+
* privacy rule: a private original shows only to its owner or a super_admin,
|
|
8
|
+
* and the rest are counted, never shown. Read-only.
|
|
9
|
+
*/
|
|
10
|
+
import { z } from "zod";
|
|
11
|
+
import { callAgentDoor } from "./agent-door.js";
|
|
12
|
+
import { doorFailureText, errorResult, queryString, registrarFor, textResult, withSession } from "./tool-result.js";
|
|
13
|
+
export function registerMemoryChainTools(server, deps) {
|
|
14
|
+
registrarFor(server)("memory_chain", {
|
|
15
|
+
title: "Read the originals behind a consolidated memory",
|
|
16
|
+
description: "For a memory the nightly consolidation wrote (it says 'from N memories by Name, Name' in search and on the Memory page), the memories that were folded into it: date, project, text and who saved each. A private original is shown only to its owner or a super_admin; the rest are counted ('1 private original not shown'). An id the night never wrote answers consolidated=false. Read-only.",
|
|
17
|
+
inputSchema: { id: z.string().min(1).describe("The memory id from search_memory, memory_rules or memory_knowledge.") },
|
|
18
|
+
}, async (args) => withSession(deps, async (session) => {
|
|
19
|
+
const params = new URLSearchParams();
|
|
20
|
+
params.set("id", String(args.id));
|
|
21
|
+
const response = await callAgentDoor(session, deps.fetchImpl, "GET", `/api/memory/chain${queryString(params)}`);
|
|
22
|
+
if (!response.ok)
|
|
23
|
+
return errorResult(doorFailureText("memory_chain", response));
|
|
24
|
+
const body = response.body;
|
|
25
|
+
if (!body.consolidated) {
|
|
26
|
+
return textResult(`Memory ${String(args.id)} was not written by the nightly consolidation, so it has no originals.`, response.body);
|
|
27
|
+
}
|
|
28
|
+
const originals = body.originals ?? [];
|
|
29
|
+
const count = body.count ?? originals.length;
|
|
30
|
+
const lines = [`Memory ${String(args.id)} is from ${count} ${count === 1 ? "memory" : "memories"}${(body.people ?? []).length ? ` by ${(body.people ?? []).join(", ")}` : ""}.`];
|
|
31
|
+
for (const original of originals) {
|
|
32
|
+
lines.push(`[${(original.createdAt ?? "").slice(0, 10)}]${original.project ? ` [${original.project}]` : ""}${original.savedBy ? ` ${original.savedBy}:` : ""} ${original.text ?? ""}`);
|
|
33
|
+
}
|
|
34
|
+
const hiddenPrivate = body.hiddenPrivate ?? 0;
|
|
35
|
+
if (hiddenPrivate > 0)
|
|
36
|
+
lines.push(`${hiddenPrivate} private ${hiddenPrivate === 1 ? "original" : "originals"} not shown.`);
|
|
37
|
+
const gone = (body.hiddenForgotten ?? 0) + (body.missing ?? 0);
|
|
38
|
+
if (gone > 0)
|
|
39
|
+
lines.push(`${gone} ${gone === 1 ? "original is" : "originals are"} no longer available.`);
|
|
40
|
+
if (body.truncated)
|
|
41
|
+
lines.push("Only the first originals are listed.");
|
|
42
|
+
return textResult(lines.join("\n"), response.body);
|
|
43
|
+
}));
|
|
44
|
+
}
|
|
@@ -23,6 +23,17 @@ function counts(value) {
|
|
|
23
23
|
const entries = Object.entries(value);
|
|
24
24
|
return entries.length === 0 ? "none" : entries.map(([key, count]) => `${key} ${String(count)}`).join(", ");
|
|
25
25
|
}
|
|
26
|
+
/** One scope of last night, as the door computed it (BLI-13376). */
|
|
27
|
+
function scopeLine(scope) {
|
|
28
|
+
const refused = Array.isArray(scope.topRefused)
|
|
29
|
+
? scope.topRefused.map((item) => `${String(item.label)} ${String(item.count)}`).join(", ")
|
|
30
|
+
: "";
|
|
31
|
+
const cost = typeof scope.costUsd === "number" ? `$${scope.costUsd.toFixed(4)}` : "$?";
|
|
32
|
+
return `${String(scope.label ?? scope.containerTag ?? "?")} ${String(scope.mode ?? "?")} clusters ${String(scope.clusters ?? 0)}; `
|
|
33
|
+
+ `merged ${String(scope.merged ?? 0)}; rewritten ${String(scope.rewritten ?? 0)}; corrected ${String(scope.corrected ?? 0)}; `
|
|
34
|
+
+ `kept ${String(scope.kept ?? 0)}; refused ${String(scope.refused ?? 0)}${refused ? ` (${refused})` : ""}; `
|
|
35
|
+
+ `cross-person ${String(scope.crossPersonClusters ?? 0)}; ${cost}`;
|
|
36
|
+
}
|
|
26
37
|
function runLine(run) {
|
|
27
38
|
const cost = typeof run.costUsd === "number" ? `$${run.costUsd.toFixed(4)}` : "$?";
|
|
28
39
|
return `${String(run.startedAt ?? "?")} ${String(run.containerTag ?? "?")} ${String(run.mode ?? "?")} `
|
|
@@ -33,7 +44,7 @@ function runLine(run) {
|
|
|
33
44
|
export function registerMemoryConsolidationTools(server, deps) {
|
|
34
45
|
registrarFor(server)("memory_consolidation", {
|
|
35
46
|
title: "Read the nightly memory consolidation runs",
|
|
36
|
-
description: "What the nightly BLI Memory consolidation pass did: clusters,
|
|
47
|
+
description: "What the nightly BLI Memory consolidation pass did, LAST NIGHT first: a one-line night total (runs, finished vs unfinished, cost) and per container or scope the clusters, merged, rewritten, corrected, kept, refused (with top labels), cross-person clusters and cost; the structured half is the full door body plus a computed `night` object. With a container tag it answers that space's recent runs; without one, the newest run per container. Read-only — the pass never deletes a memory and this tool never starts one. An empty list means no run is visible to this account (the receipts are super_admin-readable), not an outage.",
|
|
37
48
|
inputSchema: {
|
|
38
49
|
container: z
|
|
39
50
|
.string()
|
|
@@ -63,6 +74,13 @@ export function registerMemoryConsolidationTools(server, deps) {
|
|
|
63
74
|
// One line per run in the text half. It used to carry only the count,
|
|
64
75
|
// so a Claude-family client never saw a run (BLI-10432).
|
|
65
76
|
const lines = runs.map(runLine).join("\n");
|
|
77
|
+
// Last night first (BLI-13376): the door's one-line total, then a line per scope.
|
|
78
|
+
const nightLine = typeof response.body.nightLine === "string" ? response.body.nightLine : null;
|
|
79
|
+
const scopes = (Array.isArray(response.body.nightScopes) ? response.body.nightScopes : []);
|
|
80
|
+
if (nightLine) {
|
|
81
|
+
return textResult(`${nightLine}\n${scopes.map(scopeLine).join("\n")}\n`
|
|
82
|
+
+ "merged/rewritten/corrected are what an apply run wrote or what a report run only proposed (mode); kept is always a proposal; nothing is ever deleted.", response.body);
|
|
83
|
+
}
|
|
66
84
|
return textResult(runs.length === 0
|
|
67
85
|
? `No consolidation runs are visible (${reason}). The pass is a nightly cron and its receipts are readable by a super_admin; this is the access rule answering, not an outage.`
|
|
68
86
|
: `${runs.length} consolidation run${runs.length === 1 ? "" : "s"}. report = proposals only, nothing on the shelf changed; apply = accepted edits written as supersessions, which never delete anything.`
|
|
@@ -1,6 +1,37 @@
|
|
|
1
1
|
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
|
+
/** The plain sentence, or the machine wording marked as the original while no plain one is written yet. Same rule as `cockpit memory`. */
|
|
5
|
+
function sentence(idea) {
|
|
6
|
+
const c = idea.consolidated;
|
|
7
|
+
// "(from 4 memories by Edward, Vee)" marks a memory the nightly pass wrote (BLI-13376).
|
|
8
|
+
const origin = c ? ` (from ${c.count} ${c.count === 1 ? "memory" : "memories"}${c.people.length ? ` by ${c.people.join(", ")}` : ""})` : "";
|
|
9
|
+
return (idea.human_text ? idea.human_text : `${idea.text} (original wording)`) + origin;
|
|
10
|
+
}
|
|
11
|
+
/** The text half of the answer: the same header words the page and the CLI print, then each project's ideas as plain sentences. */
|
|
12
|
+
function answerText(body, action) {
|
|
13
|
+
const lines = [body.words?.summary, body.words?.ranking, body.freshness?.words, ...(body.notes ?? [])].filter((line) => Boolean(line));
|
|
14
|
+
if (body.projectGroups?.length)
|
|
15
|
+
for (const group of body.projectGroups) {
|
|
16
|
+
lines.push("", `${group.project}, ${group.count.toLocaleString("en-US")} memories`);
|
|
17
|
+
const topics = group.topics ?? [];
|
|
18
|
+
const headed = Boolean(group.ranked) && topics.length > 1;
|
|
19
|
+
if (topics.length)
|
|
20
|
+
for (const topic of topics)
|
|
21
|
+
lines.push(...(headed ? [` ${topic.title}`] : []), ...topic.memories.map(idea => `${headed ? " " : " "}${sentence(idea)}`));
|
|
22
|
+
else
|
|
23
|
+
lines.push(...(group.memories ?? []).map(idea => ` ${sentence(idea)}`));
|
|
24
|
+
}
|
|
25
|
+
else
|
|
26
|
+
for (const group of body.groups ?? []) {
|
|
27
|
+
lines.push(group.owner, ...group.rules.map(rule => ` [${rule.created_at.slice(0, 10)}] ${rule.project ? `[${rule.project}] ` : ""}${sentence(rule)}`));
|
|
28
|
+
}
|
|
29
|
+
if (!body.returned)
|
|
30
|
+
lines.push(`No ${action === "rules" ? "ground rules" : "project knowledge"} matched.`);
|
|
31
|
+
if (body.capped)
|
|
32
|
+
lines.push("Result capped; narrow by person or project, or raise limit (max 1000) or per_project (max 100).");
|
|
33
|
+
return lines.join("\n");
|
|
34
|
+
}
|
|
4
35
|
export function registerMemoryRulesTools(server, deps) {
|
|
5
36
|
registrarFor(server)("memory_map", {
|
|
6
37
|
title: "Read the memory map",
|
|
@@ -18,25 +49,21 @@ export function registerMemoryRulesTools(server, deps) {
|
|
|
18
49
|
for (const action of ["rules", "knowledge"])
|
|
19
50
|
registrarFor(server)(`memory_${action}`, {
|
|
20
51
|
title: `Read BLI agent ${action}`,
|
|
21
|
-
description: `Memories grouped by project
|
|
22
|
-
inputSchema: {
|
|
52
|
+
description: `Memories grouped by project, each project with its own quota of ideas, ranked by importance, repetition, then recency. The text is plain header words (what the list covers, how it is ordered, how fresh it is, any problems) then one plain sentence per idea; quote those sentences, never ids or scores. structuredContent is the whole answer, with both wordings per idea: human_text for people, text for agents. Reads only memories visible to you, over the same door as cockpit memory ${action}. Default 15 ideas per project, 1000 in all; capped means narrow the filters or raise a bound.`,
|
|
53
|
+
inputSchema: {
|
|
54
|
+
person: z.string().min(1).optional().describe("Exact roster name, or unattributed for memories with no author."),
|
|
55
|
+
project: z.string().min(1).optional().describe("Case-insensitive project name or canonical slug, for example Pearl Portal or pearl-portal."),
|
|
56
|
+
limit: z.number().int().min(1).max(1000).optional().describe("Maximum ideas in all, default 1000, maximum 1000."),
|
|
57
|
+
per_project: z.number().int().min(1).max(100).optional().describe("Ideas shown from each project, default 15, maximum 100."),
|
|
58
|
+
},
|
|
23
59
|
}, async (args) => withSession(deps, async (session) => {
|
|
24
60
|
const params = new URLSearchParams();
|
|
25
|
-
for (const key of ["person", "project", "limit"])
|
|
61
|
+
for (const key of ["person", "project", "limit", "per_project"])
|
|
26
62
|
if (args[key] !== undefined)
|
|
27
63
|
params.set(key, String(args[key]));
|
|
28
64
|
const response = await callAgentDoor(session, deps.fetchImpl, "GET", `/api/memory/${action}${queryString(params)}`);
|
|
29
65
|
if (!response.ok)
|
|
30
66
|
return errorResult(doorFailureText(`memory_${action}`, response));
|
|
31
|
-
|
|
32
|
-
const lines = body.projectGroups ? body.projectGroups.flatMap(group => [
|
|
33
|
-
`${group.project} (${group.count}): ${group.headline.split("\n")[0]}`,
|
|
34
|
-
...group.topics.flatMap(topic => [topic.title, ...topic.memories.map(memory => `${memory.text}${memory.repetition > 1 ? ` (said ${memory.repetition} times)` : ""}${memory.importance !== null ? ` [importance ${memory.importance}/100]` : ""}`)]),
|
|
35
|
-
]) : (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})` : ""}`)]);
|
|
36
|
-
if (!lines.length)
|
|
37
|
-
lines.push(`No ${action === "rules" ? "ground rules" : "project knowledge"} matched.`);
|
|
38
|
-
if (body.capped)
|
|
39
|
-
lines.push("Result capped; narrow by person/project or increase limit (max 1000).");
|
|
40
|
-
return textResult(lines.join("\n"), response.body);
|
|
67
|
+
return textResult(answerText(response.body, action), response.body);
|
|
41
68
|
}));
|
|
42
69
|
}
|
package/dist/server.js
CHANGED
|
@@ -20,6 +20,7 @@ import { registerNotesTools } from "./notes-tools.js";
|
|
|
20
20
|
import { registerNotesBotUpcomingTool } from "./notes-bot-upcoming-tool.js";
|
|
21
21
|
import { registerNotesGranolaStatusTool } from "./notes-granola-status-tool.js";
|
|
22
22
|
import { registerNotesWriteTools } from "./notes-write-tools.js";
|
|
23
|
+
import { registerMemoryChainTools } from "./memory-chain-tools.js";
|
|
23
24
|
import { registerMemoryConsolidationTools } from "./memory-consolidation-tools.js";
|
|
24
25
|
import { registerMemoryExperienceTools } from "./memory-experience-tools.js";
|
|
25
26
|
import { registerMemoryRulesTools } from "./memory-rules-tools.js";
|
|
@@ -487,6 +488,8 @@ export function createServer(deps, options = {}) {
|
|
|
487
488
|
// BLI-4534: the nightly consolidation receipts, twin of `cockpit memory
|
|
488
489
|
// consolidation`. Read-only, and the pass it reports on never deletes.
|
|
489
490
|
registerMemoryConsolidationTools(server, { fetchImpl: deps.fetchImpl });
|
|
491
|
+
// BLI-13376: the originals behind a consolidated memory, twin of `cockpit memory chain`.
|
|
492
|
+
registerMemoryChainTools(server, { fetchImpl: deps.fetchImpl });
|
|
490
493
|
// BLI-4539: putting a window of refused extractions back, the twin of
|
|
491
494
|
// `cockpit memory requeue`. Dry run by default, super_admin at the door.
|
|
492
495
|
registerMemoryRequeueTools(server, { fetchImpl: deps.fetchImpl });
|
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_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"];
|
|
13
|
+
export declare const READ_TOOLS: readonly ["get_ticket_timeline", "get_active_tickets", "docs_list", "docs_tree", "docs_read", "docs_sync_status", "msg_channels", "msg_read", "msg_thread", "work_board", "work_list_issues", "work_get_issue", "work_issue_history", "work_list_projects", "careers_list", "careers_show", "careers_resume_url", "careers_takehome_show", "careers_settings_show", "rename_pending", "github_pr", "setup_read", "speed_read", "speed_seen", "usage_people", "usage_sessions", "mail_accounts", "mail_inbox", "mail_read", "mail_search", "mail_attachment", "mail_sync_status", "cal_today", "cal_week", "cal_next", "cal_find", "cal_calendars", "cal_sync_status", "jarvis_ask", "jarvis_trace", "jarvis_check", "models_list", "models_card", "models_compare", "brief_read", "brief_history", "brief_status", "brief_missed", "notes_folders", "notes_list", "glossary_list", "notes_show", "notes_shelf", "notes_shelves", "notes_place", "notes_accounts", "notes_recordings", "notes_recording", "notes_sync_status", "notes_granola_status", "notes_bot_upcoming", "ops_status", "slack_coverage", "slack_connect", "slack_disconnect", "slack_read", "memory_map", "memory_rules", "memory_knowledge", "memory_profile", "memory_consolidation", "memory_chain", "settings_show", "settings_list", "model_show", "team_members", "team_github_login_add", "team_github_login_remove", "team_github_login_list", "team_device_list", "scout_board", "workbook_read", "tower_search", "sources_check", "logs_digest", "brain_status"];
|
|
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
|
@@ -354,6 +354,9 @@ export const MCP_TWINS = {
|
|
|
354
354
|
// `bli-tower` rather than `bli-memory`, because it is a read of a JOB s
|
|
355
355
|
// counts and not a read or write of the store itself.
|
|
356
356
|
"memory consolidation": { tool: "memory_consolidation", door: "GET /api/memory/consolidation" },
|
|
357
|
+
// BLI-13376: the originals behind a consolidated memory. Also on `bli-tower`:
|
|
358
|
+
// the door filters private and forgotten originals before anything leaves.
|
|
359
|
+
"memory chain": { tool: "memory_chain", door: "GET /api/memory/chain" },
|
|
357
360
|
// BLI-4539: the recovery verb, after a window of extractions whose writes
|
|
358
361
|
// the store refused. Also on `bli-tower`, for the same reason
|
|
359
362
|
// `memory profile` is: it is ours, not the vendor's four.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bli-cockpit/mcp",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.115",
|
|
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",
|