@thehammer/danx-dashboard-mcp 0.1.85 → 0.1.86

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.
Files changed (2) hide show
  1. package/dist/listen.js +74 -1
  2. package/package.json +1 -1
package/dist/listen.js CHANGED
@@ -11,7 +11,10 @@
11
11
  * - `{type:"event", id, text}` — exactly one per event, the moment it arrives.
12
12
  * `text` is what the session reads; an event it cannot read still produces one
13
13
  * (`could not read event …`), never silence. `id` is `null` only for a frame
14
- * that carried no usable id.
14
+ * that carried no usable id — which includes DX-2885's `nudge` frame
15
+ * (`formatNudgeLine`): it is advisory, re-derived fresh every tick by the
16
+ * dashboard's own keep-alive tick, and never replayed, so it rides this
17
+ * same shape with `id: null` rather than growing a THIRD output variant.
15
18
  * - `{type:"stopped", reason, detail, refusal?, boards?}` — once, last. `reason`
16
19
  * is the dashboard's own end reason (`superseded`, `replaced`, `revoked`,
17
20
  * `scope_narrowed`, `not_connected`), `refused` (the ticket was not
@@ -219,6 +222,46 @@ export function formatActivityLine(event) {
219
222
  throw new Error(`malformed activity event: ${invalid}`);
220
223
  return oneLine(`[${event.cardId} "${event.cardTitle}" ${event.boardId}] ${describe(event)}`);
221
224
  }
225
+ function isNudgeCardRef(value) {
226
+ return isRecord(value) && typeof value.id === "string" && typeof value.title === "string";
227
+ }
228
+ function isNudgeCardRefArray(value) {
229
+ return Array.isArray(value) && value.every(isNudgeCardRef);
230
+ }
231
+ /** Why a nudge cannot be read, or `null` when it can — same checked-before-describing shape as `invalidEventReason`. */
232
+ export function invalidNudgeReason(value) {
233
+ if (!isRecord(value))
234
+ return "the nudge is not an object";
235
+ if (typeof value.idleMinutes !== "number" || !Number.isFinite(value.idleMinutes)) {
236
+ return "idleMinutes is not a number";
237
+ }
238
+ if (!isNudgeCardRefArray(value.startable))
239
+ return "startable is not a {id,title}[] array";
240
+ if (!isNudgeCardRefArray(value.held))
241
+ return "held is not a {id,title}[] array";
242
+ return null;
243
+ }
244
+ function cardList(cards) {
245
+ return cards.map((c) => `${c.id} "${c.title}"`).join(", ");
246
+ }
247
+ /**
248
+ * The one readable line for a nudge (DX-2885). Always names the actual
249
+ * waiting cards — never a generic "keep going" — because that is the whole
250
+ * point of the feature: a session that only sees "you're idle" learns
251
+ * nothing a bare timer couldn't have told it. Always a single line; throws,
252
+ * naming the fault, on a malformed nudge.
253
+ */
254
+ export function formatNudgeLine(event) {
255
+ const invalid = invalidNudgeReason(event);
256
+ if (invalid !== null)
257
+ throw new Error(`malformed nudge event: ${invalid}`);
258
+ const parts = [];
259
+ if (event.startable.length > 0)
260
+ parts.push(`startable: ${cardList(event.startable)}`);
261
+ if (event.held.length > 0)
262
+ parts.push(`held: ${cardList(event.held)}`);
263
+ return oneLine(`${LINE_PREFIX} this session has been idle ${event.idleMinutes}m with work waiting — ${parts.join("; ")}`);
264
+ }
222
265
  /**
223
266
  * Incremental SSE parser. Comment lines (keep-alives, the connect marker) and
224
267
  * field-less blocks produce no message, which is what keeps them out of the output.
@@ -347,6 +390,36 @@ export async function runListener(options, deps) {
347
390
  // elsewhere — see `ListenStopReason`'s docblock.
348
391
  return { kind: "ended", reason: isKnownWireEndReason(reason) ? reason : "unknown" };
349
392
  }
393
+ if (message.event === "nudge") {
394
+ // DX-2885 — a nudge carries no usable id (it is advisory, re-derived
395
+ // fresh every tick, never replayed — see `plan-session-stream.ts`'s
396
+ // module doc), so it rides the SAME `{type:"event", id:null, text}`
397
+ // shape an ordinary activity line uses (`id` is `null` only for a frame
398
+ // that carried no usable id — a nudge is exactly that case). That is
399
+ // what lets `bridge.ts` and the plugin's relay
400
+ // (`danxbot/scripts/plan-event-bridge.mjs`, outside this repo) forward
401
+ // it with NO changes at all: both already forward any `{type:"event",
402
+ // id, text}` line generically, keyed only on `type`.
403
+ //
404
+ // DX-2885 — this frame must stay exempt from DX-2730 D4's digest-
405
+ // batching when that lands (see DX-2730 AC 29939, and the matching note
406
+ // at the dashboard's own emit site): it must keep delivering
407
+ // immediately, never held for a later digest flush.
408
+ let parsed;
409
+ let invalid;
410
+ try {
411
+ parsed = JSON.parse(message.data);
412
+ invalid = invalidNudgeReason(parsed);
413
+ }
414
+ catch (err) {
415
+ invalid = `not JSON: ${err.message}`;
416
+ }
417
+ const text = invalid === null
418
+ ? formatNudgeLine(parsed)
419
+ : `${LINE_PREFIX} could not read nudge (${invalid}): ${oneLine(message.data, 300)}`;
420
+ deps.write({ type: "event", id: null, text });
421
+ return null;
422
+ }
350
423
  if (message.event !== "activity")
351
424
  return null;
352
425
  // `Number(null)` is 0, so a frame with no id must never be read as event 0: that id
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@thehammer/danx-dashboard-mcp",
3
- "version": "0.1.85",
3
+ "version": "0.1.86",
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",