@bli-cockpit/mcp 0.1.65 → 0.1.67
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/docs-delete-tool.d.ts +7 -0
- package/dist/docs-delete-tool.js +58 -0
- package/dist/docs-msg-tools.js +13 -0
- package/dist/docs-read-tool.d.ts +14 -0
- package/dist/docs-read-tool.js +5 -2
- package/dist/tool-catalog.d.ts +1 -1
- package/dist/tool-catalog.js +1 -0
- package/dist/verb-census.js +1 -0
- package/dist/work-issue-rows.d.ts +94 -0
- package/dist/work-issue-rows.js +75 -0
- package/dist/work-tools.js +1 -60
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -103,7 +103,7 @@ about what an agent can reach.
|
|
|
103
103
|
|
|
104
104
|
<!-- BEGIN GENERATED verb census — `npm run mcp:readme` -->
|
|
105
105
|
|
|
106
|
-
**
|
|
106
|
+
**110 of 115 Tower verbs have an MCP twin.**
|
|
107
107
|
Each tool goes through the SAME door its CLI verb calls, with the same
|
|
108
108
|
collector device token — never a second route and never a service-role
|
|
109
109
|
reader. `src/verb-census.test.ts` fails when a verb is in none of the
|
|
@@ -139,6 +139,7 @@ three tables below.
|
|
|
139
139
|
| `cockpit careers takehome show` | `careers_takehome_show` | `GET /api/careers/takehomes/[role]` |
|
|
140
140
|
| `cockpit correct` | `brief_correct` | `POST /api/jarvis/corrections` |
|
|
141
141
|
| `cockpit docs create` | `docs_create` | `POST /api/docs/documents` |
|
|
142
|
+
| `cockpit docs delete` | `docs_delete` | `DELETE /api/docs/documents/[id]` |
|
|
142
143
|
| `cockpit docs link` | `docs_link` | `PUT /api/docs/documents/[id]/notion-link` |
|
|
143
144
|
| `cockpit docs list` | `docs_list` | `GET /api/docs/documents` |
|
|
144
145
|
| `cockpit docs read` | `docs_read` | `GET /api/docs/documents/[id]` |
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { type DocsMsgDeps } from "./docs-msg-helpers.js";
|
|
2
|
+
import { type ToolResult } from "./tool-result.js";
|
|
3
|
+
export interface DocsDeleteArgs {
|
|
4
|
+
id?: unknown;
|
|
5
|
+
ids?: unknown;
|
|
6
|
+
}
|
|
7
|
+
export declare function runDocsDeleteTool(deps: DocsMsgDeps, args: DocsDeleteArgs): Promise<ToolResult>;
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `docs_delete` (BLI‑10555) — the MCP twin of `cockpit docs delete`, the same
|
|
3
|
+
* `DELETE /api/docs/documents/[id]` door the browser's own delete calls.
|
|
4
|
+
*
|
|
5
|
+
* Soft delete only: the door sets `deleted_at` and never issues a real SQL
|
|
6
|
+
* `DELETE`, so it is recoverable (only a super_admin clearing `deleted_at`
|
|
7
|
+
* brings it back — there is no self-service undo) and never cascades: a
|
|
8
|
+
* deleted document's children keep their own `parent_id` and stay exactly
|
|
9
|
+
* where they were.
|
|
10
|
+
*
|
|
11
|
+
* Mirrors `docs-read-tool.ts`'s multi-ref shape: `resolveDocumentIds` runs
|
|
12
|
+
* ONE shared list read for every non-uuid ref, then one `DELETE` per
|
|
13
|
+
* document — one bad ref never loses the rest of the batch.
|
|
14
|
+
*/
|
|
15
|
+
import { callAgentDoor } from "./agent-door.js";
|
|
16
|
+
import { resolveDocumentId } from "./docs-msg-helpers.js";
|
|
17
|
+
import { resolveDocumentIds } from "./docs-read-tool.js";
|
|
18
|
+
import { doorFailureText, errorResult, textResult, withSession } from "./tool-result.js";
|
|
19
|
+
async function deleteOneDocument(deps, session, ref, id) {
|
|
20
|
+
if (!id)
|
|
21
|
+
return { ref, ok: false, text: `No document matches "${ref}" — check docs_list for the id or slug.` };
|
|
22
|
+
const response = await callAgentDoor(session, deps.fetchImpl, "DELETE", `/api/docs/documents/${encodeURIComponent(id)}`);
|
|
23
|
+
if (!response.ok)
|
|
24
|
+
return { ref, ok: false, text: doorFailureText("docs_delete", response) };
|
|
25
|
+
return { ref, ok: true, id };
|
|
26
|
+
}
|
|
27
|
+
export async function runDocsDeleteTool(deps, args) {
|
|
28
|
+
return withSession(deps, async (session) => {
|
|
29
|
+
const refs = Array.isArray(args.ids) && args.ids.length > 0 ? args.ids.map(String) : [String(args.id ?? "")];
|
|
30
|
+
if (refs.length === 1 && refs[0] === "")
|
|
31
|
+
return errorResult("docs_delete needs id or ids.");
|
|
32
|
+
const resolved = refs.length === 1
|
|
33
|
+
? await (async () => {
|
|
34
|
+
const one = await resolveDocumentId(deps, session, refs[0]);
|
|
35
|
+
if (one.status === "list_failed")
|
|
36
|
+
return { status: "list_failed", text: one.text };
|
|
37
|
+
const ids = new Map();
|
|
38
|
+
if (one.status === "ok")
|
|
39
|
+
ids.set(refs[0], one.id);
|
|
40
|
+
return { status: "ok", ids };
|
|
41
|
+
})()
|
|
42
|
+
: await resolveDocumentIds(deps, session, refs);
|
|
43
|
+
if (resolved.status === "list_failed")
|
|
44
|
+
return errorResult(resolved.text);
|
|
45
|
+
const results = await Promise.all(refs.map((ref) => deleteOneDocument(deps, session, ref, resolved.ids.get(ref))));
|
|
46
|
+
const failed = results.filter((result) => !result.ok);
|
|
47
|
+
if (refs.length === 1) {
|
|
48
|
+
const [result] = results;
|
|
49
|
+
if (!result?.ok)
|
|
50
|
+
return errorResult(result?.text ?? "docs_delete could not delete that document.");
|
|
51
|
+
return textResult(`Deleted ${result.id}. Recoverable by a super_admin; its children stay in place.`, { document_id: result.id });
|
|
52
|
+
}
|
|
53
|
+
const lines = results.map((result) => (result.ok ? `Deleted ${result.ref} (${result.id}).` : `${result.ref}: ${result.text}`)).join("\n");
|
|
54
|
+
return textResult(`${results.length - failed.length}/${results.length} document(s) deleted. Recoverable by a super_admin; children stay in place.\n${lines}`, {
|
|
55
|
+
documents: results.map((result) => result.ok ? { ref: result.ref, ok: true, document_id: result.id } : { ref: result.ref, ok: false, error: result.text }),
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
}
|
package/dist/docs-msg-tools.js
CHANGED
|
@@ -13,6 +13,7 @@ import { z } from "zod";
|
|
|
13
13
|
import { callAgentDoor } from "./agent-door.js";
|
|
14
14
|
import { DEFAULT_LIST_LIMIT, doorFailureText, errorResult, moreNote, registrarFor, textResult, withSession, } from "./tool-result.js";
|
|
15
15
|
import { DOCS_LIST_MAX, slimTree, renderDocTree, countTree, docListRow, docListLine, resolveDocumentId, resolveChannelId, messageLine, docsListPath } from "./docs-msg-helpers.js";
|
|
16
|
+
import { runDocsDeleteTool } from "./docs-delete-tool.js";
|
|
16
17
|
import { runDocsReadTool } from "./docs-read-tool.js";
|
|
17
18
|
export { docsListPath } from "./docs-msg-helpers.js";
|
|
18
19
|
export function registerDocsMsgTools(server, deps) {
|
|
@@ -133,6 +134,18 @@ export function registerDocsMsgTools(server, deps) {
|
|
|
133
134
|
const document = response.body.document;
|
|
134
135
|
return textResult(`Updated "${document?.title ?? ""}" (${document?.id ?? resolved.id}).`, { document });
|
|
135
136
|
}));
|
|
137
|
+
register("docs_delete", {
|
|
138
|
+
title: "Delete a Tower document",
|
|
139
|
+
description: "Soft-deletes one or more documents (id or slug — docs_list returns both), or several with ids. Sets "
|
|
140
|
+
+ "deleted_at; never a real SQL delete. Recoverable — only a super_admin clearing deleted_at brings it back, "
|
|
141
|
+
+ "there is no self-service undo. Deleting a parent does NOT delete its children; they keep their own "
|
|
142
|
+
+ "parent_id and stay exactly where they were. One shared list resolves every ref, then one delete each — "
|
|
143
|
+
+ "one bad ref never loses the rest.",
|
|
144
|
+
inputSchema: {
|
|
145
|
+
id: z.string().min(1).max(200).optional().describe("A document id, or its slug. Omit when passing ids."),
|
|
146
|
+
ids: z.array(z.string().min(1).max(200)).min(1).max(50).optional().describe("Several documents in one call."),
|
|
147
|
+
},
|
|
148
|
+
}, async (args) => runDocsDeleteTool(deps, args));
|
|
136
149
|
register("msg_channels", {
|
|
137
150
|
title: "List Tower channels",
|
|
138
151
|
description: "Every channel you are a member of: id, name (a direct message has none and lists its participants instead), "
|
package/dist/docs-read-tool.d.ts
CHANGED
|
@@ -11,6 +11,20 @@ export declare function sliceDocRange(body: string, offset: number | undefined,
|
|
|
11
11
|
truncated: boolean;
|
|
12
12
|
totalChars: number;
|
|
13
13
|
};
|
|
14
|
+
/**
|
|
15
|
+
* One shared list read for every non-uuid ref, never one per ref. Exported
|
|
16
|
+
* for `docs-delete-tool.ts` (BLI‑10555), which resolves a batch the same way.
|
|
17
|
+
*/
|
|
18
|
+
export declare function resolveDocumentIds(deps: DocsMsgDeps, session: {
|
|
19
|
+
dashboardUrl: string;
|
|
20
|
+
deviceToken: string;
|
|
21
|
+
}, refs: string[]): Promise<{
|
|
22
|
+
status: "ok";
|
|
23
|
+
ids: Map<string, string>;
|
|
24
|
+
} | {
|
|
25
|
+
status: "list_failed";
|
|
26
|
+
text: string;
|
|
27
|
+
}>;
|
|
14
28
|
export interface DocsReadArgs {
|
|
15
29
|
id?: unknown;
|
|
16
30
|
ids?: unknown;
|
package/dist/docs-read-tool.js
CHANGED
|
@@ -70,8 +70,11 @@ function narrowBody(document, section, offset, limit, applyCharRange) {
|
|
|
70
70
|
return { ok: true };
|
|
71
71
|
}
|
|
72
72
|
const UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/iu;
|
|
73
|
-
/**
|
|
74
|
-
|
|
73
|
+
/**
|
|
74
|
+
* One shared list read for every non-uuid ref, never one per ref. Exported
|
|
75
|
+
* for `docs-delete-tool.ts` (BLI‑10555), which resolves a batch the same way.
|
|
76
|
+
*/
|
|
77
|
+
export async function resolveDocumentIds(deps, session, refs) {
|
|
75
78
|
const ids = new Map();
|
|
76
79
|
const needsList = refs.filter((ref) => !UUID_PATTERN.test(ref));
|
|
77
80
|
for (const ref of refs)
|
package/dist/tool-catalog.d.ts
CHANGED
|
@@ -21,7 +21,7 @@ export declare const DRY_RUN_TOOLS: readonly ["ops_recompile", "memory_requeue"]
|
|
|
21
21
|
* candidates, share calendars. Never called by the smoke; covered by the unit
|
|
22
22
|
* suites (`write-tools.test.ts` and each family's own).
|
|
23
23
|
*/
|
|
24
|
-
export declare const WRITE_TOOLS: readonly ["emit_event", "docs_create", "docs_update", "docs_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_takehome_set", "careers_settings_set", "mail_send", "mail_sync", "mail_detach", "cal_create", "cal_share", "cal_sync", "cal_detach", "jarvis_dispatch", "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"];
|
|
24
|
+
export declare const WRITE_TOOLS: readonly ["emit_event", "docs_create", "docs_update", "docs_delete", "docs_link", "msg_create_channel", "msg_dm", "msg_send", "work_create_issue", "work_update_issue", "work_move_issue", "work_comment_issue", "careers_rescreen", "careers_send_takehome", "careers_invite", "careers_grade", "careers_decide", "careers_takehome_set", "careers_settings_set", "mail_send", "mail_sync", "mail_detach", "cal_create", "cal_share", "cal_sync", "cal_detach", "jarvis_dispatch", "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"];
|
|
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
package/dist/verb-census.js
CHANGED
|
@@ -234,6 +234,7 @@ export const MCP_TWINS = {
|
|
|
234
234
|
"docs read": { tool: "docs_read", door: "GET /api/docs/documents/[id]" },
|
|
235
235
|
"docs create": { tool: "docs_create", door: "POST /api/docs/documents" },
|
|
236
236
|
"docs update": { tool: "docs_update", door: "PATCH /api/docs/documents/[id]" },
|
|
237
|
+
"docs delete": { tool: "docs_delete", door: "DELETE /api/docs/documents/[id]" },
|
|
237
238
|
"docs link": { tool: "docs_link", door: "PUT /api/docs/documents/[id]/notion-link" },
|
|
238
239
|
"docs sync-status": { tool: "docs_sync_status", door: "GET /api/docs/notion-sync" },
|
|
239
240
|
"msg channels": { tool: "msg_channels", door: "GET /api/msg/channels" },
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The row shapes the `work_*` tools read off `/api/work/**`, and the pure
|
|
3
|
+
* functions that turn them into what a tool answers with: the trimmed list
|
|
4
|
+
* row, the completeness and outside-project notes, one issue's text, one
|
|
5
|
+
* history line.
|
|
6
|
+
*
|
|
7
|
+
* Split out of `work-tools.ts` (BLI-10445) when that file crossed the
|
|
8
|
+
* 500-line readability ratchet. The seam: `work-tools.ts` registers the tools
|
|
9
|
+
* and calls the doors; nothing here calls a door or knows a session, so this
|
|
10
|
+
* module changes when a door's row shape or a rendered sentence does, and can
|
|
11
|
+
* be read without the registration code around it.
|
|
12
|
+
*
|
|
13
|
+
* Every string moved verbatim.
|
|
14
|
+
*/
|
|
15
|
+
import { type AssigneeIdentity } from "./work-directory.js";
|
|
16
|
+
export interface IssueRow {
|
|
17
|
+
id: string;
|
|
18
|
+
identifier: string;
|
|
19
|
+
title: string;
|
|
20
|
+
state: string;
|
|
21
|
+
priority?: number | null;
|
|
22
|
+
assignee_id?: string | null;
|
|
23
|
+
project_id?: string | null;
|
|
24
|
+
parent_id?: string | null;
|
|
25
|
+
updated_at?: string | null;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* The fields `work_list_issues` promises. The door's row carries the whole
|
|
29
|
+
* description and a dozen bookkeeping columns; with no limit that was 1,000
|
|
30
|
+
* issues and 4.2 MB, 3.3 MB of it descriptions the tool said it never
|
|
31
|
+
* returned (BLI-10432). work_get_issue reads one issue in full.
|
|
32
|
+
*/
|
|
33
|
+
export interface IssueListRow {
|
|
34
|
+
id: string;
|
|
35
|
+
identifier: string;
|
|
36
|
+
title: string;
|
|
37
|
+
state: string;
|
|
38
|
+
priority: number | null;
|
|
39
|
+
assignee_id: string | null;
|
|
40
|
+
project_id: string | null;
|
|
41
|
+
parent_id: string | null;
|
|
42
|
+
updated_at: string | null;
|
|
43
|
+
}
|
|
44
|
+
export declare function issueListRow(row: IssueRow): IssueListRow;
|
|
45
|
+
export interface ListCompleteness {
|
|
46
|
+
status: "complete" | "partial" | "unknown";
|
|
47
|
+
total: number | null;
|
|
48
|
+
}
|
|
49
|
+
export declare function listCompleteness(value: unknown): ListCompleteness | null;
|
|
50
|
+
export interface OutsideProject {
|
|
51
|
+
status: string;
|
|
52
|
+
issues: {
|
|
53
|
+
identifier: string;
|
|
54
|
+
title: string;
|
|
55
|
+
project_id: string | null;
|
|
56
|
+
parent_identifier: string | null;
|
|
57
|
+
}[];
|
|
58
|
+
}
|
|
59
|
+
export declare function outsideProject(value: unknown): OutsideProject | null;
|
|
60
|
+
/** BLI-10447: sub-issues of the listed issues that live in no project or another one. */
|
|
61
|
+
export declare function outsideNote(outside: OutsideProject | null): string;
|
|
62
|
+
export interface OneIssueOk {
|
|
63
|
+
ref: string;
|
|
64
|
+
ok: true;
|
|
65
|
+
issue?: IssueRow & {
|
|
66
|
+
description?: string | null;
|
|
67
|
+
};
|
|
68
|
+
comments: Array<{
|
|
69
|
+
body_markdown?: string;
|
|
70
|
+
created_at?: string;
|
|
71
|
+
}>;
|
|
72
|
+
commentsUnread?: boolean;
|
|
73
|
+
}
|
|
74
|
+
export interface OneIssueFailure {
|
|
75
|
+
ref: string;
|
|
76
|
+
ok: false;
|
|
77
|
+
text: string;
|
|
78
|
+
}
|
|
79
|
+
export declare function renderOneIssue(result: OneIssueOk, directory: Map<string, AssigneeIdentity>): string;
|
|
80
|
+
export interface HistoryRow {
|
|
81
|
+
occurred_at?: string;
|
|
82
|
+
change_type?: string;
|
|
83
|
+
from_value?: string | null;
|
|
84
|
+
to_value?: string | null;
|
|
85
|
+
actor_name?: string | null;
|
|
86
|
+
source?: string;
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* A row the Linear import could not give state names to says which it is.
|
|
90
|
+
* Printing "(none) -> (none)" is honest and useless; naming the gap keeps
|
|
91
|
+
* nobody reading a real gap as a broken tool (`commands/issue.ts` says the
|
|
92
|
+
* same thing in the same words).
|
|
93
|
+
*/
|
|
94
|
+
export declare function historyChange(row: HistoryRow): string;
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The row shapes the `work_*` tools read off `/api/work/**`, and the pure
|
|
3
|
+
* functions that turn them into what a tool answers with: the trimmed list
|
|
4
|
+
* row, the completeness and outside-project notes, one issue's text, one
|
|
5
|
+
* history line.
|
|
6
|
+
*
|
|
7
|
+
* Split out of `work-tools.ts` (BLI-10445) when that file crossed the
|
|
8
|
+
* 500-line readability ratchet. The seam: `work-tools.ts` registers the tools
|
|
9
|
+
* and calls the doors; nothing here calls a door or knows a session, so this
|
|
10
|
+
* module changes when a door's row shape or a rendered sentence does, and can
|
|
11
|
+
* be read without the registration code around it.
|
|
12
|
+
*
|
|
13
|
+
* Every string moved verbatim.
|
|
14
|
+
*/
|
|
15
|
+
import { assigneeLabel } from "./work-directory.js";
|
|
16
|
+
export function issueListRow(row) {
|
|
17
|
+
return {
|
|
18
|
+
id: row.id,
|
|
19
|
+
identifier: row.identifier,
|
|
20
|
+
title: row.title,
|
|
21
|
+
state: row.state,
|
|
22
|
+
priority: row.priority ?? null,
|
|
23
|
+
assignee_id: row.assignee_id ?? null,
|
|
24
|
+
project_id: row.project_id ?? null,
|
|
25
|
+
parent_id: row.parent_id ?? null,
|
|
26
|
+
updated_at: row.updated_at ?? null,
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
export function listCompleteness(value) {
|
|
30
|
+
if (!value || typeof value !== "object")
|
|
31
|
+
return null;
|
|
32
|
+
const record = value;
|
|
33
|
+
const status = record["status"];
|
|
34
|
+
if (status !== "complete" && status !== "partial" && status !== "unknown")
|
|
35
|
+
return null;
|
|
36
|
+
return { status, total: typeof record["total"] === "number" ? record["total"] : null };
|
|
37
|
+
}
|
|
38
|
+
export function outsideProject(value) {
|
|
39
|
+
if (!value || typeof value !== "object")
|
|
40
|
+
return null;
|
|
41
|
+
const record = value;
|
|
42
|
+
if (!Array.isArray(record["issues"]))
|
|
43
|
+
return null;
|
|
44
|
+
return { status: String(record["status"] ?? "complete"), issues: record["issues"] };
|
|
45
|
+
}
|
|
46
|
+
/** BLI-10447: sub-issues of the listed issues that live in no project or another one. */
|
|
47
|
+
export function outsideNote(outside) {
|
|
48
|
+
if (!outside)
|
|
49
|
+
return "";
|
|
50
|
+
const named = outside.issues.map((issue) => `${issue.identifier} (under ${issue.parent_identifier ?? "?"}, ${issue.project_id ? `project ${issue.project_id}` : "no project"})`);
|
|
51
|
+
const found = named.length > 0 ? ` ${named.length} sub-issue(s) of these sit outside the project: ${named.join(", ")}.` : "";
|
|
52
|
+
const gap = outside.status === "failed" ? " The outside-project check failed (read_failed)."
|
|
53
|
+
: outside.status === "capped" ? " The outside-project check was capped; more may exist." : "";
|
|
54
|
+
return found + gap;
|
|
55
|
+
}
|
|
56
|
+
export function renderOneIssue(result, directory) {
|
|
57
|
+
const { issue, comments } = result;
|
|
58
|
+
const commentText = comments.map((comment) => `[${comment.created_at ?? ""}] ${comment.body_markdown ?? ""}`).join("\n\n");
|
|
59
|
+
return `${issue?.identifier ?? result.ref} ${issue?.title ?? ""} (${issue?.state ?? "?"}) · assignee ${assigneeLabel(issue?.assignee_id, directory)}`
|
|
60
|
+
+ `\n\n${issue?.description ?? "(no description)"}`
|
|
61
|
+
+ (commentText ? `\n\n--- comments ---\n${commentText}` : "")
|
|
62
|
+
+ (result.commentsUnread ? "\n\n(comments could not be read on this call)" : "");
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* A row the Linear import could not give state names to says which it is.
|
|
66
|
+
* Printing "(none) -> (none)" is honest and useless; naming the gap keeps
|
|
67
|
+
* nobody reading a real gap as a broken tool (`commands/issue.ts` says the
|
|
68
|
+
* same thing in the same words).
|
|
69
|
+
*/
|
|
70
|
+
export function historyChange(row) {
|
|
71
|
+
if (row.from_value === null && row.to_value === null) {
|
|
72
|
+
return `(no state names recorded — imported from ${row.source ?? "elsewhere"})`;
|
|
73
|
+
}
|
|
74
|
+
return `${row.from_value ?? "(none)"} -> ${row.to_value ?? "(none)"}`;
|
|
75
|
+
}
|
package/dist/work-tools.js
CHANGED
|
@@ -18,6 +18,7 @@ import { z } from "zod";
|
|
|
18
18
|
import { callAgentDoor } from "./agent-door.js";
|
|
19
19
|
import { DEFAULT_LIST_LIMIT, doorFailureText, errorResult, moreNote, registrarFor, textResult, withSession, } from "./tool-result.js";
|
|
20
20
|
import { assigneeLabel, resolveAssigneeDirectory, withAssigneeIdentity } from "./work-directory.js";
|
|
21
|
+
import { historyChange, issueListRow, listCompleteness, outsideNote, outsideProject, renderOneIssue, } from "./work-issue-rows.js";
|
|
21
22
|
/** The closed state vocabulary, verbatim from `lib/work/api-doors.ts` `WORK_ISSUE_STATES`. */
|
|
22
23
|
export const WORK_ISSUE_STATES = [
|
|
23
24
|
"backlog",
|
|
@@ -29,46 +30,6 @@ export const WORK_ISSUE_STATES = [
|
|
|
29
30
|
];
|
|
30
31
|
/** `/api/work/issues`' own ceiling, and the most a caller may ask for here. */
|
|
31
32
|
const ISSUE_LIST_MAX = 1000;
|
|
32
|
-
function issueListRow(row) {
|
|
33
|
-
return {
|
|
34
|
-
id: row.id,
|
|
35
|
-
identifier: row.identifier,
|
|
36
|
-
title: row.title,
|
|
37
|
-
state: row.state,
|
|
38
|
-
priority: row.priority ?? null,
|
|
39
|
-
assignee_id: row.assignee_id ?? null,
|
|
40
|
-
project_id: row.project_id ?? null,
|
|
41
|
-
parent_id: row.parent_id ?? null,
|
|
42
|
-
updated_at: row.updated_at ?? null,
|
|
43
|
-
};
|
|
44
|
-
}
|
|
45
|
-
function listCompleteness(value) {
|
|
46
|
-
if (!value || typeof value !== "object")
|
|
47
|
-
return null;
|
|
48
|
-
const record = value;
|
|
49
|
-
const status = record["status"];
|
|
50
|
-
if (status !== "complete" && status !== "partial" && status !== "unknown")
|
|
51
|
-
return null;
|
|
52
|
-
return { status, total: typeof record["total"] === "number" ? record["total"] : null };
|
|
53
|
-
}
|
|
54
|
-
function outsideProject(value) {
|
|
55
|
-
if (!value || typeof value !== "object")
|
|
56
|
-
return null;
|
|
57
|
-
const record = value;
|
|
58
|
-
if (!Array.isArray(record["issues"]))
|
|
59
|
-
return null;
|
|
60
|
-
return { status: String(record["status"] ?? "complete"), issues: record["issues"] };
|
|
61
|
-
}
|
|
62
|
-
/** BLI-10447: sub-issues of the listed issues that live in no project or another one. */
|
|
63
|
-
function outsideNote(outside) {
|
|
64
|
-
if (!outside)
|
|
65
|
-
return "";
|
|
66
|
-
const named = outside.issues.map((issue) => `${issue.identifier} (under ${issue.parent_identifier ?? "?"}, ${issue.project_id ? `project ${issue.project_id}` : "no project"})`);
|
|
67
|
-
const found = named.length > 0 ? ` ${named.length} sub-issue(s) of these sit outside the project: ${named.join(", ")}.` : "";
|
|
68
|
-
const gap = outside.status === "failed" ? " The outside-project check failed (read_failed)."
|
|
69
|
-
: outside.status === "capped" ? " The outside-project check was capped; more may exist." : "";
|
|
70
|
-
return found + gap;
|
|
71
|
-
}
|
|
72
33
|
/** One issue plus its comments — a comment read that fails does not lose the issue already in hand. */
|
|
73
34
|
async function fetchOneIssue(deps, session, ref) {
|
|
74
35
|
const response = await callAgentDoor(session, deps.fetchImpl, "GET", `/api/work/issues/${encodeURIComponent(ref)}`);
|
|
@@ -87,26 +48,6 @@ async function fetchOneIssue(deps, session, ref) {
|
|
|
87
48
|
...(commentsResponse.ok ? {} : { commentsUnread: true }),
|
|
88
49
|
};
|
|
89
50
|
}
|
|
90
|
-
function renderOneIssue(result, directory) {
|
|
91
|
-
const { issue, comments } = result;
|
|
92
|
-
const commentText = comments.map((comment) => `[${comment.created_at ?? ""}] ${comment.body_markdown ?? ""}`).join("\n\n");
|
|
93
|
-
return `${issue?.identifier ?? result.ref} ${issue?.title ?? ""} (${issue?.state ?? "?"}) · assignee ${assigneeLabel(issue?.assignee_id, directory)}`
|
|
94
|
-
+ `\n\n${issue?.description ?? "(no description)"}`
|
|
95
|
-
+ (commentText ? `\n\n--- comments ---\n${commentText}` : "")
|
|
96
|
-
+ (result.commentsUnread ? "\n\n(comments could not be read on this call)" : "");
|
|
97
|
-
}
|
|
98
|
-
/**
|
|
99
|
-
* A row the Linear import could not give state names to says which it is.
|
|
100
|
-
* Printing "(none) -> (none)" is honest and useless; naming the gap keeps
|
|
101
|
-
* nobody reading a real gap as a broken tool (`commands/issue.ts` says the
|
|
102
|
-
* same thing in the same words).
|
|
103
|
-
*/
|
|
104
|
-
function historyChange(row) {
|
|
105
|
-
if (row.from_value === null && row.to_value === null) {
|
|
106
|
-
return `(no state names recorded — imported from ${row.source ?? "elsewhere"})`;
|
|
107
|
-
}
|
|
108
|
-
return `${row.from_value ?? "(none)"} -> ${row.to_value ?? "(none)"}`;
|
|
109
|
-
}
|
|
110
51
|
/** A project id from a project NAME or a uuid — exact after case-folding, never fuzzy. */
|
|
111
52
|
async function resolveProjectId(deps, session, ref) {
|
|
112
53
|
const response = await callAgentDoor(session, deps.fetchImpl, "GET", "/api/work/projects?include_archived=true");
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bli-cockpit/mcp",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.67",
|
|
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",
|