@bli-cockpit/mcp 0.1.65 → 0.1.66

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.
@@ -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
+ }
@@ -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.65",
3
+ "version": "0.1.66",
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",