@thehammer/danx-dashboard-mcp 0.1.176 → 0.1.179
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 +18 -19
- package/dist/event-text.js +5 -6
- package/dist/http-client.js +40 -1
- package/dist/index.js +14 -31
- package/dist/lifecycle.js +36 -12
- package/dist/one-shot-session-request.js +12 -11
- package/package.json +1 -1
- package/dist/plan-state.js +0 -174
package/dist/background-work.js
CHANGED
|
@@ -5,8 +5,8 @@
|
|
|
5
5
|
* WHO RUNS IT. The danxbot Claude Code plugin's `Stop` / `SubagentStop` /
|
|
6
6
|
* `SessionStart` / `StopFailure` hooks (`danxbot/hooks/hooks.json` in
|
|
7
7
|
* claude-plugins), via the SAME `npx -y @thehammer/danx-dashboard-mcp`
|
|
8
|
-
* MECHANISM the plugin already uses for `bridge`
|
|
9
|
-
*
|
|
8
|
+
* MECHANISM the plugin already uses for `bridge` — no separate install, no
|
|
9
|
+
* deep import of this package's `dist/` internals.
|
|
10
10
|
* DX-3367 fix-up — that does NOT mean the SAME PINNED VERSION: `npx -y
|
|
11
11
|
* <pkg>` resolves whatever version string that ONE hook's own invocation
|
|
12
12
|
* names, and this plugin's hooks pin each call independently (verified
|
|
@@ -18,9 +18,8 @@
|
|
|
18
18
|
* sending `clear` on `SessionStart`/`StopFailure`); this module only
|
|
19
19
|
* delivers the report.
|
|
20
20
|
*
|
|
21
|
-
* THE CONTRACT,
|
|
22
|
-
*
|
|
23
|
-
* this file):
|
|
21
|
+
* THE CONTRACT (binding, so a future plugin build against this module can
|
|
22
|
+
* trust it without re-reading this file):
|
|
24
23
|
* - print EXACTLY ONE JSON line on stdout, and ALWAYS exit 0 — success or
|
|
25
24
|
* failure alike, because a hook must stay silent rather than spam the
|
|
26
25
|
* session's own turn with a stack trace;
|
|
@@ -40,21 +39,21 @@
|
|
|
40
39
|
* reports by this time;
|
|
41
40
|
* - human-readable diagnostics go to stderr only, never stdout.
|
|
42
41
|
*
|
|
43
|
-
* THE CREDENTIAL — the SAME resolver `bridge`
|
|
44
|
-
*
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
* the identical DX-2862 reasoning `plan-state.ts` already gives for itself.
|
|
42
|
+
* THE CREDENTIAL — the SAME resolver `bridge` uses, via `resolveBridgeOptions`
|
|
43
|
+
* (`bridge.ts`): the dashboard URL, credential source, and credential come
|
|
44
|
+
* from the connection record this session's OWN danx-dashboard MCP server
|
|
45
|
+
* wrote on its last successful `plan_connect` (`session-connection.ts`),
|
|
46
|
+
* never from this process's own ambient env (DX-2862).
|
|
49
47
|
*
|
|
50
48
|
* A HARD TIMEOUT — `BACKGROUND_WORK_REQUEST_TIMEOUT_MS`. A `Stop` hook
|
|
51
49
|
* blocks the session's turn boundary, so this call must never hang: a slow
|
|
52
50
|
* or wedged dashboard is `{ok:false, reason:"timeout"}`, not a stuck hook.
|
|
53
51
|
*
|
|
54
|
-
* THE REQUEST/RUN SCAFFOLDING
|
|
55
|
-
*
|
|
56
|
-
*
|
|
57
|
-
* timeout logic or the session/credential
|
|
52
|
+
* THE REQUEST/RUN SCAFFOLDING (`one-shot-session-request.ts`, DX-3367
|
|
53
|
+
* fix-up) is shared, general-purpose plumbing for any one-shot subcommand
|
|
54
|
+
* shaped like this one — not maintained as an independent copy of the
|
|
55
|
+
* authenticated-fetch-with-timeout logic or the session/credential
|
|
56
|
+
* resolution boilerplate.
|
|
58
57
|
*/
|
|
59
58
|
import { resolveSessionOptions, sendSessionRequest, tryParseJsonBody } from "./one-shot-session-request.js";
|
|
60
59
|
export const BACKGROUND_WORK_SUBCOMMAND = "background-work";
|
|
@@ -90,10 +89,10 @@ export function parseEventAtArg(raw) {
|
|
|
90
89
|
* DX-3367 fix-up — STATUS IS CHECKED BEFORE THE BODY IS EVER PARSED. A 5xx
|
|
91
90
|
* (or any other non-2xx) response can carry an HTML error page instead of
|
|
92
91
|
* JSON; parsing first would misreport that as `bad_response` when the true
|
|
93
|
-
* classification is `http_error`.
|
|
94
|
-
*
|
|
95
|
-
*
|
|
96
|
-
*
|
|
92
|
+
* classification is `http_error`. This subcommand has no status that needs
|
|
93
|
+
* the BODY inspected to classify (no `409 session_not_connected`
|
|
94
|
+
* equivalent), so status alone decides whether a body read is even
|
|
95
|
+
* attempted.
|
|
97
96
|
*/
|
|
98
97
|
export async function putBackgroundWork(options, report, deps) {
|
|
99
98
|
const sent = await sendSessionRequest(options, BACKGROUND_WORK_PATH, { method: "PUT", body: JSON.stringify({ count: report.count, eventAt: report.eventAt }) }, deps);
|
package/dist/event-text.js
CHANGED
|
@@ -10,11 +10,10 @@
|
|
|
10
10
|
* collapse into the one hook + this one subcommand). `<event>` is one of
|
|
11
11
|
* `session_start` | `session_resume` | `after_compaction` |
|
|
12
12
|
* `sub_agent_start` — the SAME `npx -y @thehammer/danx-dashboard-mcp@<pin>`
|
|
13
|
-
* path the plugin's other dashboard calls already use (`
|
|
14
|
-
*
|
|
15
|
-
* package's `dist/` internals.
|
|
13
|
+
* path the plugin's other dashboard calls already use (`bridge`) — never a
|
|
14
|
+
* separate install, never a deep import of this package's `dist/` internals.
|
|
16
15
|
*
|
|
17
|
-
* THE CREDENTIAL — the SAME resolver `bridge`
|
|
16
|
+
* THE CREDENTIAL — the SAME resolver `bridge` uses
|
|
18
17
|
* (`resolveBridgeOptions`, `bridge.ts`): the dashboard URL, credential source
|
|
19
18
|
* and credential all come from the connection record this session's OWN
|
|
20
19
|
* danx-dashboard MCP server wrote on its last successful `plan_connect`
|
|
@@ -37,8 +36,8 @@
|
|
|
37
36
|
* stderr, exit 1. `event-hook.sh` is the one that decides what to print
|
|
38
37
|
* for the agent — this module only reports whether the fetch worked.
|
|
39
38
|
*
|
|
40
|
-
* A HARD TIMEOUT
|
|
41
|
-
*
|
|
39
|
+
* A HARD TIMEOUT — a SessionStart hook must never hang on a wedged
|
|
40
|
+
* dashboard.
|
|
42
41
|
*/
|
|
43
42
|
import { SESSION_ID_HEADER, resolveBridgeOptions } from "./bridge.js";
|
|
44
43
|
export const EVENT_TEXT_SUBCOMMAND = "event-text";
|
package/dist/http-client.js
CHANGED
|
@@ -1,4 +1,38 @@
|
|
|
1
1
|
import { assertVersionFloorSatisfied, MCP_MIN_VERSION_HEADER } from "./version-floor.js";
|
|
2
|
+
/**
|
|
3
|
+
* DX-3750 — route-path prefixes (`<basePath><path>`, BEFORE any query
|
|
4
|
+
* string) that are NEVER board-scoped: the dashboard route serving them
|
|
5
|
+
* takes no `?board=` at all — either because the resource is GLOBAL
|
|
6
|
+
* (plans — see `plan_list`'s own doc: "Plans are GLOBAL, not board-scoped")
|
|
7
|
+
* or because it scopes some other way a `board` param would be meaningless
|
|
8
|
+
* for (dispatches scope by `repo`; a board resolves from a PATH segment on
|
|
9
|
+
* every route but its own list, which itself scopes by `repo`; `/api/routes`
|
|
10
|
+
* and `/api/openapi` are cross-board discovery). Server-side, each of these
|
|
11
|
+
* families' LIST route validates its query params with its own
|
|
12
|
+
* `rejectUnknownParams(params, TOP_LEVEL_..._PARAM_KEYS)` allowlist
|
|
13
|
+
* (`src/issues/plans-routes.ts`, `src/dashboard/dispatches-routes.ts`,
|
|
14
|
+
* `src/issues/boards-routes.ts`) — none of those allowlists names `board`
|
|
15
|
+
* (DX-3725), so stamping it 400s. THIS is the one list; `isBoardScopedRoute`
|
|
16
|
+
* is the one function that reads it — no second, independently-drifting
|
|
17
|
+
* copy anywhere else in this package.
|
|
18
|
+
*
|
|
19
|
+
* Deliberately prefix-matched, not exact-matched: every sub-route of a
|
|
20
|
+
* non-board-scoped family (`/api/plans/:id/cards`, `/api/boards/:id/export`,
|
|
21
|
+
* `/api/dispatches/:id/logs`, …) is equally never board-scoped — a plan, a
|
|
22
|
+
* dispatch and a board-export all resolve their target from a PATH segment
|
|
23
|
+
* or a `repo`/global scope, never from `?board=`.
|
|
24
|
+
*/
|
|
25
|
+
const NON_BOARD_SCOPED_PATH_PREFIXES = [
|
|
26
|
+
"/api/plans",
|
|
27
|
+
"/api/dispatches",
|
|
28
|
+
"/api/boards",
|
|
29
|
+
"/api/routes",
|
|
30
|
+
"/api/openapi",
|
|
31
|
+
];
|
|
32
|
+
/** DX-3750 — does `routePath` (`<basePath><path>`, no query string) take a `?board=` at all? See `NON_BOARD_SCOPED_PATH_PREFIXES`'s own doc. */
|
|
33
|
+
export function isBoardScopedRoute(routePath) {
|
|
34
|
+
return !NON_BOARD_SCOPED_PATH_PREFIXES.some((prefix) => routePath === prefix || routePath.startsWith(`${prefix}/`));
|
|
35
|
+
}
|
|
2
36
|
export class DashboardHttpClient {
|
|
3
37
|
config;
|
|
4
38
|
fetchImpl;
|
|
@@ -97,7 +131,12 @@ export class DashboardHttpClient {
|
|
|
97
131
|
cleanPath = rawPath.startsWith("/") ? rawPath : `/${rawPath}`;
|
|
98
132
|
}
|
|
99
133
|
const params = new URLSearchParams(existingQs ?? "");
|
|
100
|
-
|
|
134
|
+
// DX-3750 — only a board-scoped route takes `?board=` at all; see
|
|
135
|
+
// `isBoardScopedRoute`'s own doc for why an unconditional stamp 400s
|
|
136
|
+
// plans/dispatches/boards-list through `danxbot_api`.
|
|
137
|
+
if (isBoardScopedRoute(`${basePath}${cleanPath}`)) {
|
|
138
|
+
params.set("board", boardOverride ?? this.config.board);
|
|
139
|
+
}
|
|
101
140
|
if (extraQuery) {
|
|
102
141
|
for (const [k, v] of Object.entries(extraQuery)) {
|
|
103
142
|
if (v === undefined || v === null)
|
package/dist/index.js
CHANGED
|
@@ -74,7 +74,6 @@
|
|
|
74
74
|
*/
|
|
75
75
|
import { isEntrypointModule } from "./entrypoint.js";
|
|
76
76
|
import { BRIDGE_SUBCOMMAND, runBridgeCommand } from "./bridge.js";
|
|
77
|
-
import { PLAN_STATE_SUBCOMMAND, runPlanStateCommand } from "./plan-state.js";
|
|
78
77
|
import { BACKGROUND_WORK_SUBCOMMAND, runBackgroundWorkCommand } from "./background-work.js";
|
|
79
78
|
import { EVENT_TEXT_SUBCOMMAND, runEventTextCommand } from "./event-text.js";
|
|
80
79
|
import { resolveDeclaredCredential } from "./credential.js";
|
|
@@ -460,22 +459,14 @@ async function main() {
|
|
|
460
459
|
// symlink-aware (DX-1647) so it holds under the symlinked `npx` bin the worker
|
|
461
460
|
// spawns, not just a direct `node dist/index.js`.
|
|
462
461
|
//
|
|
463
|
-
// `bridge`
|
|
464
|
-
// event
|
|
465
|
-
//
|
|
466
|
-
//
|
|
467
|
-
//
|
|
468
|
-
//
|
|
469
|
-
//
|
|
470
|
-
//
|
|
471
|
-
//
|
|
472
|
-
// `plan-state` (DX-2921) is the ONE-SHOT client of
|
|
473
|
-
// `GET /api/plan-sessions/me/turn-state`, run by the DX-2888 plugin's
|
|
474
|
-
// turn-start / turn-end hooks — see `plan-state.ts`'s own doc comment for the
|
|
475
|
-
// full contract. It ALWAYS exits 0 (success or failure alike: the hook must
|
|
476
|
-
// stay silent), so even an unexpected rejection here is caught and reported
|
|
477
|
-
// the same `{ok:false, reason}` way rather than propagating a non-zero exit
|
|
478
|
-
// or an uncaught stack trace into the hook.
|
|
462
|
+
// `bridge` is the session event stream client the danxbot plugin's plan
|
|
463
|
+
// event bridge runs. It never boots the MCP server; its only input is
|
|
464
|
+
// `CLAUDE_CODE_SESSION_ID` (read from its environment) plus `--resume-ids` —
|
|
465
|
+
// the dashboard URL, credential source, and credential itself come from the
|
|
466
|
+
// session-connection record this server's own `plan_connect` wrote
|
|
467
|
+
// (`session-connection.ts`), never from the bridge's own environment
|
|
468
|
+
// (DX-2862: a bridge trusting its own ambient `DANXBOT_DISPATCH_TOKEN` is the
|
|
469
|
+
// exact bug this repo fixed).
|
|
479
470
|
//
|
|
480
471
|
// Any OTHER argument is refused rather than ignored, so a typo cannot
|
|
481
472
|
// silently start an MCP server on stdio where a subcommand was meant to run.
|
|
@@ -487,13 +478,6 @@ if (isEntrypointModule(import.meta.url, process.argv[1])) {
|
|
|
487
478
|
process.exit(1);
|
|
488
479
|
});
|
|
489
480
|
}
|
|
490
|
-
else if (subcommand === PLAN_STATE_SUBCOMMAND) {
|
|
491
|
-
runPlanStateCommand(rest).then((code) => process.exit(code), (err) => {
|
|
492
|
-
console.error(`[danx-dashboard-mcp] plan-state fatal: ${err.message}`);
|
|
493
|
-
process.stdout.write(`${JSON.stringify({ ok: false, reason: "fatal" })}\n`);
|
|
494
|
-
process.exit(0);
|
|
495
|
-
});
|
|
496
|
-
}
|
|
497
481
|
else if (subcommand === BACKGROUND_WORK_SUBCOMMAND) {
|
|
498
482
|
runBackgroundWorkCommand(rest).then((code) => process.exit(code), (err) => {
|
|
499
483
|
console.error(`[danx-dashboard-mcp] background-work fatal: ${err.message}`);
|
|
@@ -502,12 +486,11 @@ if (isEntrypointModule(import.meta.url, process.argv[1])) {
|
|
|
502
486
|
});
|
|
503
487
|
}
|
|
504
488
|
else if (subcommand === EVENT_TEXT_SUBCOMMAND) {
|
|
505
|
-
// DX-3421 (was DX-3366's `mantra` subcommand) — unlike
|
|
506
|
-
//
|
|
507
|
-
//
|
|
508
|
-
//
|
|
509
|
-
//
|
|
510
|
-
// use.
|
|
489
|
+
// DX-3421 (was DX-3366's `mantra` subcommand) — unlike background-work, a
|
|
490
|
+
// rejection here is a genuine fatal (never expected: runEventTextCommand
|
|
491
|
+
// itself already classifies every network/shape failure into its own
|
|
492
|
+
// {ok:false} exit 1), so it exits 1 too rather than the "always 0, stay
|
|
493
|
+
// silent" contract background-work's hook-only subcommand uses.
|
|
511
494
|
runEventTextCommand(rest).then((code) => process.exit(code), (err) => {
|
|
512
495
|
console.error(`[danx-dashboard-mcp] event-text fatal: ${err.message}`);
|
|
513
496
|
process.exit(1);
|
|
@@ -515,7 +498,7 @@ if (isEntrypointModule(import.meta.url, process.argv[1])) {
|
|
|
515
498
|
}
|
|
516
499
|
else if (subcommand !== undefined) {
|
|
517
500
|
console.error(`[danx-dashboard-mcp] unknown subcommand "${subcommand}" (the only ones are ` +
|
|
518
|
-
`"${BRIDGE_SUBCOMMAND}", "${
|
|
501
|
+
`"${BRIDGE_SUBCOMMAND}", "${BACKGROUND_WORK_SUBCOMMAND}", and "${EVENT_TEXT_SUBCOMMAND}")`);
|
|
519
502
|
process.exit(2);
|
|
520
503
|
}
|
|
521
504
|
else {
|
package/dist/lifecycle.js
CHANGED
|
@@ -77,6 +77,7 @@ export function readLifecycleUrlsFromEnv() {
|
|
|
77
77
|
slackRecoveryMessage: opt("DANXBOT_SLACK_RECOVERY_MESSAGE_URL"),
|
|
78
78
|
slackRecoveryCreateOrLink: opt("DANXBOT_SLACK_RECOVERY_CREATE_OR_LINK_URL"),
|
|
79
79
|
qualityGate: opt("DANXBOT_QUALITY_GATE_URL"),
|
|
80
|
+
requestCodeReview: opt("DANXBOT_REQUEST_CODE_REVIEW_URL"),
|
|
80
81
|
evaluatorSummary: opt("DANXBOT_EVALUATOR_SUMMARY_URL"),
|
|
81
82
|
saveTemplateApp: opt("DANXBOT_TEMPLATE_APP_SAVE_URL"),
|
|
82
83
|
loadTemplateApp: opt("DANXBOT_TEMPLATE_APP_LOAD_URL"),
|
|
@@ -323,18 +324,24 @@ export function registerLifecycleTools(strictTool) {
|
|
|
323
324
|
return { content: [{ type: "text", text: JSON.stringify(result) }] };
|
|
324
325
|
});
|
|
325
326
|
// ---------------- quality_gate_complete (absorbs quality_gate_skip, DX-3394 problem 1682) ----------------
|
|
326
|
-
const qualityGateComplete = strictTool("quality_gate_complete", "Sign off a registered quality gate.
|
|
327
|
-
"
|
|
328
|
-
"
|
|
329
|
-
"
|
|
330
|
-
"status='
|
|
331
|
-
"
|
|
332
|
-
"
|
|
333
|
-
"
|
|
334
|
-
"
|
|
335
|
-
|
|
336
|
-
"
|
|
337
|
-
"
|
|
327
|
+
const qualityGateComplete = strictTool("quality_gate_complete", "Sign off a registered quality gate. NEVER visible to (or callable by) the WORK dispatch that authored " +
|
|
328
|
+
"the diff — DX-3741: every gate, PLAN or CODE, runs as its OWN independent gate-reviewer dispatch, so " +
|
|
329
|
+
"seeing this tool at all means you ARE that reviewer, not the author. Two flavors by the gate's PHASE: " +
|
|
330
|
+
"(PLAN, e.g. plan-architecture) — reviews the CARD's plan before any code exists; this call is your " +
|
|
331
|
+
"dispatch's TERMINAL call: status='pass' releases the dispatch (the picker launches the next gate or " +
|
|
332
|
+
"the work agent); status='fail' BLOCKS the card (your message becomes the reason) and stops the " +
|
|
333
|
+
"dispatch (do NOT follow up with danxbot_complete); status='retry' — the fixer loop: you edited the " +
|
|
334
|
+
"card into a sounder plan and want a fresh pass. (CODE, e.g. code-quality) — reviews `git diff " +
|
|
335
|
+
"origin/main...HEAD` on the author's `card/<id>` branch, already pushed to origin; this call is ALSO " +
|
|
336
|
+
"your dispatch's TERMINAL call: report every finding via `findings[]`; status='fail' releases the " +
|
|
337
|
+
"dispatch (does NOT block the card) — the author's WORK session, re-dispatched, reads your findings, " +
|
|
338
|
+
"fixes them, and calls request_code_review again, which re-dispatches every requested code gate fresh; " +
|
|
339
|
+
"status='pass' releases the dispatch toward the next requested code gate or back to WORK to finalize; " +
|
|
340
|
+
"status='retry' is PLAN-phase only, refused here. status='skipped' — the gate is OBVIOUSLY " +
|
|
341
|
+
"UNNECESSARY for this diff (a single-line / docs-only change, or nothing in this gate's lane): use " +
|
|
342
|
+
`\`message\` as the mandatory justification (≥${QUALITY_GATE_SKIP_REASON_MIN_CHARS} chars, the ` +
|
|
343
|
+
"permanent accountability record a human will read) — never skip to dodge a real review. You never " +
|
|
344
|
+
"edit files and never transition the card yourself, on either phase.", {
|
|
338
345
|
gate: z.string().describe("Registered gate name, e.g. 'plan-architecture' or 'code-quality'."),
|
|
339
346
|
status: z.enum(["pass", "fail", "retry", "skipped"]),
|
|
340
347
|
message: z
|
|
@@ -367,6 +374,21 @@ export function registerLifecycleTools(strictTool) {
|
|
|
367
374
|
const result = await postWorkerRoute(url, body, "quality_gate_complete", agentBearer());
|
|
368
375
|
return { content: [{ type: "text", text: JSON.stringify(result) }] };
|
|
369
376
|
});
|
|
377
|
+
// ---------------- request_code_review (DX-3741) ----------------
|
|
378
|
+
const requestCodeReview = strictTool("request_code_review", "TERMINAL for this dispatch — call it when your diff is done and its own tests are green, instead of " +
|
|
379
|
+
"danxbot_complete. Pushes your branch (card/<id> to origin/card/<id>), then dispatches every code " +
|
|
380
|
+
"gate this card carries as its OWN independent gate-reviewer dispatch (you never sign or review your " +
|
|
381
|
+
"own diff — DX-3741). Ends this dispatch 'completed' and releases the card to ToDo; when the card is " +
|
|
382
|
+
"re-dispatched to you, read its quality_gates (through danxbot_api): every code gate pass/skipped → " +
|
|
383
|
+
"run agent-finalize.sh and complete normally; any fail → fix its findings, then call " +
|
|
384
|
+
"request_code_review again (never sign or remove a code gate yourself — removal is refused, 409). " +
|
|
385
|
+
"Takes no arguments. A card with no code gates has nothing to request — finalize directly instead.", {}, async () => {
|
|
386
|
+
const url = readLifecycleUrlsFromEnv().requestCodeReview;
|
|
387
|
+
if (!url)
|
|
388
|
+
throw new Error("request_code_review called outside a work dispatch");
|
|
389
|
+
const result = await postWorkerRoute(url, {}, "request_code_review", agentBearer());
|
|
390
|
+
return { content: [{ type: "text", text: JSON.stringify(result) }] };
|
|
391
|
+
});
|
|
370
392
|
// ---------------- danxbot_set_evaluator_summary ----------------
|
|
371
393
|
const evaluatorSummary = strictTool("danxbot_set_evaluator_summary", "Write the root-cause summary for a 3-strike broken agent. ONLY the system-evaluator dispatch sees this " +
|
|
372
394
|
"tool. Call exactly once at the END of the dispatch, immediately before danxbot_complete. A dispatch " +
|
|
@@ -424,6 +446,7 @@ export function registerLifecycleTools(strictTool) {
|
|
|
424
446
|
slackRecoveryMessage,
|
|
425
447
|
slackRecoveryCreateOrLink,
|
|
426
448
|
qualityGateComplete,
|
|
449
|
+
requestCodeReview,
|
|
427
450
|
evaluatorSummary,
|
|
428
451
|
saveTemplateApp,
|
|
429
452
|
loadTemplateApp,
|
|
@@ -453,6 +476,7 @@ export function applyLifecycleVisibility(tools, urls) {
|
|
|
453
476
|
setVisible(tools.slackRecoveryMessage, !!urls.slackRecoveryMessage);
|
|
454
477
|
setVisible(tools.slackRecoveryCreateOrLink, !!urls.slackRecoveryCreateOrLink);
|
|
455
478
|
setVisible(tools.qualityGateComplete, !!urls.qualityGate);
|
|
479
|
+
setVisible(tools.requestCodeReview, !!urls.requestCodeReview);
|
|
456
480
|
setVisible(tools.evaluatorSummary, !!urls.evaluatorSummary);
|
|
457
481
|
setVisible(tools.saveTemplateApp, !!urls.saveTemplateApp);
|
|
458
482
|
setVisible(tools.loadTemplateApp, !!urls.loadTemplateApp);
|
|
@@ -1,21 +1,22 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* DX-3367 fix-up — shared scaffolding for a "one-shot session request"
|
|
3
|
-
* subcommand
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* This module is the ONE
|
|
8
|
-
*
|
|
3
|
+
* subcommand. Extracted when `plan-state` (`GET /api/plan-sessions/me/turn-state`,
|
|
4
|
+
* removed by DX-3689 — it never had a caller) and `background-work`
|
|
5
|
+
* (`PUT /api/plan-sessions/me/background-work`) turned out to duplicate this
|
|
6
|
+
* exact shape independently (`fetchTurnState` / `putBackgroundWork`,
|
|
7
|
+
* `runPlanStateCommand` / `runBackgroundWorkCommand`). This module is the ONE
|
|
8
|
+
* place that shape lives now; a future one-shot subcommand extends it
|
|
9
|
+
* instead of copying `background-work.ts` again.
|
|
9
10
|
*
|
|
10
|
-
* Two pieces
|
|
11
|
+
* Two pieces:
|
|
11
12
|
* - `sendSessionRequest` — the authenticated HTTP call itself: bearer +
|
|
12
13
|
* session header, a hard abort-timeout, and network-level failure
|
|
13
14
|
* classification (`timeout` / `request_failed`). Returns the raw
|
|
14
15
|
* `{status, text}` UNPARSED — each caller decides for itself which
|
|
15
|
-
* statuses need the body inspected
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
16
|
+
* statuses need the body inspected and which don't, so a non-2xx status
|
|
17
|
+
* with an unparseable (e.g. HTML) body is classified from the status
|
|
18
|
+
* alone rather than colliding with "the JSON didn't parse"
|
|
19
|
+
* (`bad_response`).
|
|
19
20
|
* - `resolveSessionOptions` — session-id + credential resolution via
|
|
20
21
|
* `resolveBridgeOptions`, with `BridgeStartError` narrowed by
|
|
21
22
|
* `instanceof` rather than an unchecked `as BridgeStartError` cast, so
|
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.179",
|
|
4
4
|
"description": "Stdio MCP server fronting danxbot's dashboard API for agents (operator sessions and dispatched workers alike): the generic danxbot_api/danxbot_api_list/danxbot_api_spec tools reach any route the caller's credential may call, plus dedicated agent-specific tools (plan_connect, danxbot_complete, and other per-dispatch lifecycle callbacks) (DX-3465, PLN-11 R-25).",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
package/dist/plan-state.js
DELETED
|
@@ -1,174 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* `danx-dashboard-mcp plan-state` — the ONE-SHOT client of
|
|
3
|
-
* `GET /api/plan-sessions/me/turn-state` (DX-2921).
|
|
4
|
-
*
|
|
5
|
-
* WHO RUNS IT. The danxbot Claude Code plugin's DX-2888 turn-start /
|
|
6
|
-
* turn-end hooks, via the plugin's existing `bridgeCommand()` npx path — the
|
|
7
|
-
* SAME package pin, no separate install, no deep import of this package's
|
|
8
|
-
* `dist/` internals (that was DX-2921's whole reason to exist: the DX-2888
|
|
9
|
-
* architecture review found the first build of this logic vendored a private
|
|
10
|
-
* copy of the MCP package and hardcoded the dashboard HTTP contract inside
|
|
11
|
-
* the PLUGIN). The plugin decides what to DO with the answer (inject plan
|
|
12
|
-
* numbers at turn start, refuse to let a turn end while the session could
|
|
13
|
-
* legally start work and has nothing running); this module only fetches it.
|
|
14
|
-
*
|
|
15
|
-
* THE CONTRACT (agreed with the DX-2888 plugin side, issue comment 4245 on
|
|
16
|
-
* DX-2921 — binding, because the plugin is already built against it):
|
|
17
|
-
* - print EXACTLY ONE JSON line on stdout, and ALWAYS exit 0 — success or
|
|
18
|
-
* failure alike, because the hook must stay silent rather than spam the
|
|
19
|
-
* session's own turn with a stack trace;
|
|
20
|
-
* - a session that is not connected to a plan prints
|
|
21
|
-
* `{"ok":false,"reason":"session_not_connected"}` — that EXACT literal,
|
|
22
|
-
* because the plugin keys its no-log-spam rule on it (it is also the
|
|
23
|
-
* dashboard's own 409 error code for this case — see
|
|
24
|
-
* `requireConnectedPlanId`, `plan-session-context.ts`);
|
|
25
|
-
* - on success, every count is a finite number, `plan` carries `id` +
|
|
26
|
-
* `name`, and every `startable` / `held` entry carries a string `id` +
|
|
27
|
-
* `title` — the plugin rejects anything else as `bad_response`, so this
|
|
28
|
-
* module validates the server's response BEFORE printing it, rather than
|
|
29
|
-
* trusting a 200 status to mean a well-shaped body;
|
|
30
|
-
* - human-readable diagnostics go to stderr only, never stdout.
|
|
31
|
-
*
|
|
32
|
-
* THE CREDENTIAL — the SAME resolver `bridge` uses (DX-2862 / DX-2921 AC),
|
|
33
|
-
* via `resolveBridgeOptions` (`bridge.ts`): the dashboard URL, credential
|
|
34
|
-
* source and credential all come from the connection record this session's
|
|
35
|
-
* OWN danx-dashboard MCP server wrote on its last successful `plan_connect`
|
|
36
|
-
* (`session-connection.ts`), never from this process's own ambient env. That
|
|
37
|
-
* is deliberate here for the identical reason it is deliberate for `bridge`:
|
|
38
|
-
* a hook process trusting whatever `DANXBOT_DISPATCH_TOKEN` happens to be in
|
|
39
|
-
* its environment is precisely the DX-2862 bug (a hook's ambient credential
|
|
40
|
-
* silently pointed at a different dashboard from the one the session's tools
|
|
41
|
-
* actually use).
|
|
42
|
-
*
|
|
43
|
-
* A HARD TIMEOUT (AC 30672) — `PLAN_STATE_REQUEST_TIMEOUT_MS`. This call
|
|
44
|
-
* blocks a turn boundary the plugin is holding open, so it must never hang:
|
|
45
|
-
* a slow or wedged dashboard is `{ok:false, reason:"timeout"}`, not a stuck
|
|
46
|
-
* hook.
|
|
47
|
-
*
|
|
48
|
-
* THE REQUEST/RUN SCAFFOLDING IS SHARED WITH `background-work.ts`
|
|
49
|
-
* (`one-shot-session-request.ts`, DX-3367 fix-up).
|
|
50
|
-
*/
|
|
51
|
-
import { resolveSessionOptions, sendSessionRequest, tryParseJsonBody } from "./one-shot-session-request.js";
|
|
52
|
-
export const PLAN_STATE_SUBCOMMAND = "plan-state";
|
|
53
|
-
export const TURN_STATE_PATH = "/api/plan-sessions/me/turn-state";
|
|
54
|
-
/** How long this one-shot call may take before it counts as a timeout. */
|
|
55
|
-
export const PLAN_STATE_REQUEST_TIMEOUT_MS = 5_000;
|
|
56
|
-
const USAGE = `usage: CLAUDE_CODE_SESSION_ID=<session-id> danx-dashboard-mcp ${PLAN_STATE_SUBCOMMAND}`;
|
|
57
|
-
function errorCodeOf(body) {
|
|
58
|
-
return typeof body === "object" && body !== null && typeof body.error === "string"
|
|
59
|
-
? body.error
|
|
60
|
-
: null;
|
|
61
|
-
}
|
|
62
|
-
function isFiniteNumber(v) {
|
|
63
|
-
return typeof v === "number" && Number.isFinite(v);
|
|
64
|
-
}
|
|
65
|
-
function isCardRef(v) {
|
|
66
|
-
return (typeof v === "object" &&
|
|
67
|
-
v !== null &&
|
|
68
|
-
typeof v.id === "string" &&
|
|
69
|
-
typeof v.title === "string");
|
|
70
|
-
}
|
|
71
|
-
/**
|
|
72
|
-
* The strict shape check the contract pins (comment 4245): finite counts, a
|
|
73
|
-
* `plan` with `id` + `name`, and a string `id` + `title` on every
|
|
74
|
-
* `startable` / `held` entry. `null` on anything short of that — a 200 whose
|
|
75
|
-
* body does not match is `bad_response`, exactly like a non-2xx.
|
|
76
|
-
*/
|
|
77
|
-
export function parseTurnStateBody(body) {
|
|
78
|
-
if (typeof body !== "object" || body === null)
|
|
79
|
-
return null;
|
|
80
|
-
const b = body;
|
|
81
|
-
const plan = b.plan;
|
|
82
|
-
if (typeof plan !== "object" || plan === null)
|
|
83
|
-
return null;
|
|
84
|
-
const p = plan;
|
|
85
|
-
if (!isFiniteNumber(p.id) || typeof p.name !== "string")
|
|
86
|
-
return null;
|
|
87
|
-
const counts = b.counts;
|
|
88
|
-
if (typeof counts !== "object" || counts === null)
|
|
89
|
-
return null;
|
|
90
|
-
const c = counts;
|
|
91
|
-
if (!isFiniteNumber(c.open) || !isFiniteNumber(c.inProgress) || !isFiniteNumber(c.needsYou))
|
|
92
|
-
return null;
|
|
93
|
-
if (!Array.isArray(b.startable) || !b.startable.every(isCardRef))
|
|
94
|
-
return null;
|
|
95
|
-
if (!Array.isArray(b.held) || !b.held.every(isCardRef))
|
|
96
|
-
return null;
|
|
97
|
-
return {
|
|
98
|
-
plan: { id: p.id, name: p.name },
|
|
99
|
-
counts: { open: c.open, inProgress: c.inProgress, needsYou: c.needsYou },
|
|
100
|
-
startable: b.startable,
|
|
101
|
-
held: b.held,
|
|
102
|
-
};
|
|
103
|
-
}
|
|
104
|
-
/**
|
|
105
|
-
* One authenticated GET, with a hard timeout, classified into the output
|
|
106
|
-
* shape this subcommand always prints — never a throw, never a non-2xx
|
|
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`.
|
|
115
|
-
*/
|
|
116
|
-
export async function fetchTurnState(options, deps) {
|
|
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" };
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
if (status === 401 || status === 403) {
|
|
133
|
-
return { ok: false, reason: "unauthorized" };
|
|
134
|
-
}
|
|
135
|
-
if (status < 200 || status >= 300) {
|
|
136
|
-
return { ok: false, reason: "http_error" };
|
|
137
|
-
}
|
|
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) {
|
|
143
|
-
return { ok: false, reason: "bad_response" };
|
|
144
|
-
}
|
|
145
|
-
return { ok: true, ...turnState };
|
|
146
|
-
}
|
|
147
|
-
/** The bin's `plan-state` subcommand, wired to the real process. Always resolves to exit code 0. */
|
|
148
|
-
export async function runPlanStateCommand(argv, env = process.env,
|
|
149
|
-
/** Test seam: the home the session's connection record is read from. */
|
|
150
|
-
resolveFrom = {}) {
|
|
151
|
-
const write = (output) => {
|
|
152
|
-
process.stdout.write(`${JSON.stringify(output)}\n`);
|
|
153
|
-
};
|
|
154
|
-
if (argv.length !== 0) {
|
|
155
|
-
// A malformed invocation is still a "the hook must stay silent" failure,
|
|
156
|
-
// not a crash — same ok:false/exit-0 contract as every other failure
|
|
157
|
-
// path here, with the usage line reserved for a human reading stderr.
|
|
158
|
-
write({ ok: false, reason: "usage" });
|
|
159
|
-
process.stderr.write(`${USAGE}\n`);
|
|
160
|
-
return 0;
|
|
161
|
-
}
|
|
162
|
-
const resolved = await resolveSessionOptions(env, resolveFrom, USAGE);
|
|
163
|
-
if (!resolved.ok) {
|
|
164
|
-
write(resolved.output);
|
|
165
|
-
process.stderr.write(`${resolved.detail}\n`);
|
|
166
|
-
return 0;
|
|
167
|
-
}
|
|
168
|
-
const output = await fetchTurnState(resolved.options, {
|
|
169
|
-
fetch: (input, init) => fetch(input, init),
|
|
170
|
-
requestTimeoutMs: PLAN_STATE_REQUEST_TIMEOUT_MS,
|
|
171
|
-
});
|
|
172
|
-
write(output);
|
|
173
|
-
return 0;
|
|
174
|
-
}
|