@thehammer/danx-dashboard-mcp 0.1.136 → 0.1.139
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/background-work.js +44 -59
- package/dist/one-shot-session-request.js +104 -0
- package/dist/plan-state.js +35 -55
- package/package.json +1 -1
package/dist/background-work.js
CHANGED
|
@@ -4,13 +4,19 @@
|
|
|
4
4
|
*
|
|
5
5
|
* WHO RUNS IT. The danxbot Claude Code plugin's `Stop` / `SubagentStop` /
|
|
6
6
|
* `SessionStart` / `StopFailure` hooks (`danxbot/hooks/hooks.json` in
|
|
7
|
-
* claude-plugins), via the SAME `npx -y @thehammer/danx-dashboard-mcp`
|
|
8
|
-
* the plugin already uses for `bridge` and `plan-state` — no
|
|
9
|
-
* install, no deep import of this package's `dist/` internals.
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
7
|
+
* claude-plugins), via the SAME `npx -y @thehammer/danx-dashboard-mcp`
|
|
8
|
+
* MECHANISM the plugin already uses for `bridge` and `plan-state` — no
|
|
9
|
+
* separate install, no deep import of this package's `dist/` internals.
|
|
10
|
+
* DX-3367 fix-up — that does NOT mean the SAME PINNED VERSION: `npx -y
|
|
11
|
+
* <pkg>` resolves whatever version string that ONE hook's own invocation
|
|
12
|
+
* names, and this plugin's hooks pin each call independently (verified
|
|
13
|
+
* against production drift, comment 7102 on the card: the background-work
|
|
14
|
+
* hooks pinned `0.1.136` while `bridge` was still pinned `0.1.95`) — a
|
|
15
|
+
* version bump to one hook's pin never moves the others. The plugin decides
|
|
16
|
+
* WHAT to report (parsing `Stop`/`SubagentStop`'s own `background_tasks`
|
|
17
|
+
* snapshot, counting only `shell` / `subagent` / `workflow` entries,
|
|
18
|
+
* sending `clear` on `SessionStart`/`StopFailure`); this module only
|
|
19
|
+
* delivers the report.
|
|
14
20
|
*
|
|
15
21
|
* THE CONTRACT, mirroring `plan-state.ts`'s (binding the same way, so a
|
|
16
22
|
* future plugin build against this module can trust it without re-reading
|
|
@@ -23,8 +29,10 @@
|
|
|
23
29
|
* no-op — `{"ok":false,"reason":"no_connection_record"}` — the plugin
|
|
24
30
|
* hook simply does not forward anything further in this case, exactly
|
|
25
31
|
* what "no-op when the session isn't plan-connected" means in practice;
|
|
26
|
-
* - on success, `{"ok":true,"count":<number|null>}` echoes
|
|
27
|
-
*
|
|
32
|
+
* - on success, `{"ok":true,"count":<number|null>}` echoes the `count`
|
|
33
|
+
* that was SUBMITTED for storage, not a re-read of the row — see
|
|
34
|
+
* `recordBackgroundWork`'s own doc (`db/plan-session-background-work.ts`)
|
|
35
|
+
* for why a plain no-op write is indistinguishable from a real one here;
|
|
28
36
|
* - human-readable diagnostics go to stderr only, never stdout.
|
|
29
37
|
*
|
|
30
38
|
* THE CREDENTIAL — the SAME resolver `bridge` and `plan-state` use, via
|
|
@@ -37,8 +45,13 @@
|
|
|
37
45
|
* A HARD TIMEOUT — `BACKGROUND_WORK_REQUEST_TIMEOUT_MS`. A `Stop` hook
|
|
38
46
|
* blocks the session's turn boundary, so this call must never hang: a slow
|
|
39
47
|
* or wedged dashboard is `{ok:false, reason:"timeout"}`, not a stuck hook.
|
|
48
|
+
*
|
|
49
|
+
* THE REQUEST/RUN SCAFFOLDING IS SHARED WITH `plan-state.ts`
|
|
50
|
+
* (`one-shot-session-request.ts`, DX-3367 fix-up) — the two subcommands no
|
|
51
|
+
* longer maintain independent copies of the authenticated-fetch-with-
|
|
52
|
+
* timeout logic or the session/credential resolution boilerplate.
|
|
40
53
|
*/
|
|
41
|
-
import {
|
|
54
|
+
import { resolveSessionOptions, sendSessionRequest, tryParseJsonBody } from "./one-shot-session-request.js";
|
|
42
55
|
export const BACKGROUND_WORK_SUBCOMMAND = "background-work";
|
|
43
56
|
export const BACKGROUND_WORK_PATH = "/api/plan-sessions/me/background-work";
|
|
44
57
|
/** How long this one-shot call may take before it counts as a timeout. */
|
|
@@ -59,48 +72,30 @@ export function parseCountArg(raw) {
|
|
|
59
72
|
* One authenticated PUT, with a hard timeout, classified into the output
|
|
60
73
|
* shape this subcommand always prints — never a throw, never a non-2xx
|
|
61
74
|
* bubbling past this function.
|
|
75
|
+
*
|
|
76
|
+
* DX-3367 fix-up — STATUS IS CHECKED BEFORE THE BODY IS EVER PARSED. A 5xx
|
|
77
|
+
* (or any other non-2xx) response can carry an HTML error page instead of
|
|
78
|
+
* JSON; parsing first would misreport that as `bad_response` when the true
|
|
79
|
+
* classification is `http_error`. Unlike `plan-state.ts`'s `fetchTurnState`,
|
|
80
|
+
* this subcommand has no status that needs the BODY inspected to classify
|
|
81
|
+
* (no `409 session_not_connected` equivalent), so status alone decides
|
|
82
|
+
* whether a body read is even attempted.
|
|
62
83
|
*/
|
|
63
84
|
export async function putBackgroundWork(options, count, deps) {
|
|
64
|
-
const
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
try {
|
|
69
|
-
try {
|
|
70
|
-
const response = await deps.fetch(`${options.dashboardUrl}${BACKGROUND_WORK_PATH}`, {
|
|
71
|
-
method: "PUT",
|
|
72
|
-
headers: {
|
|
73
|
-
Authorization: `Bearer ${options.token}`,
|
|
74
|
-
Accept: "application/json",
|
|
75
|
-
"Content-Type": "application/json",
|
|
76
|
-
[SESSION_ID_HEADER]: options.sessionId,
|
|
77
|
-
},
|
|
78
|
-
body: JSON.stringify({ count }),
|
|
79
|
-
signal: controller.signal,
|
|
80
|
-
});
|
|
81
|
-
status = response.status;
|
|
82
|
-
text = await response.text();
|
|
83
|
-
}
|
|
84
|
-
catch (err) {
|
|
85
|
-
return { ok: false, reason: controller.signal.aborted ? "timeout" : "request_failed" };
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
finally {
|
|
89
|
-
clearTimeout(timer);
|
|
90
|
-
}
|
|
91
|
-
let body = null;
|
|
92
|
-
try {
|
|
93
|
-
body = text === "" ? null : JSON.parse(text);
|
|
94
|
-
}
|
|
95
|
-
catch {
|
|
96
|
-
return { ok: false, reason: "bad_response" };
|
|
97
|
-
}
|
|
85
|
+
const sent = await sendSessionRequest(options, BACKGROUND_WORK_PATH, { method: "PUT", body: JSON.stringify({ count }) }, deps);
|
|
86
|
+
if ("reason" in sent)
|
|
87
|
+
return sent;
|
|
88
|
+
const { status, text } = sent;
|
|
98
89
|
if (status === 401 || status === 403) {
|
|
99
90
|
return { ok: false, reason: "unauthorized" };
|
|
100
91
|
}
|
|
101
92
|
if (status < 200 || status >= 300) {
|
|
102
93
|
return { ok: false, reason: "http_error" };
|
|
103
94
|
}
|
|
95
|
+
const parsed = tryParseJsonBody(text);
|
|
96
|
+
if (!parsed.ok)
|
|
97
|
+
return { ok: false, reason: "bad_response" };
|
|
98
|
+
const body = parsed.body;
|
|
104
99
|
if (typeof body !== "object" ||
|
|
105
100
|
body === null ||
|
|
106
101
|
!("count" in body) ||
|
|
@@ -128,26 +123,16 @@ resolveFrom = {}) {
|
|
|
128
123
|
return 0;
|
|
129
124
|
}
|
|
130
125
|
const count = parsed;
|
|
131
|
-
const
|
|
132
|
-
if (!
|
|
133
|
-
write({ ok: false, reason: "no_session_id" });
|
|
134
|
-
process.stderr.write(`${USAGE}\n`);
|
|
135
|
-
return 0;
|
|
136
|
-
}
|
|
137
|
-
let options;
|
|
138
|
-
try {
|
|
139
|
-
options = resolveBridgeOptions({ sessionId, resumeIds: [] }, env, resolveFrom);
|
|
140
|
-
}
|
|
141
|
-
catch (err) {
|
|
142
|
-
const start = err;
|
|
126
|
+
const resolved = await resolveSessionOptions(env, resolveFrom, USAGE);
|
|
127
|
+
if (!resolved.ok) {
|
|
143
128
|
// A session that never `plan_connect`-ed has no connection record — this
|
|
144
129
|
// is the ORDINARY "not plan-connected" case, not an error the plugin
|
|
145
130
|
// should surface: it simply forwards nothing further this turn.
|
|
146
|
-
write(
|
|
147
|
-
process.stderr.write(`${
|
|
131
|
+
write(resolved.output);
|
|
132
|
+
process.stderr.write(`${resolved.detail}\n`);
|
|
148
133
|
return 0;
|
|
149
134
|
}
|
|
150
|
-
const output = await putBackgroundWork(options, count, {
|
|
135
|
+
const output = await putBackgroundWork(resolved.options, count, {
|
|
151
136
|
fetch: (input, init) => fetch(input, init),
|
|
152
137
|
requestTimeoutMs: BACKGROUND_WORK_REQUEST_TIMEOUT_MS,
|
|
153
138
|
});
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DX-3367 fix-up — shared scaffolding for a "one-shot session request"
|
|
3
|
+
* subcommand: `plan-state` (`GET /api/plan-sessions/me/turn-state`) and
|
|
4
|
+
* `background-work` (`PUT /api/plan-sessions/me/background-work`) each
|
|
5
|
+
* duplicated this exact shape independently (`fetchTurnState` /
|
|
6
|
+
* `putBackgroundWork`, `runPlanStateCommand` / `runBackgroundWorkCommand`).
|
|
7
|
+
* This module is the ONE place that shape lives now; a future one-shot
|
|
8
|
+
* subcommand extends it instead of copying either file again.
|
|
9
|
+
*
|
|
10
|
+
* Two pieces, matching the two duplicated concerns:
|
|
11
|
+
* - `sendSessionRequest` — the authenticated HTTP call itself: bearer +
|
|
12
|
+
* session header, a hard abort-timeout, and network-level failure
|
|
13
|
+
* classification (`timeout` / `request_failed`). Returns the raw
|
|
14
|
+
* `{status, text}` UNPARSED — each caller decides for itself which
|
|
15
|
+
* statuses need the body inspected (e.g. `plan-state`'s 409
|
|
16
|
+
* `session_not_connected`) and which don't, so a non-2xx status with an
|
|
17
|
+
* unparseable (e.g. HTML) body is classified from the status alone
|
|
18
|
+
* rather than colliding with "the JSON didn't parse" (`bad_response`).
|
|
19
|
+
* - `resolveSessionOptions` — session-id + credential resolution via
|
|
20
|
+
* `resolveBridgeOptions`, with `BridgeStartError` narrowed by
|
|
21
|
+
* `instanceof` rather than an unchecked `as BridgeStartError` cast, so
|
|
22
|
+
* an error of some OTHER shape (a bug in the resolver, a thrown
|
|
23
|
+
* non-Error) still comes back as a reported reason instead of a value
|
|
24
|
+
* whose `.reason`/`.message`/`.fix` fields are silently `undefined`.
|
|
25
|
+
*/
|
|
26
|
+
import { SESSION_ID_HEADER, resolveBridgeOptions, BridgeStartError } from "./bridge.js";
|
|
27
|
+
/**
|
|
28
|
+
* One authenticated HTTP call to this session's dashboard, with a hard
|
|
29
|
+
* timeout. Never throws — a network failure or an abort is reported as
|
|
30
|
+
* `OneShotFailure`, exactly like any other classified failure the caller
|
|
31
|
+
* might produce from the status/body it gets back on success.
|
|
32
|
+
*/
|
|
33
|
+
export async function sendSessionRequest(options, path, init, deps) {
|
|
34
|
+
const controller = new AbortController();
|
|
35
|
+
const timer = setTimeout(() => controller.abort(), deps.requestTimeoutMs);
|
|
36
|
+
try {
|
|
37
|
+
try {
|
|
38
|
+
const response = await deps.fetch(`${options.dashboardUrl}${path}`, {
|
|
39
|
+
method: init.method,
|
|
40
|
+
headers: {
|
|
41
|
+
Authorization: `Bearer ${options.token}`,
|
|
42
|
+
Accept: "application/json",
|
|
43
|
+
...(init.body !== undefined ? { "Content-Type": "application/json" } : {}),
|
|
44
|
+
[SESSION_ID_HEADER]: options.sessionId,
|
|
45
|
+
},
|
|
46
|
+
body: init.body,
|
|
47
|
+
signal: controller.signal,
|
|
48
|
+
});
|
|
49
|
+
const text = await response.text();
|
|
50
|
+
return { status: response.status, text };
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
// DX-3367 fix-up — the caught value is never inspected (an aborted
|
|
54
|
+
// fetch's own rejection shape varies by runtime), so binding it to a
|
|
55
|
+
// name the code never reads was dead weight the linter never caught.
|
|
56
|
+
return { ok: false, reason: controller.signal.aborted ? "timeout" : "request_failed" };
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
finally {
|
|
60
|
+
clearTimeout(timer);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
/** `JSON.parse`, classified rather than thrown — the ONE shared parse point
|
|
64
|
+
* every caller uses once it has decided (from `status`) that the body is
|
|
65
|
+
* worth reading at all. */
|
|
66
|
+
export function tryParseJsonBody(text) {
|
|
67
|
+
try {
|
|
68
|
+
return { ok: true, body: text === "" ? null : JSON.parse(text) };
|
|
69
|
+
}
|
|
70
|
+
catch {
|
|
71
|
+
return { ok: false };
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Resolve this process's session id + dashboard credential, the identical
|
|
76
|
+
* first step every one-shot subcommand's `runXCommand` takes before it can
|
|
77
|
+
* make its own request. `detail` is the stderr-only human-readable line the
|
|
78
|
+
* caller prints alongside `output` — never part of the stdout JSON contract.
|
|
79
|
+
*/
|
|
80
|
+
export async function resolveSessionOptions(env, resolveFrom, usage) {
|
|
81
|
+
const sessionId = env.CLAUDE_CODE_SESSION_ID;
|
|
82
|
+
if (!sessionId) {
|
|
83
|
+
return { ok: false, output: { ok: false, reason: "no_session_id" }, detail: usage };
|
|
84
|
+
}
|
|
85
|
+
try {
|
|
86
|
+
const options = resolveBridgeOptions({ sessionId, resumeIds: [] }, env, resolveFrom);
|
|
87
|
+
return { ok: true, options };
|
|
88
|
+
}
|
|
89
|
+
catch (err) {
|
|
90
|
+
// DX-3367 fix-up — `instanceof`, never an unchecked `as BridgeStartError`
|
|
91
|
+
// cast: `resolveBridgeOptions` throws exactly `BridgeStartError` today,
|
|
92
|
+
// but a cast reports a fabricated reason/message/fix (all `undefined`)
|
|
93
|
+
// for any OTHER thrown shape instead of a real one, silently.
|
|
94
|
+
if (err instanceof BridgeStartError) {
|
|
95
|
+
return {
|
|
96
|
+
ok: false,
|
|
97
|
+
output: { ok: false, reason: err.reason },
|
|
98
|
+
detail: `${err.reason}: ${err.message}. Fix: ${err.fix}.`,
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
102
|
+
return { ok: false, output: { ok: false, reason: "start_failed" }, detail: `start_failed: ${message}` };
|
|
103
|
+
}
|
|
104
|
+
}
|
package/dist/plan-state.js
CHANGED
|
@@ -44,8 +44,11 @@
|
|
|
44
44
|
* blocks a turn boundary the plugin is holding open, so it must never hang:
|
|
45
45
|
* a slow or wedged dashboard is `{ok:false, reason:"timeout"}`, not a stuck
|
|
46
46
|
* hook.
|
|
47
|
+
*
|
|
48
|
+
* THE REQUEST/RUN SCAFFOLDING IS SHARED WITH `background-work.ts`
|
|
49
|
+
* (`one-shot-session-request.ts`, DX-3367 fix-up).
|
|
47
50
|
*/
|
|
48
|
-
import {
|
|
51
|
+
import { resolveSessionOptions, sendSessionRequest, tryParseJsonBody } from "./one-shot-session-request.js";
|
|
49
52
|
export const PLAN_STATE_SUBCOMMAND = "plan-state";
|
|
50
53
|
export const TURN_STATE_PATH = "/api/plan-sessions/me/turn-state";
|
|
51
54
|
/** How long this one-shot call may take before it counts as a timeout. */
|
|
@@ -102,45 +105,29 @@ export function parseTurnStateBody(body) {
|
|
|
102
105
|
* One authenticated GET, with a hard timeout, classified into the output
|
|
103
106
|
* shape this subcommand always prints — never a throw, never a non-2xx
|
|
104
107
|
* bubbling past this function.
|
|
108
|
+
*
|
|
109
|
+
* DX-3367 fix-up — status is checked BEFORE the body is parsed, except for
|
|
110
|
+
* the ONE status (`409`) whose classification genuinely depends on the body
|
|
111
|
+
* (`session_not_connected`). Any other non-2xx — including a 5xx that comes
|
|
112
|
+
* back as an HTML error page instead of JSON — is `http_error` without ever
|
|
113
|
+
* attempting to parse it, so a body-parse failure on those statuses can
|
|
114
|
+
* never be misreported as `bad_response`.
|
|
105
115
|
*/
|
|
106
116
|
export async function fetchTurnState(options, deps) {
|
|
107
|
-
const
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
signal: controller.signal,
|
|
121
|
-
});
|
|
122
|
-
status = response.status;
|
|
123
|
-
text = await response.text();
|
|
117
|
+
const sent = await sendSessionRequest(options, TURN_STATE_PATH, { method: "GET" }, deps);
|
|
118
|
+
if ("reason" in sent)
|
|
119
|
+
return sent;
|
|
120
|
+
const { status, text } = sent;
|
|
121
|
+
if (status === 409) {
|
|
122
|
+
// The exact literal the plugin's no-log-spam rule keys on (AC 30672 /
|
|
123
|
+
// comment 4245) — the dashboard's own 409 error code for an unconnected
|
|
124
|
+
// session, passed through verbatim rather than renamed. A 409 whose body
|
|
125
|
+
// does NOT carry that code falls through to the ordinary `http_error`
|
|
126
|
+
// classification below, same as any other non-2xx.
|
|
127
|
+
const parsed409 = tryParseJsonBody(text);
|
|
128
|
+
if (parsed409.ok && errorCodeOf(parsed409.body) === "session_not_connected") {
|
|
129
|
+
return { ok: false, reason: "session_not_connected" };
|
|
124
130
|
}
|
|
125
|
-
catch (err) {
|
|
126
|
-
return { ok: false, reason: controller.signal.aborted ? "timeout" : "request_failed" };
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
finally {
|
|
130
|
-
clearTimeout(timer);
|
|
131
|
-
}
|
|
132
|
-
let body = null;
|
|
133
|
-
try {
|
|
134
|
-
body = text === "" ? null : JSON.parse(text);
|
|
135
|
-
}
|
|
136
|
-
catch {
|
|
137
|
-
return { ok: false, reason: "bad_response" };
|
|
138
|
-
}
|
|
139
|
-
// The exact literal the plugin's no-log-spam rule keys on (AC 30672 /
|
|
140
|
-
// comment 4245) — the dashboard's own 409 error code for an unconnected
|
|
141
|
-
// session, passed through verbatim rather than renamed.
|
|
142
|
-
if (status === 409 && errorCodeOf(body) === "session_not_connected") {
|
|
143
|
-
return { ok: false, reason: "session_not_connected" };
|
|
144
131
|
}
|
|
145
132
|
if (status === 401 || status === 403) {
|
|
146
133
|
return { ok: false, reason: "unauthorized" };
|
|
@@ -148,11 +135,14 @@ export async function fetchTurnState(options, deps) {
|
|
|
148
135
|
if (status < 200 || status >= 300) {
|
|
149
136
|
return { ok: false, reason: "http_error" };
|
|
150
137
|
}
|
|
151
|
-
const parsed =
|
|
152
|
-
if (parsed
|
|
138
|
+
const parsed = tryParseJsonBody(text);
|
|
139
|
+
if (!parsed.ok)
|
|
140
|
+
return { ok: false, reason: "bad_response" };
|
|
141
|
+
const turnState = parseTurnStateBody(parsed.body);
|
|
142
|
+
if (turnState === null) {
|
|
153
143
|
return { ok: false, reason: "bad_response" };
|
|
154
144
|
}
|
|
155
|
-
return { ok: true, ...
|
|
145
|
+
return { ok: true, ...turnState };
|
|
156
146
|
}
|
|
157
147
|
/** The bin's `plan-state` subcommand, wired to the real process. Always resolves to exit code 0. */
|
|
158
148
|
export async function runPlanStateCommand(argv, env = process.env,
|
|
@@ -169,23 +159,13 @@ resolveFrom = {}) {
|
|
|
169
159
|
process.stderr.write(`${USAGE}\n`);
|
|
170
160
|
return 0;
|
|
171
161
|
}
|
|
172
|
-
const
|
|
173
|
-
if (!
|
|
174
|
-
write(
|
|
175
|
-
process.stderr.write(`${
|
|
176
|
-
return 0;
|
|
177
|
-
}
|
|
178
|
-
let options;
|
|
179
|
-
try {
|
|
180
|
-
options = resolveBridgeOptions({ sessionId, resumeIds: [] }, env, resolveFrom);
|
|
181
|
-
}
|
|
182
|
-
catch (err) {
|
|
183
|
-
const start = err;
|
|
184
|
-
write({ ok: false, reason: start.reason });
|
|
185
|
-
process.stderr.write(`${start.reason}: ${start.message}. Fix: ${start.fix}.\n`);
|
|
162
|
+
const resolved = await resolveSessionOptions(env, resolveFrom, USAGE);
|
|
163
|
+
if (!resolved.ok) {
|
|
164
|
+
write(resolved.output);
|
|
165
|
+
process.stderr.write(`${resolved.detail}\n`);
|
|
186
166
|
return 0;
|
|
187
167
|
}
|
|
188
|
-
const output = await fetchTurnState(options, {
|
|
168
|
+
const output = await fetchTurnState(resolved.options, {
|
|
189
169
|
fetch: (input, init) => fetch(input, init),
|
|
190
170
|
requestTimeoutMs: PLAN_STATE_REQUEST_TIMEOUT_MS,
|
|
191
171
|
});
|
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.139",
|
|
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",
|