@thehammer/danx-dashboard-mcp 0.1.77 → 0.1.79

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
@@ -45,7 +45,7 @@ Every plan carries a `status`, computed fresh on every read and never stored —
45
45
  | Status | Meaning |
46
46
  |---|---|
47
47
  | `complete` | At least one card, and every card Done or Cancelled. Wins even with no session — a finished plan needs nobody. |
48
- | `awaiting-session` | Not complete, and no session is LIVE on the plan. A `plan_sessions` row is never released when a session merely ends (only when the plan itself is deleted), so this checks real liveness — an unrevoked/unexpired listener ticket, OR `last_active_at` within the same lease window — never just "has a session ever connected". |
48
+ | `awaiting-session` | Not complete, and no session is LIVE on the plan. A `plan_sessions` row is never released when a session merely ends (only when the plan itself is deleted), so this checks real liveness — `last_active_at` within the last minute — never just "has a session ever connected". A working session's event stream stamps that activity every 15 seconds on its own, so an open, idle session stays live with no tool calls. |
49
49
  | `building` | Not complete, a live session is connected, and at least one card is ToDo/In Progress, or carries an unnamed active state (Blocked, Needs Help — i.e. an OPEN problem, DX-2830 — and, once it exists, Verify): work started and is either moving or stuck. |
50
50
  | `planning` | Everything else: no cards, or only Review/Backlog/terminal cards with at least one not Done/Cancelled. |
51
51
 
@@ -63,7 +63,7 @@ CLAUDE_CODE_SESSION_ID=<session> npx -y @thehammer/danx-dashboard-mcp@<version>
63
63
  - **Reach check.** After minting, `bridge` proves the credential can read every board the connected plan's cards live on (`GET /api/plans/mine?fields=cards`, then `GET /api/issues/:id/problems` for one card per board — the routes that run the same board-allowlist + `read` check the stream itself applies). A board it cannot read stops it with `board_unreadable` rather than streaming on and dropping that board's events. Then, once, `{"type":"ready","boards":[…]}`.
64
64
  - **Ticket.** `bridge` mints the session's listener ticket (`POST /api/plan-sessions/me/stream-ticket`, 10 s timeout) and keeps it in the process. A ticket authorizes reading that one session's event stream and nothing else, and is only issued while the session is connected to a plan. Minting a new one ends the previous listener.
65
65
  - **Output — JSON Lines.** One `{"type":"event","id":<id|null>,"text":"…"}` per event on the connected plan's cards, where `text` is `[DX-8 "Title" repo:board] newms87 answered "Which rollout order?": chose "Pause E2E" — note: "only this week"`, `… answered "…": "<free-form answer>"`, `… commented: "…"`, `… opened a problem: "…"`, `… blocked the card: "…"`, or `… unblocked the card`. An event it cannot read still produces one, with a `could not read event` text. Nothing for keep-alives, reconnects or re-mints. The session's own writes are never echoed back to it.
66
- - **Stopping.** Last, one `{"type":"stopped","reason":"…","detail":"…"}` and exit, only on a terminal outcome: `not_connected`, `unauthorized` (401/403), `mint_refused` (any other non-transient refusal), `mint_bad_response`, `superseded` / `replaced` (exit 0), `revoked`, or `refused` (two freshly minted tickets refused in a row). A transient mint failure (network, timeout, 408, 429, 5xx) backs off and retries; a lapsed ticket lease re-mints.
66
+ - **Stopping.** Last, one `{"type":"stopped","reason":"…","detail":"…","fix":"…"}` and exit, only on a terminal outcome. `fix` is the remedy shown to the session (`STOP_FIXES` in `src/bridge.ts` — never a log-only hint), so a session that hits a stop it cannot otherwise see still knows what to do about it. Terminal reasons: `no_connection_record` / `credential_unavailable` / `credential_mismatch` (the start-time checks above), `not_connected`, `unauthorized` (401/403), `mint_refused` (any other non-transient refusal), `mint_bad_response`, `board_unreadable` / `scope_check_failed` (the reach check above), `superseded` / `replaced` (exit 0 — a newer or replacing connection already serves this session, so `fix` says no action is needed), `revoked`, or `refused` (two freshly minted tickets refused in a row). A transient mint failure (network, timeout, 408, 429, 5xx) backs off and retries; a lapsed ticket lease re-mints.
67
67
  - **Reconnect and resume.** A read-idle timeout (three missed keep-alives) turns a silently dead connection into a drop. Capped exponential backoff (1s → 30s) that resets only after a healthy connection, with `Last-Event-ID`, so a dashboard restart replays what was missed and nothing is emitted twice. `--resume-ids` carries the same guarantee across a process restart: the ids already delivered seed the duplicate guard, and the highest is the first `Last-Event-ID`. The dashboard floors that replay at the later of the session's first ticket and when it joined its current plan, so a restart loses nothing and a plan move replays nothing from before the move.
68
68
 
69
69
  ## Build + test
package/dist/bridge.js CHANGED
@@ -76,9 +76,14 @@ const TAKEOVER_REASONS = new Set(["superseded", "replaced"]);
76
76
  const USAGE = `usage: CLAUDE_CODE_SESSION_ID=<session-id> danx-dashboard-mcp ${BRIDGE_SUBCOMMAND} ` +
77
77
  `[--resume-ids <event-id>[,<event-id>...]]`;
78
78
  /**
79
- * The remedy per terminal reason. ONE map, so the message a session is shown and
80
- * the message a log carries can never drift, and a new reason without a remedy
81
- * is a compile-visible omission rather than a silent blank.
79
+ * The remedy per terminal reason THIS module knows about. ONE map, so the
80
+ * message a session is shown and the message a log carries can never drift.
81
+ * `satisfies` makes a reason missing its remedy a compile error here — but
82
+ * `reason` can also arrive at runtime from the dashboard's own SSE end event
83
+ * (`ListenStopped.reason` is an open string, not this union), so a build that
84
+ * predates a newer dashboard reason still needs an honest, generic fallback
85
+ * rather than an index miss: that is `DEFAULT_STOP_FIX`, reached only for a
86
+ * reason genuinely outside this list, never for one of the reasons above.
82
87
  */
83
88
  export const STOP_FIXES = {
84
89
  no_connection_record: "call plan_connect again in this session — its danx-dashboard MCP server records the connection the bridge needs " +
@@ -91,6 +96,11 @@ export const STOP_FIXES = {
91
96
  mint_bad_response: "check that DANXBOT_DASHBOARD_URL for this session's MCP server points at a danxbot dashboard",
92
97
  board_unreadable: "scope this session's dashboard credential to read that board, or connect a plan whose cards all live on boards it can read",
93
98
  scope_check_failed: "check the dashboard is reachable and this session is connected to a plan, then call plan_connect again",
99
+ // A newer ticket exists (superseded) or another connection is already using
100
+ // this one (replaced) — THIS process is ending because the session is
101
+ // already served elsewhere, not because anything is broken.
102
+ superseded: "no action needed — a newer ticket was minted for this session, and a fresh bridge is already using it",
103
+ replaced: "no action needed — another connection is already using this session's ticket; call plan_connect again only if that was unexpected",
94
104
  revoked: "call plan_connect again to mint a new listener ticket for this session",
95
105
  refused: "call plan_connect again to mint a new listener ticket for this session",
96
106
  };
@@ -149,7 +159,9 @@ export function resolveBridgeOptions(args, env, options = {}) {
149
159
  }
150
160
  catch (err) {
151
161
  const credentialError = err;
152
- throw new BridgeStartError("credential_unavailable", `the credential this session's danx-dashboard MCP server declared cannot be resolved here: ${credentialError.message}`, credentialError.fix ?? fixForStop("credential_unavailable"));
162
+ throw new BridgeStartError("credential_unavailable", `the credential this session's danx-dashboard MCP server declared cannot be resolved here: ${credentialError.message}`,
163
+ // CredentialError.fix is a required string, never absent — no fallback needed.
164
+ credentialError.fix);
153
165
  }
154
166
  const fingerprint = credentialFingerprint(token);
155
167
  if (fingerprint !== record.fingerprint) {
@@ -333,6 +345,9 @@ export async function verifyPlanBoardsReadable(options, deps) {
333
345
  break;
334
346
  }
335
347
  for (const [boardId, cardId] of firstCardPerBoard) {
348
+ // `?board=` is not read by this route's auth check (it derives the board from
349
+ // `cardId`'s own `board_id` — `resolveIssueBoardId`) — kept only so the URL and
350
+ // the `board read of ${boardId}` label above it name the same board at a glance.
336
351
  const url = `${options.dashboardUrl}${ISSUES_PATH}/${encodeURIComponent(cardId)}/problems?board=${encodeURIComponent(boardId)}`;
337
352
  const outcome = await request(options, deps, `board read of ${boardId}`, url);
338
353
  if (outcome.kind === "transient")
package/dist/index.js CHANGED
@@ -882,10 +882,15 @@ async function main() {
882
882
  // spawns, not just a direct `node dist/index.js`.
883
883
  //
884
884
  // `bridge` is the one subcommand: the session event stream client the danxbot
885
- // plugin's plan event bridge runs. It never boots the MCP server; its whole
886
- // configuration is its environment (dashboard URL, credential, session id) plus
887
- // `--resume-ids`. Any OTHER argument is refused rather than ignored, so a typo
888
- // cannot silently start an MCP server on stdio where the bridge was meant to run.
885
+ // plugin's plan event bridge runs. It never boots the MCP server; its only
886
+ // input is `CLAUDE_CODE_SESSION_ID` (read from its environment) plus
887
+ // `--resume-ids` — the dashboard URL, credential source, and credential itself
888
+ // come from the session-connection record this server's own `plan_connect`
889
+ // wrote (`session-connection.ts`), never from the bridge's own environment
890
+ // (DX-2862: a bridge trusting its own ambient `DANXBOT_DISPATCH_TOKEN` is the
891
+ // exact bug this repo fixed). Any OTHER argument is refused rather than
892
+ // ignored, so a typo cannot silently start an MCP server on stdio where the
893
+ // bridge was meant to run.
889
894
  if (isEntrypointModule(import.meta.url, process.argv[1])) {
890
895
  const [subcommand, ...rest] = process.argv.slice(2);
891
896
  if (subcommand === BRIDGE_SUBCOMMAND) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@thehammer/danx-dashboard-mcp",
3
- "version": "0.1.77",
3
+ "version": "0.1.79",
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",