@thehammer/danx-dashboard-mcp 0.1.104 → 0.1.108
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/dist/handlers.js +133 -0
- package/dist/index.js +14 -5
- package/package.json +1 -1
package/dist/handlers.js
CHANGED
|
@@ -1383,3 +1383,136 @@ export async function failureCategoryUpdate(client, args) {
|
|
|
1383
1383
|
body: patch,
|
|
1384
1384
|
});
|
|
1385
1385
|
}
|
|
1386
|
+
// ---------------- dispatch_transcript_search (DX-3221) ----------------
|
|
1387
|
+
/**
|
|
1388
|
+
* DX-3221 — search or tail ANOTHER dispatch's stored JSONL transcript
|
|
1389
|
+
* without ever touching the filesystem. Wraps the existing durable
|
|
1390
|
+
* `GET /api/dispatches/:id/logs` route (DX-1682, reading the `dispatch_logs`
|
|
1391
|
+
* sink every worker already streams its raw JSONL lines into per DX-1484 —
|
|
1392
|
+
* no dashboard-side change needed, that data is already captured today for
|
|
1393
|
+
* every worker dispatch).
|
|
1394
|
+
*
|
|
1395
|
+
* Why this exists instead of a filesystem path: CLAUDE.md Core Principle 5
|
|
1396
|
+
* makes the worktree boundary binary (inside allow, outside reject, no
|
|
1397
|
+
* per-card opt-in) — a Grep/Glob tool pointed at `~/.claude/projects/`, or a
|
|
1398
|
+
* worktree-guard carve-out for that path, both widen that boundary the same
|
|
1399
|
+
* way a Bash `grep` would. This tool needs no such carve-out: it is an
|
|
1400
|
+
* ordinary HTTP call authenticated the same way every other
|
|
1401
|
+
* `mcp__danx-dashboard__*` tool already is, so worktree-guard never enters
|
|
1402
|
+
* the picture and CP5 stays untouched.
|
|
1403
|
+
*
|
|
1404
|
+
* Why this ALSO fixes the underlying complaint `Read` can't: `Read`
|
|
1405
|
+
* paginates by LINE, and a persisted JSONL entry can itself be one giant
|
|
1406
|
+
* line (a large MCP tool-result payload, escaped-newline JSON) that exceeds
|
|
1407
|
+
* the 25000-token cap with no way to sub-page inside it (`offset`/`limit`
|
|
1408
|
+
* are line-granular). This handler does the string/regex search ITSELF,
|
|
1409
|
+
* server-side of the model (inside this Node process), and only ever hands
|
|
1410
|
+
* back a bounded excerpt around a match — never the whole line — so a
|
|
1411
|
+
* single oversized line is no longer un-searchable.
|
|
1412
|
+
*/
|
|
1413
|
+
const DISPATCHES_BASE_PATH = "/api/dispatches";
|
|
1414
|
+
/** Tail lines returned when no `pattern` is given. */
|
|
1415
|
+
const DEFAULT_TAIL_LINES = 20;
|
|
1416
|
+
/** Hard ceiling on `tail` — keeps a mistaken huge request bounded. */
|
|
1417
|
+
const MAX_TAIL_LINES = 200;
|
|
1418
|
+
/** Characters of context returned around (or up to, for tail) each line. */
|
|
1419
|
+
const DEFAULT_CONTEXT_CHARS = 1000;
|
|
1420
|
+
/** Hard ceiling on `contextChars` — this is what keeps a single giant line from blowing the model's context the way `Read` does today. */
|
|
1421
|
+
const MAX_CONTEXT_CHARS = 4000;
|
|
1422
|
+
/** Matches returned before search stops scanning further lines. */
|
|
1423
|
+
const DEFAULT_MAX_MATCHES = 10;
|
|
1424
|
+
/** Hard ceiling on `maxMatches`. */
|
|
1425
|
+
const MAX_MATCHES_CEILING = 50;
|
|
1426
|
+
function clampPositiveInt(value, fallback, ceiling) {
|
|
1427
|
+
const n = value ?? fallback;
|
|
1428
|
+
if (!Number.isFinite(n) || n <= 0)
|
|
1429
|
+
return fallback;
|
|
1430
|
+
return Math.min(Math.floor(n), ceiling);
|
|
1431
|
+
}
|
|
1432
|
+
/**
|
|
1433
|
+
* `GET /api/dispatches/:id/logs` (DX-1682) → search or tail, in-process.
|
|
1434
|
+
* Proxies the SAME durable sink `handleGetDispatchLogs` already serves —
|
|
1435
|
+
* this tool adds no new dashboard route, only client-side windowing.
|
|
1436
|
+
*/
|
|
1437
|
+
export async function dispatchTranscriptSearch(client, args) {
|
|
1438
|
+
const hasPattern = args.pattern !== undefined && args.pattern !== "";
|
|
1439
|
+
// Validate the regex BEFORE the network call — a malformed `pattern` is a
|
|
1440
|
+
// caller input error, not a reason to spend an HTTP round-trip first.
|
|
1441
|
+
let re = null;
|
|
1442
|
+
if (hasPattern) {
|
|
1443
|
+
try {
|
|
1444
|
+
re = new RegExp(args.pattern, "gi");
|
|
1445
|
+
}
|
|
1446
|
+
catch (err) {
|
|
1447
|
+
return {
|
|
1448
|
+
ok: false,
|
|
1449
|
+
status: 400,
|
|
1450
|
+
body: {
|
|
1451
|
+
error: `dispatch_transcript_search: invalid \`pattern\` regex: ${err instanceof Error ? err.message : String(err)}`,
|
|
1452
|
+
},
|
|
1453
|
+
};
|
|
1454
|
+
}
|
|
1455
|
+
}
|
|
1456
|
+
const logsResult = await client.request({
|
|
1457
|
+
method: "GET",
|
|
1458
|
+
path: `/${encodeURIComponent(args.dispatchId)}/logs`,
|
|
1459
|
+
basePath: DISPATCHES_BASE_PATH,
|
|
1460
|
+
});
|
|
1461
|
+
if (!logsResult.ok) {
|
|
1462
|
+
return logsResult;
|
|
1463
|
+
}
|
|
1464
|
+
const body = logsResult.body;
|
|
1465
|
+
const lines = body?.lines ?? [];
|
|
1466
|
+
const contextChars = clampPositiveInt(args.contextChars, DEFAULT_CONTEXT_CHARS, MAX_CONTEXT_CHARS);
|
|
1467
|
+
if (re) {
|
|
1468
|
+
const maxMatches = clampPositiveInt(args.maxMatches, DEFAULT_MAX_MATCHES, MAX_MATCHES_CEILING);
|
|
1469
|
+
const results = [];
|
|
1470
|
+
for (const { ordinal, line } of lines) {
|
|
1471
|
+
re.lastIndex = 0;
|
|
1472
|
+
const match = re.exec(line);
|
|
1473
|
+
if (!match)
|
|
1474
|
+
continue;
|
|
1475
|
+
const half = Math.floor(contextChars / 2);
|
|
1476
|
+
const start = Math.max(0, match.index - half);
|
|
1477
|
+
const end = Math.min(line.length, match.index + match[0].length + half);
|
|
1478
|
+
const truncated = start > 0 || end < line.length;
|
|
1479
|
+
results.push({
|
|
1480
|
+
ordinal,
|
|
1481
|
+
excerpt: `${start > 0 ? "…" : ""}${line.slice(start, end)}${end < line.length ? "…" : ""}`,
|
|
1482
|
+
truncated,
|
|
1483
|
+
});
|
|
1484
|
+
if (results.length >= maxMatches)
|
|
1485
|
+
break;
|
|
1486
|
+
}
|
|
1487
|
+
return {
|
|
1488
|
+
ok: true,
|
|
1489
|
+
status: 200,
|
|
1490
|
+
body: {
|
|
1491
|
+
dispatch_id: args.dispatchId,
|
|
1492
|
+
pattern: args.pattern,
|
|
1493
|
+
total_lines: lines.length,
|
|
1494
|
+
matched_lines: results.length,
|
|
1495
|
+
results,
|
|
1496
|
+
},
|
|
1497
|
+
};
|
|
1498
|
+
}
|
|
1499
|
+
const tail = clampPositiveInt(args.tail, DEFAULT_TAIL_LINES, MAX_TAIL_LINES);
|
|
1500
|
+
const results = lines.slice(-tail).map(({ ordinal, line }) => {
|
|
1501
|
+
const truncated = line.length > contextChars;
|
|
1502
|
+
return {
|
|
1503
|
+
ordinal,
|
|
1504
|
+
excerpt: truncated ? `${line.slice(0, contextChars)}…` : line,
|
|
1505
|
+
truncated,
|
|
1506
|
+
};
|
|
1507
|
+
});
|
|
1508
|
+
return {
|
|
1509
|
+
ok: true,
|
|
1510
|
+
status: 200,
|
|
1511
|
+
body: {
|
|
1512
|
+
dispatch_id: args.dispatchId,
|
|
1513
|
+
total_lines: lines.length,
|
|
1514
|
+
returned_lines: results.length,
|
|
1515
|
+
results,
|
|
1516
|
+
},
|
|
1517
|
+
};
|
|
1518
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -52,6 +52,7 @@
|
|
|
52
52
|
* - failure_category_list GET /api/failure-categories (DX-2791/DX-2792, board-less)
|
|
53
53
|
* - failure_category_create POST /api/failure-categories (DX-2791/DX-2792, board-less)
|
|
54
54
|
* - failure_category_update PATCH /api/failure-categories/:id (DX-2791/DX-2792, board-less)
|
|
55
|
+
* - dispatch_transcript_search GET /api/dispatches/:id/logs (DX-3221 — search/tail, no new route)
|
|
55
56
|
*
|
|
56
57
|
* DX-2683 — THE PLAN TOOLS ARE SESSION-BOUND, and asymmetrically so. Reads
|
|
57
58
|
* may name any plan; WRITES take no plan id at all and act on the plan this
|
|
@@ -101,7 +102,7 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
|
101
102
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
102
103
|
import { z } from "zod";
|
|
103
104
|
import { DashboardHttpClient } from "./http-client.js";
|
|
104
|
-
import { issueAttach, issueChecklist, issueComment, issueCreate, issueDependency, issueEdit, issueGet, issueList, issueProblem, issueQualityGate, issueQualityGateVerdict, issueRetireBranch, issueRetro, issueSolution, issueTransition, issueTriage, briefGetPage, briefList, briefSetPage, failureCategoryCreate, failureCategoryList, failureCategoryUpdate, planAddArchitectureSection, planAddCard, planAddNote, planAddRecord, planConnect, planCreate, planDeleteArchitectureSection, planDeleteNote, planDeleteRecord, planGet, PLAN_FIELD_GROUPS, PLAN_EVENT_KINDS, PLAN_EVENT_ORIGINS, PLAN_STATUSES, ISSUE_BATCH_GET_MAX, LIST_PAGE_MAX_LIMIT, PLAN_GET_CARDS_DEFAULT_LIMIT, PLAN_GET_EVENTS_DEFAULT_LIMIT, PLAN_GET_EVENTS_MAX_LIMIT, planGetArchitectureSection, planGetRecord, planList, planRemoveCard, planRename, planReorderArchitectureSection, planUpdateArchitectureSection, planUpdateNote, planUpdateRecord, repoKnowledgeGet, repoKnowledgeSet, } from "./handlers.js";
|
|
105
|
+
import { issueAttach, issueChecklist, issueComment, issueCreate, issueDependency, issueEdit, issueGet, issueList, issueProblem, issueQualityGate, issueQualityGateVerdict, issueRetireBranch, issueRetro, issueSolution, issueTransition, issueTriage, briefGetPage, briefList, briefSetPage, failureCategoryCreate, failureCategoryList, failureCategoryUpdate, dispatchTranscriptSearch, planAddArchitectureSection, planAddCard, planAddNote, planAddRecord, planConnect, planCreate, planDeleteArchitectureSection, planDeleteNote, planDeleteRecord, planGet, PLAN_FIELD_GROUPS, PLAN_EVENT_KINDS, PLAN_EVENT_ORIGINS, PLAN_STATUSES, ISSUE_BATCH_GET_MAX, LIST_PAGE_MAX_LIMIT, PLAN_GET_CARDS_DEFAULT_LIMIT, PLAN_GET_EVENTS_DEFAULT_LIMIT, PLAN_GET_EVENTS_MAX_LIMIT, planGetArchitectureSection, planGetRecord, planList, planRemoveCard, planRename, planReorderArchitectureSection, planUpdateArchitectureSection, planUpdateNote, planUpdateRecord, repoKnowledgeGet, repoKnowledgeSet, } from "./handlers.js";
|
|
105
106
|
import { PRIORITY_TIER_WORDS } from "./priority.js";
|
|
106
107
|
function readEnvOrDie(name) {
|
|
107
108
|
const v = process.env[name];
|
|
@@ -278,7 +279,7 @@ const sortField = z
|
|
|
278
279
|
order: z.enum(SORT_ORDERS),
|
|
279
280
|
}))
|
|
280
281
|
.optional()
|
|
281
|
-
.describe("Multi-column sort — ordered list of {column, order}. Absent → the server's default order (priority desc, repo_name asc, with a numeric-id tiebreaker always appended).");
|
|
282
|
+
.describe("Multi-column sort — ordered list of {column, order}. Absent → the server's default order (priority desc (higher priority=most urgent first), repo_name asc, with a numeric-id tiebreaker always appended).");
|
|
282
283
|
// DX-1290 — the uniform checklist-item status, extended DX-2653 with
|
|
283
284
|
// `deferred` (a named real-world/post-deploy check still outstanding).
|
|
284
285
|
// Terminal = passing|cancelled|deferred.
|
|
@@ -325,7 +326,7 @@ const DESCRIPTION_DESCRIBE = 'The full body ("Context"): evidence, examples, tec
|
|
|
325
326
|
server.tool("issue_list",
|
|
326
327
|
// DX-2735: trimmed to pay for the problem tools inside the work-profile
|
|
327
328
|
// injected-surface budget — same facts, no repeated prose.
|
|
328
|
-
"List cards via GET /api/issues. Board-scoped; see `board`. `filter`: type, parent_id, dispatchable_derived, status_derived[], self_dispatchable_derived, assigned_agent, include_closed, include_deleted, q (free text over id+title+description). THE DEFAULT ROW IS MINIMAL — scalars only (id, type, title, status, parent_id, priority, created_at, updated_at, assigned_agent, content_hash), no joins; opt into heavy data with `fields` groups: description (+ summary), problems (open_problem_count — a card needs a human exactly when this is > 0), ac, comments, retro, dependencies, triage, assignment, quality_gates, children, effort. `sort`: [{column, order}] over id|priority|repo_name|title|type|status_derived|triage_ice_total|created_at|updated_at; default priority desc, repo_name asc, numeric-id tiebreaker. `limit`/`offset` page (uncapped by default). issue_get reads one card in full.", {
|
|
329
|
+
"List cards via GET /api/issues. Board-scoped; see `board`. `filter`: type, parent_id, dispatchable_derived, status_derived[], self_dispatchable_derived, assigned_agent, include_closed, include_deleted, q (free text over id+title+description). THE DEFAULT ROW IS MINIMAL — scalars only (id, type, title, status, parent_id, priority, created_at, updated_at, assigned_agent, content_hash), no joins; opt into heavy data with `fields` groups: description (+ summary), problems (open_problem_count — a card needs a human exactly when this is > 0), ac, comments, retro, dependencies, triage, assignment, quality_gates, children, effort. `sort`: [{column, order}] over id|priority|repo_name|title|type|status_derived|triage_ice_total|created_at|updated_at; default priority desc (highest priority=most urgent first), repo_name asc, numeric-id tiebreaker. `limit`/`offset` page (uncapped by default). DX-3113 — `include_closed` defaults to FALSE: a bare call silently excludes every Done/Cancelled card (leaf AND container alike). The response always carries `total` (the full count matching every filter except limit/offset — compare against `issues.length` to tell an exhausted list from a truncated one) and, whenever `include_closed` was not explicitly `true`, `closed_excluded` (how many additional terminal cards the default withheld — re-call with `include_closed: true` to see them). issue_get reads one card in full.", {
|
|
329
330
|
filter: z
|
|
330
331
|
.object({
|
|
331
332
|
q: z.string().optional(),
|
|
@@ -421,7 +422,7 @@ server.tool("issue_create", '`plan` is REQUIRED on every create (DX-3006): pass
|
|
|
421
422
|
...boardField,
|
|
422
423
|
}, async (args) => jsonResult(await issueCreate(client, args, config.board)));
|
|
423
424
|
// ---------------- issue_edit ----------------
|
|
424
|
-
server.tool("issue_edit", 'Patch a card via PATCH /api/issues/:id/edit. ALLOWED keys: title, summary, description, ac, checklists, effort_level, parent_id, priority, list_id, triage_enabled, type, content_hash. Any other key (lifecycle, triage, dependencies, retro, blocked) is refused 400 with offending_keys[] naming the right tool: issue_transition / issue_triage / issue_comment / issue_dependency / issue_problem / issue_retro. `type`: Story/Bug/Chore = eligible for autonomous pickup; Task or a container (Epic/Feature) removes that eligibility — how a planning item becomes work. `priority` (
|
|
425
|
+
server.tool("issue_edit", 'Patch a card via PATCH /api/issues/:id/edit. ALLOWED keys: title, summary, description, ac, checklists, effort_level, parent_id, priority, list_id, triage_enabled, type, content_hash. Any other key (lifecycle, triage, dependencies, retro, blocked) is refused 400 with offending_keys[] naming the right tool: issue_transition / issue_triage / issue_comment / issue_dependency / issue_problem / issue_retro. `type`: Story/Bug/Chore = eligible for autonomous pickup; Task or a container (Epic/Feature) removes that eligibility — how a planning item becomes work. `priority` (tier word: "lowest"–"critical", or number 0–6; higher = more urgent, prefer tier word) is the ONLY way to set priority; a "Priority:" line does nothing. CHECKLISTS: each item has one status `incomplete|failing|passing|cancelled|deferred`; `deferred` (work done, a real-world/post-deploy check outstanding) REQUIRES `detail`, and a `📡`-prefixed item can never be `passing`. `ac` edits the default "Acceptance Criteria" checklist (items matched by check_item_id, else exact title); `checklists` REPLACES every named checklist with full status control (`{name, items:[{label, detail?, status}]}`). Send `ac` OR `checklists`, not both (400). `list_id` pins the card to a list by id or display NAME; its type must match the card\'s current derived status (400 otherwise — e.g. ready the card first before pinning it to a `ready`-type list); null clears the pin. `content_hash` (DX-2741) is the card\'s optimistic-concurrency token — REQUIRED whenever the edit touches `title` / `description` / `checklists` (NOT `ac`, which keeps its own check_item_id/title diffing): missing → 400, stale → 409 `stale_issue_content` carrying `currentHash` + `currentTitle` + `currentDescription`. Read it off `issue_get`/`issue_list`\'s `content_hash` scalar (present even minimal); on 409, re-`issue_get` and retry with the fresh hash — never blindly.', {
|
|
425
426
|
id: z.string().min(1),
|
|
426
427
|
title: z.string().min(1).optional().describe(TITLE_DESCRIBE),
|
|
427
428
|
summary: z
|
|
@@ -469,7 +470,7 @@ server.tool("issue_edit", 'Patch a card via PATCH /api/issues/:id/edit. ALLOWED
|
|
|
469
470
|
priority: z
|
|
470
471
|
.union([z.enum(PRIORITY_TIER_WORDS), z.number()])
|
|
471
472
|
.optional()
|
|
472
|
-
.describe('A tier word ("lowest"…"critical"
|
|
473
|
+
.describe('A tier word ("lowest"=0–1…"critical"=5–6; higher number = more urgent, prefer tier word) or number in [0,6); higher numbers are more urgent. The only way to set priority.'),
|
|
473
474
|
list_id: z.string().min(1).nullable().optional(),
|
|
474
475
|
triage_enabled: z
|
|
475
476
|
.boolean()
|
|
@@ -938,6 +939,14 @@ server.tool("failure_category_update", "Patch an existing failure category via P
|
|
|
938
939
|
ignoreReason: z.string().nullable().optional(),
|
|
939
940
|
expectedRate: expectedRateField,
|
|
940
941
|
}, async (args) => jsonResult(await failureCategoryUpdate(client, args)));
|
|
942
|
+
// ---------------- dispatch_transcript_search (DX-3221) ----------------
|
|
943
|
+
server.tool("dispatch_transcript_search", "Search or tail ANOTHER dispatch's stored JSONL session transcript via the existing durable GET /api/dispatches/:id/logs sink (DX-1682/DX-1484) — every worker dispatch's raw transcript lines are already captured there today, so this adds no new server route, only in-process search/windowing. Use this instead of trying to Read a session transcript file directly: it needs no filesystem access to `~/.claude/projects/` (an ordinary authenticated HTTP call, so it never touches worktree-guard or CLAUDE.md Core Principle 5's worktree boundary), and it fixes what Read structurally cannot — Read paginates by LINE, and one persisted JSONL entry can itself be a single line far past Read's 25000-token cap with no way to sub-page inside it; this tool does the string/regex search itself and only ever returns a bounded excerpt AROUND a match, never the whole line. With `pattern`: returns up to `maxMatches` matching lines (case-insensitive regex), each as a windowed excerpt of up to `contextChars` characters centered on the first match. Without `pattern`: returns the most recent `tail` lines instead, each capped at `contextChars` characters (a `truncated: true` flag marks a capped excerpt either way — never a caller-visible error the way an oversized Read would throw). `dispatchId` is a real `dispatches.id` — exactly the ids a failure-repair card's own body already lists under \"dispatches that hit it\".", {
|
|
944
|
+
dispatchId: z.string().min(1).describe("The dispatch id whose transcript to search — from a failure-repair card's own body, or any other dispatch id you already have."),
|
|
945
|
+
pattern: z.string().min(1).optional().describe("Case-insensitive regex tested against each raw JSONL line. Omit to get a tail read of the most recent lines instead."),
|
|
946
|
+
tail: z.number().int().positive().optional().describe("Only used when `pattern` is omitted. How many of the most recent lines to return. Defaults to 20, capped at 200."),
|
|
947
|
+
contextChars: z.number().int().positive().optional().describe("Characters of context per returned line/match. Defaults to 1000, capped at 4000 — this cap is what keeps even a single oversized line searchable instead of erroring the way Read does."),
|
|
948
|
+
maxMatches: z.number().int().positive().optional().describe("Only used with `pattern`. Stop after this many matching lines. Defaults to 10, capped at 50."),
|
|
949
|
+
}, async (args) => jsonResult(await dispatchTranscriptSearch(client, args)));
|
|
941
950
|
// ---------------- main ----------------
|
|
942
951
|
async function main() {
|
|
943
952
|
boot();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@thehammer/danx-dashboard-mcp",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.108",
|
|
4
4
|
"description": "Stdio MCP server wrapping danxbot's dashboard /api/issues/* normalized DB-backed HTTP routes for dispatched agents (DX-704 Phase 2).",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|