@thehammer/danx-dashboard-mcp 0.1.106 → 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 +10 -1
- 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];
|
|
@@ -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",
|