@thehammer/danx-dashboard-mcp 0.1.138 → 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/index.js +1 -13
- package/dist/one-shot-session-request.js +104 -0
- package/dist/plan-state.js +35 -55
- package/package.json +1 -1
- package/dist/mantra.js +0 -128
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
|
});
|
package/dist/index.js
CHANGED
|
@@ -97,7 +97,6 @@ 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";
|
|
101
100
|
import { resolveDeclaredCredential } from "./credential.js";
|
|
102
101
|
import { recordSessionConnectionAfterConnect } from "./session-connection.js";
|
|
103
102
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
@@ -1189,20 +1188,9 @@ if (isEntrypointModule(import.meta.url, process.argv[1])) {
|
|
|
1189
1188
|
process.exit(0);
|
|
1190
1189
|
});
|
|
1191
1190
|
}
|
|
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
|
-
}
|
|
1203
1191
|
else if (subcommand !== undefined) {
|
|
1204
1192
|
console.error(`[danx-dashboard-mcp] unknown subcommand "${subcommand}" (the only ones are ` +
|
|
1205
|
-
`"${BRIDGE_SUBCOMMAND}", "${PLAN_STATE_SUBCOMMAND}",
|
|
1193
|
+
`"${BRIDGE_SUBCOMMAND}", "${PLAN_STATE_SUBCOMMAND}", and "${BACKGROUND_WORK_SUBCOMMAND}")`);
|
|
1206
1194
|
process.exit(2);
|
|
1207
1195
|
}
|
|
1208
1196
|
else {
|
|
@@ -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",
|
package/dist/mantra.js
DELETED
|
@@ -1,128 +0,0 @@
|
|
|
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
|
-
}
|