@thehammer/danx-dashboard-mcp 0.1.189 → 0.1.190
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/bridge.js +39 -14
- package/package.json +1 -1
package/dist/bridge.js
CHANGED
|
@@ -34,7 +34,7 @@
|
|
|
34
34
|
* 5. Streams through `runListener` (`listen.ts`), which reconnects within the
|
|
35
35
|
* ticket, and re-mints when that ticket's lease runs out or a fresh ticket
|
|
36
36
|
* is refused once.
|
|
37
|
-
* 6. Writes JSON Lines to stdout: ONE `{type:"ready", boards, cardCount, degraded}`
|
|
37
|
+
* 6. Writes JSON Lines to stdout: ONE `{type:"ready", boards, cardCount, degraded, inventoryError}`
|
|
38
38
|
* once a ticket has been minted and streaming is about to start —
|
|
39
39
|
* DX-2970's `fetchPlanInventory`, a single BEST-EFFORT (never a gate)
|
|
40
40
|
* read of the connected plan's real board/card counts, purely so a
|
|
@@ -140,6 +140,13 @@ function formatDigest(entries) {
|
|
|
140
140
|
return [header, ...entries.map((e) => e.text)].join("\n");
|
|
141
141
|
}
|
|
142
142
|
export const BRIDGE_SUBCOMMAND = "bridge";
|
|
143
|
+
/**
|
|
144
|
+
* DX-3912 — the longest heartbeat `reason` the bridge sends: one under the dashboard's own
|
|
145
|
+
* limit (`HEARTBEAT_REASON_MAX_CHARS` = 200, `src/issues/db/plan-session-heartbeat.ts`),
|
|
146
|
+
* because `oneLine` appends a "…" to a truncated string and a longer reason refuses the whole
|
|
147
|
+
* heartbeat. `bridge.test.ts` asserts the cap.
|
|
148
|
+
*/
|
|
149
|
+
const HEARTBEAT_REASON_MAX_CODE_POINTS = 199;
|
|
143
150
|
export const STREAM_TICKET_PATH = "/api/plan-sessions/me/stream-ticket";
|
|
144
151
|
// DX-2970 — the bare (no `fields=`) cheap-scalars read, used ONLY to report the
|
|
145
152
|
// connected plan's real `boards`/`cardCount` on the `ready` record (see
|
|
@@ -449,15 +456,29 @@ export async function mintTicket(options, deps) {
|
|
|
449
456
|
leaseMs: ticket.leaseMs,
|
|
450
457
|
};
|
|
451
458
|
}
|
|
452
|
-
/**
|
|
459
|
+
/**
|
|
460
|
+
* What is wrong with a 2xx bare `/api/plans/mine` body, or `null` when it carries a usable inventory.
|
|
461
|
+
*
|
|
462
|
+
* DX-3912 — the field names here are the plan resource's WIRE names (`boards`, `card_count`,
|
|
463
|
+
* snake_case since DX-3428), the same ones `src/resources/plan/plan-resource.ts` emits; the
|
|
464
|
+
* contract test `src/issues/__tests__/plan-inventory-bridge-contract.test.ts` runs this parser
|
|
465
|
+
* on the server's own response, so a rename there fails that test instead of silently
|
|
466
|
+
* degrading every bridge. A missing field lists the keys the body DID carry, so a rename is
|
|
467
|
+
* named in the first message anyone reads.
|
|
468
|
+
*/
|
|
453
469
|
function badInventoryReason(body) {
|
|
454
|
-
if (
|
|
470
|
+
if (!isRecord(body))
|
|
455
471
|
return "the body is not a JSON object";
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
472
|
+
if (!("boards" in body))
|
|
473
|
+
return `boards is missing (response keys: ${Object.keys(body).join(", ")})`;
|
|
474
|
+
if (!Array.isArray(body.boards) || body.boards.some((x) => typeof x !== "string")) {
|
|
475
|
+
return "boards is not an array of strings";
|
|
476
|
+
}
|
|
477
|
+
if (!("card_count" in body))
|
|
478
|
+
return `card_count is missing (response keys: ${Object.keys(body).join(", ")})`;
|
|
479
|
+
const count = body.card_count;
|
|
480
|
+
if (typeof count !== "number" || !Number.isSafeInteger(count) || count < 0) {
|
|
481
|
+
return `card_count is not a non-negative integer (got ${typeof count === "number" ? count : typeof count})`;
|
|
461
482
|
}
|
|
462
483
|
return null;
|
|
463
484
|
}
|
|
@@ -485,7 +506,7 @@ export async function fetchPlanInventory(options, deps) {
|
|
|
485
506
|
return { kind: "unavailable", detail: `plan inventory read HTTP ${status}: ${bad}` };
|
|
486
507
|
}
|
|
487
508
|
const b = body;
|
|
488
|
-
return { kind: "ok", boards: b.boards, cardCount: b.
|
|
509
|
+
return { kind: "ok", boards: b.boards, cardCount: b.card_count };
|
|
489
510
|
}
|
|
490
511
|
/**
|
|
491
512
|
* DX-2920 — a stream admission refusal (`listen.ts`'s STRUCTURED `refused`
|
|
@@ -604,7 +625,8 @@ export async function runBridge(options, deps) {
|
|
|
604
625
|
let connectionState = "reconnecting";
|
|
605
626
|
let stoppedReason = null;
|
|
606
627
|
let heartbeatSuppressed = false;
|
|
607
|
-
|
|
628
|
+
// DX-3912 — `null` is "the inventory read succeeded"; a string is why it did not.
|
|
629
|
+
let lastInventoryError = null;
|
|
608
630
|
// DX-3099 — consecutive heartbeat POSTs that failed to LAND while this
|
|
609
631
|
// process still believed `connectionState === "streaming"`. Reset to 0 the
|
|
610
632
|
// moment a heartbeat lands, or the moment state leaves "streaming" for any
|
|
@@ -619,7 +641,9 @@ export async function runBridge(options, deps) {
|
|
|
619
641
|
// process, so an in-process re-mint never touches its state.
|
|
620
642
|
const sessionStats = new OperatorSessionStats(options.transcriptPath ?? null, deps.makeStatsWatcher);
|
|
621
643
|
const connectionNow = () => connectionState === "streaming"
|
|
622
|
-
?
|
|
644
|
+
? lastInventoryError === null
|
|
645
|
+
? { state: "streaming", inventory: "ok" }
|
|
646
|
+
: { state: "streaming", inventory: "unavailable", reason: oneLine(lastInventoryError, HEARTBEAT_REASON_MAX_CODE_POINTS) }
|
|
623
647
|
: connectionState === "stopped"
|
|
624
648
|
? { state: "stopped", reason: stoppedReason ?? undefined }
|
|
625
649
|
: { state: "reconnecting" };
|
|
@@ -822,23 +846,24 @@ export async function runBridge(options, deps) {
|
|
|
822
846
|
readyEmitted = true;
|
|
823
847
|
const inventory = await fetchPlanInventory(options, deps);
|
|
824
848
|
// DX-3028 (AC1, code-review + architecture-review finding) — this is
|
|
825
|
-
// the ONE place `
|
|
849
|
+
// the ONE place `lastInventoryError` is ever written; the 15s heartbeat
|
|
826
850
|
// reads it on every "streaming" tick (see `heartbeatTick` above). Must
|
|
827
851
|
// agree with the SAME `ready` record's own `degraded` decision below —
|
|
828
852
|
// a heartbeat reporting `inventory:"ok"` while the `ready` record just
|
|
829
853
|
// told the operator `degraded:true` would be exactly the untruthful
|
|
830
854
|
// state AC1 exists to prevent.
|
|
831
|
-
|
|
855
|
+
lastInventoryError = inventory.kind === "ok" ? null : inventory.detail;
|
|
832
856
|
if (inventory.kind === "ok") {
|
|
833
857
|
deps.write({
|
|
834
858
|
type: "ready",
|
|
835
859
|
boards: inventory.boards,
|
|
836
860
|
cardCount: inventory.cardCount,
|
|
837
861
|
degraded: inventory.boards.length === 0 || inventory.cardCount === 0,
|
|
862
|
+
inventoryError: null,
|
|
838
863
|
});
|
|
839
864
|
}
|
|
840
865
|
else {
|
|
841
|
-
deps.write({ type: "ready", boards: null, cardCount: null, degraded: true });
|
|
866
|
+
deps.write({ type: "ready", boards: null, cardCount: null, degraded: true, inventoryError: inventory.detail });
|
|
842
867
|
}
|
|
843
868
|
}
|
|
844
869
|
backoff = MINT_INITIAL_BACKOFF_MS;
|
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.190",
|
|
4
4
|
"description": "Stdio MCP server fronting danxbot's dashboard API for agents (operator sessions and dispatched workers alike): the generic danxbot_api/danxbot_api_list/danxbot_api_spec tools reach any route the caller's credential may call, plus dedicated agent-specific tools (plan_connect, danxbot_complete, and other per-dispatch lifecycle callbacks) (DX-3465, PLN-11 R-25).",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|