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