@thehammer/danx-dashboard-mcp 0.1.139 → 0.1.140

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/index.js CHANGED
@@ -97,6 +97,7 @@ import { isEntrypointModule } from "./entrypoint.js";
97
97
  import { BRIDGE_SUBCOMMAND, runBridgeCommand } from "./bridge.js";
98
98
  import { PLAN_STATE_SUBCOMMAND, runPlanStateCommand } from "./plan-state.js";
99
99
  import { BACKGROUND_WORK_SUBCOMMAND, runBackgroundWorkCommand } from "./background-work.js";
100
+ import { MANTRA_SUBCOMMAND, runMantraCommand } from "./mantra.js";
100
101
  import { resolveDeclaredCredential } from "./credential.js";
101
102
  import { recordSessionConnectionAfterConnect } from "./session-connection.js";
102
103
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
@@ -1188,9 +1189,20 @@ if (isEntrypointModule(import.meta.url, process.argv[1])) {
1188
1189
  process.exit(0);
1189
1190
  });
1190
1191
  }
1192
+ else if (subcommand === MANTRA_SUBCOMMAND) {
1193
+ // DX-3366 — unlike plan-state/background-work, a rejection here is a
1194
+ // genuine fatal (never expected: runMantraCommand itself already
1195
+ // classifies every network/shape failure into its own {ok:false} exit
1196
+ // 1), so it exits 1 too rather than the "always 0, stay silent" contract
1197
+ // those two hook-only subcommands use.
1198
+ runMantraCommand(rest).then((code) => process.exit(code), (err) => {
1199
+ console.error(`[danx-dashboard-mcp] mantra fatal: ${err.message}`);
1200
+ process.exit(1);
1201
+ });
1202
+ }
1191
1203
  else if (subcommand !== undefined) {
1192
1204
  console.error(`[danx-dashboard-mcp] unknown subcommand "${subcommand}" (the only ones are ` +
1193
- `"${BRIDGE_SUBCOMMAND}", "${PLAN_STATE_SUBCOMMAND}", and "${BACKGROUND_WORK_SUBCOMMAND}")`);
1205
+ `"${BRIDGE_SUBCOMMAND}", "${PLAN_STATE_SUBCOMMAND}", "${BACKGROUND_WORK_SUBCOMMAND}", and "${MANTRA_SUBCOMMAND}")`);
1194
1206
  process.exit(2);
1195
1207
  }
1196
1208
  else {
package/dist/mantra.js ADDED
@@ -0,0 +1,128 @@
1
+ /**
2
+ * `danx-dashboard-mcp mantra` — the ONE-SHOT client of
3
+ * `GET /api/reminders/mantra.session_start` (DX-3366).
4
+ *
5
+ * WHO RUNS IT. The claude-plugins `danxbot` plugin's `mantra.sh` SessionStart
6
+ * hook, via the SAME `npx -y @thehammer/danx-dashboard-mcp@<pin>` path the
7
+ * plugin's other dashboard calls already use (`plan-state`, `bridge`) — never
8
+ * a separate install, never a deep import of this package's `dist/`
9
+ * internals.
10
+ *
11
+ * THE CREDENTIAL — the SAME resolver `bridge` / `plan-state` use
12
+ * (`resolveBridgeOptions`, `bridge.ts`): the dashboard URL, credential source
13
+ * and credential all come from the connection record this session's OWN
14
+ * danx-dashboard MCP server wrote on its last successful `plan_connect`
15
+ * (`session-connection.ts`), never from this process's own ambient env. The
16
+ * mantra is only ever fetched once a session is already connected (`mantra.sh`
17
+ * checks that first via `plan-connection.mjs` and stays with the short nudge
18
+ * otherwise), so a connection record always exists by the time this runs.
19
+ *
20
+ * THE CONTRACT — unlike `plan-state` (which must stay silent for a hook, so it
21
+ * always exits 0), this subcommand's caller (`mantra.sh`) needs to tell
22
+ * success from failure so it can fall back to the git-committed `mantra.md` +
23
+ * a one-line notice (DX-3366 AC: "Dashboard unreachable → the hook prints the
24
+ * committed file and says so in one line; never silently"). So:
25
+ *
26
+ * - success: the reminder's EFFECTIVE text (`override_text ?? default_text`)
27
+ * on stdout, nothing else, exit 0.
28
+ * - failure (missing env, no connection record, network error, timeout,
29
+ * non-2xx, 404, bad shape): NOTHING on stdout, one short reason on
30
+ * stderr, exit 1. `mantra.sh` is the one that decides what to print for
31
+ * a human — this module only reports whether the fetch worked.
32
+ *
33
+ * A HARD TIMEOUT (mirrors `plan-state.ts`) — a SessionStart hook must never
34
+ * hang on a wedged dashboard.
35
+ */
36
+ import { SESSION_ID_HEADER, resolveBridgeOptions } from "./bridge.js";
37
+ export const MANTRA_SUBCOMMAND = "mantra";
38
+ export const MANTRA_REMINDER_KEY = "mantra.session_start";
39
+ export const MANTRA_REQUEST_TIMEOUT_MS = 5_000;
40
+ const USAGE = `usage: CLAUDE_CODE_SESSION_ID=<session-id> danx-dashboard-mcp ${MANTRA_SUBCOMMAND}`;
41
+ function reminderPath(key) {
42
+ return `/api/reminders/${encodeURIComponent(key)}`;
43
+ }
44
+ /** One authenticated GET, with a hard timeout — never throws, never bubbles a non-2xx. */
45
+ export async function fetchMantraText(options, deps) {
46
+ const controller = new AbortController();
47
+ const timer = setTimeout(() => controller.abort(), deps.requestTimeoutMs);
48
+ let status;
49
+ let text;
50
+ try {
51
+ try {
52
+ const response = await deps.fetch(`${options.dashboardUrl}${reminderPath(MANTRA_REMINDER_KEY)}`, {
53
+ method: "GET",
54
+ headers: {
55
+ Authorization: `Bearer ${options.token}`,
56
+ Accept: "application/json",
57
+ [SESSION_ID_HEADER]: options.sessionId,
58
+ },
59
+ signal: controller.signal,
60
+ });
61
+ status = response.status;
62
+ text = await response.text();
63
+ }
64
+ catch {
65
+ return { ok: false, reason: controller.signal.aborted ? "timeout" : "request_failed" };
66
+ }
67
+ }
68
+ finally {
69
+ clearTimeout(timer);
70
+ }
71
+ if (status === 401 || status === 403)
72
+ return { ok: false, reason: "unauthorized" };
73
+ if (status === 404)
74
+ return { ok: false, reason: "not_found" };
75
+ if (status < 200 || status >= 300)
76
+ return { ok: false, reason: "http_error" };
77
+ let body;
78
+ try {
79
+ body = text === "" ? null : JSON.parse(text);
80
+ }
81
+ catch {
82
+ return { ok: false, reason: "bad_response" };
83
+ }
84
+ const effectiveText = typeof body === "object" && body !== null && typeof body.effective_text === "string"
85
+ ? body.effective_text
86
+ : null;
87
+ if (effectiveText === null)
88
+ return { ok: false, reason: "bad_response" };
89
+ return { ok: true, text: effectiveText };
90
+ }
91
+ /**
92
+ * The bin's `mantra` subcommand, wired to the real process. Success prints
93
+ * the effective text on stdout and returns 0; every failure prints nothing on
94
+ * stdout, one reason on stderr, and returns 1 — `mantra.sh` reads the exit
95
+ * code, never parses stdout to guess.
96
+ */
97
+ export async function runMantraCommand(argv, env = process.env,
98
+ /** Test seam: the home the session's connection record is read from. */
99
+ resolveFrom = {}) {
100
+ if (argv.length !== 0) {
101
+ process.stderr.write(`unrecognized arguments\n${USAGE}\n`);
102
+ return 1;
103
+ }
104
+ const sessionId = env.CLAUDE_CODE_SESSION_ID;
105
+ if (!sessionId) {
106
+ process.stderr.write(`no_session_id: CLAUDE_CODE_SESSION_ID is not set\n${USAGE}\n`);
107
+ return 1;
108
+ }
109
+ let options;
110
+ try {
111
+ options = resolveBridgeOptions({ sessionId, resumeIds: [] }, env, resolveFrom);
112
+ }
113
+ catch (err) {
114
+ const start = err;
115
+ process.stderr.write(`${start.reason}: ${start.message}. Fix: ${start.fix}.\n`);
116
+ return 1;
117
+ }
118
+ const output = await fetchMantraText(options, {
119
+ fetch: (input, init) => fetch(input, init),
120
+ requestTimeoutMs: MANTRA_REQUEST_TIMEOUT_MS,
121
+ });
122
+ if (!output.ok) {
123
+ process.stderr.write(`${output.reason}: could not fetch the effective mantra text from the dashboard\n`);
124
+ return 1;
125
+ }
126
+ process.stdout.write(output.text);
127
+ return 0;
128
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@thehammer/danx-dashboard-mcp",
3
- "version": "0.1.139",
3
+ "version": "0.1.140",
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",