@thehammer/danx-dashboard-mcp 0.1.283 → 0.1.285
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 +1 -1
- package/dist/design-build-child.js +17 -0
- package/dist/design-build.js +95 -30
- package/dist/design-publish.js +4 -0
- package/dist/index.js +6 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -50,7 +50,7 @@ All exposed as `mcp__danx-dashboard__<name>` once wired through the workspace `m
|
|
|
50
50
|
| `danxbot_api_list` | No args. Lists the routes this caller's credential may reach as `{routes: ["METHOD /path"]}` (one bare string per route, exactly what `danxbot_api_spec` takes; a dispatch's own callbacks are in it) — call this first when you don't know the route for what you need to do. |
|
|
51
51
|
| `danxbot_api_spec` | `{endpoints}` (comma-joined `METHOD /path` list, required, min 1). Returns the exact OpenAPI contract for those routes — call this when you know the route but not its exact request/response shape. |
|
|
52
52
|
| `attach_file` | `{card_id, path, replaces?, primary?, filename?, content_type?}` — the server reads the LOCAL file at `path` (absolute, `/tmp/a.png` or `C:\x\a.png`) and uploads it, so its bytes never pass through the agent's context. `replaces: <attachment id>` swaps that attachment's file in place (`PUT /api/issues/:id/attachments/:aid`: same id, name and cover flag) instead of adding one; `primary: true` makes it the card's single cover, atomically with the write. Reply: `{issue: <compact row>, attachment_id, url, thumbnail_url, is_primary}`. Shown to a key holding `cards.edit` (DX-4523); a dispatch profile still receives it only when its catalog selection names it (`slack-agent` and `work`, DX-4386, through `extraTools`). A screenshot of browser-visible work: first `primary: true`, then `replaces` at each milestone. |
|
|
53
|
-
| `design_publish` | `{target, dir, card_id?, board?, title?, summary?, description?}` — builds a design with the TARGET repo's own Vite (resolved from the target's root at run time: this package depends on neither `vite` nor `playwright`) and publishes it as a revision of a Design card (DX-4077). `target` names one of the repo's design targets, read from the dashboard (`GET /api/repos/<repo>`, edited in Settings > Board > General), never from a file; `dir` holds `Design.tsx` under the target's root. The repo is the board's `repo_name` (`GET /api/boards/<board>`). The build runs in `<root>/.danx-design/<build-id>/` (removed afterwards), the page is written to `<tmpdir>/danx-design/<build-id>.html` before the upload, and a 1280x800 screenshot is taken when Playwright resolves from the target or this server's cwd (`screenshot: "none: <reason>"` otherwise, never a failed publish). Without `card_id` it creates the Design card first; a refused revision comes back verbatim with that `card_id`. Shown to a key holding `cards.edit`; opt-in, so a dispatch profile receives it only when its catalog selection names it (none does yet). |
|
|
53
|
+
| `design_publish` | `{target, dir, card_id?, board?, title?, summary?, description?}` — builds a design with the TARGET repo's own Vite (resolved from the target's root at run time: this package depends on neither `vite` nor `playwright`) and publishes it as a revision of a Design card (DX-4077). `target` names one of the repo's design targets, read from the dashboard (`GET /api/repos/<repo>`, edited in Settings > Board > General), never from a file; `dir` holds `Design.tsx` under the target's root. The repo is the board's `repo_name` (`GET /api/boards/<board>`). The build runs in `<root>/.danx-design/<build-id>/` (removed afterwards), in a short-lived `node` child process that exits when the build ends, so the server never holds the design's worktree files open and no esbuild outlives the publish (DX-4727), the page is written to `<tmpdir>/danx-design/<build-id>.html` before the upload, and a 1280x800 screenshot is taken when Playwright resolves from the target or this server's cwd (`screenshot: "none: <reason>"` otherwise, never a failed publish). Without `card_id` it creates the Design card first; a refused revision comes back verbatim with that `card_id`. Shown to a key holding `cards.edit`; opt-in, so a dispatch profile receives it only when its catalog selection names it (none does yet). |
|
|
54
54
|
| `request_permission` | `{permissions, reason}` — asks a person to grant this key ONE MORE permission (see "Asking for one more permission" above). Answers at once with the approval URL and confirm code; the grant is picked up in the background. |
|
|
55
55
|
|
|
56
56
|
There is no dedicated `resource_fields` tool (removed DX-3465 — it was a thin wrapper `danxbot_api` already reaches with no agent-specific behavior earning it its own tool). What an opt-in field TREE may name for one resource (`{resource, description, always, hashes, fields, relations}`) is `danxbot_api({method:"GET", path:"/api/resources/<resource>/fields"})` — install-global, no `board`; `resource: "issue"` is the root, and a relation's own `resource` in the response is what to call this again with, one level deeper.
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DX-4727 - the short-lived process `buildDesign` runs the target's Vite build in. Its one argument is a JSON `ViteBuildInput`.
|
|
3
|
+
* Everything Vite loads (native modules, the esbuild service) belongs to this process and is released when it exits, so the
|
|
4
|
+
* long-lived MCP server never holds a file of the design's worktree. A failure writes its message to stderr and exits 1; the
|
|
5
|
+
* parent turns that text into the tool's error.
|
|
6
|
+
*/
|
|
7
|
+
import { runViteBuild } from "./design-build.js";
|
|
8
|
+
try {
|
|
9
|
+
await runViteBuild(JSON.parse(process.argv[2]));
|
|
10
|
+
// An explicit exit: Vite's esbuild service would otherwise keep the loop alive until it is stopped. The empty write's callback
|
|
11
|
+
// runs once earlier stderr output (Vite's warnings) has been flushed, so the exit cannot cut it off.
|
|
12
|
+
process.stderr.write("", () => process.exit(0));
|
|
13
|
+
}
|
|
14
|
+
catch (error) {
|
|
15
|
+
// Vite's build() rethrows without logging (only its CLI logs), so the message is written once, here.
|
|
16
|
+
process.stderr.write(error instanceof Error ? error.message : String(error), () => process.exit(1));
|
|
17
|
+
}
|
package/dist/design-build.js
CHANGED
|
@@ -9,12 +9,13 @@
|
|
|
9
9
|
* This module carries no validator for the document it produces: that the document is self-contained is checked by exactly ONE
|
|
10
10
|
* validator, the dashboard's own parse5 check on the revision route, whose 400 comes back to the caller verbatim.
|
|
11
11
|
*/
|
|
12
|
+
import { spawn } from "node:child_process";
|
|
12
13
|
import { randomBytes } from "node:crypto";
|
|
13
14
|
import { mkdir, readdir, readFile, rm, rmdir, writeFile } from "node:fs/promises";
|
|
14
15
|
import { createRequire } from "node:module";
|
|
15
16
|
import os from "node:os";
|
|
16
17
|
import path from "node:path";
|
|
17
|
-
import { pathToFileURL } from "node:url";
|
|
18
|
+
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
18
19
|
import { isStrictlyInside, repoRelative } from "./design-checkout.js";
|
|
19
20
|
/** The directory (inside the target root) every build gets its own subdirectory of; the target's `.gitignore` names it. */
|
|
20
21
|
export const BUILD_ROOT_DIR = ".danx-design";
|
|
@@ -187,9 +188,97 @@ export function assertConfigContained(merged, rootAbs, buildDir, indexHtml) {
|
|
|
187
188
|
}
|
|
188
189
|
}
|
|
189
190
|
/**
|
|
190
|
-
*
|
|
191
|
-
*
|
|
192
|
-
*
|
|
191
|
+
* Run the target's Vite build over the generated files: load its config, merge the overrides, guard, build. This runs ONLY inside the
|
|
192
|
+
* short-lived build child (`design-build-child.ts`), never in the MCP server: Vite loads native modules (rollup, lightningcss,
|
|
193
|
+
* @tailwindcss/oxide) and starts an esbuild service, and in the long-lived server those stayed locked / running after the publish and
|
|
194
|
+
* stranded the design's worktree on Windows (DX-4727).
|
|
195
|
+
*/
|
|
196
|
+
export async function runViteBuild(input) {
|
|
197
|
+
const { rootAbs, buildDir, indexHtml } = input;
|
|
198
|
+
const vite = await loadTargetVite(rootAbs);
|
|
199
|
+
// "warn": Vite logs info to stdout; warnings and errors go to stderr, which the parent relays.
|
|
200
|
+
const loaded = await vite.loadConfigFromFile({ command: "build", mode: "production" }, input.viteConfigAbs, rootAbs, "warn");
|
|
201
|
+
if (loaded === null)
|
|
202
|
+
throw new Error(`design_publish: Vite found no config at ${input.viteConfigAbs}`);
|
|
203
|
+
const merged = vite.mergeConfig(loaded.config, {
|
|
204
|
+
// The repo's config is already loaded: the build must not read it a second time.
|
|
205
|
+
configFile: false,
|
|
206
|
+
root: rootAbs,
|
|
207
|
+
base: "./",
|
|
208
|
+
logLevel: "warn",
|
|
209
|
+
cacheDir: path.join(buildDir, VITE_CACHE_DIR),
|
|
210
|
+
build: {
|
|
211
|
+
outDir: path.join(buildDir, OUT_DIR),
|
|
212
|
+
assetsInlineLimit: () => true,
|
|
213
|
+
cssCodeSplit: false,
|
|
214
|
+
modulePreload: false,
|
|
215
|
+
emptyOutDir: false,
|
|
216
|
+
reportCompressedSize: false,
|
|
217
|
+
rollupOptions: { input: indexHtml, output: { inlineDynamicImports: true } },
|
|
218
|
+
},
|
|
219
|
+
});
|
|
220
|
+
assertConfigContained(merged, rootAbs, buildDir, indexHtml);
|
|
221
|
+
await vite.build(merged);
|
|
222
|
+
}
|
|
223
|
+
/** The build child's entry: `design-build-child.js` once compiled, `.ts` when the package runs from source (tests, `npm run dev`). */
|
|
224
|
+
function childEntry() {
|
|
225
|
+
const here = fileURLToPath(import.meta.url);
|
|
226
|
+
const ext = path.extname(here);
|
|
227
|
+
const file = path.join(path.dirname(here), `design-build-child${ext}`);
|
|
228
|
+
// Source runs need tsx to load the .ts child; the compiled package has no such dependency.
|
|
229
|
+
return { file, execArgv: ext === ".ts" ? ["--import", pathToFileURL(createRequire(import.meta.url).resolve("tsx/esm")).href] : [] };
|
|
230
|
+
}
|
|
231
|
+
/** How long the build child may run before it is killed: a hung Vite must not hold the tool call (and the design's files) forever. */
|
|
232
|
+
export const BUILD_TIMEOUT_MS = 5 * 60 * 1000;
|
|
233
|
+
/**
|
|
234
|
+
* Run the Vite build in a fresh `node` child and wait for it to exit. When the child exits, the OS releases every module it loaded
|
|
235
|
+
* and its esbuild service ends with it, so nothing of the build outlives the publish (DX-4727). A failed build rejects with everything
|
|
236
|
+
* the child wrote to stdout and stderr, verbatim; a child still running after `timeoutMs` is killed and rejects with the output so far;
|
|
237
|
+
* warnings from a good build are relayed to this process's stderr.
|
|
238
|
+
*/
|
|
239
|
+
export async function runViteBuildInChild(input, timeoutMs = BUILD_TIMEOUT_MS) {
|
|
240
|
+
const { file, execArgv } = childEntry();
|
|
241
|
+
const done = await new Promise((resolve, reject) => {
|
|
242
|
+
const child = spawn(process.execPath, [...execArgv, file, JSON.stringify(input)], {
|
|
243
|
+
cwd: input.rootAbs,
|
|
244
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
245
|
+
windowsHide: true,
|
|
246
|
+
});
|
|
247
|
+
let text = "";
|
|
248
|
+
let timedOut = false;
|
|
249
|
+
for (const stream of [child.stdout, child.stderr]) {
|
|
250
|
+
stream.setEncoding("utf8");
|
|
251
|
+
stream.on("data", (chunk) => void (text += chunk));
|
|
252
|
+
}
|
|
253
|
+
const timer = setTimeout(() => {
|
|
254
|
+
timedOut = true;
|
|
255
|
+
child.kill();
|
|
256
|
+
}, timeoutMs);
|
|
257
|
+
child.once("error", (err) => {
|
|
258
|
+
clearTimeout(timer);
|
|
259
|
+
reject(err);
|
|
260
|
+
});
|
|
261
|
+
child.once("close", (code, signal) => {
|
|
262
|
+
clearTimeout(timer);
|
|
263
|
+
resolve({ code, signal, text, timedOut });
|
|
264
|
+
});
|
|
265
|
+
});
|
|
266
|
+
const output = done.text.trim();
|
|
267
|
+
if (done.timedOut) {
|
|
268
|
+
throw new Error(`design_publish: the build did not finish within ${Math.round(timeoutMs / 1000)}s and was killed${output === "" ? "" : `; its output so far:
|
|
269
|
+
${output}`}`);
|
|
270
|
+
}
|
|
271
|
+
if (done.code !== 0) {
|
|
272
|
+
const ended = done.signal !== null ? `was killed by ${done.signal}` : `exited with code ${String(done.code)}`;
|
|
273
|
+
throw new Error(output || `design_publish: the build process ${ended} and wrote nothing`);
|
|
274
|
+
}
|
|
275
|
+
if (done.text !== "")
|
|
276
|
+
process.stderr.write(done.text);
|
|
277
|
+
}
|
|
278
|
+
/**
|
|
279
|
+
* Build the design: generate the three files in a fresh `<root>/.danx-design/<build-id>/`, run the target's Vite over them in a
|
|
280
|
+
* child process, read the output, fold it into one document. The build dir is removed in a `finally`; a failed cleanup never
|
|
281
|
+
* replaces the error that came first.
|
|
193
282
|
*/
|
|
194
283
|
export async function buildDesign(input) {
|
|
195
284
|
const buildDir = path.join(input.rootAbs, BUILD_ROOT_DIR, input.buildId);
|
|
@@ -201,32 +290,8 @@ export async function buildDesign(input) {
|
|
|
201
290
|
await writeFile(indexHtml, files.indexHtml, "utf8");
|
|
202
291
|
await writeFile(path.join(buildDir, ENTRY_FILE), files.entry, "utf8");
|
|
203
292
|
await writeFile(path.join(buildDir, CSS_FILE), files.css, "utf8");
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
const loaded = await vite.loadConfigFromFile({ command: "build", mode: "production" }, input.viteConfigAbs, input.rootAbs, "warn");
|
|
207
|
-
if (loaded === null)
|
|
208
|
-
throw new Error(`design_publish: Vite found no config at ${input.viteConfigAbs}`);
|
|
209
|
-
const outDir = path.join(buildDir, OUT_DIR);
|
|
210
|
-
const merged = vite.mergeConfig(loaded.config, {
|
|
211
|
-
// The repo's config is already loaded: the build must not read it a second time.
|
|
212
|
-
configFile: false,
|
|
213
|
-
root: input.rootAbs,
|
|
214
|
-
base: "./",
|
|
215
|
-
logLevel: "warn",
|
|
216
|
-
cacheDir: path.join(buildDir, VITE_CACHE_DIR),
|
|
217
|
-
build: {
|
|
218
|
-
outDir,
|
|
219
|
-
assetsInlineLimit: () => true,
|
|
220
|
-
cssCodeSplit: false,
|
|
221
|
-
modulePreload: false,
|
|
222
|
-
emptyOutDir: false,
|
|
223
|
-
reportCompressedSize: false,
|
|
224
|
-
rollupOptions: { input: indexHtml, output: { inlineDynamicImports: true } },
|
|
225
|
-
},
|
|
226
|
-
});
|
|
227
|
-
assertConfigContained(merged, input.rootAbs, buildDir, indexHtml);
|
|
228
|
-
await vite.build(merged);
|
|
229
|
-
const built = await readBuildOutput(outDir);
|
|
293
|
+
await runViteBuildInChild({ rootAbs: input.rootAbs, viteConfigAbs: input.viteConfigAbs, buildDir, indexHtml });
|
|
294
|
+
const built = await readBuildOutput(path.join(buildDir, OUT_DIR));
|
|
230
295
|
return { html: inlineAssets(built.html.text, built.js, built.css), buildId: input.buildId };
|
|
231
296
|
}
|
|
232
297
|
catch (error) {
|
package/dist/design-publish.js
CHANGED
|
@@ -48,6 +48,9 @@ async function resolveBoard(client, args, deps) {
|
|
|
48
48
|
return { ok: true, board };
|
|
49
49
|
}
|
|
50
50
|
export async function designPublish(client, args, deps) {
|
|
51
|
+
// DX-4729: refused before any call, so a blank note never leaves a built page, a screenshot or a card behind.
|
|
52
|
+
if (args.note.trim() === "")
|
|
53
|
+
fail("`note` is required: say what changed since the last revision, which decision note it answers and what is still open");
|
|
51
54
|
// With `card_id` the card already exists: its title, summary and description are not this call's to set, and silently ignoring them would hide the mistake.
|
|
52
55
|
if (args.card_id !== undefined) {
|
|
53
56
|
const given = ["title", "summary", "description"].filter((k) => args[k] !== undefined);
|
|
@@ -103,6 +106,7 @@ export async function designPublish(client, args, deps) {
|
|
|
103
106
|
method: "POST",
|
|
104
107
|
path: `/api/issues/${encodeURIComponent(cardId)}/design/revisions`,
|
|
105
108
|
body: {
|
|
109
|
+
note: args.note,
|
|
106
110
|
target: target.name,
|
|
107
111
|
entry: repoRelative(repoRoot, path.join(checked.dirAbs, DESIGN_FILE)),
|
|
108
112
|
source_files: sourceFiles,
|
package/dist/index.js
CHANGED
|
@@ -650,8 +650,12 @@ strictTool("design_publish", "Build a design against this repo's REAL components
|
|
|
650
650
|
"`captured` (1280x800) or `none: <reason>` (no Playwright in the repo: the publish still succeeds). Build errors and a " +
|
|
651
651
|
"refused revision (400, 413) come back verbatim; a refusal after the card was created carries its `card_id`: fix the " +
|
|
652
652
|
"design and call again with it. Limits: the frame has no network, so a component that fetches its own data fails inside " +
|
|
653
|
-
"it (use fixture props); React only; targets are
|
|
654
|
-
target: z.string().min(1).describe("A design target's name, as
|
|
653
|
+
"it (use fixture props); React only; targets are a dashboard setting, read from `design_targets` of `GET /api/repos/<repo>`, never a file.", {
|
|
654
|
+
target: z.string().min(1).describe("A design target's name, as listed in `design_targets` of `GET /api/repos/<repo>` (`danxbot_api`, fields {design_targets: true})."),
|
|
655
|
+
note: z
|
|
656
|
+
.string()
|
|
657
|
+
.min(1)
|
|
658
|
+
.describe("The revision note, shown beside the revision on the card: what changed since the last revision, which decision note it answers, what is still open."),
|
|
655
659
|
dir: z
|
|
656
660
|
.string()
|
|
657
661
|
.min(1)
|
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.285",
|
|
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, request_permission, attach_file, and danxbot_slack_reply) (DX-3465, PLN-11 R-25).",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|