@bli-cockpit/mcp 0.1.87 → 0.1.89

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 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
- **124 of 129 Tower verbs have an MCP twin.**
112
+ **125 of 130 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
@@ -169,6 +169,7 @@ three tables below.
169
169
  | `cockpit issue update` | `work_update_issue` | `PATCH /api/work/issues/[id]` |
170
170
  | `cockpit jarvis` | `jarvis_ask` | `POST /api/jarvis/cli` |
171
171
  | `cockpit jarvis --trace` | `jarvis_trace` | `GET /api/ops/trace/[id]` |
172
+ | `cockpit logs` | `logs_digest` | `LOCAL @bli-cockpit/telemetry-core/log-digest (this machine's logs; no HTTP door)` |
172
173
  | `cockpit mail accounts` | `mail_accounts` | `GET /api/mail/accounts` |
173
174
  | `cockpit mail attachment` | `mail_attachment` | `GET /api/mail/attachments/[id]?meta=1` |
174
175
  | `cockpit mail detach` | `mail_detach` | `DELETE /api/mail/accounts` |
@@ -0,0 +1,28 @@
1
+ /**
2
+ * `logs_digest` — the twin of `cockpit logs` (BLI-12833).
3
+ *
4
+ * A token-cheap digest of Tower's own logs, read on THIS machine: the same
5
+ * `digestSource` from `@bli-cockpit/telemetry-core/log-digest` the CLI calls, so the two print
6
+ * the same text. Variable parts of each line are masked into a template, equal
7
+ * templates are counted, routine runs become counts, and every template is
8
+ * classified: `our_bug` and `unknown` first, harmless ones counted on one line.
9
+ *
10
+ * It is local and read-only. Sources: `collector` (this machine's sync logs),
11
+ * `dashboard` (Vercel runtime logs through the `vercel` CLI), `railway` and
12
+ * `railway:<service>` (through the `railway` CLI), and `-` with the lines passed
13
+ * as `text`. A CLI that is missing or not logged in is a named refusal
14
+ * (`vercel_cli_missing`, `railway_not_logged_in`), never a stack trace.
15
+ *
16
+ * Not on the hosted server: it reads the machine's files and spawns programs
17
+ * there (`REMOTE_EXCLUDED_TOOLS`).
18
+ */
19
+ import { digestSource, type DigestOutcome } from "@bli-cockpit/telemetry-core/log-digest";
20
+ export interface LogsToolDeps {
21
+ /** Test seam: replaces the digest. */
22
+ digest?: (...args: Parameters<typeof digestSource>) => Promise<DigestOutcome>;
23
+ /** Test seam: where the collector's state lives; defaults to this machine's. */
24
+ stateDir?: string;
25
+ }
26
+ export declare function registerLogsTools(server: {
27
+ registerTool: (...args: never[]) => unknown;
28
+ }, deps?: LogsToolDeps): void;
@@ -0,0 +1,79 @@
1
+ /**
2
+ * `logs_digest` — the twin of `cockpit logs` (BLI-12833).
3
+ *
4
+ * A token-cheap digest of Tower's own logs, read on THIS machine: the same
5
+ * `digestSource` from `@bli-cockpit/telemetry-core/log-digest` the CLI calls, so the two print
6
+ * the same text. Variable parts of each line are masked into a template, equal
7
+ * templates are counted, routine runs become counts, and every template is
8
+ * classified: `our_bug` and `unknown` first, harmless ones counted on one line.
9
+ *
10
+ * It is local and read-only. Sources: `collector` (this machine's sync logs),
11
+ * `dashboard` (Vercel runtime logs through the `vercel` CLI), `railway` and
12
+ * `railway:<service>` (through the `railway` CLI), and `-` with the lines passed
13
+ * as `text`. A CLI that is missing or not logged in is a named refusal
14
+ * (`vercel_cli_missing`, `railway_not_logged_in`), never a stack trace.
15
+ *
16
+ * Not on the hosted server: it reads the machine's files and spawns programs
17
+ * there (`REMOTE_EXCLUDED_TOOLS`).
18
+ */
19
+ import os from "node:os";
20
+ import { digestSource, routineText } from "@bli-cockpit/telemetry-core/log-digest";
21
+ import { getUserLocalCockpitPaths } from "@bli-cockpit/telemetry-core";
22
+ import { z } from "zod";
23
+ import { errorResult, registrarFor, textResult } from "./tool-result.js";
24
+ /** Past this the structured half would crowd the 32 KB per-half read budget. */
25
+ const MAX_TEMPLATES_RETURNED = 40;
26
+ const MAX_TEXT_CHARS = 256_000;
27
+ export function registerLogsTools(server, deps = {}) {
28
+ const digest = deps.digest ?? digestSource;
29
+ registrarFor(server)("logs_digest", {
30
+ title: "Digest Tower's logs (about 2 KB)",
31
+ 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 "
32
+ + "busy source is tens of thousands of lines; this returns about 2 KB. Each line's variable parts are masked into a template, equal templates "
33
+ + "are counted with first and last time, routine runs become counts, and every template is classified (our_bug and unknown first, then "
34
+ + "capacity, config_drift, rate_pushback, hiccup; dead_source and benign are counted on one line and never alerted). Sources: `collector` "
35
+ + "(this machine's sync.log and sync.err.log), `dashboard` (Vercel runtime logs through the `vercel` CLI), `railway` (every Tower service) "
36
+ + "or `railway:<service>` (through the `railway` CLI), `-` (lines passed as `text`). `since` is 15m, 1h, 2d or an ISO time (default 15m). "
37
+ + "Pass `raw` (an 8-hex template id from a digest line) to get the original lines behind that template, redacted, newest last. A missing or "
38
+ + "logged-out CLI is a named refusal (vercel_cli_missing, vercel_not_logged_in, railway_cli_missing, railway_not_logged_in). `cockpit logs`, same code.",
39
+ inputSchema: {
40
+ source: z.string().min(1).max(80).describe("collector, dashboard, railway, railway:<service> or - (lines in `text`)."),
41
+ since: z.string().min(1).max(40).optional().describe("Window: 15m, 1h, 2d or an ISO time. Default 15m."),
42
+ raw: z.string().regex(/^[0-9a-f]{8}$/).optional().describe("A template id from a digest line: return the original lines behind it."),
43
+ limit: z.number().int().min(1).max(200).optional().describe("With raw: how many original lines. Default 20."),
44
+ all_levels: z.boolean().optional().describe("dashboard only: also read info-only requests (slow, minutes)."),
45
+ text: z.string().max(MAX_TEXT_CHARS).optional().describe("The lines to digest when source is -."),
46
+ },
47
+ }, async (args) => answer(digest, deps, args));
48
+ }
49
+ async function answer(digest, deps, args) {
50
+ const source = String(args["source"]);
51
+ const text = typeof args["text"] === "string" ? args["text"] : undefined;
52
+ if (source === "-" && !text) {
53
+ return errorResult('logs_digest refused (stdin_empty): source "-" digests the lines you pass in `text`; none were given.');
54
+ }
55
+ const outcome = await digest({
56
+ source,
57
+ since: typeof args["since"] === "string" ? args["since"] : undefined,
58
+ raw: typeof args["raw"] === "string" ? args["raw"] : undefined,
59
+ rawLimit: typeof args["limit"] === "number" ? args["limit"] : undefined,
60
+ allLevels: args["all_levels"] === true,
61
+ }, { stateDir: deps.stateDir ?? getUserLocalCockpitPaths(os.homedir()).state_dir, stdinText: text, stdinIsTerminal: false });
62
+ if (!outcome.ok)
63
+ return errorResult(`logs_digest refused (${outcome.reason}): ${outcome.detail}`);
64
+ if (outcome.kind === "raw") {
65
+ return textResult(outcome.text, { kind: "raw", source: outcome.source, tid: outcome.tid, window: outcome.window, matched: outcome.matched, lines: outcome.lines });
66
+ }
67
+ return textResult(outcome.text, {
68
+ kind: "digest",
69
+ source: outcome.source,
70
+ window: outcome.window,
71
+ lines: outcome.lines,
72
+ noise: outcome.noise,
73
+ notes: outcome.notes,
74
+ measure: outcome.measure,
75
+ templates_total: outcome.templates.length,
76
+ templates: outcome.templates.slice(0, MAX_TEMPLATES_RETURNED),
77
+ work: routineText(outcome.routine, 12),
78
+ });
79
+ }
package/dist/server.js CHANGED
@@ -34,6 +34,7 @@ import { registerWorkTools } from "./work-tools.js";
34
34
  import { registerCareersTools } from "./careers-tools.js";
35
35
  import { registerRenameTools } from "./rename-tools.js";
36
36
  import { registerUsageTools } from "./usage-tools.js";
37
+ import { registerLogsTools } from "./logs-tools.js";
37
38
  import { registerSourcesTools } from "./sources-tools.js";
38
39
  import { TOWER_SERVER_ICONS } from "./server-icons.js";
39
40
  import { installToolRegistration } from "./tool-registration.js";
@@ -500,5 +501,8 @@ export function createServer(deps, options = {}) {
500
501
  // `cockpit sources check` on this machine. No delete twin, ever: Tower never
501
502
  // deletes a person's transcripts and an agent must not do it through a tool.
502
503
  registerSourcesTools(server);
504
+ // BLI-12833: a token-cheap digest of Tower's own logs, read on this machine
505
+ // with the shared `@bli-cockpit/telemetry-core/log-digest`. Read-only; not on the hosted server.
506
+ registerLogsTools(server);
503
507
  return server;
504
508
  }
@@ -36,6 +36,7 @@ export const REMOTE_EXCLUDED_TOOLS = {
36
36
  get_active_tickets: "legacy event stream; authenticates with an operator token the host does not hold",
37
37
  notes_upload: "reads a file by path from the machine the server runs on; use notes_paste with the text",
38
38
  sources_check: "spawns `cockpit sources check` on the machine the server runs on",
39
+ logs_digest: "reads the machine's collector logs and spawns the vercel and railway CLIs on the machine the server runs on",
39
40
  };
40
41
  const READS = new Set(READ_TOOLS);
41
42
  const DESTRUCTIVE = new Set(DESTRUCTIVE_TOOLS);
@@ -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", "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", "notes_show", "notes_shelf", "notes_shelves", "notes_place", "notes_accounts", "notes_sync_status", "notes_granola_status", "ops_status", "slack_coverage", "slack_read", "memory_profile", "memory_consolidation", "settings_show", "settings_list", "model_show", "team_members", "team_device_list", "scout_board", "workbook_read", "tower_search", "sources_check"];
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", "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", "notes_show", "notes_shelf", "notes_shelves", "notes_place", "notes_accounts", "notes_sync_status", "notes_granola_status", "ops_status", "slack_coverage", "slack_read", "memory_profile", "memory_consolidation", "settings_show", "settings_list", "model_show", "team_members", "team_device_list", "scout_board", "workbook_read", "tower_search", "sources_check", "logs_digest"];
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).
@@ -78,6 +78,8 @@ export const READ_TOOLS = [
78
78
  "workbook_read",
79
79
  "tower_search",
80
80
  "sources_check",
81
+ // BLI-12833: a digest of Tower's own logs, read on this machine. Reads only.
82
+ "logs_digest",
81
83
  ];
82
84
  /**
83
85
  * Tools that write, but answer a dry run that writes nothing. The smoke calls
@@ -239,6 +239,9 @@ export const MCP_TWINS = {
239
239
  "github merge": { tool: "github_merge", door: "POST /api/github/pull-request (merge)" },
240
240
  "github retitle": { tool: "github_retitle", door: "POST /api/github/pull-request (retitle)" },
241
241
  "github rerun": { tool: "github_rerun", door: "POST /api/github/pull-request (rerun)" },
242
+ // BLI-12833: a local read, not a Tower door: it runs the shared log digest over
243
+ // this machine's collector logs and the `vercel` / `railway` CLIs.
244
+ "logs": { tool: "logs_digest", door: "LOCAL @bli-cockpit/telemetry-core/log-digest (this machine's logs; no HTTP door)" },
242
245
  "usage sessions": { tool: "usage_sessions", door: "GET /api/usage/sessions" },
243
246
  "usage people": { tool: "usage_people", door: "GET /api/usage/people" },
244
247
  "docs list": { tool: "docs_list", door: "GET /api/docs/documents" },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bli-cockpit/mcp",
3
- "version": "0.1.87",
3
+ "version": "0.1.89",
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.58",
40
+ "@bli-cockpit/telemetry-core": "0.1.59",
41
41
  "@modelcontextprotocol/sdk": "^1.29.0",
42
42
  "zod": "^4.3.6"
43
43
  },